diff --git a/administrator/components/com_categories/Model/CategoryModel.php b/administrator/components/com_categories/Model/CategoryModel.php index 1b44c6eee3bcb..7e0dc59787d2c 100644 --- a/administrator/components/com_categories/Model/CategoryModel.php +++ b/administrator/components/com_categories/Model/CategoryModel.php @@ -388,15 +388,9 @@ protected function preprocessForm(\JForm $form, $data, $group = 'content') // Get the component form if it exists $name = 'category' . ($section ? ('.' . $section) : ''); - // Looking first in the component forms folder + // Looking first in the component models/forms folder $path = \JPath::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = \JPath::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - // Old way: looking in the component folder if (!file_exists($path)) { diff --git a/administrator/components/com_content/Helper/ContentHelper.php b/administrator/components/com_content/Helper/ContentHelper.php new file mode 100644 index 0000000000000..2f0b0c2c81b5c --- /dev/null +++ b/administrator/components/com_content/Helper/ContentHelper.php @@ -0,0 +1,388 @@ +get('workflows_enable', 1)) + { + \JHtmlSidebar::addEntry( + \JText::_('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( + '- ' . \JText::_('COM_WORKFLOW_STATES'), + 'index.php?option=com_workflow&view=states&workflow_id=' . $workflowID . "&extension=com_content", + $vName == 'states`' + ); + + \JHtmlSidebar::addEntry( + '- ' . \JText::_('COM_WORKFLOW_TRANSITIONS'), + 'index.php?option=com_workflow&view=transitions&workflow_id=' . $workflowID . "&extension=com_content", + $vName == 'transitions' + ); + } + } + + if (\JComponentHelper::isEnabled('com_fields') && \JComponentHelper::getParams('com_content')->get('custom_fields_enable', '1')) + { + \JHtmlSidebar::addEntry( + \JText::_('JGLOBAL_FIELDS'), + 'index.php?option=com_fields&context=com_content.article', + $vName == 'fields.fields' + ); + \JHtmlSidebar::addEntry( + \JText::_('JGLOBAL_FIELD_GROUPS'), + 'index.php?option=com_fields&view=groups&context=com_content.article', + $vName == 'fields.groups' + ); + } + } + + /** + * 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->qn('condition')) + ->select('COUNT(*) AS ' . $db->qn('count')) + ->from($db->qn('#__content', 'c')) + ->from($db->qn('#__workflow_states', 's')) + ->from($db->qn('#__workflow_associations', 'a')) + ->where($db->qn('a.item_id') . ' = ' . $db->qn('c.id')) + ->where($db->qn('s.id') . ' = ' . $db->qn('a.state_id')) + ->where('catid = ' . (int) $item->id) + ->where('a.extension = ' . $db->quote('com_content')) + ->group($db->qn('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->qn('#__content') . ' AS c ON ct.content_item_id=c.id'; + $state = 'state'; + + if ($section === 'category') + { + $join = $db->qn('#__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->qn('#__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->qn('#__content')) + ->where('state = ' . (int) $stateID); + $db->setQuery($query); + $states = $db->loadResult(); + + return empty($states); + } + + /** + * Method to filter transitions by given id of state + * + * @param int $transitions Array of transitions + * @param int $pk Id of state + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function filterTransitions($transitions, $pk) + { + return array_values( + array_filter( + $transitions, + function ($var) use ($pk) + { + return $var['from_state_id'] == $pk; + } + ) + ); + } + + /** + * Method to change state of multiple ids + * + * @param int $pks Array of IDs + * @param int $condition Condition of the workflow state + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public static function updateContentState($pks, $condition) + { + if (empty($pks)) + { + return false; + } + + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->update($db->qn('#__content')) + ->set($db->qn('state') . '=' . (int) $condition) + ->where($db->qn('id') . ' IN (' . implode(', ', $pks) . ')'); + + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + return false; + } + + return true; + } +} diff --git a/administrator/components/com_content/Model/ArticleModel.php b/administrator/components/com_content/Model/ArticleModel.php index c50de3440bad1..44d0565f828e3 100644 --- a/administrator/components/com_content/Model/ArticleModel.php +++ b/administrator/components/com_content/Model/ArticleModel.php @@ -11,9 +11,17 @@ defined('_JEXEC') or die; +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\Workflow\Workflow; /** * Item Model for an Article. @@ -157,6 +165,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); + } + /** * Method to test whether a record can be deleted. * @@ -170,7 +222,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; } @@ -305,7 +363,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 */ @@ -320,6 +378,8 @@ public function getForm($data = array(), $loadData = true) } $jinput = \JFactory::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. @@ -328,15 +388,28 @@ 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 { @@ -344,7 +417,7 @@ public function getForm($data = array(), $loadData = true) $form->setFieldAttribute('catid', 'action', 'core.create'); } - $user = \JFactory::getUser(); + $user = \JFactory::getUser(); // Check for existing article. // Modify the form based on Edit State access controls. @@ -481,6 +554,8 @@ public function save($data) { $input = \JFactory::getApplication()->input; $filter = \JFilterInput::getInstance(); + $db = $this->getDbo(); + $user = Factory::getUser(); if (isset($data['metadata']) && isset($data['metadata']['author'])) { @@ -569,8 +644,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->qn(['ws.id', 'ws.condition'])) + ->from($db->qn('#__workflow_states', 'ws')) + ->from($db->qn('#__workflow_transitions', 'wt')) + ->where($db->qn('wt.to_state_id') . ' = ' . $db->qn('ws.id')) + ->where($db->qn('wt.id') . ' = ' . (int) $data['transition']) + ->where($db->qn('ws.published') . ' = 1') + ->where($db->qn('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 @@ -604,6 +739,8 @@ public function save($data) } } + $workflow = new Workflow(['extension' => 'com_content']); + if (parent::save($data)) { if (isset($data['featured'])) @@ -611,6 +748,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; } @@ -841,8 +1021,126 @@ 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(); + + $category = Categories::getInstance('content')->get($catId); + + if (!empty($category->id) && !empty($category->params)) + { + $catparams = new Registry($category->params); + + // Recheck if the workflow still exists + if ($catparams->get('workflow_id') > 0) + { + $query = $db->getQuery(true); + + $query ->select( + $db->qn( + [ + 'w.id', + 'ws.condition' + ] + ) + ) + ->select($db->qn('ws.id', 'state_id')) + ->from($db->qn('#__workflow_states', 'ws')) + ->from($db->qn('#__workflows', 'w')) + ->where($db->qn('ws.default') . ' = 1') + ->where($db->qn('ws.workflow_id') . ' = ' . $db->qn('w.id')) + ->where($db->qn('w.published') . ' = 1') + ->where($db->qn('ws.published') . ' = 1') + ->where($db->qn('w.id') . ' = ' . (int) $catparams->get('workflow_id')); + + $workflow = $db->setQuery($query)->loadObject(); + + if (!empty($workflow->id)) + { + return $workflow; + } + } + } + + // Use default workflow + $query = $db->getQuery(true); + + $query ->select( + $db->qn( + [ + 'w.id', + 'ws.condition' + ] + ) + ) + ->select($db->qn('ws.id', 'state_id')) + ->from($db->qn('#__workflow_states', 'ws')) + ->from($db->qn('#__workflows', 'w')) + ->where($db->qn('ws.default') . ' = 1') + ->where($db->qn('ws.workflow_id') . ' = ' . $db->qn('w.id')) + ->where($db->qn('w.published') . ' = 1') + ->where($db->qn('ws.published') . ' = 1') + ->where($db->qn('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 2ccf1941786c2..801d3fa97d98e 100644 --- a/administrator/components/com_content/Model/ArticlesModel.php +++ b/administrator/components/com_content/Model/ArticlesModel.php @@ -13,6 +13,8 @@ use Joomla\CMS\MVC\Model\ListModel; use Joomla\Utilities\ArrayHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Workflow\Workflow; /** * Methods supporting a list of article records. @@ -105,6 +107,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); @@ -216,6 +221,27 @@ 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' + ], + [ + 'state_title', + 'state_condition' + ] + ) + ) + ->innerJoin($query->quoteName('#__workflow_states', 'ws')) + ->where($query->quoteName('ws.id') . ' = ' . $query->quoteName('wa.state_id')); + // Join on voting table $associationsGroupBy = array( 'a.id', @@ -246,7 +272,7 @@ protected function getListQuery() if (\JPluginHelper::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'); @@ -284,17 +310,32 @@ protected function getListQuery() } // Filter by published state - $published = (string) $this->getState('filter.published'); + $workflowState = (string) $this->getState('filter.state'); - if (is_numeric($published)) + if (is_numeric($workflowState)) { - $query->where('a.state = ' . (int) $published); + $query->where('wa.state_id = ' . (int) $workflowState); + } + + $condition = (string) $this->getState('filter.condition'); + + if (is_numeric($condition)) + { + switch ((int) $condition) + { + case Workflow::PUBLISHED: + case Workflow::UNPUBLISHED: + case Workflow::TRASHED: + $query->where($db->qn('ws.condition') . ' = ' . (int) $condition); + } } - elseif ($published === '') + elseif (!is_numeric($workflowState)) { - $query->where('(a.state = 0 OR a.state = 1)'); + $query->where($db->qn('ws.condition') . ' IN (' . $query->quote(Workflow::PUBLISHED) . ',' . $query->quote(Workflow::UNPUBLISHED) . ')'); } + $query->where($db->qn('wa.extension') . '=' . $db->quote('com_content')); + // Filter by categories and by level $categoryId = $this->getState('filter.category_id', array()); $level = $this->getState('filter.level'); @@ -410,6 +451,102 @@ 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)); + + $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', + 's.id', + 's.title', + 's.condition' + ), + array( + 'value', + 'text', + 'from_state_id', + 'state_id', + 'state_title', + 'state_condition' + ) + ); + + $query->select($select) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->leftJoin($db->quoteName('#__workflow_states', 's') . ' ON ' . $db->qn('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->qn('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 40111ab225057..c152bea1a18a8 100644 --- a/administrator/components/com_content/Model/FeaturedModel.php +++ b/administrator/components/com_content/Model/FeaturedModel.php @@ -111,6 +111,14 @@ 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('ws.title AS state_title, ws.condition AS state_condition') + ->join('LEFT', '#__workflow_states AS ws ON ws.id = wa.state_id'); + // Join on voting table if (\JPluginHelper::isEnabled('content', 'vote')) { @@ -133,16 +141,25 @@ protected function getListQuery() } // Filter by published state - $published = $this->getState('filter.published'); + $workflowState = (string) $this->getState('filter.state'); - if (is_numeric($published)) + if (is_numeric($workflowState)) { - $query->where('a.state = ' . (int) $published); + $query->where('wa.state_id = ' . (int) $workflowState); } - elseif ($published === '') + + $condition = (string) $this->getState('filter.condition'); + + if (is_numeric($condition)) { - $query->where('(a.state = 0 OR a.state = 1)'); + $query->where($db->qn('ws.condition') . '=' . $db->quote($condition)); } + elseif (!is_numeric($workflowState)) + { + $query->where($db->qn('ws.condition') . ' IN ("0","1")'); + } + + $query->where($db->qn('wa.extension') . '=' . $db->quote('com_content')); // Filter by a single or group of categories. $baselevel = 1; diff --git a/administrator/components/com_content/View/Articles/HtmlView.php b/administrator/components/com_content/View/Articles/HtmlView.php index 0e98e34190654..d3264b9881f11 100644 --- a/administrator/components/com_content/View/Articles/HtmlView.php +++ b/administrator/components/com_content/View/Articles/HtmlView.php @@ -12,8 +12,8 @@ defined('_JEXEC') or die; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; - -\JLoader::register('ContentHelper', JPATH_ADMINISTRATOR . '/components/com_content/helpers/content.php'); +use Joomla\Component\Content\Administrator\Helper\ContentHelper; +use Joomla\CMS\Workflow\Workflow; /** * View class for a list of articles. @@ -91,7 +91,7 @@ public function display($tpl = null) { if ($this->getLayout() !== 'modal') { - \ContentHelper::addSubmenu('articles'); + ContentHelper::addSubmenu('articles'); } $this->items = $this->get('Items'); @@ -100,6 +100,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 = \JPluginHelper::isEnabled('content', 'vote'); // Check for errors. @@ -166,18 +167,15 @@ protected function addToolbar() if ($canDo->get('core.edit.state')) { - \JToolbarHelper::publish('articles.publish', 'JTOOLBAR_PUBLISH', true); - \JToolbarHelper::unpublish('articles.unpublish', 'JTOOLBAR_UNPUBLISH', true); \JToolbarHelper::custom('articles.featured', 'featured.png', 'featured_f2.png', 'JFEATURE', true); \JToolbarHelper::custom('articles.unfeatured', 'unfeatured.png', 'featured_f2.png', 'JUNFEATURE', true); - \JToolbarHelper::archiveList('articles.archive'); \JToolbarHelper::checkin('articles.checkin'); } // 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')) { $title = \JText::_('JTOOLBAR_BATCH'); @@ -188,14 +186,10 @@ protected function addToolbar() $bar->appendButton('Custom', $dhtml, 'batch'); } - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) + if ($this->state->get('filter.condition') == Workflow::TRASHED && $canDo->get('core.delete')) { \JToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'articles.delete', 'JTOOLBAR_EMPTY_TRASH'); } - elseif ($canDo->get('core.edit.state')) - { - \JToolbarHelper::trash('articles.trash'); - } if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { diff --git a/administrator/components/com_content/View/Featured/HtmlView.php b/administrator/components/com_content/View/Featured/HtmlView.php index 97806770ad654..60ab919e0a0ea 100644 --- a/administrator/components/com_content/View/Featured/HtmlView.php +++ b/administrator/components/com_content/View/Featured/HtmlView.php @@ -97,6 +97,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 = \JPluginHelper::isEnabled('content', 'vote'); // Check for errors. @@ -139,10 +140,7 @@ protected function addToolbar() if ($canDo->get('core.edit.state')) { - \JToolbarHelper::publish('articles.publish', 'JTOOLBAR_PUBLISH', true); - \JToolbarHelper::unpublish('articles.unpublish', 'JTOOLBAR_UNPUBLISH', true); \JToolbarHelper::custom('articles.unfeatured', 'unfeatured.png', 'featured_f2.png', 'JUNFEATURE', true); - \JToolbarHelper::archiveList('articles.archive'); \JToolbarHelper::checkin('articles.checkin'); } @@ -150,10 +148,6 @@ protected function addToolbar() { \JToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'articles.delete', 'JTOOLBAR_EMPTY_TRASH'); } - elseif ($canDo->get('core.edit.state')) - { - \JToolbarHelper::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 60283278e0b82..c3370b3654556 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" + > + + + + + + + + JText::_('JOPTION_SELECT_TAG'))); @@ -49,10 +50,22 @@ JHtml::_('draggablelist.draggable'); } -$assoc = JLanguageAssociations::isEnabled(); +$js = " + ;(function($) + { + $(function() + { + $('.article-status').on('click', function(e) + { + e.stopPropagation(); + }); + }); + })(jQuery); +"; + +\Joomla\CMS\Factory::getDocument()->addScriptDeclaration($js); -// Configure content state button renderer. -$publishedButton = new PublishedButton(['task_prefix' => 'articles.', 'checkbox_name' => 'cb']); +$assoc = JLanguageAssociations::isEnabled(); // Configure featured button renderer. $featuredButton = (new ActionButton(['tip_title' => 'JGLOBAL_TOGGLE_FEATURED'])) @@ -70,8 +83,8 @@
$this)); + // Search tools bar + echo JLayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?> items)) : ?> @@ -86,7 +99,7 @@ - + @@ -143,6 +156,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); + + $hasTransitions = count($transitions) > 0; + + $default = [ + JHtml::_('select.option', '', $this->escape($item->state_title)), + JHtml::_('select.option', '-1', '--------', ['disable' => true]) + ]; + + $transitions = array_merge($default, $transitions); + ?> @@ -167,10 +192,48 @@ id); ?> - -
- render($item->state, $i, ['disabled' => !$canChange], $item->publish_up, $item->publish_down); ?> + +
+
render($item->featured, $i, ['disabled' => !$canChange]); ?> + state_condition) : + + case -2: + $icon = 'trash'; + break; + + case 0: + $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); + ?> +
+
@@ -259,8 +322,7 @@ authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.edit.state', 'com_content')) : ?> + && $user->authorise('core.edit', 'com_content')) : ?> state->get('filter.published'); + +$user = \Joomla\CMS\Factory::getUser(); ?>
@@ -31,5 +33,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 4d836745fca65..b27f411b3fed6 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_footer.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_footer.php @@ -15,6 +15,7 @@ document.getElementById('batch-language-id').value = ''; document.getElementById('batch-user-id').value = ''; document.getElementById('batch-tag-id').value = ''; + document.getElementById('batch-workflowstate-id').value = ''; }); "); diff --git a/administrator/components/com_content/tmpl/articles/modal.php b/administrator/components/com_content/tmpl/articles/modal.php index 76b7be859ec7d..6f950e7255122 100644 --- a/administrator/components/com_content/tmpl/articles/modal.php +++ b/administrator/components/com_content/tmpl/articles/modal.php @@ -53,7 +53,7 @@ - + @@ -85,7 +85,6 @@ -2 => 'icon-trash', 0 => 'icon-unpublish', 1 => 'icon-publish', - 2 => 'icon-archive', ); ?> items as $i => $item) : ?> @@ -111,7 +110,7 @@ ?> - + escape($onclick) . '"' diff --git a/administrator/components/com_content/tmpl/featured/default.php b/administrator/components/com_content/tmpl/featured/default.php index 467aede1e5211..9ac118dd74e89 100644 --- a/administrator/components/com_content/tmpl/featured/default.php +++ b/administrator/components/com_content/tmpl/featured/default.php @@ -9,6 +9,8 @@ defined('_JEXEC') or die; +use Joomla\CMS\Workflow\Workflow; + JHtml::_('behavior.multiselect'); JHtml::_('formbehavior.chosen', '.multipleAccessLevels', null, array('placeholder_text_multiple' => JText::_('JOPTION_SELECT_ACCESS'))); JHtml::_('formbehavior.chosen', '.multipleAuthors', null, array('placeholder_text_multiple' => JText::_('JOPTION_SELECT_AUTHOR'))); @@ -40,6 +42,22 @@ $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component'; JHtml::_('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); + ?>
@@ -67,8 +85,8 @@ - - + + @@ -122,6 +140,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); + + $hasTransitions = count($transitions) > 0; + + $default = [ + JHtml::_('select.option', '', $this->escape($item->state_title)), + JHtml::_('select.option', '-1', '--------', ['disable' => true]) + ]; + + $transitions = array_merge($default, $transitions); + ?> @@ -147,10 +177,49 @@ id); ?> - -
- state, $i, 'articles.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - featured, $i, $canChange); ?> + +
+
+ featured, $i, $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', + 'style' => 'min-width: 50%;', + 'onchange' => "listItemTask('cb" . (int) $i . "', 'articles.runTransition')"] + ]; + echo JHTML::_('select.genericlist', $transitions, 'transition_' . (int) $item->id, $attribs); + ?> +
+
@@ -166,8 +235,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 ac057aa145afc..399e17ce4ce94 100644 --- a/administrator/components/com_menus/presets/joomla.xml +++ b/administrator/components/com_menus/presets/joomla.xml @@ -225,6 +225,15 @@ + + + + + + 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..2d6d5f6839c3f --- /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 !== 1) + { + $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_ITEM_MUST_PUBLISHED'), '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..403e0065c2428 --- /dev/null +++ b/administrator/components/com_workflow/Model/TransitionsModel.php @@ -0,0 +1,230 @@ +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.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..37b30a9b97387 --- /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..ff78e35ff7588 --- /dev/null +++ b/administrator/components/com_workflow/forms/transition.xml @@ -0,0 +1,82 @@ + +
+ +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + +
+
diff --git a/administrator/components/com_workflow/forms/workflow.xml b/administrator/components/com_workflow/forms/workflow.xml new file mode 100644 index 0000000000000..6220c0fbcc570 --- /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..fb0fa66f40a3b --- /dev/null +++ b/administrator/components/com_workflow/tmpl/state/edit.php @@ -0,0 +1,55 @@ + 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..8720cd977cea4 --- /dev/null +++ b/administrator/components/com_workflow/tmpl/states/default.php @@ -0,0 +1,147 @@ +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; + ?> + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ pagination->getListFooter(); ?> +
+ + + + + + + + id); ?> + +
+ published, $i, 'states.', $canChange); ?> +
+
+ default, $i, 'states.', $canChange); ?> + + + '; ?> + + title; ?> + + + title; ?> + + + condition); ?> + + id; ?> +
+ + + + + + +
+
+
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..efe2234d78433 --- /dev/null +++ b/administrator/components/com_workflow/tmpl/transition/edit.php @@ -0,0 +1,79 @@ + 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..cfadf3f7753b8 --- /dev/null +++ b/administrator/components/com_workflow/tmpl/transitions/default.php @@ -0,0 +1,145 @@ +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; ?> + + 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..d8e1492fc613e --- /dev/null +++ b/administrator/components/com_workflow/tmpl/workflow/edit.php @@ -0,0 +1,90 @@ + 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->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..71ddafe29a7ae --- /dev/null +++ b/administrator/components/com_workflow/tmpl/workflows/default.php @@ -0,0 +1,196 @@ +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 2d5f7d21e52da..282ee82a7c1f9 100644 --- a/administrator/language/en-GB/en-GB.com_content.ini +++ b/administrator/language/en-GB/en-GB.com_content.ini @@ -3,15 +3,17 @@ ; 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_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" @@ -20,7 +22,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" @@ -28,30 +29,39 @@ 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_DEFAULT_WORKFLOW="- Use default -" 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" @@ -66,34 +76,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_0="No 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,28 @@ 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_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 it is 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 it is 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..430fc603985bd --- /dev/null +++ b/administrator/language/en-GB/en-GB.com_workflow.ini @@ -0,0 +1,101 @@ +; 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="This is for choose the category from list of it" +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="This means how assigned item to this state should behavior" +COM_WORKFLOW_CONFIGURATION="Workflow: Options" +COM_WORKFLOW_COUNT_STATES="States" +COM_WORKFLOW_COUNT_TRANSITIONS="Transitions" +COM_WORKFLOW_CREATED_AT_ASC="Created Date ASC" +COM_WORKFLOW_CREATED_AT_DESC="Created Date DESC" +COM_WORKFLOW_CREATED_DESC="When item was created" +COM_WORKFLOW_CREATED_LABEL="Creation date" +COM_WORKFLOW_DATE_CREATED="Created Date" +COM_WORKFLOW_DATE_MODIFIED="Modified Date" +COM_WORKFLOW_DEFAULT="Default" +COM_WORKFLOW_DEFAULT_ITEM="Default option is already set for different item" +COM_WORKFLOW_DESCRIPTION="Description" +COM_WORKFLOW_DESC_CONDITION="Condition descending" +COM_WORKFLOW_DESC_TAB="Description" +COM_WORKFLOW_DISABLE_DEFAULT="Can not change default state of this item." +COM_WORKFLOW_EDIT="Edit" +COM_WORKFLOW_EDIT_TAB="Edit" +COM_WORKFLOW_ERROR_UPDATE_STATE="Error with updating item to new state" +COM_WORKFLOW_FIELD_CATEGORY_LIST_LABEL="Category List" +COM_WORKFLOW_FIELD_IS_DEFAULT_DESC="If Workflow is default then is for whole component" +COM_WORKFLOW_FIELD_IS_DEFAULT_LABEL="Default" +COM_WORKFLOW_FIELD_TITLE_DESC="Title of item" +COM_WORKFLOW_FIELD_TITLE_LABEL="Title" +COM_WORKFLOW_FILTER_SEARCH_DESC="You can filter content by typing here" +COM_WORKFLOW_FILTER_SEARCH_LABEL="Search" +COM_WORKFLOW_FROM_STATE="From state" +COM_WORKFLOW_FROM_STATE_DESC="Select from what state, this transition should be" +COM_WORKFLOW_ID="ID" +COM_WORKFLOW_ITEM_MUST_PUBLISHED="To set default, item must be published" +COM_WORKFLOW_ITEM_SET_DEFAULT="Item set to default" +COM_WORKFLOW_ITEM_UNSET_DEFAULT="Item uset default" +COM_WORKFLOW_LIST_LIMIT="Limit output" +COM_WORKFLOW_LIST_LIMIT_DESC="You can limit list of items" +COM_WORKFLOW_MANAGE="manage" +COM_WORKFLOW_MODIFIED_AT_ASC="Modified Date ascending" +COM_WORKFLOW_MODIFIED_AT_DESC="Modified Date descending" +COM_WORKFLOW_MODIFIED_DESC="When item was modificated" +COM_WORKFLOW_MODIFIED_LABEL="Modification date" +COM_WORKFLOW_MSG_DELETE_DEFAULT="You are trying to delete default item" +COM_WORKFLOW_MSG_DELETE_IS_ASSIGNED="This item is used in component" +COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR="There was a problem with deleting item: " +COM_WORKFLOW_NA="N/A" +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="This means how assigned item to this, should behavior" +COM_WORKFLOW_PUBLISHED_LABEL="Status" +COM_WORKFLOW_RULES_TAB="Permissions" +COM_WORKFLOW_SELECT_CONDITION="- Select Condition -" +COM_WORKFLOW_SELECT_FROM_STATE="- Select From State -" +COM_WORKFLOW_SELECT_TO_STATE="- Select To State -" +COM_WORKFLOW_STATE="State" +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="This means if state is published" +COM_WORKFLOW_STATUS_LABEL="Status" +COM_WORKFLOW_TITLE="Title" +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 to what state, this transition should be" +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="That transition already exist" +COM_WORKFLOW_TRANSITION_EDIT="Edit Transition" +COM_WORKFLOW_TRANSITION_THE_SAME_STATE="You choose the same state 'from' and 'to'" +COM_WORKFLOW_TRASHED="Trashed" +COM_WORKFLOW_UNPUBLISHED="Unpublished" +COM_WORKFLOW_USER_GROUPS="User group" +COM_WORKFLOW_USER_GROUPS_DESC="Select user group for performing transition" +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 62e68db0592e2..052573d227115 100644 --- a/administrator/language/en-GB/en-GB.ini +++ b/administrator/language/en-GB/en-GB.ini @@ -143,6 +143,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" @@ -303,6 +304,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)" @@ -621,6 +623,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 01b08e6af2ccf..a8141d0bea179 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." @@ -376,6 +380,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 343757407caae..03d138ff9cd83 100644 --- a/administrator/language/en-GB/en-GB.mod_menu.ini +++ b/administrator/language/en-GB/en-GB.mod_menu.ini @@ -9,6 +9,7 @@ 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" @@ -98,3 +99,4 @@ MOD_MENU_SYSTEM_INFORMATION="System Information" MOD_MENU_SYSTEM="System" MOD_MENU_USER_PROFILE="My Profile" MOD_MENU_XML_DESCRIPTION="This module displays an administrator menu module." +MOD_MENU_COM_CONTENT_WORKFLOW="Workflows" 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 ce4353f1da4e0..647ae4a8579be 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 dbe2f9af15add..d999ce9cd26d5 100644 --- a/administrator/modules/mod_latest/tmpl/default.php +++ b/administrator/modules/mod_latest/tmpl/default.php @@ -16,7 +16,7 @@
    $item) : ?> -
  • +
  • checked_out) : ?> editor, $item->checked_out_time); ?> diff --git a/administrator/modules/mod_menu/Menu/CssMenu.php b/administrator/modules/mod_menu/Menu/CssMenu.php index b421cf9938643..50e65b554a976 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'); + } if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) { diff --git a/components/com_content/Model/ArchiveModel.php b/components/com_content/Model/ArchiveModel.php index 8242cef0c264e..6f2db17411612 100644 --- a/components/com_content/Model/ArchiveModel.php +++ b/components/com_content/Model/ArchiveModel.php @@ -10,6 +10,7 @@ defined('_JEXEC') or die; +use Joomla\CMS\Factory; use Joomla\Component\Content\Site\Helper\QueryHelper; use Joomla\Component\Content\Site\Model\ArticlesModel; use Joomla\Utilities\ArrayHelper; @@ -44,13 +45,13 @@ protected function populateState($ordering = null, $direction = null) { parent::populateState(); - $app = \JFactory::getApplication(); + $app = Factory::getApplication(); // Add archive properties - $params = $this->state->params; + $params = $this->state->get('params'); // Filter on archived articles - $this->setState('filter.published', 2); + $this->setState('filter.condition', 1); // Filter on month, year $this->setState('filter.month', $app->input->getInt('month')); @@ -61,7 +62,7 @@ protected function populateState($ordering = null, $direction = null) // Get list limit $itemid = $app->input->get('Itemid', 0, 'int'); - $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); + $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); $this->setState('list.limit', $limit); // Set the archive ordering @@ -85,12 +86,18 @@ protected function populateState($ordering = null, $direction = null) protected function getListQuery() { $params = $this->state->params; - $app = \JFactory::getApplication(); + $app = Factory::getApplication(); $catids = $app->input->getVar('catid', array()); $catids = array_values(array_diff($catids, array(''))); + $states = $app->input->getVar('state', array()); + $states = array_values(array_diff($states, array(''))); + $articleOrderDate = $params->get('order_date'); + $this->setState('filter.condition', false); + // Create a new query object. + $db = $this->getDbo(); $query = parent::getListQuery(); // Add routing for archive @@ -116,6 +123,11 @@ protected function getListQuery() $query->where('c.id IN (' . implode(', ', $catids) . ')'); } + if (count($states) > 0) + { + $query->where($db->qn('state_id') . ' IN (' . implode(', ', $states) . ')'); + } + return $query; } @@ -127,7 +139,7 @@ protected function getListQuery() */ public function getData() { - $app = \JFactory::getApplication(); + $app = Factory::getApplication(); // Lets load the content if it doesn't already exist if (empty($this->_data)) @@ -185,13 +197,13 @@ public function getYears() { $db = $this->getDbo(); $nullDate = $db->quote($db->getNullDate()); - $nowDate = $db->quote(\JFactory::getDate()->toSql()); + $nowDate = $db->quote(Factory::getDate()->toSql()); $query = $db->getQuery(true); $years = $query->year($db->qn('created')); $query->select('DISTINCT (' . $years . ')') ->from($db->qn('#__content')) - ->where($db->qn('state') . '= 2') + ->where($db->qn('state') . '= 3') ->where('(publish_up = ' . $nullDate . ' OR publish_up <= ' . $nowDate . ')') ->where('(publish_down = ' . $nullDate . ' OR publish_down >= ' . $nowDate . ')') ->order('1 ASC'); diff --git a/components/com_content/Model/ArticleModel.php b/components/com_content/Model/ArticleModel.php index 447d37934110f..0755a4f4d29c9 100644 --- a/components/com_content/Model/ArticleModel.php +++ b/components/com_content/Model/ArticleModel.php @@ -59,7 +59,6 @@ protected function populateState() if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); } $this->setState('filter.language', Multilanguage::isEnabled()); @@ -103,6 +102,9 @@ public function getItem($pk = null) $query->from('#__content AS a') ->where('a.id = ' . (int) $pk); + $query->select($db->qn('ws.condition')) + ->join('LEFT', '#__workflow_states AS ws ON ws.id = a.state'); + // Join on category table. $query->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') ->innerJoin('#__categories AS c on c.id = a.catid') @@ -140,13 +142,13 @@ public function getItem($pk = null) // Filter by published state. $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); if (is_numeric($published)) { - $query->where('(a.state = ' . (int) $published . ' OR a.state =' . (int) $archived . ')'); + $query->where($db->qn('ws.condition') . ' = ' . $db->quote((int) $published)); } + $db->setQuery($query); $data = $db->loadObject(); @@ -157,7 +159,7 @@ public function getItem($pk = null) } // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && (($data->state != $published) && ($data->state != $archived))) + if (is_numeric($published) && $data->condition != $published) { throw new \Exception(\JText::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); } diff --git a/components/com_content/Model/ArticlesModel.php b/components/com_content/Model/ArticlesModel.php index 6738edbb3271e..b0baa7d5a43ce 100644 --- a/components/com_content/Model/ArticlesModel.php +++ b/components/com_content/Model/ArticlesModel.php @@ -17,6 +17,7 @@ use Joomla\Registry\Registry; use Joomla\String\StringHelper; use Joomla\Utilities\ArrayHelper; +use Joomla\CMS\Workflow\Workflow; /** * This models supports retrieving lists of articles. @@ -45,6 +46,7 @@ public function __construct($config = array()) 'checked_out_time', 'a.checked_out_time', 'catid', 'a.catid', 'category_title', 'state', 'a.state', + 'state_condition', 'ws.condition', 'access', 'a.access', 'access_level', 'created', 'a.created', 'created_by', 'a.created_by', @@ -119,7 +121,7 @@ protected function populateState($ordering = 'ordering', $direction = 'ASC') if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', 1); + $this->setState('filter.condition', Workflow::PUBLISHED); } $this->setState('filter.language', Multilanguage::isEnabled()); @@ -153,7 +155,7 @@ protected function populateState($ordering = 'ordering', $direction = 'ASC') protected function getStoreId($id = '') { // Compile the store id. - $id .= ':' . serialize($this->getState('filter.published')); + $id .= ':' . serialize($this->getState('filter.condition')); $id .= ':' . $this->getState('filter.access'); $id .= ':' . $this->getState('filter.featured'); $id .= ':' . serialize($this->getState('filter.article_id')); @@ -195,11 +197,11 @@ protected function getListQuery() $this->getState( 'list.select', 'DISTINCT a.id, a.title, a.alias, a.introtext, a.fulltext, ' . - 'a.checked_out, a.checked_out_time, ' . + 'a.checked_out, a.checked_out_time,' . 'a.catid, a.created, a.created_by, a.created_by_alias, ' . // Published/archived article in archive category is treats as archive article // If category is not published then force 0 - 'CASE WHEN c.published = 2 AND a.state > 0 THEN 2 WHEN c.published != 1 THEN 0 ELSE a.state END as state,' . + 'CASE WHEN c.published = 2 AND ws.condition > 2 THEN 3 WHEN c.published != 1 THEN 1 ELSE ws.condition END as state,' . // Use created if modified is 0 'CASE WHEN a.modified = ' . $db->quote($db->getNullDate()) . ' THEN a.created ELSE a.modified END as modified, ' . 'a.modified_by, uam.name as modified_by_name,' . @@ -234,6 +236,14 @@ protected function getListQuery() $query->join('LEFT', '#__content_frontpage AS fp ON fp.content_id = a.id'); } + // 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('ws.title AS state_title, ws.condition AS state_condition') + ->join('LEFT', '#__workflow_states AS ws ON ws.id = wa.state_id'); + // Join over the categories. $query->select('c.title AS category_title, c.path AS category_route, c.access AS category_access, c.alias AS category_alias') ->select('c.published, c.published AS parents_published, c.lft') @@ -266,26 +276,32 @@ protected function getListQuery() } // Filter by published state - $published = $this->getState('filter.published'); + $condition = $this->getState('filter.condition'); - if (is_numeric($published) && $published == 2) + if (is_numeric($condition) && $condition == 2) { // If category is archived then article has to be published or archived. // If categogy is published then article has to be archived. $query->where('((c.published = 2 AND a.state > 0) OR (c.published = 1 AND a.state = 2))'); } - elseif (is_numeric($published)) + elseif (is_numeric($condition)) { // Category has to be published - $query->where('c.published = 1 AND a.state = ' . (int) $published); + $query->where("c.published = 1 AND ws.condition = " . $db->quote($condition)); } - elseif (is_array($published)) + elseif (is_array($condition)) { - $published = ArrayHelper::toInteger($published); - $published = implode(',', $published); + $condition = array_map( + function ($data) use ($db) + { + return $db->quote($data); + }, + $condition + ); + $condition = implode(',', $condition); // Category has to be published - $query->where('c.published = 1 AND a.state IN (' . $published . ')'); + $query->where('c.published = 1 AND ws.condition IN (' . $condition . ')'); } // Filter by featured state diff --git a/components/com_content/Model/CategoriesModel.php b/components/com_content/Model/CategoriesModel.php index 89aa8cc4c057f..78d57366f25f0 100644 --- a/components/com_content/Model/CategoriesModel.php +++ b/components/com_content/Model/CategoriesModel.php @@ -60,7 +60,7 @@ protected function populateState($ordering = null, $direction = null) $params = $app->getParams(); $this->setState('params', $params); - $this->setState('filter.published', 1); + $this->setState('filter.condition', 1); $this->setState('filter.access', true); } @@ -79,7 +79,7 @@ protected function getStoreId($id = '') { // Compile the store id. $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.condition'); $id .= ':' . $this->getState('filter.access'); $id .= ':' . $this->getState('filter.parentId'); diff --git a/components/com_content/Model/CategoryModel.php b/components/com_content/Model/CategoryModel.php index a3a2cb0c3c777..19d9e959616f3 100644 --- a/components/com_content/Model/CategoryModel.php +++ b/components/com_content/Model/CategoryModel.php @@ -145,11 +145,11 @@ protected function populateState($ordering = null, $direction = null) if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); + $this->setState('filter.condition', 1); } else { - $this->setState('filter.published', array(0, 1, 2)); + $this->setState('filter.condition', array(0, 1)); } // Process show_noauth parameter @@ -238,7 +238,7 @@ public function getItems() $model = new ArticlesModel(array('ignore_request' => true)); $model->setState('params', \JFactory::getApplication()->getParams()); $model->setState('filter.category_id', $category->id); - $model->setState('filter.published', $this->getState('filter.published')); + $model->setState('filter.condition', $this->getState('filter.condition')); $model->setState('filter.access', $this->getState('filter.access')); $model->setState('filter.language', $this->getState('filter.language')); $model->setState('filter.featured', $this->getState('filter.featured')); diff --git a/components/com_content/Model/FeaturedModel.php b/components/com_content/Model/FeaturedModel.php index 8fc5d2cc381a6..34bff879ae973 100644 --- a/components/com_content/Model/FeaturedModel.php +++ b/components/com_content/Model/FeaturedModel.php @@ -9,6 +9,7 @@ namespace Joomla\Component\Content\Site\Model; use Joomla\Component\Content\Site\Helper\QueryHelper; +use Joomla\CMS\Workflow\Workflow; defined('_JEXEC') or die; @@ -59,11 +60,11 @@ protected function populateState($ordering = null, $direction = null) if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', 1); + $this->setState('filter.condition', Workflow::PUBLISHED); } else { - $this->setState('filter.published', array(0, 1, 2)); + $this->setState('filter.condition', array(Workflow::UNPUBLISHED, Workflow::PUBLISHED)); } // Check for category selection diff --git a/components/com_content/forms/article.xml b/components/com_content/forms/article.xml index d0c8005d38ad0..e406f364e0edb 100644 --- a/components/com_content/forms/article.xml +++ b/components/com_content/forms/article.xml @@ -58,18 +58,12 @@ /> - - - - + name="transition" + type="transition" + component="com_content" + addfieldprefix="Joomla\Component\Workflow\Administrator\Field" + label="COM_CONTENT_TRANSITION" + >
    -
    +
    diff --git a/components/com_content/tmpl/article/default.php b/components/com_content/tmpl/article/default.php index 2b146cfb00a6e..e359e604f9ad6 100644 --- a/components/com_content/tmpl/article/default.php +++ b/components/com_content/tmpl/article/default.php @@ -52,7 +52,7 @@ escape($this->item->title); ?> - item->state == 0) : ?> + item->condition == 2) : ?> item->publish_up) > strtotime(JFactory::getDate())) : ?> diff --git a/components/com_content/tmpl/category/blog_item.php b/components/com_content/tmpl/category/blog_item.php index 61e2f5e939775..ff28b471ed61c 100644 --- a/components/com_content/tmpl/category/blog_item.php +++ b/components/com_content/tmpl/category/blog_item.php @@ -23,7 +23,7 @@ item); ?>
    - item->state == 0 || strtotime($this->item->publish_up) > strtotime(JFactory::getDate()) + item->condition == 3 || strtotime($this->item->publish_up) > strtotime(JFactory::getDate()) || ((strtotime($this->item->publish_down) < strtotime(JFactory::getDate())) && $this->item->publish_down != JFactory::getDbo()->getNullDate())) : ?>
    @@ -79,7 +79,7 @@ - item->state == 0 || strtotime($this->item->publish_up) > strtotime(JFactory::getDate()) + item->condition == 2 || strtotime($this->item->publish_up) > strtotime(JFactory::getDate()) || ((strtotime($this->item->publish_down) < strtotime(JFactory::getDate())) && $this->item->publish_down != JFactory::getDbo()->getNullDate())) : ?>
    diff --git a/components/com_content/tmpl/category/default_articles.php b/components/com_content/tmpl/category/default_articles.php index bd233d697c78b..3875e05241602 100644 --- a/components/com_content/tmpl/category/default_articles.php +++ b/components/com_content/tmpl/category/default_articles.php @@ -145,7 +145,7 @@ items as $i => $article) : ?> - items[$i]->state == 0) : ?> + items[$i]->condition == 2) : ?> @@ -192,7 +192,7 @@ - state == 0) : ?> + condition == 2) : ?> diff --git a/components/com_content/tmpl/form/edit.php b/components/com_content/tmpl/form/edit.php index 60149c04f346d..bc44b1d36308f 100644 --- a/components/com_content/tmpl/form/edit.php +++ b/components/com_content/tmpl/form/edit.php @@ -110,8 +110,10 @@ get('show_publishing_options', 1) == 1) : ?> form->renderField('created_by_alias'); ?> + item->id > 0) : ?> + form->renderField('transition'); ?> + item->params->get('access-change')) : ?> - form->renderField('state'); ?> form->renderField('featured'); ?> get('show_publishing_options', 1) == 1) : ?> form->renderField('publish_up'); ?> diff --git a/installation/sql/mysql/joomla.sql b/installation/sql/mysql/joomla.sql index 57f0940063945..034575cc180cc 100644 --- a/installation/sql/mysql/joomla.sql +++ b/installation/sql/mysql/joomla.sql @@ -31,59 +31,76 @@ CREATE TABLE IF NOT EXISTS `#__assets` ( -- INSERT INTO `#__assets` (`id`, `parent_id`, `lft`, `rgt`, `level`, `name`, `title`, `rules`) VALUES -(1, 0, 0, 105, 0, 'root.1', 'Root Asset', '{"core.login.site":{"6":1,"2":1},"core.login.admin":{"6":1},"core.login.offline":{"6":1},"core.admin":{"8":1},"core.manage":{"7":1},"core.create":{"6":1,"3":1},"core.delete":{"6":1},"core.edit":{"6":1,"4":1},"core.edit.state":{"6":1,"5":1},"core.edit.own":{"6":1,"3":1}}'), +(1, 0, 0, 139, 0, 'root.1', 'Root Asset', '{"core.login.site":{"6":1,"2":1},"core.login.admin":{"6":1},"core.login.offline":{"6":1},"core.admin":{"8":1},"core.manage":{"7":1},"core.create":{"6":1,"3":1},"core.delete":{"6":1},"core.edit":{"6":1,"4":1},"core.edit.state":{"6":1,"5":1},"core.edit.own":{"6":1,"3":1}}'), (2, 1, 1, 2, 1, 'com_admin', 'com_admin', '{}'), (3, 1, 3, 6, 1, 'com_banners', 'com_banners', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), (4, 1, 7, 8, 1, 'com_cache', 'com_cache', '{"core.admin":{"7":1},"core.manage":{"7":1}}'), (5, 1, 9, 10, 1, 'com_checkin', 'com_checkin', '{"core.admin":{"7":1},"core.manage":{"7":1}}'), (6, 1, 11, 12, 1, 'com_config', 'com_config', '{}'), (7, 1, 13, 16, 1, 'com_contact', 'com_contact', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), -(8, 1, 17, 20, 1, 'com_content', 'com_content', '{"core.admin":{"7":1},"core.manage":{"6":1},"core.create":{"3":1},"core.edit":{"4":1},"core.edit.state":{"5":1}}'), -(9, 1, 21, 22, 1, 'com_cpanel', 'com_cpanel', '{}'), -(10, 1, 23, 24, 1, 'com_installer', 'com_installer', '{"core.manage":{"7":0},"core.delete":{"7":0},"core.edit.state":{"7":0}}'), -(11, 1, 25, 26, 1, 'com_languages', 'com_languages', '{"core.admin":{"7":1}}'), -(12, 1, 27, 28, 1, 'com_login', 'com_login', '{}'), -(13, 1, 29, 30, 1, 'com_mailto', 'com_mailto', '{}'), -(14, 1, 31, 32, 1, 'com_massmail', 'com_massmail', '{}'), -(15, 1, 33, 34, 1, 'com_media', 'com_media', '{"core.admin":{"7":1},"core.manage":{"6":1},"core.create":{"3":1},"core.delete":{"5":1}}'), -(16, 1, 35, 38, 1, 'com_menus', 'com_menus', '{"core.admin":{"7":1}}'), -(17, 1, 39, 40, 1, 'com_messages', 'com_messages', '{"core.admin":{"7":1},"core.manage":{"7":1}}'), -(18, 1, 41, 74, 1, 'com_modules', 'com_modules', '{"core.admin":{"7":1}}'), -(19, 1, 75, 78, 1, 'com_newsfeeds', 'com_newsfeeds', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), -(20, 1, 79, 80, 1, 'com_plugins', 'com_plugins', '{"core.admin":{"7":1}}'), -(21, 1, 81, 82, 1, 'com_redirect', 'com_redirect', '{"core.admin":{"7":1}}'), -(22, 1, 83, 84, 1, 'com_search', 'com_search', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), -(23, 1, 85, 86, 1, 'com_templates', 'com_templates', '{"core.admin":{"7":1}}'), -(24, 1, 87, 90, 1, 'com_users', 'com_users', '{"core.admin":{"7":1}}'), -(26, 1, 91, 92, 1, 'com_wrapper', 'com_wrapper', '{}'), +(8, 1, 17, 54, 1, 'com_content', 'com_content', '{"core.admin":{"7":1},"core.manage":{"6":1},"core.create":{"3":1},"core.edit":{"4":1},"core.edit.state":{"5":1},"core.execute.transition":{"6":1,"5":1}}'), +(9, 1, 55, 56, 1, 'com_cpanel', 'com_cpanel', '{}'), +(10, 1, 57, 58, 1, 'com_installer', 'com_installer', '{"core.manage":{"7":0},"core.delete":{"7":0},"core.edit.state":{"7":0}}'), +(11, 1, 59, 60, 1, 'com_languages', 'com_languages', '{"core.admin":{"7":1}}'), +(12, 1, 61, 62, 1, 'com_login', 'com_login', '{}'), +(13, 1, 63, 64, 1, 'com_mailto', 'com_mailto', '{}'), +(14, 1, 65, 66, 1, 'com_massmail', 'com_massmail', '{}'), +(15, 1, 67, 68, 1, 'com_media', 'com_media', '{"core.admin":{"7":1},"core.manage":{"6":1},"core.create":{"3":1},"core.delete":{"5":1}}'), +(16, 1, 69, 72, 1, 'com_menus', 'com_menus', '{"core.admin":{"7":1}}'), +(17, 1, 73, 74, 1, 'com_messages', 'com_messages', '{"core.admin":{"7":1},"core.manage":{"7":1}}'), +(18, 1, 75, 108, 1, 'com_modules', 'com_modules', '{"core.admin":{"7":1}}'), +(19, 1, 109, 112, 1, 'com_newsfeeds', 'com_newsfeeds', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), +(20, 1, 113, 114, 1, 'com_plugins', 'com_plugins', '{"core.admin":{"7":1}}'), +(21, 1, 115, 116, 1, 'com_redirect', 'com_redirect', '{"core.admin":{"7":1}}'), +(22, 1, 117, 118, 1, 'com_search', 'com_search', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), +(23, 1, 119, 120, 1, 'com_templates', 'com_templates', '{"core.admin":{"7":1}}'), +(24, 1, 121, 124, 1, 'com_users', 'com_users', '{"core.admin":{"7":1}}'), +(26, 1, 125, 126, 1, 'com_wrapper', 'com_wrapper', '{}'), (27, 8, 18, 19, 2, 'com_content.category.2', 'Uncategorised', '{}'), (28, 3, 4, 5, 2, 'com_banners.category.3', 'Uncategorised', '{}'), (29, 7, 14, 15, 2, 'com_contact.category.4', 'Uncategorised', '{}'), -(30, 19, 76, 77, 2, 'com_newsfeeds.category.5', 'Uncategorised', '{}'), -(32, 24, 88, 89, 2, 'com_users.category.7', 'Uncategorised', '{}'), -(33, 1, 93, 94, 1, 'com_finder', 'com_finder', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), -(34, 1, 95, 96, 1, 'com_joomlaupdate', 'com_joomlaupdate', '{}'), -(35, 1, 97, 98, 1, 'com_tags', 'com_tags', '{}'), -(36, 1, 99, 100, 1, 'com_contenthistory', 'com_contenthistory', '{}'), -(37, 1, 101, 102, 1, 'com_ajax', 'com_ajax', '{}'), -(38, 1, 103, 104, 1, 'com_postinstall', 'com_postinstall', '{}'), -(39, 18, 42, 43, 2, 'com_modules.module.1', 'Main Menu', '{}'), -(40, 18, 44, 45, 2, 'com_modules.module.2', 'Login', '{}'), -(41, 18, 46, 47, 2, 'com_modules.module.3', 'Popular Articles', '{}'), -(42, 18, 48, 49, 2, 'com_modules.module.4', 'Recently Added Articles', '{}'), -(43, 18, 50, 51, 2, 'com_modules.module.8', 'Toolbar', '{}'), -(44, 18, 52, 53, 2, 'com_modules.module.9', 'Quick Icons', '{}'), -(45, 18, 54, 55, 2, 'com_modules.module.10', 'Logged-in Users', '{}'), -(46, 18, 56, 57, 2, 'com_modules.module.12', 'Admin Menu', '{}'), -(47, 18, 58, 59, 2, 'com_modules.module.13', 'Admin Submenu', '{}'), -(48, 18, 60, 61, 2, 'com_modules.module.14', 'User Status', '{}'), -(49, 18, 62, 63, 2, 'com_modules.module.15', 'Title', '{}'), -(50, 18, 64, 65, 2, 'com_modules.module.16', 'Login Form', '{}'), -(51, 18, 66, 67, 2, 'com_modules.module.17', 'Breadcrumbs', '{}'), -(52, 18, 68, 69, 2, 'com_modules.module.79', 'Multilanguage status', '{}'), -(53, 18, 70, 71, 2, 'com_modules.module.86', 'Joomla Version', '{}'), -(54, 16, 36, 37, 2, 'com_menus.menu.1', 'Main Menu', '{}'), -(55, 18, 72, 73, 2, 'com_modules.module.87', 'Sample Data', '{}'); +(30, 19, 110, 111, 2, 'com_newsfeeds.category.5', 'Uncategorised', '{}'), +(32, 24, 122, 123, 2, 'com_users.category.7', 'Uncategorised', '{}'), +(33, 1, 127, 128, 1, 'com_finder', 'com_finder', '{"core.admin":{"7":1},"core.manage":{"6":1}}'), +(34, 1, 129, 130, 1, 'com_joomlaupdate', 'com_joomlaupdate', '{}'), +(35, 1, 131, 132, 1, 'com_tags', 'com_tags', '{}'), +(36, 1, 133, 134, 1, 'com_contenthistory', 'com_contenthistory', '{}'), +(37, 1, 135, 136, 1, 'com_ajax', 'com_ajax', '{}'), +(38, 1, 137, 138, 1, 'com_postinstall', 'com_postinstall', '{}'), +(39, 18, 76, 77, 2, 'com_modules.module.1', 'Main Menu', '{}'), +(40, 18, 78, 79, 2, 'com_modules.module.2', 'Login', '{}'), +(41, 18, 80, 81, 2, 'com_modules.module.3', 'Popular Articles', '{}'), +(42, 18, 82, 83, 2, 'com_modules.module.4', 'Recently Added Articles', '{}'), +(43, 18, 84, 85, 2, 'com_modules.module.8', 'Toolbar', '{}'), +(44, 18, 86, 87, 2, 'com_modules.module.9', 'Quick Icons', '{}'), +(45, 18, 88, 89, 2, 'com_modules.module.10', 'Logged-in Users', '{}'), +(46, 18, 90, 91, 2, 'com_modules.module.12', 'Admin Menu', '{}'), +(47, 18, 92, 93, 2, 'com_modules.module.13', 'Admin Submenu', '{}'), +(48, 18, 94, 95, 2, 'com_modules.module.14', 'User Status', '{}'), +(49, 18, 96, 97, 2, 'com_modules.module.15', 'Title', '{}'), +(50, 18, 98, 99, 2, 'com_modules.module.16', 'Login Form', '{}'), +(51, 18, 100, 101, 2, 'com_modules.module.17', 'Breadcrumbs', '{}'), +(52, 18, 102, 103, 2, 'com_modules.module.79', 'Multilanguage status', '{}'), +(53, 18, 104, 105, 2, 'com_modules.module.86', 'Joomla Version', '{}'), +(54, 16, 70, 71, 2, 'com_menus.menu.1', 'Main Menu', '{}'), +(55, 18, 106, 107, 2, 'com_modules.module.87', 'Sample Data', '{}'), +(56, 8, 20, 53, 2, 'com_content.workflow.1', 'Joomla! Default', '{}'), +(57, 56, 21, 22, 3, 'com_content.state.1', 'Unpublished', '{}'), +(58, 56, 23, 24, 3, 'com_content.state.2', 'Published', '{}'), +(59, 56, 25, 26, 3, 'com_content.state.3', 'Trashed', '{}'), +(60, 56, 27, 28, 3, 'com_content.state.4', 'Archived', '{}'), +(61, 56, 29, 30, 3, 'com_content.transition.1', 'Unpublish', '{}'), +(62, 56, 31, 32, 3, 'com_content.transition.2', 'Unpublish', '{}'), +(63, 56, 33, 34, 3, 'com_content.transition.3', 'Unpublish', '{}'), +(64, 56, 35, 36, 3, 'com_content.transition.4', 'Publish', '{}'), +(65, 56, 37, 38, 3, 'com_content.transition.5', 'Publish', '{}'), +(66, 56, 39, 40, 3, 'com_content.transition.6', 'Publish', '{}'), +(67, 56, 41, 42, 3, 'com_content.transition.7', 'Trash', '{}'), +(68, 56, 43, 44, 3, 'com_content.transition.8', 'Trash', '{}'), +(69, 56, 45, 46, 3, 'com_content.transition.9', 'Trash', '{}'), +(70, 56, 47, 48, 3, 'com_content.transition.10', 'Archive', '{}'), +(71, 56, 49, 50, 3, 'com_content.transition.11', 'Archive', '{}'), +(72, 56, 51, 52, 3, 'com_content.transition.12', 'Archive', '{}'); -- -------------------------------------------------------- @@ -241,7 +258,7 @@ CREATE TABLE IF NOT EXISTS `#__categories` ( INSERT INTO `#__categories` (`id`, `asset_id`, `parent_id`, `lft`, `rgt`, `level`, `path`, `extension`, `title`, `alias`, `note`, `description`, `published`, `checked_out`, `checked_out_time`, `access`, `params`, `metadesc`, `metakey`, `metadata`, `created_user_id`, `created_time`, `modified_user_id`, `modified_time`, `hits`, `language`, `version`) VALUES (1, 0, 0, 0, 11, 0, '', 'system', 'ROOT', 'root', '', '', 1, 0, '0000-00-00 00:00:00', 1, '{}', '', '', '{}', 42, '2011-01-01 00:00:01', 0, '0000-00-00 00:00:00', 0, '*', 1), -(2, 27, 1, 1, 2, 1, 'uncategorised', 'com_content', 'Uncategorised', 'uncategorised', '', '', 1, 0, '0000-00-00 00:00:00', 1, '{"category_layout":"","image":""}', '', '', '{"author":"","robots":""}', 42, '2011-01-01 00:00:01', 0, '0000-00-00 00:00:00', 0, '*', 1), +(2, 27, 1, 1, 2, 1, 'uncategorised', 'com_content', 'Uncategorised', 'uncategorised', '', '', 1, 0, '0000-00-00 00:00:00', 1, '{"category_layout":"","image":"","workflow_id":"1"}', '', '', '{"author":"","robots":""}', 42, '2011-01-01 00:00:01', 0, '0000-00-00 00:00:00', 0, '*', 1), (3, 28, 1, 3, 4, 1, 'uncategorised', 'com_banners', 'Uncategorised', 'uncategorised', '', '', 1, 0, '0000-00-00 00:00:00', 1, '{"category_layout":"","image":""}', '', '', '{"author":"","robots":""}', 42, '2011-01-01 00:00:01', 0, '0000-00-00 00:00:00', 0, '*', 1), (4, 29, 1, 5, 6, 1, 'uncategorised', 'com_contact', 'Uncategorised', 'uncategorised', '', '', 1, 0, '0000-00-00 00:00:00', 1, '{"category_layout":"","image":""}', '', '', '{"author":"","robots":""}', 42, '2011-01-01 00:00:01', 0, '0000-00-00 00:00:00', 0, '*', 1), (5, 30, 1, 7, 8, 1, 'uncategorised', 'com_newsfeeds', 'Uncategorised', 'uncategorised', '', '', 1, 0, '0000-00-00 00:00:00', 1, '{"category_layout":"","image":""}', '', '', '{"author":"","robots":""}', 42, '2011-01-01 00:00:01', 0, '0000-00-00 00:00:00', 0, '*', 1), @@ -321,7 +338,7 @@ CREATE TABLE IF NOT EXISTS `#__content` ( `alias` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '', `introtext` mediumtext NOT NULL, `fulltext` mediumtext NOT NULL, - `state` tinyint(3) NOT NULL DEFAULT 0, + `state` int(10) NOT NULL DEFAULT 0, `catid` int(10) unsigned NOT NULL DEFAULT 0, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `created_by` int(10) unsigned NOT NULL DEFAULT 0, @@ -518,6 +535,7 @@ INSERT INTO `#__extensions` (`extension_id`, `package_id`, `name`, `type`, `elem (32, 0, 'com_postinstall', 'component', 'com_postinstall', '', 1, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0, 'Joomla\\Component\\Postinstall'), (33, 0, 'com_fields', 'component', 'com_fields', '', 1, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0, 'Joomla\\Component\\Fields'), (34, 0, 'com_associations', 'component', 'com_associations', '', 1, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0, 'Joomla\\Component\\Associations'), +(35, 0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 1, '', '{}', 0, '0000-00-00 00:00:00', 0, 0, 'Joomla\\Component\\Workflow'), (103, 0, 'Joomla! Platform', 'library', 'joomla', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0, ''), (106, 0, 'PHPass', 'library', 'phpass', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0, ''), (200, 0, 'mod_articles_archive', 'module', 'mod_articles_archive', '', 0, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0, ''), @@ -648,7 +666,6 @@ INSERT INTO `#__extensions` (`extension_id`, `package_id`, `name`, `type`, `elem (601, 802, 'English (en-GB)', 'language', 'en-GB', '', 1, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0, ''), (700, 0, 'files_joomla', 'file', 'joomla', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0, ''), (802, 0, 'English (en-GB) Language Pack', 'package', 'pkg_en-GB', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0, ''); - -- -------------------------------------------------------- -- @@ -2111,3 +2128,123 @@ INSERT INTO `#__viewlevels` (`id`, `title`, `ordering`, `rules`) VALUES (3, 'Special', 3, '[6,3,8]'), (5, 'Guest', 1, '[9]'), (6, 'Super Users', 4, '[8]'); + +-- +-- 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, 56, 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, 57, 1, 1, 1, 'Unpublished', '', '0', 0), +(2, 58, 2, 1, 1, 'Published', '', '1', 1), +(3, 59, 3, 1, 1, 'Trashed', '', '-2', 0), +(4, 60, 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, 61, 1, 1, 1, 'Unpublish', '', 2, 1), +(2, 62, 1, 2, 1, 'Unpublish', '', 3, 1), +(3, 63, 1, 3, 1, 'Unpublish', '', 4, 1), +(4, 64, 1, 4, 1, 'Publish', '', 1, 2), +(5, 65, 1, 5, 1, 'Publish', '', 3, 2), +(6, 66, 1, 6, 1, 'Publish', '', 4, 2), +(7, 67, 1, 7, 1, 'Trash', '', 1, 3), +(8, 68, 1, 8, 1, 'Trash', '', 2, 3), +(9, 69, 1, 9, 1, 'Trash', '', 4, 3), +(10, 70, 1, 10, 1, 'Archive', '', 1, 4), +(11, 71, 1, 11, 1, 'Archive', '', 2, 4), +(12, 72, 1, 12, 1, 'Archive', '', 3, 4); diff --git a/language/en-GB/en-GB.com_content.ini b/language/en-GB/en-GB.com_content.ini index 6cdfc93c045d0..14300f6d1ad16 100644 --- a/language/en-GB/en-GB.com_content.ini +++ b/language/en-GB/en-GB.com_content.ini @@ -84,3 +84,5 @@ COM_CONTENT_TITLE_FILTER_LABEL="Title Filter" COM_CONTENT_VOTES="Vote" COM_CONTENT_VOTES_COUNT="Vote: %s" COM_CONTENT_WRITTEN_BY="Written by %s" +COM_CONTENT_STATE="State" +COM_CONTENT_TRANSITION="Transition" diff --git a/language/en-GB/en-GB.mod_articles_archive.ini b/language/en-GB/en-GB.mod_articles_archive.ini index 7736d681c8878..2a5e826c2401f 100644 --- a/language/en-GB/en-GB.mod_articles_archive.ini +++ b/language/en-GB/en-GB.mod_articles_archive.ini @@ -3,8 +3,8 @@ ; 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 -MOD_ARTICLES_ARCHIVE="Articles - Archived" +MOD_ARTICLES_ARCHIVE="Articles - List" MOD_ARTICLES_ARCHIVE_FIELD_COUNT_LABEL="# of Months" -MOD_ARTICLES_ARCHIVE_XML_DESCRIPTION="This module shows a list of the calendar months containing archived articles. After you have changed the status of an article to archived, this list will be automatically generated." +MOD_ARTICLES_ARCHIVE_XML_DESCRIPTION="This module shows a list of the calendar months containing articles. This list will be automatically generated from the chosen state." MOD_ARTICLES_ARCHIVE_DATE="%1$s, %2$s" - +MOD_ARTICLES_ARCHIVE_CHOOSE_STATE="Choose State" diff --git a/language/en-GB/en-GB.mod_articles_archive.sys.ini b/language/en-GB/en-GB.mod_articles_archive.sys.ini index 23be5f2e93729..4f5ad497252a6 100644 --- a/language/en-GB/en-GB.mod_articles_archive.sys.ini +++ b/language/en-GB/en-GB.mod_articles_archive.sys.ini @@ -3,7 +3,7 @@ ; 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 -MOD_ARTICLES_ARCHIVE="Articles - Archived" -MOD_ARTICLES_ARCHIVE_XML_DESCRIPTION="This module shows a list of the calendar months containing Archived Articles. After you have changed the status of an Article to Archived, this list will be automatically generated." +MOD_ARTICLES_ARCHIVE="Articles - List" +MOD_ARTICLES_ARCHIVE_XML_DESCRIPTION="This module shows a list of the calendar months containing articles. This list will be automatically generated from the chosen state." MOD_ARTICLES_ARCHIVE_LAYOUT_DEFAULT="Default" diff --git a/layouts/joomla/edit/global.php b/layouts/joomla/edit/global.php index 6c9ae9cc5efbc..4b6a107b5a94a 100644 --- a/layouts/joomla/edit/global.php +++ b/layouts/joomla/edit/global.php @@ -40,6 +40,11 @@ 'version_note', ); +if ($displayData->get('item')->id !== null) +{ + array_unshift($fields, 'transition'); +} + $hiddenFields = $displayData->get('hidden_fields') ?: array(); if (!$saveHistory) diff --git a/layouts/joomla/html/batch/workflowstate.php b/layouts/joomla/html/batch/workflowstate.php new file mode 100644 index 0000000000000..91f575a9c0856 --- /dev/null +++ b/layouts/joomla/html/batch/workflowstate.php @@ -0,0 +1,33 @@ + + + + 'batch-workflowstate-id', + 'group.label' => 'text', + 'group.items' => null +); + +$groups = JHtml::_('workflowstate.existing', array('title' => JText::_('JLIB_HTML_BATCH_WORKFLOW_STATE_NOCHANGE'))); + +echo JHtml::_('select.groupedlist', $groups, 'batch[workflowstate_id]', $attr); diff --git a/libraries/cms/html/workflowstate.php b/libraries/cms/html/workflowstate.php new file mode 100644 index 0000000000000..f9099f768f9a8 --- /dev/null +++ b/libraries/cms/html/workflowstate.php @@ -0,0 +1,84 @@ +getQuery(true); + + // Build the query. + $query->select( + $db->quoteName( + [ + 'ws.id', + 'ws.title', + 'w.id', + 'w.title' + ], + [ + 'workflow_state_id', + 'workflow_state_title', + 'workflow_id', + 'workflow_title' + ] + ) + ) + ->from('#__workflow_states AS ws') + ->leftJoin($db->quoteName('#__workflows', 'w') . ' ON w.id = ws.workflow_id') + ->order('ws.ordering'); + + // Set the query and load the options. + $states = $db->setQuery($query)->loadObjectList(); + + $workflowStates = array(); + + // Grouping the states by workflow + foreach ($states as $state) + { + // Using workflow ID to differentiate workflows having same title + $workflowStateKey = $state->workflow_title . ' (' . $state->workflow_id . ')'; + + if (!array_key_exists($workflowStateKey, $workflowStates)) + { + $workflowStates[$workflowStateKey] = array(); + } + + $workflowStates[$workflowStateKey][] = HTMLHelper::_('select.option', $state->workflow_state_id, $state->workflow_state_title); + } + + $prefix[] = array( + HTMLHelper::_('select.option', '', $options['title']) + ); + + return array_merge($prefix, $workflowStates); + } +} diff --git a/libraries/src/Form/Field/TransitionField.php b/libraries/src/Form/Field/TransitionField.php new file mode 100644 index 0000000000000..d2058b9d0a6a2 --- /dev/null +++ b/libraries/src/Form/Field/TransitionField.php @@ -0,0 +1,140 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since __DEPLOY_VERSION__ + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result) + { + $this->extension = $element['extension'] ?? 'com_content'; + } + + return $result; + } + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since __DEPLOY_VERSION__ + */ + protected function getOptions() + { + // Let's get the id for the current item, either category or content item. + $jinput = Factory::getApplication()->input; + + // Initialise variable. + $db = Factory::getDbo(); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); + $workflowState = $this->element['workflow_state'] ? (int) $this->element['workflow_state'] : (int) $jinput->getInt('id', 0); + + $query = $db->getQuery(true) + ->select($db->quoteName(['t.id', 't.title', 's.condition'], ['value', 'text', 'condition'])) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->from($db->quoteName('#__workflow_states', 's')) + ->where($db->quoteName('t.from_state_id') . ' = ' . $workflowState) + ->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')); + + $items = $db->setQuery($query)->loadObjectList(); + + if (count($items)) + { + $user = Factory::getUser(); + + $items = array_filter( + $items, + function ($item) use ($user, $extension) + { + return $user->authorise('core.execute.transition', $extension . '.transition.' . $item->value); + } + ); + + // Sort by transition name + $items = ArrayHelper::sortObjects($items, 'value', 1, true, true); + + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $workflow = new Workflow(['extension' => $this->extension]); + + foreach ($items as $item) + { + $conditionName = $workflow->getConditionName($item->condition); + + $item->text .= ' [' . \JText::_($conditionName) . ']'; + } + } + + // Get state title + $query + ->clear() + ->select($db->quoteName('title')) + ->from($db->quoteName('#__workflow_states')) + ->where($db->quoteName('id') . '=' . $workflowState); + + $workflowName = $db->setQuery($query)->loadResult(); + + $default = [\JHtml::_('select.option', '', $workflowName)]; + + $options = array_merge(parent::getOptions(), $items); + + if (count($options)) + { + $default[] = \JHtml::_('select.option', '-1', '--------', ['disable' => true]); + } + + // Merge with defaults + return array_merge($default, $options); + } +} diff --git a/libraries/src/Form/Field/WorkflowStateField.php b/libraries/src/Form/Field/WorkflowStateField.php new file mode 100644 index 0000000000000..3982032f1b8d3 --- /dev/null +++ b/libraries/src/Form/Field/WorkflowStateField.php @@ -0,0 +1,134 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since __DEPLOY_VERSION__ + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $success = parent::setup($element, $value, $group); + + if ($success) + { + if (strlen($element['extension'])) + { + $this->extension = (string) $element['extension']; + } + + if ((string) $element['activeonly'] == '1' || (string) $element['activeonly'] == 'true') + { + $this->activeonly = true; + } + } + + return $success; + } + + /** + * Method to get the field option groups. + * + * @return array The field option objects as a nested array in groups. + * + * @since __DEPLOY_VERSION__ + * @throws \UnexpectedValueException + */ + protected function getGroups() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + // Select distinct states for existing articles + $query + ->select('DISTINCT ' . $db->quoteName('ws.id', 'workflow_state_id')) + ->select($db->quoteName(['ws.title', 'w.title', 'w.id', 'w.ordering'], ['workflow_state_title', 'workflow_title', 'workflow_id', 'ordering'])) + ->from($db->quoteName('#__workflow_states', 'ws')) + ->from($db->quoteName('#__workflows', 'w')) + ->where($db->quoteName('ws.workflow_id') . ' = ' . $db->quoteName('w.id')) + ->where($db->quoteName('w.extension') . ' = ' . $db->quote($this->extension)) + ->order($db->quoteName('w.ordering')); + + if ($this->activeonly) + { + $query + ->from($db->quoteName('#__workflow_associations', 'wa')) + ->where($db->quoteName('wa.state_id') . ' = ' . $db->quoteName('ws.id')) + ->where($db->quoteName('wa.extension') . ' = ' . $db->quote($this->extension)); + + } + + $states = $db->setQuery($query)->loadObjectList(); + + $workflowStates = array(); + + // Grouping the states by workflow + foreach ($states as $state) + { + // Using workflow ID to differentiate workflows having same title + $workflowStateKey = $state->workflow_title . ' (' . $state->workflow_id . ')'; + + if (!array_key_exists($workflowStateKey, $workflowStates)) + { + $workflowStates[$workflowStateKey] = array(); + } + + $workflowStates[$workflowStateKey][] = HTMLHelper::_('select.option', $state->workflow_state_id, $state->workflow_state_title); + } + + // Merge any additional options in the XML definition. + return array_merge(parent::getGroups(), $workflowStates); + } +} diff --git a/libraries/src/MVC/Controller/AdminController.php b/libraries/src/MVC/Controller/AdminController.php index 548c5fa1fb4d1..ccb7ce7a597ff 100644 --- a/libraries/src/MVC/Controller/AdminController.php +++ b/libraries/src/MVC/Controller/AdminController.php @@ -79,6 +79,9 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu $this->registerTask('orderup', 'reorder'); $this->registerTask('orderdown', 'reorder'); + // Transition + $this->registerTask('runTransition', 'runTransition'); + // Guess the option as com_NameOfController. if (empty($this->option)) { @@ -408,4 +411,49 @@ public function saveOrderAjax() // Close the application $this->app->close(); } + + /** + * Method to run Transition by id of item. + * + * @return void + * + * @since 4.0 + */ + public function runTransition() + { + // Get the input + $pks = $this->input->post->get('cid', array(), 'array'); + + if (!count($pks)) + { + return false; + } + + $pk = (int) $pks[0]; + + $transitionId = $this->input->post->get('transition_' . $pk, -1, 'int'); + + // Get the model + $model = $this->getModel(); + $return = $model->runTransition($pk, $transitionId); + + if ($return === false) + { + // Reorder failed. + $message = \JText::sprintf('JLIB_APPLICATION_ERROR_RUN_TRANSITION', $model->getError()); + $this->setRedirect(\JRoute::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false), $message, 'error'); + + return false; + } + else + { + // Reorder succeeded. + $message = \JText::_('JLIB_APPLICATION_SUCCESS_RUN_TRANSITION'); + $this->setRedirect(\JRoute::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false), $message); + + return true; + } + + $this->setRedirect(\JRoute::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $extensionURL, false)); + } } diff --git a/libraries/src/MVC/Model/AdminModel.php b/libraries/src/MVC/Model/AdminModel.php index bc17bf823437d..30ce246644d7c 100644 --- a/libraries/src/MVC/Model/AdminModel.php +++ b/libraries/src/MVC/Model/AdminModel.php @@ -91,6 +91,7 @@ abstract class AdminModel extends FormModel 'assetgroup_id' => 'batchAccess', 'language_id' => 'batchLanguage', 'tag' => 'batchTag', + 'workflowstate_id' => 'batchWorkflowState', ); /** diff --git a/libraries/src/Table/Module.php b/libraries/src/Table/Module.php index c962cf1a0ff75..d8ead8521ea84 100644 --- a/libraries/src/Table/Module.php +++ b/libraries/src/Table/Module.php @@ -197,6 +197,12 @@ public function store($updateNulls = false) $this->publish_down = $this->_db->getNullDate(); } + if (!$this->ordering) + { + $query = $this->_db->getQuery(true); + $this->ordering = $this->getNextOrder($query->qn('position') . ' = ' . $query->q($this->position)); + } + return parent::store($updateNulls); } } diff --git a/libraries/src/Workflow/Workflow.php b/libraries/src/Workflow/Workflow.php new file mode 100644 index 0000000000000..2015b5548fe5a --- /dev/null +++ b/libraries/src/Workflow/Workflow.php @@ -0,0 +1,327 @@ + 'COM_WORKFLOW_PUBLISHED', + self::UNPUBLISHED => 'COM_WORKFLOW_UNPUBLISHED', + self::TRASHED => 'COM_WORKFLOW_TRASHED' + ]; + + protected $db; + + /** + * Every item with a state which has the condition PUBLISHED is visible/active on the page + */ + const PUBLISHED = 1; + + /** + * Every item with a state which has the condition UNPUBLISHED is not visible/inactive on the page + */ + const UNPUBLISHED = 0; + + /** + * Every sitem with a state which has the condition TRASHED is trashed + */ + const TRASHED = -2; + + /** + * Class constructor + * + * @param array $options Array of options + * + * @since __DEPLOY_VERSION__ + */ + public function __construct($options) + { + // Required options + $this->extension = $options['extension']; + + // Default some optional options + $this->options['access'] = 'true'; + $this->options['published'] = 1; + $this->options['countItems'] = 0; + + $this->setOptions($options); + } + + /** + * Returns the translated condition name, based on the given number + * + * @param int $value The condition ID + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getConditionName($value) + { + return ArrayHelper::getValue($this->names, $value, '', 'string'); + } + + /** + * Executes a transition to change the current state in the association table + * + * @param array|int $pks The item IDs, which should use the transition + * @param int $transition_id The transition which should be executed + * + * @return boolean + */ + public function executeTransition($pks, $transition_id) + { + if (!is_array($pks)) + { + $pks = [(int) $pks]; + } + + // Check if there are any non numeric values + if (count(array_filter($pks, function($value) { return !is_numeric($value); }))) + { + return false; + } + + $pks = ArrayHelper::toInteger($pks); + + $db = Factory::getDbo(); + + $query = $db->getQuery(true); + + $select = $db->quoteName( + [ + 'id', + 'to_state_id', + 'from_state_id' + ] + ); + + $query ->select($select) + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('id') . ' = ' . (int) $transition_id); + + if (!empty($this->options['published'])) + { + $query ->where($db->quoteName('published') . ' = 1'); + } + + $transition = $db->setQuery($query)->loadObject(); + + // Check if the items can exetute this transition + foreach ($pks as $pk) + { + $assoc = $this->getAssociation($pk); + + if ($assoc->state_id != $transition->from_state_id) + { + return false; + } + } + + $parts = explode('.', $this->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, 'executeTransition'])) + { + return call_user_func([$class, 'executeTransition']); + } + + return $this->updateAssociations($pks, $transition->to_state_id); + } + + /** + * Creates an association for the workflow_associations table + * + * @param int $pk ID of the item + * @param int $state ID of state + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function createAssociation($pk, $state) + { + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query + ->insert($db->quoteName('#__workflow_associations')) + ->columns($db->quoteName(array('item_id', 'state_id', 'extension'))) + ->values((int) $pk . ', ' . (int) $state . ', ' . $db->quote($this->extension)); + + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + return false; + } + + return true; + } + + /** + * Update an existing association with a new state + * + * @param array $pks An Array of item IDs which should be changed + * @param int $state The new state ID + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function updateAssociations($pks, $state) + { + if (!is_array($pks)) + { + $pks = [$pks]; + } + + $pks = ArrayHelper::toInteger($pks); + + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query + ->update($db->quoteName('#__workflow_associations')) + ->set($db->quoteName('state_id') . '=' . (int) $state) + ->where($db->quoteName('item_id') . ' IN(' . implode(',', $pks) . ')') + ->where($db->quoteName('extension') . '=' . $db->quote($this->extension)); + + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + return false; + } + + return true; + } + + /** + * Removes associations form the workflow_associations table + * + * @param int $pks ID of content + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function deleteAssociation($pks) + { + $pks = ArrayHelper::toInteger($pks); + + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query + ->delete($db->quoteName('#__workflow_associations')) + ->where($db->quoteName('item_id') . ' IN (' . implode(',', $pks) . ')') + ->andWhere($db->quoteName('extension') . '=' . $db->quote($this->extension)); + + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + return false; + } + + return true; + } + + /** + * Loads an existing association item with state and item ID + * + * @param int $item_id The item ID to load + * + * @return object + * + * @since __DEPLOY_VERSION__ + */ + public function getAssociation($item_id) + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true); + + $select = $db->quoteName( + [ + 'item_id', + 'state_id' + ] + ); + + $query ->select($select) + ->from($db->quoteName('#__workflow_associations')) + ->where($db->quoteName('item_id') . ' = ' . (int) $item_id) + ->where($db->quoteName('extension') . ' = ' . $db->quote($this->extension)); + + return $db->setQuery($query)->loadObject(); + } + + /** + * Allows to set some optional options, eg. if the access level should be considered. + * + * @param array $options The new options + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setOptions(array $options) + { + if (isset($options['access'])) + { + $this->options['access'] = $options['access']; + } + + if (isset($options['published'])) + { + $this->options['published'] = $options['published']; + } + + if (isset($options['countItems'])) + { + $this->options['countItems'] = $options['countItems']; + } + } +} diff --git a/media/system/js/core.js b/media/system/js/core.js index 1072530a70c3d..f19546dee4e70 100644 --- a/media/system/js/core.js +++ b/media/system/js/core.js @@ -324,6 +324,76 @@ Joomla.editors.instances = Joomla.editors.instances || { 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 diff --git a/media/system/js/core.min.js b/media/system/js/core.min.js index bb9e71b6ff2c9..faab9bd5bc039 100644 --- a/media/system/js/core.min.js +++ b/media/system/js/core.min.js @@ -1 +1 @@ -Joomla=window.Joomla||{},Joomla.editors=Joomla.editors||{},Joomla.editors.instances=Joomla.editors.instances||{},function(e,t){"use strict";e.submitform=function(e,o,n){o||(o=t.getElementById("adminForm")),e&&(o.task.value=e),o.noValidate=!n,n?o.hasAttribute("novalidate")&&o.removeAttribute("novalidate"):o.setAttribute("novalidate","");var r=t.createElement("input");r.style.display="none",r.type="submit",o.appendChild(r).click(),o.removeChild(r)},e.submitbutton=function(o){var n=t.querySelectorAll("form.form-validate");if(n.length>0)for(var r=0,s=n.length;r-1?(f="notice"===i?"info":i,f="message"===i?"success":f,f="error"===i?"danger":f):f="info",c.setAttribute("type",f),c.setAttribute("dismiss","true"),s&&parseInt(s)>0&&c.setAttribute("autodismiss",s);else{c=t.createElement("div"),["notice","message","error"].indexOf(i)>-1?(f="notice"===i?"info":i,f="message"===i?"success":f,f="error"===i?"danger":f):f="info",c.className="alert "+f;var p=t.createElement("button");p.setAttribute("type","button"),p.setAttribute("data-dismiss","alert"),p.className="close",p.innerHTML="×",c.appendChild(p)}for(void 0!==e.JText._(i)&&((d=t.createElement("h4")).className="alert-heading",d.innerHTML=e.JText._(i)?e.JText._(i):i,c.appendChild(d)),m=l.length-1;m>=0;m--)(u=t.createElement("div")).innerHTML=l[m],c.appendChild(u);a.appendChild(c),"object"!=typeof window.customElements&&"function"!=typeof window.customElements.get("joomla-alert")&&s&&parseInt(s)>0&&setTimeout(function(){e.removeMessages(a)},s)}},e.removeMessages=function(e){var o;if(o=e||t.getElementById("system-message-container"),"object"==typeof window.customElements&&window.customElements.get("joomla-alert")){var n=o.querySelectorAll("joomla-alert");if(n.length)for(var r=0,s=n.length;r=0;i--)a.unshift(["&#",s[i].charCodeAt(),";"].join(""));s=a.join(""),r.error=[e.JText._("JLIB_JS_AJAX_ERROR_PARSE").replace("%s",s)]}else"nocontent"===o?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_NO_CONTENT")]:"timeout"===o?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_TIMEOUT")]:"abort"===o?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_CONNECTION_ABORT")]:t.responseJSON&&t.responseJSON.message?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_OTHER").replace("%s",t.status)+" "+t.responseJSON.message+""]:t.statusText?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_OTHER").replace("%s",t.status)+" "+t.statusText+""]:r.error=[e.JText._("JLIB_JS_AJAX_ERROR_OTHER").replace("%s",t.status)];return r},e.isChecked=function(e,o){if(void 0===o&&(o=t.getElementById("adminForm")),o.boxchecked.value=e?parseInt(o.boxchecked.value)+1:parseInt(o.boxchecked.value)-1,o.elements["checkall-toggle"]){var n,r,s,a=!0;for(n=0,s=o.elements.length;n-1?n.options[r].value:null},window.listItemTask=function(t,o){return e.listItemTask(t,o)},e.listItemTask=function(e,o){var n,r=t.adminForm,s=0,a=r[e];if(!a)return!1;for(;n=r["cb"+s];)n.checked=!1,s++;return a.checked=!0,r.boxchecked.value=1,window.submitform(o),!1},window.submitbutton=function(t){e.submitbutton(t)},window.submitform=function(t){e.submitform(t)},window.saveorder=function(e,t){window.checkAll_button(e,t)},window.checkAll_button=function(o,n){var r,s;for(n=n||"saveorder",r=0;r<=o;r++){if(!(s=t.adminForm["cb"+r]))return void alert("You cannot change the order of items, as an item in the list is `Checked Out`");s.checked=!0}e.submitform(n)},e.loadingLayer=function(o,n){if(o=o||"show",n=n||t.body,"load"===o){var r=(e.getOptions("system.paths")||{}).root||"",s=t.createElement("div");s.id="loading-logo",s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="100%",s.style.height="100%",s.style.opacity="0.8",s.style.filter="alpha(opacity=80)",s.style.overflow="hidden",s.style["z-index"]="10000",s.style.display="none",s.style["background-color"]="#fff",s.style["background-image"]='url("'+r+'/media/system/images/ajax-loader.gif")',s.style["background-position"]="center",s.style["background-repeat"]="no-repeat",s.style["background-attachment"]="fixed",n.appendChild(s)}else t.getElementById("loading-logo")||e.loadingLayer("load",n),t.getElementById("loading-logo").style.display="show"==o?"block":"none";return t.getElementById("loading-logo")},e.extend=function(e,t){for(var o in null===e&&(e={}),t)t.hasOwnProperty(o)&&(e[o]=t[o]);return e},e.request=function(t){t=e.extend({url:"",method:"GET",data:null,perform:!0},t);try{var o=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("MSXML2.XMLHTTP.3.0");if(o.open(t.method,t.url,!0),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("X-Ajax-Engine","Joomla!"),"GET"!==t.method){var n=e.getOptions("csrf.token","");n&&o.setRequestHeader("X-CSRF-Token",n),t.headers&&t.headers["Content-Type"]||o.setRequestHeader("Content-Type","application/x-www-form-urlencoded")}if(t.headers)for(var r in t.headers)t.headers.hasOwnProperty(r)&&o.setRequestHeader(r,t.headers[r]);if(o.onreadystatechange=function(){4===o.readyState&&(200===o.status?t.onSuccess&&t.onSuccess.call(window,o.responseText,o):t.onError&&t.onError.call(window,o))},t.perform){if(t.onBefore&&!1===t.onBefore.call(window,o))return o;o.send(t.data)}}catch(e){return window.console&&console.log(e),!1}return o},e.localStorageEnabled=function(){var e="joomla-cms";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch(e){return!1}},e.WebComponents=function(){var o=[],n=e.getOptions("webcomponents"),r=function(){n&&n.length&&n.forEach(function(e){if(e.match(/\.js/g)){var o,n=t.createElement("script");if(function(){try{return new Function("(a = 0) => a"),!0}catch(e){return!1}}())n.src=e;else e.match(/\.min\.js/g)?o=e.replace(/\.min\.js/g,"-es5.min.js"):e.match(/\.js/g)&&(o=e.replace(/\.js/g,"-es5.js")),n.src=o}n&&t.head.appendChild(n)})};if("import"in t.createElement("link")||o.push("hi"),(!("attachShadow"in Element.prototype&&"getRootNode"in Element.prototype)||window.ShadyDOM&&window.ShadyDOM.force)&&o.push("sd"),window.customElements&&!window.customElements.forcePolyfill||o.push("ce"),"content"in t.createElement("template")&&window.Promise&&Array.from&&t.createDocumentFragment().cloneNode()instanceof DocumentFragment||(o=["lite"]),o.length&&n&&n.length){var s="core.min.js",a=t.querySelector('script[src*="'+s+'"]');if(a||(s="core.js",a=t.querySelector('script[src*="'+s+'"]')),!a)throw new Error("core(.min).js is not registered correctly!");var i=t.createElement("script"),l="media/vendor/webcomponentsjs/js/webcomponents-"+o.join("-")+".min.js",c=a.src.match(/\?.*/)[0],d=e.getOptions("system.paths");if(!d)throw new Error("core(.min).js is not registered correctly!");i.src=d.rootFull+l+(c||""),t.head.appendChild(i),t.addEventListener("WebComponentsReady",function(){r()})}else{if(!n||!n.length)return;var m=function(){requestAnimationFrame(function(){t.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0})),r()})};"loading"!==t.readyState?m():t.addEventListener("readystatechange",function e(){m(),t.removeEventListener("readystatechange",e)})}}}(Joomla,document),function(e,t){"use strict";t.Event||(t.Event={},t.Event.dispatch=function(t,o,n){var r;"string"==typeof t&&(n=o,o=t,t=e),n=n||{},e.CustomEvent&&"function"==typeof e.CustomEvent?r=new CustomEvent(o,{detail:n,bubbles:!0,cancelable:!0}):((r=document.createEvent("Event")).initEvent(o,!0,!0),r.detail=n),t.dispatchEvent(r)},t.Event.listenOnce=function(e,t,o){var n=function(r){return e.removeEventListener(t,n),o.call(e,r)};e.addEventListener(t,n)})}(window,Joomla),document.addEventListener("DOMContentLoaded",function(){Joomla.WebComponents()}); \ No newline at end of file +Joomla=window.Joomla||{},Joomla.editors=Joomla.editors||{},Joomla.editors.instances=Joomla.editors.instances||{},function(e,t){"use strict";e.submitform=function(e,o,n){o||(o=t.getElementById("adminForm")),e&&(o.task.value=e),o.noValidate=!n,n?o.hasAttribute("novalidate")&&o.removeAttribute("novalidate"):o.setAttribute("novalidate","");var r=t.createElement("input");r.style.display="none",r.type="submit",o.appendChild(r).click(),o.removeChild(r)},e.submitbutton=function(o){var n=t.querySelectorAll("form.form-validate");if(n.length>0)for(var r=0,s=n.length;r-1?(f="notice"===i?"info":i,f="message"===i?"success":f,f="error"===i?"danger":f):f="info",c.setAttribute("type",f),c.setAttribute("dismiss","true"),s&&parseInt(s)>0&&c.setAttribute("autodismiss",s);else{c=t.createElement("div"),["notice","message","error"].indexOf(i)>-1?(f="notice"===i?"info":i,f="message"===i?"success":f,f="error"===i?"danger":f):f="info",c.className="alert "+f;var p=t.createElement("button");p.setAttribute("type","button"),p.setAttribute("data-dismiss","alert"),p.className="close",p.innerHTML="×",c.appendChild(p)}for(void 0!==e.JText._(i)&&((d=t.createElement("h4")).className="alert-heading",d.innerHTML=e.JText._(i)?e.JText._(i):i,c.appendChild(d)),m=l.length-1;m>=0;m--)(u=t.createElement("div")).innerHTML=l[m],c.appendChild(u);a.appendChild(c),"object"!=typeof window.customElements&&"function"!=typeof window.customElements.get("joomla-alert")&&s&&parseInt(s)>0&&setTimeout(function(){e.removeMessages(a)},s)}},e.removeMessages=function(e){var o;if(o=e||t.getElementById("system-message-container"),"object"==typeof window.customElements&&window.customElements.get("joomla-alert")){var n=o.querySelectorAll("joomla-alert");if(n.length)for(var r=0,s=n.length;r=0;i--)a.unshift(["&#",s[i].charCodeAt(),";"].join(""));s=a.join(""),r.error=[e.JText._("JLIB_JS_AJAX_ERROR_PARSE").replace("%s",s)]}else"nocontent"===o?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_NO_CONTENT")]:"timeout"===o?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_TIMEOUT")]:"abort"===o?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_CONNECTION_ABORT")]:t.responseJSON&&t.responseJSON.message?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_OTHER").replace("%s",t.status)+" "+t.responseJSON.message+""]:t.statusText?r.error=[e.JText._("JLIB_JS_AJAX_ERROR_OTHER").replace("%s",t.status)+" "+t.statusText+""]:r.error=[e.JText._("JLIB_JS_AJAX_ERROR_OTHER").replace("%s",t.status)];return r},e.isChecked=function(e,o){if(void 0===o&&(o=t.getElementById("adminForm")),o.boxchecked.value=e?parseInt(o.boxchecked.value)+1:parseInt(o.boxchecked.value)-1,o.elements["checkall-toggle"]){var n,r,s,a=!0;for(n=0,s=o.elements.length;n-1?n.options[r].value:null},window.listItemTask=function(t,o){return e.listItemTask(t,o)},e.listItemTask=function(e,o){var n,r=t.adminForm,s=0,a=r[e];if(!a)return!1;for(;n=r["cb"+s];)n.checked=!1,s++;return a.checked=!0,r.boxchecked.value=1,window.submitform(o),!1},window.submitbutton=function(t){e.submitbutton(t)},window.submitform=function(t){e.submitform(t)},window.saveorder=function(e,t){window.checkAll_button(e,t)},window.checkAll_button=function(o,n){var r,s;for(n=n||"saveorder",r=0;r<=o;r++){if(!(s=t.adminForm["cb"+r]))return void alert("You cannot change the order of items, as an item in the list is `Checked Out`");s.checked=!0}e.submitform(n)},e.loadingLayer=function(o,n){if(o=o||"show",n=n||t.body,"load"===o){var r=(e.getOptions("system.paths")||{}).root||"",s=t.createElement("div");s.id="loading-logo",s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="100%",s.style.height="100%",s.style.opacity="0.8",s.style.filter="alpha(opacity=80)",s.style.overflow="hidden",s.style["z-index"]="10000",s.style.display="none",s.style["background-color"]="#fff",s.style["background-image"]='url("'+r+'/media/system/images/ajax-loader.gif")',s.style["background-position"]="center",s.style["background-repeat"]="no-repeat",s.style["background-attachment"]="fixed",n.appendChild(s)}else t.getElementById("loading-logo")||e.loadingLayer("load",n),t.getElementById("loading-logo").style.display="show"==o?"block":"none";return t.getElementById("loading-logo")},e.extend=function(e,t){for(var o in null===e&&(e={}),t)t.hasOwnProperty(o)&&(e[o]=t[o]);return e},e.request=function(t){t=e.extend({url:"",method:"GET",data:null,perform:!0},t);try{var o=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("MSXML2.XMLHTTP.3.0");if(o.open(t.method,t.url,!0),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("X-Ajax-Engine","Joomla!"),"GET"!==t.method){var n=e.getOptions("csrf.token","");n&&o.setRequestHeader("X-CSRF-Token",n),t.headers&&t.headers["Content-Type"]||o.setRequestHeader("Content-Type","application/x-www-form-urlencoded")}if(t.headers)for(var r in t.headers)t.headers.hasOwnProperty(r)&&o.setRequestHeader(r,t.headers[r]);if(o.onreadystatechange=function(){4===o.readyState&&(200===o.status?t.onSuccess&&t.onSuccess.call(window,o.responseText,o):t.onError&&t.onError.call(window,o))},t.perform){if(t.onBefore&&!1===t.onBefore.call(window,o))return o;o.send(t.data)}}catch(e){return window.console&&console.log(e),!1}return o},e.localStorageEnabled=function(){var e="joomla-cms";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch(e){return!1}},e.WebComponents=function(){var o=[],n=e.getOptions("webcomponents"),r=function(){n&&n.length&&n.forEach(function(e){if(e.match(/\.js/g)){var o,n=t.createElement("script");if(function(){try{return new Function("(a = 0) => a"),!0}catch(e){return!1}}())n.src=e;else e.match(/\.min\.js/g)?o=e.replace(/\.min\.js/g,"-es5.min.js"):e.match(/\.js/g)&&(o=e.replace(/\.js/g,"-es5.js")),n.src=o}n&&t.head.appendChild(n)})};if("import"in t.createElement("link")||o.push("hi"),(!("attachShadow"in Element.prototype&&"getRootNode"in Element.prototype)||window.ShadyDOM&&window.ShadyDOM.force)&&o.push("sd"),window.customElements&&!window.customElements.forcePolyfill||o.push("ce"),"content"in t.createElement("template")&&window.Promise&&Array.from&&t.createDocumentFragment().cloneNode()instanceof DocumentFragment||(o=["lite"]),o.length&&n&&n.length){var s="core.min.js",a=t.querySelector('script[src*="'+s+'"]');if(a||(s="core.js",a=t.querySelector('script[src*="'+s+'"]')),!a)throw new Error("core(.min).js is not registered correctly!");var i=t.createElement("script"),l="media/vendor/webcomponentsjs/js/webcomponents-"+o.join("-")+".min.js",c=a.src.match(/\?.*/)[0],d=e.getOptions("system.paths");if(!d)throw new Error("core(.min).js is not registered correctly!");i.src=d.rootFull+l+(c||""),t.head.appendChild(i),t.addEventListener("WebComponentsReady",function(){r()})}else{if(!n||!n.length)return;var m=function(){requestAnimationFrame(function(){t.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0})),r()})};"loading"!==t.readyState?m():t.addEventListener("readystatechange",function e(){m(),t.removeEventListener("readystatechange",e)})}}}(Joomla,document),function(e,t){"use strict";t.Event||(t.Event={},t.Event.dispatch=function(t,o,n){var r;"string"==typeof t&&(n=o,o=t,t=e),n=n||{},e.CustomEvent&&"function"==typeof e.CustomEvent?r=new CustomEvent(o,{detail:n,bubbles:!0,cancelable:!0}):((r=document.createEvent("Event")).initEvent(o,!0,!0),r.detail=n),t.dispatchEvent(r)},t.Event.listenOnce=function(e,t,o){var n=function(r){return e.removeEventListener(t,n),o.call(e,r)};e.addEventListener(t,n)})}(window,Joomla),document.addEventListener("DOMContentLoaded",function(){Joomla.WebComponents()}); diff --git a/modules/mod_articles_archive/Helper/ArticlesArchiveHelper.php b/modules/mod_articles_archive/Helper/ArticlesArchiveHelper.php index 8ecbc2a7c4598..ccdddc99a574b 100644 --- a/modules/mod_articles_archive/Helper/ArticlesArchiveHelper.php +++ b/modules/mod_articles_archive/Helper/ArticlesArchiveHelper.php @@ -12,6 +12,7 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\Utilities\ArrayHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Router\Route; @@ -39,14 +40,23 @@ public static function getList(&$params) // Get database $db = Factory::getDbo(); + $states = (array) $params->get('state', [4]); + $query = $db->getQuery(true); $query->select($query->month($db->quoteName('created')) . ' AS created_month') ->select('MIN(' . $db->quoteName('created') . ') AS created') ->select($query->year($db->quoteName('created')) . ' AS created_year') - ->from('#__content') - ->where('state = 2') - ->group($query->year($db->quoteName('created')) . ', ' . $query->month($db->quoteName('created'))) - ->order($query->year($db->quoteName('created')) . ' DESC, ' . $query->month($db->quoteName('created')) . ' DESC'); + ->from($db->qn('#__content', 'c')) + ->innerJoin($db->qn('#__workflow_associations', 'wa') . ' ON wa.item_id = c.id') + ->group($query->year($db->quoteName('c.created')) . ', ' . $query->month($db->quoteName('c.created'))) + ->order($query->year($db->quoteName('c.created')) . ' DESC, ' . $query->month($db->quoteName('c.created')) . ' DESC'); + + if (!empty($states)) + { + $states = ArrayHelper::toInteger($states); + + $query->where($db->qn('wa.state_id') . ' IN (' . implode(', ', $states) . ')'); + } // Filter by language if ($app->getLanguageFilter()) @@ -64,7 +74,7 @@ public static function getList(&$params) { $app->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); - return; + return []; } $menu = $app->getMenu(); @@ -74,6 +84,16 @@ public static function getList(&$params) $i = 0; $lists = array(); + $states = array_map( + function ($el) + { + return '&state[]=' . (int) $el; + }, + $states + ); + + $states = implode($states); + foreach ($rows as $row) { $date = Factory::getDate($row->created); @@ -86,7 +106,9 @@ public static function getList(&$params) $lists[$i] = new \stdClass; - $lists[$i]->link = Route::_('index.php?option=com_content&view=archive&year=' . $created_year . '&month=' . $created_month . $itemid); + $route = 'index.php?option=com_content&view=archive' . $states . '&year=' . $created_year . '&month=' . $created_month . $itemid; + + $lists[$i]->link = Route::_($route); $lists[$i]->text = Text::sprintf('MOD_ARTICLES_ARCHIVE_DATE', $month_name_cal, $created_year_cal); $i++; diff --git a/modules/mod_articles_archive/mod_articles_archive.php b/modules/mod_articles_archive/mod_articles_archive.php index 376dadc003493..a869e6122a86d 100644 --- a/modules/mod_articles_archive/mod_articles_archive.php +++ b/modules/mod_articles_archive/mod_articles_archive.php @@ -13,6 +13,7 @@ use Joomla\Module\ArticlesArchive\Site\Helper\ArticlesArchiveHelper; $params->def('count', 10); +$params->def('state', 0); $list = ArticlesArchiveHelper::getList($params); require ModuleHelper::getLayoutPath('mod_articles_archive', $params->get('layout', 'default')); diff --git a/modules/mod_articles_archive/mod_articles_archive.xml b/modules/mod_articles_archive/mod_articles_archive.xml index 367048dcda373..28ec881b1c95a 100644 --- a/modules/mod_articles_archive/mod_articles_archive.xml +++ b/modules/mod_articles_archive/mod_articles_archive.xml @@ -29,6 +29,16 @@ label="MOD_ARTICLES_ARCHIVE_FIELD_COUNT_LABEL" default="10" /> + + +
diff --git a/modules/mod_related_items/Helper/RelatedItemsHelper.php b/modules/mod_related_items/Helper/RelatedItemsHelper.php index 88d3516e6f6fa..598e61b81ca3f 100644 --- a/modules/mod_related_items/Helper/RelatedItemsHelper.php +++ b/modules/mod_related_items/Helper/RelatedItemsHelper.php @@ -126,8 +126,9 @@ public static function getList(&$params) ->from('#__content AS a') ->join('LEFT', '#__content_frontpage AS f ON f.content_id = a.id') ->join('LEFT', '#__categories AS cc ON cc.id = a.catid') + ->join('LEFT', '#__workflow_states AS ws ON ws.id = a.state') ->where('a.id != ' . (int) $id) - ->where('a.state = 1') + ->where('ws.condition = 1') ->where('a.access IN (' . $groups . ')'); $wheres = array(); diff --git a/modules/mod_stats/Helper/StatsHelper.php b/modules/mod_stats/Helper/StatsHelper.php index d1116ed31aed0..ee2a6657e4402 100644 --- a/modules/mod_stats/Helper/StatsHelper.php +++ b/modules/mod_stats/Helper/StatsHelper.php @@ -94,7 +94,8 @@ public static function &getList(&$params) $query->clear() ->select('COUNT(id) AS count_items') ->from('#__content') - ->where('state = 1'); + ->join('LEFT', '#__workflow_states AS ws ON ws.id = state') + ->where('ws.condition = 1'); $db->setQuery($query); try @@ -128,7 +129,8 @@ public static function &getList(&$params) $query->clear() ->select('SUM(hits) AS count_hits') ->from('#__content') - ->where('state = 1'); + ->join('LEFT', '#__workflow_states AS ws ON ws.id = state') + ->where('ws.condition = 1'); $db->setQuery($query); try diff --git a/plugins/content/joomla/joomla.php b/plugins/content/joomla/joomla.php index 50a60e59c06f9..30a3dc8ffcaa7 100644 --- a/plugins/content/joomla/joomla.php +++ b/plugins/content/joomla/joomla.php @@ -17,6 +17,9 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Component\ComponentHelper; use Joomla\Component\Messages\Administrator\Model\MessageModel; +use Joomla\Component\Content\Administrator\Table\ArticleTable; +use Joomla\CMS\Workflow\Workflow; +use Joomla\Utilities\ArrayHelper; /** * Example Content Plugin @@ -25,6 +28,8 @@ */ class PlgContentJoomla extends CMSPlugin { + protected $db; + /** * Example after save content method * Article is passed by reference, but after the save, so no changes will be saved. @@ -115,11 +120,30 @@ public function onContentAfterSave($context, $article, $isNew) public function onContentBeforeDelete($context, $data) { // Skip plugin if we are deleting something other than categories - if ($context !== 'com_categories.category') + if (!in_array($context, ['com_categories.category', 'com_workflow.state'])) { return true; } + switch ($context) + { + case 'com_categories.category': + return $this->_canDeleteCategories($data); + + case 'com_workflow.state': + return $this->_canDeleteStates($data->id); + } + } + + /** + * Checks if a given category can be deleted + * + * @param object $data The category object + * + * @return boolean + */ + private function _canDeleteCategories($data) + { // Check if this function is enabled. if (!$this->params->def('check_categories', 1)) { @@ -182,9 +206,61 @@ public function onContentBeforeDelete($context, $data) } } } + } + + return $result; + } - return $result; + /** + * Checks if a given state can be deleted + * + * @param int $pk The state ID + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + private function _canDeleteStates($pk) + { + // Check if this function is enabled. + if (!$this->params->def('check_states', 1)) + { + return true; } + + $extension = Factory::getApplication()->input->getString('extension'); + + // Default to true if not a core extension + $result = true; + + $tableInfo = [ + 'com_content' => array('table_name' => '#__content') + ]; + + // Now check to see if this is a known core extension + if (isset($tableInfo[$extension])) + { + // See if this category has any content items + $count = $this->_countItemsFromState($extension, $pk, $tableInfo[$extension]); + + // Return false if db error + if ($count === false) + { + $result = false; + } + else + { + // Show error if items are found assigned to the state + if ($count > 0) + { + $msg = Text::_('COM_WORKFLOW_MSG_DELETE_IS_ASSIGNED'); + Factory::getApplication()->enqueueMessage($msg, 'error'); + $result = false; + } + } + } + + return $result; } /** @@ -279,6 +355,42 @@ private function _countItemsInChildren($table, $catid, $data) } } + /** + * Get count of items assigned to a state + * + * @param string $extension The extension to search for + * @param integer $state_id ID of the state to check + * @param string $table The table to search for + * + * @return mixed count of items found or false if db error + * + * @since __DEPLOY_VERSION__ + */ + private function _countItemsFromState($extension, $state_id, $table) + { + $query = $this->db->getQuery(true); + + $query ->select('COUNT(' . $this->db->quoteName('wa.item_id') . ')') + ->from($query->quoteName('#__workflow_associations', 'wa')) + ->from($this->db->quoteName($table, 'b')) + ->where($this->db->quoteName('wa.item_id') . ' = ' . $query->quoteName('b.id')) + ->where($this->db->quoteName('wa.state_id') . ' = ' . (int) $state_id) + ->where($this->db->quoteName('wa.extension') . ' = ' . $this->db->quote($extension)); + + try + { + $count = $this->db->setQuery($query)->loadResult(); + } + catch (RuntimeException $e) + { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + return $count; + } + /** * Change the state in core_content if the state in a table is changed * @@ -292,18 +404,120 @@ private function _countItemsInChildren($table, $catid, $data) */ public function onContentChangeState($context, $pks, $value) { + $pks = ArrayHelper::toInteger($pks); + + if ($context == 'com_workflow.state' && $value == -2) + { + foreach ($pks as $pk) + { + if (!$this->_canDeleteStates($pk)) + { + return false; + } + } + + return true; + } + + // Check if this function is enabled. + if (!$this->params->def('email_new_state', 0) || $context != 'com_content.article') + { + return true; + } + $db = Factory::getDbo(); $query = $db->getQuery(true) ->select($db->quoteName('core_content_id')) ->from($db->quoteName('#__ucm_content')) ->where($db->quoteName('core_type_alias') . ' = ' . $db->quote($context)) - ->where($db->quoteName('core_content_item_id') . ' IN (' . $pksImploded = implode(',', $pks) . ')'); + ->where($db->quoteName('core_content_item_id') . ' IN (' . implode(',', $pks) . ')'); $db->setQuery($query); $ccIds = $db->loadColumn(); $cctable = new CoreContent($db); $cctable->publish($ccIds, $value); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $users = (array) $db->setQuery($query)->loadColumn(); + + if (empty($users)) + { + return true; + } + + $user = JFactory::getUser(); + + // Messaging for changed items + $default_language = JComponentHelper::getParams('com_languages')->get('administrator'); + $debug = JFactory::getConfig()->get('debug_lang'); + $result = true; + + $article = new ArticleTable($db); + + $workflow = new Workflow(['extension' => 'com_content']); + + foreach ($pks as $pk) + { + if (!$article->load($pk)) + { + continue; + } + + $assoc = $workflow->getAssociation($pk); + + // Load new transitions + $query = $db->getQuery(true) + ->select($db->qn(['t.id'])) + ->from($db->qn('#__workflow_transitions', 't')) + ->from($db->qn('#__workflow_states', 's')) + ->where($db->qn('t.from_state_id') . ' = ' . (int) $assoc->state_id) + ->where($db->qn('t.to_state_id') . ' = ' . $db->qn('s.id')) + ->where($db->qn('t.published') . '= 1') + ->where($db->qn('s.published') . '= 1') + ->order($db->qn('t.ordering')); + + $transitions = $db->setQuery($query)->loadObjectList(); + + foreach ($users as $user_id) + { + if ($user_id != $user->id) + { + // Check if the user has available transitions + $items = array_filter( + $transitions, + function ($item) use ($user) + { + return $user->authorise('core.execute.transition', 'com_content.transition.' . $item->id); + } + ); + + if (!count($items)) + { + continue; + } + + // Load language for messaging + $receiver = JUser::getInstance($user_id); + $lang = JLanguage::getInstance($receiver->getParam('admin_language', $default_language), $debug); + $lang->load('plg_content_joomla'); + + $message = array( + 'user_id_to' => $user_id, + 'subject' => $lang->_('PLG_CONTENT_JOOMLA_ON_STATE_CHANGE_SUBJECT'), + 'message' => sprintf($lang->_('PLG_CONTENT_JOOMLA_ON_STATE_CHANGE_MSG'), $user->name, $article->title) + ); + + $model_message = new MessageModel; + $result = $model_message->save($message); + } + } + } + return true; } } diff --git a/plugins/content/joomla/joomla.xml b/plugins/content/joomla/joomla.xml index 7a0d2519b4b44..d034a2be4efce 100644 --- a/plugins/content/joomla/joomla.xml +++ b/plugins/content/joomla/joomla.xml @@ -30,8 +30,19 @@ + + + + - JNO + + + + +
diff --git a/plugins/content/pagenavigation/pagenavigation.php b/plugins/content/pagenavigation/pagenavigation.php index 6c49185762b3d..94badd35c4cdb 100644 --- a/plugins/content/pagenavigation/pagenavigation.php +++ b/plugins/content/pagenavigation/pagenavigation.php @@ -124,7 +124,7 @@ public function onContentBeforeDisplay($context, &$row, &$params, $page = 0) break; } - $xwhere = ' AND (a.state = 1 OR a.state = -1)' + $xwhere = ' AND (ws.condition = 1 OR ws.condition = -2)' . ' AND (publish_up = ' . $db->quote($nullDate) . ' OR publish_up <= ' . $db->quote($now) . ')' . ' AND (publish_down = ' . $db->quote($nullDate) . ' OR publish_down >= ' . $db->quote($now) . ')'; @@ -143,7 +143,8 @@ public function onContentBeforeDisplay($context, &$row, &$params, $page = 0) ->select($case_when) ->select($case_when1) ->from('#__content AS a') - ->join('LEFT', '#__categories AS cc ON cc.id = a.catid'); + ->join('LEFT', '#__categories AS cc ON cc.id = a.catid') + ->join('LEFT', '#__workflow_states AS ws ON ws.id = a.state'); if ($order_method === 'author' || $order_method === 'rauthor') { diff --git a/plugins/search/content/content.php b/plugins/search/content/content.php index b421f40ac46c6..f72bd2a2dd4f9 100644 --- a/plugins/search/content/content.php +++ b/plugins/search/content/content.php @@ -279,8 +279,9 @@ public function onContentSearch($text, $phrase = '', $ordering = '', $areas = nu ->select($db->quote('2') . ' AS browsernav') ->from($db->quoteName('#__content', 'a')) ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->join('LEFT', '#__workflow_states AS ws ON ws.id = state') ->where( - '(' . $where . ') AND a.state=1 AND c.published = 1 AND a.access IN (' . $groups . ') ' + '(' . $where . ') AND ws.condition=1 AND c.published = 1 AND a.access IN (' . $groups . ') ' . 'AND c.access IN (' . $groups . ')' . 'AND (a.publish_up = ' . $db->quote($nullDate) . ' OR a.publish_up <= ' . $db->quote($now) . ') ' . 'AND (a.publish_down = ' . $db->quote($nullDate) . ' OR a.publish_down >= ' . $db->quote($now) . ')'