delay); } if ($delay) { $delayed = new StdClass; $delayed->type = $at->id; $delayed->data = serialize($data); $delayed->ctime = db_format_timestamp(time()); insert_record('activity_queue', $delayed); } else { handle_activity($at, $data); } } /** * This function dispatches all the activity stuff to whatever notification * plugin it needs to, and figures out all the implications of activity and who * needs to know about it. * * @param object $activitytype record from database table activity_type * @param mixed $data must contain message to save. * each activity type has different requirements of $data - * - viewaccess must contain $owner userid of view owner AND $view (id of view) and $oldusers array of userids before access change was committed. * @param $cron = true if called by a cron job * @param object $queuedactivity record of the activity in the queue (from the table activity_queue) * @return int The ID of the last processed user * = 0 if all users get processed */ function handle_activity($activitytype, $data, $cron=false, $queuedactivity=null) { $data = (object)$data; if ($cron && isset($queuedactivity)) { $data->last_processed_userid = $queuedactivity->last_processed_userid; $data->activity_queue_id = $queuedactivity->id; } $classname = get_activity_type_classname($activitytype); $activity = new $classname($data, $cron); if (!$activity->any_users()) { return 0; } return $activity->notify_users(); } /** * Given an activity type id or record, calculate the class name. * * @param mixed $activitytype either numeric activity type id or an activity type record (containing name, plugintype, pluginname) * @return string */ function get_activity_type_classname($activitytype) { $activitytype = activity_locate_typerecord($activitytype); $classname = 'ActivityType' . ucfirst($activitytype->name); if (!empty($activitytype->plugintype)) { safe_require($activitytype->plugintype, $activitytype->pluginname); $classname = 'ActivityType' . ucfirst($activitytype->plugintype) . ucfirst($activitytype->pluginname) . ucfirst($activitytype->name); } return $classname; } /** * this function returns an array of users who subsribe to a particular activitytype * including the notification method they are using to subscribe to it. * * @param int $activitytype the id of the activity type * @param array $userids an array of userids to filter by * @param array $userobjs an array of user objects to filterby * @param bool $adminonly whether to filter by admin flag * @param array $admininstitutions list of institution names to get admins for * @return array of users */ function activity_get_users($activitytype, $userids=null, $userobjs=null, $adminonly=false, $admininstitutions = array()) { $values = array($activitytype); $sql = ' SELECT u.id, u.username, u.firstname, u.lastname, u.preferredname, u.email, u.admin, u.staff, p.method, ap.value AS lang, apm.value AS maildisabled, aic.value AS mnethostwwwroot, h.appname AS mnethostapp FROM {usr} u LEFT JOIN {usr_activity_preference} p ON (p.usr = u.id AND p.activity = ?)' . (empty($admininstitutions) ? '' : ' LEFT OUTER JOIN {usr_institution} ui ON (u.id = ui.usr AND ui.institution IN ('.join(',',array_map('db_quote',$admininstitutions)).'))') . ' LEFT OUTER JOIN {usr_account_preference} ap ON (ap.usr = u.id AND ap.field = \'lang\') LEFT OUTER JOIN {usr_account_preference} apm ON (apm.usr = u.id AND apm.field = \'maildisabled\') LEFT OUTER JOIN {auth_instance} ai ON (ai.id = u.authinstance AND ai.authname = \'xmlrpc\') LEFT OUTER JOIN {auth_instance_config} aic ON (aic.instance = ai.id AND aic.field = \'wwwroot\') LEFT OUTER JOIN {host} h ON aic.value = h.wwwroot WHERE u.deleted = 0'; if (!empty($userobjs) && is_array($userobjs)) { $sql .= ' AND u.id IN (' . implode(',',db_array_to_ph($userobjs)) . ')'; $values = array_merge($values, array_to_fields($userobjs)); } else if (!empty($userids) && is_array($userids)) { $sql .= ' AND u.id IN (' . implode(',',db_array_to_ph($userids)) . ')'; $values = array_merge($values, $userids); } if (!empty($admininstitutions)) { $sql .= ' GROUP BY u.id, u.username, u.firstname, u.lastname, u.preferredname, u.email, u.admin, u.staff, p.method, ap.value, apm.value, aic.value, h.appname HAVING (u.admin = 1 OR SUM(ui.admin) > 0)'; } else if ($adminonly) { $sql .= ' AND u.admin = 1'; } return get_records_sql_assoc($sql, $values); } /** * this function inserts a default set of activity preferences for a given user * id */ function activity_set_defaults($eventdata) { $user_id = $eventdata['id']; $activitytypes = get_records_array('activity_type', 'admin', 0); foreach ($activitytypes as $type) { insert_record('usr_activity_preference', (object)array( 'usr' => $user_id, 'activity' => $type->id, 'method' => $type->defaultmethod, )); } } function activity_add_admin_defaults($userids) { $activitytypes = get_records_array('activity_type', 'admin', 1); foreach ($activitytypes as $type) { foreach ($userids as $id) { if (!record_exists('usr_activity_preference', 'usr', $id, 'activity', $type->id)) { insert_record('usr_activity_preference', (object)array( 'usr' => $id, 'activity' => $type->id, 'method' => $type->defaultmethod, )); } } } } function activity_process_queue() { if ($toprocess = get_records_array('activity_queue')) { // Hack to avoid duplicate watchlist notifications on the same view $watchlist = activity_locate_typerecord('watchlist'); $viewsnotified = array(); foreach ($toprocess as $activity) { $data = unserialize($activity->data); if ($activity->type == $watchlist->id && !empty($data->view)) { if (isset($viewsnotified[$data->view])) { continue; } $viewsnotified[$data->view] = true; } try { $last_processed_userid = handle_activity($activity->type, $data, true, $activity); } catch (MaharaException $e) { // Exceptions can happen while processing the queue, we just // log them and continue log_debug($e->getMessage()); } // Update the activity queue // or Remove this activity from the queue if all the users get processed // to make sure we // never send duplicate emails even if part of the // activity handler fails for whatever reason if (!empty($last_processed_userid)) { update_record('activity_queue', array('last_processed_userid' => $last_processed_userid), array('id' => $activity->id)); } else { if (!delete_records('activity_queue', 'id', $activity->id)) { log_warn("Unable to remove activity $activity->id from the queue. Skipping it."); } } } } } /** * event-listener is called when an artefact is changed or a block instance * is commited. Saves the view, the block instance, user and time into the * database * * @global User $USER * @param string $event * @param object $eventdata */ function watchlist_record_changes($event){ global $USER; // don't catch root's changes, especially not when installing... if ($USER->get('id') <= 0) { return; } if ($event instanceof BlockInstance) { $viewid = $event->get('view'); if ($viewid) { set_field('view', 'mtime', db_format_timestamp(time()), 'id', $viewid); } if (record_exists('usr_watchlist_view', 'view', $viewid)) { $whereobj = new stdClass(); $whereobj->block = $event->get('id'); $whereobj->view = $viewid; $whereobj->usr = $USER->get('id'); $dataobj = clone $whereobj; $dataobj->changed_on = date('Y-m-d H:i:s'); ensure_record_exists('watchlist_queue', $whereobj, $dataobj); } } else if ($event instanceof ArtefactType) { $blockid = $event->get('id'); $getcolumnquery = ' SELECT DISTINCT "view", "block" FROM {view_artefact} WHERE artefact =' . $blockid; $relations = get_records_sql_array($getcolumnquery, array()); // fix unnecessary type-inconsistency of get_records_sql_array if (false === $relations) { $relations = array(); } foreach ($relations as $rel) { $viewid = $rel->view; if ($viewid) { set_field('view', 'mtime', db_format_timestamp(time()), 'id', $viewid); } if (!record_exists('usr_watchlist_view', 'view', $viewid)) { continue; } $whereobj = new stdClass(); $whereobj->block = $rel->block; $whereobj->view = $viewid; $whereobj->usr = $USER->get('id'); $dataobj = clone $whereobj; $dataobj->changed_on = date('Y-m-d H:i:s'); ensure_record_exists('watchlist_queue', $whereobj, $dataobj); } } else if (!is_object($event) && !empty($event['id'])) { $viewid = $event['id']; if ($viewid) { set_field('view', 'mtime', db_format_timestamp(time()), 'id', $viewid); } if (record_exists('usr_watchlist_view', 'view', $viewid)) { $whereobj = new stdClass(); $whereobj->view = $viewid; $whereobj->usr = $USER->get('id'); $whereobj->block = null; $dataobj = clone $whereobj; $dataobj->changed_on = date('Y-m-d H:i:s'); ensure_record_exists('watchlist_queue', $whereobj, $dataobj); } } else { return; } } /** * is triggered when a blockinstance is deleted. Deletes all watchlist_queue * entries that refer to this blockinstance * * @param BlockInstance $blockinstance */ function watchlist_block_deleted(BlockInstance $block) { global $USER; // don't catch root's changes, especially not when installing... if ($USER->get('id') <= 0) { return; } delete_records('watchlist_queue', 'block', $block->get('id')); if (record_exists('usr_watchlist_view', 'view', $block->get('view'))) { $whereobj = new stdClass(); $whereobj->view = $block->get('view'); $whereobj->block = null; $whereobj->usr = $USER->get('id'); $dataobj = clone $whereobj; $dataobj->changed_on = date('Y-m-d H:i:s'); ensure_record_exists('watchlist_queue', $whereobj, $dataobj); } } /** * is called by the cron-job to process the notifications stored into * watchlist_queue. */ function watchlist_process_notifications() { $delayMin = get_config('watchlistnotification_delay'); $comparetime = time() - $delayMin * 60; $sql = "SELECT usr, view, MAX(changed_on) AS time FROM {watchlist_queue} GROUP BY usr, view"; $results = get_records_sql_array($sql, array()); if (false === $results) { return; } foreach ($results as $viewuserdaterow) { if ($viewuserdaterow->time > date('Y-m-d H:i:s', $comparetime)) { continue; } // don't send a notification if only blockinstances are referenced // that were deleted (block exists but corresponding // block_instance doesn't) $sendnotification = false; $blockinstance_ids = get_column('watchlist_queue', 'block', 'usr', $viewuserdaterow->usr, 'view', $viewuserdaterow->view); if (is_array($blockinstance_ids)) { $blockinstance_ids = array_unique($blockinstance_ids); } $viewuserdaterow->blocktitles = array(); // need to check if view has an owner, group or institution $view = get_record('view', 'id', $viewuserdaterow->view); if (empty($view->owner) && empty($view->group) && empty($view->institution)) { continue; } // ignore root pages, owner = 0, this account is not meant to produce content if (isset($view->owner) && empty($view->owner)) { continue; } // Ignore system templates, institution = 'mahara' and template = 2 require_once(get_config('libroot') . 'view.php'); if (isset($view->institution) && $view->institution == 'mahara' && $view->template == View::SITE_TEMPLATE) { continue; } foreach ($blockinstance_ids as $blockinstance_id) { if (empty($blockinstance_id)) { // if no blockinstance is given, assume that the form itself // was changed, e.g. the theme, or a block was removed $sendnotification = true; continue; } require_once(get_config('docroot') . 'blocktype/lib.php'); try { $block = new BlockInstance($blockinstance_id); } catch (BlockInstanceNotFoundException $exc) { // maybe the block was deleted continue; } $blocktype = $block->get('blocktype'); $title = ''; // try to get title rendered by plugin-class safe_require('blocktype', $blocktype); if (class_exists(generate_class_name('blocktype', $blocktype))) { $title = $block->get_title(); } else { log_warn('class for blocktype could not be loaded: ' . $blocktype); $title = $block->get('title'); } // if no title was given to the blockinstance, try to get one // from the artefact if (empty($title)) { $configdata = $block->get('configdata'); if (array_key_exists('artefactid', $configdata)) { try { $artefact = $block->get_artefact_instance($configdata['artefactid']); $title = $artefact->get('title'); } catch(Exception $exc) { log_warn('couldn\'t identify title of blockinstance ' . $block->get('id') . $exc->getMessage()); } } } // still no title, maybe the default-name for the blocktype if (empty($title)) { $title = get_string('title', 'blocktype.' . $blocktype); } // no title could be retrieved, so let's tell the user at least // what type of block was changed if (empty($title)) { $title = '[' . $blocktype . '] (' . get_string('nonamegiven', 'activity') . ')'; } $viewuserdaterow->blocktitles[] = $title; $sendnotification = true; } // only send notification if there is something to talk about (don't // send notification for example when new blockelement was aborted) if ($sendnotification) { try{ $watchlistnotification = new ActivityTypeWatchlistnotification($viewuserdaterow, false); $watchlistnotification->notify_users(); } catch (ViewNotFoundException $exc) { // Seems like the view has been deleted, don't do anything } catch (SystemException $exc) { // if the view that was changed doesn't have an owner } } delete_records('watchlist_queue', 'usr', $viewuserdaterow->usr, 'view', $viewuserdaterow->view); } } function activity_get_viewaccess_users($view) { require_once(get_config('docroot') . 'lib/group.php'); $sql = "SELECT userlist.userid, usr.*, actpref.method, accpref.value AS lang, aic.value AS mnethostwwwroot, h.appname AS mnethostapp FROM ( SELECT friend.usr1 AS userid FROM {view} view JOIN {view_access} access ON (access.view = view.id AND access.accesstype = 'friends') JOIN {usr_friend} friend ON (view.owner = friend.usr2 AND view.id = ?) UNION SELECT friend.usr2 AS userid FROM {view} view JOIN {view_access} access ON (access.view = view.id AND access.accesstype = 'friends') JOIN {usr_friend} friend ON (view.owner = friend.usr1 AND view.id = ?) UNION SELECT access.usr AS userid FROM {view_access} access WHERE access.view = ? UNION SELECT members.member AS userid FROM {view_access} access JOIN {group} grp ON (access.group = grp.id AND grp.deleted = 0 AND access.view = ?) JOIN {group_member} members ON (grp.id = members.group AND members.member <> CASE WHEN access.usr IS NULL THEN -1 ELSE access.usr END) WHERE (access.role IS NULL OR access.role = members.role) AND (grp.viewnotify = " . GROUP_ROLES_ALL . " OR (grp.viewnotify = " . GROUP_ROLES_NONMEMBER . " AND (members.role = 'admin' OR members.role = 'tutor')) OR (grp.viewnotify = " . GROUP_ROLES_ADMIN . " AND members.role = 'admin') ) ) AS userlist JOIN {usr} usr ON usr.id = userlist.userid LEFT JOIN {usr_activity_preference} actpref ON actpref.usr = usr.id LEFT JOIN {activity_type} acttype ON actpref.activity = acttype.id AND acttype.name = 'viewaccess' LEFT JOIN {usr_account_preference} accpref ON accpref.usr = usr.id AND accpref.field = 'lang' LEFT JOIN {auth_instance} ai ON ai.id = usr.authinstance LEFT OUTER JOIN {auth_instance_config} aic ON (aic.instance = ai.id AND aic.field = 'wwwroot') LEFT OUTER JOIN {host} h ON aic.value = h.wwwroot"; $values = array($view, $view, $view, $view); if (!$u = get_records_sql_assoc($sql, $values)) { $u = array(); } return $u; } /** * Return the minimum and maximum access times if they exist for the page * based on user getting access. To be used with view access notifications * * @param string $viewid ID of the view * @param string $userid ID of the user * @return array Min and max access dates */ function activity_get_viewaccess_user_dates($viewid, $userid) { if ($results = get_records_sql_array(" SELECT MIN(startdate) AS mindate, MAX(stopdate) as maxdate FROM ( SELECT startdate, stopdate FROM {view} WHERE id = ? UNION SELECT startdate, stopdate FROM {view_access} WHERE view = ? AND usr = ? UNION SELECT startdate, stopdate FROM {view_access} va JOIN {group_member} gm ON gm.group = va.group WHERE va.view = ? AND gm.member = ? UNION SELECT startdate, stopdate FROM {view_access} va JOIN {usr_institution} ui ON ui.institution = va.institution WHERE va.view = ? and ui.usr = ? UNION SELECT startdate, stopdate FROM {view_access} WHERE view = ? AND accesstype IN ('loggedin','public') UNION SELECT startdate, stopdate FROM {view_access} WHERE accesstype = 'friends' AND view = ? AND EXISTS ( SELECT * FROM {usr_friend} WHERE (usr1 = (SELECT owner FROM {view} WHERE id = ?) AND usr2 = ?) OR (usr2 = (SELECT owner FROM {view} WHERE id = ?) AND usr1 = ?) ) ) AS dates", array($viewid, $viewid, $userid, $viewid, $userid, $viewid, $userid, $viewid, $viewid, $viewid, $userid, $viewid, $userid)) ) { return array('mindate' => $results[0]->mindate, 'maxdate' => $results[0]->maxdate); } return array('mindate' => null, 'maxdate' => null); } function activity_locate_typerecord($activitytype, $plugintype=null, $pluginname=null) { if (is_object($activitytype)) { return $activitytype; } if (is_numeric($activitytype)) { $at = get_record('activity_type', 'id', $activitytype); } else { if (empty($plugintype) && empty($pluginname)) { $at = get_record_select('activity_type', 'name = ? AND plugintype IS NULL AND pluginname IS NULL', array($activitytype)); } else { $at = get_record('activity_type', 'name', $activitytype, 'plugintype', $plugintype, 'pluginname', $pluginname); } } if (empty($at)) { throw new SystemException("Invalid activity type $activitytype"); } return $at; } function generate_activity_class_name($name, $plugintype, $pluginname) { if (!empty($plugintype)) { safe_require($plugintype, $pluginname); return 'ActivityType' . ucfirst($plugintype) . ucfirst($pluginname) . ucfirst($name); } return 'ActivityType' . $name; } /** * To implement a new activity type, you must subclass this class. Your subclass * MUST at minimum include the following: * * 1. Override the __construct method with one which first calls parent::__construct * and then populates $this->users with the list of recipients for this activity. * * 2. Implement the get_required_parameters method. */ abstract class ActivityType { /** * NOTE: Child classes MUST call the parent constructor, AND populate * $this->users with a list of user records which should receive the message! * * @param array $data The data needed to send the notification * @param boolean $cron Indicates whether this is being called by the cron job */ public function __construct($data, $cron=false) { $this->cron = $cron; $this->set_parameters($data); $this->ensure_parameters(); $this->activityname = strtolower(substr(get_class($this), strlen('ActivityType'))); } /** * This method should return an array which names the fields that must be present in the * $data that was passed to the class's constructor. It should include all necessary data * to determine the recipient(s) of the notification and to determine its content. * * @return array */ abstract function get_required_parameters(); /** * The number of users in a split chunk to notify */ const USERCHUNK_SIZE = 1000; /** * Who any notifications about this activity should appear to come from */ protected $fromuser; /** * When sending notifications, should the email of the person sending it be * hidden? (Almost always yes, will cause the email to appear to come from * the 'noreply' address) */ protected $hideemail = true; protected $subject; protected $message; protected $strings; protected $users = array(); protected $url; protected $urltext; protected $id; protected $type; protected $activityname; protected $cron; protected $last_processed_userid; protected $activity_queue_id; protected $overridemessagecontents; protected $parent; protected $defaultmethod; public function get_id() { if (!isset($this->id)) { $tmp = activity_locate_typerecord($this->get_type()); $this->id = $tmp->id; } return $this->id; } public function get_default_method() { if (!isset($this->defaultmethod)) { $tmp = activity_locate_typerecord($this->get_id()); $this->defaultmethod = $tmp->defaultmethod; } return $this->defaultmethod; } public function get_type() { $prefix = 'ActivityType'; return strtolower(substr(get_class($this), strlen($prefix))); } public function any_users() { return (is_array($this->users) && count($this->users) > 0); } public function get_users() { return $this->users; } private function set_parameters($data) { foreach ($data as $key => $value) { if (property_exists($this, $key)) { $this->{$key} = $value; } } } private function ensure_parameters() { foreach ($this->get_required_parameters() as $param) { if (!isset($this->{$param})) { // Allow some string parameters to be specified in $this->strings if (!in_array($param, array('subject', 'message', 'urltext')) || empty($this->strings->{$param}->key)) { throw new ParamOutOfRangeException(get_string('missingparam', 'activity', $param, $this->get_type())); } } } } public function to_stdclass() { return (object)get_object_vars($this); } public function get_string_for_user($user, $string) { if (empty($string) || empty($this->strings->{$string}->key)) { return; } $args = array_merge( array( $user->lang, $this->strings->{$string}->key, empty($this->strings->{$string}->section) ? 'mahara' : $this->strings->{$string}->section, ), empty($this->strings->{$string}->args) ? array() : $this->strings->{$string}->args ); return call_user_func_array('get_string_from_language', $args); } // Optional string to use for the link text. public function add_urltext(array $stringdef) { $def = $stringdef; if (!is_object($this->strings)) { $this->strings = new stdClass(); } $this->strings->urltext = (object) $def; } public function get_urltext($user) { if (empty($this->urltext)) { return $this->get_string_for_user($user, 'urltext'); } return $this->urltext; } public function get_message($user) { if (empty($this->message)) { return $this->get_string_for_user($user, 'message'); } return $this->message; } public function get_subject($user) { if (empty($this->subject)) { return $this->get_string_for_user($user, 'subject'); } return $this->subject; } /** * Rewrite $this->url with the ID of the internal notification record for this activity. * (Generally so that you can make a URL that sends the user to the Mahara inbox page * for this message.) * * @param int $internalid * @return boolean True if $this->url was updated, False if not. */ protected function update_url($internalid) { return false; } public function notify_user($user) { $changes = new stdClass; $userdata = $this->to_stdclass(); // some stuff gets overridden by user specific stuff if (!empty($user->url)) { $userdata->url = $user->url; } if (empty($user->lang) || $user->lang == 'default') { $user->lang = get_config('lang'); } if (empty($user->method)) { // If method is not set then either the user has selected 'none' or their setting has not been set (so use default). if ($record = get_record('usr_activity_preference', 'usr', $user->id, 'activity', $this->get_id())) { $user->method = $record->method; if (empty($user->method)) { // The user specified 'none' as their notification type. return; } } else { $user->method = $this->get_default_method(); if (empty($user->method)) { // The default notification type is 'none' for this activity type. return; } } } // always do internal foreach (PluginNotificationInternal::$userdata as &$p) { $function = 'get_' . $p; $userdata->$p = $this->$function($user); } $userdata->internalid = PluginNotificationInternal::notify_user($user, $userdata); if ($this->update_url($userdata->internalid)) { $changes->url = $userdata->url = $this->url; } if ($user->method != 'internal' || isset($changes->url)) { $changes->read = (int) ($user->method != 'internal'); $changes->id = $userdata->internalid; update_record('notification_internal_activity', $changes); } if ($user->method != 'internal') { $method = $user->method; safe_require('notification', $method); $notificationclass = generate_class_name('notification', $method); $classvars = get_class_vars($notificationclass); if (!empty($classvars['userdata'])) { foreach ($classvars['userdata'] as &$p) { $function = 'get_' . $p; if (!isset($userdata->$p) && method_exists($this, $function)) { $userdata->$p = $this->$function($user); } } } try { call_static_method($notificationclass, 'notify_user', $user, $userdata); } catch (MaharaException $e) { static $badnotification = false; static $adminnotified = array(); // We don't mind other notification methods failing, as it'll // go into the activity log as 'unread' $changes->read = 0; update_record('notification_internal_activity', $changes); if (!$badnotification && !($e instanceof EmailDisabledException || $e instanceof InvalidEmailException)) { // Admins should probably know about the error, but to avoid sending too many similar notifications, // save an initial prefix of the message being sent and throw away subsequent exceptions with the // same prefix. To cut down on spam, it's worth missing out on a few similar messages. $k = substr($e, 0, 60); if (!isset($adminnotified[$k])) { $message = (object) array( 'users' => get_column('usr', 'id', 'admin', 1), 'subject' => get_string('adminnotificationerror', 'activity'), 'message' => $e, ); $adminnotified[$k] = 1; $badnotification = true; activity_occurred('maharamessage', $message); $badnotification = false; } } } } // The user's unread message count does not need to be updated from $changes->read // because of the db trigger on notification_internal_activity. } /** * Sound out notifications to $this->users. * Note that, although this has batching properties built into it with USERCHUNK_SIZE, * it's also recommended to update a bulk ActivityType's constructor to limit the total * number of records pulled from the database. */ public function notify_users() { safe_require('notification', 'internal'); $this->type = $this->get_id(); if ($this->cron) { // Sort the list of users to notify by userid uasort($this->users, function($a, $b) {return $a->id > $b->id;}); // Notify a chunk of users $num_processed_users = 0; $last_processed_userid = 0; foreach ($this->users as $user) { if ($this->last_processed_userid && ($user->id <= $this->last_processed_userid)) { continue; } if ($num_processed_users < ActivityType::USERCHUNK_SIZE) { // Immediately update the last_processed_userid in the activity_queue // to prevent duplicated notifications $last_processed_userid = $user->id; update_record('activity_queue', array('last_processed_userid' => $last_processed_userid), array('id' => $this->activity_queue_id)); $this->notify_user($user); $num_processed_users++; } else { break; } } return $last_processed_userid; } else { while (!empty($this->users)) { $user = array_shift($this->users); $this->notify_user($user); } } return 0; } } abstract class ActivityTypeAdmin extends ActivityType { public function __construct($data, $cron=false) { parent::__construct($data, $cron); $this->users = activity_get_users($this->get_id(), null, null, true); } } class ActivityTypeContactus extends ActivityTypeAdmin { protected $fromname; protected $fromemail; protected $hideemail = false; /** * @param array $data Parameters: * - message (string) * - subject (string) (optional) * - fromname (string) * - fromaddress (email address) * - fromuser (int) (if a logged in user) */ function __construct($data, $cron=false) { parent::__construct($data, $cron); if (!empty($this->fromuser)) { $this->url = profile_url($this->fromuser, false); } else { $this->customheaders = array( 'Reply-to: ' . $this->fromname . ' <' . $this->fromemail . '>', ); } } function get_subject($user) { return get_string_from_language($user->lang, 'newcontactus', 'activity'); } function get_message($user) { return get_string_from_language($user->lang, 'newcontactusfrom', 'activity') . ' ' . $this->fromname . ' <' . $this->fromemail .'>' . (isset($this->subject) ? ': ' . $this->subject : '') . "\n\n" . $this->message; } public function get_required_parameters() { return array('message', 'fromname', 'fromemail'); } } class ActivityTypeObjectionable extends ActivityTypeAdmin { protected $view; protected $artefact; protected $reporter; protected $ctime; /** * @param array $data Parameters: * - message (string) * - view (int) * - artefact (int) (optional) * - reporter (int) * - ctime (int) (optional) */ function __construct($data, $cron=false) { parent::__construct($data, $cron); require_once('view.php'); $this->view = new View($this->view); if (!empty($this->artefact)) { require_once(get_config('docroot') . 'artefact/lib.php'); $this->artefact = artefact_instance_from_id($this->artefact); } if ($owner = $this->view->get('owner')) { // Notify institutional admins of the view owner if ($institutions = get_column('usr_institution', 'institution', 'usr', $owner)) { $this->users = activity_get_users($this->get_id(), null, null, null, $institutions); } } if (empty($this->artefact)) { $this->url = $this->view->get_url(false, true) . '&objection=1'; } else { $this->url = 'artefact/artefact.php?artefact=' . $this->artefact->get('id') . '&view=' . $this->view->get('id') . '&objection=1'; } if (empty($this->strings->subject)) { $this->overridemessagecontents = true; $viewtitle = $this->view->get('title'); $this->strings = new stdClass(); if (empty($this->artefact)) { $this->strings->subject = (object) array( 'key' => 'objectionablecontentview', 'section' => 'activity', 'args' => array($viewtitle, display_default_name($this->reporter)), ); } else { $title = $this->artefact->get('title'); $this->strings->subject = (object) array( 'key' => 'objectionablecontentviewartefact', 'section' => 'activity', 'args' => array($viewtitle, $title, display_default_name($this->reporter)), ); } } } public function get_emailmessage($user) { $reporterurl = profile_url($this->reporter); $ctime = strftime(get_string_from_language($user->lang, 'strftimedaydatetime'), $this->ctime); if (empty($this->artefact)) { return get_string_from_language( $user->lang, 'objectionablecontentviewtext', 'activity', $this->view->get('title'), display_default_name($this->reporter), $ctime, $this->message, $this->view->get_url(true, true) . "&objection=1", $reporterurl ); } else { return get_string_from_language( $user->lang, 'objectionablecontentviewartefacttext', 'activity', $this->view->get('title'), $this->artefact->get('title'), display_default_name($this->reporter), $ctime, $this->message, get_config('wwwroot') . "artefact/artefact.php?artefact=" . $this->artefact->get('id') . "&view=" . $this->view->get('id') . "&objection=1", $reporterurl ); } } public function get_htmlmessage($user) { $viewtitle = hsc($this->view->get('title')); $reportername = hsc(display_default_name($this->reporter)); $reporterurl = profile_url($this->reporter); $ctime = strftime(get_string_from_language($user->lang, 'strftimedaydatetime'), $this->ctime); $message = hsc($this->message); if (empty($this->artefact)) { return get_string_from_language( $user->lang, 'objectionablecontentviewhtml', 'activity', $viewtitle, $reportername, $ctime, $message, $this->view->get_url(true, true) . "&objection=1", $viewtitle, $reporterurl, $reportername ); } else { return get_string_from_language( $user->lang, 'objectionablecontentviewartefacthtml', 'activity', $viewtitle, hsc($this->artefact->get('title')), $reportername, $ctime, $message, get_config('wwwroot') . "artefact/artefact.php?artefact=" . $this->artefact->get('id') . "&view=" . $this->view->get('id') . "&objection=1", hsc($this->artefact->get('title')), $reporterurl, $reportername ); } } public function get_required_parameters() { return array('message', 'view', 'reporter'); } } class ActivityTypeVirusRepeat extends ActivityTypeAdmin { protected $username; protected $fullname; protected $userid; public function __construct($data, $cron=false) { parent::__construct($data, $cron); } public function get_subject($user) { $userstring = $this->username . ' (' . $this->fullname . ') (userid:' . $this->userid . ')' ; return get_string_from_language($user->lang, 'virusrepeatsubject', 'mahara', $userstring); } public function get_message($user) { return get_string_from_language($user->lang, 'virusrepeatmessage'); } public function get_required_parameters() { return array('username', 'fullname', 'userid'); } } class ActivityTypeVirusRelease extends ActivityTypeAdmin { public function __construct($data, $cron=false) { parent::__construct($data, $cron); } public function get_required_parameters() { return array(); } } class ActivityTypeMaharamessage extends ActivityType { /** * @param array $data Parameters: * - subject (string) * - message (string) * - users (list of user ids) */ public function __construct($data, $cron=false) { parent::__construct($data, $cron); $this->users = activity_get_users($this->get_id(), $this->users); } public function get_required_parameters() { return array('message', 'subject', 'users'); } } class ActivityTypeInstitutionmessage extends ActivityType { protected $messagetype; protected $institution; protected $username; protected $fullname; public function __construct($data, $cron=false) { parent::__construct($data, $cron); if ($this->messagetype == 'request') { $this->url = 'admin/users/institutionusers.php'; $this->users = activity_get_users($this->get_id(), null, null, null, array($this->institution->name)); $this->add_urltext(array('key' => 'institutionmembers', 'section' => 'admin')); } else if ($this->messagetype == 'invite') { $this->url = 'account/institutions.php'; $this->users = activity_get_users($this->get_id(), $this->users); $this->add_urltext(array('key' => 'institutionmembership', 'section' => 'mahara')); } } public function get_subject($user) { if ($this->messagetype == 'request') { $userstring = $this->fullname . ' (' . $this->username . ')'; return get_string_from_language($user->lang, 'institutionrequestsubject', 'activity', $userstring, $this->institution->displayname); } else if ($this->messagetype == 'invite') { return get_string_from_language($user->lang, 'institutioninvitesubject', 'activity', $this->institution->displayname); } } public function get_message($user) { if ($this->messagetype == 'request') { return $this->get_subject($user) .' '. get_string_from_language($user->lang, 'institutionrequestmessage', 'activity', $this->url); } else if ($this->messagetype == 'invite') { return $this->get_subject($user) .' '. get_string_from_language($user->lang, 'institutioninvitemessage', 'activity', $this->url); } } public function get_required_parameters() { return array('messagetype', 'institution'); } } class ActivityTypeUsermessage extends ActivityType { protected $userto; protected $userfrom; /** * @param array $data Parameters: * - userto (int) * - userfrom (int) * - subject (string) * - message (string) * - parent (int) */ public function __construct($data, $cron=false) { parent::__construct($data, $cron); if ($this->userfrom) { $this->fromuser = $this->userfrom; } $this->users = activity_get_users($this->get_id(), array($this->userto)); $this->add_urltext(array( 'key' => 'Reply', 'section' => 'group', )); } public function get_subject($user) { if (empty($this->subject)) { return get_string_from_language($user->lang, 'newusermessage', 'group', display_name($this->userfrom)); } return $this->subject; } protected function update_url($internalid) { $this->url = 'user/sendmessage.php?id=' . $this->userfrom . '&replyto=' . $internalid . '&returnto=inbox'; return true; } public function get_required_parameters() { return array('message', 'userto', 'userfrom'); } } class ActivityTypeWatchlist extends ActivityType { protected $view; protected $ownerinfo; protected $viewinfo; /** * @param array $data Parameters: * - view (int) */ public function __construct($data, $cron) { parent::__construct($data, $cron); require_once('view.php'); if ($this->viewinfo = new View($this->view)) { $this->ownerinfo = hsc($this->viewinfo->formatted_owner()); } if (empty($this->ownerinfo)) { if (!empty($this->cron)) { // probably deleted already return; } throw new ViewNotFoundException(get_string('viewnotfound', 'error', $this->view)); } $viewurl = $this->viewinfo->get_url(false); // mysql compatibility (sigh...) $casturl = 'CAST(? AS TEXT)'; if (is_mysql()) { $casturl = '?'; } $sql = 'SELECT u.*, p.method, ap.value AS lang, ' . $casturl . ' AS url FROM {usr_watchlist_view} wv JOIN {usr} u ON wv.usr = u.id LEFT JOIN {usr_activity_preference} p ON p.usr = u.id LEFT OUTER JOIN {usr_account_preference} ap ON (ap.usr = u.id AND ap.field = \'lang\') WHERE (p.activity = ? OR p.activity IS NULL) AND wv.view = ? '; $this->users = get_records_sql_array( $sql, array($viewurl, $this->get_id(), $this->view) ); // Remove the view from the watchlist of users who can no longer see it if ($this->users) { $userstodelete = array(); foreach($this->users as $k => &$u) { if (!can_view_view($this->view, $u->id)) { $userstodelete[] = $u->id; unset($this->users[$k]); } } if ($userstodelete) { delete_records_select( 'usr_watchlist_view', 'view = ? AND usr IN (' . join(',', $userstodelete) . ')', array($this->view) ); } } $this->add_urltext(array('key' => 'View', 'section' => 'view')); } public function get_subject($user) { return get_string_from_language($user->lang, 'newwatchlistmessage', 'activity'); } public function get_message($user) { return get_string_from_language($user->lang, 'newwatchlistmessageview1', 'activity', $this->viewinfo->get('title'), $this->ownerinfo); } public function get_required_parameters() { return array('view'); } } /** * extending ActivityTypeWatchlist to reuse the funcinality and structure */ class ActivityTypeWatchlistnotification extends ActivityTypeWatchlist{ protected $view; protected $blocktitles = array(); protected $usr; /** * @param array $data Parameters: * - view (int) * - blocktitles (array: int) * - usr (int) */ public function __construct($data, $cron) { parent::__construct($data, $cron); $this->blocktitles = $data->blocktitles; $this->usr = $data->usr; } /** * override function get_message to add information about the changed * blockinstances * * @param type $user * @return type */ public function get_message($user) { $message = get_string_from_language($user->lang, 'newwatchlistmessageview1', 'activity', $this->viewinfo->get('title'), $this->ownerinfo); try { foreach ($this->blocktitles as $blocktitle) { $message .= "\n" . get_string_from_language($user->lang, 'blockinstancenotification', 'activity', $blocktitle); } } catch(Exception $exc) { var_log(var_export($exc, true)); } return $message; } /** * overwrite get_type to obfuscate that we are not really an Activity_type */ public function get_type() { return('watchlist'); } } class ActivityTypeViewAccess extends ActivityType { protected $view; protected $oldusers; // this can be empty though protected $views; // optional array of views by id being changed private $title, $ownername; private $incollection = false; /** * @param array $data Parameters: * - view (int) * - oldusers (array of user IDs) */ public function __construct($data, $cron=false) { parent::__construct($data, $cron); if (!$viewinfo = new View($this->view)) { if (!empty($this->cron)) { // probably deleted already return; } throw new ViewNotFoundException(get_string('viewnotfound', 'error', $this->view)); } $this->url = $viewinfo->get_url(false); $this->users = array_diff_key( activity_get_viewaccess_users($this->view), $this->oldusers ); if ($viewinfo->get_collection()) { $this->incollection = true; $this->title = $viewinfo->get_collection()->get('name'); $this->add_urltext(array('key' => 'Collection', 'section' => 'collection')); } else { $this->title = $viewinfo->get('title'); $this->add_urltext(array('key' => 'View', 'section' => 'view')); } $this->ownername = $viewinfo->formatted_owner(); } public function get_view_titles() { $views = (!empty($this->views) && is_array($this->views) && sizeof($this->views) > 1) ? $this->views : false; if (!$views) { return false; } $titles = array(); foreach ($views as $view) { $titles[$view['id']] = $view['title']; } return $titles; } public function get_subject($user) { if ($titles = $this->get_view_titles()) { // because we may be updating access rules on pages both in and not in a collection // it is easiest just to return a list of the pages we have updated return get_string('newviewaccesssubjectviews', 'activity', implode('", "', $titles)); } else { return $this->incollection ? get_string('newcollectionaccesssubject', 'activity', $this->title) : get_string('newviewaccesssubject1', 'activity', $this->title); } } public function get_view_access_message($user) { $accessdates = activity_get_viewaccess_user_dates($this->view, $user->id); $accessdatemessage = ''; $fromdate = format_date(strtotime($accessdates['mindate']), 'strftimedate'); $todate = format_date(strtotime($accessdates['maxdate']), 'strftimedate'); if (!empty($accessdates['mindate']) && !empty($accessdates['maxdate'])) { $accessdatemessage .= get_string_from_language($user->lang, 'messageaccessfromto1', 'activity', $fromdate, $todate); } else if (!empty($accessdates['mindate'])) { $accessdatemessage .= get_string_from_language($user->lang, 'messageaccessfrom1', 'activity', $fromdate); } else if (!empty($accessdates['maxdate'])) { $accessdatemessage .= get_string_from_language($user->lang, 'messageaccessto1', 'activity', $todate); } else { $accessdatemessage = false; } return $accessdatemessage; } public function get_message($user) { $title = $this->title; $plural = ''; if ($titles = $this->get_view_titles()) { $title = implode('", "', $titles); $plural = 'views'; } $accessdatemessage = ($this->view && $user->id) ? $this->get_view_access_message($user) : null; $newaccessmessagestr = $this->incollection ? 'newcollectionaccessmessage' . $plural : 'newviewaccessmessage' . $plural; if ($this->ownername) { $message = get_string_from_language($user->lang, $newaccessmessagestr, 'activity', $title, $this->ownername, $this->title); } else { $newaccessmessagenoownerstr = $this->incollection ? 'newcollectionaccessmessagenoowner' : 'newviewaccessmessagenoowner' . $plural; $message = get_string_from_language($user->lang, $newaccessmessagenoownerstr, 'activity', $title, $this->title); } if ($accessdatemessage) { $message .= "\n\n" . $accessdatemessage; } return $message; } public function get_required_parameters() { return array('view', 'oldusers'); } } class ActivityTypeGroupMessage extends ActivityType { protected $group; protected $roles; protected $deletedgroup; /** * @param array $data Parameters: * - group (integer) * - roles (list of roles) */ public function __construct($data, $cron=false) { require_once('group.php'); parent::__construct($data, $cron); $members = group_get_member_ids($this->group, isset($this->roles) ? $this->roles : null, $this->deletedgroup); if (!empty($members)) { $this->users = activity_get_users($this->get_id(), $members); } } public function get_required_parameters() { return array('group'); } } abstract class ActivityTypePlugin extends ActivityType { abstract public function get_plugintype(); abstract public function get_pluginname(); public function get_type() { $prefix = 'ActivityType' . $this->get_plugintype() . $this->get_pluginname(); return strtolower(substr(get_class($this), strlen($prefix))); } public function get_id() { if (!isset($this->id)) { $tmp = activity_locate_typerecord($this->get_type(), $this->get_plugintype(), $this->get_pluginname()); $this->id = $tmp->id; } return $this->id; } } function format_notification_whitespace($message, $type=null) { $message = preg_replace('//', '', $message); $message = preg_replace('/^(\s| |\xc2\xa0)*/', '', $message); // convert any htmlspecialchars back so we don't double escape as part of format_whitespace() $message = htmlspecialchars_decode($message); $message = format_whitespace($message); // @todo: Sensibly distinguish html notifications, notifications where the full text // appears on another page and this is just an abbreviated preview, and text-only // notifications where the entire text must appear here because there's nowhere else // to see it. $replace = ($type == 'newpost' || $type == 'feedback') ? '
' : '

'; return preg_replace('/(\s*){2,}/', $replace, $message); } /** * Get one page of notifications and return html */ function activitylist_html($type='all', $limit=10, $offset=0) { global $USER; $userid = $USER->get('id'); $typesql = ''; if ($type != 'all') { // Treat as comma-separated list of activity type names $types = explode(',', preg_replace('/[^a-z,]+/', '', $type)); if ($types) { $typesql = ' at.name IN (' . join(',', array_map('db_quote', $types)) . ')'; if (in_array('adminmessages', $types)) { $typesql = '(' . $typesql . ' OR at.admin = 1)'; } $typesql = ' AND ' . $typesql; } } $from = " FROM {notification_internal_activity} a JOIN {activity_type} at ON a.type = at.id WHERE a.usr = ? $typesql"; $values = array($userid); $count = count_records_sql('SELECT COUNT(*)' . $from, $values); safe_require('module', 'multirecipientnotification'); if (PluginModuleMultirecipientnotification::is_active()) { $paginationurl = get_config('wwwroot') . 'module/multirecipientnotification/inbox.php?type=' . $type; } else { $paginationurl = get_config('wwwroot') . 'account/activity/index.php?type=' . $type; } $pagination = build_pagination(array( 'id' => 'activitylist_pagination', 'url' => $paginationurl, 'jsonscript' => 'account/activity/index.json.php', 'datatable' => 'activitylist', 'count' => $count, 'limit' => $limit, 'offset' => $offset, 'jumplinks' => 6, 'numbersincludeprevnext' => 2, )); $result = array( 'count' => $count, 'limit' => $limit, 'offset' => $offset, 'type' => $type, 'tablerows' => '', 'pagination' => $pagination['html'], 'pagination_js' => $pagination['javascript'], ); if ($count < 1) { return $result; } $records = get_records_sql_array(' SELECT a.*, at.name AS type, at.plugintype, at.pluginname' . $from . ' ORDER BY a.ctime DESC', $values, $offset, $limit ); if ($records) { foreach ($records as &$r) { $r->date = format_date(strtotime($r->ctime), 'strfdaymonthyearshort'); $section = empty($r->plugintype) ? 'activity' : "{$r->plugintype}.{$r->pluginname}"; $r->strtype = get_string('type' . $r->type, $section); $r->message = format_notification_whitespace($r->message); } } $smarty = smarty_core(); $smarty->assign('data', $records); $result['tablerows'] = $smarty->fetch('account/activity/activitylist.tpl'); return $result; } /** * Get a table of elements that can be used to set notification settings for the specified user, or for the site defaults. * * @param object $user whose settings are being displayed or... * @param bool $sitedefaults true if the elements should be loaded from the site default settings. * @return array of elements suitable for adding to a pieforms form. */ function get_notification_settings_elements($user = null, $sitedefaults = false) { global $SESSION; if ($user == null && !$sitedefaults) { throw new SystemException("Function get_notification_settings_elements requires a user or sitedefaults must be true"); } if ($sitedefaults || $user->get('admin')) { $activitytypes = get_records_array('activity_type', '', '', 'id'); } else { $activitytypes = get_records_array('activity_type', 'admin', 0, 'id'); $activitytypes = get_special_notifications($user, $activitytypes); } $notifications = plugins_installed('notification'); $elements = array(); $options = array(); foreach ($notifications as $notification) { $options[$notification->name] = get_string('name', 'notification.' . $notification->name); } $maildisabledmsg = false; foreach ($activitytypes as $type) { // Find the default value. if ($sitedefaults) { $dv = $type->defaultmethod; } else { $dv = $user->get_activity_preference($type->id); if ($dv === false) { $dv = $type->defaultmethod; } } if (empty($dv)) { $dv = 'none'; } // Create one maildisabled error message if applicable. if (!$sitedefaults && $dv == 'email' && !isset($maildisabledmsg) && get_account_preference($user->get('id'), 'maildisabled')) { $SESSION->add_error_msg(get_string('maildisableddescription', 'account', get_config('wwwroot') . 'account/index.php'), false); $maildisabledmsg = true; } // Calculate the key. if (empty($type->plugintype)) { $key = "activity_{$type->name}"; } else { $key = "activity_{$type->name}_{$type->plugintype}_{$type->pluginname}"; } // Find the row title and section. $rowtitle = $type->name; if (!empty($type->plugintype)) { $section = $type->plugintype . '.' . $type->pluginname; } else { $section = 'activity'; } // Create the element. $elements[$key] = array( 'defaultvalue' => $dv, 'type' => 'select', 'title' => get_string('type' . $rowtitle, $section), 'options' => $options, 'help' => true, ); // Set up the help. $elements[$key]['helpformname'] = 'activityprefs'; if (empty($type->plugintype)) { $elements[$key]['helpplugintype'] = 'core'; $elements[$key]['helppluginname'] = 'account'; } else { $elements[$key]['helpplugintype'] = $type->plugintype; $elements[$key]['helppluginname'] = $type->pluginname; } // Add the 'none' option if applicable. if ($type->allownonemethod) { $elements[$key]['options']['none'] = get_string('none'); } } $title = array(); foreach ($elements as $key => $row) { $title[$key] = $row['title']; } array_multisort($title, SORT_ASC, $elements); return $elements; } /** * Save the notification settings. * * @param array $values returned from submitting a pieforms form. * @param object $user whose settings are being updated or... * @param bool $sitedefaults true if the elements should be saved to the site default settings. */ function save_notification_settings($values, $user = null, $sitedefaults = false) { if ($user == null && !$sitedefaults) { throw new SystemException("Function save_notification_settings requires a user or sitedefaults must be true"); } if ($sitedefaults || $user->get('admin')) { $activitytypes = get_records_array('activity_type'); } else { $activitytypes = get_records_array('activity_type', 'admin', 0); $activitytypes = get_special_notifications($user, $activitytypes); } foreach ($activitytypes as $type) { if (empty($type->plugintype)) { $key = "activity_{$type->name}"; } else { $key = "activity_{$type->name}_{$type->plugintype}_{$type->pluginname}"; } $value = $values[$key] == 'none' ? null : $values[$key]; if ($sitedefaults) { execute_sql("UPDATE {activity_type} SET defaultmethod = ? WHERE id = ?", array($value, $type->id)); } else { $user->set_activity_preference($type->id, $value); } } } /** * Get special case activity types. * Currently checks if a non admin is an admin/moderator of a group and * adds that notification type to the array. * * @param object $user whose settings are being displayed * @param array $activitytypes array of elements * @return array $activitytypes amended array of elements */ function get_special_notifications($user, $activitytypes) { if (empty($user)) { return $activitytypes; } // Check if the non-admin is a group admin/moderator in any of their groups if ($user->get('grouproles') !== null) { $groups = $user->get('grouproles'); $allowreportpost = false; foreach ($groups as $group => $role) { if ($role == 'admin') { $allowreportpost = true; break; } else if ($moderator = get_record_sql("SELECT i.id FROM {interaction_forum_moderator} m, {interaction_instance} i WHERE i.id = m.forum AND i.group = ? AND i.deleted = 0 and m.user = ?", array($group, $user->get('id')))) { $allowreportpost = true; break; } } if ($allowreportpost) { // Add the reportpost option to the $activitytypes $reportpost = get_records_array('activity_type', 'name', 'reportpost', 'id'); $activitytypes = array_merge($activitytypes, $reportpost); } } // If user is an institution admin, should receive objectionable material notifications if ($user->is_institutional_admin()) { $objectionable = get_records_array('activity_type', 'name', 'objectionable', 'id'); $activitytypes = array_merge($activitytypes, $objectionable); } return $activitytypes; }