Current File : /home/pacjaorg/public_html/nsa/plugins/workflow/publishing/publishing.php |
<?php
/**
* @package Joomla.Plugin
* @subpackage Workflow.Publishing
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Event\Table\BeforeStoreEvent;
use Joomla\CMS\Event\View\DisplayEvent;
use Joomla\CMS\Event\Workflow\WorkflowFunctionalityUsedEvent;
use Joomla\CMS\Event\Workflow\WorkflowTransitionEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\DatabaseModelInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Table\ContentHistory;
use Joomla\CMS\Table\TableInterface;
use Joomla\CMS\Workflow\WorkflowPluginTrait;
use Joomla\CMS\Workflow\WorkflowServiceInterface;
use Joomla\Event\EventInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
use Joomla\String\Inflector;
/**
* Workflow Publishing Plugin
*
* @since 4.0.0
*/
class PlgWorkflowPublishing extends CMSPlugin implements SubscriberInterface
{
use WorkflowPluginTrait;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 4.0.0
*/
protected $autoloadLanguage = true;
/**
* Loads the CMS Application for direct access
*
* @var CMSApplicationInterface
* @since 4.0.0
*/
protected $app;
/**
* The name of the supported name to check against
*
* @var string
* @since 4.0.0
*/
protected $supportFunctionality = 'core.state';
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 4.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterDisplay' => 'onAfterDisplay',
'onContentBeforeChangeState' => 'onContentBeforeChangeState',
'onContentBeforeSave' => 'onContentBeforeSave',
'onContentPrepareForm' => 'onContentPrepareForm',
'onContentVersioningPrepareTable' => 'onContentVersioningPrepareTable',
'onTableBeforeStore' => 'onTableBeforeStore',
'onWorkflowAfterTransition' => 'onWorkflowAfterTransition',
'onWorkflowBeforeTransition' => 'onWorkflowBeforeTransition',
'onWorkflowFunctionalityUsed' => 'onWorkflowFunctionalityUsed',
];
}
/**
* The form event.
*
* @param EventInterface $event The event
*
* @since 4.0.0
*/
public function onContentPrepareForm(EventInterface $event)
{
$form = $event->getArgument('0');
$data = $event->getArgument('1');
$context = $form->getName();
// Extend the transition form
if ($context === 'com_workflow.transition')
{
$this->enhanceTransitionForm($form, $data);
return;
}
$this->enhanceItemForm($form, $data);
}
/**
* Add different parameter options to the transition view, we need when executing the transition
*
* @param Form $form The form
* @param stdClass $data The data
*
* @return boolean
*
* @since 4.0.0
*/
protected function enhanceTransitionForm(Form $form, $data)
{
$workflow = $this->enhanceWorkflowTransitionForm($form, $data);
if (!$workflow)
{
return true;
}
$form->setFieldAttribute('publishing', 'extension', $workflow->extension, 'options');
return true;
}
/**
* Disable certain fields in the item form view, when we want to take over this function in the transition
* Check also for the workflow implementation and if the field exists
*
* @param Form $form The form
* @param stdClass $data The data
*
* @return boolean
*
* @since 4.0.0
*/
protected function enhanceItemForm(Form $form, $data)
{
$context = $form->getName();
if (!$this->isSupported($context))
{
return true;
}
$parts = explode('.', $context);
$component = $this->app->bootComponent($parts[0]);
$modelName = $component->getModelName($context);
$table = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true])
->getTable();
$fieldname = $table->getColumnAlias('published');
$options = $form->getField($fieldname)->options;
$value = isset($data->$fieldname) ? $data->$fieldname : $form->getValue($fieldname, null, 0);
$text = '-';
$textclass = 'body';
switch ($value)
{
case 1:
$textclass = 'success';
break;
case 0:
case -2:
$textclass = 'danger';
}
if (!empty($options))
{
foreach ($options as $option)
{
if ($option->value == $value)
{
$text = $option->text;
break;
}
}
}
$form->setFieldAttribute($fieldname, 'type', 'spacer');
$label = '<span class="text-' . $textclass . '">' . htmlentities($text, ENT_COMPAT, 'UTF-8') . '</span>';
$form->setFieldAttribute($fieldname, 'label', Text::sprintf('PLG_WORKFLOW_PUBLISHING_PUBLISHED', $label));
return true;
}
/**
* Manipulate the generic list view
*
* @param DisplayEvent $event
*
* @since 4.0.0
*/
public function onAfterDisplay(DisplayEvent $event)
{
$app = Factory::getApplication();
if (!$app->isClient('administrator'))
{
return;
}
$component = $event->getArgument('extensionName');
$section = $event->getArgument('section');
// We need the single model context for checking for workflow
$singularsection = Inflector::singularize($section);
if (!$this->isSupported($component . '.' . $singularsection))
{
return true;
}
// That's the hard coded list from the AdminController publish method => change, when it's make dynamic in the future
$states = [
'publish',
'unpublish',
'archive',
'trash',
'report',
];
$js = "
document.addEventListener('DOMContentLoaded', function()
{
var dropdown = document.getElementById('toolbar-status-group');
if (!dropdown)
{
return;
}
" . json_encode($states) . ".forEach((action) => {
var button = document.getElementById('status-group-children-' + action);
if (button)
{
button.classList.add('d-none');
}
});
});
";
$app->getDocument()->addScriptDeclaration($js);
return true;
}
/**
* Check if we can execute the transition
*
* @param WorkflowTransitionEvent $event
*
* @return boolean
*
* @since 4.0.0
*/
public function onWorkflowBeforeTransition(WorkflowTransitionEvent $event)
{
$context = $event->getArgument('extension');
$transition = $event->getArgument('transition');
$pks = $event->getArgument('pks');
if (!$this->isSupported($context) || !is_numeric($transition->options->get('publishing')))
{
return true;
}
$value = $transition->options->get('publishing');
if (!is_numeric($value))
{
return true;
}
/**
* Here it becomes tricky. We would like to use the component models publish method, so we will
* Execute the normal "onContentBeforeChangeState" plugins. But they could cancel the execution,
* So we have to precheck and cancel the whole transition stuff if not allowed.
*/
$this->app->set('plgWorkflowPublishing.' . $context, $pks);
$result = $this->app->triggerEvent('onContentBeforeChangeState', [
$context,
$pks,
$value,
]
);
// Release allowed pks, the job is done
$this->app->set('plgWorkflowPublishing.' . $context, []);
if (in_array(false, $result, true))
{
$event->setStopTransition();
return false;
}
return true;
}
/**
* Change State of an item. Used to disable state change
*
* @param WorkflowTransitionEvent $event
*
* @return boolean
*
* @since 4.0.0
*/
public function onWorkflowAfterTransition(WorkflowTransitionEvent $event)
{
$context = $event->getArgument('extension');
$extensionName = $event->getArgument('extensionName');
$transition = $event->getArgument('transition');
$pks = $event->getArgument('pks');
if (!$this->isSupported($context))
{
return true;
}
$component = $this->app->bootComponent($extensionName);
$value = $transition->options->get('publishing');
if (!is_numeric($value))
{
return;
}
$options = [
'ignore_request' => true,
// We already have triggered onContentBeforeChangeState, so use our own
'event_before_change_state' => 'onWorkflowBeforeChangeState',
];
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), $options);
$model->publish($pks, $value);
}
/**
* Change State of an item. Used to disable state change
*
* @param EventInterface $event
*
* @return boolean
*
* @throws Exception
* @since 4.0.0
*/
public function onContentBeforeChangeState(EventInterface $event)
{
$context = $event->getArgument('0');
$pks = $event->getArgument('1');
if (!$this->isSupported($context))
{
return true;
}
// We have allowed the pks, so we're the one who triggered
// With onWorkflowBeforeTransition => free pass
if ($this->app->get('plgWorkflowPublishing.' . $context) === $pks)
{
return true;
}
throw new Exception(Text::_('PLG_WORKFLOW_PUBLISHING_CHANGE_STATE_NOT_ALLOWED'));
}
/**
* The save event.
*
* @param EventInterface $event
*
* @return boolean
*
* @since 4.0.0
*/
public function onContentBeforeSave(EventInterface $event)
{
$context = $event->getArgument('0');
/** @var TableInterface $table */
$table = $event->getArgument('1');
$isNew = $event->getArgument('2');
$data = $event->getArgument('3');
if (!$this->isSupported($context))
{
return true;
}
$keyName = $table->getColumnAlias('published');
// Check for the old value
$article = clone $table;
$article->load($table->id);
/**
* We don't allow the change of the state when we use the workflow
* As we're setting the field to disabled, no value should be there at all
*/
if (isset($data[$keyName]))
{
$this->app->enqueueMessage(Text::_('PLG_WORKFLOW_PUBLISHING_CHANGE_STATE_NOT_ALLOWED'), 'error');
return false;
}
return true;
}
/**
* We remove the publishing field from the versioning
*
* @param EventInterface $event
*
* @return boolean
*
* @since 4.0.0
*/
public function onContentVersioningPrepareTable(EventInterface $event)
{
$subject = $event->getArgument('subject');
$context = $event->getArgument('extension');
if (!$this->isSupported($context))
{
return true;
}
$parts = explode('.', $context);
$component = $this->app->bootComponent($parts[0]);
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true]);
$table = $model->getTable();
$subject->ignoreChanges[] = $table->getColumnAlias('published');
}
/**
* Pre-processor for $table->store($updateNulls)
*
* @param BeforeStoreEvent $event The event to handle
*
* @return void
*
* @since 4.0.0
*/
public function onTableBeforeStore(BeforeStoreEvent $event)
{
$subject = $event->getArgument('subject');
if (!($subject instanceof ContentHistory))
{
return;
}
$parts = explode('.', $subject->item_id);
$typeAlias = $parts[0] . (isset($parts[1]) ? '.' . $parts[1] : '');
if (!$this->isSupported($typeAlias))
{
return;
}
$component = $this->app->bootComponent($parts[0]);
$modelName = $component->getModelName($typeAlias);
$model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true]);
$table = $model->getTable();
$field = $table->getColumnAlias('published');
$versionData = new Registry($subject->version_data);
$versionData->remove($field);
$subject->version_data = $versionData->toString();
}
/**
* Check if the current plugin should execute workflow related activities
*
* @param string $context
*
* @return boolean
*
* @since 4.0.0
*/
protected function isSupported($context)
{
if (!$this->checkAllowedAndForbiddenlist($context) || !$this->checkExtensionSupport($context, $this->supportFunctionality))
{
return false;
}
$parts = explode('.', $context);
// We need at least the extension + view for loading the table fields
if (count($parts) < 2)
{
return false;
}
$component = $this->app->bootComponent($parts[0]);
if (!$component instanceof WorkflowServiceInterface
|| !$component->isWorkflowActive($context)
|| !$component->supportFunctionality($this->supportFunctionality, $context))
{
return false;
}
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true]);
if (!$model instanceof DatabaseModelInterface || !method_exists($model, 'publish'))
{
return false;
}
$table = $model->getTable();
if (!$table instanceof TableInterface || !$table->hasField('published'))
{
return false;
}
return true;
}
/**
* If plugin supports the functionality we set the used variable
*
* @param WorkflowFunctionalityUsedEvent $event
*
* @since 4.0.0
*/
public function onWorkflowFunctionalityUsed(WorkflowFunctionalityUsedEvent $event)
{
$functionality = $event->getArgument('functionality');
if ($functionality !== 'core.state')
{
return;
}
$event->setUsed();
}
}