account_sentinel.module 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. <?php
  2. /**
  3. * @file
  4. * Detects changes made to selected roles' accounts.
  5. */
  6. /**
  7. * Indicates that the username was changed.
  8. */
  9. define('ACCOUNT_SENTINEL_EVENT_TYPE_NAME', 'name');
  10. /**
  11. * Indicates that the password was changed.
  12. */
  13. define('ACCOUNT_SENTINEL_EVENT_TYPE_PASS', 'pass');
  14. /**
  15. * Indicates that the e-mail address was changed.
  16. */
  17. define('ACCOUNT_SENTINEL_EVENT_TYPE_MAIL', 'mail');
  18. /**
  19. * Indicates that a monitored role was added.
  20. */
  21. define('ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD', 'role_add');
  22. /**
  23. * Indicates that a monitored role was revoked.
  24. */
  25. define('ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE', 'role_remove');
  26. /**
  27. * Indicates that the snapshot of the user was not valid.
  28. *
  29. * This is a critical event.
  30. */
  31. define('ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID', 'snapshot_invalid');
  32. /**
  33. * Indicates that the snapshot of the user should already exist but it does not.
  34. *
  35. * This is a critical event.
  36. */
  37. define('ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING', 'snapshot_missing');
  38. /**
  39. * Indicates that a new user was created with one or more monitored permissions.
  40. */
  41. define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD', 'user_add');
  42. /**
  43. * Indicates that the user was deleted.
  44. */
  45. define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE', 'user_delete');
  46. /**
  47. * Indicates that the user got blocked.
  48. */
  49. define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK', 'user_block');
  50. /**
  51. * Indicates that the user got unblocked.
  52. */
  53. define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK', 'user_unblock');
  54. /**
  55. * Indicates that the change was caught via hooks inside Drupal.
  56. */
  57. define('ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK', 'drupal');
  58. /**
  59. * Indicates that the change was caught by checking the database.
  60. *
  61. * This is a critical event.
  62. */
  63. define('ACCOUNT_SENTINEL_EVENT_ORIGIN_DB_CHECK', 'database');
  64. /**
  65. * Implements hook_help().
  66. */
  67. function account_sentinel_help($path, $arg) {
  68. switch ($path) {
  69. case 'admin/help#account_sentinel':
  70. $help = t("Account Sentinel perceives changes made to a configurable set of monitored roles' accounts, even those that are results of direct database modification (e.g. SQL injection). A set of e-mail addresses can be configured to be notified when such a change is detected, also a log of changes can be viewed in Drupal and via Drush for manual review.");
  71. return '<p>' . $help . '</p>';
  72. }
  73. }
  74. /**
  75. * Implements hook_permission().
  76. */
  77. function account_sentinel_permission() {
  78. return array(
  79. 'access account sentinel logs' => array(
  80. 'title' => t('Access Account Sentinel logs'),
  81. ),
  82. 'administer account sentinel' => array(
  83. 'title' => t("Change Account Sentinel's configuration"),
  84. ),
  85. );
  86. }
  87. /**
  88. * Implements hook_menu().
  89. */
  90. function account_sentinel_menu() {
  91. // Report page.
  92. $items['admin/reports/account-sentinel'] = array(
  93. 'title' => 'Account Sentinel log',
  94. 'description' => 'List of changes to monitored roles\' accounts perceived by Account Sentinel.',
  95. 'page callback' => 'account_sentinel_page_report',
  96. 'access arguments' => array('access account sentinel logs'),
  97. 'file' => 'account_sentinel.pages.inc',
  98. 'type' => MENU_NORMAL_ITEM,
  99. );
  100. // Settings page.
  101. $items['admin/config/system/account-sentinel'] = array(
  102. 'title' => 'Account Sentinel settings',
  103. 'description' => 'Manage Account Sentinel settings.',
  104. 'page callback' => 'drupal_get_form',
  105. 'page arguments' => array('account_sentinel_page_settings'),
  106. 'access arguments' => array('administer account sentinel'),
  107. 'file' => 'account_sentinel.pages.inc',
  108. );
  109. // Cron handler.
  110. $items['system/account-sentinel-cron'] = array(
  111. 'title' => 'Run Account Sentinel DB check',
  112. 'page callback' => 'account_sentinel_callback_cron',
  113. 'access callback' => TRUE,
  114. 'file' => 'account_sentinel.pages.inc',
  115. 'type' => MENU_CALLBACK,
  116. );
  117. // Cron key reset handler.
  118. $items['system/account-sentinel-reset-cron-key'] = array(
  119. 'title' => "Reset Account Sentinel's cron key",
  120. 'page callback' => 'account_sentinel_callback_reset_cron_key',
  121. 'access callback' => TRUE,
  122. 'access arguments' => array('administer account sentinel'),
  123. 'file' => 'account_sentinel.pages.inc',
  124. 'type' => MENU_CALLBACK,
  125. );
  126. return $items;
  127. }
  128. /**
  129. * Implements hook_theme().
  130. */
  131. function account_sentinel_theme($existing, $type, $theme, $path) {
  132. return array(
  133. 'account_sentinel_username' => array(
  134. 'variables' => array(
  135. 'uid' => 0,
  136. ),
  137. 'file' => 'account_sentinel.themes.inc',
  138. ),
  139. );
  140. }
  141. /**
  142. * Implements hook_user_update().
  143. */
  144. function account_sentinel_user_update(&$edit, $account, $category) {
  145. $new = account_sentinel_monitored_account_data($account);
  146. $original = account_sentinel_monitored_account_data($account->original);
  147. if ($new['monitored'] || $original['monitored']) {
  148. $changes = account_sentinel_detect_changes($new, $original);
  149. if (!empty($changes)) {
  150. account_sentinel_record_events($account->uid, ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $changes);
  151. account_sentinel_update_snapshot($new);
  152. }
  153. }
  154. }
  155. /**
  156. * Implements hook_password_strength_change().
  157. */
  158. function account_sentinel_password_strength_change($account, $strength) {
  159. // Store password strength for $account for later use.
  160. account_sentinel_password_strength($account->uid, password_strength_get_score($strength['score']));
  161. }
  162. /**
  163. * Provides static storage for password strengths.
  164. *
  165. * @param int $uid
  166. * UID of the user.
  167. * @param string $score
  168. * Password's strength.
  169. *
  170. * @return mixed
  171. * Returns the score for UID's new password or FALSE if not set.
  172. */
  173. function account_sentinel_password_strength($uid, $score = NULL) {
  174. static $strengths = array();
  175. // Update score.
  176. if (isset($score)) {
  177. $strengths[$uid] = $score;
  178. }
  179. // Return score.
  180. if (isset($strengths[$uid])) {
  181. return $strengths[$uid];
  182. }
  183. // Return FALSE if score is not available.
  184. return FALSE;
  185. }
  186. /**
  187. * Implements hook_account_sentinel_changes_alter().
  188. */
  189. function account_sentinel_account_sentinel_changes_alter(array &$changes, array &$new, array &$original) {
  190. // Include password strength information if available.
  191. $strength = account_sentinel_password_strength($original['uid']);
  192. if ($strength) {
  193. foreach ($changes as &$change) {
  194. if ($change['type'] == ACCOUNT_SENTINEL_EVENT_TYPE_PASS && !isset($change['data']['strength'])) {
  195. $change['data']['strength'] = $strength;
  196. }
  197. }
  198. }
  199. }
  200. /**
  201. * Implements hook_user_insert().
  202. */
  203. function account_sentinel_user_insert(&$edit, $account, $category) {
  204. $account = account_sentinel_monitored_account_data($account);
  205. if ($account['monitored']) {
  206. $events[] = array(
  207. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD,
  208. 'data' => array(
  209. 'uid' => $account['uid'],
  210. 'name' => $account['name'],
  211. 'mail' => $account['mail'],
  212. ),
  213. );
  214. foreach ($account['roles'] as $rid) {
  215. $events[] = array(
  216. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD,
  217. 'data' => array('rid' => $rid),
  218. );
  219. }
  220. account_sentinel_record_events($account['uid'], ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $events);
  221. account_sentinel_update_snapshot($account);
  222. }
  223. }
  224. /**
  225. * Implements hook_user_delete().
  226. */
  227. function account_sentinel_user_delete($account) {
  228. $account = account_sentinel_monitored_account_data($account);
  229. if ($account['monitored']) {
  230. $events[] = array(
  231. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE,
  232. 'data' => array(
  233. 'uid' => $account['uid'],
  234. 'name' => $account['name'],
  235. 'mail' => $account['mail'],
  236. ),
  237. );
  238. account_sentinel_record_events($account['uid'], ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $events);
  239. account_sentinel_delete_snapshot($account['uid']);
  240. }
  241. }
  242. /**
  243. * Implements hook_cron().
  244. */
  245. function account_sentinel_cron() {
  246. if (variable_get('account_sentinel_cron_method', 'drupal') == 'drupal') {
  247. watchdog('account_sentinel', "Invoked database audit from Drupal's cron.");
  248. module_load_include('inc', 'account_sentinel', 'account_sentinel.audit');
  249. account_sentinel_audit();
  250. }
  251. }
  252. /**
  253. * Implements hook_mail().
  254. */
  255. function account_sentinel_mail($key, &$message, $params) {
  256. switch ($key) {
  257. // Compose notification e-mail.
  258. case 'notification':
  259. // Collect information parameters.
  260. $origin = $params['origin'];
  261. $events = $params['events'];
  262. // Collect users' data.
  263. $users = array(
  264. 'changed' => array('uid' => $params['uid']),
  265. 'by' => array('uid' => $params['meta_data']['by_uid']),
  266. );
  267. foreach ($users as &$user) {
  268. $uid = $user['uid'];
  269. $user_object = user_load($uid);
  270. if ($user_object !== FALSE) {
  271. $user['name'] = $user_object->name;
  272. $user['link'] = l($user_object->name, 'user/' . $uid, array(
  273. 'absolute' => TRUE,
  274. ));
  275. }
  276. else {
  277. $user['name'] = t('Unknown');
  278. $user['link'] = $user['name'];
  279. }
  280. }
  281. // Compose e-mail.
  282. if (!empty($events)) {
  283. $message['subject'] = t(
  284. '[AS] User #@uid (@name) was changed',
  285. array(
  286. '@uid' => $users['changed']['uid'],
  287. '@name' => $users['changed']['name'],
  288. )
  289. );
  290. if ($origin != ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK) {
  291. $message['body'][] = '<strong>' . t('Warning: these changes were made outside of Drupal!') . '</strong>';
  292. }
  293. $message['body'][] = t(
  294. 'User #@uid (!user) was changed by user #@by_uid (!by_user) (@ip) at @timestamp.',
  295. array(
  296. '@uid' => $users['changed']['uid'],
  297. '!user' => $users['changed']['link'],
  298. '@by_uid' => $users['by']['uid'],
  299. '!by_user' => $users['by']['link'],
  300. '@ip' => $params['meta_data']['ip'],
  301. '@timestamp' => format_date($params['meta_data']['timestamp']),
  302. )
  303. );
  304. $message['body'][] = t('The following changes were made to the account:');
  305. $event_list = '<ul>';
  306. foreach ($events as $event) {
  307. $event_list .= '<li>' . account_sentinel_get_event_message($event['type'], $event['data']) . '</li>';
  308. }
  309. $event_list .= '</ul>';
  310. $message['body'][] = $event_list;
  311. $message['body'][] = '-- <br/>' . t(
  312. 'Sent by Account Sentinel on <a href="!site_url">@site_name</a>.',
  313. array(
  314. '@site_name' => variable_get('site_name'),
  315. '!site_url' => url('', array('absolute' => TRUE)),
  316. )
  317. );
  318. }
  319. break;
  320. }
  321. }
  322. /**
  323. * Returns the ID's of monitored roles.
  324. *
  325. * If the monitored roles have not been set yet, it will return the
  326. * administrator role.
  327. *
  328. * @return int[]
  329. * Array of monitored roles' ids.
  330. */
  331. function account_sentinel_get_monitored_roles() {
  332. $roles = variable_get('account_sentinel_monitored_roles', NULL);
  333. if ($roles === NULL) {
  334. return array(
  335. variable_get('user_admin_role', 3),
  336. );
  337. }
  338. return $roles;
  339. }
  340. /**
  341. * Returns the module's cron key.
  342. *
  343. * @return string
  344. * The cron key.
  345. */
  346. function account_sentinel_get_cron_key() {
  347. $key = variable_get('account_sentinel_cron_key', NULL);
  348. if ($key === NULL) {
  349. return account_sentinel_reset_cron_key();
  350. }
  351. return $key;
  352. }
  353. /**
  354. * Returns the relevant monitored data of an $account object.
  355. *
  356. * The output is an associative array which only stores data needed by Account
  357. * Sentinel.
  358. *
  359. * @param object $account
  360. * A user entity.
  361. *
  362. * @return array
  363. * An associative array containing the monitored data.
  364. */
  365. function account_sentinel_monitored_account_data($account) {
  366. // Only work with role IDs.
  367. $roles = array_keys($account->roles);
  368. // Only work with monitored roles.
  369. $monitored = account_sentinel_get_monitored_roles();
  370. $roles = array_intersect($monitored, $roles);
  371. $output = array(
  372. 'uid' => $account->uid,
  373. 'name' => $account->name,
  374. 'pass' => $account->pass,
  375. 'mail' => $account->mail,
  376. 'status' => $account->status,
  377. 'roles' => $roles,
  378. 'monitored' => !empty($roles),
  379. );
  380. return $output;
  381. }
  382. /**
  383. * Returns an array of event type string - human-readable string associations.
  384. *
  385. * @return array
  386. * Array of translatable strings mapped by their event type constants.
  387. */
  388. function account_sentinel_event_type_strings() {
  389. return array(
  390. ACCOUNT_SENTINEL_EVENT_TYPE_NAME => t('name changed'),
  391. ACCOUNT_SENTINEL_EVENT_TYPE_PASS => t('password changed'),
  392. ACCOUNT_SENTINEL_EVENT_TYPE_MAIL => t('mail changed'),
  393. ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD => t('role added'),
  394. ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE => t('role removed'),
  395. ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID => t('invalid snapshot'),
  396. ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING => t('missing snapshot'),
  397. ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD => t('user added'),
  398. ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE => t('user deleted'),
  399. ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK => t('blocked'),
  400. ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK => t('unblocked'),
  401. );
  402. }
  403. /**
  404. * Returns an array of event origin string - human-readable string associations.
  405. *
  406. * @return array
  407. * Array of translatable strings mapped by their event origin constants.
  408. */
  409. function account_sentinel_event_origin_strings() {
  410. return array(
  411. ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK => t('Drupal'),
  412. ACCOUNT_SENTINEL_EVENT_ORIGIN_DB_CHECK => t('database'),
  413. );
  414. }
  415. /**
  416. * Gets the human-readable name of a given event type.
  417. *
  418. * @param string $event_type
  419. * The event's type.
  420. *
  421. * @return string
  422. * The event type's human-readable name.
  423. */
  424. function account_sentinel_event_type_get_string($event_type) {
  425. $event_type_strings = account_sentinel_event_type_strings();
  426. if (isset($event_type_strings[$event_type])) {
  427. return $event_type_strings[$event_type];
  428. }
  429. return t('unknown');
  430. }
  431. /**
  432. * Gets the human-readable name of a given event origin.
  433. *
  434. * @param string $event_origin
  435. * The event's origin.
  436. *
  437. * @return string
  438. * The event origin's human-readable name.
  439. */
  440. function account_sentinel_event_origin_get_string($event_origin) {
  441. $event_origin_strings = account_sentinel_event_origin_strings();
  442. if (isset($event_origin_strings[$event_origin])) {
  443. return $event_origin_strings[$event_origin];
  444. }
  445. return t('unknown');
  446. }
  447. /**
  448. * Generates an event's detailed human-readable message.
  449. *
  450. * @param string $event_type
  451. * The event's type.
  452. * @param array $data
  453. * Additional data from the database used to generate informative messages.
  454. *
  455. * @return string
  456. * The generated detailed event message.
  457. */
  458. function account_sentinel_get_event_message($event_type, array $data) {
  459. switch ($event_type) {
  460. case ACCOUNT_SENTINEL_EVENT_TYPE_NAME:
  461. return t('Changed name from <strong>@name_old</strong> to <strong>@name_new</strong>.', array(
  462. '@name_old' => $data['old'],
  463. '@name_new' => $data['new'],
  464. ));
  465. case ACCOUNT_SENTINEL_EVENT_TYPE_PASS:
  466. $msg = t('Changed password.');
  467. // Append strength information if set.
  468. if (isset($data['strength'])) {
  469. $msg .= ' ' . t('New strength: @strength.', array(
  470. '@strength' => $data['strength'],
  471. ));
  472. }
  473. return $msg;
  474. case ACCOUNT_SENTINEL_EVENT_TYPE_MAIL:
  475. return t('Changed mail from <strong>@mail_old</strong> to <strong>@mail_new</strong>.', array(
  476. '@mail_old' => $data['old'],
  477. '@mail_new' => $data['new'],
  478. ));
  479. case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD:
  480. $role = user_role_load($data['rid']);
  481. return t('Granted role <strong>@role</strong>.', array(
  482. '@role' => $role->name,
  483. ));
  484. case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE:
  485. $role = user_role_load($data['rid']);
  486. return t('Revoked role <strong>@role</strong>.', array(
  487. '@role' => $role->name,
  488. ));
  489. case ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID:
  490. return t("The user's snapshot was altered.");
  491. case ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING:
  492. return t("The user's snapshot is missing.");
  493. case ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD:
  494. return t('Created user <em>#@uid</em> <strong>@name</strong> (<strong>@mail</strong>).', array(
  495. '@uid' => $data['uid'],
  496. '@name' => $data['name'],
  497. '@mail' => $data['mail'],
  498. ));
  499. case ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE:
  500. return t('Deleted user <em>#@uid</em> <strong>@name</strong> (<strong>@mail</strong>).', array(
  501. '@uid' => $data['uid'],
  502. '@name' => $data['name'],
  503. '@mail' => $data['mail'],
  504. ));
  505. case ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK:
  506. return t('Blocked user.');
  507. case ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK:
  508. return t('Unblocked user.');
  509. default:
  510. return t('Unknown event.');
  511. }
  512. }
  513. /**
  514. * Resets the cron key.
  515. *
  516. * @return string
  517. * Returns the new cron key.
  518. */
  519. function account_sentinel_reset_cron_key() {
  520. $new_key = drupal_random_key();
  521. variable_set('account_sentinel_cron_key', $new_key);
  522. watchdog('account_sentinel', 'Cron key reset.');
  523. return $new_key;
  524. }
  525. /**
  526. * Compares a user account's two states and returns the list of differences.
  527. *
  528. * @param array $new
  529. * The new state of the user.
  530. * @param array $original
  531. * The original state of the user.
  532. *
  533. * @return array
  534. * Array of changes.
  535. *
  536. * @see account_sentinel_monitored_account_data($account)
  537. */
  538. function account_sentinel_detect_changes(array $new, array $original) {
  539. $changes = array();
  540. // Check whether name was changed.
  541. if (isset($new['name']) && $new['name'] != $original['name']) {
  542. $changes[] = array(
  543. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_NAME,
  544. 'data' => array(
  545. 'old' => $original['name'],
  546. 'new' => $new['name'],
  547. ),
  548. );
  549. }
  550. // Check whether pass was changed.
  551. if (isset($new['pass']) && $new['pass'] != $original['pass']) {
  552. $changes[] = array(
  553. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_PASS,
  554. 'data' => array(),
  555. );
  556. }
  557. // Check whether mail was changed.
  558. if (isset($new['mail']) && $new['mail'] != $original['mail']) {
  559. $changes[] = array(
  560. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_MAIL,
  561. 'data' => array(
  562. 'old' => $original['mail'],
  563. 'new' => $new['mail'],
  564. ),
  565. );
  566. }
  567. // Check whether status was changed.
  568. if (isset($new['status']) && $new['status'] != $original['status']) {
  569. if ($original['status']) {
  570. $type = ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK;
  571. }
  572. else {
  573. $type = ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK;
  574. }
  575. $changes[] = array(
  576. 'type' => $type,
  577. 'data' => array(),
  578. );
  579. }
  580. if (isset($new['roles'])) {
  581. // Check whether roles were changed.
  582. $roles_added = array_diff($new['roles'], $original['roles']);
  583. $roles_removed = array_diff($original['roles'], $new['roles']);
  584. foreach ($roles_added as $rid) {
  585. $changes[] = array(
  586. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD,
  587. 'data' => array('rid' => $rid),
  588. );
  589. }
  590. foreach ($roles_removed as $rid) {
  591. $changes[] = array(
  592. 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE,
  593. 'data' => array('rid' => $rid),
  594. );
  595. }
  596. }
  597. drupal_alter('account_sentinel_changes', $changes, $new, $original);
  598. return $changes;
  599. }
  600. /**
  601. * Records events.
  602. *
  603. * Stores account changes in the database, sets the new snapshot, sends an email
  604. * notification and invokes hook_account_sentinel_change().
  605. *
  606. * @param int $uid
  607. * The UID of the account.
  608. * @param string $origin
  609. * The origin of the event.
  610. * @param array $events
  611. * The array of changes.
  612. */
  613. function account_sentinel_record_events($uid, $origin, array $events) {
  614. global $user;
  615. $by_uid = ($origin == ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK) ? $user->uid : 0;
  616. $meta_data = array(
  617. 'uid' => $uid,
  618. 'origin' => $origin,
  619. 'by_uid' => $by_uid,
  620. 'ip' => ip_address(),
  621. 'timestamp' => REQUEST_TIME,
  622. );
  623. // Store changes.
  624. foreach ($events as $event_key => $event) {
  625. $record = $meta_data;
  626. $record['type'] = $event['type'];
  627. $record['data'] = $event['data'];
  628. // Log to database.
  629. drupal_write_record('account_sentinel_logs', $record);
  630. // Inform other modules.
  631. module_invoke_all('account_sentinel_change', $record);
  632. }
  633. // Send one e-mail notification per account.
  634. account_sentinel_send_notification($uid, $origin, $events, $meta_data);
  635. }
  636. /**
  637. * Sends e-mail notification about events.
  638. *
  639. * @param int $uid
  640. * The UID of the account.
  641. * @param string $origin
  642. * The origin of the event.
  643. * @param array $events
  644. * The array of changes.
  645. * @param array $meta_data
  646. * Additional meta data about the change events.
  647. */
  648. function account_sentinel_send_notification($uid, $origin, array $events, array $meta_data) {
  649. if (!empty($events)) {
  650. $to = variable_get('account_sentinel_email_to', '');
  651. if ($to != '') {
  652. drupal_mail(
  653. 'account_sentinel',
  654. 'notification',
  655. $to,
  656. language_default(),
  657. array(
  658. 'uid' => $uid,
  659. 'origin' => $origin,
  660. 'events' => $events,
  661. 'meta_data' => $meta_data,
  662. )
  663. );
  664. }
  665. }
  666. }
  667. /**
  668. * Rebuilds the snapshot tables.
  669. *
  670. * Invoked after changing which roles are monitored.
  671. *
  672. * @param array $roles_old
  673. * Array of previously monitored role IDs.
  674. * @param array $roles_new
  675. * Array of monitored role IDs.
  676. */
  677. function account_sentinel_rebuild_snapshots(array $roles_old, array $roles_new) {
  678. $roles_added = array_diff($roles_new, $roles_old);
  679. $roles_removed = array_diff($roles_old, $roles_new);
  680. $auth_in_old = array_search(DRUPAL_AUTHENTICATED_RID, $roles_old) !== FALSE;
  681. $auth_in_new = array_search(DRUPAL_AUTHENTICATED_RID, $roles_new) !== FALSE;
  682. $hash_key = drupal_get_hash_salt();
  683. // Check if we don't have to modify anything.
  684. if ($auth_in_old && $auth_in_new) {
  685. // Every user is in the database, and will remain there.
  686. return;
  687. }
  688. // Check if we have to create new user snapshots.
  689. if (!$auth_in_old && !empty($roles_added)) {
  690. // Select users' data.
  691. $select = db_select('users', 'u')
  692. ->fields('u', array('uid', 'name', 'pass', 'mail', 'status'))
  693. ->condition('u.uid', '0', '<>')
  694. ->groupBy('u.uid');
  695. // Filter by users of added roles.
  696. if (!$auth_in_new) {
  697. $select_include = db_select('users_roles', 'ur')
  698. ->fields('ur', array('uid'))
  699. ->condition('rid', $roles_added);
  700. $select->condition('uid', $select_include, 'IN');
  701. }
  702. // Exclude users of previous roles.
  703. if (!empty($roles_old)) {
  704. $select_exclude = db_select('users_roles', 'ur')
  705. ->fields('ur', array('uid'))
  706. ->condition('rid', $roles_old);
  707. $select->condition('uid', $select_exclude, 'NOT IN');
  708. }
  709. // Add checksum.
  710. $select->addExpression(
  711. 'sha2(concat(u.uid, u.name, u.pass, u.mail, u.status, :hash_key), 384)', 'checksum',
  712. array(':hash_key' => $hash_key)
  713. );
  714. // Insert.
  715. $insert = db_insert('account_sentinel_users')
  716. ->fields(array('uid', 'name', 'pass', 'mail', 'status', 'checksum'))
  717. ->from($select);
  718. $insert->execute();
  719. }
  720. // Check if we have to remove users snapshots.
  721. if (!$auth_in_new && !empty($roles_removed)) {
  722. // Delete records.
  723. $delete = db_delete('account_sentinel_users');
  724. // Exclude users of monitored roles.
  725. if (!empty($roles_new)) {
  726. $delete_exclude = db_select('users_roles', 'ur')
  727. ->fields('ur', array('uid'))
  728. ->condition('rid', $roles_new);
  729. $delete->condition('uid', $delete_exclude, 'NOT IN');
  730. }
  731. // Execute query.
  732. $delete->execute();
  733. }
  734. // Check if we have to create new users_roles snapshots.
  735. $roles_added = array_diff($roles_added, array(DRUPAL_AUTHENTICATED_RID));
  736. if (!empty($roles_added)) {
  737. // Select added roles' records.
  738. $select = db_select('users_roles', 'ur');
  739. $select->fields('ur', array('uid', 'rid'))
  740. ->condition('rid', $roles_added);
  741. // Add checksum.
  742. $select->addExpression(
  743. 'sha2(concat(ur.uid, ur.rid, :hash_key), 384)', 'checksum',
  744. array(':hash_key' => $hash_key)
  745. );
  746. // Insert.
  747. $insert = db_insert('account_sentinel_users_roles')
  748. ->fields(array('uid', 'rid', 'checksum'))
  749. ->from($select);
  750. $insert->execute();
  751. }
  752. // Check if we have to remove users_roles snapshots.
  753. $roles_removed = array_diff($roles_removed, array(DRUPAL_AUTHENTICATED_RID));
  754. if (!empty($roles_removed)) {
  755. // Delete removed roles' records.
  756. $delete = db_delete('account_sentinel_users_roles');
  757. $delete->condition('rid', $roles_removed);
  758. $delete->execute();
  759. }
  760. }
  761. /**
  762. * Updates a modified user's snapshot.
  763. *
  764. * @param array $account
  765. * Account details.
  766. *
  767. * @see account_sentinel_monitored_account_data($account)
  768. */
  769. function account_sentinel_update_snapshot(array $account) {
  770. $uid = $account['uid'];
  771. account_sentinel_delete_snapshot($uid);
  772. account_sentinel_create_snapshot($account);
  773. }
  774. /**
  775. * Deletes a user's snapshots from the database.
  776. *
  777. * @param int $uid
  778. * UID of user.
  779. */
  780. function account_sentinel_delete_snapshot($uid) {
  781. // Reset user's snapshot.
  782. db_delete('account_sentinel_users')
  783. ->condition('uid', $uid)
  784. ->execute();
  785. // Reset users_roles snapshots.
  786. db_delete('account_sentinel_users_roles')
  787. ->condition('uid', $uid)
  788. ->execute();
  789. }
  790. /**
  791. * Creates a snapshot of the given account's state in the database.
  792. *
  793. * @param array $account
  794. * The account.
  795. *
  796. * @see account_sentinel_monitored_account_data($account)
  797. */
  798. function account_sentinel_create_snapshot(array $account) {
  799. $uid = $account['uid'];
  800. $hash_key = drupal_get_hash_salt();
  801. // Update account_sentinel_users.
  802. if ($account['monitored']) {
  803. $checksum = hash('sha384', $uid . $account['name'] . $account['pass'] . $account['mail'] . $account['status'] . $hash_key);
  804. db_insert('account_sentinel_users')
  805. ->fields(array(
  806. 'uid' => $uid,
  807. 'name' => $account['name'],
  808. 'pass' => $account['pass'],
  809. 'mail' => $account['mail'],
  810. 'status' => $account['status'],
  811. 'checksum' => $checksum,
  812. ))
  813. ->execute();
  814. }
  815. // Fill users_roles snapshots.
  816. foreach ($account['roles'] as $rid) {
  817. $checksum = hash('sha384', $uid . $rid . $hash_key);
  818. db_insert('account_sentinel_users_roles')
  819. ->fields(array(
  820. 'uid' => $uid,
  821. 'rid' => $rid,
  822. 'checksum' => $checksum,
  823. ))
  824. ->execute();
  825. }
  826. }