first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="workflow" method="upgrade">
<name>plg_workflow_featuring</name>
<author>Joomla! Project</author>
<creationDate>2020-03</creationDate>
<copyright>(C) 2020 Open Source Matters, Inc.</copyright>
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
<authorEmail>admin@joomla.org</authorEmail>
<authorUrl>www.joomla.org</authorUrl>
<version>4.0.0</version>
<description>PLG_WORKFLOW_FEATURING_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\Workflow\Featuring</namespace>
<files>
<folder>forms</folder>
<folder plugin="featuring">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_workflow_featuring.ini</language>
<language tag="en-GB">language/en-GB/plg_workflow_featuring.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="allowedlist"
type="WorkflowComponentSections"
label="JWORKFLOW_EXTENSION_ALLOWED_LABEL"
description="JWORKFLOW_EXTENSION_ALLOWED_DESCRIPTION"
multiple="multiple"
layout="joomla.form.field.list-fancy-select"
/>
<field
name="forbiddenlist"
type="WorkflowComponentSections"
label="JWORKFLOW_EXTENSION_FORBIDDEN_LABEL"
description="JWORKFLOW_EXTENSION_FORBIDDEN_DESCRIPTION"
multiple="multiple"
layout="joomla.form.field.list-fancy-select"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="options">
<fieldset name="actions" label="COM_WORKFLOW_TRANSITION_ACTIONS_LABEL">
<field
name="featuring"
type="list"
label="PLG_WORKFLOW_FEATURING_TRANSITION_ACTIONS_FEATURING_LABEL"
description="PLG_WORKFLOW_FEATURING_TRANSITION_ACTIONS_FEATURING_DESC"
default=""
>
<option value="">JOPTION_DO_NOT_USE</option>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Workflow.featuring
*
* @copyright (C) 2023 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\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\Workflow\Featuring\Extension\Featuring;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.4.0
*/
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new Featuring(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('workflow', 'featuring')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,546 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Workflow.featuring
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\Workflow\Featuring\Extension;
use Joomla\CMS\Event\AbstractEvent;
use Joomla\CMS\Event\Model;
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\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\Component\Content\Administrator\Event\Model\FeatureEvent;
use Joomla\Event\EventInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
use Joomla\String\Inflector;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Workflow Featuring Plugin
*
* @since 4.0.0
*/
final class Featuring extends CMSPlugin implements SubscriberInterface
{
use WorkflowPluginTrait;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 4.0.0
*/
protected $autoloadLanguage = true;
/**
* The name of the supported functionality to check against
*
* @var string
* @since 4.0.0
*/
protected $supportFunctionality = 'core.featured';
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 4.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterDisplay' => 'onAfterDisplay',
'onContentBeforeChangeFeatured' => 'onContentBeforeChangeFeatured',
'onContentBeforeSave' => 'onContentBeforeSave',
'onContentPrepareForm' => 'onContentPrepareForm',
'onContentVersioningPrepareTable' => 'onContentVersioningPrepareTable',
'onTableBeforeStore' => 'onTableBeforeStore',
'onWorkflowAfterTransition' => 'onWorkflowAfterTransition',
'onWorkflowBeforeTransition' => 'onWorkflowBeforeTransition',
'onWorkflowFunctionalityUsed' => 'onWorkflowFunctionalityUsed',
];
}
/**
* The form event.
*
* @param Model\PrepareFormEvent $event The event
*
* @since 4.0.0
*/
public function onContentPrepareForm(Model\PrepareFormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
$context = $form->getName();
// Extend the transition form
if ($context === 'com_workflow.transition') {
$this->enhanceWorkflowTransitionForm($form, $data);
return;
}
$this->enhanceItemForm($form, $data);
}
/**
* 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 object $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->getApplication()->bootComponent($parts[0]);
$modelName = $component->getModelName($context);
$table = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->getName(), ['ignore_request' => true])
->getTable();
$fieldname = $table->getColumnAlias('featured');
$options = $form->getField($fieldname)->options;
$value = $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_FEATURING_FEATURED', $label)
);
return true;
}
/**
* Manipulate the generic list view
*
* @param DisplayEvent $event
*
* @return void
*
* @since 4.0.0
*/
public function onAfterDisplay(DisplayEvent $event)
{
if (!$this->getApplication()->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;
}
// List of related batch functions we need to hide
$states = [
'featured',
'unfeatured',
];
$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');
}
});
});
";
$this->getApplication()->getDocument()->addScriptDeclaration($js);
}
/**
* 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('featuring'))) {
return true;
}
$value = $transition->options->get('featuring');
if (!is_numeric($value)) {
return true;
}
/**
* Here it becomes tricky. We would like to use the component models featured method, so we will
* Execute the normal "onContentBeforeChangeFeatured" plugins. But they could cancel the execution,
* So we have to precheck and cancel the whole transition stuff if not allowed.
*/
$this->getApplication()->set('plgWorkflowFeaturing.' . $context, $pks);
// Trigger the change state event.
$eventResult = $this->getApplication()->getDispatcher()->dispatch(
'onContentBeforeChangeFeatured',
AbstractEvent::create(
'onContentBeforeChangeFeatured',
[
'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent',
'subject' => $this,
'extension' => $context,
'pks' => $pks,
'value' => $value,
'abort' => false,
'abortReason' => '',
]
)
);
// Release allowed pks, the job is done
$this->getApplication()->set('plgWorkflowFeaturing.' . $context, []);
if ($eventResult->getArgument('abort')) {
$event->setStopTransition();
return false;
}
return true;
}
/**
* Change Feature State of an item. Used to disable feature state change
*
* @param WorkflowTransitionEvent $event
*
* @return void
*
* @since 4.0.0
*/
public function onWorkflowAfterTransition(WorkflowTransitionEvent $event): void
{
$context = $event->getArgument('extension');
$extensionName = $event->getArgument('extensionName');
$transition = $event->getArgument('transition');
$pks = $event->getArgument('pks');
if (!$this->isSupported($context)) {
return;
}
$component = $this->getApplication()->bootComponent($extensionName);
$value = $transition->options->get('featuring');
if (!is_numeric($value)) {
return;
}
$options = [
'ignore_request' => true,
// We already have triggered onContentBeforeChangeFeatured, so use our own
'event_before_change_featured' => 'onWorkflowBeforeChangeFeatured',
];
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->getName(), $options);
$model->featured($pks, $value);
}
/**
* Change Feature State of an item. Used to disable Feature state change
*
* @param FeatureEvent $event
*
* @return boolean
*
* @throws Exception
* @since 4.0.0
*/
public function onContentBeforeChangeFeatured(FeatureEvent $event)
{
$extension = $event->getArgument('extension');
$pks = $event->getArgument('pks');
if (!$this->isSupported($extension)) {
return true;
}
// We have allowed the pks, so we're the one who triggered
// With onWorkflowBeforeTransition => free pass
if ($this->getApplication()->get('plgWorkflowFeaturing.' . $extension) === $pks) {
return true;
}
$event->setAbort('PLG_WORKFLOW_FEATURING_CHANGE_STATE_NOT_ALLOWED');
}
/**
* The save event.
*
* @param Model\BeforeSaveEvent $event
*
* @return boolean
*
* @since 4.0.0
*/
public function onContentBeforeSave(Model\BeforeSaveEvent $event)
{
$context = $event->getContext();
if (!$this->isSupported($context)) {
return true;
}
/** @var TableInterface $table */
$table = $event->getItem();
$data = $event->getData();
$keyName = $table->getColumnAlias('featured');
// Check for the old value
$article = clone $table;
$article->load($table->id);
/**
* We don't allow the change of the feature 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->getApplication()->enqueueMessage(
$this->getApplication()->getLanguage()->_('PLG_WORKFLOW_FEATURING_CHANGE_STATE_NOT_ALLOWED'),
'error'
);
return false;
}
return true;
}
/**
* We remove the featured 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->getApplication()->bootComponent($parts[0]);
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->getName(), ['ignore_request' => true]);
$table = $model->getTable();
$subject->ignoreChanges[] = $table->getColumnAlias('featured');
}
/**
* 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->getApplication()->bootComponent($parts[0]);
$modelName = $component->getModelName($typeAlias);
$model = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->getName(), ['ignore_request' => true]);
$table = $model->getTable();
$field = $table->getColumnAlias('featured');
$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->getApplication()->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->getApplication()->getName(), ['ignore_request' => true]);
if (!$model instanceof DatabaseModelInterface || !method_exists($model, 'featured')) {
return false;
}
$table = $model->getTable();
if (!$table instanceof TableInterface || !$table->hasField('featured')) {
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.featured') {
return;
}
$event->setUsed();
}
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="options">
<fieldset name="notification" label="COM_WORKFLOW_NOTIFICATION_FIELDSET_LABEL">
<field
name="notification_send_mail"
type="radio"
label="PLG_WORKFLOW_NOTIFICATION_SENDMAIL_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="boolean"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="notification_text"
type="textarea"
label="PLG_WORKFLOW_NOTIFICATION_ADDTEXT_LABEL"
description="PLG_WORKFLOW_NOTIFICATION_ADDTEXT_DESC"
rows="3"
filter="safehtml"
showon="notification_send_mail:1"
/>
<field
name="notification_groups"
type="usergrouplist"
label="PLG_WORKFLOW_NOTIFICATION_USERGROUP_LABEL"
description="PLG_WORKFLOW_NOTIFICATION_USERGROUP_DESC"
multiple="true"
layout="joomla.form.field.list-fancy-select"
default="0"
showon="notification_send_mail:1"
/>
<field
name="notification_receivers"
type="sql"
label="PLG_WORKFLOW_NOTIFICATION_RECEIVERS_LABEL"
desc="PLG_WORKFLOW_NOTIFICATION_RECEIVERS_DESC"
query="SELECT id, name FROM #__users WHERE block=0 ORDER BY name ASC"
key_field="id"
value_field="name"
multiple="true"
layout="joomla.form.field.list-fancy-select"
showon="notification_send_mail:1"
/>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="workflow" method="upgrade">
<name>plg_workflow_notification</name>
<author>Joomla! Project</author>
<creationDate>2020-05</creationDate>
<copyright>(C) 2020 Open Source Matters, Inc.</copyright>
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
<authorEmail>admin@joomla.org</authorEmail>
<authorUrl>www.joomla.org</authorUrl>
<version>4.0.0</version>
<description>PLG_WORKFLOW_NOTIFICATION_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\Workflow\Notification</namespace>
<files>
<folder>forms</folder>
<folder plugin="notification">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_workflow_notification.ini</language>
<language tag="en-GB">language/en-GB/plg_workflow_notification.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="allowedlist"
type="WorkflowComponentSections"
label="JWORKFLOW_EXTENSION_ALLOWED_LABEL"
description="JWORKFLOW_EXTENSION_ALLOWED_DESCRIPTION"
multiple="multiple"
layout="joomla.form.field.list-fancy-select"
/>
<field
name="forbiddenlist"
type="WorkflowComponentSections"
label="JWORKFLOW_EXTENSION_FORBIDDEN_LABEL"
description="JWORKFLOW_EXTENSION_FORBIDDEN_DESCRIPTION"
multiple="multiple"
layout="joomla.form.field.list-fancy-select"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,52 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Workflow.notification
*
* @copyright (C) 2023 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\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\LanguageFactoryInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\Workflow\Notification\Extension\Notification;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.4.0
*/
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new Notification(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('workflow', 'notification'),
$container->get(LanguageFactoryInterface::class)
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
$plugin->setUserFactory($container->get(UserFactoryInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,348 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Workflow.notification
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\Workflow\Notification\Extension;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Model;
use Joomla\CMS\Event\Workflow\WorkflowTransitionEvent;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\LanguageFactoryInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\CMS\Workflow\WorkflowPluginTrait;
use Joomla\CMS\Workflow\WorkflowServiceInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Workflow Notification Plugin
*
* @since 4.0.0
*/
final class Notification extends CMSPlugin implements SubscriberInterface
{
use WorkflowPluginTrait;
use DatabaseAwareTrait;
use UserFactoryAwareTrait;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 4.0.0
*/
protected $autoloadLanguage = true;
/**
* The language factory.
*
* @var LanguageFactoryInterface
* @since 4.4.0
*/
private $languageFactory;
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 4.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onContentPrepareForm' => 'onContentPrepareForm',
'onWorkflowAfterTransition' => 'onWorkflowAfterTransition',
];
}
/**
* Constructor.
*
* @param DispatcherInterface $dispatcher The dispatcher
* @param array $config An optional associative array of configuration settings
* @param LanguageFactoryInterface $languageFactory The language factory
*
* @since 4.2.0
*/
public function __construct(DispatcherInterface $dispatcher, array $config, LanguageFactoryInterface $languageFactory)
{
parent::__construct($dispatcher, $config);
$this->languageFactory = $languageFactory;
}
/**
* The form event.
*
* @param Model\PrepareFormEvent $event The event
*
* @return boolean
*
* @since 4.0.0
*/
public function onContentPrepareForm(Model\PrepareFormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
$context = $form->getName();
// Extend the transition form
if ($context === 'com_workflow.transition') {
$this->enhanceWorkflowTransitionForm($form, $data);
}
return true;
}
/**
* Send a Notification to defined users a transition is performed
*
* @param WorkflowTransitionEvent $event The workflow event being processed.
*
* @return void
*
* @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;
}
$component = $this->getApplication()->bootComponent($extensionName);
// Check if send-mail is active
if (empty($transition->options['notification_send_mail'])) {
return;
}
// ID of the items whose state has changed.
$pks = ArrayHelper::toInteger($pks);
if (empty($pks)) {
return;
}
// Get UserIds of Receivers
$userIds = $this->getUsersFromGroup($transition);
// The active user
$user = $this->getApplication()->getIdentity();
// Prepare Language for messages
$defaultLanguage = ComponentHelper::getParams('com_languages')->get('administrator');
$debug = $this->getApplication()->get('debug_lang');
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->getName(), ['ignore_request' => true]);
// Don't send the notification to the active user
$key = array_search($user->id, $userIds);
if (\is_int($key)) {
unset($userIds[$key]);
}
// Remove users with locked input box from the list of receivers
if (!empty($userIds)) {
$userIds = $this->removeLocked($userIds);
}
// If there are no receivers, stop here
if (empty($userIds)) {
$this->getApplication()->enqueueMessage($this->getApplication()->getLanguage()->_('PLG_WORKFLOW_NOTIFICATION_NO_RECEIVER'), 'error');
return;
}
// Get the model for private messages
$model_message = $this->getApplication()->bootComponent('com_messages')
->getMVCFactory()->createModel('Message', 'Administrator');
// Get the title of the stage
$model_stage = $this->getApplication()->bootComponent('com_workflow')
->getMVCFactory()->createModel('Stage', 'Administrator');
$toStage = $model_stage->getItem($transition->to_stage_id)->title;
// Get the name of the transition
$model_transition = $this->getApplication()->bootComponent('com_workflow')
->getMVCFactory()->createModel('Transition', 'Administrator');
$transitionName = $model_transition->getItem($transition->id)->title;
$hasGetItem = method_exists($model, 'getItem');
foreach ($pks as $pk) {
// Get the title of the item which has changed, unknown as fallback
$title = $this->getApplication()->getLanguage()->_('PLG_WORKFLOW_NOTIFICATION_NO_TITLE');
if ($hasGetItem) {
$item = $model->getItem($pk);
$title = !empty($item->title) ? $item->title : $title;
}
// Send Email to receivers
foreach ($userIds as $user_id) {
$receiver = $this->getUserFactory()->loadUserById($user_id);
if ($receiver->authorise('core.manage', 'com_messages')) {
// Load language for messaging
$lang = $this->languageFactory->createLanguage($user->getParam('admin_language', $defaultLanguage), $debug);
$lang->load('plg_workflow_notification');
$messageText = sprintf(
$lang->_('PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_MSG'),
$title,
$lang->_($transitionName),
$user->name,
$lang->_($toStage)
);
if (!empty($transition->options['notification_text'])) {
$messageText .= '<br>' . htmlspecialchars($lang->_($transition->options['notification_text']));
}
$message = [
'id' => 0,
'user_id_to' => $receiver->id,
'subject' => sprintf($lang->_('PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_SUBJECT'), $title),
'message' => $messageText,
];
$model_message->save($message);
}
}
}
$this->getApplication()->enqueueMessage($this->getApplication()->getLanguage()->_('PLG_WORKFLOW_NOTIFICATION_SENT'), 'message');
}
/**
* Get user_ids of receivers
*
* @param object $data Object containing data about the transition
*
* @return array $userIds The receivers
*
* @since 4.0.0
*/
private function getUsersFromGroup($data): array
{
$users = [];
// Single userIds
if (!empty($data->options['notification_receivers'])) {
$users = ArrayHelper::toInteger($data->options['notification_receivers']);
}
// Usergroups
$groups = [];
if (!empty($data->options['notification_groups'])) {
$groups = ArrayHelper::toInteger($data->options['notification_groups']);
}
$users2 = [];
if (!empty($groups)) {
// UserIds from usergroups
$model = $this->getApplication()->bootComponent('com_users')
->getMVCFactory()->createModel('Users', 'Administrator', ['ignore_request' => true]);
$model->setState('list.select', 'id');
$model->setState('filter.groups', $groups);
$model->setState('filter.state', 0);
// Ids from usergroups
$groupUsers = $model->getItems();
$users2 = ArrayHelper::getColumn($groupUsers, 'id');
}
// Merge userIds from individual entries and userIDs from groups
return array_unique(array_merge($users, $users2));
}
/**
* 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)) {
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->getApplication()->bootComponent($parts[0]);
if (!$component instanceof WorkflowServiceInterface || !$component->isWorkflowActive($context)) {
return false;
}
return true;
}
/**
* Remove receivers who have locked their message inputbox
*
* @param array $userIds The userIds which must be checked
*
* @return array users with active message input box
*
* @since 4.0.0
*/
private function removeLocked(array $userIds): array
{
if (empty($userIds)) {
return [];
}
$db = $this->getDatabase();
// Check for locked inboxes would be better to have _cdf settings in the user_object or a filter in users model
$query = $db->getQuery(true);
$query->select($db->quoteName('user_id'))
->from($db->quoteName('#__messages_cfg'))
->whereIn($db->quoteName('user_id'), $userIds)
->where($db->quoteName('cfg_name') . ' = ' . $db->quote('locked'))
->where($db->quoteName('cfg_value') . ' = 1');
$locked = $db->setQuery($query)->loadColumn();
return array_diff($userIds, $locked);
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="options">
<fieldset name="actions"
label="COM_WORKFLOW_TRANSITION_ACTIONS_LABEL"
>
<field
name="publishing"
type="workflowcondition"
label="PLG_WORKFLOW_PUBLISHING_TRANSITION_ACTIONS_PUBLISHING_LABEL"
description="PLG_WORKFLOW_PUBLISHING_TRANSITION_ACTIONS_PUBLISHING_DESC"
default=""
hide_all="true"
>
<option value="">JOPTION_DO_NOT_USE</option>
</field>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="workflow" method="upgrade">
<name>plg_workflow_publishing</name>
<author>Joomla! Project</author>
<creationDate>2020-03</creationDate>
<copyright>(C) 2020 Open Source Matters, Inc.</copyright>
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
<authorEmail>admin@joomla.org</authorEmail>
<authorUrl>www.joomla.org</authorUrl>
<version>4.0.0</version>
<description>PLG_WORKFLOW_PUBLISHING_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\Workflow\Publishing</namespace>
<files>
<folder>forms</folder>
<folder plugin="publishing">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_workflow_publishing.ini</language>
<language tag="en-GB">language/en-GB/plg_workflow_publishing.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="allowedlist"
type="WorkflowComponentSections"
label="JWORKFLOW_EXTENSION_ALLOWED_LABEL"
description="JWORKFLOW_EXTENSION_ALLOWED_DESCRIPTION"
multiple="multiple"
layout="joomla.form.field.list-fancy-select"
/>
<field
name="forbiddenlist"
type="WorkflowComponentSections"
label="JWORKFLOW_EXTENSION_FORBIDDEN_LABEL"
description="JWORKFLOW_EXTENSION_FORBIDDEN_DESCRIPTION"
multiple="multiple"
layout="joomla.form.field.list-fancy-select"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Workflow.publishing
*
* @copyright (C) 2023 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\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\Workflow\Publishing\Extension\Publishing;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.4.0
*/
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new Publishing(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('workflow', 'publishing')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,551 @@
<?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
*/
namespace Joomla\Plugin\Workflow\Publishing\Extension;
use Joomla\CMS\Event\Model;
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\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;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Workflow Publishing Plugin
*
* @since 4.0.0
*/
final class Publishing extends CMSPlugin implements SubscriberInterface
{
use WorkflowPluginTrait;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 4.0.0
*/
protected $autoloadLanguage = true;
/**
* 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 Model\PrepareFormEvent $event The event
*
* @since 4.0.0
*/
public function onContentPrepareForm(Model\PrepareFormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
$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->getApplication()->bootComponent($parts[0]);
$modelName = $component->getModelName($context);
$table = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->getName(), ['ignore_request' => true])
->getTable();
$fieldname = $table->getColumnAlias('published');
$options = $form->getField($fieldname)->options;
$value = $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
*
* @return void
*
* @since 4.0.0
*/
public function onAfterDisplay(DisplayEvent $event)
{
if (!$this->getApplication()->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;
}
// 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');
}
});
});
";
$this->getApplication()->getDocument()->addScriptDeclaration($js);
}
/**
* 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->getApplication()->set('plgWorkflowPublishing.' . $context, $pks);
$result = $this->getApplication()->triggerEvent('onContentBeforeChangeState', [
$context,
$pks,
$value,
]);
// Release allowed pks, the job is done
$this->getApplication()->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->getApplication()->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->getApplication()->getName(), $options);
$model->publish($pks, $value);
}
/**
* Change State of an item. Used to disable state change
*
* @param Model\BeforeChangeStateEvent $event
*
* @return boolean
*
* @throws \Exception
* @since 4.0.0
*/
public function onContentBeforeChangeState(Model\BeforeChangeStateEvent $event)
{
$context = $event->getContext();
$pks = $event->getPks();
if (!$this->isSupported($context)) {
return true;
}
// We have allowed the pks, so we're the one who triggered
// With onWorkflowBeforeTransition => free pass
if ($this->getApplication()->get('plgWorkflowPublishing.' . $context) === $pks) {
return true;
}
throw new \Exception($this->getApplication()->getLanguage()->_('PLG_WORKFLOW_PUBLISHING_CHANGE_STATE_NOT_ALLOWED'));
}
/**
* The save event.
*
* @param Model\BeforeSaveEvent $event
*
* @return boolean
*
* @since 4.0.0
*/
public function onContentBeforeSave(Model\BeforeSaveEvent $event)
{
$context = $event->getContext();
if (!$this->isSupported($context)) {
return true;
}
/** @var TableInterface $table */
$table = $event->getItem();
$data = $event->getData();
$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->getApplication()->enqueueMessage($this->getApplication()->getLanguage()->_('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->getApplication()->bootComponent($parts[0]);
$modelName = $component->getModelName($context);
$model = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->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->getApplication()->bootComponent($parts[0]);
$modelName = $component->getModelName($typeAlias);
$model = $component->getMVCFactory()->createModel($modelName, $this->getApplication()->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->getApplication()->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->getApplication()->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();
}
}