' . $help . '
';
}
}
/**
* 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'][] = '' . t('Warning: these changes were made outside of Drupal!') . '';
}
$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 = '';
foreach ($events as $event) {
$event_list .= '- ' . account_sentinel_get_event_message($event['type'], $event['data']) . '
';
}
$event_list .= '
';
$message['body'][] = $event_list;
$message['body'][] = '--
' . t(
'Sent by Account Sentinel on @site_name.',
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 @name_old to @name_new.', 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 @mail_old to @mail_new.', 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 @role.', array(
'@role' => $role->name,
));
case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE:
$role = user_role_load($data['rid']);
return t('Revoked role @role.', 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 #@uid @name (@mail).', array(
'@uid' => $data['uid'],
'@name' => $data['name'],
'@mail' => $data['mail'],
));
case ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE:
return t('Deleted user #@uid @name (@mail).', 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();
}
}