* @license GNU General Public License version 2 or later; see LICENSE */ namespace Joomla\CMS\MVC\Model; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Form\Form; use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Workflow\Workflow; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Trait which supports state behavior * * @since 4.0.0 */ trait WorkflowBehaviorTrait { /** * The name of the component. * * @var string * @since 4.0.0 */ protected $extension = null; /** * The section of the component. * * @var string * @since 4.0.0 */ protected $section = ''; /** * Is workflow for this component enabled? * * @var boolean * @since 4.0.0 */ protected $workflowEnabled = false; /** * The workflow object * * @var Workflow * @since 4.0.0 */ protected $workflow; /** * Set Up the workflow * * @param string $extension The option and section separated by. * * @return void * * @since 4.0.0 */ public function setUpWorkflow($extension) { $parts = explode('.', $extension); $this->extension = array_shift($parts); if (count($parts)) { $this->section = array_shift($parts); } $this->workflow = new Workflow($extension); $params = ComponentHelper::getParams($this->extension); $this->workflowEnabled = $params->get('workflow_enabled'); $this->enableWorkflowBatch(); } /** * Add the workflow batch to the command list. Can be overwritten bei the child class * * @return void * * @since 4.0.0 */ protected function enableWorkflowBatch() { // Enable batch if ($this->workflowEnabled && property_exists($this, 'batch_commands')) { $this->batch_commands['workflowstage_id'] = 'batchWorkflowStage'; } } /** * Method to allow derived classes to preprocess the form. * * @param Form $form A Form object. * @param mixed $data The data expected for the form. * * @return void * * @since 4.0.0 * @see FormField */ public function workflowPreprocessForm(Form $form, $data) { $this->addTransitionField($form, $data); if (!$this->workflowEnabled) { return; } // Import the workflow plugin group to allow form manipulation. $this->importWorkflowPlugins(); } /** * Let plugins access stage change events * * @return void * * @since 4.0.0 */ public function workflowBeforeStageChange() { if (!$this->workflowEnabled) { return; } $this->importWorkflowPlugins(); } /** * Preparation of workflow data/plugins * * @return void * * @since 4.0.0 */ public function workflowBeforeSave() { if (!$this->workflowEnabled) { return; } $this->importWorkflowPlugins(); } /** * Executing of relevant workflow methods * * @return void * * @since 4.0.0 */ public function workflowAfterSave($data) { // Regardless if workflow is active or not, we have to set the default stage // So we can work with the workflow, when the user activates it later $id = $this->getState($this->getName() . '.id'); $isNew = $this->getState($this->getName() . '.new'); // We save the first stage if ($isNew) { // We have to add the paths, because it could be called outside of the extension context $path = JPATH_BASE . '/components/' . $this->extension; $path = Path::check($path); Form::addFormPath($path . '/forms'); Form::addFormPath($path . '/models/forms'); Form::addFieldPath($path . '/models/fields'); Form::addFormPath($path . '/model/form'); Form::addFieldPath($path . '/model/field'); $form = $this->getForm(); $stage_id = $this->getStageForNewItem($form, $data); $this->workflow->createAssociation($id, $stage_id); } if (!$this->workflowEnabled) { return; } // Execute transition if (!empty($data['transition'])) { $this->executeTransition([$id], $data['transition']); } } /** * Batch change workflow stage or current. * * @param integer $value The workflow stage 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 4.0.0 */ public function batchWorkflowStage(int $value, array $pks, array $contexts) { $user = Factory::getApplication()->getIdentity(); $workflow = Factory::getApplication()->bootComponent('com_workflow'); if (!$user->authorise('core.admin', $this->option)) { $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EXECUTE_TRANSITION')); } // Get workflow stage information $stage = $workflow->getMVCFactory()->createTable('Stage', 'Administrator'); if (empty($value) || !$stage->load($value)) { Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_BATCH_WORKFLOW_STAGE_ROW_NOT_FOUND'), 'error'); return false; } if (empty($pks)) { Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_BATCH_WORKFLOW_STAGE_ROW_NOT_FOUND'), 'error'); return false; } // Update workflow associations return $this->workflow->updateAssociations($pks, $value); } /** * Batch change workflow stage or current. * * @param integer $oldId The ID of the item copied from * @param integer $newId The ID of the new item * * @return null * * @since 4.0.0 */ public function workflowCleanupBatchMove($oldId, $newId) { // Trigger workflow plugins only if enable (will be triggered from parent class) if ($this->workflowEnabled) { $this->importWorkflowPlugins(); } // We always need an association, so create one $table = $this->getTable(); $table->load($newId); $catKey = $table->getColumnAlias('catid'); $stage_id = $this->workflow->getDefaultStageByCategory($table->$catKey); if (empty($stage_id)) { return; } $this->workflow->createAssociation((int) $newId, (int) $stage_id); } /** * Runs transition for item. * * @param array $pks Id of items to execute the transition * @param integer $transitionId Id of transition * * @return boolean * * @since 4.0.0 */ public function executeTransition(array $pks, int $transitionId) { $result = $this->workflow->executeTransition($pks, $transitionId); if (!$result) { $app = Factory::getApplication(); $app->enqueueMessage(Text::_('COM_CONTENT_ERROR_UPDATE_STAGE', $app::MSG_WARNING)); return false; } return true; } /** * Import the Workflow plugins. * * @param Form $form A Form object. * @param mixed $data The data expected for the form. * * @return void */ protected function importWorkflowPlugins() { PluginHelper::importPlugin('workflow'); } /** * Adds a transition field to the form. Can be overwritten by the child class if not needed * * @param Form $form A Form object. * @param mixed $data The data expected for the form. * * @return void * @since 4.0.0 */ protected function addTransitionField(Form $form, $data) { $extension = $this->extension . ($this->section ? '.' . $this->section : ''); $field = new \SimpleXMLElement(''); $field->addAttribute('name', 'transition'); $field->addAttribute('type', $this->workflowEnabled ? 'transition' : 'hidden'); $field->addAttribute('label', 'COM_CONTENT_WORKFLOW'); $field->addAttribute('extension', $extension); $form->setField($field); $table = $this->getTable(); $key = $table->getKeyName(); $id = isset($data->$key) ? $data->$key : $form->getValue($key); if ($id) { // Transition field $assoc = $this->workflow->getAssociation($id); if (!empty($assoc->stage_id)) { $form->setFieldAttribute('transition', 'workflow_stage', (int) $assoc->stage_id); } } else { $stage_id = $this->getStageForNewItem($form, $data); if (!empty($stage_id)) { $form->setFieldAttribute('transition', 'workflow_stage', (int) $stage_id); } } } /** * Try to load a workflow stage for newly created items * which does not have a workflow assigned yet. If the category is not the * carrier, overwrite it on your model and deliver your own carrier. * * @param Form $form A Form object. * @param mixed $data The data expected for the form. * * @return boolean|integer An integer, holding the stage ID or false * @since 4.0.0 */ protected function getStageForNewItem(Form $form, $data) { $table = $this->getTable(); $hasKey = $table->hasField('catid'); if (!$hasKey) { return false; } $catKey = $table->getColumnAlias('catid'); $field = $form->getField($catKey); if (!$field) { return false; } $catId = isset(((object) $data)->$catKey) ? ((object) $data)->$catKey : $form->getValue($catKey); // Try to get the category from the html code of the field if (empty($catId)) { $catId = $field->getAttribute('default', null); if (!$catId) { // Choose the first category available $catOptions = $field->options; if ($catOptions && !empty($catOptions[0]->value)) { $catId = (int) $catOptions[0]->value; } } } if (empty($catId)) { return false; } return $this->workflow->getDefaultStageByCategory($catId); } }