diff --git a/masquerade.module b/masquerade.module index 63e4c88..2013a8a 100644 --- a/masquerade.module +++ b/masquerade.module @@ -5,6 +5,8 @@ * Allows privileged users to masquerade as another user. */ +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Routing\RouteMatchInterface; // Used for account_switcher service method. @@ -56,36 +58,6 @@ function masquerade_help($route_name, RouteMatchInterface $route_match) { } /** - * Implements hook_menu_link_defaults(). - */ -function masquerade_menu_link_defaults() { - $links['masquerade.unmasquerade'] = array( - 'link_title' => 'Unmasquerade', - 'route_name' => 'masquerade.unmasquerade', - 'weight' => 9, - 'menu_name' => 'account', - // Invoke masquerade_translated_menu_link_alter() to append token. - 'options' => array('alter' => TRUE), - ); - - return $links; -} - -/** - * Implements hook_translated_menu_link_alter(). - * - * Adds a CSRF protection token to masquerade menu links. - */ -function masquerade_translated_menu_link_alter(MenuLinkInterface &$menu_link, $map) { - if ($menu_link->href == 'masquerade_switch_user_page') { - $menu_link->localized_options['query']['token'] = drupal_get_token($menu_link->href); - } - elseif ($menu_link->href == 'masquerade_switch_back_page') { - $menu_link->localized_options['query']['token'] = drupal_get_token('unmasquerade'); - } -} - -/** * Implements hook_toolbar(). * * @todo Nest this with the "View profile", "Edit profile", and "Log out" @@ -184,37 +156,13 @@ function masquerade_masquerade_access($user, UserInterface $target_account) { } /** - * Returns whether the current user is masquerading. - */ -function masquerade_user_is_masquerading() { - return isset($_SESSION['masquerading']); -} - -/** - * Implements hook_field_extra_fields(). - */ -function masquerade_field_extra_fields() { - $return['user']['user'] = array( - 'display' => array( - 'masquerade' => array( - 'label' => t('Masquerade'), - 'description' => t('Masquerade as user link.'), - 'weight' => 50, - ), - ), - ); - - return $return; -} - -/** * Implements hook_entity_extra_field_info(). */ function masquerade_entity_extra_field_info() { $fields['user']['user']['display']['masquerade'] = array( 'label' => t('Masquerade'), - 'description' => t('Masquerade module \'masquerade\' view element.'), - 'weight' => -500, + 'description' => t('Masquerade as user link.'), + 'weight' => 50, ); return $fields; } @@ -222,39 +170,72 @@ function masquerade_entity_extra_field_info() { /** * Implements hook_entity_view(). */ -function masquerade_user_view(array &$build, UserInterface $account, $display, $view_mode, $langcode) { - if ($display->getComponent('masquerade') && masquerade_target_user_access($account)) { - $masquerade_user_url = Url::fromRoute('masquerade.user', ['user' => $account->id()]); - $masquerade_user_link = \Drupal::l(t('Masquerade as @name', ['@name' => $account->getUsername()]), $masquerade_user_url); - - // @todo Attaching a CSS class to this would be nice. - $build['masquerade'] = array( - '#type' => 'item', - '#markup' => $masquerade_user_link, +function masquerade_user_view(array &$build, UserInterface $account, EntityViewDisplayInterface $display, $view_mode, $langcode) { + if ($display->getComponent('masquerade')) { + // Use post render to allow caching. + $callback = 'masquerade_render_cache_link'; + $context = array( + 'account_id' => $account->id(), ); + $placeholder = drupal_render_cache_generate_placeholder($callback, $context); + $build['masquerade'] = [ + '#post_render_cache' => [ + $callback => [ + $context, + ], + ], + '#markup' => $placeholder, + ]; } } /** - * Page callback; Masquerades as a given user. + * #post_render_cache callback; replaces placeholder with masquerade link. * - * @param \Drupal\user\UserInterface $user - * The user account object to masquerade as. + * @param array $element + * The renderable array that contains the to be replaced placeholder. + * @param array $context + * An array with the following keys: + * - account_id: a target account ID * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - * Redirect to previous page. + * @return array + * A renderable array containing the comment form. * - * @deprecated Use \Drupal\masquerade\SwitchController::switchTo(). + * @todo Move to service. */ -function masquerade_switch_user_page(UserInterface $user) { - $error = masquerade_switch_user_validate($user); - if (empty($error)) { - masquerade_switch_user($user); +function masquerade_render_cache_link(array $element, array $context) { + $account = User::load($context['account_id']); + $callback = __FUNCTION__; + $placeholder = drupal_render_cache_generate_placeholder($callback, $context); + $markup = ''; + if (masquerade_target_user_access($account)) { + // @todo Attaching a CSS class to this would be nice. + $markup = [ + '#type' => 'link', + '#title' => t('Masquerade as @name', ['@name' => $account->getUsername()]), + '#url' => Url::fromRoute('entity.user.masquerade', ['user' => $account->id()]), + ]; + $markup = \Drupal::service('renderer')->render($markup); } - else { - drupal_set_message($error, 'error'); + $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); + return $element; +} + +/** + * Implements hook_entity_operation(). + */ +function masquerade_entity_operation(EntityInterface $entity) { + $operations = array(); + if ($entity->getEntityTypeId() === 'user') { + if (masquerade_target_user_access($entity)) { + $operations['masquerade'] = array( + 'title' => t('Masquerade as'), + 'weight' => 100, + 'url' => Url::fromRoute('entity.user.masquerade', array('user' => $entity->id())), + ); + } } - return new RedirectResponse(\Drupal::request()->server->get('HTTP_REFERER')); + return $operations; } /** @@ -273,7 +254,7 @@ function masquerade_switch_user_page(UserInterface $user) { function masquerade_switch_user_validate(UserInterface $target_account) { $user = \Drupal::currentUser(); - if (masquerade_user_is_masquerading()) { + if (\Drupal::service('masquerade')->isMasquerading()) { return t('You are masquerading already. Please switch back to your account to masquerade as another user.', array( '@unmasquerade-url' => Url::fromRoute('masquerade.unmasquerade'), )); @@ -295,128 +276,3 @@ function masquerade_switch_user_validate(UserInterface $target_account) { )); } } - -/** - * Masquerades the current user as a given user. - * - * Access to masquerade as the target user account has to checked by all callers - * via masquerade_target_user_access() already. - * - * @param \Drupal\user\UserInterface $target_account - * The user account object to masquerade as. - */ -function masquerade_switch_user(UserInterface $target_account) { - $account = \Drupal::currentUser(); - $original_username = $account->getUsername(); - - // Call logout hooks when switching from original user. - \Drupal::moduleHandler()->invokeAll('user_logout', [$account]); - - // Regenerate the session ID to prevent against session fixation attacks. - \Drupal::service('session_manager')->regenerate(); - - $_SESSION['masquerading'] = $account->id(); - - // Supposed "safe" user switch method: - // https://www.drupal.org/node/218104 - //$accountSwitcher = Drupal::service('account_switcher'); - //$accountSwitcher->switchTo(new UserSession(array('uid' => $account->id()))); - $account->setAccount($target_account); - - global $user; - $user = User::load($target_account->id()); - - // Call all login hooks when switching to masquerading user. - \Drupal::moduleHandler()->invokeAll('user_login', [$target_account]); - - $target_user_name = $target_account->getUsername(); - \Drupal::logger('masquerade')->info('User %username masqueraded as %target_username.', array( - '%username' => $original_username, - '%target_username' => $target_user_name, - 'link' => \Drupal::l(t('view'), new Url('entity.user.canonical', ['user' => $target_account->id()])), - )); - - drupal_set_message(t('You are now masquerading as @user.', array( - '@user' => $target_user_name, - ))); -} - -/** - * Allows a user who is currently masquerading to become a new user. - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * - * @deprecated Use \Drupal\masquerade\SwitchController::switchBack(). - */ -function masquerade_switch_back_page() { - $account = \Drupal::currentUser(); - - if (masquerade_user_is_masquerading()) { - masquerade_switch_back(); - - drupal_set_message(t('You are no longer masquerading as @user.', array( - '@user' => $account->getUsername(), - ))); - return new RedirectResponse(\Drupal::request()->server->get('HTTP_REFERER')); - } - else { - throw new AccessDeniedHttpException(); - } -} - -/** - * Function for a masquerading user to switch back to the previous user. - */ -function masquerade_switch_back() { - $account = \Drupal::currentUser(); - - $uid = $_SESSION['masquerading']; - - if (!$new_user = User::load($uid)) { - // Ensure the flag is cleared. - unset($_SESSION['masquerading']); - return FALSE; - } - - // Call logout hooks when switching from masquerading user. - \Drupal::moduleHandler()->invokeAll('user_logout', [$account]); - // Regenerate the session ID to prevent against session fixation attacks. - \Drupal::service('session_manager')->regenerate(); - - // Swap the original user object back into the session. - global $user; - $user = $new_user; - - // Remove the masquerading flag from the user's session. - unset($_SESSION['masquerading']); - - // Call all login hooks when switching back to original user. - \Drupal::moduleHandler()->invokeAll('user_login', [$new_user]); - - \Drupal::logger('masquerade')->info('User %username stopped masquerading as %old_username.', array( - '%username' => $new_user->getUsername(), - '%old_username' => $account->getUsername(), - 'link' => \Drupal::l(t('view'), new Url('entity.user.canonical', ['user' => $new_user->id()])), - )); -} - -/** - * Implements hook_form_FORMID_alter(). - */ -function masquerade_form_user_admin_account_alter(&$form, &$form_state) { - $destination = drupal_get_destination(); - foreach ($form['accounts']['#options'] as $uid => &$row) { - // @todo Core: The already loaded accounts are not provided. - $account = user_load($uid); - if (masquerade_target_user_access($account)) { - $path = 'user/' . $account->id() . '/masquerade'; - $row['operations']['data']['#links']['masquerade'] = array( - 'title' => t('Masquerade'), - 'href' => $path, - 'query' => array( - 'token' => drupal_get_token($path), - ) + $destination, - ); - } - } -} diff --git a/masquerade.routing.yml b/masquerade.routing.yml index dcf3bff..e4bdcc0 100644 --- a/masquerade.routing.yml +++ b/masquerade.routing.yml @@ -16,11 +16,11 @@ masquerade.block: requirements: _masquerade_switch_access: 'TRUE' -masquerade.user: +entity.user.masquerade: path: '/user/{user}/masquerade' defaults: _controller: '\Drupal\masquerade\Controller\SwitchController::switchTo' - _title_callback: '\Drupal\masquerade\Controller\SwitchController::getTitle' + _title: 'Masquerade' requirements: _csrf_token: 'TRUE' diff --git a/masquerade.services.yml b/masquerade.services.yml index ac8c9d5..4c395de 100644 --- a/masquerade.services.yml +++ b/masquerade.services.yml @@ -1,4 +1,7 @@ services: + masquerade: + class: Drupal\masquerade\Masquerade + arguments: ['@current_user', '@entity.manager', '@module_handler', '@session_manager', '@logger.channel.masquerade'] logger.channel.masquerade: parent: logger.channel_base arguments: ['masquerade'] @@ -8,5 +11,6 @@ services: - { name: access_check, applies_to: _masquerade_switch_access } access_check.masquerade.unmasquerade: class: Drupal\masquerade\Access\UnmasqueradeAccessCheck + arguments: ['@masquerade'] tags: - { name: access_check, applies_to: _masquerade_unmasquerade_access } diff --git a/src/Access/SwitchAccessCheck.php b/src/Access/SwitchAccessCheck.php index 806c509..31a3824 100644 --- a/src/Access/SwitchAccessCheck.php +++ b/src/Access/SwitchAccessCheck.php @@ -10,7 +10,6 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Session\AccountInterface; use Drupal\user\Entity\Role; -use Symfony\Component\Routing\Route; /** * Checks access for any masquerade permissions. @@ -22,6 +21,9 @@ class SwitchAccessCheck implements AccessInterface { * * @param \Drupal\Core\Session\AccountInterface $account * Run access checks for this account. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. */ public function access(AccountInterface $account) { // Uid 1 may masquerade as anyone. @@ -42,4 +44,5 @@ class SwitchAccessCheck implements AccessInterface { } return AccessResult::neutral()->cachePerUser(); } + } diff --git a/src/Access/UnmasqueradeAccessCheck.php b/src/Access/UnmasqueradeAccessCheck.php index 3c6ce73..10d54dd 100644 --- a/src/Access/UnmasqueradeAccessCheck.php +++ b/src/Access/UnmasqueradeAccessCheck.php @@ -8,9 +8,7 @@ namespace Drupal\masquerade\Access; use Drupal\Core\Access\AccessResult; use Drupal\Core\Routing\Access\AccessInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\user\Entity\Role; -use Symfony\Component\Routing\Route; +use Drupal\masquerade\Masquerade; /** * Checks access based on the masquerade status of the user. @@ -18,12 +16,30 @@ use Symfony\Component\Routing\Route; class UnmasqueradeAccessCheck implements AccessInterface { /** + * The masquerade service. + * + * @var \Drupal\masquerade\Masquerade + */ + protected $masquerade; + + /** + * Constructs a new UnmasqueradeAccessCheck object. + * + * @param \Drupal\masquerade\Masquerade $masquerade + * The masquerade service. + */ + public function __construct(Masquerade $masquerade) { + $this->masquerade = $masquerade; + } + + /** * Check to see if user is masquerading. * - * @param \Drupal\Core\Session\AccountInterface $account - * Run access checks for this account. + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. */ - public function access(AccountInterface $account) { - return AccessResult::allowedIf(!empty($_SESSION['masquerading']))->setCacheable(FALSE); + public function access() { + return AccessResult::allowedIf($this->masquerade->isMasquerading())->setCacheable(FALSE); } + } diff --git a/src/Controller/SwitchController.php b/src/Controller/SwitchController.php index 96b15b8..84b17fa 100644 --- a/src/Controller/SwitchController.php +++ b/src/Controller/SwitchController.php @@ -8,7 +8,12 @@ namespace Drupal\masquerade\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Session\AccountInterface; +use Drupal\masquerade\Masquerade; use Drupal\user\UserInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; /** * Controller for switch and back to masquerade as user. @@ -16,6 +21,36 @@ use Drupal\user\UserInterface; class SwitchController extends ControllerBase { /** + * The masquerade service. + * + * @var \Drupal\masquerade\Masquerade + */ + protected $masquerade; + + /** + * Constructs a new SwitchController object. + * + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user + * @param \Drupal\masquerade\Masquerade $masquerade + * The masquerade service. + */ + public function __construct(AccountInterface $current_user, Masquerade $masquerade) { + $this->currentUser = $current_user; + $this->masquerade = $masquerade; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('current_user'), + $container->get('masquerade') + ); + } + + /** * Masquerades the current user as a given user. * * Access to masquerade as the target user account has to checked by all callers @@ -28,26 +63,45 @@ class SwitchController extends ControllerBase { * Redirect to previous page. */ public function switchTo(UserInterface $user) { - // @todo Refactor switching to service. - return masquerade_switch_user_page($user); + // Store current user for messages. + $account = $this->currentUser; + $error = masquerade_switch_user_validate($user); + if (empty($error)) { + if ($this->masquerade->switchTo($user)) { + drupal_set_message($this->t('You are now masquerading as @user.', array( + '@user' => $account->getUsername(), + ))); + } + } + else { + drupal_set_message($error, 'error'); + } + return new RedirectResponse(\Drupal::request()->server->get('HTTP_REFERER')); } /** * Allows a user who is currently masquerading to become a new user. * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. * * @return \Symfony\Component\HttpFoundation\RedirectResponse + * Redirect response to previous page. */ - public function switchBack() { - // @todo Refactor switching to service. - return masquerade_switch_back_page(); + public function switchBack(Request $request) { + // Store current user for messages. + $account = $this->currentUser; + if ($this->masquerade->switchBack()) { + drupal_set_message($this->t('You are no longer masquerading as @user.', array( + '@user' => $account->getUsername(), + ))); + } + else { + drupal_set_message($this->t('Error trying unmasquerading as @user.', array( + '@user' => $account->getUsername(), + )), 'error'); + } + return new RedirectResponse($request->server->get('HTTP_REFERER')); } - /** - * Title callback for the masquerade.user route. - */ - public function getTitle(UserInterface $user) { - return t('Masquerade as @user', ['@user' => $user->getUsername()]); - } } diff --git a/src/Form/MasqueradeForm.php b/src/Form/MasqueradeForm.php index 5016d47..fbce4a4 100644 --- a/src/Form/MasqueradeForm.php +++ b/src/Form/MasqueradeForm.php @@ -10,6 +10,7 @@ namespace Drupal\masquerade\Form; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\masquerade\Masquerade; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -25,13 +26,23 @@ class MasqueradeForm extends FormBase { protected $entityManager; /** + * The masquerade service. + * + * @var \Drupal\masquerade\Masquerade + */ + protected $masquerade; + + /** * Constructs a MasqueradeForm object. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. + * @param \Drupal\masquerade\Masquerade $masquerade + * The masquerade service. */ - public function __construct(EntityManagerInterface $entity_manager) { + public function __construct(EntityManagerInterface $entity_manager, Masquerade $masquerade) { $this->entityManager = $entity_manager; + $this->masquerade = $masquerade; } /** @@ -39,7 +50,8 @@ class MasqueradeForm extends FormBase { */ public static function create(ContainerInterface $container) { return new static( - $container->get('entity.manager') + $container->get('entity.manager'), + $container->get('masquerade') ); } @@ -58,6 +70,7 @@ class MasqueradeForm extends FormBase { '#type' => 'container', '#attributes' => array('class' => array('container-inline')), ); + // @todo Use core's auto-complete element. $form['autocomplete']['masquerade_as'] = array( '#type' => 'textfield', '#title' => $this->t('Username'), @@ -102,7 +115,7 @@ class MasqueradeForm extends FormBase { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - masquerade_switch_user($form_state->getValue('masquerade_target_account')); + $this->masquerade->switchTo($form_state->getValue('masquerade_target_account')); } } diff --git a/src/Masquerade.php b/src/Masquerade.php new file mode 100644 index 0000000..e6cae1e --- /dev/null +++ b/src/Masquerade.php @@ -0,0 +1,155 @@ +currentUser = $current_user; + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + $this->sessionManager = $session_manager; + $this->logger = $logger; + } + + /** + * Returns whether the current user is masquerading. + * + * @return bool + */ + public function isMasquerading() { + // @todo Check to use some session related service. + return !empty($_SESSION['masquerading']); + } + + /** + * Masquerades the current user as a given user. + * + * @param \Drupal\user\UserInterface $target_account + * The user account object to masquerade as. + * + * @return bool + * TRUE when masqueraded, FALSE otherwise. + */ + public function switchTo(UserInterface $target_account) { + $account = $this->currentUser; + + // Call logout hooks when switching from original user. + $this->moduleHandler->invokeAll('user_logout', [$account]); + + // Regenerate the session ID to prevent against session fixation attacks. + $this->sessionManager->regenerate(); + + $_SESSION['masquerading'] = $account->id(); + + // Supposed "safe" user switch method: + // https://www.drupal.org/node/218104 + //$accountSwitcher = Drupal::service('account_switcher'); + //$accountSwitcher->switchTo(new UserSession(array('uid' => $account->id()))); + $this->currentUser->setAccount($target_account); + + // Call all login hooks when switching to masquerading user. + $this->moduleHandler->invokeAll('user_login', [$target_account]); + + $this->logger->info('User %username masqueraded as %target_username.', array( + '%username' => $account->getUsername(), + '%target_username' => $target_account->getUsername(), + 'link' => $this->l($this->t('view'), $target_account->urlInfo()), + )); + return TRUE; + } + + /** + * Switching back to previous user. + * + * @return bool + * TRUE when switched back, FALSE otherwise. + */ + public function switchBack() { + if (empty($_SESSION['masquerading'])) { + return FALSE; + } + $new_user = $this->entityManager + ->getStorage('user') + ->load($_SESSION['masquerading']); + + // Ensure the flag is cleared. + unset($_SESSION['masquerading']); + if (!$new_user) { + return FALSE; + } + + $account = $this->currentUser; + // Call logout hooks when switching from masquerading user. + $this->moduleHandler->invokeAll('user_logout', [$account]); + // Regenerate the session ID to prevent against session fixation attacks. + // @todo Maybe session service migrate. + $this->sessionManager->regenerate(); + + $this->currentUser->setAccount($new_user); + // Call all login hooks when switching back to original user. + $this->moduleHandler->invokeAll('user_login', [$new_user]); + + $this->logger->info('User %username stopped masquerading as %old_username.', array( + '%username' => $new_user->getUsername(), + '%old_username' => $account->getUsername(), + 'link' => $this->l($this->t('view'), $new_user->urlInfo()), + )); + return TRUE; + } + +} diff --git a/src/Plugin/Block/MasqueradeBlock.php b/src/Plugin/Block/MasqueradeBlock.php index e3a9768..4898ce3 100644 --- a/src/Plugin/Block/MasqueradeBlock.php +++ b/src/Plugin/Block/MasqueradeBlock.php @@ -11,6 +11,7 @@ use Drupal\Core\Block\BlockBase; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\masquerade\Masquerade; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -32,6 +33,13 @@ class MasqueradeBlock extends BlockBase implements ContainerFactoryPluginInterfa protected $formBuilder; /** + * The masquerade service. + * + * @var \Drupal\masquerade\Masquerade + */ + protected $masquerade; + + /** * Constructs a new MasqueradeBlock object. * * @param array $configuration @@ -42,11 +50,14 @@ class MasqueradeBlock extends BlockBase implements ContainerFactoryPluginInterfa * The plugin implementation definition. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder service. + * @param \Drupal\masquerade\Masquerade $masquerade + * The masquerade service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder, Masquerade $masquerade) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->formBuilder = $form_builder; + $this->masquerade = $masquerade; } /** @@ -57,7 +68,8 @@ class MasqueradeBlock extends BlockBase implements ContainerFactoryPluginInterfa $configuration, $plugin_id, $plugin_definition, - $container->get('form_builder') + $container->get('form_builder'), + $container->get('masquerade') ); } @@ -65,7 +77,7 @@ class MasqueradeBlock extends BlockBase implements ContainerFactoryPluginInterfa * {@inheritdoc} */ protected function blockAccess(AccountInterface $account) { - return $account->hasPermission('masquerade as any user') && !masquerade_user_is_masquerading(); + return $account->hasPermission('masquerade as any user') && !$this->masquerade->isMasquerading(); } /**