diff --git a/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql new file mode 100644 index 0000000000000..9ef54e614f257 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql @@ -0,0 +1,124 @@ +-- +-- Table structure for table `#__workflows` +-- + +CREATE TABLE IF NOT EXISTS `#__workflows` ( + `id` int(10) NOT NULL AUTO_INCREMENT, + `asset_id` int(10) DEFAULT 0, + `published` tinyint(1) NOT NULL DEFAULT 0, + `title` varchar(255) NOT NULL, + `description` text NOT NULL DEFAULT '', + `extension` varchar(255) NOT NULL, + `default` tinyint(1) NOT NULL DEFAULT 0, + `ordering` int(11) NOT NULL DEFAULT 0, + `created` datetime NOT NULL DEFAULT NOW(), + `created_by` int(10) NOT NULL DEFAULT 0, + `modified` datetime NOT NULL DEFAULT NOW(), + `modified_by` int(10) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `asset_id` (`asset_id`), + KEY `title` (`title`(191)), + KEY `extension` (`extension`(191)), + KEY `default` (`default`), + KEY `created` (`created`), + KEY `created_by` (`created_by`), + KEY `modified` (`modified`), + KEY `modified_by` (`modified_by`) +) ENGINE=InnoDB COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `#__workflows` +-- + +INSERT INTO `#__workflows` (`id`, `asset_id`, `published`, `title`, `description`, `extension`, `default`, `ordering`, `created`, `created_by`, `modified`, `modified_by`) VALUES +(1, 0, 1, 'Joomla! Default', '', 'com_content', 1, 1, NOW(), 0, '0000-00-00 00:00:00', 0); + +-- +-- Table structure for table `#__workflow_associations` +-- + +CREATE TABLE IF NOT EXISTS `#__workflow_associations` ( + `item_id` int(10) NOT NULL DEFAULT 0 COMMENT 'Extension table id value', + `state_id` int(10) NOT NULL COMMENT 'Foreign Key to #__workflow_states.id', + `extension` varchar(100) NOT NULL, + PRIMARY KEY (`item_id`, `state_id`, `extension`), + KEY `idx_item_id` (`item_id`), + KEY `idx_state_id` (`state_id`), + KEY `idx_extension` (`extension`(100)) +) ENGINE=InnoDB COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `#__workflow_states` +-- + +CREATE TABLE IF NOT EXISTS `#__workflow_states` ( + `id` int(10) NOT NULL AUTO_INCREMENT, + `asset_id` int(10) DEFAULT 0, + `ordering` int(11) NOT NULL DEFAULT 0, + `workflow_id` int(10) NOT NULL, + `published` tinyint(1) NOT NULL DEFAULT 0, + `title` varchar(255) NOT NULL, + `description` text NOT NULL DEFAULT '', + `condition` enum('0','1','-2') NOT NULL, + `default` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `workflow_id` (`workflow_id`), + KEY `title` (`title`(191)), + KEY `asset_id` (`asset_id`), + KEY `default` (`default`) +) ENGINE=InnoDB DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `#__workflow_states` +-- + +INSERT INTO `#__workflow_states` (`id`, `asset_id`, `ordering`, `workflow_id`, `published`, `title`, `description`, `condition`, `default`) VALUES +(1, 0, 1, 1, 1, 'Unpublished', '', '0', 0), +(2, 0, 2, 1, 1, 'Published', '', '1', 1), +(3, 0, 3, 1, 1, 'Trashed', '', '-2', 0), +(4, 0, 4, 1, 1, 'Archived', '', '1', 0); + +-- +-- Table structure for table `#__workflow_transitions` +-- + +CREATE TABLE IF NOT EXISTS `#__workflow_transitions` ( + `id` int(10) NOT NULL AUTO_INCREMENT, + `asset_id` int(10) DEFAULT 0, + `ordering` int(11) NOT NULL DEFAULT 0, + `workflow_id` int(10) NOT NULL, + `published` tinyint(1) NOT NULL DEFAULT 0, + `title` varchar(255) NOT NULL, + `description` text NOT NULL DEFAULT '', + `from_state_id` int(10) NOT NULL, + `to_state_id` int(10) NOT NULL, + PRIMARY KEY (`id`), + KEY `title` (`title`(191)), + KEY `asset_id` (`asset_id`), + KEY `from_state_id` (`from_state_id`), + KEY `to_state_id` (`to_state_id`), + KEY `workflow_id` (`workflow_id`) +) ENGINE=InnoDB DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `#__workflow_transitions` +-- + +INSERT INTO `#__workflow_transitions` (`id`, `asset_id`, `published`, `ordering`, `workflow_id`, `title`, `description`, `from_state_id`, `to_state_id`) VALUES +(1, 0, 1, 1, 1, 'Unpublish', '', -1, 1), +(2, 0, 1, 2, 1, 'Publish', '', -1, 2), +(3, 0, 1, 3, 1, 'Trash', '', -1, 3), +(4, 0, 1, 4, 1, 'Archive', '', -1, 4); + +-- +-- Creating extension entry +-- + +INSERT INTO `#__extensions` (`extension_id`, `package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `checked_out`, `checked_out_time`, `ordering`, `state`, `namespace`) VALUES +(35, 0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0, 'Joomla\\Component\\Workflow'); + +-- +-- Creating Associations for existing content +-- +INSERT INTO `#__workflow_associations` (`item_id`, `state_id`, `extension`) +SELECT `id`, CASE WHEN `state` = -2 THEN 3 WHEN `state` = 0 THEN 1 WHEN `state` = 2 THEN 4 ELSE 2 END, 'com_content' FROM `#__content`; diff --git a/administrator/components/com_categories/Model/CategoryModel.php b/administrator/components/com_categories/Model/CategoryModel.php index d1cad320131c8..68e839b205064 100644 --- a/administrator/components/com_categories/Model/CategoryModel.php +++ b/administrator/components/com_categories/Model/CategoryModel.php @@ -422,30 +422,39 @@ protected function preprocessForm(\JForm $form, $data, $group = 'content') } } - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); + $componentInterface = Factory::getApplication()->bootComponent($component); - if (file_exists($path)) + if ($componentInterface instanceof CategoriesServiceInterface) { - $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; - - \JLoader::register($cName, $path); + $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 (class_exists($cName) && is_callable(array($cName, 'onPrepareForm'))) + if (file_exists($path)) { - $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()); + $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; - return false; + \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; + } } } } diff --git a/administrator/components/com_content/Extension/ContentComponent.php b/administrator/components/com_content/Extension/ContentComponent.php index 01cf8e5a0388d..7d9401a68c13f 100644 --- a/administrator/components/com_content/Extension/ContentComponent.php +++ b/administrator/components/com_content/Extension/ContentComponent.php @@ -19,7 +19,12 @@ use Joomla\CMS\Extension\BootableExtensionInterface; use Joomla\CMS\Extension\MVCComponent; use Joomla\CMS\Fields\FieldsServiceInterface; +use Joomla\CMS\Form\Form; use Joomla\CMS\HTML\HTMLRegistryAwareTrait; +use Joomla\CMS\MVC\Factory\MVCFactoryServiceTrait; +use Joomla\CMS\MVC\Factory\MVCFactoryServiceInterface; +use Joomla\CMS\Workflow\WorkflowServiceInterface; +use Joomla\Component\Content\Administrator\Helper\ContentHelper; use Joomla\Component\Content\Administrator\Service\HTML\AdministratorService; use Joomla\Component\Content\Administrator\Service\HTML\Icon; use Psr\Container\ContainerInterface; @@ -29,10 +34,11 @@ /** * Component class for com_content * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ class ContentComponent extends MVCComponent implements - BootableExtensionInterface, CategoriesServiceInterface, FieldsServiceInterface, AssociationServiceInterface + BootableExtensionInterface, MVCFactoryServiceInterface, CategoriesServiceInterface, FieldsServiceInterface, + AssociationServiceInterface, WorkflowServiceInterface { use CategoriesServiceTrait; use AssociationServiceTrait; @@ -49,7 +55,7 @@ class ContentComponent extends MVCComponent implements * * @return void * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function boot(ContainerInterface $container) { @@ -69,7 +75,7 @@ public function boot(ContainerInterface $container) * * @return string|null The new section * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function validateSection($section, $item = null) { @@ -102,7 +108,7 @@ public function validateSection($section, $item = null) * * @return array * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ public function getContexts(): array { @@ -123,10 +129,68 @@ public function getContexts(): array * * @return string|null * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ protected function getTableNameForSection(string $section = null) { return '#__content'; } + + /** + * 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 __DEPLOY_VERSION__ + */ + public function filterTransitions($transitions, $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 __DEPLOY_VERSION__ + */ + public function countItems(array $items, string $section) + { + return ContentHelper::countItems($items); + } + + /** + * 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); + } + + /** + * Method to change state of multiple ids + * + * @param array $pks Array of IDs + * @param int $condition Condition of the workflow state + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public static function updateContentState($pks, $condition): bool + { + return ContentHelper::updateContentState($pks, $condition); + } } diff --git a/administrator/components/com_content/Helper/ContentHelper.php b/administrator/components/com_content/Helper/ContentHelper.php index d2dcd72851a77..d74b632ea415f 100644 --- a/administrator/components/com_content/Helper/ContentHelper.php +++ b/administrator/components/com_content/Helper/ContentHelper.php @@ -11,7 +11,12 @@ defined('_JEXEC') or die; +use Joomla\CMS\Factory; +use Joomla\CMS\Workflow\Workflow; +use Joomla\CMS\Form\Form; use Joomla\CMS\Language\Text; +use Joomla\CMS\Table\Category; +use Joomla\Registry\Registry; use Joomla\CMS\Component\ComponentHelper; /** @@ -21,6 +26,8 @@ */ class ContentHelper extends \Joomla\CMS\Helper\ContentHelper { + public static $extension = 'com_content'; + /** * Configure the Linkbar. * @@ -48,6 +55,33 @@ public static function addSubmenu($vName) $vName == 'featured' ); + if (ComponentHelper::isEnabled('com_workflow') && ComponentHelper::getParams('com_content')->get('workflows_enable', 1)) + { + \JHtmlSidebar::addEntry( + Text::_('COM_CONTENT_SUBMENU_WORKFLOWS'), + 'index.php?option=com_workflow&extension=com_content', + $vName == 'workflows' + ); + + if ($vName == 'states' || $vName == 'transitions') + { + $app = Factory::getApplication(); + $workflowID = $app->getUserStateFromRequest('filter.workflow_id', 'workflow_id', 1, 'int'); + + \JHtmlSidebar::addEntry( + Text::_('COM_WORKFLOW_STATES'), + 'index.php?option=com_workflow&view=states&workflow_id=' . $workflowID . "&extension=com_content", + $vName == 'states`' + ); + + \JHtmlSidebar::addEntry( + Text::_('COM_WORKFLOW_TRANSITIONS'), + 'index.php?option=com_workflow&view=transitions&workflow_id=' . $workflowID . "&extension=com_content", + $vName == 'transitions' + ); + } + } + if (ComponentHelper::isEnabled('com_fields') && ComponentHelper::getParams('com_content')->get('custom_fields_enable', '1')) { \JHtmlSidebar::addEntry( @@ -62,4 +96,387 @@ public static function addSubmenu($vName) ); } } + + /** + * Applies the content tag filters to arbitrary text as per settings for current user group + * + * @param text $text The string to filter + * + * @return string The filtered string + * + * @deprecated 4.0 Use \JComponentHelper::filterText() instead. + */ + public static function filterText($text) + { + try + { + \JLog::add( + sprintf('%s() is deprecated. Use JComponentHelper::filterText() instead', __METHOD__), + \JLog::WARNING, + 'deprecated' + ); + } + catch (\RuntimeException $exception) + { + // Informational log only + } + + return \JComponentHelper::filterText($text); + } + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] &$items The banner category objects + * + * @return \stdClass[] + * + * @since 3.5 + */ + public static function countItems(&$items) + { + $db = \JFactory::getDbo(); + + foreach ($items as $item) + { + $item->count_trashed = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + $query = $db->getQuery(true); + + $query ->select($db->quoteName('condition')) + ->select('COUNT(*) AS ' . $db->quoteName('count')) + ->from($db->quoteName('#__content', 'c')) + ->from($db->quoteName('#__workflow_states', 's')) + ->from($db->quoteName('#__workflow_associations', 'a')) + ->where($db->quoteName('a.item_id') . ' = ' . $db->quoteName('c.id')) + ->where($db->quoteName('s.id') . ' = ' . $db->quoteName('a.state_id')) + ->where('catid = ' . (int) $item->id) + ->where('a.extension = ' . $db->quote('com_content')) + ->group($db->quoteName('condition')); + + $articles = $db->setQuery($query)->loadObjectList(); + + foreach ($articles as $article) + { + if ($article->condition == Workflow::PUBLISHED) + { + $item->count_published = $article->count; + } + + if ($article->condition == Workflow::UNPUBLISHED) + { + $item->count_unpublished = $article->count; + } + + if ($article->condition == Workflow::TRASHED) + { + $item->count_trashed = $article->count; + } + } + } + + return $items; + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] &$items The content objects + * @param string $extension The name of the active view. + * + * @return \stdClass[] + * + * @since 3.6 + */ + public static function countTagItems(&$items, $extension) + { + $db = \JFactory::getDbo(); + $parts = explode('.', $extension); + $section = null; + + if (count($parts) > 1) + { + $section = $parts[1]; + } + + $join = $db->quoteName('#__content') . ' AS c ON ct.content_item_id=c.id'; + $state = 'state'; + + if ($section === 'category') + { + $join = $db->quoteName('#__categories') . ' AS c ON ct.content_item_id=c.id'; + $state = 'published as state'; + } + + foreach ($items as $item) + { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + $query = $db->getQuery(true); + $query->select($state . ', count(*) AS count') + ->from($db->quoteName('#__contentitem_tag_map') . 'AS ct ') + ->where('ct.tag_id = ' . (int) $item->id) + ->where('ct.type_alias =' . $db->q($extension)) + ->join('LEFT', $join) + ->group('state'); + $db->setQuery($query); + $contents = $db->loadObjectList(); + + foreach ($contents as $content) + { + if ($content->state == 1) + { + $item->count_published = $content->count; + } + + if ($content->state == 0) + { + $item->count_unpublished = $content->count; + } + + if ($content->state == 2) + { + $item->count_archived = $content->count; + } + + if ($content->state == -2) + { + $item->count_trashed = $content->count; + } + } + } + + return $items; + } + + /** + * Returns a valid section for articles. 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 + */ + public static function validateSection($section) + { + if (\JFactory::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 3.7.0 + */ + public static function getContexts() + { + \JFactory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => \JText::_('COM_CONTENT'), + 'com_content.categories' => \JText::_('JCATEGORY') + ); + + return $contexts; + } + + /** + * Check if state can be deleted + * + * @param int $stateID Id of state to delete + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public static function canDeleteState($stateID) + { + $db = \JFactory::getDbo(); + $query = $db->getQuery(true); + + $query->select('id') + ->from($db->quoteName('#__content')) + ->where('state = ' . (int) $stateID); + $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 $workflow_id Id of the workflow + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function filterTransitions($transitions, $pk, $workflow_id = 0): array + { + return array_values( + array_filter( + $transitions, + function ($var) use ($pk, $workflow_id) + { + return in_array($var['from_state_id'], [-1, $pk]) && $var['to_state_id'] != $pk && $workflow_id == $var['workflow_id']; + } + ) + ); + } + + /** + * Method to change state of multiple ids + * + * @param array $pks Array of IDs + * @param int $condition Condition of the workflow state + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public static function updateContentState($pks, $condition): bool + { + if (empty($pks)) + { + return false; + } + + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->update($db->quoteName('#__content')) + ->set($db->quoteName('state') . '=' . (int) $condition) + ->where($db->quoteName('id') . ' IN (' . implode(', ', $pks) . ')'); + + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + return false; + } + + return true; + } + + /** + * Prepares a form + * + * @param \Joomla\CMS\Categories\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; + + $form->setFieldAttribute('workflow_id', 'default', 'inherit'); + + $query = $db->getQuery(true); + + $query ->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('default') . ' = 1') + ->where($db->quoteName('published') . ' = 1'); + + $defaulttitle = $db->setQuery($query)->loadResult(); + + $option = Text::_('COM_CONTENT_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 vor inherit from parent + array_shift($categories); + + $option = Text::sprintf('COM_CONTENT_WORKFLOW_INHERIT_WORKFLOW', $defaulttitle); + + if (!empty($categories)) + { + $categories = array_reverse($categories); + + 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 ((int) $workflow_id > 0) + { + $query ->clear('where') + ->where($db->quoteName('id') . ' = ' . (int) $workflow_id) + ->where($db->quoteName('published') . ' = 1'); + + $title = $db->setQuery($query)->loadResult(); + + if (!is_null($title)) + { + $option = Text::sprintf('COM_CONTENT_WORKFLOW_INHERIT_WORKFLOW', $title); + + break; + } + } + } + } + } + + $field = $form->getField('workflow_id', 'params'); + + $field->addOption($option, ['value' => 'inherit']); + + $field->addOption(Text::sprintf('COM_CONTENT_WORKFLOW_DEFAULT_WORKFLOW', $defaulttitle), ['value' => 'use_default']); + + $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); + } } diff --git a/administrator/components/com_content/Model/ArticleModel.php b/administrator/components/com_content/Model/ArticleModel.php index fa18852b62a3c..72884a8291800 100644 --- a/administrator/components/com_content/Model/ArticleModel.php +++ b/administrator/components/com_content/Model/ArticleModel.php @@ -11,17 +11,25 @@ defined('_JEXEC') or die; -use Joomla\CMS\Dispatcher\DispatcherFactory; +use Joomla\CMS\Factory; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Categories\Categories; +use Joomla\CMS\Model\Form; +use Joomla\Component\Content\Administrator\Helper\ContentHelper; +use Joomla\Component\Workflow\Administrator\Helper\WorkflowHelper; +use Joomla\Component\Workflow\Administrator\Table\StateTable; use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; use Joomla\CMS\MVC\Model\AdminModel; +use Joomla\CMS\Table\Category; +use Joomla\CMS\Workflow\Workflow; +use Joomla\CMS\Dispatcher\DispatcherFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\String\PunycodeHelper; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\LanguageHelper; use Joomla\CMS\UCM\UCMType; -use Joomla\CMS\Factory; /** * Item Model for an Article. @@ -187,6 +195,50 @@ protected function batchCopy($value, $pks, $contexts) return $newIds; } + /** + * Batch change workflow state or current. + * + * @param integer $value The workflow state ID. + * @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 __DEPLOY_VERSION__ + */ + protected function batchWorkflowState($value, $pks, $contexts) + { + $user = Factory::getUser(); + + if (!$user->authorise('core.admin', 'com_content')) + { + $this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EXECUTE_TRANSITION')); + } + + // Get state information + $state = new StateTable($this->_db); + + if (empty($value) || !$state->load($value)) + { + Factory::getApplication()->enqueueMessage(\JText::sprintf('JGLOBAL_BATCH_WORKFLOW_STATE_ROW_NOT_FOUND'), 'error'); + + return false; + } + + if (empty($pks)) + { + Factory::getApplication()->enqueueMessage(\JText::sprintf('JGLOBAL_BATCH_WORKFLOW_STATE_ROW_NOT_FOUND'), 'error'); + + return false; + } + + $workflow = new Workflow(['extension' => 'com_content']); + + // Update content state value and workflow associations + return ContentHelper::updateContentState($pks, $state->condition) + && $workflow->updateAssociations($pks, $value); + } + /** * Batch move categories to a new category. * @@ -306,7 +358,13 @@ protected function canDelete($record) { if (!empty($record->id)) { - if ($record->state != -2) + $state = new StateTable($this->_db); + + $workflow = new Workflow(['extension' => 'com_content']); + + $assoc = $workflow->getAssociation($record->id); + + if (!$state->load($assoc->state_id) || $state->condition != Workflow::TRASHED) { return false; } @@ -441,7 +499,7 @@ public function getItem($pk = null) * @param array $data Data for the form. * @param boolean $loadData True if the form is to load its own data (default case), false if not. * - * @return \JForm|boolean A \JForm object on success, false on failure + * @return Form|boolean A \JForm object on success, false on failure * * @since 1.6 */ @@ -456,6 +514,8 @@ public function getForm($data = array(), $loadData = true) } $jinput = Factory::getApplication()->input; + $db = $this->getDbo(); + $query = $db->getQuery(true); /* * The front end calls this model and uses a_id to avoid id clashes so we need to check for that first. @@ -464,15 +524,25 @@ public function getForm($data = array(), $loadData = true) $id = $jinput->get('a_id', $jinput->get('id', 0)); // Determine correct permissions to check. - if ($this->getState('article.id')) + if ($id = $this->getState('article.id', $id)) { - $id = $this->getState('article.id'); - // Existing record. Can only edit in selected categories. $form->setFieldAttribute('catid', 'action', 'core.edit'); // Existing record. Can only edit own articles in selected categories. $form->setFieldAttribute('catid', 'action', 'core.edit.own'); + + $table = $this->getTable(); + + if ($table->load(array('id' => $id))) + { + $workflow = new Workflow(['extension' => 'com_content']); + + // Transition field + $assoc = $workflow->getAssociation($table->id); + + $form->setFieldAttribute('transition', 'workflow_state', (int) $assoc->state_id); + } } else { @@ -617,6 +687,8 @@ public function save($data) { $input = Factory::getApplication()->input; $filter = \JFilterInput::getInstance(); + $db = $this->getDbo(); + $user = Factory::getUser(); if (isset($data['metadata']) && isset($data['metadata']['author'])) { @@ -705,8 +777,68 @@ public function save($data) $data['alias'] = ''; } } + } + + $workflowId = 0; + $stateId = 0; + + // Set status depending on category + if (empty($data['id'])) + { + $workflow = $this->getWorkflowByCategory($data['catid']); + + if (empty($workflow->id)) + { + $this->setError(\JText::_('COM_CONTENT_WORKFLOW_NOT_FOUND')); + + return false; + } + + $stateId = (int) $workflow->state_id; + $workflowId = (int) $workflow->id; + + // B/C state + $data['state'] = (int) $workflow->condition; + + // No transition for new articles + if (isset($data['transition'])) + { + unset($data['transition']); + } + } + // Calculate new status depending on transition + elseif (!empty($data['transition'])) + { + // Check if the user is allowed to execute this transition + if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $data['transition'])) + { + $this->setError(JText::_('COM_CONTENT_WORKFLOW_TRANSITION_NOT_ALLOWED')); + + return false; + } + + // Set the new state + $query = $db->getQuery(true); + + $query ->select($db->quoteName(['ws.id', 'ws.condition'])) + ->from($db->quoteName('#__workflow_states', 'ws')) + ->from($db->quoteName('#__workflow_transitions', 'wt')) + ->where($db->quoteName('wt.to_state_id') . ' = ' . $db->quoteName('ws.id')) + ->where($db->quoteName('wt.id') . ' = ' . (int) $data['transition']) + ->where($db->quoteName('ws.published') . ' = 1') + ->where($db->quoteName('wt.published') . ' = 1'); + + $state = $db->setQuery($query)->loadObject(); + + if (empty($state->id)) + { + $this->setError(JText::_('COM_CONTENT_WORKFLOW_TRANSITION_NOT_ALLOWED')); + + return false; + } + + $data['state'] = (int) $state->condition; - $data['state'] = 0; } // Automatic handling of alias for empty fields @@ -740,6 +872,8 @@ public function save($data) } } + $workflow = new Workflow(['extension' => 'com_content']); + if (parent::save($data)) { if (isset($data['featured'])) @@ -747,6 +881,49 @@ public function save($data) $this->featured($this->getState($this->getName() . '.id'), $data['featured']); } + // Run the transition and update the workflow association + if (!empty($data['transition'])) + { + $this->runTransition((int) $this->getState($this->getName() . '.id'), (int) $data['transition']); + } + + // Let's check if we have workflow association (perhaps something went wrong before) + if (empty($stateId)) + { + $assoc = $workflow->getAssociation($this->getState($this->getName() . '.id')); + + // If not, reset the state and let's create the associations + if (empty($assoc->item_id)) + { + $table = $this->getTable(); + + $table->load((int) $this->getState($this->getName() . '.id')); + + $workflow = $this->getWorkflowByCategory($table->catid); + + if (empty($workflow->id)) + { + $this->setError(\JText::_('COM_CONTENT_WORKFLOW_NOT_FOUND')); + + return false; + } + + $stateId = (int) $workflow->state_id; + $workflowId = (int) $workflow->id; + + // B/C state + $table->state = $workflow->condition; + + $table->store(); + } + } + + // If we have a new state, create the workflow association + if (!empty($stateId)) + { + $workflow->createAssociation($this->getState($this->getName() . '.id'), (int) $stateId); + } + return true; } @@ -977,8 +1154,150 @@ public function delete(&$pks) ->where('content_id IN (' . implode(',', $pks) . ')'); $db->setQuery($query); $db->execute(); + + $workflow = new Workflow(['extension' => 'com_content']); + + $workflow->deleteAssociation($pks); } return $return; } + + /** + * Load the assigned workflow information by a given category ID + * + * @param int $catId The give category + * + * @return integer|boolean If found, the workflow ID, otherwise false + */ + protected function getWorkflowByCategory($catId) + { + $db = $this->getDbo(); + + // Search categories and parents (if requested) for a workflow + $category = new Category($db); + + $categories = array_reverse($category->getPath($catId)); + + $workflow_id = 0; + + foreach ($categories as $cat) + { + $cat->params = new Registry($cat->params); + + $workflow_id = $cat->params->get('workflow_id'); + + if ($workflow_id == 'inherit') + { + $workflow_id = 0; + + continue; + } + elseif ($workflow_id == 'use_default') + { + $workflow_id = 0; + + break; + } + elseif ($workflow_id > 0) + { + break; + } + } + + // Check if the workflow exists + if ($workflow_id > 0) + { + $query = $db->getQuery(true); + + $query ->select( + $db->quoteName( + [ + 'w.id', + 'ws.condition' + ] + ) + ) + ->select($db->quoteName('ws.id', 'state_id')) + ->from($db->quoteName('#__workflow_states', 'ws')) + ->from($db->quoteName('#__workflows', 'w')) + ->where($db->quoteName('ws.workflow_id') . ' = ' . $db->quoteName('w.id')) + ->where($db->quoteName('ws.default') . ' = 1') + ->where($db->quoteName('w.published') . ' = 1') + ->where($db->quoteName('ws.published') . ' = 1') + ->where($db->quoteName('w.id') . ' = ' . (int) $workflow_id); + + $workflow = $db->setQuery($query)->loadObject(); + + if (!empty($workflow->id)) + { + return $workflow; + } + } + + // Use default workflow + $query = $db->getQuery(true); + + $query ->select( + $db->quoteName( + [ + 'w.id', + 'ws.condition' + ] + ) + ) + ->select($db->quoteName('ws.id', 'state_id')) + ->from($db->quoteName('#__workflow_states', 'ws')) + ->from($db->quoteName('#__workflows', 'w')) + ->where($db->quoteName('ws.default') . ' = 1') + ->where($db->quoteName('ws.workflow_id') . ' = ' . $db->quoteName('w.id')) + ->where($db->quoteName('w.published') . ' = 1') + ->where($db->quoteName('ws.published') . ' = 1') + ->where($db->quoteName('w.default') . ' = 1'); + + $workflow = $db->setQuery($query)->loadObject(); + + // Last check if we have a workflow ID + if (!empty($workflow->id)) + { + return $workflow; + } + + return false; + } + + /** + * Runs transition for item. + * + * @param array $pk Id of article + * @param array $transition_id Id of transition + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function runTransition($pk, $transition_id) + { + $workflow = new Workflow(['extension' => 'com_content']); + + $runTransaction = $workflow->executeTransition($pk, $transition_id); + + if (!$runTransaction) + { + $this->setError(\JText::_('COM_CONTENT_ERROR_UPDATE_STATE')); + + return false; + } + + // B/C state change trigger for UCM + $context = $this->option . '.' . $this->name; + + // Include the plugins for the change of state event. + \JPluginHelper::importPlugin($this->events_map['change_state']); + + // Trigger the change state event. + \JFactory::getApplication()->triggerEvent($this->event_change_state, [$context, [$pk], $transition_id]); + + return true; + } } diff --git a/administrator/components/com_content/Model/ArticlesModel.php b/administrator/components/com_content/Model/ArticlesModel.php index ca8ec81d72c1a..ca33c6ec15d0a 100644 --- a/administrator/components/com_content/Model/ArticlesModel.php +++ b/administrator/components/com_content/Model/ArticlesModel.php @@ -11,11 +11,12 @@ defined('_JEXEC') or die; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Associations; use Joomla\CMS\MVC\Model\ListModel; -use Joomla\Utilities\ArrayHelper; use Joomla\CMS\Plugin\PluginHelper; -use Joomla\CMS\Language\Associations; -use Joomla\CMS\Factory; +use Joomla\CMS\Workflow\Workflow; +use Joomla\Utilities\ArrayHelper; /** * Methods supporting a list of article records. @@ -108,6 +109,9 @@ protected function populateState($ordering = 'a.id', $direction = 'desc') $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); $this->setState('filter.published', $published); + $condition = $this->getUserStateFromRequest($this->context . '.filter.condition', 'filter_condition', ''); + $this->setState('filter.condition', $condition); + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); $this->setState('filter.level', $level); @@ -219,6 +223,29 @@ protected function getListQuery() $query->select('ua.name AS author_name') ->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); + // Join over the associations. + $query ->select($query->quoteName('wa.state_id', 'state_id')) + ->innerJoin($query->quoteName('#__workflow_associations', 'wa')) + ->where($query->quoteName('wa.item_id') . ' = ' . $query->quoteName('a.id')); + + // Join over the states. + $query ->select( + $query->quoteName( + [ + 'ws.title', + 'ws.condition', + 'ws.workflow_id' + ], + [ + 'state_title', + 'state_condition', + 'workflow_id' + ] + ) + ) + ->innerJoin($query->quoteName('#__workflow_states', 'ws')) + ->where($query->quoteName('ws.id') . ' = ' . $query->quoteName('wa.state_id')); + // Join on voting table $associationsGroupBy = array( 'a.id', @@ -249,7 +276,7 @@ protected function getListQuery() if (PluginHelper::isEnabled('content', 'vote')) { - $query->select('COALESCE(NULLIF(ROUND(v.rating_sum / v.rating_count, 0), 0), 0) AS rating, + $query->select('COALESCE(NULLIF(ROUND(v.rating_sum / v.rating_count, 0), 0), 0) AS rating, COALESCE(NULLIF(v.rating_count, 0), 0) as rating_count') ->join('LEFT', '#__content_rating AS v ON a.id = v.content_id'); @@ -288,17 +315,32 @@ protected function getListQuery() } // Filter by published state - $published = (string) $this->getState('filter.published'); + $workflowState = (string) $this->getState('filter.state'); + + if (is_numeric($workflowState)) + { + $query->where('wa.state_id = ' . (int) $workflowState); + } + + $condition = (string) $this->getState('filter.condition'); - if (is_numeric($published)) + if (is_numeric($condition)) { - $query->where('a.state = ' . (int) $published); + switch ((int) $condition) + { + case Workflow::PUBLISHED: + case Workflow::UNPUBLISHED: + case Workflow::TRASHED: + $query->where($db->quoteName('ws.condition') . ' = ' . $query->quote($condition)); + } } - elseif ($published === '') + elseif (!is_numeric($workflowState)) { - $query->where('(a.state = 0 OR a.state = 1)'); + $query->where($db->quoteName('ws.condition') . ' IN (' . $query->quote(Workflow::PUBLISHED) . ',' . $query->quote(Workflow::UNPUBLISHED) . ')'); } + $query->where($db->quoteName('wa.extension') . '=' . $db->quote('com_content')); + // Filter by categories and by level $categoryId = $this->getState('filter.category_id', array()); $level = $this->getState('filter.level'); @@ -415,6 +457,108 @@ protected function getListQuery() return $query; } + /** + * Method to get all transitions at once for all articles + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + 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->getDbo(); + $user = Factory::getUser(); + + $items = $this->getItems(); + + $ids = ArrayHelper::getColumn($items, 'state_id'); + $ids = ArrayHelper::toInteger($ids); + $ids = array_unique(array_filter($ids)); + + $ids[] = -1; + + $this->cache[$store] = array(); + + try + { + if (count($ids)) + { + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $query = $db->getQuery(true); + + $select = $db->quoteName( + array( + 't.id', + 't.title', + 't.from_state_id', + 't.to_state_id', + 's.id', + 's.title', + 's.condition', + 's.workflow_id' + ), + array( + 'value', + 'text', + 'from_state_id', + 'to_state_id', + 'state_id', + 'state_title', + 'state_condition', + 'workflow_id' + ) + ); + + $query->select($select) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->leftJoin($db->quoteName('#__workflow_states', 's') . ' ON ' . $db->quoteName('t.from_state_id') . ' IN(' . implode(',', $ids) . ')') + ->where($db->quoteName('t.to_state_id') . ' = ' . $db->quoteName('s.id')) + ->where($db->quoteName('t.published') . ' = 1') + ->where($db->quoteName('s.published') . ' = 1') + ->order($db->quoteName('t.ordering')); + + $transitions = $db->setQuery($query)->loadAssocList(); + + $workflow = new Workflow(['extension' => 'com_content']); + + foreach ($transitions as $key => $transition) + { + if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) + { + unset($transitions[$key]); + } + else + { + // Update the transition text with final state value + $conditionName = $workflow->getConditionName($transition['state_condition']); + + $transitions[$key]['text'] .= ' [' . \JText::_($conditionName) . ']'; + } + } + + $this->cache[$store] = $transitions; + } + } + catch (\RuntimeException $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + /** * Build a list of authors * diff --git a/administrator/components/com_content/Model/FeaturedModel.php b/administrator/components/com_content/Model/FeaturedModel.php index 6fe20f1fafa54..223cd99930ef0 100644 --- a/administrator/components/com_content/Model/FeaturedModel.php +++ b/administrator/components/com_content/Model/FeaturedModel.php @@ -113,6 +113,28 @@ protected function getListQuery() $query->select('ua.name AS author_name') ->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); + // Join over the states. + $query->select('wa.state_id AS state_id') + ->join('LEFT', '#__workflow_associations AS wa ON wa.item_id = a.id'); + + // Join over the states. + $query ->select( + $query->quoteName( + [ + 'ws.title', + 'ws.condition', + 'ws.workflow_id' + ], + [ + 'state_title', + 'state_condition', + 'workflow_id' + ] + ) + ) + ->innerJoin($query->quoteName('#__workflow_states', 'ws')) + ->where($query->quoteName('ws.id') . ' = ' . $query->quoteName('wa.state_id')); + // Join on voting table if (PluginHelper::isEnabled('content', 'vote')) { @@ -135,17 +157,26 @@ protected function getListQuery() } // Filter by published state - $published = $this->getState('filter.published'); + $workflowState = (string) $this->getState('filter.state'); + + if (is_numeric($workflowState)) + { + $query->where('wa.state_id = ' . (int) $workflowState); + } + + $condition = (string) $this->getState('filter.condition'); - if (is_numeric($published)) + if (is_numeric($condition)) { - $query->where('a.state = ' . (int) $published); + $query->where($db->quoteName('ws.condition') . '=' . $db->quote($condition)); } - elseif ($published === '') + elseif (!is_numeric($workflowState)) { - $query->where('(a.state = 0 OR a.state = 1)'); + $query->where($db->quoteName('ws.condition') . ' IN ("0","1")'); } + $query->where($db->quoteName('wa.extension') . '=' . $db->quote('com_content')); + // Filter by a single or group of categories. $baselevel = 1; $categoryId = $this->getState('filter.category_id'); diff --git a/administrator/components/com_content/View/Articles/HtmlView.php b/administrator/components/com_content/View/Articles/HtmlView.php index 22e3e6949e816..05d9f09ca5af0 100644 --- a/administrator/components/com_content/View/Articles/HtmlView.php +++ b/administrator/components/com_content/View/Articles/HtmlView.php @@ -12,16 +12,15 @@ defined('_JEXEC') or die; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\Component\Content\Administrator\Helper\ContentHelper; +use Joomla\CMS\Workflow\Workflow; use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Multilanguage; use Joomla\CMS\Plugin\PluginHelper; -use Joomla\CMS\Helper\ContentHelper; use Joomla\CMS\Factory; -\JLoader::register('ContentHelper', JPATH_ADMINISTRATOR . '/components/com_content/helpers/content.php'); - /** * View class for a list of articles. * @@ -98,7 +97,7 @@ public function display($tpl = null) { if ($this->getLayout() !== 'modal') { - \ContentHelper::addSubmenu('articles'); + ContentHelper::addSubmenu('articles'); } $this->items = $this->get('Items'); @@ -107,6 +106,7 @@ public function display($tpl = null) $this->authors = $this->get('Authors'); $this->filterForm = $this->get('FilterForm'); $this->activeFilters = $this->get('ActiveFilters'); + $this->transitions = $this->get('Transitions'); $this->vote = PluginHelper::isEnabled('content', 'vote'); // Check for errors. @@ -173,10 +173,6 @@ protected function addToolbar() if ($canDo->get('core.edit.state')) { - $toolbar->publish('articles.publish')->listCheck(true); - - $toolbar->unpublish('articles.unpublish')->listCheck(true); - $toolbar->standardButton('featured') ->text('JFEATURE') ->task('articles.featured') @@ -187,15 +183,13 @@ protected function addToolbar() ->task('articles.unfeatured') ->listCheck(true); - $toolbar->archive('articles.archive')->listCheck(true); - $toolbar->checkin('articles.checkin')->listCheck(true); } // Add a batch button if ($user->authorise('core.create', 'com_content') && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.edit.state', 'com_content')) + && $user->authorise('core.execute.transition', 'com_content')) { $toolbar->popupButton('batch') ->text('JTOOLBAR_BATCH') @@ -203,7 +197,7 @@ protected function addToolbar() ->listCheck(true); } - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) + if ($this->state->get('filter.condition') == Workflow::TRASHED && $canDo->get('core.delete')) { $toolbar->delete('articles.delete') ->text('JTOOLBAR_EMPTY_TRASH') diff --git a/administrator/components/com_content/View/Featured/HtmlView.php b/administrator/components/com_content/View/Featured/HtmlView.php index 54cab6b57f538..2e78535b95732 100644 --- a/administrator/components/com_content/View/Featured/HtmlView.php +++ b/administrator/components/com_content/View/Featured/HtmlView.php @@ -102,6 +102,7 @@ public function display($tpl = null) $this->authors = $this->get('Authors'); $this->filterForm = $this->get('FilterForm'); $this->activeFilters = $this->get('ActiveFilters'); + $this->transitions = $this->get('Transitions'); $this->vote = PluginHelper::isEnabled('content', 'vote'); // Check for errors. @@ -144,8 +145,6 @@ protected function addToolbar() if ($canDo->get('core.edit.state')) { - ToolbarHelper::publish('articles.publish', 'JTOOLBAR_PUBLISH', true); - ToolbarHelper::unpublish('articles.unpublish', 'JTOOLBAR_UNPUBLISH', true); ToolbarHelper::custom('articles.unfeatured', 'unfeatured.png', 'featured_f2.png', 'JUNFEATURE', true); ToolbarHelper::archiveList('articles.archive'); ToolbarHelper::checkin('articles.checkin'); @@ -155,10 +154,6 @@ protected function addToolbar() { ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'articles.delete', 'JTOOLBAR_EMPTY_TRASH'); } - elseif ($canDo->get('core.edit.state')) - { - ToolbarHelper::trash('articles.trash'); - } if ($canDo->get('core.admin') || $canDo->get('core.options')) { diff --git a/administrator/components/com_content/access.xml b/administrator/components/com_content/access.xml index ac2db9a908bdf..a75bbc9216795 100644 --- a/administrator/components/com_content/access.xml +++ b/administrator/components/com_content/access.xml @@ -10,6 +10,7 @@ +
@@ -37,4 +38,12 @@
+
+ + + + + + +
diff --git a/administrator/components/com_content/config.xml b/administrator/components/com_content/config.xml index e3b4c54cbd28f..461ebff62b3fe 100644 --- a/administrator/components/com_content/config.xml +++ b/administrator/components/com_content/config.xml @@ -350,8 +350,8 @@ -
@@ -518,7 +518,7 @@ > JSHOW - JSHOW - JSHOW - JSHOW - J5 - JSHOW - JSHOW - JSHOW - -
- JSHOW - J5 - JSHOW - JSHOW - -
- - - - - JGLOBAL_ACROSS - -
-
- JSHOW - JTAG - JSHOW - JPUBLISHED - - JSHOW - JSHOW - JSHOW - - - JPUBLISHED - JGLOBAL_AUTO - JSHOW - @@ -973,7 +973,7 @@
-
JYES + + + + +
- - - - - - - + + + +
+ +
+ +
+
+ +
diff --git a/administrator/components/com_content/forms/filter_articles.xml b/administrator/components/com_content/forms/filter_articles.xml index 41043d08ad985..4c7658251294d 100644 --- a/administrator/components/com_content/forms/filter_articles.xml +++ b/administrator/components/com_content/forms/filter_articles.xml @@ -10,11 +10,25 @@ /> - + name="state" + type="workflowstate" + label="COM_CONTENT_STATES" + onchange="this.form.submit();" + activeonly="true" + > + + + + + + + + - + name="state" + type="workflowstate" + label="COM_CONTENT_STATES" + onchange="this.form.submit();" + activeonly="true" + > + + + + + + + + addScriptDeclaration($js); -// Configure content state button renderer. -$publishedButton = new PublishedButton(['task_prefix' => 'articles.', 'checkbox_name' => 'cb']); +$assoc = Associations::isEnabled(); // Configure featured button renderer. $featuredButton = (new ActionButton(['tip_title' => 'JGLOBAL_TOGGLE_FEATURED'])) @@ -98,8 +112,8 @@ - - + + @@ -148,6 +162,18 @@ $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || $item->checked_out == 0; $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; + + $transitions = ContentHelper::filterTransitions($this->transitions, $item->state_id, $item->workflow_id); + + $hasTransitions = count($transitions) > 0; + + $default = [ + JHtml::_('select.option', '', $this->escape($item->state_title)), + JHtml::_('select.option', '-1', '--------', ['disable' => true]) + ]; + + $transitions = array_merge($default, $transitions); + ?> @@ -172,10 +198,48 @@ id, false, 'cid', 'cb', $item->title); ?> - -
- render($item->state, $i, ['disabled' => !$canChange], $item->publish_up, $item->publish_down); ?> + +
+
render($item->featured, $i, ['disabled' => !$canChange]); ?> + state_condition) : + + case Workflow::TRASHED: + $icon = 'trash'; + break; + + case Workflow::UNPUBLISHED: + $icon = 'unpublish'; + break; + + endswitch; + ?> + + + + + + + +
+
escape($item->state_title); ?>
+ +
+ 'transition-select_' . (int) $item->id, + 'list.attr' => [ + 'class' => 'custom-select custom-select-sm form-control form-control-sm', + 'onchange' => "listItemTask('cb" . (int) $i . "', 'articles.runTransition')"] + ]; + echo JHTML::_('select.genericlist', $transitions, 'transition_' . (int) $item->id, $attribs); + ?> +
+
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 fbc310d19ec1e..858b7c2b2ad93 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_body.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_body.php @@ -11,6 +11,8 @@ use Joomla\CMS\HTML\HTMLHelper; $published = $this->state->get('filter.published'); + +$user = \Joomla\CMS\Factory::getUser(); ?>
@@ -39,5 +41,14 @@
+ authorise('core.admin', 'com_content')) : ?> +
+
+ 'com_content']; + echo JLayoutHelper::render('joomla.html.batch.workflowstate', $displayData); ?> +
+
+ 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 02d45617d1a89..5288831caa06b 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_footer.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_footer.php @@ -12,6 +12,17 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\HTML\HTMLHelper; +JFactory::getDocument()->addScriptDeclaration(" + jQuery('#exampleModal').on('hide.bs.modal', function (e) { + document.getElementById('batch-category-id').value = ''; + document.getElementById('batch-access').value = ''; + document.getElementById('batch-language-id').value = ''; + document.getElementById('batch-user-id').value = ''; + document.getElementById('batch-tag-id').value = ''; + document.getElementById('batch-workflowstate-id').value = ''; + }); +"); + HTMLHelper::_('script', 'com_content/admin-articles-default-batch-footer.js', ['relative' => true, 'version' => 'auto']); ?> diff --git a/administrator/components/com_content/tmpl/articles/modal.php b/administrator/components/com_content/tmpl/articles/modal.php index d53aa589125d8..64f43ae2d1359 100644 --- a/administrator/components/com_content/tmpl/articles/modal.php +++ b/administrator/components/com_content/tmpl/articles/modal.php @@ -86,7 +86,6 @@ -2 => 'icon-trash', 0 => 'icon-unpublish', 1 => 'icon-publish', - 2 => 'icon-archive', ); ?> items as $i => $item) : ?> @@ -112,7 +111,7 @@ ?> - + escape($onclick) . '"' diff --git a/administrator/components/com_content/tmpl/featured/default.php b/administrator/components/com_content/tmpl/featured/default.php index ad557adc5423e..14fbbe22a2ccf 100644 --- a/administrator/components/com_content/tmpl/featured/default.php +++ b/administrator/components/com_content/tmpl/featured/default.php @@ -9,12 +9,14 @@ defined('_JEXEC') or die; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Router\Route; -use Joomla\CMS\Language\Multilanguage; -use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Multilanguage; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Workflow\Workflow; +use Joomla\Component\Content\Administrator\Helper\ContentHelper; HTMLHelper::_('behavior.multiselect'); HTMLHelper::_('formbehavior.chosen', '.multipleAccessLevels', null, array('placeholder_text_multiple' => Text::_('JOPTION_SELECT_ACCESS'))); @@ -46,6 +48,22 @@ $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component'; HTMLHelper::_('sortablelist.sortable', 'articleList', 'adminForm', strtolower($listDirn), $saveOrderingUrl); } + +$js = " + ;(function($) + { + $(function() + { + $('.article-status').on('click', function(e) + { + e.stopPropagation(); + }); + }); + })(jQuery); +"; + +\Joomla\CMS\Factory::getDocument()->addScriptDeclaration($js); + ?>
@@ -74,7 +92,7 @@ - + @@ -119,6 +137,18 @@ $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || $item->checked_out == 0; $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + + $transitions = ContentHelper::filterTransitions($this->transitions, $item->state_id, $item->workflow_id); + + $hasTransitions = count($transitions) > 0; + + $default = [ + JHtml::_('select.option', '', $this->escape($item->state_title)), + JHtml::_('select.option', '-1', '--------', ['disable' => true]) + ]; + + $transitions = array_merge($default, $transitions); + ?> @@ -144,10 +174,48 @@ id); ?> - -
- state, $i, 'articles.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - featured, $i, $canChange); ?> + +
+ +
escape($item->state_title); ?>
+ +
+ 'transition-select_' . (int) $item->id, + 'list.attr' => [ + 'class' => 'custom-select custom-select-sm form-control form-control-sm', + 'onchange' => "listItemTask('cb" . (int) $i . "', 'articles.runTransition')"] + ]; + echo HTMLHelper::_('select.genericlist', $transitions, 'transition_' . (int) $item->id, $attribs); + ?> +
+
@@ -163,8 +231,8 @@ escape($item->title); ?> - escape($item->alias)); ?> - + escape($item->alias)); ?> +
escape($item->category_title); ?>
diff --git a/administrator/components/com_menus/presets/joomla.xml b/administrator/components/com_menus/presets/joomla.xml index e80ceea85b4a8..147cd35e222d5 100644 --- a/administrator/components/com_menus/presets/joomla.xml +++ b/administrator/components/com_menus/presets/joomla.xml @@ -33,8 +33,14 @@ type="component" element="com_content" link="index.php?option=com_content&view=featured" - class="class:featured" - /> + class="class:featured" > + + + @@ -189,7 +195,19 @@ link="index.php?option=com_users&view=levels"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/administrator/components/com_messages/Model/MessageModel.php b/administrator/components/com_messages/Model/MessageModel.php index cb8a7aed7ff6d..2d4250a2d9c61 100644 --- a/administrator/components/com_messages/Model/MessageModel.php +++ b/administrator/components/com_messages/Model/MessageModel.php @@ -14,6 +14,7 @@ use Joomla\CMS\Language\Language; use Joomla\CMS\Log\Log; use Joomla\CMS\MVC\Model\AdminModel; +use Joomla\Component\Messages\Administrator\Model\ConfigModel; use Joomla\CMS\Language\Text; use Joomla\CMS\User\User; use Joomla\CMS\Uri\Uri; diff --git a/administrator/components/com_workflow/Controller/DisplayController.php b/administrator/components/com_workflow/Controller/DisplayController.php new file mode 100644 index 0000000000000..504308afa5171 --- /dev/null +++ b/administrator/components/com_workflow/Controller/DisplayController.php @@ -0,0 +1,61 @@ +extension)) + { + $this->extension = $this->input->get('extension', 'com_content'); + } + } +} diff --git a/administrator/components/com_workflow/Controller/StateController.php b/administrator/components/com_workflow/Controller/StateController.php new file mode 100644 index 0000000000000..697a18fa6a48e --- /dev/null +++ b/administrator/components/com_workflow/Controller/StateController.php @@ -0,0 +1,150 @@ +workflowID)) + { + $this->workflowID = $this->input->get('workflow_id'); + } + + if (empty($this->extension)) + { + $this->extension = $this->input->get('extension'); + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowAdd($data = array()) + { + $user = Factory::getUser(); + + return $user->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 __DEPLOY_VERSION__ + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = Factory::getUser(); + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.state.' . $recordId)) + { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.state.' . $recordId)) + { + // Need to do a lookup from the model to get the owner + $record = $this->getModel()->getItem($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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowID . '&extension=' . $this->extension; + + return $append; + } +} diff --git a/administrator/components/com_workflow/Controller/StatesController.php b/administrator/components/com_workflow/Controller/StatesController.php new file mode 100644 index 0000000000000..2519485922a5b --- /dev/null +++ b/administrator/components/com_workflow/Controller/StatesController.php @@ -0,0 +1,146 @@ +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\Model\Model The model. + * + * @since __DEPLOY_VERSION__ + */ + public function getModel($name = 'State', $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 __DEPLOY_VERSION__ + */ + public function setDefault() + { + // Check for request forgeries + \JSession::checkToken('request') or die(\JText::_('JINVALID_TOKEN')); + + // Get items to publish from the request. + $cid = $this->input->get('cid', array(), 'array'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) + { + $this->setMessage(\JText::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension"), false + ) + ); + + return; + } + + if (empty($cid) || !is_array($cid)) + { + $this->setMessage(\JText::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } + elseif (count($cid) > 1) + { + $this->setMessage(\JText::_('COM_WORKFLOW_TO_MANY_ITEMS'), 'error'); + } + else + { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = (int) reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) + { + $this->setMessage($model->getError(), 'warning'); + } + else + { + $this->setMessage(\JText::_('COM_WORKFLOW_ITEM_SET_DEFAULT')); + } + } + + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension") + . '&workflow_id=' . $this->input->getCmd("workflow_id"), false + ) + ); + } + + /** + * Deletes and returns correctly. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function delete() + { + parent::delete(); + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension") + . '&workflow_id=' . $this->input->getCmd("workflow_id"), false + ) + ); + } +} diff --git a/administrator/components/com_workflow/Controller/TransitionController.php b/administrator/components/com_workflow/Controller/TransitionController.php new file mode 100644 index 0000000000000..8579a3aa81763 --- /dev/null +++ b/administrator/components/com_workflow/Controller/TransitionController.php @@ -0,0 +1,149 @@ +workflowID)) + { + $this->workflowID = $this->input->get('workflow_id'); + } + + if (empty($this->extension)) + { + $this->extension = $this->input->get('extension'); + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function allowAdd($data = array()) + { + $user = Factory::getUser(); + + return $user->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 __DEPLOY_VERSION__ + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = Factory::getUser(); + + // 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)) + { + // Need to do a lookup from the model to get the owner + $record = $this->getModel()->getItem($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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowID . '&extension=' . $this->extension; + + return $append; + } +} diff --git a/administrator/components/com_workflow/Controller/TransitionsController.php b/administrator/components/com_workflow/Controller/TransitionsController.php new file mode 100644 index 0000000000000..33c7390bdc6f6 --- /dev/null +++ b/administrator/components/com_workflow/Controller/TransitionsController.php @@ -0,0 +1,58 @@ + true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Deletes and returns correctly. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function delete() + { + parent::delete(); + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension") + . '&workflow_id=' . $this->input->getCmd("workflow_id"), false + ) + ); + } +} diff --git a/administrator/components/com_workflow/Controller/WorkflowController.php b/administrator/components/com_workflow/Controller/WorkflowController.php new file mode 100644 index 0000000000000..8876255948fdc --- /dev/null +++ b/administrator/components/com_workflow/Controller/WorkflowController.php @@ -0,0 +1,244 @@ +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 __DEPLOY_VERSION__ + */ + protected function allowAdd($data = array()) + { + $user = Factory::getUser(); + + return $user->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 __DEPLOY_VERSION__ + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = Factory::getUser(); + + // 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)) + { + // Need to do a lookup from the model to get the owner + $record = $this->getModel()->getItem($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 __DEPLOY_VERSION__ + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $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 __DEPLOY_VERSION__ + */ + 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 \JModelLegacy $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function postSaveHook(\JModelLegacy $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 = $this->input->getInt($key); + + $db = $model->getDbo(); + $query = $db->getQuery(true); + + $query->select('*') + ->from($db->quoteName('#__workflow_states')) + ->where($db->quoteName('workflow_id') . ' = ' . (int) $recordId); + + $statuses = $db->setQuery($query)->loadAssocList(); + + $smodel = $this->getModel('State'); + + $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->clear(); + + $query->select('*') + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = ' . (int) $recordId); + + $transitions = $db->setQuery($query)->loadAssocList(); + + $tmodel = $this->getModel('Transition'); + + foreach ($transitions as $transition) + { + $table = $tmodel->getTable(); + + $transition['from_state_id'] = $mapping[$transition['from_state_id']]; + $transition['to_state_id'] = $mapping[$transition['to_state_id']]; + + $transition['workflow_id'] = $workflowID; + $transition['id'] = 0; + unset($transition['asset_id']); + + $table->save($transition); + } + } + } + + /** + * Method to save a workflow. + * + * @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 __DEPLOY_VERSION__ + */ + public function save($key = null, $urlVar = null) + { + $task = $this->getTask(); + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') + { + $data = $this->input->post->get('jform', array(), 'array'); + + // Prevent default + $data['default'] = 0; + + $this->input->post->set('jform', $data); + } + + parent::save($key, $urlVar); + } +} diff --git a/administrator/components/com_workflow/Controller/WorkflowsController.php b/administrator/components/com_workflow/Controller/WorkflowsController.php new file mode 100644 index 0000000000000..8b2726f38e310 --- /dev/null +++ b/administrator/components/com_workflow/Controller/WorkflowsController.php @@ -0,0 +1,152 @@ +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\Model\Model The model. + * + * @since __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + public function setDefault() + { + // Check for request forgeries + \JSession::checkToken('request') or die(\JText::_('JINVALID_TOKEN')); + + // Get items to publish from the request. + $cid = $this->input->get('cid', array(), 'array'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) + { + $this->setMessage(\JText::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension"), false + ) + ); + + return; + } + + if (empty($cid) || !is_array($cid)) + { + $this->setMessage(\JText::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } + elseif (count($cid) > 1) + { + $this->setMessage(\JText::_('COM_WORKFLOW_TO_MANY_ITEMS'), 'error'); + } + else + { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = (int) reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) + { + $this->setMessage($model->getError(), 'warning'); + } + else + { + if ($value === 1) + { + $ntext = 'COM_WORKFLOW_ITEM_SET_DEFAULT'; + } + else + { + $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; + } + + $this->setMessage(\JText::_($ntext, count($cid))); + } + } + + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension"), false + ) + ); + } + + /** + * Deletes and returns correctly. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function delete() + { + parent::delete(); + $this->setRedirect( + \JRoute::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->input->getCmd("extension"), false + ) + ); + } +} diff --git a/administrator/components/com_workflow/Helper/StateHelper.php b/administrator/components/com_workflow/Helper/StateHelper.php new file mode 100644 index 0000000000000..48519b77fa389 --- /dev/null +++ b/administrator/components/com_workflow/Helper/StateHelper.php @@ -0,0 +1,25 @@ +input->getCmd('extension'); + + $parts = explode('.', $extension); + + $component = reset($parts); + + $eName = ucfirst(str_replace('com_', '', $component)); + $cName = $eName . 'Helper'; + + $class = '\\Joomla\\Component\\' . $eName . '\\Administrator\\Helper\\' . $cName; + + if (class_exists($class) && is_callable([$class, 'addSubmenu'])) + { + $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_BASE, null, false, true) + || $lang->load($component, \JPath::clean(JPATH_ADMINISTRATOR . '/components/' . $component), null, false, true); + + call_user_func([$class, 'addSubmenu'], $vName); + } + } +} diff --git a/administrator/components/com_workflow/Model/StateModel.php b/administrator/components/com_workflow/Model/StateModel.php new file mode 100644 index 0000000000000..6ee084cf64cec --- /dev/null +++ b/administrator/components/com_workflow/Model/StateModel.php @@ -0,0 +1,331 @@ +option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $category_id The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since __DEPLOY_VERSION__ + */ + protected function generateNewTitle($category_id, $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 __DEPLOY_VERSION__ + */ + public function save($data) + { + $context = $this->option . '.' . $this->name; + $app = \JFactory::getApplication(); + $input = $app->input; + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) + { + $data['workflow_id'] = $workflowID; + } + + 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 __DEPLOY_VERSION__ + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) + { + $this->setError(\JText::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', 'com_content', 'cmd'); + + $parts = explode('.', $extension); + + $component = reset($parts); + + if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) + { + $this->setError(\JText::_('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 __DEPLOY_VERSION__ + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', 'com_content', 'cmd'); + + // 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 \JForm|boolean A JForm object on success, false on failure + * + * @since __DEPLOY_VERSION__ + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.state', + 'state', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) + { + return false; + } + + if ($loadData) + { + $data = $this->loadFormData(); + } + + $item = $this->getItem($form->getValue('id')); + + // Deactivate switcher if default + // Use $item, otherwise we'll be locked when we get the data from the request + if (!empty($item->default)) + { + $form->setValue('default', null, 1); + $form->setFieldAttribute('default', 'readonly', 'true'); + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) + { + // 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'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since __DEPLOY_VERSION__ + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = \JFactory::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 __DEPLOY_VERSION__ + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load(array('id' => $pk))) + { + if (!$table->published) + { + $this->setError(\JText::_("COM_WORKFLOW_ITEM_MUST_PUBLISHED")); + + 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(array('id' => $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 __DEPLOY_VERSION__ + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', 'com_content', 'cmd'); + + // Default item existence checks. + if ($value != 1) + { + foreach ($pks as $i => $pk) + { + if ($table->load(array('id' => $pk)) && $table->default) + { + // Prune items that you can't change. + $app->enqueueMessage(\JText::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); + unset($pks[$i]); + } + } + } + + return parent::publish($pks, $value); + } +} diff --git a/administrator/components/com_workflow/Model/StatesModel.php b/administrator/components/com_workflow/Model/StatesModel.php new file mode 100644 index 0000000000000..b4630aeb98722 --- /dev/null +++ b/administrator/components/com_workflow/Model/StatesModel.php @@ -0,0 +1,184 @@ +getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', '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 add to ordering queries. + * + * @since __DEPLOY_VERSION__ + */ + protected function getReorderConditions($table) + { + return 'workflow_id = ' . $this->getDbo()->q((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 JTable object + * + * @since __DEPLOY_VERSION__ + */ + public function getTable($type = 'State', $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 __DEPLOY_VERSION__ + */ + public function getListQuery() + { + $db = $this->getDbo(); + + $query = parent::getListQuery(); + + $select = $db->quoteName( + array( + 's.id', + 's.title', + 's.ordering', + 's.condition', + 's.default', + 's.published' + ) + ); + + $query + ->select($select) + ->from($db->quoteName('#__workflow_states', 's')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) + { + $query->where($db->quoteName('s.workflow_id') . ' = ' . $workflowID); + } + + // Filter by condition + if ($condition = $this->getState('filter.condition')) + { + $query->where($db->quoteName('s.condition') . ' = ' . $db->quote($db->escape($condition))); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by condition + if (is_numeric($status)) + { + $query->where($db->quoteName('s.published') . ' = ' . (int) $status); + } + 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 = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where('(' . $db->quoteName('s.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('s.description') . ' LIKE ' . $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; + } +} diff --git a/administrator/components/com_workflow/Model/TransitionModel.php b/administrator/components/com_workflow/Model/TransitionModel.php new file mode 100644 index 0000000000000..9b987ceee4438 --- /dev/null +++ b/administrator/components/com_workflow/Model/TransitionModel.php @@ -0,0 +1,271 @@ +option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', 'com_content', '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 __DEPLOY_VERSION__ + */ + 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', 'com_content', '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 __DEPLOY_VERSION__ + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', 'com_content', 'cmd'); + + // 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 save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since __DEPLOY_VERSION__ + */ + public function save($data) + { + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $input = $app->input; + + if ($pk > 0) + { + $isNew = false; + } + + if ($data['to_state_id'] == $data['from_state_id']) + { + $this->setError(\JText::_('You choose the same state from and to')); + + return false; + } + + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('from_state_id') . ' = ' . (int) $data['from_state_id']) + ->where($db->quoteName('to_state_id') . ' = ' . (int) $data['to_state_id']); + + if (!$isNew) + { + $query->where($db->quoteName('id') . ' <> ' . (int) $data['id']); + } + + $db->setQuery($query); + $checkDupliaction = $db->loadResult(); + + if (!empty($checkDupliaction)) + { + $this->setError(\JText::_("COM_WORKFLOW_TRANSITION_DUPLICATE")); + + return false; + } + + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) + { + $data['workflow_id'] = $workflowID; + } + + 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 $category_id The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since __DEPLOY_VERSION__ + */ + protected function generateNewTitle($category_id, $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 \JForm|boolean A JForm object on success, false on failure + * + * @since __DEPLOY_VERSION__ + */ + 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; + } + + if ($loadData) + { + $data = (object) $this->loadFormData(); + } + + if (!$this->canEditState($data)) + { + // 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'); + } + + $app = Factory::getApplication(); + + $workflow_id = $app->input->getInt('workflow_id'); + + $where = $this->getDbo()->quoteName('workflow_id') . ' = ' . $workflow_id . ' AND ' . $this->getDbo()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_state_id', 'sql_where', $where); + $form->setFieldAttribute('to_state_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 __DEPLOY_VERSION__ + */ + 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; + } +} diff --git a/administrator/components/com_workflow/Model/TransitionsModel.php b/administrator/components/com_workflow/Model/TransitionsModel.php new file mode 100644 index 0000000000000..888f756a5d4b3 --- /dev/null +++ b/administrator/components/com_workflow/Model/TransitionsModel.php @@ -0,0 +1,233 @@ +getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', '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); + + // TODO: Change the autogenerated stub + } + + /** + * 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 JTable object + * + * @since __DEPLOY_VERSION__ + */ + 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 add to ordering queries. + * + * @since __DEPLOY_VERSION__ + */ + protected function getReorderConditions($table) + { + return 'workflow_id = ' . $this->getDbo()->q((int) $table->workflow_id); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since __DEPLOY_VERSION__ + */ + public function getListQuery() + { + $db = $this->getDbo(); + + $query = parent::getListQuery(); + + $select = $db->quoteName( + array( + 't.id', + 't.title', + 't.from_state_id', + 't.to_state_id', + 't.published', + 't.ordering', + ) + ); + + $select[] = $db->quoteName('f_state.title', 'from_state'); + $select[] = $db->quoteName('t_state.title', 'to_state'); + $joinTo = $db->quoteName('#__workflow_states', 't_state') . + ' ON ' . $db->quoteName('t_state.id') . ' = ' . $db->quoteName('t.to_state_id'); + + $query + ->select($select) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->leftJoin( + $db->quoteName('#__workflow_states', 'f_state') . ' ON ' . $db->quoteName('f_state.id') . ' = ' . $db->quoteName('t.from_state_id') + ) + ->leftJoin($joinTo); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) + { + $query->where($db->quoteName('t.workflow_id') . ' = ' . $workflowID); + } + + $status = $this->getState('filter.published'); + + // Filter by condition + if (is_numeric($status)) + { + $query->where($db->quoteName('t.published') . ' = ' . (int) $status); + } + elseif ($status == '') + { + $query->where($db->quoteName('t.published') . ' IN (0, 1)'); + } + + // Filter by column from_state_id + if ($fromState = $this->getState('filter.from_state')) + { + $query->where($db->quoteName('from_state_id') . ' = ' . (int) $fromState); + } + + // Filter by column from_state_id + if ($toState = $this->getState('filter.to_state')) + { + $query->where($db->quoteName('to_state_id') . ' = ' . (int) $toState); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) + { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where('(' . $db->quoteName('title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('description') . ' LIKE ' . $search . ')'); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 't.id'); + $orderDirn = strtolower($this->state->get('list.direction', 'asc')); + + $query->order($db->quoteName($orderCol) . ' ' . $db->escape($orderDirn == 'desc' ? 'DESC' : 'ASC')); + + return $query; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \JForm|boolean The \JForm object or false on error + * + * @since __DEPLOY_VERSION__ + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $id = (int) $this->getState('filter.workflow_id'); + + if ($form) + { + $where = $this->getDbo()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDbo()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_state', 'sql_where', $where, 'filter'); + $form->setFieldAttribute('to_state', 'sql_where', $where, 'filter'); + } + + return $form; + } +} diff --git a/administrator/components/com_workflow/Model/WorkflowModel.php b/administrator/components/com_workflow/Model/WorkflowModel.php new file mode 100644 index 0000000000000..0a83bb66118bf --- /dev/null +++ b/administrator/components/com_workflow/Model/WorkflowModel.php @@ -0,0 +1,373 @@ +option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $category_id The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since __DEPLOY_VERSION__ + */ + protected function generateNewTitle($category_id, $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 __DEPLOY_VERSION__ + */ + public function save($data) + { + $user = \JFactory::getUser(); + $app = \JFactory::getApplication(); + $input = $app->input; + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', 'com_content', 'cmd'); + $data['extension'] = $extension; + $data['asset_id'] = 0; + + 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; + } + + $result = parent::save($data); + + // Create a default state + if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) + { + $state = $this->getTable('State'); + + $newstate = new \stdClass; + + $newstate->workflow_id = (int) $this->getState($this->getName() . '.id'); + $newstate->title = \JText::_('COM_WORKFLOW_PUBLISHED'); + $newstate->description = ''; + $newstate->published = 1; + $newstate->condition = 1; + $newstate->default = 1; + + $state->save($newstate); + } + + 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 \JForm|boolean A JForm object on success, false on failure + * + * @since __DEPLOY_VERSION__ + */ + 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; + } + + if ($loadData) + { + $data = $this->loadFormData(); + } + + $item = $this->getItem($form->getValue('id')); + + // Deactivate switcher if default + // Use $item, otherwise we'll be locked when we get the data from the request + if (!empty($item->default)) + { + $form->setFieldAttribute('default', 'readonly', 'true'); + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) + { + // 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'); + } + + $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 __DEPLOY_VERSION__ + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = \JFactory::getApplication()->getUserState( + 'com_workflow.edit.workflow.data', + array() + ); + + if (empty($data)) + { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to preprocess the form. + * + * @param \JForm $form A \JForm 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 __DEPLOY_VERSION__ + */ + protected function preprocessForm(\JForm $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension', 'com_content'); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + $form->setFieldAttribute('rules', 'section', 'workflow'); + + 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 add to ordering queries. + * + * @since __DEPLOY_VERSION__ + */ + protected function getReorderConditions($table) + { + return 'extension = ' . $this->getDbo()->q($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 __DEPLOY_VERSION__ + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load(array('id' => $pk))) + { + if ($table->published !== 1) + { + $this->setError(\JText::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + $date = Factory::getDate()->toSql(); + + if ($value) + { + // Unset other default item + if ($table->load(array('default' => '1'))) + { + $table->default = 0; + $table->modified = $date; + $table->store(); + } + } + + if ($table->load(array('id' => $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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $date = Factory::getDate()->toSql(); + + // Default workflow item existence checks. + foreach ($pks as $i => $pk) + { + if ($value != 1 && $table->default) + { + $this->setError(\JText::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + unset($pks[$i]); + break; + } + + $table->load($pk); + $table->modified = $date; + $table->store(); + } + + return parent::publish($pks, $value); + } +} diff --git a/administrator/components/com_workflow/Model/WorkflowsModel.php b/administrator/components/com_workflow/Model/WorkflowsModel.php new file mode 100644 index 0000000000000..21687ebc38d2f --- /dev/null +++ b/administrator/components/com_workflow/Model/WorkflowsModel.php @@ -0,0 +1,274 @@ +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); + + parent::populateState($ordering, $direction); + + // TODO: Change the autogenerated stub + } + + /** + * 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 JTable object + * + * @since __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items) + { + $this->countItems($items); + } + + return $items; + } + + /** + * 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 __DEPLOY_VERSION__ + */ + protected function countItems($items) + { + $db = $this->getDbo(); + + $ids = [0]; + + foreach ($items as $item) + { + $ids[] = (int) $item->id; + + $item->count_states = 0; + $item->count_transitions = 0; + } + + $query = $db->getQuery(true); + + $query ->select('workflow_id, count(*) AS count') + ->from($db->quoteName('#__workflow_states')) + ->where($db->quoteName('workflow_id') . ' IN(' . implode(',', $ids) . ')') + ->where($db->quoteName('published') . '>= 0') + ->group('workflow_id'); + + $status = $db->setQuery($query)->loadObjectList('workflow_id'); + + $query = $db->getQuery(true); + + $query ->select('workflow_id, count(*) AS count') + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' IN(' . implode(',', $ids) . ')') + ->where($db->quoteName('published') . '>= 0') + ->group('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 __DEPLOY_VERSION__ + */ + public function getListQuery() + { + $db = $this->getDbo(); + + $query = parent::getListQuery(); + + $select = $db->quoteName( + array( + 'w.id', + 'w.title', + 'w.created', + 'w.modified', + 'w.published', + 'w.ordering', + 'w.default', + 'w.created_by', + 'u.name' + ) + ); + + $query + ->select($select) + ->from($db->quoteName('#__workflows', 'w')) + ->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')); + + // Filter by extension + if ($extension = $this->getState('filter.extension')) + { + $query->where($db->quoteName('extension') . ' = ' . $db->quote($db->escape($extension))); + } + + // Filter by author + $authorId = $this->getState('filter.created_by'); + + if (is_numeric($authorId)) + { + $type = $this->getState('filter.created_by.include', true) ? '= ' : '<>'; + $query->where($db->quoteName('w.created_by') . $type . (int) $authorId); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by condition + if (is_numeric($status)) + { + $query->where($db->quoteName('w.published') . ' = ' . (int) $status); + } + 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 = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where('(' . $db->quoteName('w.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('w.description') . ' LIKE ' . $search . ')'); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'w.ordering'); + $orderDirn = strtolower($this->state->get('list.direction', 'asc')); + + $query->order($db->quoteName($db->escape($orderCol)) . ' ' . $db->escape($orderDirn == 'desc' ? 'DESC' : 'ASC')); + + return $query; + } + + /** + * Build a list of authors + * + * @return stdClass[] + * + * @since __DEPLOY_VERSION__ + */ + public function getAuthors() + { + $query = $this->getDbo()->getQuery(true); + + $query->select('u.id AS value, u.name AS text') + ->from('#__users AS u') + ->join('INNER', '#__workflows AS c ON c.created_by = u.id') + ->group('u.id, u.name') + ->order('u.name'); + + return $this->getDbo()->setQuery($query)->loadObjectList(); + } +} diff --git a/administrator/components/com_workflow/Table/StateTable.php b/administrator/components/com_workflow/Table/StateTable.php new file mode 100644 index 0000000000000..dd2f86a1389bc --- /dev/null +++ b/administrator/components/com_workflow/Table/StateTable.php @@ -0,0 +1,236 @@ +access = (int) Factory::getConfig()->get('access'); + } + + /** + * Deletes workflow with transition and states. + * + * @param int $pk Extension ids to delete. + * + * @return boolean True on success. + * + * @since __DEPLOY_VERSION__ + * + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + // @TODO: correct ACL check should be done in $model->canDelete(...) not here + if (!\JFactory::getUser()->authorise('core.delete', 'com_workflows')) + { + throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + $db = $this->getDbo(); + $app = \JFactory::getApplication(); + + // Gets the update site names. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'title'))) + ->from($db->quoteName('#__workflow_states')) + ->where($db->quoteName('id') . ' = ' . (int) $pk); + $db->setQuery($query); + $state = $db->loadResult(); + + if ($state->default) + { + $app->enqueueMessage(\JText::sprintf('COM_WORKFLOW_MSG_DELETE_DEFAULT', $state->title), 'error'); + + return false; + } + + // Delete the update site from all tables. + try + { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('to_state_id') . ' = ' . (int) $pk, 'OR') + ->where($db->quoteName('from_state_id') . ' = ' . (int) $pk); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } + catch (\RuntimeException $e) + { + $app->enqueueMessage(\JText::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $state->title, $e->getMessage()), 'error'); + } + + return false; + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0 + */ + public function check() + { + try + { + parent::check(); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') + { + $this->setError(\JText::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); + + return false; + } + + if (!empty($this->default)) + { + if ((int) $this->published !== 1) + { + $this->setError(\JText::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + else + { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflow_states')) + ->where($db->quoteName('workflow_id') . '=' . $this->workflow_id) + ->where($db->quoteName('default') . '= 1'); + + $state = $db->setQuery($query)->loadObject(); + + if (empty($state) || $state->id === $this->id) + { + $this->default = '1'; + + $this->setError(\JText::_('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 + */ + public function store($updateNulls = false) + { + $table = new StateTable($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 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 __DEPLOY_VERSION__ + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + return $workflow->extension . '.state.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A JTable object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since __DEPLOY_VERSION__ + */ + 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); + $name = $workflow->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/Table/TransitionTable.php b/administrator/components/com_workflow/Table/TransitionTable.php new file mode 100644 index 0000000000000..5a7431efb1784 --- /dev/null +++ b/administrator/components/com_workflow/Table/TransitionTable.php @@ -0,0 +1,89 @@ +access = (int) Factory::getConfig()->get('access'); + } + + /** + * 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 __DEPLOY_VERSION__ + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + return $workflow->extension . '.transition.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A JTable object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since __DEPLOY_VERSION__ + */ + 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); + $name = $workflow->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/Table/WorkflowTable.php b/administrator/components/com_workflow/Table/WorkflowTable.php new file mode 100644 index 0000000000000..b17e4ea944def --- /dev/null +++ b/administrator/components/com_workflow/Table/WorkflowTable.php @@ -0,0 +1,282 @@ +typeAlias = '{extension}.workflow'; + + parent::__construct('#__workflows', 'id', $db); + + $this->access = (int) Factory::getConfig()->get('access'); + } + + /** + * Deletes workflow with transition and states. + * + * @param int $pk Extension ids to delete. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws \Exception on ACL error + */ + public function delete($pk = null) + { + if (!\JFactory::getUser()->authorise('core.delete', 'com_installer')) + { + throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + $db = $this->getDbo(); + $app = \JFactory::getApplication(); + + // Gets the update site names. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'title'))) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('id') . ' = ' . (int) $pk); + $db->setQuery($query); + $workflow = $db->loadResult(); + + if ($workflow->default) + { + $app->enqueueMessage(\JText::sprintf('COM_WORKFLOW_MSG_DELETE_DEFAULT', $workflow->title), 'error'); + + return false; + } + + // Delete the update site from all tables. + try + { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_states')) + ->where($db->quoteName('workflow_id') . ' = ' . (int) $pk); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = ' . (int) $pk); + $db->setQuery($query); + $db->execute(); + + return parent::delete($pk); + } + catch (\RuntimeException $e) + { + $app->enqueueMessage(\JText::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $workflow->title, $e->getMessage()), 'error'); + + return; + } + + return false; + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0 + */ + public function check() + { + try + { + parent::check(); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') + { + $this->setError(\JText::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); + + return false; + } + + if (!empty($this->default)) + { + if ((int) $this->published !== 1) + { + $this->setError(\JText::_('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'); + + $state = $db->setQuery($query)->loadObject(); + + if (empty($state) || $state->id === $this->id) + { + $this->default = '1'; + + $this->setError(\JText::_('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 + */ + public function store($updateNulls = false) + { + $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; + $this->modified = $this->getDbo()->getNullDate(); + } + + if (!(int) $this->created) + { + $this->created = $date->toSql(); + } + + if (empty($this->created_by)) + { + $this->created_by = $user->id; + } + + if ($this->default == '1') + { + // Verify that the default is unique for this workflow + if ($table->load(array('default' => '1'))) + { + $table->default = 0; + $table->store(); + } + } + + 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 __DEPLOY_VERSION__ + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + + return $this->extension . '.workflow.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A JTable object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since __DEPLOY_VERSION__ + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $assetId = null; + + // Build the query to get the asset id for the parent category. + $query = $this->_db->getQuery(true) + ->select($this->_db->quoteName('id')) + ->from($this->_db->quoteName('#__assets')) + ->where($this->_db->quoteName('name') . ' = ' . $this->_db->quote($this->extension)); + + // Get the asset id from the database. + $this->_db->setQuery($query); + + if ($result = $this->_db->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/View/State/HtmlView.php b/administrator/components/com_workflow/View/State/HtmlView.php new file mode 100644 index 0000000000000..7ac8196710ccc --- /dev/null +++ b/administrator/components/com_workflow/View/State/HtmlView.php @@ -0,0 +1,147 @@ +get('Errors'))) + { + throw new JViewGenericdataexception(implode("\n", $errors), 500); + } + + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->extension = $this->state->get('filter.extension'); + + // Set the toolbar + $this->addToolBar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = Factory::getUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StateHelper::getActions($this->extension, 'state', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? \JText::_('COM_WORKFLOW_STATE_ADD') : \JText::_('COM_WORKFLOW_STATE_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) + { + // For new records, check the create permission. + if ($canDo->get('core.edit')) + { + $toolbarButtons = [['apply', 'state.apply'], ['save', 'state.save'], ['save2new', 'state.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } + 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) + { + $toolbarButtons = [['apply', 'state.apply'], ['save', 'state.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', 'state.save2new']; + $toolbarButtons[] = ['save2copy', 'state.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } + + ToolbarHelper::cancel('state.cancel'); + ToolbarHelper::divider(); + } +} diff --git a/administrator/components/com_workflow/View/States/HtmlView.php b/administrator/components/com_workflow/View/States/HtmlView.php new file mode 100644 index 0000000000000..309c22a018444 --- /dev/null +++ b/administrator/components/com_workflow/View/States/HtmlView.php @@ -0,0 +1,195 @@ +get('Errors'))) + { + throw new \JViewGenericdataexception(implode("\n", $errors), 500); + } + + $this->state = $this->get('State'); + $this->states = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $this->workflowID = $this->state->get('filter.workflow_id'); + $this->extension = $this->state->get('filter.extension'); + + WorkflowHelper::addSubmenu('states'); + + $this->sidebar = \JHtmlSidebar::render(); + + if (!empty($this->states)) + { + $workflow = new Workflow(['extension' => 'com_content']); + + foreach ($this->states as $i => $item) + { + $item->condition = $workflow->getConditionName($item->condition); + } + } + + $this->addToolbar(); + + return parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $workflow = !empty($this->state->get('active_workflow', '')) ? $this->state->get('active_workflow', '') . ': ' : ''; + + ToolbarHelper::title(\JText::sprintf('COM_WORKFLOW_STATES_LIST', $this->escape($workflow)), 'address contact'); + + if ($canDo->get('core.create')) + { + ToolbarHelper::addNew('state.add'); + } + + if ($canDo->get('core.edit.state')) + { + ToolbarHelper::publishList('states.publish'); + ToolbarHelper::unpublishList('states.unpublish'); + ToolbarHelper::makeDefault('states.setDefault', 'COM_WORKFLOW_TOOLBAR_SET_HOME'); + } + + if ($canDo->get('core.admin')) + { + ToolbarHelper::checkin('states.checkin', 'JTOOLBAR_CHECKIN', true); + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) + { + ToolbarHelper::deleteList(\JText::_('COM_WORKFLOW_ARE_YOU_SURE'), 'states.delete'); + } + elseif ($canDo->get('core.edit.state')) + { + ToolbarHelper::trash('states.trash'); + } + + ToolbarHelper::help('JHELP_WORKFLOW_STATES_LIST'); + } + + /** + * Returns an array of fields the table can be sorted by + * + * @return array Array containing the field name to sort by as the key and display text as value + * + * @since __DEPLOY_VERSION__ + */ + protected function getSortFields() + { + return array( + 'a.published' => \JText::_('JSTATUS'), + 'a.title' => \JText::_('JGLOBAL_TITLE'), + 'a.id' => \JText::_('JGRID_HEADING_ID'), + ); + } +} diff --git a/administrator/components/com_workflow/View/Transition/HtmlView.php b/administrator/components/com_workflow/View/Transition/HtmlView.php new file mode 100644 index 0000000000000..429fcbc7ec483 --- /dev/null +++ b/administrator/components/com_workflow/View/Transition/HtmlView.php @@ -0,0 +1,170 @@ +get('Errors'))) + { + throw new \JViewGenericdataexception(implode("\n", $errors), 500); + } + + $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'); + $this->extension = $this->state->get('filter.extension'); + + // 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 __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = Factory::getUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StateHelper::getActions($this->extension, 'transition', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? \JText::_('COM_WORKFLOW_TRANSITION_ADD') : \JText::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) + { + // For new records, check the create permission. + if ($canDo->get('core.edit')) + { + $toolbarButtons = [['apply', 'transition.apply'], ['save', 'transition.save'], ['save2new', 'transition.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } + 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) + { + $toolbarButtons = [['apply', 'transition.apply'], ['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 ($canDo->get('core.create')) + { + $toolbarButtons[] = ['save2new', 'transition.save2new']; + $toolbarButtons[] = ['save2copy', 'transition.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } + + ToolbarHelper::cancel('transition.cancel'); + ToolbarHelper::divider(); + } +} diff --git a/administrator/components/com_workflow/View/Transitions/HtmlView.php b/administrator/components/com_workflow/View/Transitions/HtmlView.php new file mode 100644 index 0000000000000..bae44a7568fdb --- /dev/null +++ b/administrator/components/com_workflow/View/Transitions/HtmlView.php @@ -0,0 +1,162 @@ +get('Errors'))) + { + throw new \JViewGenericdataexception(implode("\n", $errors), 500); + } + + $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->workflowID = $this->state->get('filter.workflow_id'); + $this->extension = $this->state->get('filter.extension'); + + WorkflowHelper::addSubmenu('transitions'); + + $this->sidebar = \JHtmlSidebar::render(); + + $this->addToolbar(); + + return parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $workflow = !empty($this->state->get('active_workflow', '')) ? $this->state->get('active_workflow', '') . ': ' : ''; + + ToolbarHelper::title(\JText::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', $this->escape($workflow)), 'address contact'); + + if ($canDo->get('core.create')) + { + ToolbarHelper::addNew('transition.add'); + } + + if ($canDo->get('core.edit.state')) + { + ToolbarHelper::publishList('transitions.publish'); + ToolbarHelper::unpublishList('transitions.unpublish'); + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) + { + ToolbarHelper::deleteList(\JText::_('COM_WORKFLOW_ARE_YOU_SURE'), 'transitions.delete'); + } + elseif ($canDo->get('core.edit.state')) + { + ToolbarHelper::trash('transitions.trash'); + } + + ToolbarHelper::help('JHELP_WORKFLOW_TRANSITIONS_LIST'); + } +} diff --git a/administrator/components/com_workflow/View/Workflow/HtmlView.php b/administrator/components/com_workflow/View/Workflow/HtmlView.php new file mode 100644 index 0000000000000..0f555a71f30b9 --- /dev/null +++ b/administrator/components/com_workflow/View/Workflow/HtmlView.php @@ -0,0 +1,151 @@ +state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->extension = $this->state->get('filter.extension'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) + { + throw new \JViewGenericdataexception(implode("\n", $errors), 500); + } + + // Set the toolbar + $this->addToolBar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = Factory::getUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? \JText::_('COM_WORKFLOW_WORKFLOWS_ADD') : \JText::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) + { + // For new records, check the create permission. + if ($canDo->get('core.edit')) + { + $toolbarButtons = [['apply', 'workflow.apply'], ['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } + 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) + { + $toolbarButtons = [['apply', 'workflow.apply'], ['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']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } + + ToolbarHelper::cancel('workflow.cancel'); + } +} diff --git a/administrator/components/com_workflow/View/Workflows/HtmlView.php b/administrator/components/com_workflow/View/Workflows/HtmlView.php new file mode 100644 index 0000000000000..9642c232de32d --- /dev/null +++ b/administrator/components/com_workflow/View/Workflows/HtmlView.php @@ -0,0 +1,176 @@ +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 \JViewGenericdataexception(implode("\n", $errors), 500); + } + + $this->extension = $this->state->get('filter.extension'); + + WorkflowHelper::addSubmenu($this->state->get('filter.extension')); + $this->sidebar = \JHtmlSidebar::render(); + + $this->addToolbar(); + + return parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension); + + ToolbarHelper::title(\JText::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'address contact'); + + if ($canDo->get('core.create')) + { + ToolbarHelper::addNew('workflow.add'); + } + + if ($canDo->get('core.edit.state')) + { + ToolbarHelper::publishList('workflows.publish'); + ToolbarHelper::unpublishList('workflows.unpublish'); + ToolbarHelper::makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_SET_HOME'); + } + + if ($canDo->get('core.admin')) + { + ToolbarHelper::checkin('workflows.checkin', 'JTOOLBAR_CHECKIN', true); + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) + { + ToolbarHelper::deleteList(\JText::_('COM_WORKFLOW_ARE_YOU_SURE'), 'workflows.delete'); + } + elseif ($canDo->get('core.edit.state')) + { + ToolbarHelper::trash('workflows.trash'); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) + { + ToolbarHelper::preferences($this->extension); + } + + ToolbarHelper::help('JHELP_WORKFLOWS_LIST'); + } + + /** + * Returns an array of fields the table can be sorted by + * + * @return array Array containing the field name to sort by as the key and display text as value + * + * @since __DEPLOY_VERSION__ + */ + protected function getSortFields() + { + return array( + 'a.published' => \JText::_('JSTATUS'), + 'a.title' => \JText::_('JGLOBAL_TITLE'), + 'a.id' => \JText::_('JGRID_HEADING_ID'), + ); + } +} diff --git a/administrator/components/com_workflow/access.xml b/administrator/components/com_workflow/access.xml new file mode 100644 index 0000000000000..9d46096118d78 --- /dev/null +++ b/administrator/components/com_workflow/access.xml @@ -0,0 +1,6 @@ + + +
+ +
+
diff --git a/administrator/components/com_workflow/dispatcher.php b/administrator/components/com_workflow/dispatcher.php new file mode 100644 index 0000000000000..9086fbf177120 --- /dev/null +++ b/administrator/components/com_workflow/dispatcher.php @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/administrator/components/com_workflow/forms/filter_transitions.xml b/administrator/components/com_workflow/forms/filter_transitions.xml new file mode 100644 index 0000000000000..deaa7b1b8cc11 --- /dev/null +++ b/administrator/components/com_workflow/forms/filter_transitions.xml @@ -0,0 +1,81 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/administrator/components/com_workflow/forms/filter_workflows.xml b/administrator/components/com_workflow/forms/filter_workflows.xml new file mode 100644 index 0000000000000..c9177bee04784 --- /dev/null +++ b/administrator/components/com_workflow/forms/filter_workflows.xml @@ -0,0 +1,79 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/administrator/components/com_workflow/forms/state.xml b/administrator/components/com_workflow/forms/state.xml new file mode 100644 index 0000000000000..a1a8b93a189b3 --- /dev/null +++ b/administrator/components/com_workflow/forms/state.xml @@ -0,0 +1,87 @@ + +
+ +
+ + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ +
diff --git a/administrator/components/com_workflow/forms/transition.xml b/administrator/components/com_workflow/forms/transition.xml new file mode 100644 index 0000000000000..aa367de61a084 --- /dev/null +++ b/administrator/components/com_workflow/forms/transition.xml @@ -0,0 +1,83 @@ + +
+ +
+ + + + + + + + + + +
+ +
+ + +
+ +
+ + +
+
diff --git a/administrator/components/com_workflow/forms/workflow.xml b/administrator/components/com_workflow/forms/workflow.xml new file mode 100644 index 0000000000000..75a30a173a9d3 --- /dev/null +++ b/administrator/components/com_workflow/forms/workflow.xml @@ -0,0 +1,97 @@ + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + +
+ +
diff --git a/administrator/components/com_workflow/tmpl/state/edit.php b/administrator/components/com_workflow/tmpl/state/edit.php new file mode 100644 index 0000000000000..c8b0e6685f06e --- /dev/null +++ b/administrator/components/com_workflow/tmpl/state/edit.php @@ -0,0 +1,57 @@ + 0)); + +$app = JFactory::getApplication(); +$input = $app->input; + +// In case of modal +$isModal = $input->get('layout') == 'modal' ? true : false; +$layout = $isModal ? 'modal' : 'edit'; +$tmpl = $isModal || $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : ''; +?> + +
+ + + + 'details')); ?> + + +
+
+ form->renderField('condition'); ?> + form->getInput('description'); ?> +
+
+
+
+
+ form->renderField('published'); ?> + form->renderField('default'); ?> +
+
+
+
+
+ + + + + form->getInput('workflow_id'); ?> + + +
diff --git a/administrator/components/com_workflow/tmpl/states/default.php b/administrator/components/com_workflow/tmpl/states/default.php new file mode 100644 index 0000000000000..c4b644416761a --- /dev/null +++ b/administrator/components/com_workflow/tmpl/states/default.php @@ -0,0 +1,143 @@ +id; + +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); +$saveOrderingUrl = ''; + +$saveOrder = ($listOrder == 's.ordering'); + +if ($saveOrder) +{ + $saveOrderingUrl = 'index.php?option=com_workflow&task=states.saveOrderAjax&' . JSession::getFormToken() . '=1'; + JHtml::_('draggablelist.draggable'); +} +?> +
+
+
+ sidebar; ?> +
+
+
+ $this)); + ?> + states)) : ?> +
+ +
+ + + + + + + + + + + + + + + states as $i => $item): + $edit = JRoute::_('index.php?option=com_workflow&task=state.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); + + $canEdit = $user->authorise('core.edit', $this->extension . '.state.' . $item->id); + // @TODO set proper checkin fields + $canCheckin = true || $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || $item->checked_out == 0; + $canChange = $user->authorise('core.edit.state', $this->extension . '.state.' . $item->id) && $canCheckin; + ?> + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + id); ?> + +
+ published, $i, 'states.', $canChange); ?> +
+
+ default, $i, 'states.', $canChange); ?> + + + '; ?> + + title; ?> + + + title; ?> + + + condition); ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + + + +
+
+
diff --git a/administrator/components/com_workflow/tmpl/transition/edit.php b/administrator/components/com_workflow/tmpl/transition/edit.php new file mode 100644 index 0000000000000..5d735ae6ad51e --- /dev/null +++ b/administrator/components/com_workflow/tmpl/transition/edit.php @@ -0,0 +1,81 @@ + 0 )); + +// In case of modal +$isModal = $this->input->get('layout') == 'modal' ? true : false; +$layout = $isModal ? 'modal' : 'edit'; +$tmpl = $isModal || $this->input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : ''; + +?> + + + + + +
+ 'details')); ?> + + +
+
+
+
+ form->getLabel('from_state_id'); ?> +
+
+ form->getInput('from_state_id'); ?> +
+
+
+
+ form->getLabel('to_state_id'); ?> +
+
+ form->getInput('to_state_id'); ?> +
+
+ form->getInput('description'); ?> +
+
+
+
+
+
+
+ form->getLabel('published'); ?> +
+
+ form->getInput('published'); ?> +
+
+
+
+
+
+
+ + + + form->getInput('rules'); ?> + + + +
+ form->getInput('workflow_id'); ?> + + +
diff --git a/administrator/components/com_workflow/tmpl/transitions/default.php b/administrator/components/com_workflow/tmpl/transitions/default.php new file mode 100644 index 0000000000000..01c187724f471 --- /dev/null +++ b/administrator/components/com_workflow/tmpl/transitions/default.php @@ -0,0 +1,146 @@ +escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); +$saveOrderingUrl = ''; + +$saveOrder = ($listOrder == 't.ordering'); + +if ($saveOrder) +{ + $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&' . JSession::getFormToken() . '=1'; + JHtml::_('draggablelist.draggable'); +} +?> +
+
+
+ sidebar; ?> +
+
+
+ $this)); + ?> + transitions)) : ?> +
+ +
+ + + + + + + + + + + + + + + transitions as $i => $item): + $edit = JRoute::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); + + $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); + // @TODO set proper checkin fields + $canCheckin = true || $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || $item->checked_out == 0; + $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + id); ?> + +
+ published, $i, 'transitions.', $canChange); ?> +
+
+ + '; ?> + + title; ?> + + + title; ?> + + + from_state_id < 0) : ?> + + + from_state; ?> + + + escape($item->to_state); ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + + +
+
+
diff --git a/administrator/components/com_workflow/tmpl/workflow/edit.php b/administrator/components/com_workflow/tmpl/workflow/edit.php new file mode 100644 index 0000000000000..46f6325073089 --- /dev/null +++ b/administrator/components/com_workflow/tmpl/workflow/edit.php @@ -0,0 +1,92 @@ + 0 )); + +$app = JFactory::getApplication(); +$input = $app->input; + +// In case of modal +$isModal = $input->get('layout') == 'modal' ? true : false; +$layout = $isModal ? 'modal' : 'edit'; +$tmpl = $isModal || $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : ''; + +?> + + + + + +
+ 'general')); ?> + + +
+
+ form->getInput('description'); ?> +
+
+
+
+
+
+
+ form->getLabel('published'); ?> +
+
+ form->getInput('published'); ?> +
+
+
+
+ form->getLabel('default'); ?> +
+
+ form->getInput('default'); ?> +
+
+
+
+ form->getLabel('created'); ?> +
+
+ form->getInput('created'); ?> +
+
+
+
+ form->getLabel('modified'); ?> +
+
+ form->getInput('modified'); ?> +
+
+
+
+
+
+
+ + + + form->getInput('rules'); ?> + + + +
+ form->getInput('extension'); ?> + + +
diff --git a/administrator/components/com_workflow/tmpl/workflows/default.php b/administrator/components/com_workflow/tmpl/workflows/default.php new file mode 100644 index 0000000000000..e2f12dc5fc3dc --- /dev/null +++ b/administrator/components/com_workflow/tmpl/workflows/default.php @@ -0,0 +1,189 @@ +escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); + +$saveOrder = $listOrder == 'w.ordering'; + +$orderingColumn = 'created'; + +if (strpos($listOrder, 'modified') !== false) +{ + $orderingColumn = 'modified'; +} + +if ($saveOrder) +{ + $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component' . JSession::getFormToken() . '=1'; + JHtml::_('draggablelist.draggable'); +} + +$extension = $this->escape($this->state->get('filter.extension')); + +$user = Factory::getUser(); +$userId = $user->id; +?> +
+
+
+ sidebar; ?> +
+
+
+ $this)); + ?> + workflows)) : ?> +
+ +
+ + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + workflows as $i => $item): + $states = JRoute::_('index.php?option=com_workflow&view=states&workflow_id=' . $item->id . '&extension=' . $extension); + $transitions = JRoute::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); + $edit = JRoute::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id); + + $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); + // @TODO set proper checkin fields + $canCheckin = true || $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || $item->checked_out == 0; + $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); ?> + +
+ published, $i, 'workflows.', $canChange); ?> +
+
+ + '; ?> + + title; ?> + + + title; ?> + + + + + default, $i, 'workflows.', $canChange); ?> + + + count_states; ?> + + + count_transitions; ?> + + {$orderingColumn}; + echo $date > 0 ? JHtml::_('date', $date, JText::_('DATE_FORMAT_LC4')) : '-'; + ?> + + name) ? JText::_('COM_WORKFLOW_NA') : $item->name; ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + + +
+
+
+
diff --git a/administrator/components/com_workflow/workflow.xml b/administrator/components/com_workflow/workflow.xml new file mode 100644 index 0000000000000..b636a00aed646 --- /dev/null +++ b/administrator/components/com_workflow/workflow.xml @@ -0,0 +1,38 @@ + + + com_workflow + Joomla! Project + June 2017 + (C) 2005 - 2017 Open Source Matters. All rights reserved. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 4.0.0 + COM_WORKFLOW_XML_DESCRIPTION + Joomla\Component\Workflow + + + COM_WORKFLOW + + access.xml + config.xml + workflow.php + dispacher.php + Controller + Field + forms + Helper + helpers + Model + Table + tmpl + View + + + + \ No newline at end of file diff --git a/administrator/language/en-GB/en-GB.com_content.ini b/administrator/language/en-GB/en-GB.com_content.ini index 880ba4982b6e1..d959b8d07d14a 100644 --- a/administrator/language/en-GB/en-GB.com_content.ini +++ b/administrator/language/en-GB/en-GB.com_content.ini @@ -3,16 +3,19 @@ ; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php ; Note : All ini files need to be saved as UTF-8 + COM_CONTENT="Articles" COM_CONTENT_ADD_NEW_MENU_ITEM="New Menu Item" COM_CONTENT_ARTICLE_CONTENT="Content" COM_CONTENT_ARTICLES_TITLE="Articles" +COM_CONTENT_ARTICLE_CONTENT="Content" COM_CONTENT_ATTRIBS_ARTICLE_SETTINGS_LABEL="Options" COM_CONTENT_ATTRIBS_FIELDSET_LABEL="Options" COM_CONTENT_BATCH_OPTIONS="Batch process the selected articles" COM_CONTENT_BATCH_TIP="If a category is selected for move/copy, any actions selected will be applied to the copied or moved articles. Otherwise, all actions are applied to the selected articles." COM_CONTENT_CHANGE_ARTICLE="Select or Change article" COM_CONTENT_CHANGE_ARTICLE_BUTTON="Select/Change" +COM_CONTENT_CONFIGURATION="Articles: Options" COM_CONTENT_CONFIG_ARTICLE_SETTINGS_DESC="These settings apply for article layouts unless they are changed for a specific menu item." COM_CONTENT_CONFIG_BLOG_SETTINGS_DESC="These settings apply for blog or featured layouts unless they are changed for a specific menu item." COM_CONTENT_CONFIG_BLOG_SETTINGS_LABEL="Blog/Featured Layouts" @@ -21,7 +24,6 @@ COM_CONTENT_CONFIG_CATEGORY_SETTINGS_DESC="These settings apply for Articles Cat COM_CONTENT_CONFIG_EDITOR_LAYOUT="These options control the layout of the article editing page." COM_CONTENT_CONFIG_INTEGRATION_SETTINGS_DESC="These settings determine how the Article Component will integrate with other extensions." COM_CONTENT_CONFIG_LIST_SETTINGS_DESC="These settings apply for List Layouts Options unless they are changed for a specific menu item or category." -COM_CONTENT_CONFIGURATION="Articles: Options" COM_CONTENT_CREATE_ARTICLE_CANCEL_REDIRECT_MENU_DESC="Select the page the user will be redirected to after Canceling article submission. The default is to redirect to the same article submission page (cleaning form)." COM_CONTENT_CREATE_ARTICLE_CANCEL_REDIRECT_MENU_LABEL="Cancel Redirect" COM_CONTENT_CREATE_ARTICLE_CATEGORY_LABEL="Default Category" @@ -29,30 +31,38 @@ COM_CONTENT_CREATE_ARTICLE_CUSTOM_CANCEL_REDIRECT_DESC="If set to 'Yes', you can COM_CONTENT_CREATE_ARTICLE_CUSTOM_CANCEL_REDIRECT_LABEL="Custom Redirect on Cancel" COM_CONTENT_CREATE_ARTICLE_REDIRECTMENU_DESC="Select the page the user will be redirected to after a successful article submission and after cancel (if not set differently below). The default is to redirect to the home page." COM_CONTENT_CREATE_ARTICLE_REDIRECTMENU_LABEL="Submission/Cancel Redirect" -COM_CONTENT_EDIT_ARTICLE="Edit Article" -COM_CONTENT_EDITORCONFIG_FIELDSET_LABEL="Configure Edit Screen" COM_CONTENT_EDITING_LAYOUT="Editing Layout" +COM_CONTENT_EDITORCONFIG_FIELDSET_LABEL="Configure Edit Screen" +COM_CONTENT_EDIT_ARTICLE="Edit Article" COM_CONTENT_ERROR_ALL_LANGUAGE_ASSOCIATED="A content item set to All languages can't be associated. Associations have not been set." +COM_CONTENT_ERROR_UPDATE_STATE="Can not set state to the item" COM_CONTENT_FEATURED="Featured Article" COM_CONTENT_FEATURED_ARTICLES="Featured Articles" COM_CONTENT_FEATURED_CATEGORIES_LABEL="Select Categories" COM_CONTENT_FEATURED_ORDER="Featured Articles Order" COM_CONTENT_FEATURED_TITLE="Articles: Featured" -COM_CONTENT_FIELD_BROWSER_PAGE_TITLE_LABEL="Browser Page Title" +COM_CONTENT_FIELDSET_PUBLISHING="Publishing" +COM_CONTENT_FIELDSET_RULES="Permissions" +COM_CONTENT_FIELDSET_URLS_AND_IMAGES="Images and Links" +COM_CONTENT_FIELDS_ARTICLE_FIELDS_TITLE="Articles: Fields" +COM_CONTENT_FIELDS_ARTICLE_FIELD_ADD_TITLE="Articles: New Field" +COM_CONTENT_FIELDS_ARTICLE_FIELD_EDIT_TITLE="Articles: Edit Field" +COM_CONTENT_FIELDS_TYPE_MODAL_ARTICLE="Article" COM_CONTENT_FIELD_ARTICLETEXT_LABEL="Article Text" +COM_CONTENT_FIELD_BROWSER_PAGE_TITLE_LABEL="Browser Page Title" COM_CONTENT_FIELD_CAPTCHA_LABEL="Allow Captcha on submit" COM_CONTENT_FIELD_CREATED_BY_ALIAS_LABEL="Created by Alias" COM_CONTENT_FIELD_CREATED_BY_LABEL="Created By" COM_CONTENT_FIELD_CREATED_LABEL="Created Date" -COM_CONTENT_FIELD_FULL_LABEL="Full Article Image" COM_CONTENT_FIELD_FULLTEXT="Full text" +COM_CONTENT_FIELD_FULL_LABEL="Full Article Image" COM_CONTENT_FIELD_IMAGE_ALT_LABEL="Alt Text" COM_CONTENT_FIELD_IMAGE_CAPTION_LABEL="Caption" COM_CONTENT_FIELD_IMAGE_OPTIONS="Image Options" COM_CONTENT_FIELD_INFOBLOCK_POSITION_LABEL="Position of Article Info" COM_CONTENT_FIELD_INFOBLOCK_TITLE_LABEL="Article Info Title" -COM_CONTENT_FIELD_INTRO_LABEL="Intro Image" COM_CONTENT_FIELD_INTROTEXT="Intro Text" +COM_CONTENT_FIELD_INTRO_LABEL="Intro Image" COM_CONTENT_FIELD_OPTION_ABOVE="Above" COM_CONTENT_FIELD_OPTION_BELOW="Below" COM_CONTENT_FIELD_OPTION_SPLIT="Split" @@ -67,34 +77,32 @@ COM_CONTENT_FIELD_URLB_LABEL="Link B" COM_CONTENT_FIELD_URLB_LINK_TEXT_LABEL="Link B Text" COM_CONTENT_FIELD_URLC_LABEL="Link C" COM_CONTENT_FIELD_URLC_LINK_TEXT_LABEL="Link C Text" -COM_CONTENT_FIELD_URLS_OPTIONS="URL Options" COM_CONTENT_FIELD_URLSPOSITION_LABEL="Positioning of the Links" +COM_CONTENT_FIELD_URLS_OPTIONS="URL Options" COM_CONTENT_FIELD_VALUE_USE_ARTICLE_SETTINGS="Use Article Settings" COM_CONTENT_FIELD_VERSION_LABEL="Revision" COM_CONTENT_FIELD_XREFERENCE_DESC="An optional reference used to link to external data sources." COM_CONTENT_FIELD_XREFERENCE_LABEL="External Reference" -COM_CONTENT_FIELDS_ARTICLE_FIELDS_TITLE="Articles: Fields" -COM_CONTENT_FIELDS_ARTICLE_FIELD_ADD_TITLE="Articles: New Field" -COM_CONTENT_FIELDS_ARTICLE_FIELD_EDIT_TITLE="Articles: Edit Field" -COM_CONTENT_FIELDS_TYPE_MODAL_ARTICLE="Article" -COM_CONTENT_FIELDSET_PUBLISHING="Publishing" -COM_CONTENT_FIELDSET_RULES="Permissions" -COM_CONTENT_FIELDSET_URLS_AND_IMAGES="Images and Links" COM_CONTENT_FILTER_SEARCH_DESC="Search in title and alias. Prefix with ID: or AUTHOR: to search for an article ID or article author." COM_CONTENT_FILTER_SEARCH_LABEL="Search Articles" COM_CONTENT_FLOAT_FULLTEXT_LABEL="Full Text Image Float" -COM_CONTENT_FLOAT_LABEL="Image Float" COM_CONTENT_FLOAT_INTRO_LABEL="Intro Image Float" +COM_CONTENT_FLOAT_LABEL="Image Float" COM_CONTENT_HEADING_ASSOCIATION="Association" COM_CONTENT_HEADING_DATE_CREATED="Date Created" COM_CONTENT_HEADING_DATE_MODIFIED="Date Modified" -COM_CONTENT_HEADING_DATE_PUBLISH_UP="Start Publishing" COM_CONTENT_HEADING_DATE_PUBLISH_DOWN="Finish Publishing" +COM_CONTENT_HEADING_DATE_PUBLISH_UP="Start Publishing" COM_CONTENT_ID_LABEL="ID" COM_CONTENT_LEFT="Left" COM_CONTENT_MODIFIED_ASC="Date Modified ascending" COM_CONTENT_MODIFIED_DESC="Date Modified descending" COM_CONTENT_MONTH="Month" +COM_CONTENT_NEW_ARTICLE="New Article" +COM_CONTENT_NONE="None" +COM_CONTENT_NO_ARTICLES_LABEL="No Articles Message" +COM_CONTENT_NO_ITEM_SELECTED="Please first make a selection from the list." +COM_CONTENT_NUMBER_CATEGORY_ITEMS_LABEL="# Articles in Category" COM_CONTENT_N_ITEMS_ARCHIVED="%s articles archived." COM_CONTENT_N_ITEMS_ARCHIVED_1="%s article archived." COM_CONTENT_N_ITEMS_CHECKED_IN_1="%d article checked in." @@ -111,26 +119,26 @@ COM_CONTENT_N_ITEMS_UNFEATURED="%s articles unfeatured." COM_CONTENT_N_ITEMS_UNFEATURED_1="%s article unfeatured." COM_CONTENT_N_ITEMS_UNPUBLISHED="%s articles unpublished." COM_CONTENT_N_ITEMS_UNPUBLISHED_1="%s article unpublished." -COM_CONTENT_NEW_ARTICLE="New Article" -COM_CONTENT_NO_ARTICLES_LABEL="No Articles Message" -COM_CONTENT_NO_ITEM_SELECTED="Please first make a selection from the list." -COM_CONTENT_NONE="None" -COM_CONTENT_NUMBER_CATEGORY_ITEMS_LABEL="# Articles in Category" -COM_CONTENT_PAGE_ADD_ARTICLE="Articles: New" -COM_CONTENT_PAGE_EDIT_ARTICLE="Articles: Edit" -COM_CONTENT_PAGE_VIEW_ARTICLE="Articles: View" COM_CONTENT_PAGEBREAK_DOC_TITLE="Page Break" COM_CONTENT_PAGEBREAK_INSERT_BUTTON="Insert Page Break" COM_CONTENT_PAGEBREAK_TITLE="Page Title:" COM_CONTENT_PAGEBREAK_TOC="Table of Contents Alias:" +COM_CONTENT_PAGE_ADD_ARTICLE="Articles: New" +COM_CONTENT_PAGE_EDIT_ARTICLE="Articles: Edit" +COM_CONTENT_PAGE_VIEW_ARTICLE="Articles: View" +COM_CONTENT_PUBLISHED="Published" COM_CONTENT_PUBLISH_DOWN_ASC="Finish Publishing ascending" COM_CONTENT_PUBLISH_DOWN_DESC="Finish Publishing descending" COM_CONTENT_PUBLISH_UP_ASC="Start Publishing ascending" COM_CONTENT_PUBLISH_UP_DESC="Start Publishing descending" COM_CONTENT_RIGHT="Right" +COM_CONTENT_RUN_TRANSITIONS="Run Transitions" COM_CONTENT_SAVE_SUCCESS="Article saved." COM_CONTENT_SAVE_WARNING="Alias already existed so a number was added at the end. You can re-edit the article to customise the alias." COM_CONTENT_SELECT_AN_ARTICLE="Select an Article" +COM_CONTENT_SELECT_CONDITION="- Select Condition -" +COM_CONTENT_SELECT_STATE="- Select State -" +COM_CONTENT_SELECT_TRANSITION="- Select Transition -" COM_CONTENT_SHARED_DESC="These settings apply for Shared Options in List, Blog and Featured unless they are changed by the menu settings." COM_CONTENT_SHARED_LABEL="Shared" COM_CONTENT_SHOW_ARTICLE_OPTIONS_LABEL="Article Options" @@ -138,19 +146,31 @@ COM_CONTENT_SHOW_IMAGES_URLS_BACK_LABEL="Administrator Images and Links" COM_CONTENT_SHOW_IMAGES_URLS_FRONT_LABEL="Frontend Images and Links" COM_CONTENT_SHOW_PUBLISHING_OPTIONS_LABEL="Publishing Options" COM_CONTENT_SLIDER_EDITOR_CONFIG="Configure Edit Screen" +COM_CONTENT_STATE="State" COM_CONTENT_SUBMENU_CATEGORIES="Categories" COM_CONTENT_SUBMENU_FEATURED="Featured Articles" +COM_CONTENT_SUBMENU_WORKFLOWS="Workflows" COM_CONTENT_TIP_ASSOCIATION="Associated articles" +COM_CONTENT_TRANSITION="Status" +COM_CONTENT_TRASHED="Trashed" COM_CONTENT_UNFEATURED="Unfeatured Article" -COM_CONTENT_URL_FIELD_BROWSERNAV_LABEL="URL Target Window" +COM_CONTENT_UNPUBLISHED="Unpublished" COM_CONTENT_URL_FIELD_A_BROWSERNAV_LABEL="URL A Target Window" +COM_CONTENT_URL_FIELD_BROWSERNAV_LABEL="URL Target Window" COM_CONTENT_URL_FIELD_B_BROWSERNAV_LABEL="URL B Target Window" COM_CONTENT_URL_FIELD_C_BROWSERNAV_LABEL="URL C Target Window" COM_CONTENT_WARNING_PROVIDE_VALID_NAME="Please provide a valid, non-blank title." +COM_CONTENT_WORKFLOW="Workflow" +COM_CONTENT_WORKFLOW_DEFAULT_WORKFLOW="Use default (%s)" +COM_CONTENT_WORKFLOW_INHERIT_WORKFLOW="Inherit (%s)" +COM_CONTENT_WORKFLOW_INHERIT_WORKFLOW_NEW="Inherit" +COM_CONTENT_WORKFLOW_NOT_FOUND="No default workflow available, please define one or contact an administrator." +COM_CONTENT_WORKFLOW_TRANSITION_NOT_ALLOWED="You're not allowed to execute this transition" +COM_CONTENT_WORKFLOWS="Workflows" COM_CONTENT_XML_DESCRIPTION="Article management component." - JGLOBAL_NO_ITEM_SELECTED="No articles selected" JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE="You are not allowed to create new articles in this category." JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT="You are not allowed to edit one or more of these articles." +JLIB_APPLICATION_ERROR_BATCH_CANNOT_EXECUTE_TRANSITION="You are not allowed to execute a transition for one or more of these articles." JLIB_RULES_SETTING_NOTES="Changes apply to this component only.
Inherited - a Global Configuration setting or higher level setting is applied.
Denied always wins - whatever is set at the Global or higher level and applies to all child elements.
Allowed will enable the action for this component unless overruled by a Global Configuration setting." JLIB_RULES_SETTING_NOTES_ITEM="Changes apply to this article only.
Inherited - a Global Configuration setting or higher level setting is applied.
Denied always wins - whatever is set at the Global or higher level and applies to all child elements.
Allowed will enable the action for this component unless overruled by a Global Configuration setting." diff --git a/administrator/language/en-GB/en-GB.com_workflow.ini b/administrator/language/en-GB/en-GB.com_workflow.ini new file mode 100644 index 0000000000000..1cbdd6aac5cd6 --- /dev/null +++ b/administrator/language/en-GB/en-GB.com_workflow.ini @@ -0,0 +1,100 @@ +; Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved. +; Joomla! Project +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +COM_CONTACT_FIELD_CATEGORY_LIST_DESC="Please choose a category from the list." +COM_WORKFLOW_ARE_YOU_SURE="Are you sure?" +COM_WORKFLOW_ASC_CONDITION="Condition ascending" +COM_WORKFLOW_AUTHOR="Author" +COM_WORKFLOW_BASIC_TAB="Basic" +COM_WORKFLOW_CONDITION="Condition of items in this state: " +COM_WORKFLOW_CONDITION_DESC="Defines item behaviour." +COM_WORKFLOW_CONFIGURATION="Workflow: Options" +COM_WORKFLOW_COUNT_STATES="States" +COM_WORKFLOW_COUNT_TRANSITIONS="Transitions" +COM_WORKFLOW_CREATED_AT_ASC="Date Created ascending" +COM_WORKFLOW_CREATED_AT_DESC="Date Created descending" +COM_WORKFLOW_CREATED_DESC="Date Created" +COM_WORKFLOW_CREATED_LABEL="Date Created" +COM_WORKFLOW_DATE_CREATED="Date Created" +COM_WORKFLOW_DATE_MODIFIED="Date Modified" +COM_WORKFLOW_DEFAULT="Default" +COM_WORKFLOW_DEFAULT_ITEM="Default option is already set for a different item." +COM_WORKFLOW_DESCRIPTION="Description" +COM_WORKFLOW_DESC_CONDITION="Condition descending" +COM_WORKFLOW_DESC_TAB="Description" +COM_WORKFLOW_DISABLE_DEFAULT="Cannot change default state of this item." +COM_WORKFLOW_EDIT="Edit" +COM_WORKFLOW_EDIT_TAB="Edit" +COM_WORKFLOW_ERROR_UPDATE_STATE="Error while updating the state." +COM_WORKFLOW_FIELD_CATEGORY_LIST_LABEL="Category List" +COM_WORKFLOW_FIELD_IS_DEFAULT_DESC="If set to default, this will be saved for the component." +COM_WORKFLOW_FIELD_IS_DEFAULT_LABEL="Default" +COM_WORKFLOW_FIELD_NAME_LABEL="Name" +COM_WORKFLOW_FILTER_SEARCH_DESC="Filter the list of items." +COM_WORKFLOW_FILTER_SEARCH_LABEL="Search" +COM_WORKFLOW_FROM_STATE="From state" +COM_WORKFLOW_FROM_STATE_DESC="Select current state." +COM_WORKFLOW_ID="ID" +COM_WORKFLOW_ITEM_MUST_PUBLISHED="Item must be published to set default state." +COM_WORKFLOW_ITEM_SET_DEFAULT="Item set to default." +COM_WORKFLOW_ITEM_UNSET_DEFAULT="Default state unset." +COM_WORKFLOW_LIST_LIMIT="Output Limit" +COM_WORKFLOW_LIST_LIMIT_DESC="Limit the list of items." +COM_WORKFLOW_MANAGE="Manage" +COM_WORKFLOW_MODIFIED_AT_ASC="Date Modified ascending" +COM_WORKFLOW_MODIFIED_AT_DESC="Date Modified descending" +COM_WORKFLOW_MODIFIED_DESC="Date Modified" +COM_WORKFLOW_MODIFIED_LABEL="Date Modified" +COM_WORKFLOW_MSG_DELETE_DEFAULT="You are trying to delete the default item." +COM_WORKFLOW_MSG_DELETE_IS_ASSIGNED="This item is in use by the component." +COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR="There was a problem while deleting the item: " +COM_WORKFLOW_NA="N/A" +COM_WORKFLOW_NAME="Name" +COM_WORKFLOW_NEW="New" +COM_WORKFLOW_N_ITEMS_ARCHIVED="%s items archived." +COM_WORKFLOW_N_ITEMS_ARCHIVED_1="%s item archived." +COM_WORKFLOW_N_ITEMS_CHECKED_IN="%s items checked in." +COM_WORKFLOW_N_ITEMS_CHECKED_IN_1="%s item checked in." +COM_WORKFLOW_N_ITEMS_DELETED="%s items deleted." +COM_WORKFLOW_N_ITEMS_DELETED_1="%s item deleted." +COM_WORKFLOW_N_ITEMS_PUBLISHED="%s items published." +COM_WORKFLOW_N_ITEMS_PUBLISHED_1="%s item published." +COM_WORKFLOW_N_ITEMS_TRASHED="%s items trashed." +COM_WORKFLOW_N_ITEMS_TRASHED_1="%s item trashed." +COM_WORKFLOW_N_ITEMS_UNPUBLISHED="%s items unpublished." +COM_WORKFLOW_N_ITEMS_UNPUBLISHED_1="%s item unpublished." +COM_WORKFLOW_PARAMS_TAB="Params" +COM_WORKFLOW_PUBLISHED="Published" +COM_WORKFLOW_PUBLISHED_DESC="Defines item behaviour." +COM_WORKFLOW_PUBLISHED_LABEL="Status" +COM_WORKFLOW_RULES_TAB="Permissions" +COM_WORKFLOW_SELECT_CONDITION="- Select Condition -" +COM_WORKFLOW_SELECT_FROM_STATE="- Select Current State -" +COM_WORKFLOW_SELECT_TO_STATE="- Select Target State -" +COM_WORKFLOW_STATE="Existing States" +COM_WORKFLOW_STATES="States" +COM_WORKFLOW_STATES_LIST="%s States List" +COM_WORKFLOW_STATE_ADD="Add State" +COM_WORKFLOW_STATE_EDIT="Edit State" +COM_WORKFLOW_STATUS_DESC="Status" +COM_WORKFLOW_STATUS_LABEL="Status" +COM_WORKFLOW_TOOLBAR_SET_HOME="Default" +COM_WORKFLOW_TO_MANY_ITEMS="Too many items selected." +COM_WORKFLOW_TO_STATE="To State" +COM_WORKFLOW_TO_STATE_DESC="Select target state." +COM_WORKFLOW_TRANSITION="Transition" +COM_WORKFLOW_TRANSITIONS="Transitions" +COM_WORKFLOW_TRANSITIONS_LIST="%s Transitions List" +COM_WORKFLOW_TRANSITION_ADD="Add Transition" +COM_WORKFLOW_TRANSITION_DUPLICATE="This transition already exists." +COM_WORKFLOW_TRANSITION_EDIT="Edit Transition" +COM_WORKFLOW_TRANSITION_THE_SAME_STATE="Current state and target state are the same." +COM_WORKFLOW_TRASHED="Trashed" +COM_WORKFLOW_UNPUBLISHED="Unpublished" +COM_WORKFLOW_USER_GROUPS="User Group" +COM_WORKFLOW_USER_GROUPS_DESC="Select user group." +COM_WORKFLOW_WORKFLOWS_ADD="Add Workflow" +COM_WORKFLOW_WORKFLOWS_EDIT="Edit Workflow" +COM_WORKFLOW_WORKFLOWS_LIST="Workflows List" diff --git a/administrator/language/en-GB/en-GB.com_workflow.sys.ini b/administrator/language/en-GB/en-GB.com_workflow.sys.ini new file mode 100644 index 0000000000000..d6e59ad7bc424 --- /dev/null +++ b/administrator/language/en-GB/en-GB.com_workflow.sys.ini @@ -0,0 +1,9 @@ +; Joomla! Project +; Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +COM_WORKFLOW="Workflows" +COM_WORKFLOW_MESSAGES_VIEW_DEFAULT_DESC="Customized workflow support for Joomla! site" +COM_WORKFLOW_MESSAGES_VIEW_DEFAULT_TITLE="Workflows" +COM_WORKFLOW_XML_DESCRIPTION="Customized workflow support for Joomla! site" \ No newline at end of file diff --git a/administrator/language/en-GB/en-GB.ini b/administrator/language/en-GB/en-GB.ini index 458b09df9eae1..0d5308a54ebca 100644 --- a/administrator/language/en-GB/en-GB.ini +++ b/administrator/language/en-GB/en-GB.ini @@ -144,6 +144,7 @@ JACTION_EDIT="Edit" JACTION_EDITOWN="Edit Own" JACTION_EDITVALUE="Edit Custom Field Value" JACTION_EDITSTATE="Edit State" +JACTION_EXECUTETRANSITION="Execute transition" JACTION_LOGIN_ADMIN="Administrator Login" JACTION_LOGIN_OFFLINE="Offline Access" JACTION_LOGIN_SITE="Site Login" @@ -304,6 +305,7 @@ JGLOBAL_AUTO="Auto" JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND="Can't find the destination parent for this move." JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND="Can't find the destination row for this move." JGLOBAL_BATCH_PROCESS="Process" +JGLOBAL_BATCH_WORKFLOW_STATE_ROW_NOT_FOUND="Can't find the destination row for this state change." JGLOBAL_BLOG="Blog" JGLOBAL_BLOG_CLASS="Blog Class" JGLOBAL_BLOG_CLASS_LEADING="Blog Class (Leading Articles)" @@ -618,6 +620,7 @@ JGLOBAL_VOTES_ASC="Votes ascending" JGLOBAL_VOTES_DESC="Votes descending" JGLOBAL_WARNJAVASCRIPT="Warning! JavaScript must be enabled for proper operation of the Administrator Backend." JGLOBAL_WIDTH="Width" +JGLOBAL_WORKFLOWS_ENABLE_LABEL="Enable Workflows" JGRID_HEADING_ACCESS="Access" JGRID_HEADING_ACCESS_ASC="Access ascending" diff --git a/administrator/language/en-GB/en-GB.lib_joomla.ini b/administrator/language/en-GB/en-GB.lib_joomla.ini index 83dd7da72251e..3bba3e9e83f76 100644 --- a/administrator/language/en-GB/en-GB.lib_joomla.ini +++ b/administrator/language/en-GB/en-GB.lib_joomla.ini @@ -42,6 +42,7 @@ JLIB_APPLICATION_ERROR_MODEL_GET_NAME="JModel: :getName() : Can't get or parse c JLIB_APPLICATION_ERROR_MODULE_LOAD="Error loading module %s" JLIB_APPLICATION_ERROR_PATHWAY_LOAD="Unable to load pathway: %s" JLIB_APPLICATION_ERROR_REORDER_FAILED="Reorder failed. Error: %s" +JLIB_APPLICATION_ERROR_RUN_TRANSITION="Unable to run transition." JLIB_APPLICATION_ERROR_ROUTER_LOAD="Unable to load router: %s" JLIB_APPLICATION_ERROR_MODELCLASS_NOT_FOUND="Model class %s not found in file." JLIB_APPLICATION_ERROR_SAVE_FAILED="Save failed with the following error: %s" @@ -58,6 +59,7 @@ JLIB_APPLICATION_SUBMIT_SAVE_SUCCESS="Item submitted." JLIB_APPLICATION_SUCCESS_BATCH="Batch process completed." JLIB_APPLICATION_SUCCESS_ITEM_REORDERED="Ordering saved." JLIB_APPLICATION_SUCCESS_ORDERING_SAVED="Ordering saved." +JLIB_APPLICATION_SUCCESS_RUN_TRANSITION="New state saved." JLIB_APPLICATION_SUCCESS_LOAD_HISTORY="Prior version restored. Saved on %s %s." JLIB_LOGIN_AUTHENTICATE="Username and password do not match or you do not have an account yet." @@ -201,7 +203,9 @@ JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY="Category must have a title." JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_EXTENSION="Extension must have a title." JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_MENUITEM="Menu Item must have a title." JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_MODULE="Module must have a title." +JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE="State must have a title." JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_UPDATESITE="Update site must have a title." +JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW="Workflow must have a title." JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED="%s can't be negative." JLIB_DATABASE_ERROR_NO_ROWS_SELECTED="No rows selected." JLIB_DATABASE_ERROR_NOT_SUPPORTED_FILE_NOT_FOUND="Table %s not supported. File not found." @@ -369,6 +373,9 @@ JLIB_HTML_BATCH_USER_LABEL="Set User." JLIB_HTML_BATCH_USER_LABEL_DESC="Not making a selection will keep the original user when processing." JLIB_HTML_BATCH_USER_NOCHANGE="- Keep original User -" JLIB_HTML_BATCH_USER_NOUSER="No User." +JLIB_HTML_BATCH_WORKFLOW_STATE_LABEL="Change State" +JLIB_HTML_BATCH_WORKFLOW_STATE_LABEL_DESC="Change state of selected items" +JLIB_HTML_BATCH_WORKFLOW_STATE_NOCHANGE="- Keep original Workflow State -" JLIB_HTML_BEHAVIOR_ABOUT_THE_CALENDAR="About the Calendar" JLIB_HTML_BEHAVIOR_CLOSE="Close" JLIB_HTML_BEHAVIOR_DATE_SELECTION="Date selection:\n" diff --git a/administrator/language/en-GB/en-GB.mod_menu.ini b/administrator/language/en-GB/en-GB.mod_menu.ini index 12f671701fad8..e4f0d8e783052 100644 --- a/administrator/language/en-GB/en-GB.mod_menu.ini +++ b/administrator/language/en-GB/en-GB.mod_menu.ini @@ -9,14 +9,13 @@ MOD_MENU_COMPONENTS="Components" MOD_MENU_COM_CONTENT="Content" MOD_MENU_COM_CONTENT_ARTICLE_MANAGER="Articles" MOD_MENU_COM_CONTENT_CATEGORY_MANAGER="Categories" +MOD_MENU_COM_CONTENT_WORKFLOW_MANAGER="Workflows" MOD_MENU_COM_CONTENT_FEATURED="Featured Articles" MOD_MENU_COM_CONTENT_NEW_ARTICLE="Add New Article" MOD_MENU_COM_CONTENT_NEW_CATEGORY="Add New Category" MOD_MENU_COM_LANGUAGES_SUBMENU_CONTENT="Content Languages" MOD_MENU_COM_LANGUAGES_SUBMENU_INSTALLED="Installed" MOD_MENU_COM_LANGUAGES_SUBMENU_OVERRIDES="Overrides" - - MOD_MENU_COM_USERS="Users" MOD_MENU_COM_USERS_ADD_GROUP="Add New Group" MOD_MENU_COM_USERS_ADD_LEVEL="Add New Access Level" @@ -38,7 +37,6 @@ MOD_MENU_EXTENSIONS_LANGUAGE_MANAGER="Language(s)" MOD_MENU_EXTENSIONS_MODULE_MANAGER_SITE="Site Modules" MOD_MENU_EXTENSIONS_MODULE_MANAGER_ADMINISTRATOR="Administrator Modules" MOD_MENU_EXTENSIONS_PLUGIN_MANAGER="Plugins" - MOD_MENU_FIELD_CHECK_DESC="Check for the presence of important menu items." MOD_MENU_FIELD_CHECK_LABEL="Check Menu" MOD_MENU_FIELD_FORUMURL_LABEL="Custom Support Forum URL" @@ -64,8 +62,8 @@ MOD_MENU_TOGGLE_MENU="Toggle Menu" MOD_MENU_HELP_SUPPORT_CUSTOM_FORUM="Custom Support Forum" ; Enter in the string below the # of the specific language forum in https://forum.joomla.org/ (example: 19 for French). If left empty, it will use '511' which is the section for all languages forums. MOD_MENU_HELP_SUPPORT_OFFICIAL_LANGUAGE_FORUM_VALUE="511" -; If you have chosen to display in the string above the section for all languages, translate the string below. -; If you have displayed the specific language forum, use something like "Official French Forum" in your language. +; If you have chosen to display in the string above the section for all languages, translate the string below. +; If you have displayed the specific language forum, use something like "Official French Forum" in your language. MOD_MENU_HELP_SUPPORT_OFFICIAL_LANGUAGE_FORUM="Official Language Forums" MOD_MENU_HELP_TRANSLATIONS="Joomla! Translations" MOD_MENU_HELP_XCHANGE="Stack Exchange" @@ -76,9 +74,6 @@ MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER="Module Manager" MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER="Components Container" MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING="The administrator menu %1$s does not have - %2$s. Select to turn on the menu recovery mode." MOD_MENU_INSTALLER_SUBMENU_DATABASE="Database" - - - MOD_MENU_INSTALLER_SUBMENU_MANAGE="Manage" MOD_MENU_INSTALLER_SUBMENU_UPDATE="Update" MOD_MENU_INSTALLER_SUBMENU_UPDATESITES="Update Sites" @@ -97,11 +92,9 @@ MOD_MENU_RECOVERY_EXIT="Exit Recovery Mode" MOD_MENU_RECOVERY_MENU_ROOT="Menu Recovery" MOD_MENU_SETTINGS="Settings" MOD_MENU_SYSTEM_INFORMATION="System Information" - - MOD_MENU_USER_PROFILE="My Profile" MOD_MENU_XML_DESCRIPTION="This module displays an administrator menu module." - +MOD_MENU_COM_CONTENT_WORKFLOW="Workflows" MOD_MENU_SYSTEM="System" MOD_MENU_INSTALL="Install" MOD_MENU_TEMPLATES="Templates" @@ -110,21 +103,17 @@ MOD_MENU_MAINTAIN="Maintain" MOD_MENU_MANAGE="Manage" MOD_MENU_INFORMATION="Information" MOD_MENU_UPDATE="Update" - MOD_MENU_INSTALL_EXTENSIONS="Extensions" MOD_MENU_INSTALL_LANGUAGES="Install Languages" MOD_MENU_INSTALL_DISCOVER="Discover" - MOD_MENU_TEMPLATE_SITE_TEMPLATES="Site Templates" MOD_MENU_TEMPLATE_SITE_STYLES="Site Template Styles" MOD_MENU_TEMPLATE_ADMIN_STYLES="Administrator Template Styles" MOD_MENU_TEMPLATE_ADMIN_TEMPLATES="Administrator Templates" - MOD_MENU_ACCESS_GROUPS="Groups" MOD_MENU_ACCESS_LEVELS="Levels" MOD_MENU_ACCESS_SETTINGS="Settings" MOD_MENU_ACCESS_TEXT_FILTERS="Text Filters" - MOD_MENU_MANAGE_CSP="CSP" MOD_MENU_MANAGE_EXTENSIONS="Extensions" MOD_MENU_MANAGE_LANGUAGES="Language(s)" @@ -132,12 +121,10 @@ MOD_MENU_MANAGE_LANGUAGES_CONTENT="Content Language(s)" MOD_MENU_MANAGE_LANGUAGES_OVERRIDES="Language Overrides" MOD_MENU_MANAGE_PLUGINS="Plugins" MOD_MENU_MANAGE_REDIRECTS="Redirects" - MOD_MENU_INFORMATION_WARNINGS="Warnings" MOD_MENU_INFORMATION_POST_INSTALL_MESSAGES="Installation Messages" MOD_MENU_SYSTEM_INFORMATION_SYSINFO="System Information" MOD_MENU_SYSTEM_INFORMATION_DATABASE="Database" - MOD_MENU_UPDATE_JOOMLA="Joomla" MOD_MENU_UPDATE_EXTENSIONS="Extensions" MOD_MENU_UPDATE_SOURCES="Update Sources" diff --git a/administrator/language/en-GB/en-GB.plg_content_joomla.ini b/administrator/language/en-GB/en-GB.plg_content_joomla.ini index 6ef4aba23a195..1224045655753 100644 --- a/administrator/language/en-GB/en-GB.plg_content_joomla.ini +++ b/administrator/language/en-GB/en-GB.plg_content_joomla.ini @@ -6,6 +6,12 @@ PLG_CONTENT_JOOMLA="Content - Joomla" PLG_CONTENT_JOOMLA_FIELD_CHECK_CATEGORIES_DESC="Check that categories are fully empty before they are deleted." PLG_CONTENT_JOOMLA_FIELD_CHECK_CATEGORIES_LABEL="Check Category Deletion" +PLG_CONTENT_JOOMLA_FIELD_CHECK_STATES_DESC="Check that states are not assigned to an item before they are deleted." +PLG_CONTENT_JOOMLA_FIELD_CHECK_STATES_LABEL="Check States Deletion" PLG_CONTENT_JOOMLA_FIELD_EMAIL_NEW_FE_DESC="Email users if 'Send email' is on when there is a new article submitted via the Frontend." PLG_CONTENT_JOOMLA_FIELD_EMAIL_NEW_FE_LABEL="Email on New Site Article" -PLG_CONTENT_JOOMLA_XML_DESCRIPTION="This plugin does category processing for core extensions; sends an email when new article is submitted in the Frontend." \ No newline at end of file +PLG_CONTENT_JOOMLA_FIELD_EMAIL_NEW_STATE_DESC="Email users if 'Send email' is on when there is a status change of an article." +PLG_CONTENT_JOOMLA_FIELD_EMAIL_NEW_STATE_LABEL="Email on transition execution" +PLG_CONTENT_JOOMLA_ON_STATE_CHANGE_MSG="The status of an article has been changed by '%1$s' entitled '%2$s'." +PLG_CONTENT_JOOMLA_ON_STATE_CHANGE_SUBJECT="Status of article has changed" +PLG_CONTENT_JOOMLA_XML_DESCRIPTION="This plugin does category processing for core extensions; sends an email when new article is submitted in the Frontend or a transition is executed." diff --git a/administrator/modules/mod_latest/tmpl/default.php b/administrator/modules/mod_latest/tmpl/default.php index 9af55c31f690a..7093ec06b5173 100644 --- a/administrator/modules/mod_latest/tmpl/default.php +++ b/administrator/modules/mod_latest/tmpl/default.php @@ -42,7 +42,6 @@ author_name; ?> - diff --git a/administrator/modules/mod_menu/Menu/CssMenu.php b/administrator/modules/mod_menu/Menu/CssMenu.php index 979640f4e7e2e..9795ae78da751 100644 --- a/administrator/modules/mod_menu/Menu/CssMenu.php +++ b/administrator/modules/mod_menu/Menu/CssMenu.php @@ -319,6 +319,25 @@ protected function preprocess($items) list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); } + elseif ($item->element === 'com_workflow') + { + parse_str($item->link, $query); + + // Only display Fields menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) + { + $workflow = ComponentHelper::getParams($query['extension'])->get('workflows_enable', 1); + } + + if (!$workflow) + { + continue; + } + + list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } elseif ($item->element === 'com_config' && !$user->authorise('core.admin')) { continue; diff --git a/build/media/system/js/core.js b/build/media/system/js/core.js new file mode 100644 index 0000000000000..ca293537011c9 --- /dev/null +++ b/build/media/system/js/core.js @@ -0,0 +1,1312 @@ +/** + * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Only define the Joomla namespace if not defined. +Joomla = window.Joomla || {}; + +// Only define editors if not defined +Joomla.editors = Joomla.editors || {}; + +// An object to hold each editor instance on page, only define if not defined. +Joomla.editors.instances = Joomla.editors.instances || { + /** + * ***************************************************************** + * All Editors MUST register, per instance, the following callbacks: + * ***************************************************************** + * + * getValue Type Function Should return the complete data from the editor + * Example: function () { return this.element.value; } + * setValue Type Function Should replace the complete data of the editor + * Example: function (text) { return this.element.value = text; } + * replaceSelection Type Function Should replace the selected text of the editor + * If nothing selected, will insert the data at the cursor + * Example: function (text) { return insertAtCursor(this.element, text); } + * + * USAGE (assuming that jform_articletext is the textarea id) + * { + * To get the current editor value: + * Joomla.editors.instances['jform_articletext'].getValue(); + * To set the current editor value: + * Joomla.editors.instances['jform_articletext'].setValue('Joomla! rocks'); + * To replace(selection) or insert a value at the current editor cursor: + * replaceSelection: Joomla.editors.instances['jform_articletext'].replaceSelection('Joomla! rocks') + * } + * + * ********************************************************* + * ANY INTERACTION WITH THE EDITORS SHOULD USE THE ABOVE API + * ********************************************************* + * + * jInsertEditorText() @deprecated 4.0 + */ +}; + +Joomla.Modal = { + /** + * ***************************************************************** + * Modals should implement + * ***************************************************************** + * + * getCurrent Type Function Should return the modal element + * setCurrent Type Function Should set the modal element + * current Type {node} The modal element + * + * USAGE (assuming that exampleId is the modal id) + * To get the current modal element: + * Joomla.Modal.current; // Returns node element, eg: document.getElementById('exampleId') + * To set the current modal element: + * Joomla.Modal.setCurrent(document.getElementById('exampleId')); + * + * ************************************************************* + * Joomla's UI modal uses `element.close();` to close the modal + * and `element.open();` to open the modal + * If you are using another modal make sure the same + * functionality is bound to the modal element + * @see media/legacy/bootstrap.init.js + * ************************************************************* + */ + current: '', + setCurrent: function(element) { + this.current = element; + }, + getCurrent: function() { + return this.current; + }, +}; + +(function( Joomla, document ) { + "use strict"; + + /** + * Generic submit form + * + * @param {String} task The given task + * @param {node} form The form element + * @param {bool} validate The form element + * + * @returns {void} + */ + Joomla.submitform = function(task, form, validate) { + + if (!form) { + form = document.getElementById('adminForm'); + } + + if (task) { + form.task.value = task; + } + + // Toggle HTML5 validation + form.noValidate = !validate; + + if (!validate) { + form.setAttribute('novalidate', ''); + } else if (form.hasAttribute('novalidate')) { + form.removeAttribute('novalidate'); + } + + // Submit the form. + // Create the input type="submit" + var button = document.createElement('input'); + button.style.display = 'none'; + button.type = 'submit'; + + // Append it and click it + form.appendChild(button).click(); + + // If "submit" was prevented, make sure we don't get a build up of buttons + form.removeChild(button); + }; + + /** + * Default function. Can be overriden by the component to add custom logic + * + * @param {String} task The given task + * @param {String} formSelector The form selector eg '#adminForm' + * @param {bool} validate The form element + * + * @returns {void} + */ + Joomla.submitbutton = function( task, formSelector, validate ) { + var form = document.querySelector( formSelector || 'form.form-validate' ); + + if (form) { + + if (validate === undefined || validate === null) { + var pressbutton = task.split('.'), + cancelTask = form.getAttribute('data-cancel-task'); + + if (!cancelTask) { + cancelTask = pressbutton[0] + '.cancel'; + } + + validate = task !== cancelTask; + } + + if (!validate || document.formvalidator.isValid( form )) { + Joomla.submitform( task, form ); + } + + } else { + Joomla.submitform( task ); + } + }; + + /** + * Custom behavior for JavaScript I18N in Joomla! 1.6 + * + * @type {{}} + * + * Allows you to call Joomla.JText._() to get a translated JavaScript string pushed in with JText::script() in Joomla. + */ + Joomla.JText = { + strings: {}, + + /** + * Translates a string into the current language. + * + * @param {String} key The string to translate + * @param {String} def Default string + * + * @returns {String} + */ + '_': function( key, def ) { + + // Check for new strings in the optionsStorage, and load them + var newStrings = Joomla.getOptions('joomla.jtext'); + if ( newStrings ) { + this.load(newStrings); + + // Clean up the optionsStorage from useless data + Joomla.loadOptions({'joomla.jtext': null}); + } + + def = def === undefined ? '' : def; + key = key.toUpperCase(); + + return this.strings[ key ] !== undefined ? this.strings[ key ] : def; + }, + + /** + * Load new strings in to Joomla.JText + * + * @param {Object} object Object with new strings + * @returns {Joomla.JText} + */ + load: function( object ) { + for ( var key in object ) { + if (!object.hasOwnProperty(key)) continue; + this.strings[ key.toUpperCase() ] = object[ key ]; + } + + return this; + } + }; + + /** + * Joomla options storage + * + * @type {{}} + * + * @since 3.7.0 + */ + Joomla.optionsStorage = Joomla.optionsStorage || null; + + /** + * Get script(s) options + * + * @param {String} key Name in Storage + * @param {mixed} def Default value if nothing found + * + * @return {mixed} + * + * @since 3.7.0 + */ + Joomla.getOptions = function( key, def ) { + // Load options if they not exists + if (!Joomla.optionsStorage) { + Joomla.loadOptions(); + } + + return Joomla.optionsStorage[key] !== undefined ? Joomla.optionsStorage[key] : def; + }; + + /** + * Load new options from given options object or from Element + * + * @param {Object|undefined} options The options object to load. Eg {"com_foobar" : {"option1": 1, "option2": 2}} + * + * @since 3.7.0 + */ + Joomla.loadOptions = function( options ) { + // Load form the script container + if (!options) { + var elements = document.querySelectorAll('.joomla-script-options.new'), + str, element, option, counter = 0; + + for (var i = 0, l = elements.length; i < l; i++) { + element = elements[i]; + str = element.text || element.textContent; + option = JSON.parse(str); + + if (option) { + Joomla.loadOptions(option); + counter++; + } + + element.className = element.className.replace(' new', ' loaded'); + } + + if (counter) { + return; + } + } + + // Initial loading + if (!Joomla.optionsStorage) { + Joomla.optionsStorage = options || {}; + } + // Merge with existing + else if ( options ) { + for (var p in options) { + if (options.hasOwnProperty(p)) { + /** + * If both existing and new options are objects, merge them with Joomla.extend(). But test for new + * option being null, as null is an object, but we want to allow clearing of options with ... + * + * Joomla.loadOptions({'joomla.jtext': null}); + */ + if (options[p] !== null && typeof Joomla.optionsStorage[p] === 'object' && typeof options[p] === 'object') { + Joomla.optionsStorage[p] = Joomla.extend(Joomla.optionsStorage[p], options[p]); + } else { + Joomla.optionsStorage[p] = options[p]; + } + } + } + } + }; + + /** + * Method to replace all request tokens on the page with a new one. + * + * @param {String} newToken The token + * + * Used in Joomla Installation + */ + Joomla.replaceTokens = function( newToken ) { + if (!/^[0-9A-F]{32}$/i.test(newToken)) { return; } + + var els = document.getElementsByTagName( 'input' ), + i, el, n; + + for ( i = 0, n = els.length; i < n; i++ ) { + el = els[i]; + + if ( el.type == 'hidden' && el.value == '1' && el.name.length == 32 ) { + el.name = newToken; + } + } + }; + + /** + * USED IN: all list forms. + * + * Toggles the check state of a group of boxes + * + * Checkboxes must have an id attribute in the form cb0, cb1... + * + * @param {mixed} checkbox The number of box to 'check', for a checkbox element + * @param {string} stub An alternative field name + * + * @return {boolean} + */ + Joomla.checkAll = function( checkbox, stub ) { + if (!checkbox.form) return false; + + stub = stub ? stub : 'cb'; + + var c = 0, + i, e, n; + + for ( i = 0, n = checkbox.form.elements.length; i < n; i++ ) { + e = checkbox.form.elements[ i ]; + + if ( e.type == checkbox.type && e.id.indexOf( stub ) === 0 ) { + e.checked = checkbox.checked; + c += e.checked ? 1 : 0; + } + } + + if ( checkbox.form.boxchecked ) { + checkbox.form.boxchecked.value = c; + Joomla.Event.dispatch(checkbox.form.boxchecked, 'change'); + } + + return true; + }; + + /** + * Toggles the check state of a group of boxes + * + * Checkboxes must have an id attribute in the form cb0, cb1... + * + * @param {node} item The form + * @param {string} stub An alternative field name + * + * @return {boolean} + */ + Joomla.uncheckAll = function( item, stub ) { + if (!item.form) return false; + + stub = stub ? stub : 'cb'; + + var c = 0, + i, e, n; + + for ( i = 0, n = item.form.elements.length; i < n; i++ ) { + e = item.form.elements[ i ]; + + if ( e.type === 'checkbox' && e.id.indexOf( stub ) === 0 ) { + e.checked = false; + } + } + + if ( item.form.boxchecked ) { + item.form.boxchecked.value = c; + } + + return true; + }; + + /** + * Toggles the check state of a group of boxes + * + * Checkboxes must have an id attribute in the form cb0, cb1... + * + * @param {node} el The form item + * @param {bool} cond An alternative value to set checkbox + * + * @return {boolean} + */ + Joomla.toggleOne = function( el, cond ) { + if (!el.form) return false; + + var item = el; + + while (item = item.parentNode) { + if (item.tagName.toLowerCase() === 'tr') { + break; + } + } + + var checkbox = item.querySelector('input[name="cid[]"]'); + + if (checkbox) { + checkbox.checked = cond ? cond : !checkbox.checked; + if (checkbox.checked) { + cond = checkbox.checked; + } + } + + if ( el.form.boxchecked && cond) { + el.form.boxchecked.value = parseInt(el.form.boxchecked.value) + 1; + } + + return true; + }; + + /** + * Render messages send via JSON + * Used by some javascripts such as validate.js + * + * @param {object} messages JavaScript object containing the messages to render. Example: + * var messages = { + * "message": ["Message one", "Message two"], + * "error": ["Error one", "Error two"] + * }; + * @param {string} selector The selector of the container where the message will be rendered + * @param {bool} keepOld If we shall discard old messages + * @param {int} timeout The milliseconds before the message self destruct + * @return void + */ + Joomla.renderMessages = function( messages, selector, keepOld, timeout ) { + var messageContainer, type, typeMessages, messagesBox, title, titleWrapper, i, messageWrapper, alertClass; + + if (typeof selector === 'undefined' || selector && selector === '#system-message-container') { + messageContainer = document.getElementById( 'system-message-container' ); + } else { + messageContainer = document.querySelector( selector ); + } + + if (typeof keepOld === 'undefined' || keepOld && keepOld === false) { + Joomla.removeMessages( messageContainer ); + } + + for ( type in messages ) { + if ( !messages.hasOwnProperty( type ) ) { continue; } + // Array of messages of this type + typeMessages = messages[ type ]; + + if (typeof window.customElements === 'object' && typeof window.customElements.get('joomla-alert') === 'function') { + messagesBox = document.createElement( 'joomla-alert' ); + + if (['notice','message', 'error'].indexOf(type) > -1) { + alertClass = (type === 'notice') ? 'info' : type; + alertClass = (type === 'message') ? 'success' : alertClass; + alertClass = (type === 'error') ? 'danger' : alertClass; + } else { + alertClass = 'info'; + } + + messagesBox.setAttribute('type', alertClass); + messagesBox.setAttribute('dismiss', 'true'); + + if (timeout && parseInt(timeout) > 0) { + messagesBox.setAttribute('autodismiss', timeout); + } + } else { + // Create the alert box + messagesBox = document.createElement( 'div' ); + + // Message class + if (['notice','message', 'error'].indexOf(type) > -1) { + alertClass = (type === 'notice') ? 'info' : type; + alertClass = (type === 'message') ? 'success' : alertClass; + alertClass = (type === 'error') ? 'danger' : alertClass; + } else { + alertClass = 'info'; + } + + messagesBox.className = 'alert ' + alertClass; + + // Close button + var buttonWrapper = document.createElement( 'button' ); + buttonWrapper.setAttribute('type', 'button'); + buttonWrapper.setAttribute('data-dismiss', 'alert'); + buttonWrapper.className = 'close'; + buttonWrapper.innerHTML = '×'; + messagesBox.appendChild( buttonWrapper ); + } + + // Title + title = Joomla.JText._( type ); + + // Skip titles with untranslated strings + if ( typeof title != 'undefined' ) { + titleWrapper = document.createElement( 'h4' ); + titleWrapper.className = 'alert-heading'; + titleWrapper.innerHTML = Joomla.JText._( type ) ? Joomla.JText._( type ) : type; + messagesBox.appendChild( titleWrapper ); + } + + // Add messages to the message box + for ( i = typeMessages.length - 1; i >= 0; i-- ) { + messageWrapper = document.createElement( 'div' ); + messageWrapper.innerHTML = typeMessages[ i ]; + messagesBox.appendChild( messageWrapper ); + } + + messageContainer.appendChild( messagesBox ); + + if (typeof window.customElements !== 'object' && typeof window.customElements.get('joomla-alert') !== 'function') { + if (timeout && parseInt(timeout) > 0) { + setTimeout(function () { + Joomla.removeMessages(messageContainer); + }, timeout); + } + } + } + }; + + + /** + * Remove messages + * + * @param {element} container The element of the container of the message to be removed + * + * @return {void} + */ + Joomla.removeMessages = function( container ) { + var messageContainer; + + if (container) { + messageContainer = container; + } else { + messageContainer = document.getElementById( 'system-message-container' ); + } + + if (typeof window.customElements === 'object' && window.customElements.get('joomla-alert')) { + var messages = messageContainer.querySelectorAll('joomla-alert'); + if (messages.length) { + for (var i = 0, l = messages.length; i < l; i++) { + messages[i].close(); + } + } + } else { + // Empty container with a while for Chrome performance issues + while ( messageContainer.firstChild ) messageContainer.removeChild( messageContainer.firstChild ); + + // Fix Chrome bug not updating element height + messageContainer.style.display = 'none'; + messageContainer.offsetHeight; + messageContainer.style.display = ''; + } + }; + + /** + * Treat AJAX errors. + * Used by some javascripts such as sendtestmail.js and permissions.js + * + * @param {object} xhr XHR object. + * @param {string} textStatus Type of error that occurred. + * @param {string} error Textual portion of the HTTP status. + * + * @return {object} JavaScript object containing the system error message. + * + * @since 3.6.0 + */ + Joomla.ajaxErrorsMessages = function( xhr, textStatus, error ) { + var msg = {}; + + // For jQuery jqXHR + if (textStatus === 'parsererror') + { + // Html entity encode. + var encodedJson = xhr.responseText.trim(); + + var buf = []; + for (var i = encodedJson.length-1; i >= 0; i--) { + buf.unshift( [ '&#', encodedJson[i].charCodeAt(), ';' ].join('') ); + } + + encodedJson = buf.join(''); + + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_PARSE').replace('%s', encodedJson) ]; + } + else if (textStatus === 'nocontent') + { + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_NO_CONTENT') ]; + } + else if (textStatus === 'timeout') + { + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_TIMEOUT') ]; + } + else if (textStatus === 'abort') + { + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_CONNECTION_ABORT') ]; + } + // For vannila XHR + else if (xhr.responseJSON && xhr.responseJSON.message) + { + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_OTHER').replace('%s', xhr.status) + ' ' + xhr.responseJSON.message + '' ]; + } + else if (xhr.statusText) + { + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_OTHER').replace('%s', xhr.status) + ' ' + xhr.statusText + '' ]; + } + else + { + msg.error = [ Joomla.JText._('JLIB_JS_AJAX_ERROR_OTHER').replace('%s', xhr.status) ]; + } + + return msg; + }; + + /** + * USED IN: administrator/components/com_cache/views/cache/tmpl/default.php + * administrator/components/com_installer/views/discover/tmpl/default_item.php + * administrator/components/com_installer/views/update/tmpl/default_item.php + * administrator/components/com_languages/helpers/html/languages.php + * libraries/joomla/html/html/grid.php + * + * @param {boolean} isitchecked Flag for checked + * @param {node} form The form + * + * @return {void} + */ + Joomla.isChecked = function( isitchecked, form ) { + if ( typeof form === 'undefined' ) { + form = document.getElementById( 'adminForm' ); + } + + form.boxchecked.value = isitchecked ? parseInt(form.boxchecked.value) + 1 : parseInt(form.boxchecked.value) - 1; + + Joomla.Event.dispatch(form.boxchecked, 'change'); + + // If we don't have a checkall-toggle, done. + if ( !form.elements[ 'checkall-toggle' ] ) return; + + // Toggle main toggle checkbox depending on checkbox selection + var c = true, + i, e, n; + + for ( i = 0, n = form.elements.length; i < n; i++ ) { + e = form.elements[ i ]; + + if ( e.type == 'checkbox' && e.name != 'checkall-toggle' && !e.checked ) { + c = false; + break; + } + } + + form.elements[ 'checkall-toggle' ].checked = c; + }; + + /** + * USED IN: libraries/joomla/html/html/grid.php + * In other words, on any reorderable table + * + * @param {string} order The order value + * @param {string} dir The direction + * @param {string} task The task + * @param {node} form The form + * + * return {void} + */ + Joomla.tableOrdering = function( order, dir, task, form ) { + if ( typeof form === 'undefined' ) { + form = document.getElementById( 'adminForm' ); + } + + form.filter_order.value = order; + form.filter_order_Dir.value = dir; + Joomla.submitform( task, form ); + }; + + /** + * USED IN: administrator/components/com_users/views/mail/tmpl/default.php + * Let's get rid of this and kill it + * + * @param frmName + * @param srcListName + * @return + * + * @deprecated 4.0 No replacement + */ + window.getSelectedValue = function ( frmName, srcListName ) { + var srcList = document[ frmName ][ srcListName ], + i = srcList.selectedIndex; + + if ( i !== null && i > -1 ) { + return srcList.options[ i ].value; + } else { + return null; + } + }; + + /** + * USED IN: all over :) + * + * @param id + * @param task + * @return + * + * @deprecated 4.0 Use Joomla.listItemTask() instead + */ + window.listItemTask = function ( id, task ) { + return Joomla.listItemTask( id, task ); + }; + + /** + * USED IN: all over :) + * + * @param {string} id The id + * @param {string} task The task + * + * @return {boolean} + */ + Joomla.listItemTask = function ( id, task ) { + var f = document.adminForm, + i = 0, cbx, + cb = f[ id ]; + + if ( !cb ) return false; + + while ( true ) { + cbx = f[ 'cb' + i ]; + + if ( !cbx ) break; + + cbx.checked = false; + + i++; + } + + cb.checked = true; + f.boxchecked.value = 1; + window.submitform( task ); + + return false; + }; + + /** + * Default function. Usually would be overriden by the component + * + * @deprecated 4.0 Use Joomla.submitbutton() instead. + */ + window.submitbutton = function ( pressbutton ) { + Joomla.submitbutton( pressbutton ); + }; + + /** + * Submit the admin form + * + * @deprecated 4.0 Use Joomla.submitform() instead. + */ + window.submitform = function ( pressbutton ) { + Joomla.submitform(pressbutton); + }; + + // needed for Table Column ordering + /** + * USED IN: libraries/joomla/html/html/grid.php + * There's a better way to do this now, can we try to kill it? + * + * @deprecated 4.0 No replacement + */ + window.saveorder = function ( n, task ) { + window.checkAll_button( n, task ); + }; + + /** + * Checks all the boxes unless one is missing then it assumes it's checked out. + * Weird. Probably only used by ^saveorder + * + * @param {int} n The total number of checkboxes expected + * @param {string} task The task to perform + * + * @return void + * + * @deprecated 4.0 No replacement + */ + window.checkAll_button = function ( n, task ) { + task = task ? task : 'saveorder'; + + var j, box; + + for ( j = 0; j <= n; j++ ) { + box = document.adminForm[ 'cb' + j ]; + + if ( box ) { + box.checked = true; + } else { + alert( "You cannot change the order of items, as an item in the list is `Checked Out`" ); + return; + } + } + + Joomla.submitform( task ); + }; + + /** + * Add Joomla! loading image layer. + * + * Used in: /administrator/components/com_installer/views/languages/tmpl/default.php + * /installation/template/js/installation.js + * + * @param {String} task The task to do [load, show, hide] (defaults to show). + * @param {HTMLElement} parentElement The HTML element where we are appending the layer (defaults to body). + * + * @return {HTMLElement} The HTML loading layer element. + * + * @since 3.6.0 + */ + Joomla.loadingLayer = function(task, parentElement) { + // Set default values. + task = task || 'show'; + parentElement = parentElement || document.body; + + // Create the loading layer (hidden by default). + if (task === 'load') + { + // Gets the site base path + var systemPaths = Joomla.getOptions('system.paths') || {}, + basePath = systemPaths.root || ''; + + var loadingDiv = document.createElement('div'); + + loadingDiv.id = 'loading-logo'; + + // The loading layer CSS styles are JS hardcoded so they can be used without adding CSS. + + // Loading layer style and positioning. + loadingDiv.style['position'] = 'fixed'; + loadingDiv.style['top'] = '0'; + loadingDiv.style['left'] = '0'; + loadingDiv.style['width'] = '100%'; + loadingDiv.style['height'] = '100%'; + loadingDiv.style['opacity'] = '0.8'; + loadingDiv.style['filter'] = 'alpha(opacity=80)'; + loadingDiv.style['overflow'] = 'hidden'; + loadingDiv.style['z-index'] = '10000'; + loadingDiv.style['display'] = 'none'; + loadingDiv.style['background-color'] = '#fff'; + + // Loading logo positioning. + loadingDiv.style['background-image'] = 'url("' + basePath + '/media/system/images/ajax-loader.gif")'; + loadingDiv.style['background-position'] = 'center'; + loadingDiv.style['background-repeat'] = 'no-repeat'; + loadingDiv.style['background-attachment'] = 'fixed'; + + parentElement.appendChild(loadingDiv); + } + // Show or hide the layer. + else + { + if (!document.getElementById('loading-logo')) + { + Joomla.loadingLayer('load', parentElement); + } + + document.getElementById('loading-logo').style['display'] = (task == 'show') ? 'block' : 'none'; + } + + return document.getElementById('loading-logo'); + }; + + /** + * Method to Extend Objects + * + * @param {Object} destination + * @param {Object} source + * + * @return Object + */ + Joomla.extend = function (destination, source) { + /** + * Technically null is an object, but trying to treat the destination as one in this context will error out. + * So emulate jQuery.extend(), and treat a destination null as an empty object. + */ + if (destination === null) { + destination = {}; + } + for (var p in source) { + if (source.hasOwnProperty(p)) { + destination[p] = source[p]; + } + } + + return destination; + }; + + /** + * Method to perform AJAX request + * + * @param {Object} options Request options: + * { + * url: 'index.php', // Request URL + * method: 'GET', // Request method GET (default), POST + * data: null, // Data to be sent, see https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/send + * perform: true, // Perform the request immediately, or return XMLHttpRequest instance and perform it later + * headers: null, // Object of custom headers, eg {'X-Foo': 'Bar', 'X-Bar': 'Foo'} + * + * onBefore: function(xhr){} // Callback on before the request + * onSuccess: function(response, xhr){}, // Callback on the request success + * onError: function(xhr){}, // Callback on the request error + * } + * + * @return XMLHttpRequest|Boolean + * + * @example + * + * Joomla.request({ + * url: 'index.php?option=com_example&view=example', + * onSuccess: function(response, xhr){ + * console.log(response); + * } + * }) + * + * @see https://developer.mozilla.org/docs/Web/API/XMLHttpRequest + */ + Joomla.request = function (options) { + + // Prepare the options + options = Joomla.extend({ + url: '', + method: 'GET', + data: null, + perform: true + }, options); + + // Set up XMLHttpRequest instance + try{ + var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('MSXML2.XMLHTTP.3.0'); + + xhr.open(options.method, options.url, true); + + // Set the headers + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('X-Ajax-Engine', 'Joomla!'); + + if (options.method !== 'GET') { + var token = Joomla.getOptions('csrf.token', ''); + + if (token) { + xhr.setRequestHeader('X-CSRF-Token', token); + } + + if (!options.headers || !options.headers['Content-Type']) { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + } + + // Custom headers + if (options.headers){ + for (var p in options.headers) { + if (options.headers.hasOwnProperty(p)) { + xhr.setRequestHeader(p, options.headers[p]); + } + } + } + + xhr.onreadystatechange = function () { + // Request not finished + if (xhr.readyState !== 4) return; + + // Request finished and response is ready + if (xhr.status === 200) { + if(options.onSuccess) { + options.onSuccess.call(window, xhr.responseText, xhr); + } + } else if(options.onError) { + options.onError.call(window, xhr); + } + }; + + // Do request + if (options.perform) { + if (options.onBefore && options.onBefore.call(window, xhr) === false) { + // Request interrupted + return xhr; + } + + xhr.send(options.data); + } + + } catch (error) { + window.console ? console.log(error) : null; + return false; + } + + return xhr; + }; + + /** + * Check if HTML5 localStorage enabled on the browser + * + * @since 4.0.0 + */ + Joomla.localStorageEnabled = function() { + var test = 'joomla-cms'; + try { + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch(e) { + return false; + } + }; + + /** + * Loads any needed polyfill for web components and async load any web components + * + * Parts of the WebComponents method belong to The Polymer Project Authors. License http://polymer.github.io/LICENSE.txt + * + * @since 4.0.0 + */ + Joomla.WebComponents = function() { + var wc = Joomla.getOptions('webcomponents'); + + // Return early + if (!wc || !wc.length) { + return; + } + + var polyfillsLoaded = false; + var whenLoadedFns = []; + var allowUpgrades = false; + var flushFn; + + var fireEvent = function() { + window.WebComponents.ready = true; + document.dispatchEvent(new CustomEvent('WebComponentsReady', { bubbles: true })); + loadWC(); + }; + + var batchCustomElements = function() { + if (window.customElements && customElements.polyfillWrapFlushCallback) { + customElements.polyfillWrapFlushCallback(function (flushCallback) { + flushFn = flushCallback; + if (allowUpgrades) { + flushFn(); + } + }); + } + }; + + var asyncReady = function() { + batchCustomElements(); + ready(); + }; + + var ready = function() { + // bootstrap