|
@@ -0,0 +1,922 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+/**
|
|
|
+ * @file
|
|
|
+ * Detects changes made to selected roles' accounts.
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the username was changed.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_NAME', 'name');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the password was changed.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_PASS', 'pass');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the e-mail address was changed.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_MAIL', 'mail');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that a monitored role was added.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD', 'role_add');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that a monitored role was revoked.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE', 'role_remove');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the snapshot of the user was not valid.
|
|
|
+ *
|
|
|
+ * This is a critical event.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID', 'snapshot_invalid');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the snapshot of the user should already exist but it does not.
|
|
|
+ *
|
|
|
+ * This is a critical event.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING', 'snapshot_missing');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that a new user was created with one or more monitored permissions.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD', 'user_add');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the user was deleted.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE', 'user_delete');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the user got blocked.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK', 'user_block');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the user got unblocked.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK', 'user_unblock');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the change was caught via hooks inside Drupal.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK', 'drupal');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Indicates that the change was caught by checking the database.
|
|
|
+ *
|
|
|
+ * This is a critical event.
|
|
|
+ */
|
|
|
+define('ACCOUNT_SENTINEL_EVENT_ORIGIN_DB_CHECK', 'database');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_help().
|
|
|
+ */
|
|
|
+function account_sentinel_help($path, $arg) {
|
|
|
+ switch ($path) {
|
|
|
+ case 'admin/help#account_sentinel':
|
|
|
+ $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.");
|
|
|
+ return '<p>' . $help . '</p>';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_permission().
|
|
|
+ */
|
|
|
+function account_sentinel_permission() {
|
|
|
+ return array(
|
|
|
+ 'access account sentinel logs' => array(
|
|
|
+ 'title' => t('Access Account Sentinel logs'),
|
|
|
+ ),
|
|
|
+ 'administer account sentinel' => array(
|
|
|
+ 'title' => t("Change Account Sentinel's configuration"),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_menu().
|
|
|
+ */
|
|
|
+function account_sentinel_menu() {
|
|
|
+ // Report page.
|
|
|
+ $items['admin/reports/account-sentinel'] = array(
|
|
|
+ 'title' => 'Account Sentinel log',
|
|
|
+ 'description' => 'List of changes to monitored roles\' accounts perceived by Account Sentinel.',
|
|
|
+ 'page callback' => 'account_sentinel_page_report',
|
|
|
+ 'access arguments' => array('access account sentinel logs'),
|
|
|
+ 'file' => 'account_sentinel.pages.inc',
|
|
|
+ 'type' => MENU_NORMAL_ITEM,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Settings page.
|
|
|
+ $items['admin/config/system/account-sentinel'] = array(
|
|
|
+ 'title' => 'Account Sentinel settings',
|
|
|
+ 'description' => 'Manage Account Sentinel settings.',
|
|
|
+ 'page callback' => 'drupal_get_form',
|
|
|
+ 'page arguments' => array('account_sentinel_page_settings'),
|
|
|
+ 'access arguments' => array('administer account sentinel'),
|
|
|
+ 'file' => 'account_sentinel.pages.inc',
|
|
|
+ );
|
|
|
+
|
|
|
+ // Cron handler.
|
|
|
+ $items['system/account-sentinel-cron'] = array(
|
|
|
+ 'title' => 'Run Account Sentinel DB check',
|
|
|
+ 'page callback' => 'account_sentinel_callback_cron',
|
|
|
+ 'access callback' => TRUE,
|
|
|
+ 'file' => 'account_sentinel.pages.inc',
|
|
|
+ 'type' => MENU_CALLBACK,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Cron key reset handler.
|
|
|
+ $items['system/account-sentinel-reset-cron-key'] = array(
|
|
|
+ 'title' => "Reset Account Sentinel's cron key",
|
|
|
+ 'page callback' => 'account_sentinel_callback_reset_cron_key',
|
|
|
+ 'access callback' => TRUE,
|
|
|
+ 'access arguments' => array('administer account sentinel'),
|
|
|
+ 'file' => 'account_sentinel.pages.inc',
|
|
|
+ 'type' => MENU_CALLBACK,
|
|
|
+ );
|
|
|
+
|
|
|
+ return $items;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_theme().
|
|
|
+ */
|
|
|
+function account_sentinel_theme($existing, $type, $theme, $path) {
|
|
|
+ return array(
|
|
|
+ 'account_sentinel_username' => array(
|
|
|
+ 'variables' => array(
|
|
|
+ 'uid' => 0,
|
|
|
+ ),
|
|
|
+ 'file' => 'account_sentinel.themes.inc',
|
|
|
+ ),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_user_update().
|
|
|
+ */
|
|
|
+function account_sentinel_user_update(&$edit, $account, $category) {
|
|
|
+ $new = account_sentinel_monitored_account_data($account);
|
|
|
+ $original = account_sentinel_monitored_account_data($account->original);
|
|
|
+
|
|
|
+ if ($new['monitored'] || $original['monitored']) {
|
|
|
+ $changes = account_sentinel_detect_changes($new, $original);
|
|
|
+ if (!empty($changes)) {
|
|
|
+ account_sentinel_record_events($account->uid, ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $changes);
|
|
|
+ account_sentinel_update_snapshot($new);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_password_strength_change().
|
|
|
+ */
|
|
|
+function account_sentinel_password_strength_change($account, $strength) {
|
|
|
+ // Store password strength for $account for later use.
|
|
|
+ account_sentinel_password_strength($account->uid, password_strength_get_score($strength['score']));
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Provides static storage for password strengths.
|
|
|
+ *
|
|
|
+ * @param int $uid
|
|
|
+ * UID of the user.
|
|
|
+ * @param string $score
|
|
|
+ * Password's strength.
|
|
|
+ *
|
|
|
+ * @return mixed
|
|
|
+ * Returns the score for UID's new password or FALSE if not set.
|
|
|
+ */
|
|
|
+function account_sentinel_password_strength($uid, $score = NULL) {
|
|
|
+ static $strengths = array();
|
|
|
+
|
|
|
+ // Update score.
|
|
|
+ if (isset($score)) {
|
|
|
+ $strengths[$uid] = $score;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Return score.
|
|
|
+ if (isset($strengths[$uid])) {
|
|
|
+ return $strengths[$uid];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Return FALSE if score is not available.
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_account_sentinel_changes_alter().
|
|
|
+ */
|
|
|
+function account_sentinel_account_sentinel_changes_alter(array &$changes, array &$new, array &$original) {
|
|
|
+ // Include password strength information if available.
|
|
|
+ $strength = account_sentinel_password_strength($original['uid']);
|
|
|
+ if ($strength) {
|
|
|
+ foreach ($changes as &$change) {
|
|
|
+ if ($change['type'] == ACCOUNT_SENTINEL_EVENT_TYPE_PASS && !isset($change['data']['strength'])) {
|
|
|
+ $change['data']['strength'] = $strength;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_user_insert().
|
|
|
+ */
|
|
|
+function account_sentinel_user_insert(&$edit, $account, $category) {
|
|
|
+ $account = account_sentinel_monitored_account_data($account);
|
|
|
+ if ($account['monitored']) {
|
|
|
+ $events[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD,
|
|
|
+ 'data' => array(
|
|
|
+ 'uid' => $account['uid'],
|
|
|
+ 'name' => $account['name'],
|
|
|
+ 'mail' => $account['mail'],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ foreach ($account['roles'] as $rid) {
|
|
|
+ $events[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD,
|
|
|
+ 'data' => array('rid' => $rid),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ account_sentinel_record_events($account['uid'], ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $events);
|
|
|
+ account_sentinel_update_snapshot($account);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_user_delete().
|
|
|
+ */
|
|
|
+function account_sentinel_user_delete($account) {
|
|
|
+ $account = account_sentinel_monitored_account_data($account);
|
|
|
+ if ($account['monitored']) {
|
|
|
+ $events[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE,
|
|
|
+ 'data' => array(
|
|
|
+ 'uid' => $account['uid'],
|
|
|
+ 'name' => $account['name'],
|
|
|
+ 'mail' => $account['mail'],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ account_sentinel_record_events($account['uid'], ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $events);
|
|
|
+ account_sentinel_delete_snapshot($account['uid']);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_cron().
|
|
|
+ */
|
|
|
+function account_sentinel_cron() {
|
|
|
+ if (variable_get('account_sentinel_cron_method', 'drupal') == 'drupal') {
|
|
|
+ watchdog('account_sentinel', "Invoked database audit from Drupal's cron.");
|
|
|
+ module_load_include('inc', 'account_sentinel', 'account_sentinel.audit');
|
|
|
+ account_sentinel_audit();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements hook_mail().
|
|
|
+ */
|
|
|
+function account_sentinel_mail($key, &$message, $params) {
|
|
|
+ switch ($key) {
|
|
|
+ // Compose notification e-mail.
|
|
|
+ case 'notification':
|
|
|
+ // Collect information parameters.
|
|
|
+ $origin = $params['origin'];
|
|
|
+ $events = $params['events'];
|
|
|
+
|
|
|
+ // Collect users' data.
|
|
|
+ $users = array(
|
|
|
+ 'changed' => array('uid' => $params['uid']),
|
|
|
+ 'by' => array('uid' => $params['meta_data']['by_uid']),
|
|
|
+ );
|
|
|
+ foreach ($users as &$user) {
|
|
|
+ $uid = $user['uid'];
|
|
|
+ $user_object = user_load($uid);
|
|
|
+ if ($user_object !== FALSE) {
|
|
|
+ $user['name'] = $user_object->name;
|
|
|
+ $user['link'] = l($user_object->name, 'user/' . $uid, array(
|
|
|
+ 'absolute' => TRUE,
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ $user['name'] = t('Unknown');
|
|
|
+ $user['link'] = $user['name'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Compose e-mail.
|
|
|
+ if (!empty($events)) {
|
|
|
+ $message['subject'] = t(
|
|
|
+ '[AS] User #@uid (@name) was changed',
|
|
|
+ array(
|
|
|
+ '@uid' => $users['changed']['uid'],
|
|
|
+ '@name' => $users['changed']['name'],
|
|
|
+ )
|
|
|
+ );
|
|
|
+ if ($origin != ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK) {
|
|
|
+ $message['body'][] = '<strong>' . t('Warning: these changes were made outside of Drupal!') . '</strong>';
|
|
|
+ }
|
|
|
+ $message['body'][] = t(
|
|
|
+ 'User #@uid (!user) was changed by user #@by_uid (!by_user) (@ip) at @timestamp.',
|
|
|
+ array(
|
|
|
+ '@uid' => $users['changed']['uid'],
|
|
|
+ '!user' => $users['changed']['link'],
|
|
|
+ '@by_uid' => $users['by']['uid'],
|
|
|
+ '!by_user' => $users['by']['link'],
|
|
|
+ '@ip' => $params['meta_data']['ip'],
|
|
|
+ '@timestamp' => format_date($params['meta_data']['timestamp']),
|
|
|
+ )
|
|
|
+ );
|
|
|
+ $message['body'][] = t('The following changes were made to the account:');
|
|
|
+ $event_list = '<ul>';
|
|
|
+ foreach ($events as $event) {
|
|
|
+ $event_list .= '<li>' . account_sentinel_get_event_message($event['type'], $event['data']) . '</li>';
|
|
|
+ }
|
|
|
+ $event_list .= '</ul>';
|
|
|
+ $message['body'][] = $event_list;
|
|
|
+ $message['body'][] = '-- <br/>' . t(
|
|
|
+ 'Sent by Account Sentinel on <a href="!site_url">@site_name</a>.',
|
|
|
+ array(
|
|
|
+ '@site_name' => variable_get('site_name'),
|
|
|
+ '!site_url' => url('', array('absolute' => TRUE)),
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Returns the ID's of monitored roles.
|
|
|
+ *
|
|
|
+ * If the monitored roles have not been set yet, it will return the
|
|
|
+ * administrator role.
|
|
|
+ *
|
|
|
+ * @return int[]
|
|
|
+ * Array of monitored roles' ids.
|
|
|
+ */
|
|
|
+function account_sentinel_get_monitored_roles() {
|
|
|
+ $roles = variable_get('account_sentinel_monitored_roles', NULL);
|
|
|
+ if ($roles === NULL) {
|
|
|
+ return array(
|
|
|
+ variable_get('user_admin_role', 3),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return $roles;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Returns the module's cron key.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * The cron key.
|
|
|
+ */
|
|
|
+function account_sentinel_get_cron_key() {
|
|
|
+ $key = variable_get('account_sentinel_cron_key', NULL);
|
|
|
+ if ($key === NULL) {
|
|
|
+ return account_sentinel_reset_cron_key();
|
|
|
+ }
|
|
|
+ return $key;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Returns the relevant monitored data of an $account object.
|
|
|
+ *
|
|
|
+ * The output is an associative array which only stores data needed by Account
|
|
|
+ * Sentinel.
|
|
|
+ *
|
|
|
+ * @param object $account
|
|
|
+ * A user entity.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * An associative array containing the monitored data.
|
|
|
+ */
|
|
|
+function account_sentinel_monitored_account_data($account) {
|
|
|
+ // Only work with role IDs.
|
|
|
+ $roles = array_keys($account->roles);
|
|
|
+
|
|
|
+ // Only work with monitored roles.
|
|
|
+ $monitored = account_sentinel_get_monitored_roles();
|
|
|
+ $roles = array_intersect($monitored, $roles);
|
|
|
+
|
|
|
+ $output = array(
|
|
|
+ 'uid' => $account->uid,
|
|
|
+ 'name' => $account->name,
|
|
|
+ 'pass' => $account->pass,
|
|
|
+ 'mail' => $account->mail,
|
|
|
+ 'status' => $account->status,
|
|
|
+ 'roles' => $roles,
|
|
|
+ 'monitored' => !empty($roles),
|
|
|
+ );
|
|
|
+
|
|
|
+ return $output;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Returns an array of event type string - human-readable string associations.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of translatable strings mapped by their event type constants.
|
|
|
+ */
|
|
|
+function account_sentinel_event_type_strings() {
|
|
|
+ return array(
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_NAME => t('name changed'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_PASS => t('password changed'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_MAIL => t('mail changed'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD => t('role added'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE => t('role removed'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID => t('invalid snapshot'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING => t('missing snapshot'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD => t('user added'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE => t('user deleted'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK => t('blocked'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK => t('unblocked'),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Returns an array of event origin string - human-readable string associations.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of translatable strings mapped by their event origin constants.
|
|
|
+ */
|
|
|
+function account_sentinel_event_origin_strings() {
|
|
|
+ return array(
|
|
|
+ ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK => t('Drupal'),
|
|
|
+ ACCOUNT_SENTINEL_EVENT_ORIGIN_DB_CHECK => t('database'),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Gets the human-readable name of a given event type.
|
|
|
+ *
|
|
|
+ * @param string $event_type
|
|
|
+ * The event's type.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * The event type's human-readable name.
|
|
|
+ */
|
|
|
+function account_sentinel_event_type_get_string($event_type) {
|
|
|
+ $event_type_strings = account_sentinel_event_type_strings();
|
|
|
+ if (isset($event_type_strings[$event_type])) {
|
|
|
+ return $event_type_strings[$event_type];
|
|
|
+ }
|
|
|
+ return t('unknown');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Gets the human-readable name of a given event origin.
|
|
|
+ *
|
|
|
+ * @param string $event_origin
|
|
|
+ * The event's origin.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * The event origin's human-readable name.
|
|
|
+ */
|
|
|
+function account_sentinel_event_origin_get_string($event_origin) {
|
|
|
+ $event_origin_strings = account_sentinel_event_origin_strings();
|
|
|
+ if (isset($event_origin_strings[$event_origin])) {
|
|
|
+ return $event_origin_strings[$event_origin];
|
|
|
+ }
|
|
|
+ return t('unknown');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Generates an event's detailed human-readable message.
|
|
|
+ *
|
|
|
+ * @param string $event_type
|
|
|
+ * The event's type.
|
|
|
+ * @param array $data
|
|
|
+ * Additional data from the database used to generate informative messages.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * The generated detailed event message.
|
|
|
+ */
|
|
|
+function account_sentinel_get_event_message($event_type, array $data) {
|
|
|
+ switch ($event_type) {
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_NAME:
|
|
|
+ return t('Changed name from <strong>@name_old</strong> to <strong>@name_new</strong>.', array(
|
|
|
+ '@name_old' => $data['old'],
|
|
|
+ '@name_new' => $data['new'],
|
|
|
+ ));
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_PASS:
|
|
|
+ $msg = t('Changed password.');
|
|
|
+ // Append strength information if set.
|
|
|
+ if (isset($data['strength'])) {
|
|
|
+ $msg .= ' ' . t('New strength: @strength.', array(
|
|
|
+ '@strength' => $data['strength'],
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ return $msg;
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_MAIL:
|
|
|
+ return t('Changed mail from <strong>@mail_old</strong> to <strong>@mail_new</strong>.', array(
|
|
|
+ '@mail_old' => $data['old'],
|
|
|
+ '@mail_new' => $data['new'],
|
|
|
+ ));
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD:
|
|
|
+ $role = user_role_load($data['rid']);
|
|
|
+ return t('Granted role <strong>@role</strong>.', array(
|
|
|
+ '@role' => $role->name,
|
|
|
+ ));
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE:
|
|
|
+ $role = user_role_load($data['rid']);
|
|
|
+ return t('Revoked role <strong>@role</strong>.', array(
|
|
|
+ '@role' => $role->name,
|
|
|
+ ));
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID:
|
|
|
+ return t("The user's snapshot was altered.");
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING:
|
|
|
+ return t("The user's snapshot is missing.");
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD:
|
|
|
+ return t('Created user <em>#@uid</em> <strong>@name</strong> (<strong>@mail</strong>).', array(
|
|
|
+ '@uid' => $data['uid'],
|
|
|
+ '@name' => $data['name'],
|
|
|
+ '@mail' => $data['mail'],
|
|
|
+ ));
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE:
|
|
|
+ return t('Deleted user <em>#@uid</em> <strong>@name</strong> (<strong>@mail</strong>).', array(
|
|
|
+ '@uid' => $data['uid'],
|
|
|
+ '@name' => $data['name'],
|
|
|
+ '@mail' => $data['mail'],
|
|
|
+ ));
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK:
|
|
|
+ return t('Blocked user.');
|
|
|
+
|
|
|
+ case ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK:
|
|
|
+ return t('Unblocked user.');
|
|
|
+
|
|
|
+ default:
|
|
|
+ return t('Unknown event.');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Resets the cron key.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * Returns the new cron key.
|
|
|
+ */
|
|
|
+function account_sentinel_reset_cron_key() {
|
|
|
+ $new_key = drupal_random_key();
|
|
|
+ variable_set('account_sentinel_cron_key', $new_key);
|
|
|
+ watchdog('account_sentinel', 'Cron key reset.');
|
|
|
+ return $new_key;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Compares a user account's two states and returns the list of differences.
|
|
|
+ *
|
|
|
+ * @param array $new
|
|
|
+ * The new state of the user.
|
|
|
+ * @param array $original
|
|
|
+ * The original state of the user.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of changes.
|
|
|
+ *
|
|
|
+ * @see account_sentinel_monitored_account_data($account)
|
|
|
+ */
|
|
|
+function account_sentinel_detect_changes(array $new, array $original) {
|
|
|
+ $changes = array();
|
|
|
+
|
|
|
+ // Check whether name was changed.
|
|
|
+ if (isset($new['name']) && $new['name'] != $original['name']) {
|
|
|
+ $changes[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_NAME,
|
|
|
+ 'data' => array(
|
|
|
+ 'old' => $original['name'],
|
|
|
+ 'new' => $new['name'],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check whether pass was changed.
|
|
|
+ if (isset($new['pass']) && $new['pass'] != $original['pass']) {
|
|
|
+ $changes[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_PASS,
|
|
|
+ 'data' => array(),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check whether mail was changed.
|
|
|
+ if (isset($new['mail']) && $new['mail'] != $original['mail']) {
|
|
|
+ $changes[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_MAIL,
|
|
|
+ 'data' => array(
|
|
|
+ 'old' => $original['mail'],
|
|
|
+ 'new' => $new['mail'],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check whether status was changed.
|
|
|
+ if (isset($new['status']) && $new['status'] != $original['status']) {
|
|
|
+ if ($original['status']) {
|
|
|
+ $type = ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ $type = ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK;
|
|
|
+ }
|
|
|
+ $changes[] = array(
|
|
|
+ 'type' => $type,
|
|
|
+ 'data' => array(),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($new['roles'])) {
|
|
|
+ // Check whether roles were changed.
|
|
|
+ $roles_added = array_diff($new['roles'], $original['roles']);
|
|
|
+ $roles_removed = array_diff($original['roles'], $new['roles']);
|
|
|
+
|
|
|
+ foreach ($roles_added as $rid) {
|
|
|
+ $changes[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD,
|
|
|
+ 'data' => array('rid' => $rid),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($roles_removed as $rid) {
|
|
|
+ $changes[] = array(
|
|
|
+ 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE,
|
|
|
+ 'data' => array('rid' => $rid),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ drupal_alter('account_sentinel_changes', $changes, $new, $original);
|
|
|
+
|
|
|
+ return $changes;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Records events.
|
|
|
+ *
|
|
|
+ * Stores account changes in the database, sets the new snapshot, sends an email
|
|
|
+ * notification and invokes hook_account_sentinel_change().
|
|
|
+ *
|
|
|
+ * @param int $uid
|
|
|
+ * The UID of the account.
|
|
|
+ * @param string $origin
|
|
|
+ * The origin of the event.
|
|
|
+ * @param array $events
|
|
|
+ * The array of changes.
|
|
|
+ */
|
|
|
+function account_sentinel_record_events($uid, $origin, array $events) {
|
|
|
+ global $user;
|
|
|
+ $by_uid = ($origin == ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK) ? $user->uid : 0;
|
|
|
+
|
|
|
+ $meta_data = array(
|
|
|
+ 'uid' => $uid,
|
|
|
+ 'origin' => $origin,
|
|
|
+ 'by_uid' => $by_uid,
|
|
|
+ 'ip' => ip_address(),
|
|
|
+ 'timestamp' => REQUEST_TIME,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Store changes.
|
|
|
+ foreach ($events as $event_key => $event) {
|
|
|
+ $record = $meta_data;
|
|
|
+ $record['type'] = $event['type'];
|
|
|
+ $record['data'] = $event['data'];
|
|
|
+
|
|
|
+ // Log to database.
|
|
|
+ drupal_write_record('account_sentinel_logs', $record);
|
|
|
+
|
|
|
+ // Inform other modules.
|
|
|
+ module_invoke_all('account_sentinel_change', $record);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Send one e-mail notification per account.
|
|
|
+ account_sentinel_send_notification($uid, $origin, $events, $meta_data);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Sends e-mail notification about events.
|
|
|
+ *
|
|
|
+ * @param int $uid
|
|
|
+ * The UID of the account.
|
|
|
+ * @param string $origin
|
|
|
+ * The origin of the event.
|
|
|
+ * @param array $events
|
|
|
+ * The array of changes.
|
|
|
+ * @param array $meta_data
|
|
|
+ * Additional meta data about the change events.
|
|
|
+ */
|
|
|
+function account_sentinel_send_notification($uid, $origin, array $events, array $meta_data) {
|
|
|
+ if (!empty($events)) {
|
|
|
+ $to = variable_get('account_sentinel_email_to', '');
|
|
|
+ if ($to != '') {
|
|
|
+ drupal_mail(
|
|
|
+ 'account_sentinel',
|
|
|
+ 'notification',
|
|
|
+ $to,
|
|
|
+ language_default(),
|
|
|
+ array(
|
|
|
+ 'uid' => $uid,
|
|
|
+ 'origin' => $origin,
|
|
|
+ 'events' => $events,
|
|
|
+ 'meta_data' => $meta_data,
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Rebuilds the snapshot tables.
|
|
|
+ *
|
|
|
+ * Invoked after changing which roles are monitored.
|
|
|
+ *
|
|
|
+ * @param array $roles_old
|
|
|
+ * Array of previously monitored role IDs.
|
|
|
+ * @param array $roles_new
|
|
|
+ * Array of monitored role IDs.
|
|
|
+ */
|
|
|
+function account_sentinel_rebuild_snapshots(array $roles_old, array $roles_new) {
|
|
|
+ $roles_added = array_diff($roles_new, $roles_old);
|
|
|
+ $roles_removed = array_diff($roles_old, $roles_new);
|
|
|
+ $auth_in_old = array_search(DRUPAL_AUTHENTICATED_RID, $roles_old) !== FALSE;
|
|
|
+ $auth_in_new = array_search(DRUPAL_AUTHENTICATED_RID, $roles_new) !== FALSE;
|
|
|
+ $hash_key = drupal_get_hash_salt();
|
|
|
+
|
|
|
+ // Check if we don't have to modify anything.
|
|
|
+ if ($auth_in_old && $auth_in_new) {
|
|
|
+ // Every user is in the database, and will remain there.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if we have to create new user snapshots.
|
|
|
+ if (!$auth_in_old && !empty($roles_added)) {
|
|
|
+ // Select users' data.
|
|
|
+ $select = db_select('users', 'u')
|
|
|
+ ->fields('u', array('uid', 'name', 'pass', 'mail', 'status'))
|
|
|
+ ->condition('u.uid', '0', '<>')
|
|
|
+ ->groupBy('u.uid');
|
|
|
+
|
|
|
+ // Filter by users of added roles.
|
|
|
+ if (!$auth_in_new) {
|
|
|
+ $select_include = db_select('users_roles', 'ur')
|
|
|
+ ->fields('ur', array('uid'))
|
|
|
+ ->condition('rid', $roles_added);
|
|
|
+ $select->condition('uid', $select_include, 'IN');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Exclude users of previous roles.
|
|
|
+ if (!empty($roles_old)) {
|
|
|
+ $select_exclude = db_select('users_roles', 'ur')
|
|
|
+ ->fields('ur', array('uid'))
|
|
|
+ ->condition('rid', $roles_old);
|
|
|
+ $select->condition('uid', $select_exclude, 'NOT IN');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add checksum.
|
|
|
+ $select->addExpression(
|
|
|
+ 'sha2(concat(u.uid, u.name, u.pass, u.mail, u.status, :hash_key), 384)', 'checksum',
|
|
|
+ array(':hash_key' => $hash_key)
|
|
|
+ );
|
|
|
+
|
|
|
+ // Insert.
|
|
|
+ $insert = db_insert('account_sentinel_users')
|
|
|
+ ->fields(array('uid', 'name', 'pass', 'mail', 'status', 'checksum'))
|
|
|
+ ->from($select);
|
|
|
+ $insert->execute();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if we have to remove users snapshots.
|
|
|
+ if (!$auth_in_new && !empty($roles_removed)) {
|
|
|
+ // Delete records.
|
|
|
+ $delete = db_delete('account_sentinel_users');
|
|
|
+
|
|
|
+ // Exclude users of monitored roles.
|
|
|
+ if (!empty($roles_new)) {
|
|
|
+ $delete_exclude = db_select('users_roles', 'ur')
|
|
|
+ ->fields('ur', array('uid'))
|
|
|
+ ->condition('rid', $roles_new);
|
|
|
+ $delete->condition('uid', $delete_exclude, 'NOT IN');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Execute query.
|
|
|
+ $delete->execute();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if we have to create new users_roles snapshots.
|
|
|
+ $roles_added = array_diff($roles_added, array(DRUPAL_AUTHENTICATED_RID));
|
|
|
+ if (!empty($roles_added)) {
|
|
|
+ // Select added roles' records.
|
|
|
+ $select = db_select('users_roles', 'ur');
|
|
|
+ $select->fields('ur', array('uid', 'rid'))
|
|
|
+ ->condition('rid', $roles_added);
|
|
|
+
|
|
|
+ // Add checksum.
|
|
|
+ $select->addExpression(
|
|
|
+ 'sha2(concat(ur.uid, ur.rid, :hash_key), 384)', 'checksum',
|
|
|
+ array(':hash_key' => $hash_key)
|
|
|
+ );
|
|
|
+
|
|
|
+ // Insert.
|
|
|
+ $insert = db_insert('account_sentinel_users_roles')
|
|
|
+ ->fields(array('uid', 'rid', 'checksum'))
|
|
|
+ ->from($select);
|
|
|
+ $insert->execute();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if we have to remove users_roles snapshots.
|
|
|
+ $roles_removed = array_diff($roles_removed, array(DRUPAL_AUTHENTICATED_RID));
|
|
|
+ if (!empty($roles_removed)) {
|
|
|
+ // Delete removed roles' records.
|
|
|
+ $delete = db_delete('account_sentinel_users_roles');
|
|
|
+ $delete->condition('rid', $roles_removed);
|
|
|
+ $delete->execute();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Updates a modified user's snapshot.
|
|
|
+ *
|
|
|
+ * @param array $account
|
|
|
+ * Account details.
|
|
|
+ *
|
|
|
+ * @see account_sentinel_monitored_account_data($account)
|
|
|
+ */
|
|
|
+function account_sentinel_update_snapshot(array $account) {
|
|
|
+ $uid = $account['uid'];
|
|
|
+ account_sentinel_delete_snapshot($uid);
|
|
|
+ account_sentinel_create_snapshot($account);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Deletes a user's snapshots from the database.
|
|
|
+ *
|
|
|
+ * @param int $uid
|
|
|
+ * UID of user.
|
|
|
+ */
|
|
|
+function account_sentinel_delete_snapshot($uid) {
|
|
|
+ // Reset user's snapshot.
|
|
|
+ db_delete('account_sentinel_users')
|
|
|
+ ->condition('uid', $uid)
|
|
|
+ ->execute();
|
|
|
+
|
|
|
+ // Reset users_roles snapshots.
|
|
|
+ db_delete('account_sentinel_users_roles')
|
|
|
+ ->condition('uid', $uid)
|
|
|
+ ->execute();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Creates a snapshot of the given account's state in the database.
|
|
|
+ *
|
|
|
+ * @param array $account
|
|
|
+ * The account.
|
|
|
+ *
|
|
|
+ * @see account_sentinel_monitored_account_data($account)
|
|
|
+ */
|
|
|
+function account_sentinel_create_snapshot(array $account) {
|
|
|
+ $uid = $account['uid'];
|
|
|
+ $hash_key = drupal_get_hash_salt();
|
|
|
+
|
|
|
+ // Update account_sentinel_users.
|
|
|
+ if ($account['monitored']) {
|
|
|
+ $checksum = hash('sha384', $uid . $account['name'] . $account['pass'] . $account['mail'] . $account['status'] . $hash_key);
|
|
|
+ db_insert('account_sentinel_users')
|
|
|
+ ->fields(array(
|
|
|
+ 'uid' => $uid,
|
|
|
+ 'name' => $account['name'],
|
|
|
+ 'pass' => $account['pass'],
|
|
|
+ 'mail' => $account['mail'],
|
|
|
+ 'status' => $account['status'],
|
|
|
+ 'checksum' => $checksum,
|
|
|
+ ))
|
|
|
+ ->execute();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fill users_roles snapshots.
|
|
|
+ foreach ($account['roles'] as $rid) {
|
|
|
+ $checksum = hash('sha384', $uid . $rid . $hash_key);
|
|
|
+ db_insert('account_sentinel_users_roles')
|
|
|
+ ->fields(array(
|
|
|
+ 'uid' => $uid,
|
|
|
+ 'rid' => $rid,
|
|
|
+ 'checksum' => $checksum,
|
|
|
+ ))
|
|
|
+ ->execute();
|
|
|
+ }
|
|
|
+}
|