activity.php 65.2 KB
Newer Older
1
2
3
4
5
<?php
/**
 *
 * @package    mahara
 * @subpackage core
6
 * @author     Catalyst IT Ltd
7
8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9
10
11
12
13
14
15
16
 *
 */

defined('INTERNAL') || die();

/**
 * This is the function to call whenever anything happens
 * that is going to end up on a user's activity page.
Aaron Wells's avatar
Aaron Wells committed
17
 *
18
 * @param string $activitytype type of activity
19
20
21
22
 * @param array $data must contain the fields specified by get_required_parameters of the activity type subclass.
 * @param string $plugintype
 * @param string $pluginname
 * @param bool $delay
23
 */
24
function activity_occurred($activitytype, $data, $plugintype=null, $pluginname=null, $delay=null) {
25
26
27
28
29
30
    try {
        $at = activity_locate_typerecord($activitytype, $plugintype, $pluginname);
    }
    catch (Exception $e) {
        return;
    }
31
32
33
34
    if (is_null($delay)) {
        $delay = !empty($at->delay);
    }
    if ($delay) {
35
        $delayed = new StdClass;
36
        $delayed->type = $at->id;
37
38
39
40
41
        $delayed->data = serialize($data);
        $delayed->ctime = db_format_timestamp(time());
        insert_record('activity_queue', $delayed);
    }
    else {
42
        handle_activity($at, $data);
43
    }
44
45
}

Aaron Wells's avatar
Aaron Wells committed
46
47
48
/**
 * This function dispatches all the activity stuff to whatever notification
 * plugin it needs to, and figures out all the implications of activity and who
49
 * needs to know about it.
Aaron Wells's avatar
Aaron Wells committed
50
 *
51
 * @param object $activitytype record from database table activity_type
52
 * @param mixed $data must contain message to save.
Aaron Wells's avatar
Aaron Wells committed
53
 * each activity type has different requirements of $data -
54
 *  - <b>viewaccess</b> must contain $owner userid of view owner AND $view (id of view) and $oldusers array of userids before access change was committed.
55
56
57
58
 * @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
59
 */
60
function handle_activity($activitytype, $data, $cron=false, $queuedactivity=null) {
61
    $data = (object)$data;
62

63
64
65
66
67
    if ($cron && isset($queuedactivity)) {
        $data->last_processed_userid = $queuedactivity->last_processed_userid;
        $data->activity_queue_id = $queuedactivity->id;
    }

68
    $classname = get_activity_type_classname($activitytype);
69
    $activity = new $classname($data, $cron);
70
    if (!$activity->any_users()) {
71
        return 0;
72
    }
73

74
    return $activity->notify_users();
75
76
}

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
 * 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;
}

97
/**
Aaron Wells's avatar
Aaron Wells committed
98
 * this function returns an array of users who subsribe to a particular activitytype
99
 * including the notification method they are using to subscribe to it.
100
 *
101
 * @param int $activitytype the id of the activity type
102
103
104
 * @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
105
 * @param array $admininstitutions list of institution names to get admins for
106
107
 * @return array of users
 */
108
109
function activity_get_users($activitytype, $userids=null, $userobjs=null, $adminonly=false,
                            $admininstitutions = array()) {
110
    $values = array($activitytype);
111
112
    $sql = '
        SELECT
Aaron Wells's avatar
Aaron Wells committed
113
            u.id, u.username, u.firstname, u.lastname, u.preferredname, u.email, u.admin, u.staff,
114
115
            p.method, ap.value AS lang, apm.value AS maildisabled, aic.value AS mnethostwwwroot,
            h.appname AS mnethostapp
116
117
        FROM {usr} u
        LEFT JOIN {usr_activity_preference} p
118
            ON (p.usr = u.id AND p.activity = ?)' . (empty($admininstitutions) ? '' : '
119
120
121
        LEFT OUTER JOIN {usr_institution} ui
            ON (u.id = ui.usr
                AND ui.institution IN ('.join(',',array_map('db_quote',$admininstitutions)).'))') . '
122
123
        LEFT OUTER JOIN {usr_account_preference} ap
            ON (ap.usr = u.id AND ap.field = \'lang\')
124
        LEFT OUTER JOIN {usr_account_preference} apm
125
            ON (apm.usr = u.id AND apm.field = \'maildisabled\')
126
127
128
129
130
131
        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
132
        WHERE u.deleted = 0';
133
134
135
    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));
Aaron Wells's avatar
Aaron Wells committed
136
    }
137
138
139
140
    else if (!empty($userids) && is_array($userids)) {
        $sql .= ' AND u.id IN (' . implode(',',db_array_to_ph($userids)) . ')';
        $values = array_merge($values, $userids);
    }
141
142
143
    if (!empty($admininstitutions)) {
        $sql .= '
        GROUP BY
144
            u.id, u.username, u.firstname, u.lastname, u.preferredname, u.email, u.admin, u.staff,
145
            p.method, ap.value, apm.value, aic.value, h.appname
146
147
148
149
        HAVING (u.admin = 1 OR SUM(ui.admin) > 0)';
    } else if ($adminonly) {
        $sql .= ' AND u.admin = 1';
    }
Aaron Wells's avatar
Aaron Wells committed
150
    return get_records_sql_assoc($sql, $values);
151
152
}

153

154
155
156
157
/**
 * this function inserts a default set of activity preferences for a given user
 * id
 */
158
159
function activity_set_defaults($eventdata) {
    $user_id = $eventdata['id'];
160
    $activitytypes = get_records_array('activity_type', 'admin', 0);
161

162
    foreach ($activitytypes as $type) {
163
164
        insert_record('usr_activity_preference', (object)array(
            'usr' => $user_id,
Richard Mansfield's avatar
Richard Mansfield committed
165
            'activity' => $type->id,
166
            'method' => $type->defaultmethod,
167
        ));
168
169
170
    }
}

171
172
function activity_add_admin_defaults($userids) {
    $activitytypes = get_records_array('activity_type', 'admin', 1);
173

174
175
    foreach ($activitytypes as $type) {
        foreach ($userids as $id) {
176
            if (!record_exists('usr_activity_preference', 'usr', $id, 'activity', $type->id)) {
177
178
                insert_record('usr_activity_preference', (object)array(
                    'usr' => $id,
179
                    'activity' => $type->id,
180
                    'method' => $type->defaultmethod,
181
182
183
184
185
186
187
                ));
            }
        }
    }
}


188
189
function activity_process_queue() {

190
    if ($toprocess = get_records_array('activity_queue')) {
191
192
193
        // Hack to avoid duplicate watchlist notifications on the same view
        $watchlist = activity_locate_typerecord('watchlist');
        $viewsnotified = array();
194
        foreach ($toprocess as $activity) {
195
196
197
198
199
200
201
            $data = unserialize($activity->data);
            if ($activity->type == $watchlist->id && !empty($data->view)) {
                if (isset($viewsnotified[$data->view])) {
                    continue;
                }
                $viewsnotified[$data->view] = true;
            }
202

203
            try {
204
                $last_processed_userid = handle_activity($activity->type, $data, true, $activity);
205
206
            }
            catch (MaharaException $e) {
Aaron Wells's avatar
Aaron Wells committed
207
                // Exceptions can happen while processing the queue, we just
208
209
210
                // log them and continue
                log_debug($e->getMessage());
            }
211
212
213
214
215
216
217
218
219
220
221
222
223
            // 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.");
                }
            }
224
225
226
        }
    }
}
227

228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/**
 * 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) {
245
246
247
248
249
        $viewid = $event->get('view');
        if ($viewid) {
            set_field('view', 'mtime', db_format_timestamp(time()), 'id', $viewid);
        }
        if (record_exists('usr_watchlist_view', 'view', $viewid)) {
250
251
            $whereobj = new stdClass();
            $whereobj->block = $event->get('id');
252
            $whereobj->view = $viewid;
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
            $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) {
276
277
278
279
280
            $viewid = $rel->view;
            if ($viewid) {
                set_field('view', 'mtime', db_format_timestamp(time()), 'id', $viewid);
            }
            if (!record_exists('usr_watchlist_view', 'view', $viewid)) {
281
282
283
284
                continue;
            }
            $whereobj = new stdClass();
            $whereobj->block = $rel->block;
285
            $whereobj->view = $viewid;
286
287
288
289
290
291
292
293
            $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'];
294
295
296
        if ($viewid) {
            set_field('view', 'mtime', db_format_timestamp(time()), 'id', $viewid);
        }
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
        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;
        }
382
383
384
385
386
387
388
        // 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;
        }
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471

        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);
    }
}

472
function activity_get_viewaccess_users($view) {
473
    require_once(get_config('docroot') . 'lib/group.php');
474
475
    $sql = "SELECT userlist.userid, usr.*, actpref.method, accpref.value AS lang,
              aic.value AS mnethostwwwroot, h.appname AS mnethostapp
476
                FROM (
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
                    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
493
494
495
496
497
498
499
                      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')
                      )
500
                ) AS userlist
501
502
503
                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'
504
505
506
507
                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";
508
    $values = array($view, $view, $view, $view);
509
    if (!$u = get_records_sql_assoc($sql, $values)) {
510
511
512
        $u = array();
    }
    return $u;
513
514
}

515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
/**
 * 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);
}

559
560
561
562
563
564
565
566
567
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)) {
Aaron Wells's avatar
Aaron Wells committed
568
569
            $at = get_record_select('activity_type',
                'name = ? AND plugintype IS NULL AND pluginname IS NULL',
570
                array($activitytype));
Aaron Wells's avatar
Aaron Wells committed
571
        }
572
573
574
575
576
        else {
            $at = get_record('activity_type', 'name', $activitytype, 'plugintype', $plugintype, 'pluginname', $pluginname);
        }
    }
    if (empty($at)) {
577
        throw new SystemException("Invalid activity type $activitytype");
578
579
580
    }
    return $at;
}
581

582
583
584
585
586
587
588
589
590
591
592
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;
}

593
594
595
596
597
598
599
600
601
/**
 * 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.
 */
602
abstract class ActivityType {
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626

    /**
     * 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();

627
628
629
630
631
    /**
     * The number of users in a split chunk to notify
     */
    const USERCHUNK_SIZE = 1000;

632
633
634
635
636
637
    /**
     * Who any notifications about this activity should appear to come from
     */
    protected $fromuser;

    /**
Aaron Wells's avatar
Aaron Wells committed
638
639
     * 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
640
641
642
643
     * the 'noreply' address)
     */
    protected $hideemail = true;

644
645
    protected $subject;
    protected $message;
646
    protected $strings;
647
648
    protected $users = array();
    protected $url;
649
    protected $urltext;
650
    protected $id;
651
652
    protected $type;
    protected $activityname;
653
    protected $cron;
654
655
    protected $last_processed_userid;
    protected $activity_queue_id;
656
    protected $overridemessagecontents;
Evan Goldenberg's avatar
Evan Goldenberg committed
657
    protected $parent;
658
    protected $defaultmethod;
659

660
661
662
663
664
665
666
    public function get_id() {
        if (!isset($this->id)) {
            $tmp = activity_locate_typerecord($this->get_type());
            $this->id = $tmp->id;
        }
        return $this->id;
    }
667
668
669
670
671
672
673
674
675

    public function get_default_method() {
        if (!isset($this->defaultmethod)) {
            $tmp = activity_locate_typerecord($this->get_id());
            $this->defaultmethod = $tmp->defaultmethod;
        }
        return $this->defaultmethod;
    }

676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
    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;
            }
694
        }
695
696
697
698
699
    }

    private function ensure_parameters() {
        foreach ($this->get_required_parameters() as $param) {
            if (!isset($this->{$param})) {
700
701
702
703
                // 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()));
                }
704
705
706
707
708
            }
        }
    }

    public function to_stdclass() {
Aaron Wells's avatar
Aaron Wells committed
709
       return (object)get_object_vars($this);
710
    }
711

712
    public function get_string_for_user($user, $string) {
713
        if (empty($string) || empty($this->strings->{$string}->key)) {
714
715
            return;
        }
716
717
718
719
        $args = array_merge(
            array(
                $user->lang,
                $this->strings->{$string}->key,
720
                empty($this->strings->{$string}->section) ? 'mahara' : $this->strings->{$string}->section,
721
            ),
722
            empty($this->strings->{$string}->args) ? array() : $this->strings->{$string}->args
723
724
725
726
        );
        return call_user_func_array('get_string_from_language', $args);
    }

727
728
    // Optional string to use for the link text.
    public function add_urltext(array $stringdef) {
729
        $def = $stringdef;
730
731
732
        if (!is_object($this->strings)) {
            $this->strings = new stdClass();
        }
733
        $this->strings->urltext = (object) $def;
734
735
    }

736
737
738
739
740
741
742
    public function get_urltext($user) {
        if (empty($this->urltext)) {
            return $this->get_string_for_user($user, 'urltext');
        }
        return $this->urltext;
    }

743
    public function get_message($user) {
744
745
746
        if (empty($this->message)) {
            return $this->get_string_for_user($user, 'message');
        }
747
748
        return $this->message;
    }
Aaron Wells's avatar
Aaron Wells committed
749

750
    public function get_subject($user) {
751
752
753
        if (empty($this->subject)) {
            return $this->get_string_for_user($user, 'subject');
        }
754
755
        return $this->subject;
    }
756

757
758
759
760
761
762
763
764
765
    /**
     * 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) {
766
767
768
        return false;
    }

769
    public function notify_user($user) {
770
        $changes = new stdClass;
771

772
773
774
775
776
777
778
779
780
        $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)) {
781
            // If method is not set then either the user has selected 'none' or their setting has not been set (so use default).
782
783
784
785
786
787
            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;
                }
788
            }
789
790
791
792
793
794
            else {
                $user->method = $this->get_default_method();
                if (empty($user->method)) {
                    // The default notification type is 'none' for this activity type.
                    return;
                }
795
            }
796
        }
797
798

        // always do internal
799
800
801
802
803
804
        foreach (PluginNotificationInternal::$userdata as &$p) {
            $function = 'get_' . $p;
            $userdata->$p = $this->$function($user);
        }

        $userdata->internalid = PluginNotificationInternal::notify_user($user, $userdata);
805
        if ($this->update_url($userdata->internalid)) {
806
            $changes->url = $userdata->url = $this->url;
807
808
        }

809
810
811
812
813
814
        if ($user->method != 'internal' || isset($changes->url)) {
            $changes->read = (int) ($user->method != 'internal');
            $changes->id = $userdata->internalid;
            update_record('notification_internal_activity', $changes);
        }

815
816
        if ($user->method != 'internal') {
            $method = $user->method;
817
            safe_require('notification', $method);
818
819
820
821
822
823
824
825
826
827
            $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);
                    }
                }
            }
828
            try {
829
                call_static_method($notificationclass, 'notify_user', $user, $userdata);
830
            }
831
            catch (MaharaException $e) {
832
                static $badnotification = false;
833
                static $adminnotified = array();
834
                // We don't mind other notification methods failing, as it'll
835
                // go into the activity log as 'unread'
836
837
                $changes->read = 0;
                update_record('notification_internal_activity', $changes);
838
                if (!$badnotification && !($e instanceof EmailDisabledException || $e instanceof InvalidEmailException)) {
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
                    // 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;
                    }
854
                }
855
856
            }
        }
857

858
859
        // The user's unread message count does not need to be updated from $changes->read
        // because of the db trigger on notification_internal_activity.
860
861
    }

862
863
864
865
866
867
    /**
     * 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.
     */
868
    public function notify_users() {
869
        safe_require('notification', 'internal');
870
871
        $this->type = $this->get_id();

872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
        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 {
891
                    break;
892
893
                }
            }
894
            return $last_processed_userid;
895
896
897
898
899
900
        }
        else {
            while (!empty($this->users)) {
                $user = array_shift($this->users);
                $this->notify_user($user);
            }
901
        }
902
        return 0;
903
    }
904
}
905

Aaron Wells's avatar
Aaron Wells committed
906
abstract class ActivityTypeAdmin extends ActivityType {
907

908
909
    public function __construct($data, $cron=false) {
        parent::__construct($data, $cron);
910
        $this->users = activity_get_users($this->get_id(), null, null, true);
911
912
913
914
    }
}

class ActivityTypeContactus extends ActivityTypeAdmin {
Aaron Wells's avatar
Aaron Wells committed
915

916
917
    protected $fromname;
    protected $fromemail;
918
919
920
921
922
923
924
925
926
927
    protected $hideemail = false;

    /**
     * @param array $data Parameters:
     *                    - message (string)
     *                    - subject (string) (optional)
     *                    - fromname (string)
     *                    - fromaddress (email address)
     *                    - fromuser (int) (if a logged in user)
     */
Aaron Wells's avatar
Aaron Wells committed
928
    function __construct($data, $cron=false) {
929
        parent::__construct($data, $cron);
930
        if (!empty($this->fromuser)) {
931
            $this->url = profile_url($this->fromuser, false);
932
        }
933
934
935
936
937
        else {
            $this->customheaders = array(
                'Reply-to: ' . $this->fromname . ' <' . $this->fromemail . '>',
            );
        }
938
    }
939
940
941
942
943
944

    function get_subject($user) {
        return get_string_from_language($user->lang, 'newcontactus', 'activity');
    }

    function get_message($user) {
Aaron Wells's avatar
Aaron Wells committed
945
        return get_string_from_language($user->lang, 'newcontactusfrom', 'activity') . ' ' . $this->fromname
946
            . ' <' . $this->fromemail .'>' . (isset($this->subject) ? ': ' . $this->subject : '')
947
948
949
            . "\n\n" . $this->message;
    }

950
951
952
953
954
955
956
957
958
    public function get_required_parameters() {
        return array('message', 'fromname', 'fromemail');
    }
}

class ActivityTypeObjectionable extends ActivityTypeAdmin {

    protected $view;
    protected $artefact;
959
    protected $reporter;
960
    protected $ctime;
961

962
963
964
965
966
967
    /**
     * @param array $data Parameters:
     *                    - message (string)
     *                    - view (int)
     *                    - artefact (int) (optional)
     *                    - reporter (int)
968
     *                    - ctime (int) (optional)
969
     */
Aaron Wells's avatar
Aaron Wells committed
970
    function __construct($data, $cron=false) {
971
        parent::__construct($data, $cron);
972

973
974
975
976
977
978
979
980
981
        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')) {
982
            // Notify institutional admins of the view owner
983
            if ($institutions = get_column('usr_institution', 'institution', 'usr', $owner)) {
984
985
986
987
                $this->users = activity_get_users($this->get_id(), null, null, null, $institutions);
            }
        }

988
        if (empty($this->artefact)) {
989
            $this->url = $this->view->get_url(false, true) . '&objection=1';
990
991
        }
        else {
992
            $this->url = 'artefact/artefact.php?artefact=' . $this->artefact->get('id') . '&view=' . $this->view->get('id') . '&objection=1';
993
        }
994

995
        if (empty($this->strings->subject)) {
996
            $this->overridemessagecontents = true;
997
            $viewtitle = $this->view->get('title');
998
            $this->strings = new stdClass();
999
1000
1001
1002
1003
1004
1005
1006
            if (empty($this->artefact)) {
                $this->strings->subject = (object) array(
                    'key'     => 'objectionablecontentview',
                    'section' => 'activity',
                    'args'    => array($viewtitle, display_default_name($this->reporter)),
                );
            }
            else {
1007
                $title = $this->artefact->get('title');
1008
1009
1010
1011
1012
                $this->strings->subject = (object) array(
                    'key'     => 'objectionablecontentviewartefact',
                    'section' => 'activity',
                    'args'    => array($viewtitle, $title, display_default_name($this->reporter)),
                );
1013
1014
1015
1016
            }
        }
    }

1017
    public function get_emailmessage($user) {
1018
        $reporterurl = profile_url($this->reporter);
1019
1020
1021
1022
1023
        $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,
1024
                $this->message, $this->view->get_url(true, true) . "&objection=1", $reporterurl
1025
1026
1027
1028
1029
1030
            );
        }
        else {
            return get_string_from_language(
                $user->lang, 'objectionablecontentviewartefacttext', 'activity',
                $this->view->get('title'), $this->artefact->get('title'), display_default_name($this->reporter), $ctime,
1031
                $this->message, get_config('wwwroot') . "artefact/artefact.php?artefact=" . $this->artefact->get('id') . "&view=" . $this->view->get('id') . "&objection=1", $reporterurl
1032
1033
1034
1035
1036
1037
1038
            );
        }
    }

    public function get_htmlmessage($user) {
        $viewtitle = hsc($this->view->get('title'));
        $reportername = hsc(display_default_name($this->reporter));
1039
        $reporterurl = profile_url($this->reporter);
1040
1041
1042
1043
1044
1045
        $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,
1046
                $message, $this->view->get_url(true, true) . "&objection=1", $viewtitle,
1047
1048
1049
1050
1051
1052
1053
                $reporterurl, $reportername
            );
        }
        else {
            return get_string_from_language(
                $user->lang, 'objectionablecontentviewartefacthtml', 'activity',
                $viewtitle, hsc($this->artefact->get('title')), $reportername, $ctime,
1054
                $message, get_config('wwwroot') . "artefact/artefact.php?artefact=" . $this->artefact->get('id') . "&view=" . $this->view->get('id') . "&objection=1", hsc($this->artefact->get('title')),
1055
1056
1057
1058
1059
                $reporterurl, $reportername
            );
        }
    }

1060
    public function get_required_parameters() {
1061
        return array('message', 'view', 'reporter');
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
    }

}

class ActivityTypeVirusRepeat extends ActivityTypeAdmin {

    protected $username;
    protected $fullname;
    protected $userid;

Aaron Wells's avatar
Aaron Wells committed
1072
    public function __construct($data, $cron=false) {
1073
        parent::__construct($data, $cron);
1074
1075
1076
    }

    public function get_subject($user) {
1077
        $userstring = $this->username . ' (' . $this->fullname . ') (userid:' . $this->userid . ')' ;
1078
1079
1080
1081
1082
        return get_string_from_language($user->lang, 'virusrepeatsubject', 'mahara', $userstring);
    }

    public function get_message($user) {
        return get_string_from_language($user->lang, 'virusrepeatmessage');
1083
1084
1085
1086
1087
1088
1089
1090
1091
    }

    public function get_required_parameters() {
        return array('username', 'fullname', 'userid');
    }
}

class ActivityTypeVirusRelease extends ActivityTypeAdmin {

Aaron Wells's avatar
Aaron Wells committed
1092
    public function __construct($data, $cron=false) {
1093
        parent::__construct($data, $cron);
1094
1095
1096
1097
1098
1099
1100
1101
1102
    }

    public function get_required_parameters() {
        return array();
    }
}

class ActivityTypeMaharamessage extends ActivityType {

1103
1104
1105
1106
1107
1108
    /**
     * @param array $data Parameters:
     *                    - subject (string)
     *                    - message (string)
     *                    - users (list of user ids)
     */
Aaron Wells's avatar
Aaron Wells committed
1109
    public function __construct($data, $cron=false) {
1110
        parent::__construct($data, $cron);
1111
        $this->users = activity_get_users($this->get_id(), $this->users);
1112
1113
1114
1115
1116
1117
1118
    }

    public function get_required_parameters() {
        return array('message', 'subject', 'users');
    }
}

1119
1120
1121
1122
1123
1124
1125
class ActivityTypeInstitutionmessage extends ActivityType {

    protected $messagetype;
    protected $institution;
    protected $username;
    protected $fullname;

1126
1127
    public function __construct($data, $cron=false) {
        parent::__construct($data, $cron);
1128
        if ($this->messagetype == 'request') {
1129
            $this->url = 'admin/users/institutionusers.php';
1130
1131
            $this->users = activity_get_users($this->get_id(), null, null, null,
                                              array($this->institution->name));
1132
            $this->add_urltext(array('key' => 'institutionmembers', 'section' => 'admin'));
1133
        } else if ($this->messagetype == 'invite') {
1134
            $this->url = 'account/institutions.php';
1135
            $this->users = activity_get_users($this->get_id(), $this->users);
1136
            $this->add_urltext(array('key' => 'institutionmembership', 'section' => 'mahara'));
1137
1138
1139
        }
    }

1140
1141
1142
    public function get_subject($user) {
        if ($this->messagetype == 'request') {
            $userstring = $this->fullname . ' (' . $this->username . ')';
Aaron Wells's avatar
Aaron Wells committed
1143
            return get_string_from_language($user->lang, 'institutionrequestsubject', 'activity', $userstring,
1144
1145
                                            $this->institution->displayname);
        } else if ($this->messagetype == 'invite') {
Aaron Wells's avatar
Aaron Wells committed
1146
            return get_string_from_language($user->lang, 'institutioninvitesubject', 'activity',
1147
1148
1149
1150
1151
1152
                                            $this->institution->displayname);
        }
    }

    public function get_message($user) {
        if ($this->messagetype == 'request') {
1153
            return $this->get_subject($user) .' '. get_string_from_language($user->lang, 'institutionrequestmessage', 'activity', $this->url);
1154
        } else if ($this->messagetype == 'invite') {
1155
            return $this->get_subject($user) .' '. get_string_from_language($user->lang, 'institutioninvitemessage', 'activity', $this->url);
1156
1157
1158
        }
    }

1159
1160
1161
1162
1163
    public function get_required_parameters() {
        return array('messagetype', 'institution');
    }
}

Aaron Wells's avatar
Aaron Wells committed
1164
class ActivityTypeUsermessage extends ActivityType {
1165
1166
1167
1168

    protected $userto;
    protected $userfrom;

1169
1170
1171
1172
1173
1174
    /**
     * @param array $data Parameters:
     *                    - userto (int)
     *                    - userfrom (int)
     *                    - subject (string)
     *                    - message (string)
Evan Goldenberg's avatar
Evan Goldenberg committed
1175
     *                    - parent (int)
1176
     */
Aaron Wells's avatar
Aaron Wells committed
1177
    public function __construct($data, $cron=false) {
1178
        parent::__construct($data, $cron);
1179
1180
1181
        if ($this->userfrom) {
            $this->fromuser = $this->userfrom;
        }
1182
        $this->users = activity_get_users($this->get_id(), array($this->userto));
1183
1184
1185
1186
        $this->add_urltext(array(
            'key'     => 'Reply',
            'section' => 'group',
        ));
Aaron Wells's avatar
Aaron Wells committed
1187
    }
1188

1189
1190
    public function get_subject($user) {
        if (empty($this->subject)) {
Clare Lenihan's avatar
Clare Lenihan committed
1191
            return get_string_from_language($user->lang, 'newusermessage', 'group',
1192
1193
1194
1195
1196
                                            display_name($this->userfrom));
        }
        return $this->subject;
    }

1197
    protected function update_url($internalid) {
1198
        $this->url = 'user/sendmessage.php?id=' . $this->userfrom . '&replyto=' . $internalid . '&returnto=inbox';
1199
1200
1201
        return true;
    }

1202
1203
1204
    public function get_required_parameters() {
        return array('message', 'userto', 'userfrom');
    }
Aaron Wells's avatar
Aaron Wells committed
1205

1206
1207
}

Aaron Wells's avatar
Aaron Wells committed
1208
class ActivityTypeWatchlist extends ActivityType {
1209
1210
1211

    protected $view;

1212
1213
    protected $ownerinfo;
    protected $viewinfo;