trigger.module

You are here

  1. 7 modules/trigger/trigger.module
  2. 6 modules/trigger/trigger.module

Enables functions to be stored and executed at a later time.

Functions

Namesort descending Description
trigger_actions_delete Implements hook_actions_delete().
trigger_comment_delete Implements hook_comment_delete().
trigger_comment_insert Implements hook_comment_insert().
trigger_comment_presave Implements hook_comment_presave().
trigger_comment_update Implements hook_comment_update().
trigger_comment_view Implements hook_comment_view().
trigger_cron Implements hook_cron().
trigger_forms Implements hook_forms().
trigger_get_assigned_actions Gets the action IDs of actions to be executed for a hook.
trigger_help Implements hook_help().
trigger_menu Implements hook_menu().
trigger_menu_unassign_access Access callback: Determines if triggers can be unassigned.
trigger_node_delete Implements hook_node_delete().
trigger_node_insert Implements hook_node_insert().
trigger_node_presave Implements hook_node_presave().
trigger_node_update Implements hook_node_update().
trigger_node_view Implements hook_node_view().
trigger_taxonomy_term_delete Implements hook_taxonomy_term_delete().
trigger_taxonomy_term_insert Implements hook_taxonomy_term_insert().
trigger_taxonomy_term_update Implements hook_taxonomy_term_update().
trigger_theme Implements hook_theme().
trigger_trigger_info Implements hook_trigger_info().
trigger_user_cancel Implements hook_user_cancel().
trigger_user_delete Implements hook_user_delete().
trigger_user_insert Implements hook_user_insert().
trigger_user_login Implements hook_user_login().
trigger_user_logout Implements hook_user_logout().
trigger_user_update Implements hook_user_update().
trigger_user_view Implements hook_user_view().
_trigger_comment Calls action functions for comment triggers.
_trigger_get_all_info Retrieves and caches information from hook_trigger_info() implementations.
_trigger_node Calls action functions for node triggers.
_trigger_normalize_comment_context Loads associated objects for comment triggers.
_trigger_normalize_node_context Loads associated objects for node triggers.
_trigger_normalize_user_context Loads associated objects for user triggers.
_trigger_tab_information Gathers information about tabs on the triggers administration screen.
_trigger_taxonomy Calls action functions for taxonomy triggers.
_trigger_user Calls action functions for user triggers.

File

modules/trigger/trigger.module
View source
  1. <?php
  2. /**
  3. * @file
  4. * Enables functions to be stored and executed at a later time.
  5. */
  6. /**
  7. * Implements hook_help().
  8. */
  9. function trigger_help($path, $arg) {
  10. // Generate help text for admin/structure/trigger/(module) tabs.
  11. $matches = array();
  12. if (preg_match('|^admin/structure/trigger/(.*)$|', $path, $matches)) {
  13. $explanation = '<p>' . t('Triggers are events on your site, such as new content being added or a user logging in. The Trigger module associates these triggers with actions (functional tasks), such as unpublishing content containing certain keywords or e-mailing an administrator. The <a href="@url">Actions settings page</a> contains a list of existing actions and provides the ability to create and configure advanced actions (actions requiring configuration, such as an e-mail address or a list of banned words).', array('@url' => url('admin/config/system/actions'))) . '</p>';
  14. $module = $matches[1];
  15. $trigger_info = _trigger_tab_information();
  16. if (!empty($trigger_info[$module])) {
  17. $explanation .= '<p>' . t('There is a tab on this page for each module that defines triggers. On this tab you can assign actions to run when triggers from the <a href="@module-help">@module-name module</a> happen.', array('@module-help' => url('admin/help/' . $module), '@module-name' => $trigger_info[$module])) . '</p>';
  18. }
  19. return $explanation;
  20. }
  21. if ($path == 'admin/help#trigger') {
  22. $output = '';
  23. $output .= '<h3>' . t('About') . '</h3>';
  24. $output .= '<p>' . t('The Trigger module provides the ability to cause <em>actions</em> to run when certain <em>triggers</em> take place on your site. Triggers are events, such as new content being added to your site or a user logging in, and actions are tasks, such as unpublishing content or e-mailing an administrator. For more information, see the online handbook entry for <a href="@trigger">Trigger module</a>.', array('@trigger' => 'http://drupal.org/documentation/modules/trigger/')) . '</p>';
  25. $output .= '<h3>' . t('Uses') . '</h3>';
  26. $output .= '<dl>';
  27. $output .= '<dt>' . t('Configuring triggers and actions') . '</dt>';
  28. $output .= '<dd>' . t('The combination of actions and triggers can perform many useful tasks, such as e-mailing an administrator if a user account is deleted, or automatically unpublishing comments that contain certain words. To set up a trigger/action combination, first visit the <a href="@actions-page">Actions configuration page</a>, where you can either verify that the action you want is already listed, or create a new <em>advanced</em> action. You will need to set up an advanced action if there are configuration options in your trigger/action combination, such as specifying an e-mail address or a list of banned words. After configuring or verifying your action, visit the <a href="@triggers-page">Triggers configuration page</a> and choose the appropriate tab (Comment, Taxonomy, etc.), where you can assign the action to run when the trigger event occurs.', array('@triggers-page' => url('admin/structure/trigger'), '@actions-page' => url('admin/config/system/actions'))) . '</dd>';
  29. $output .= '</dl>';
  30. return $output;
  31. }
  32. }
  33. /**
  34. * Implements hook_menu().
  35. */
  36. function trigger_menu() {
  37. $items['admin/structure/trigger'] = array(
  38. 'title' => 'Triggers',
  39. 'description' => 'Configure when to execute actions.',
  40. 'page callback' => 'trigger_assign',
  41. 'access arguments' => array('administer actions'),
  42. 'file' => 'trigger.admin.inc',
  43. );
  44. $trigger_info = _trigger_tab_information();
  45. foreach ($trigger_info as $module => $module_name) {
  46. $items["admin/structure/trigger/$module"] = array(
  47. 'title' => $module_name,
  48. 'page callback' => 'trigger_assign',
  49. 'page arguments' => array($module),
  50. 'access arguments' => array('administer actions'),
  51. 'type' => MENU_LOCAL_TASK,
  52. 'file' => 'trigger.admin.inc',
  53. );
  54. }
  55. $items['admin/structure/trigger/unassign'] = array(
  56. 'title' => 'Unassign',
  57. 'description' => 'Unassign an action from a trigger.',
  58. 'page callback' => 'drupal_get_form',
  59. 'page arguments' => array('trigger_unassign'),
  60. // Only accessible if there are any actions that can be unassigned.
  61. 'access callback' => 'trigger_menu_unassign_access',
  62. // Only output in the breadcrumb, not in menu trees.
  63. 'type' => MENU_VISIBLE_IN_BREADCRUMB,
  64. 'file' => 'trigger.admin.inc',
  65. );
  66. return $items;
  67. }
  68. /**
  69. * Access callback: Determines if triggers can be unassigned.
  70. *
  71. * @return bool
  72. * TRUE if there are triggers that the user can unassign, FALSE otherwise.
  73. *
  74. * @see trigger_menu()
  75. */
  76. function trigger_menu_unassign_access() {
  77. if (!user_access('administer actions')) {
  78. return FALSE;
  79. }
  80. $count = db_select('trigger_assignments')
  81. ->countQuery()
  82. ->execute()
  83. ->fetchField();
  84. return $count > 0;
  85. }
  86. /**
  87. * Implements hook_trigger_info().
  88. *
  89. * Defines all the triggers that this module implements triggers for.
  90. */
  91. function trigger_trigger_info() {
  92. return array(
  93. 'node' => array(
  94. 'node_presave' => array(
  95. 'label' => t('When either saving new content or updating existing content'),
  96. ),
  97. 'node_insert' => array(
  98. 'label' => t('After saving new content'),
  99. ),
  100. 'node_update' => array(
  101. 'label' => t('After saving updated content'),
  102. ),
  103. 'node_delete' => array(
  104. 'label' => t('After deleting content'),
  105. ),
  106. 'node_view' => array(
  107. 'label' => t('When content is viewed by an authenticated user'),
  108. ),
  109. ),
  110. 'comment' => array(
  111. 'comment_presave' => array(
  112. 'label' => t('When either saving a new comment or updating an existing comment'),
  113. ),
  114. 'comment_insert' => array(
  115. 'label' => t('After saving a new comment'),
  116. ),
  117. 'comment_update' => array(
  118. 'label' => t('After saving an updated comment'),
  119. ),
  120. 'comment_delete' => array(
  121. 'label' => t('After deleting a comment'),
  122. ),
  123. 'comment_view' => array(
  124. 'label' => t('When a comment is being viewed by an authenticated user'),
  125. ),
  126. ),
  127. 'taxonomy' => array(
  128. 'taxonomy_term_insert' => array(
  129. 'label' => t('After saving a new term to the database'),
  130. ),
  131. 'taxonomy_term_update' => array(
  132. 'label' => t('After saving an updated term to the database'),
  133. ),
  134. 'taxonomy_term_delete' => array(
  135. 'label' => t('After deleting a term'),
  136. ),
  137. ),
  138. 'system' => array(
  139. 'cron' => array(
  140. 'label' => t('When cron runs'),
  141. ),
  142. ),
  143. 'user' => array(
  144. 'user_insert' => array(
  145. 'label' => t('After creating a new user account'),
  146. ),
  147. 'user_update' => array(
  148. 'label' => t('After updating a user account'),
  149. ),
  150. 'user_delete' => array(
  151. 'label' => t('After a user has been deleted'),
  152. ),
  153. 'user_login' => array(
  154. 'label' => t('After a user has logged in'),
  155. ),
  156. 'user_logout' => array(
  157. 'label' => t('After a user has logged out'),
  158. ),
  159. 'user_view' => array(
  160. 'label' => t("When a user's profile is being viewed"),
  161. ),
  162. ),
  163. );
  164. }
  165. /**
  166. * Gets the action IDs of actions to be executed for a hook.
  167. *
  168. * @param $hook
  169. * The name of the hook being fired.
  170. *
  171. * @return
  172. * An array whose keys are action IDs that the user has associated with
  173. * this trigger, and whose values are arrays containing the action type and
  174. * label.
  175. */
  176. function trigger_get_assigned_actions($hook) {
  177. $actions = &drupal_static(__FUNCTION__, array());
  178. if (!isset($actions[$hook])) {
  179. $actions[$hook] = db_query("SELECT ta.aid, a.type, a.label FROM {trigger_assignments} ta LEFT JOIN {actions} a ON ta.aid = a.aid WHERE ta.hook = :hook ORDER BY ta.weight", array(
  180. ':hook' => $hook,
  181. ))->fetchAllAssoc('aid', PDO::FETCH_ASSOC);
  182. }
  183. return $actions[$hook];
  184. }
  185. /**
  186. * Implements hook_theme().
  187. */
  188. function trigger_theme() {
  189. return array(
  190. 'trigger_display' => array(
  191. 'render element' => 'element',
  192. 'file' => 'trigger.admin.inc',
  193. ),
  194. );
  195. }
  196. /**
  197. * Implements hook_forms().
  198. *
  199. * We re-use code by using the same assignment form definition for each hook.
  200. */
  201. function trigger_forms() {
  202. $trigger_info = _trigger_get_all_info();
  203. $forms = array();
  204. foreach ($trigger_info as $module => $hooks) {
  205. foreach ($hooks as $hook => $description) {
  206. $forms['trigger_' . $hook . '_assign_form'] = array('callback' => 'trigger_assign_form');
  207. }
  208. }
  209. return $forms;
  210. }
  211. /**
  212. * Loads associated objects for node triggers.
  213. *
  214. * When an action is called in a context that does not match its type, the
  215. * object that the action expects must be retrieved. For example, when an action
  216. * that works on users is called during a node hook implementation, the user
  217. * object is not available since the node hook call doesn't pass it. So here we
  218. * load the object the action expects.
  219. *
  220. * @param $type
  221. * The type of action that is about to be called.
  222. * @param $node
  223. * The node that was passed via the node hook.
  224. *
  225. * @return
  226. * The object expected by the action that is about to be called.
  227. */
  228. function _trigger_normalize_node_context($type, $node) {
  229. // Note that comment-type actions are not supported in node contexts,
  230. // because we wouldn't know which comment to choose.
  231. switch ($type) {
  232. // An action that works on users is being called in a node context.
  233. // Load the user object of the node's author.
  234. case 'user':
  235. return user_load($node->uid);
  236. }
  237. }
  238. /**
  239. * Calls action functions for node triggers.
  240. *
  241. * @param $node
  242. * Node object.
  243. * @param $hook
  244. * Hook to trigger.
  245. * @param $a3
  246. * Additional argument to action function.
  247. * @param $a4
  248. * Additional argument to action function.
  249. */
  250. function _trigger_node($node, $hook, $a3 = NULL, $a4 = NULL) {
  251. // Keep objects for reuse so that changes actions make to objects can persist.
  252. static $objects;
  253. // Prevent recursion by tracking which operations have already been called.
  254. static $recursion;
  255. $aids = trigger_get_assigned_actions($hook);
  256. if (!$aids) {
  257. return;
  258. }
  259. if (isset($recursion[$hook])) {
  260. return;
  261. }
  262. $recursion[$hook] = TRUE;
  263. $context = array(
  264. 'group' => 'node',
  265. 'hook' => $hook,
  266. );
  267. // We need to get the expected object if the action's type is not 'node'.
  268. // We keep the object in $objects so we can reuse it if we have multiple actions
  269. // that make changes to an object.
  270. foreach ($aids as $aid => $info) {
  271. $type = $info['type'];
  272. if ($type != 'node') {
  273. if (!isset($objects[$type])) {
  274. $objects[$type] = _trigger_normalize_node_context($type, $node);
  275. }
  276. // Since we know about the node, we pass that info along to the action.
  277. $context['node'] = $node;
  278. $result = actions_do($aid, $objects[$type], $context, $a3, $a4);
  279. }
  280. else {
  281. actions_do($aid, $node, $context, $a3, $a4);
  282. }
  283. }
  284. unset($recursion[$hook]);
  285. }
  286. /**
  287. * Implements hook_node_view().
  288. */
  289. function trigger_node_view($node, $view_mode) {
  290. _trigger_node($node, 'node_view', $view_mode);
  291. }
  292. /**
  293. * Implements hook_node_update().
  294. */
  295. function trigger_node_update($node) {
  296. _trigger_node($node, 'node_update');
  297. }
  298. /**
  299. * Implements hook_node_presave().
  300. */
  301. function trigger_node_presave($node) {
  302. _trigger_node($node, 'node_presave');
  303. }
  304. /**
  305. * Implements hook_node_insert().
  306. */
  307. function trigger_node_insert($node) {
  308. _trigger_node($node, 'node_insert');
  309. }
  310. /**
  311. * Implements hook_node_delete().
  312. */
  313. function trigger_node_delete($node) {
  314. _trigger_node($node, 'node_delete');
  315. }
  316. /**
  317. * Loads associated objects for comment triggers.
  318. *
  319. * When an action is called in a context that does not match its type, the
  320. * object that the action expects must be retrieved. For example, when an action
  321. * that works on nodes is called during the comment hook, the node object is not
  322. * available since the comment hook doesn't pass it. So here we load the object
  323. * the action expects.
  324. *
  325. * @param $type
  326. * The type of action that is about to be called.
  327. * @param $comment
  328. * The comment that was passed via the comment hook.
  329. *
  330. * @return
  331. * The object expected by the action that is about to be called.
  332. */
  333. function _trigger_normalize_comment_context($type, $comment) {
  334. switch ($type) {
  335. // An action that works with nodes is being called in a comment context.
  336. case 'node':
  337. return node_load(is_array($comment) ? $comment['nid'] : $comment->nid);
  338. // An action that works on users is being called in a comment context.
  339. case 'user':
  340. return user_load(is_array($comment) ? $comment['uid'] : $comment->uid);
  341. }
  342. }
  343. /**
  344. * Implements hook_comment_presave().
  345. */
  346. function trigger_comment_presave($comment) {
  347. _trigger_comment($comment, 'comment_presave');
  348. }
  349. /**
  350. * Implements hook_comment_insert().
  351. */
  352. function trigger_comment_insert($comment) {
  353. _trigger_comment($comment, 'comment_insert');
  354. }
  355. /**
  356. * Implements hook_comment_update().
  357. */
  358. function trigger_comment_update($comment) {
  359. _trigger_comment($comment, 'comment_update');
  360. }
  361. /**
  362. * Implements hook_comment_delete().
  363. */
  364. function trigger_comment_delete($comment) {
  365. _trigger_comment($comment, 'comment_delete');
  366. }
  367. /**
  368. * Implements hook_comment_view().
  369. */
  370. function trigger_comment_view($comment) {
  371. _trigger_comment($comment, 'comment_view');
  372. }
  373. /**
  374. * Calls action functions for comment triggers.
  375. *
  376. * @param $a1
  377. * Comment object or array of form values.
  378. * @param $hook
  379. * Hook to trigger.
  380. */
  381. function _trigger_comment($a1, $hook) {
  382. // Keep objects for reuse so that changes actions make to objects can persist.
  383. static $objects;
  384. $aids = trigger_get_assigned_actions($hook);
  385. $context = array(
  386. 'group' => 'comment',
  387. 'hook' => $hook,
  388. );
  389. // We need to get the expected object if the action's type is not 'comment'.
  390. // We keep the object in $objects so we can reuse it if we have multiple
  391. // actions that make changes to an object.
  392. foreach ($aids as $aid => $info) {
  393. $type = $info['type'];
  394. if ($type != 'comment') {
  395. if (!isset($objects[$type])) {
  396. $objects[$type] = _trigger_normalize_comment_context($type, $a1);
  397. }
  398. // Since we know about the comment, we pass it along to the action
  399. // in case it wants to peek at it.
  400. $context['comment'] = (object) $a1;
  401. actions_do($aid, $objects[$type], $context);
  402. }
  403. else {
  404. actions_do($aid, $a1, $context);
  405. }
  406. }
  407. }
  408. /**
  409. * Implements hook_cron().
  410. */
  411. function trigger_cron() {
  412. $aids = trigger_get_assigned_actions('cron');
  413. $context = array(
  414. 'group' => 'cron',
  415. 'hook' => 'cron',
  416. );
  417. // Cron does not act on any specific object.
  418. $object = NULL;
  419. actions_do(array_keys($aids), $object, $context);
  420. }
  421. /**
  422. * Loads associated objects for user triggers.
  423. *
  424. * When an action is called in a context that does not match its type, the
  425. * object that the action expects must be retrieved. For example, when an action
  426. * that works on nodes is called during the user hook, the node object is not
  427. * available since the user hook doesn't pass it. So here we load the object the
  428. * action expects.
  429. *
  430. * @param $type
  431. * The type of action that is about to be called.
  432. * @param $account
  433. * The account object that was passed via the user hook.
  434. *
  435. * @return
  436. * The object expected by the action that is about to be called.
  437. */
  438. function _trigger_normalize_user_context($type, $account) {
  439. // Note that comment-type actions are not supported in user contexts,
  440. // because we wouldn't know which comment to choose.
  441. switch ($type) {
  442. // An action that works with nodes is being called in a user context.
  443. // If a single node is being viewed, return the node.
  444. case 'node':
  445. // If we are viewing an individual node, return the node.
  446. if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == NULL) {
  447. return node_load(array('nid' => arg(1)));
  448. }
  449. break;
  450. }
  451. }
  452. /**
  453. * Implements hook_user_login().
  454. */
  455. function trigger_user_login(&$edit, $account, $category) {
  456. _trigger_user('user_login', $edit, $account, $category);
  457. }
  458. /**
  459. * Implements hook_user_logout().
  460. */
  461. function trigger_user_logout($account) {
  462. $edit = array();
  463. _trigger_user('user_logout', $edit, $account);
  464. }
  465. /**
  466. * Implements hook_user_insert().
  467. */
  468. function trigger_user_insert(&$edit, $account, $category) {
  469. _trigger_user('user_insert', $edit, $account, $category);
  470. }
  471. /**
  472. * Implements hook_user_update().
  473. */
  474. function trigger_user_update(&$edit, $account, $category) {
  475. _trigger_user('user_update', $edit, $account, $category);
  476. }
  477. /**
  478. * Implements hook_user_cancel().
  479. */
  480. function trigger_user_cancel($edit, $account, $method) {
  481. switch ($method) {
  482. case 'user_cancel_reassign':
  483. _trigger_user('user_delete', $edit, $account, $method);
  484. break;
  485. }
  486. }
  487. /**
  488. * Implements hook_user_delete().
  489. */
  490. function trigger_user_delete($account) {
  491. $edit = array();
  492. _trigger_user('user_delete', $edit, $account, NULL);
  493. }
  494. /**
  495. * Implements hook_user_view().
  496. */
  497. function trigger_user_view($account) {
  498. $edit = NULL;
  499. _trigger_user('user_view', $edit, $account, NULL);
  500. }
  501. /**
  502. * Calls action functions for user triggers.
  503. *
  504. * @param $hook
  505. * The hook that called this function.
  506. * @param $edit
  507. * Edit variable passed in to the hook or empty array if not needed.
  508. * @param $account
  509. * Account variable passed in to the hook.
  510. * @param $method
  511. * Method variable passed in to the hook or NULL if not needed.
  512. */
  513. function _trigger_user($hook, &$edit, $account, $category = NULL) {
  514. // Keep objects for reuse so that changes actions make to objects can persist.
  515. static $objects;
  516. $aids = trigger_get_assigned_actions($hook);
  517. $context = array(
  518. 'group' => 'user',
  519. 'hook' => $hook,
  520. 'form_values' => &$edit,
  521. );
  522. foreach ($aids as $aid => $info) {
  523. $type = $info['type'];
  524. if ($type != 'user') {
  525. if (!isset($objects[$type])) {
  526. $objects[$type] = _trigger_normalize_user_context($type, $account);
  527. }
  528. $context['user'] = $account;
  529. actions_do($aid, $objects[$type], $context);
  530. }
  531. else {
  532. actions_do($aid, $account, $context, $category);
  533. }
  534. }
  535. }
  536. /**
  537. * Calls action functions for taxonomy triggers.
  538. *
  539. * @param $hook
  540. * Hook to trigger actions for taxonomy_term_insert(),
  541. * taxonomy_term_update(), and taxonomy_term_delete().
  542. * @param $array
  543. * Item on which operation is being performed, either a term or
  544. * form values.
  545. */
  546. function _trigger_taxonomy($hook, $array) {
  547. $aids = trigger_get_assigned_actions($hook);
  548. $context = array(
  549. 'group' => 'taxonomy',
  550. 'hook' => $hook
  551. );
  552. actions_do(array_keys($aids), (object) $array, $context);
  553. }
  554. /**
  555. * Implements hook_taxonomy_term_insert().
  556. */
  557. function trigger_taxonomy_term_insert($term) {
  558. _trigger_taxonomy('taxonomy_term_insert', (array) $term);
  559. }
  560. /**
  561. * Implements hook_taxonomy_term_update().
  562. */
  563. function trigger_taxonomy_term_update($term) {
  564. _trigger_taxonomy('taxonomy_term_update', (array) $term);
  565. }
  566. /**
  567. * Implements hook_taxonomy_term_delete().
  568. */
  569. function trigger_taxonomy_term_delete($term) {
  570. _trigger_taxonomy('taxonomy_term_delete', (array) $term);
  571. }
  572. /**
  573. * Implements hook_actions_delete().
  574. *
  575. * Removes all trigger entries for the given action, when an action is deleted.
  576. */
  577. function trigger_actions_delete($aid) {
  578. db_delete('trigger_assignments')
  579. ->condition('aid', $aid)
  580. ->execute();
  581. drupal_static_reset('trigger_get_assigned_actions');
  582. }
  583. /**
  584. * Retrieves and caches information from hook_trigger_info() implementations.
  585. *
  586. * @return
  587. * Array of all triggers.
  588. */
  589. function _trigger_get_all_info() {
  590. $triggers = &drupal_static(__FUNCTION__);
  591. if (!isset($triggers)) {
  592. $triggers = module_invoke_all('trigger_info');
  593. drupal_alter('trigger_info', $triggers);
  594. }
  595. return $triggers;
  596. }
  597. /**
  598. * Gathers information about tabs on the triggers administration screen.
  599. *
  600. * @return
  601. * Array of modules that have triggers, with the keys being the
  602. * machine-readable name of the module, and the values being the
  603. * human-readable name of the module.
  604. */
  605. function _trigger_tab_information() {
  606. // Gather information about all triggers and modules.
  607. $trigger_info = _trigger_get_all_info();
  608. $modules = system_get_info('module');
  609. $modules = array_intersect_key($modules, $trigger_info);
  610. $return_info = array();
  611. foreach ($modules as $name => $info) {
  612. $return_info[$name] = $info['name'];
  613. }
  614. return $return_info;
  615. }