diff --git a/administrator/components/com_users/forms/before_delete.xml b/administrator/components/com_users/forms/before_delete.xml
new file mode 100644
index 0000000000000..8646956862e39
--- /dev/null
+++ b/administrator/components/com_users/forms/before_delete.xml
@@ -0,0 +1,39 @@
+
+
diff --git a/administrator/components/com_users/src/Controller/UsersController.php b/administrator/components/com_users/src/Controller/UsersController.php
index 04cb09ebd3bc8..574d1a08e914c 100644
--- a/administrator/components/com_users/src/Controller/UsersController.php
+++ b/administrator/components/com_users/src/Controller/UsersController.php
@@ -11,12 +11,17 @@
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
+use Joomla\CMS\Factory;
use Joomla\CMS\Input\Input;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Response\JsonResponse;
+use Joomla\CMS\Router\Route;
+use Joomla\CMS\User\UserFactory;
+use Joomla\CMS\User\UserFactoryAwareTrait;
+use Joomla\Component\Users\Administrator\Model\UserModel;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
@@ -30,6 +35,8 @@
*/
class UsersController extends AdminController
{
+ use UserFactoryAwareTrait;
+
/**
* @var string The prefix to use with controller messages.
* @since 1.6
@@ -170,4 +177,97 @@ public function getQuickiconContent()
echo new JsonResponse($result);
}
+
+ /**
+ * Removes an item.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function delete()
+ {
+ $error = false;
+
+ // Check for request forgeries
+ $this->checkToken();
+
+ // Get items to remove from the request.
+ $cid = (array) $this->input->get('cid', [], 'int');
+
+ // Remove zero values resulting from input filter
+ $cid = array_filter($cid);
+
+ if (empty($cid)) {
+ $this->app->getLogger()->warning(
+ Text::_($this->text_prefix . '_NO_ITEM_SELECTED'),
+ ['category' => 'jerror']
+ );
+
+ $error = true;
+ }
+
+ $fallbackUserId = (int) $this->input->get('beforeDeleteUser')['fallbackUserIdOnDelete'] ?? 0;
+ $userFactory = Factory::getContainer()->get(UserFactory::class);
+ $isUserExists = $fallbackUserId && $userFactory->loadUserById($fallbackUserId)->id === $fallbackUserId;
+
+ if (!$error && empty($fallbackUserId)) {
+ $this->app->getLogger()->error(
+ Text::_('COM_USERS_BEFORE_DELETE_USER_ERROR_FALLBACK_USER_NOT_SET_MSG'),
+ ['category' => 'jerror']
+ );
+
+ $error = true;
+ }
+
+ if (!$error && in_array($fallbackUserId, $cid)) {
+ $this->app->getLogger()->error(
+ Text::_('COM_USERS_BEFORE_DELETE_USER_ERROR_FALLBACK_USER_CONNECTED_MSG'),
+ ['category' => 'jerror']
+ );
+
+ $error = true;
+ }
+
+ if (!$error && !$isUserExists) {
+ $this->app->getLogger()->error(
+ Text::sprintf(
+ 'COM_USERS_BEFORE_DELETE_USER_ERROR_FALLBACK_USER_ID_NOT_EXISTS_MSG',
+ $fallbackUserId,
+ ),
+ ['category' => 'jerror']
+ );
+
+ $error = true;
+ }
+
+ if ($error) {
+ $this->app->getLogger()->error(
+ Text::_('COM_USERS_BEFORE_DELETE_USER_ERROR_USER_NOT_DELETED_MSG'),
+ ['category' => 'jerror']
+ );
+ } else {
+ // Get the model.
+ /** @var UserModel $model */
+ $model = $this->getModel();
+
+ // Remove the items.
+ if ($model->delete($cid)) {
+ $this->setMessage(Text::plural($this->text_prefix . '_N_ITEMS_DELETED', \count($cid)));
+ } else {
+ $this->setMessage($model->getError(), 'error');
+ }
+
+ // Invoke the postDelete method to allow for the child class to access the model.
+ $this->postDeleteHook($model, $cid);
+ }
+
+ $this->setRedirect(
+ Route::_(
+ 'index.php?option=' . $this->option . '&view=' . $this->view_list
+ . $this->getRedirectToListAppend(),
+ false
+ )
+ );
+ }
}
diff --git a/administrator/components/com_users/src/Model/UserModel.php b/administrator/components/com_users/src/Model/UserModel.php
index 794b8731edab1..d5a9b19716913 100644
--- a/administrator/components/com_users/src/Model/UserModel.php
+++ b/administrator/components/com_users/src/Model/UserModel.php
@@ -24,6 +24,7 @@
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\CMS\User\UserHelper;
use Joomla\Database\ParameterType;
+use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
@@ -63,7 +64,7 @@ public function __construct($config = [], MVCFactoryInterface $factory = null)
'event_after_save' => 'onUserAfterSave',
'event_before_delete' => 'onUserBeforeDelete',
'event_before_save' => 'onUserBeforeSave',
- 'events_map' => ['save' => 'user', 'delete' => 'user', 'validate' => 'user'],
+ 'events_map' => ['save' => 'user', 'beforeDelete' => 'beforedeleteuser', 'delete' => 'user', 'validate' => 'user'],
],
$config
);
@@ -304,13 +305,16 @@ public function save($data)
*/
public function delete(&$pks)
{
- $user = $this->getCurrentUser();
- $table = $this->getTable();
- $pks = (array) $pks;
+ $app = Factory::getApplication();
+ $user = $this->getCurrentUser();
+ $table = $this->getTable();
+ $pks = (array) $pks;
+ $beforeDeleteUserParams = new Registry((array) $app->input->get('beforeDeleteUser'));
// Check if I am a Super Admin
$iAmSuperAdmin = $user->authorise('core.admin');
+ PluginHelper::importPlugin($this->events_map['beforeDelete']);
PluginHelper::importPlugin($this->events_map['delete']);
if (\in_array($user->id, $pks)) {
@@ -332,8 +336,11 @@ public function delete(&$pks)
// Get users data for the users to delete.
$user_to_delete = $this->getUserFactory()->loadUserById($pk);
+ $beforeDeleteUserParams->set('userId', $user_to_delete->id);
+ $beforeDeleteUserParams->set('userName', $user_to_delete->name);
+
// Fire the before delete event.
- Factory::getApplication()->triggerEvent($this->event_before_delete, [$table->getProperties()]);
+ $app->triggerEvent($this->event_before_delete, [$table->getProperties(), $beforeDeleteUserParams]);
if (!$table->delete($pk)) {
$this->setError($table->getError());
@@ -346,7 +353,7 @@ public function delete(&$pks)
} else {
// Prune items that you can't change.
unset($pks[$i]);
- Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
+ $app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
}
} else {
$this->setError($table->getError());
diff --git a/administrator/components/com_users/src/View/Users/HtmlView.php b/administrator/components/com_users/src/View/Users/HtmlView.php
index eafa9c9f55daa..678c897c82e04 100644
--- a/administrator/components/com_users/src/View/Users/HtmlView.php
+++ b/administrator/components/com_users/src/View/Users/HtmlView.php
@@ -170,8 +170,9 @@ protected function addToolbar()
}
if ($canDo->get('core.delete')) {
- $childBar->delete('users.delete', 'JTOOLBAR_DELETE')
- ->message('JGLOBAL_CONFIRM_DELETE')
+ $childBar->popupButton('delete', 'JTOOLBAR_DELETE')
+ ->selector('beforeDeleteModal')
+ ->icon('icon-delete')
->listCheck(true);
}
}
diff --git a/administrator/components/com_users/tmpl/users/default.php b/administrator/components/com_users/tmpl/users/default.php
index 724bfd85d98ed..73c3697e51593 100644
--- a/administrator/components/com_users/tmpl/users/default.php
+++ b/administrator/components/com_users/tmpl/users/default.php
@@ -218,6 +218,18 @@
) : ?>
loadTemplate('batch_body'); ?>
+ canDo->get('core.delete')) : ?>
+ Text::_('COM_USERS_BEFORE_DELETE_USER'),
+ 'footer' => $this->loadTemplate('before_delete_footer'),
+ ],
+ $this->loadTemplate('before_delete_body')
+ ); ?>
+
diff --git a/administrator/components/com_users/tmpl/users/default_before_delete_body.php b/administrator/components/com_users/tmpl/users/default_before_delete_body.php
new file mode 100644
index 0000000000000..61682a1e2cfc5
--- /dev/null
+++ b/administrator/components/com_users/tmpl/users/default_before_delete_body.php
@@ -0,0 +1,27 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Form\FormFactoryInterface;
+
+/** @var \Joomla\CMS\Form\Form $form */
+$form = Factory::getContainer()
+ ->get(FormFactoryInterface::class)
+ ->createForm('beforeDeleteUser', ['control' => 'beforeDeleteUser']);
+
+$form->loadFile('before_delete');
+
+?>
+
+
+ renderFieldset('before_delete_user'); ?>
+
diff --git a/administrator/components/com_users/tmpl/users/default_before_delete_footer.php b/administrator/components/com_users/tmpl/users/default_before_delete_footer.php
new file mode 100644
index 0000000000000..9c838c43245da
--- /dev/null
+++ b/administrator/components/com_users/tmpl/users/default_before_delete_footer.php
@@ -0,0 +1,21 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Language\Text;
+
+?>
+
+
diff --git a/administrator/language/en-GB/com_users.ini b/administrator/language/en-GB/com_users.ini
index 723b24fe7a63b..8236119afad83 100644
--- a/administrator/language/en-GB/com_users.ini
+++ b/administrator/language/en-GB/com_users.ini
@@ -19,6 +19,21 @@ COM_USERS_BATCH_DELETE="Delete From Group"
COM_USERS_BATCH_GROUP="Select Group"
COM_USERS_BATCH_OPTIONS="Batch process the selected users"
COM_USERS_BATCH_SET="Move To Group"
+COM_USERS_BEFORE_DELETE_USER="Before Delete User"
+COM_USERS_BEFORE_DELETE_USER_CHANGED_FALLBACK_ALIAS_MSG="The author alias %s
was inserted."
+COM_USERS_BEFORE_DELETE_USER_CHANGED_FALLBACK_ALIAS_IF_NOT_EMPTY_MSG="The author alias %s
was inserted, if the field was empty before."
+COM_USERS_BEFORE_DELETE_USER_DELETED_MSG="%s: for the elements with the ID %s
, the old user ID %d
has been replaced with the ID %d
. %s"
+COM_USERS_BEFORE_DELETE_USER_ERROR_FALLBACK_USER_CONNECTED_MSG="You have tried to delete the fallback user. Please select another user as fallback."
+COM_USERS_BEFORE_DELETE_USER_ERROR_FALLBACK_USER_ID_NOT_EXISTS_MSG="The fallback user with the ID %d
no longer exists. Please select another user as fallback."
+COM_USERS_BEFORE_DELETE_USER_ERROR_FALLBACK_USER_NOT_SET_MSG="The fallback user has not been set. Please select a fallback user to process the deleting."
+COM_USERS_BEFORE_DELETE_USER_ERROR_USER_NOT_DELETED_MSG="No user was deleted!"
+COM_USERS_BEFORE_DELETE_USER_FIELD_FALLBACK_USER_DESC="Select user to be put in place of the deleted user."
+COM_USERS_BEFORE_DELETE_USER_FIELD_FALLBACK_USER_LABEL="Fallback User"
+COM_USERS_BEFORE_DELETE_USER_FIELD_FALLBACK_USER_OPTION_EMPTY="Select user"
+COM_USERS_BEFORE_DELETE_USER_FIELD_OVERRIDE_ALIAS_DESC="Overwrite the content in the 'Author alias' field with the name of the user to be deleted, if this is not empty."
+COM_USERS_BEFORE_DELETE_USER_FIELD_OVERRIDE_ALIAS_LABEL="Overwrite Author Alias"
+COM_USERS_BEFORE_DELETE_USER_FIELD_SET_ALIAS_DESC="Write the name of the user to be deleted in the 'Author alias' field if it is empty."
+COM_USERS_BEFORE_DELETE_USER_FIELD_SET_ALIAS_LABEL="Enter Author Alias"
COM_USERS_CATEGORIES_TITLE="User Notes: Categories"
COM_USERS_CATEGORY_HEADING="Category"
COM_USERS_CONFIGURATION="Users: Options"
diff --git a/administrator/language/en-GB/plg_beforedeleteuser_core.ini b/administrator/language/en-GB/plg_beforedeleteuser_core.ini
new file mode 100644
index 0000000000000..e6b5d03d1ba97
--- /dev/null
+++ b/administrator/language/en-GB/plg_beforedeleteuser_core.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; (C) 2023 Open Source Matters, Inc.
+; License GNU General Public License version 2 or later; see LICENSE.txt
+; Note : All ini files need to be saved as UTF-8
+
+PLG_BEFOREDELETEUSER_CORE="Before delete user - Core"
+PLG_BEFOREDELETEUSER_CORE_XML_DESCRIPTION="Changes user IDs in core, on deleting user."
diff --git a/administrator/language/en-GB/plg_beforedeleteuser_core.sys.ini b/administrator/language/en-GB/plg_beforedeleteuser_core.sys.ini
new file mode 100644
index 0000000000000..e6b5d03d1ba97
--- /dev/null
+++ b/administrator/language/en-GB/plg_beforedeleteuser_core.sys.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; (C) 2023 Open Source Matters, Inc.
+; License GNU General Public License version 2 or later; see LICENSE.txt
+; Note : All ini files need to be saved as UTF-8
+
+PLG_BEFOREDELETEUSER_CORE="Before delete user - Core"
+PLG_BEFOREDELETEUSER_CORE_XML_DESCRIPTION="Changes user IDs in core, on deleting user."
diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql
index c34c49dd41e0a..ea0db7dce7780 100644
--- a/installation/sql/mysql/base.sql
+++ b/installation/sql/mysql/base.sql
@@ -261,6 +261,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`,
(0, 'plg_authentication_cookie', 'plugin', 'cookie', 'authentication', 0, 1, 1, 0, 1, '', '', '', 1, 0),
(0, 'plg_authentication_joomla', 'plugin', 'joomla', 'authentication', 0, 1, 1, 1, 1, '', '', '', 2, 0),
(0, 'plg_authentication_ldap', 'plugin', 'ldap', 'authentication', 0, 0, 1, 0, 1, '', '{"host":"","port":"389","use_ldapV3":"0","negotiate_tls":"0","no_referrals":"0","auth_method":"bind","base_dn":"","search_string":"","users_dn":"","username":"admin","password":"bobby7","ldap_fullname":"fullName","ldap_email":"mail","ldap_uid":"uid"}', '', 3, 0),
+(0, 'plg_beforedeleteuser_core', 'plugin', 'core', 'beforedeleteuser', 0, 1, 1, 1, 1, '', '', '', 1, 0),
(0, 'plg_behaviour_compat', 'plugin', 'compat', 'behaviour', 0, 1, 1, 0, 1, '', '{"classes_aliases":"1","es5_assets":"1"}', '', 1, 0),
(0, 'plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 2, 0),
(0, 'plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 3, 0),
diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql
index 435afc0ee2112..744cc17c9db6b 100644
--- a/installation/sql/postgresql/base.sql
+++ b/installation/sql/postgresql/base.sql
@@ -267,6 +267,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder",
(0, 'plg_authentication_cookie', 'plugin', 'cookie', 'authentication', 0, 1, 1, 0, 1, '', '', '', 1, 0),
(0, 'plg_authentication_joomla', 'plugin', 'joomla', 'authentication', 0, 1, 1, 1, 1, '', '', '', 2, 0),
(0, 'plg_authentication_ldap', 'plugin', 'ldap', 'authentication', 0, 0, 1, 0, 1, '', '{"host":"","port":"389","use_ldapV3":"0","negotiate_tls":"0","no_referrals":"0","auth_method":"bind","base_dn":"","search_string":"","users_dn":"","username":"admin","password":"bobby7","ldap_fullname":"fullName","ldap_email":"mail","ldap_uid":"uid"}', '', 3, 0),
+(0, 'plg_beforedeleteuser_core', 'plugin', 'core', 'beforedeleteuser', 0, 1, 1, 1, 1, '', '', '', 1, 0),
(0, 'plg_behaviour_compat', 'plugin', 'compat', 'behaviour', 0, 1, 1, 0, 1, '', '{"classes_aliases":"1","es5_assets":"1"}', '', 1, 0),
(0, 'plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 2, 0),
(0, 'plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 3, 0),
diff --git a/libraries/src/Extension/ExtensionHelper.php b/libraries/src/Extension/ExtensionHelper.php
index f74d6bed987e9..badeb11f79519 100644
--- a/libraries/src/Extension/ExtensionHelper.php
+++ b/libraries/src/Extension/ExtensionHelper.php
@@ -171,6 +171,9 @@ class ExtensionHelper
['plugin', 'joomla', 'authentication', 0],
['plugin', 'ldap', 'authentication', 0],
+ // Core plugin extensions - beforedeleteuser
+ ['plugin', 'core', 'beforedeleteuser', 0],
+
// Core plugin extensions - behaviour
['plugin', 'compat', 'behaviour', 0],
['plugin', 'taggable', 'behaviour', 0],
diff --git a/libraries/src/User/BeforeDeleteUserInterface.php b/libraries/src/User/BeforeDeleteUserInterface.php
new file mode 100644
index 0000000000000..e8ce678476bcc
--- /dev/null
+++ b/libraries/src/User/BeforeDeleteUserInterface.php
@@ -0,0 +1,50 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\CMS\User;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('JPATH_PLATFORM') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * Description.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+interface BeforeDeleteUserInterface
+{
+ /**
+ * The list of database table and columns, where the user information to change.
+ *
+ * Expected an array list, like the following example containing the key => value pairs
+ * with the table and columns to change.
+ * Example:
+ * array(
+ * array(
+ * 'baseContext' => 'com_content', // Extension base context
+ * 'realName' => 'com_content', // Language string
+ * 'tableName' => '#__content', // Database table name
+ * 'primaryKey' => 'id', // Primary or unique key of the table
+ * 'userId ' => array( // List of column names for the user id
+ * 'created_by',
+ * 'modified_by',
+ * ),
+ * 'userName' => array( // List of column names for the user real name
+ * 'created_by_alias'
+ * ),
+ * ),
+ * )
+ *
+ * @return array[]
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getTablesListToChangeUser();
+}
diff --git a/libraries/src/User/BeforeDeleteUserTrait.php b/libraries/src/User/BeforeDeleteUserTrait.php
new file mode 100644
index 0000000000000..e2c683d49e8e7
--- /dev/null
+++ b/libraries/src/User/BeforeDeleteUserTrait.php
@@ -0,0 +1,206 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\CMS\User;
+
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Uri\Uri;
+use Joomla\Registry\Registry;
+use RuntimeException;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * Trait for classes which require to change a user in database.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+trait BeforeDeleteUserTrait
+{
+ /**
+ * Changes the user in all registered extensions before deleting them.
+ *
+ * @param Registry $params
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ private function changeUser(Registry $params)
+ {
+ if (!$this instanceof BeforeDeleteUserInterface) {
+ // TODO: Add error handling and/or message and return it
+ return;
+ }
+
+ $app = $this->getApplication();
+ $userId = (int) $params->get('userId');
+ $aliasName = $params->get('userName');
+ $fallbackUserId = (int) $params->get('fallbackUserIdOnDelete');
+ $setAliasOnDelete = $params->get('setAliasOnDelete', '1');
+ $tablesListToChangeUser = $this->getTablesListToChangeUser();
+
+ foreach ($tablesListToChangeUser as $table) {
+ $tableName = $table['tableName'] ?? false;
+ $userIdColumns = (array) $table['userId'] ?? [];
+ $userNameColumns = (array) $table['userName'] ?? [];
+ $infoAuthorAlias = '';
+
+ if ($tableName && $userIdColumns) {
+ try {
+ // Get the entries to update.
+ $selectResult = $this->getDbItems($table, $params);
+
+ if (!empty($selectResult)) {
+ $forcedChangedAlias = $this->updateDbItems($table, $params);
+ $elementList = implode(', ', $selectResult);
+
+ if ($setAliasOnDelete && $userNameColumns) {
+ if ($forcedChangedAlias) {
+ $infoAuthorAlias = Text::sprintf(
+ 'COM_USERS_BEFORE_DELETE_USER_CHANGED_FALLBACK_ALIAS_MSG',
+ $aliasName
+ );
+ } else {
+ $infoAuthorAlias = Text::sprintf(
+ 'COM_USERS_BEFORE_DELETE_USER_CHANGED_FALLBACK_ALIAS_IF_NOT_EMPTY_MSG',
+ $aliasName
+ );
+ }
+ }
+
+ // Load extension language files
+ $app->getLanguage()->load($table['baseContext']);
+ $app->getLanguage()->load($table['baseContext'] . '.sys');
+
+ $app->enqueueMessage(
+ Text::sprintf(
+ 'COM_USERS_BEFORE_DELETE_USER_DELETED_MSG',
+ Text::_($table['realName']),
+ $elementList,
+ $userId,
+ $fallbackUserId,
+ $infoAuthorAlias
+ ),
+ 'info'
+ );
+ }
+ } catch (RuntimeException $e) {
+ $app->enqueueMessage(
+ Text::_('COM_USERS_BEFORE_DELETE_USER_ERROR_USER_NOT_DELETED_MSG'),
+ 'error'
+ );
+
+ $app->enqueueMessage(
+ $e->getMessage(),
+ 'error'
+ );
+
+ $url = Uri::getInstance()->toString(['path', 'query', 'fragment']);
+ $app->redirect($url, 500);
+ }
+ }
+ }
+ }
+
+ /**
+ * Searches the database for the occurrences of the user id.
+ *
+ * @param array $table
+ * @param Registry $params
+ *
+ * @return string
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ private function getDbItems(array $table, Registry $params)
+ {
+ $db = $this->getDatabase();
+ $userId = (int) $params->get('userId');
+ $tableName = $table['tableName'] ?? false;
+ $primaryKeyColumn = $table['primaryKey'] ?? false;
+ $userIdColumn = (array) $table['userId'] ?? [];
+ $selectQuery = $db->getQuery(true);
+
+ $selectQuery->select($db->quoteName($primaryKeyColumn))
+ ->from($tableName);
+
+ foreach ($userIdColumn as $column) {
+ $selectQuery->where($db->quoteName($column) . ' = ' . $db->quote($userId), 'OR');
+ }
+
+ $selectQuery->set('FOR UPDATE');
+
+ return $db->setQuery($selectQuery)->loadColumn();
+ }
+
+ /**
+ * @param array $table
+ * @param Registry $params
+ *
+ * @return boolean True if the alias is force changed.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ private function updateDbItems(array $table, Registry $params)
+ {
+ $db = $this->getDatabase();
+ $userId = (int) $params->get('userId');
+ $tableName = $table['tableName'] ?? false;
+ $userIdColumns = (array) $table['userId'] ?? [];
+ $userNameColumns = (array) $table['userName'] ?? [];
+ $userName = $params->get('userName');
+ $fallbackUserId = (int) $params->get('fallbackUserIdOnDelete');
+ $setAliasOnDelete = $params->get('setAliasOnDelete', '1');
+ $overrideAliasOnDelete = $setAliasOnDelete && $params->get('overrideAliasOnDelete', '0');
+ $updateQuery = $db->getQuery(true);
+ $forcedChangedAlias = false;
+
+ $updateQuery->update($db->quoteName($tableName));
+
+ foreach ($userIdColumns as $userIdColumn) {
+ $updateQuery->set(
+ $db->quoteName($userIdColumn)
+ . ' = CASE WHEN ' . $db->quoteName($userIdColumn) . ' = ' . $db->quote($userId)
+ . ' THEN ' . $db->quote($fallbackUserId)
+ . ' ELSE ' . $db->quoteName($userIdColumn)
+ . ' END'
+ );
+
+ $updateQuery->where($db->quoteName($userIdColumn) . ' = ' . $db->quote($userId), 'OR');
+ }
+
+ if ($setAliasOnDelete && !empty($userNameColumns)) {
+ foreach ($userNameColumns as $userNameColumn) {
+ if ($overrideAliasOnDelete) {
+ $forcedChangedAlias = true;
+
+ $updateQuery->set($db->quoteName($userNameColumn) . ' = ' . $db->quote($userName));
+ } else {
+ $updateQuery->set(
+ $db->quoteName($userNameColumn)
+ . ' = CASE WHEN ' . $db->quoteName($userNameColumn) . ' IS NULL'
+ . ' OR CHAR_LENGTH(TRIM(' . $db->quoteName($userNameColumn) . ')) = 0'
+ . ' THEN ' . $db->quote($userName)
+ . ' ELSE ' . $db->quoteName($userNameColumn)
+ . ' END'
+ );
+ }
+ }
+ }
+
+
+ // Update the entries found.
+ $db->setQuery($updateQuery)->execute();
+
+ return $forcedChangedAlias;
+ }
+}
diff --git a/plugins/beforedeleteuser/core/core.xml b/plugins/beforedeleteuser/core/core.xml
new file mode 100644
index 0000000000000..74f7cb58f609e
--- /dev/null
+++ b/plugins/beforedeleteuser/core/core.xml
@@ -0,0 +1,22 @@
+
+
+ plg_beforedeleteuser_core
+ 1.0.0
+ 2023-06
+ Joomla! Project
+ admin@joomla.org
+ www.joomla.org
+ (C) 2023 Open Source Matters, Inc.
+ GNU General Public License version 2 or later; see LICENSE.txt
+ PLG_BEFOREDELETEUSER_CORE_XML_DESCRIPTION
+ Joomla\Plugin\BeforeDeleteUser\Core
+
+ services
+ src
+
+
+ language/en-GB/plg_beforedeleteuser_core.ini
+ language/en-GB/plg_beforedeleteuser_core.sys.ini
+
+
+
diff --git a/plugins/beforedeleteuser/core/services/provider.php b/plugins/beforedeleteuser/core/services/provider.php
new file mode 100644
index 0000000000000..a75ee4a463f9c
--- /dev/null
+++ b/plugins/beforedeleteuser/core/services/provider.php
@@ -0,0 +1,51 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+use Joomla\CMS\Extension\PluginInterface;
+use Joomla\CMS\Factory;
+use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\Database\DatabaseInterface;
+use Joomla\DI\Container;
+use Joomla\DI\ServiceProviderInterface;
+use Joomla\Event\DispatcherInterface;
+use Joomla\Plugin\BeforeDeleteUser\Core\Extension\Core;
+
+return new class () implements ServiceProviderInterface {
+ /**
+ * Registers the service provider with a DI container.
+ *
+ * @param Container $container The DI container.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function register(Container $container)
+ {
+ $container->set(
+ PluginInterface::class,
+ function (Container $container) {
+ $dispatcher = $container->get(DispatcherInterface::class);
+ $plugin = new Core(
+ $dispatcher,
+ (array) PluginHelper::getPlugin('beforedeleteuser', 'core')
+ );
+ $plugin->setApplication(Factory::getApplication());
+ $plugin->setDatabase($container->get(DatabaseInterface::class));
+
+ return $plugin;
+ }
+ );
+ }
+};
diff --git a/plugins/beforedeleteuser/core/src/Extension/Core.php b/plugins/beforedeleteuser/core/src/Extension/Core.php
new file mode 100644
index 0000000000000..219a9265beadc
--- /dev/null
+++ b/plugins/beforedeleteuser/core/src/Extension/Core.php
@@ -0,0 +1,272 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Plugin\BeforeDeleteUser\Core\Extension;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\CMS\User\BeforeDeleteUserInterface;
+use Joomla\CMS\User\BeforeDeleteUserTrait;
+use Joomla\Database\DatabaseAwareTrait;
+use Joomla\Event\Event;
+use Joomla\Event\SubscriberInterface;
+
+/**
+ * Class to support the core extensions.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+final class Core extends CMSPlugin implements SubscriberInterface, BeforeDeleteUserInterface
+{
+ use BeforeDeleteUserTrait;
+ use DatabaseAwareTrait;
+
+ /**
+ * Function for getSubscribedEvents
+ *
+ * @return array
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ 'onUserBeforeDelete' => 'onUserBeforeDelete',
+ ];
+ }
+
+ /**
+ * Event triggered before the user is deleted.
+ *
+ * @param Event $event
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function onUserBeforeDelete(Event $event)
+ {
+ /** @var array $user The user to be altered. */
+ [$user, $params] = $event->getArguments();
+
+ $this->changeUser($params);
+ }
+
+ /**
+ * The list of database table and columns, where the user information to change.
+ *
+ * @return array[]
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getTablesListToChangeUser()
+ {
+ return [
+ [
+ 'baseContext' => 'com_banners',
+ 'realName' => 'com_banners',
+ 'tableName' => '#__banners',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ 'userName' => [
+ 'created_by_alias',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_categories',
+ 'realName' => 'com_categories',
+ 'tableName' => '#__categories',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_user_id',
+ 'modified_user_id',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_contact',
+ 'realName' => 'com_contact',
+ 'tableName' => '#__contact_details',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'user_id',
+ 'created_by',
+ 'modified_by',
+ ],
+ 'userName' => [
+ 'created_by_alias',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_contenthistory',
+ 'realName' => 'com_contenthistory',
+ 'tableName' => '#__history',
+ 'primaryKey' => 'version_id',
+ 'userId' => [
+ 'editor_user_id',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_content',
+ 'realName' => 'com_content',
+ 'tableName' => '#__content',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ 'userName' => [
+ 'created_by_alias',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_fields',
+ 'realName' => 'com_fields',
+ 'tableName' => '#__fields',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_user_id',
+ 'modified_by',
+ ],
+ ],
+ [
+ 'baseContext' => 'com_fields',
+ 'realName' => 'com_fields',
+ 'tableName' => '#__fields_groups',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_finder',
+ 'realName' => 'com_finder',
+ 'tableName' => '#__finder_filters',
+ 'primaryKey' => 'filter_id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_guidedtours',
+ 'realName' => 'com_guidedtours',
+ 'tableName' => '#__guidedtours',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ ],
+ [
+ 'baseContext' => 'com_guidedtours',
+ 'realName' => 'com_guidedtours',
+ 'tableName' => '#__guidedtour_steps',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_messages',
+ 'realName' => 'com_messages',
+ 'tableName' => '#__messages',
+ 'primaryKey' => 'message_id',
+ 'userId' => [
+ 'user_id_from',
+ 'user_id_to',
+ ],
+ ],
+ [
+ 'baseContext' => 'com_messages',
+ 'realName' => 'com_messages',
+ 'tableName' => '#__messages_cfg',
+ 'primaryKey' => 'user_id',
+ 'userId' => [
+ 'user_id',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_newsfeeds',
+ 'realName' => 'com_newsfeeds',
+ 'tableName' => '#__newsfeeds',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ 'userName' => [
+ 'created_by_alias',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_privacy',
+ 'realName' => 'com_privacy',
+ 'tableName' => '#__privacy_consents',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'user_id',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_scheduler',
+ 'realName' => 'com_scheduler',
+ 'tableName' => '#__scheduler_tasks',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_tags',
+ 'realName' => 'com_tags',
+ 'tableName' => '#__tags',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_user_id',
+ 'modified_user_id',
+ ],
+ 'userName' => [
+ 'created_by_alias',
+ ],
+ ],
+
+ [
+ 'baseContext' => 'com_workflow',
+ 'realName' => 'com_workflow',
+ 'tableName' => '#__workflows',
+ 'primaryKey' => 'id',
+ 'userId' => [
+ 'created_by',
+ 'modified_by',
+ ],
+ ],
+ ];
+ }
+}