'activity', 'title' => t('Activity'), 'callback' => 'activity_page', 'access' => user_access('view public activity'), 'weight' => 1, ); $items[] = array( 'path' => 'activity/delete', 'title' => t('Delete activity'), 'callback' => 'activity_delete', 'access' => user_access('administer activity'), 'type' => MENU_CALLBACK, ); $items[] = array( 'path' => 'activity/all', 'title' => t('All activity'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'access' => user_access('view public activity'), ); $items[] = array( 'path' => 'activity/mine', 'title' => t('My activity'), 'access' => $user->uid, 'type' => MENU_LOCAL_TASK, 'access' => user_access('view own activity'), ); $items[] = array( 'path' => 'activity/all/feed', 'title' => t('All activity RSS'), 'callback' => 'activity_feed', 'callback arguments' => array(ACTIVITY_ALL), 'type' => MENU_CALLBACK, 'access' => user_access('view public activity'), ); $items[] = array( 'path' => 'activity/all/json', 'title' => t('All activity JSON'), 'callback' => 'activity_json', 'callback arguments' => array(ACTIVITY_ALL, 1), 'type' => MENU_CALLBACK, 'access' => user_access('view public activity'), ); $items[] = array( 'path' => 'admin/settings/activity', 'title' => t('Activity Settings'), 'description' => t('Customize what will display on your users activity page.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('activity_admin_settings'), ); } else { if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'activity') { if ($activity_info = activity_get_info()) { foreach ($activity_info as $module => $info) { $module_nice_name = drupal_ucfirst(str_replace('_', ' ', substr($module, 0, -8))); $items[] = array( 'path' => 'admin/settings/activity/'. $module, 'title' => $module_nice_name .' '. t('Module Activity Settings'), 'description' => t('Customize what will display on your activity pages for the @module module', array('@module' => $module_nice_name)), 'callback' => 'drupal_get_form', 'callback arguments' => array('activity_module_settings', arg(3)), 'type' => MENU_CALLBACK, ); } } } if ($user->uid) { $items[] = array( 'path' => 'activity/'. $user->uid .'/feed', 'title' => t('My activity'), 'callback' => 'activity_feed', 'callback arguments' => array($user->uid), 'type' => MENU_CALLBACK, ); $items[] = array( 'path' => 'activity/'. $user->uid .'/json', 'callback' => 'activity_json', 'callback arguments' => array($user->uid, 1), 'type' => MENU_CALLBACK, ); } } return $items; } /** * Activity admin main settings page. */ function activity_admin_settings() { $form['general_settings'] = array( '#type' => 'fieldset', '#title' => t('General settings'), '#collapsible' => FALSE, ); $form['general_settings']['activity_page_pager'] = array( '#type' => 'select', '#title' => t('Activity Page Results per Page'), '#description' => t('Select the number of activity records to show on activity pages such as this one.', array('@activity' => url('activity'))), '#options' => drupal_map_assoc(array(5, 10, 20, 25, 30, 50)), '#default_value' => variable_get('activity_page_pager', 20), ); $form['general_settings']['activity_user_profile_records'] = array( '#type' => 'select', '#title' => t('Activity Results per User Profile'), '#description' => t('Select the number of activity records to show on a user\'s profile page. Selecting 0 will disable activity from showing on user profile pages.'), '#options' => drupal_map_assoc(array(0, 5, 10, 20, 25, 30, 50)), '#default_value' => variable_get('activity_user_profile_records', 5), ); $form['general_settings']['activity_time_limit'] = array( '#type' => 'select', '#title' => t('Activity time limiter'), '#description' => t("Allows you to set a time limit for recording activity so repeated actions don't flood your activity feed. If the same action is submitted within X seconds of the last activity record of the same type from the same user then the activity is not logged."), '#options' => drupal_map_assoc(array(5, 10, 20, 30, 60, 120, 300, 600, 1800), format_interval), '#default_value' => variable_get('activity_time_limit', 30), ); $form['general_settings']['activity_purge'] = array( '#type' => 'select', '#title' => t('Activity log purge'), '#description' => t("Allows you to set a time limit for storing activity records. Select 0 to keep all activity records."), '#options' => drupal_map_assoc(array(0, 3600, 7200, 14400, 21600, 43200, 86400, 604800, 1209600, 2419200, 7257600, 15724800, 31536000), format_interval), '#default_value' => variable_get('activity_purge', 0), ); $form['general_settings']['activity_user_optout'] = array( '#type' => 'checkbox', '#title' => t('Allow user privacy opt-out'), '#description' => t('If you wish to allows users to opt-out of Activity then check the box above. If a user opts-out then activity that the user originates will not be recorded.'), '#default_value' => variable_get('activity_user_optout', 0), ); if ($activity_info = activity_get_info()) { $form['module_settings'] = array( '#type' => 'fieldset', '#title' => t('Activity contrib modules'), '#description' => t('Click on the links below to configure the activity settings for the activity contrib modules you have installed.'), '#collapsible' => FALSE, ); foreach ($activity_info as $module => $info) { if (!empty($info)) { $module_nice_name = drupal_ucfirst(str_replace('_', ' ', substr($module, 0, -8))); $form['module_settings'][$module] = array( '#type' => 'markup', '#value' => '
'. l($module_nice_name .' activity settings page', 'admin/settings/activity/'. $module, array('title' => 'Configure '. $module_nice_name)) .'
', ); } } } else { drupal_set_message(t('No supported modules enabled. Check the !activity_section for supported modules.', array('!activity_section' => l(t('Activity section'), 'admin/build/modules')))); } return system_settings_form($form); } /** * Activity module specific admin settings page. */ function activity_module_settings($module) { drupal_add_js(drupal_get_path('module', 'activity') .'/activity_admin.js'); $module_nice_name = drupal_ucfirst(str_replace('_', ' ', substr($module, 0, -8))); $module_help = t('The configuration settings below determine how activity generated by the @module module is recorded. Select which types of activity and which operations to record by checking/unchecking the options for Token Types and Operation Types. You can customize the text that is displayed on an activity record by changing the text in the fields below for each combination of token type and operation type and role.
', array('@module' => $module_nice_name)); $form['help'] = array( '#type' => 'markup', '#value' => $module_help, ); $info = module_invoke($module, 'activity_info'); if (!empty($info)) { $tokens = array(); $token_list = array(); foreach (token_get_list($module) as $name => $token_array) { $token_list = array_merge($token_list, $token_array); } foreach (token_get_list('activity') as $name => $token_array) { $token_list = array_merge($token_list, $token_array); } ksort($token_list); foreach ($token_list as $token => $desc) { $tokens[] = '['. $token .']: '. $desc; } if (count($info['types'])) { $form['token_op_types'] = array( '#type' => 'fieldset', '#title' => t('Token and operation types'), '#collapsible' => TRUE, '#collapsed' => FALSE, ); $form['token_op_types'][$module .'_token_types'] = array( '#type' => 'checkboxes', '#title' => t('Token types'), '#description' => t('Select the token types that you wish to record activity from.'), '#options' => $info['types'], '#default_value' => variable_get($module .'_token_types', array_keys($info['types'])), '#attributes' => array('class' => 'activity-token-types'), ); $form['token_op_types'][$module .'_op_types'] = array( '#type' => 'checkboxes', '#title' => t('Operation types'), '#description' => t('Select the operation types that you wish to record activity from.'), '#options' => $info['ops'], '#default_value' => variable_get($module .'_op_types', array_keys($info['ops'])), '#attributes' => array('class' => 'activity-operation-types'), ); $form['token_settings'] = array( '#type' => 'fieldset', '#title' => t('Tokens available to @name activity', array('@name' => t($module_nice_name))), '#collapsible' => TRUE, '#collapsed' => TRUE, '#description' => t('Available tokens') . theme('item_list', $tokens), ); if ($ops = $info['ops']) { if ($roles = $info['roles']) { foreach ($roles as $role_name => $role) { $form[$module][$role_name] = array( '#type' => 'fieldset', '#title' => t('Messages visible to the "@role_name" role.', array('@role_name' => $role['#name'])), '#collapsible' => TRUE, '#collapsed' => FALSE, '#description' => $role['#description'] ? $role['#description'] : '', ); if ($types = $info['types']) { $token_types = variable_get($module .'_token_types', $info['types']); $op_types = variable_get($module .'_op_types', $info['ops']); foreach ($types as $type_name => $type) { if (count($types) > 1) { $form[$module][$role_name][$type_name] = array( '#type' => 'fieldset', '#title' => t($type), '#collapsible' => TRUE, '#collapsed' => TRUE, ); } foreach ($ops as $op_name => $op) { $token_field = "{$module}_{$type_name}_{$op_name}_{$role_name}"; $default = ''; if (!is_array($role['#default']) && $role['#default']) { $default = $role['#default']; } else if (is_array($role['#default']) && isset($role['#default'][$op_name])) { $default = $role['#default'][$op_name]; } if ($default) { $form[$module][$role_name][$type_name][$token_field] = array( '#type' => 'textfield', '#title' => $type .': '. $op, '#default_value' => variable_get($token_field, $default), ); } } } } } } } } } return system_settings_form($form); } /** * Implementation of hook_user(). */ function activity_user($op, &$edit, &$account, $category = NULL) { switch ($op) { // View activity on user profile page. case 'view': if (user_access('view public activity') && variable_get('activity_user_profile_records', 5) != 0) { $activity = activity_get_activity($account->uid, NULL, variable_get('activity_user_profile_records', 5)); $activities = array(); foreach ($activity as $item) { $activities[] = theme('activity', activity_token_replace($item), $item); } $items['activity'] = array( 'value' => theme('activity_user_profile_activity', $activities), ); return array(t('Activity') => $items); } break; // Provide a privacy setting to users to opt-out of appearing in activity pages. case 'form': if ($category == 'account' && variable_get('activity_user_optout', 0)) { $form['activity'] = array( '#type' => 'fieldset', '#title' => t('Activity privacy settings'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#weight' => 6, ); $form['activity']['activity_optout'] = array( '#type' => 'checkbox', '#title' => t('Do not record my site activity'), '#description' => t('If you wish keep your site activity from appearing in the activity pages, check the box.', array('@activity' => url('activity'))), '#default_value' => isset($edit['activity_optout']) ? $edit['activity_optout'] : '', ); } return $form; break; case 'delete': // Delete activity records directly generated by the deleted user db_query('DELETE FROM {activity_targets} WHERE aid IN (SELECT aid FROM {activity} WHERE uid = %d)', $account->uid); db_query('DELETE FROM {activity} WHERE uid = %d', $account->uid); // Delete activity records related to user but not generated by the deleted user db_query('DELETE FROM {activity} WHERE aid IN (SELECT aid FROM {activity_targets} WHERE target_uid = %d)', $account->uid); db_query('DELETE FROM {activity_targets} WHERE target_uid = %d', $account->uid); break; } } /** * Implementation of hook_cron(). */ function activity_cron() { // users set an interval on the module settings page $interval = variable_get('activity_purge', 0); if ($interval > 0) { $time = (time() - $interval); if (module_exists('activityhistory')) { db_query('DELETE FROM {activity_history} WHERE aid IN (SELECT aid FROM {activity} WHERE created < %d)', $time); } db_query('DELETE FROM {activity_targets} WHERE aid IN (SELECT aid FROM {activity} WHERE created < %d)', $time); db_query('DELETE FROM {activity} WHERE created < %d', $time); } } /** * API function * * @return * An array of module names and metadata from those modules that implement * hook_activity_info(). */ function activity_get_info() { foreach (module_implements('activity_info') as $module) { $info[$module] = module_invoke($module, 'activity_info'); } return $info; } /** * API function * * Insert an activity record. This gets called by modules wishing to record * their activities. * * @param $module * The name of the module that is doing the recording, eg. 'node'. * * @param $type * Module's can track more than one type of activity. For example, the * nodeactivity module tracks activities for each content type separately. * This should be an identifier for the calling module to use. */ function activity_insert($uid, $module, $type, $operation, $data, $target_users_roles) { // check time limit, ignore activity if within the limit $result = db_query("SELECT COUNT(*) FROM {activity} WHERE uid = %d AND module = '%s' AND type = '%s' AND operation = '%s' AND data = '%s' AND created >= %d", $uid, $module, $type, $operation, serialize($data), (time() - variable_get('activity_time_limit', 30))); if (db_fetch_object($result)->count != 0) { return FALSE; } $aid = db_next_id('activity'); db_query("INSERT INTO {activity} (aid, uid, module, type, operation, created, data) VALUES (%d, %d, '%s', '%s', '%s', %d, '%s')", $aid, $uid, $module, $type, $operation, time(), serialize($data)); foreach ($target_users_roles as $target_uid => $role) { db_query("INSERT INTO {activity_targets} (aid, target_uid, target_role) VALUES (%d, %d, '%s')", $aid, $target_uid, $role); } $activity = array( 'aid' => $aid, 'uid' => $uid, 'module' => $module, 'type' => $type, 'operation' => $operation, 'data' => $data, 'target_user_roles' => $target_users_roles, ); // Invoke activityapi activity_invoke_activityapi($activity, 'insert'); return $aid; } /** * Function to allow a single activity record to be deleted. * * @param $aid * Activity id. */ function activity_delete($aid = NULL) { if ($aid && is_numeric($aid)) { if (module_exists('activityhistory')) { db_query('DELETE FROM {activity_history} WHERE aid = %d', $aid); } db_query('DELETE FROM {activity_targets} WHERE aid = %d', $aid); db_query('DELETE FROM {activity} WHERE aid = %d', $aid); } drupal_goto(drupal_get_destination()); } /** * Check if user has right to delete a single activity record and if * so, return a the activity record delete link. */ function activity_delete_link($activity) { global $user; if (($user->uid != 0 && $activity['uid'] == $user->uid) || user_access('administer activity')) { return theme('activity_delete_link', $activity); } return NULL; } /** * API function. * * Retrieve activity from the database. * * @param $uids * - a single uid * - an array of uids * - can include the special uid ACTIVITY_ALL * * @param $filters * - an array where keys are one of module, type, operation, created, target_role * - values are arrays of possible values for the keys. * For example: * array('target_role' => 'Author', 'operation' => 'delete') * this would find activity where the author had deleted something. * Example 2: * array('target_role' => array('Requester', 'Requestee')) * This shows that the values can be arrays as well. * * @param $limit * The number of results desired * * @param $tablesort_headers * An array that determines the sorting of the result set. * * @todo This should be replaced with views integration. Perhaps in 6.x. */ function activity_get_activity($uids = ACTIVITY_ALL, $filters = NULL, $limit = NULL, $tablesort_headers = NULL) { $wheres = array(); // Build the WHERE clause for user id. if (!is_array($uids)) { $wheres[] = "activity_targets.target_uid = %d"; $params[] = $uids; } else { foreach ($uids as $uid) { $nums[] = "%d"; $params[] = $uid; } $wheres[] = 'activity_targets.target_uid IN ('. implode(',', $nums) .')'; } // Build sql limiting query to on filtered fields if (!empty($filters) && is_array($filters)) { $ops = array( 'include' => " = '%s'", 'exclude' => " != '%s'", 'lt' => ' < %d', 'gt' => ' > %d', 'lte' => ' <= %d', 'gte' => ' >= %d' ); foreach ($filters as $column => $filter) { // Of the possible columns, role is in the at table and all others in the // a table. Prefix the column name with the appropriate table. if ($column == 'target_role') { $column = 'activity_targets.target_role'; } else { $column = "activity.{$column}"; } // attempt to rewrite old filters to the new format if (!is_array($filter) || count(array_intersect(array_keys($ops), array_keys($filter))) == 0) { $filter = array('include' => $filter); } foreach ($filter as $criteria => $values) { if (is_array($values)) { $strings = array(); foreach ($values as $value) { $strings[] = "'%s'"; $params[] = $value; } $wheres[] = $column . ($criteria == 'exclude' ? ' NOT IN ' : ' IN ') .'('. implode(',', $strings). ')'; } else { $wheres[] = $column . $ops[$criteria]; // $values is a string with the single value. $params[] = $values; } } } } if (count($wheres) > 0) { $where = implode(' AND ', $wheres); $where = "WHERE $where"; } // We always include tablesort_sql in the query so that this API is friendly // to sortable tables. If no headers were passed in, use the default headers. if (empty($tablesort_headers)) { $tablesort_headers = activity_get_tablesort_headers(); $tablesort_headers['activity.created']['sort'] = 'desc'; } // Build the sql and do the query. Wrapping it in db_rewrite_sql allows other // modules to impose access restrictions on activity listings. $sql = "SELECT activity.*, activity_targets.target_uid, activity_targets.target_role FROM {activity_targets} activity_targets INNER JOIN {activity} activity ON activity.aid = activity_targets.aid $where "; $tablesort_sql = tablesort_sql($tablesort_headers); $sql = db_rewrite_sql("$sql $tablesort_sql", 'activity_targets', 'aid', array('uids' => $uids)); if (is_numeric($limit)) { $result = pager_query($sql, $limit, 0, NULL, $params); } else { $result = db_query($sql, $params); } $activity = array(); while ($row = db_fetch_array($result)) { $row['data'] = unserialize($row['data']); $row['data']['aid'] = $row['aid']; $row['data']['uid'] = $row['uid']; $row['data']['module'] = $row['module']; $row['data']['type'] = $row['type']; $row['data']['operation'] = ($row['data']['operation'] ? $row['data']['operation'] : $row['operation']); $row['data']['created'] = $row['created']; // Invoke activityapi activity_invoke_activityapi($row, 'load'); if (!empty($row)) { $activity[] = $row; } } return $activity; } /** * Invoke a hook_activityapi() operation in all modules. * * @param &$activity * An activity array. * * @param $op * A string containing the name of the nodeapi operation. * 'insert' is called when a new activity is created * 'load' is called when an activity is loaded * 'render' is called before token replacement begins * * @return * The returned value of the invoked hooks. */ function activity_invoke_activityapi(&$activity, $op) { $return = array(); foreach (module_implements('activityapi') as $name) { $function = $name .'_activityapi'; $result = $function($activity, $op); if (isset($result) && is_array($result)) { $return = array_merge($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } function activity_get_tablesort_headers() { return array( 'activity_targets.aid' => array('field' => 'activity_targets.aid', 'data' => t('Id')), 'activity.module' => array('field' => 'activity.module', 'data' => t('Module')), 'activity.type' => array('field' => 'activity.type', 'data' => t('Type')), 'activity.operation' => array('field' => 'activity.operation', 'data' => t('Operation')), 'activity.created' => array('field' => 'activity.created', 'data' => t('Created')), ); } /** * Implementation of hook_block(). */ function activity_block($op = 'list', $delta = 0, $edit = array()) { global $user; switch ($op) { case 'list': $block['my']['info'] = t("Activity: Mine: show the current user's activity."); $block['all']['info'] = t("Activity: All: show all recent activity"); return $block; break; case 'configure': $form['items'] = array( '#type' => 'select', '#title' => t('Number of items'), '#default_value' => variable_get('activity_block_'. $delta, 5), '#options' => drupal_map_assoc(range(1, 50)), ); return $form; break; case 'save': variable_set('activity_block_'. $delta, $edit['items']); break; case 'view': switch ($delta) { case 'my': if (user_access('view own activity')) { // Grab the number of requested activities plus one. We use this one // to determine whether or not to show the "more" link and only display // the correct number of items. $activity = activity_get_activity($user->uid, NULL, variable_get('activity_block_'. $delta, 5) + 1); if ($count = count($activity)) { drupal_add_css(drupal_get_path('module', 'activity') .'/activity.css'); if ($count > variable_get('activity_block_'. $delta, 5)) { $more_link = theme('activity_more_link', 'activity/mine'); array_pop($activity); } $activities = array(); foreach ($activity as $item) { $activities[] = theme('activity', activity_token_replace($item), $item) . activity_delete_link($item); } return array( 'subject' => t('My activity'), 'content' => theme('activity_block', $activities, $more_link) ); } } break; case 'all': if (user_access('view public activity')) { $activity = activity_get_activity(ACTIVITY_ALL, NULL, variable_get('activity_block_'. $delta, 5) + 1); if ($count = count($activity)) { drupal_add_css(drupal_get_path('module', 'activity') .'/activity.css'); if ($count > variable_get('activity_block_'. $delta, 5)) { $more_link = theme('activity_more_link', 'activity'); array_pop($activity); } $activities = array(); foreach ($activity as $item) { $activities[] = theme('activity', activity_token_replace($item), $item) . activity_delete_link($item); } return array( 'subject' => t('Recent activity'), 'content' => theme('activity_block', $activities, $more_link) ); } } break; } break; } } /** * Menu callback for displaying site or user activity as full page. */ function activity_page($page = 'all') { global $user; drupal_add_css(drupal_get_path('module', 'activity') .'/activity.css'); if ($page == 'mine') { $activities = activity_get_activity($user->uid, NULL, variable_get('activity_page_pager', 20)); $table = theme('activity_table', $activities); $feed_url = url('activity/'. $user->uid .'/feed'); drupal_add_feed($feed_url); $feed = theme('feed_icon', $feed_url); return theme('activity_page', $activities, $table); } else if ($page == 'all') { $activities = activity_get_activity(ACTIVITY_ALL, NULL, variable_get('activity_page_pager', 20)); $table = theme('activity_table', $activities); $feed_url = url('activity/all/feed'); drupal_add_feed($feed_url); $feed = theme('feed_icon', $feed_url); return theme('activity_page', $activities, $table); } } /** * Menu callback for displaying site or user activity as an RSS feed. */ function activity_feed($arg) { global $locale; if ($arg == ACTIVITY_ALL) { $activities = activity_get_activity(ACTIVITY_ALL, NULL, variable_get('activity_page_pager', 20)); $url = url('activity/all', NULL, NULL, TRUE); $feed_title = t('All activity'); } else if (is_numeric($arg)) { $user = db_fetch_object(db_query('SELECT uid, name FROM {users} WHERE uid = %d', $arg)); if ($user) { $activities = activity_get_activity($arg, NULL, variable_get('activity_page_pager', 20)); $url = url('activity/'. $user->uid, NULL, NULL, TRUE); $feed_title = t('Activity for @username', array('@username' => $user->name)); } } if (count($activities) > 0) { foreach ($activities as $activity) { $function = $activity['module'] .'_format_rss_item'; if (function_exists($function)) { // Each module gets a chance to build its own feed item. // They should use the $activity to prepare variables and // call format_rss_item($title, $link, $item_text, $extra); $items .= $function($activity); } else { $message = activity_token_replace($activity); $items .= format_rss_item(strip_tags($message), url('activity/'. $user->uid, NULL, NULL, TRUE), format_date($activity['created']). ''. $message. '
'); } } } $channel = array( 'version' => '2.0', 'title' => variable_get('site_name', 'Drupal') .' - '. $feed_title, 'link' => $url, 'description' => variable_get('site_mission', ''), 'language' => $locale, ); // @todo Figure out what the right namespace should be. $namespaces = array('xmlns:dc="http://purl.org/dc/elements/1.1/"'); $output = "\n"; $output .= "