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 @@ ) : ?> + 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', + ], + ], + ]; + } +}