1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Class to manage subscriptions.
19 *
20 * @package    tool_monitor
21 * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace tool_monitor;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Class to manage subscriptions.
31 *
32 * @since      Moodle 2.8
33 * @package    tool_monitor
34 * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
35 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 */
37class subscription_manager {
38
39    /** @const Period of time, in days, after which an inactive subscription will be removed completely.*/
40    const INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS = 30;
41
42    /**
43     * Subscribe a user to a given rule.
44     *
45     * @param int $ruleid  Rule id.
46     * @param int $courseid Course id.
47     * @param int $cmid Course module id.
48     * @param int $userid User who is subscribing, defaults to $USER.
49     *
50     * @return bool|int returns id of the created subscription.
51     */
52    public static function create_subscription($ruleid, $courseid, $cmid, $userid = 0) {
53        global $DB, $USER;
54
55        $subscription = new \stdClass();
56        $subscription->ruleid = $ruleid;
57        $subscription->courseid = $courseid;
58        $subscription->cmid = $cmid;
59        $subscription->userid = empty($userid) ? $USER->id : $userid;
60        if ($DB->record_exists('tool_monitor_subscriptions', (array)$subscription)) {
61            // Subscription already exists.
62            return false;
63        }
64
65        $subscription->timecreated = time();
66        $subscription->id = $DB->insert_record('tool_monitor_subscriptions', $subscription);
67
68        // Trigger a subscription created event.
69        if ($subscription->id) {
70            if (!empty($subscription->courseid)) {
71                $courseid = $subscription->courseid;
72                $context = \context_course::instance($subscription->courseid);
73            } else {
74                $courseid = 0;
75                $context = \context_system::instance();
76            }
77
78            $params = array(
79                'objectid' => $subscription->id,
80                'courseid' => $courseid,
81                'context' => $context
82            );
83            $event = \tool_monitor\event\subscription_created::create($params);
84            $event->trigger();
85
86            // Let's invalidate the cache.
87            $cache = \cache::make('tool_monitor', 'eventsubscriptions');
88            $cache->delete($courseid);
89        }
90
91        return $subscription->id;
92    }
93
94    /**
95     * Delete a subscription.
96     *
97     * @param subscription|int $subscriptionorid an instance of subscription class or id.
98     * @param bool $checkuser Check if the subscription belongs to current user before deleting.
99     *
100     * @return bool
101     * @throws \coding_exception if $checkuser is true and the subscription doesn't belong to the current user.
102     */
103    public static function delete_subscription($subscriptionorid, $checkuser = true) {
104        global $DB, $USER;
105        if (is_object($subscriptionorid)) {
106            $subscription = $subscriptionorid;
107        } else {
108            $subscription = self::get_subscription($subscriptionorid);
109        }
110        if ($checkuser && $subscription->userid != $USER->id) {
111            throw new \coding_exception('Invalid subscription supplied');
112        }
113
114        // Store the subscription before we delete it.
115        $subscription = $DB->get_record('tool_monitor_subscriptions', array('id' => $subscription->id));
116
117        $success = $DB->delete_records('tool_monitor_subscriptions', array('id' => $subscription->id));
118
119        // If successful trigger a subscription_deleted event.
120        if ($success) {
121            if (!empty($subscription->courseid) &&
122                    ($coursecontext = \context_course::instance($subscription->courseid, IGNORE_MISSING))) {
123                $courseid = $subscription->courseid;
124                $context = $coursecontext;
125            } else {
126                $courseid = 0;
127                $context = \context_system::instance();
128            }
129
130            $params = array(
131                'objectid' => $subscription->id,
132                'courseid' => $courseid,
133                'context' => $context
134            );
135            $event = \tool_monitor\event\subscription_deleted::create($params);
136            $event->add_record_snapshot('tool_monitor_subscriptions', $subscription);
137            $event->trigger();
138
139            // Let's invalidate the cache.
140            $cache = \cache::make('tool_monitor', 'eventsubscriptions');
141            $cache->delete($courseid);
142        }
143
144        return $success;
145    }
146
147    /**
148     * Delete all subscriptions for a user.
149     *
150     * @param int $userid user id.
151     *
152     * @return mixed
153     */
154    public static function delete_user_subscriptions($userid) {
155        global $DB;
156        return $DB->delete_records('tool_monitor_subscriptions', array('userid' => $userid));
157    }
158
159    /**
160     * Delete all subscriptions for a course module.
161     *
162     * @param int $cmid cm id.
163     *
164     * @return mixed
165     */
166    public static function delete_cm_subscriptions($cmid) {
167        global $DB;
168        return $DB->delete_records('tool_monitor_subscriptions', array('cmid' => $cmid));
169    }
170
171    /**
172     * Delete all subscribers for a given rule.
173     *
174     * @param int $ruleid rule id.
175     * @param \context|null $coursecontext the context of the course - this is passed when we
176     *      can not get the context via \context_course as the course has been deleted.
177     *
178     * @return bool
179     */
180    public static function remove_all_subscriptions_for_rule($ruleid, $coursecontext = null) {
181        global $DB;
182
183        // Store all the subscriptions we have to delete.
184        $subscriptions = $DB->get_recordset('tool_monitor_subscriptions', array('ruleid' => $ruleid));
185
186        // Now delete them.
187        $success = $DB->delete_records('tool_monitor_subscriptions', array('ruleid' => $ruleid));
188
189        // If successful and there were subscriptions that were deleted trigger a subscription deleted event.
190        if ($success && $subscriptions) {
191            foreach ($subscriptions as $subscription) {
192                // It is possible that we are deleting rules associated with a deleted course, so we should be
193                // passing the context as the second parameter.
194                if (!is_null($coursecontext)) {
195                    $context = $coursecontext;
196                    $courseid = $subscription->courseid;
197                } else if (!empty($subscription->courseid) && ($coursecontext =
198                        \context_course::instance($subscription->courseid, IGNORE_MISSING))) {
199                    $courseid = $subscription->courseid;
200                    $context = $coursecontext;
201                } else {
202                    $courseid = 0;
203                    $context = \context_system::instance();
204                }
205
206                $params = array(
207                    'objectid' => $subscription->id,
208                    'courseid' => $courseid,
209                    'context' => $context
210                );
211                $event = \tool_monitor\event\subscription_deleted::create($params);
212                $event->add_record_snapshot('tool_monitor_subscriptions', $subscription);
213                $event->trigger();
214
215                // Let's invalidate the cache.
216                $cache = \cache::make('tool_monitor', 'eventsubscriptions');
217                $cache->delete($courseid);
218            }
219        }
220
221        $subscriptions->close();
222
223        return $success;
224    }
225
226    /**
227     * Delete all subscriptions in a course.
228     *
229     * This is called after a course was deleted, context no longer exists but we kept the object
230     *
231     * @param \context_course $coursecontext the context of the course
232     */
233    public static function remove_all_subscriptions_in_course($coursecontext) {
234        global $DB;
235
236        // Store all the subscriptions we have to delete.
237        if ($subscriptions = $DB->get_records('tool_monitor_subscriptions', ['courseid' => $coursecontext->instanceid])) {
238            // Delete subscriptions in bulk.
239            $DB->delete_records('tool_monitor_subscriptions', ['courseid' => $coursecontext->instanceid]);
240
241            // Trigger events one by one.
242            foreach ($subscriptions as $subscription) {
243                $params = ['objectid' => $subscription->id, 'context' => $coursecontext];
244                $event = \tool_monitor\event\subscription_deleted::create($params);
245                $event->add_record_snapshot('tool_monitor_subscriptions', $subscription);
246                $event->trigger();
247            }
248        }
249    }
250
251    /**
252     * Get a subscription instance for an given subscription id.
253     *
254     * @param subscription|int $subscriptionorid an instance of subscription class or id.
255     *
256     * @return subscription returns a instance of subscription class.
257     */
258    public static function get_subscription($subscriptionorid) {
259        global $DB;
260
261        if (is_object($subscriptionorid)) {
262            return new subscription($subscriptionorid);
263        }
264
265        $sql = self::get_subscription_join_rule_sql();
266        $sql .= "WHERE s.id = :id";
267        $sub = $DB->get_record_sql($sql, array('id' => $subscriptionorid), MUST_EXIST);
268        return new subscription($sub);
269    }
270
271    /**
272     * Get an array of subscriptions for a given user in a given course.
273     *
274     * @param int $courseid course id.
275     * @param int $limitfrom Limit from which to fetch rules.
276     * @param int $limitto  Limit to which rules need to be fetched.
277     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
278     * @param string $order Order to sort the subscriptions.
279     *
280     * @return array list of subscriptions
281     */
282    public static function get_user_subscriptions_for_course($courseid, $limitfrom = 0, $limitto = 0, $userid = 0,
283            $order = 's.timecreated DESC' ) {
284        global $DB, $USER;
285        if ($userid == 0) {
286            $userid = $USER->id;
287        }
288        $sql = self::get_subscription_join_rule_sql();
289        $sql .= "WHERE s.courseid = :courseid AND s.userid = :userid ORDER BY $order";
290
291        return self::get_instances($DB->get_records_sql($sql, array('courseid' => $courseid, 'userid' => $userid), $limitfrom,
292                $limitto));
293    }
294
295    /**
296     * Get count of subscriptions for a given user in a given course.
297     *
298     * @param int $courseid course id.
299     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
300     *
301     * @return int number of subscriptions
302     */
303    public static function count_user_subscriptions_for_course($courseid, $userid = 0) {
304        global $DB, $USER;
305        if ($userid == 0) {
306            $userid = $USER->id;
307        }
308        $sql = self::get_subscription_join_rule_sql(true);
309        $sql .= "WHERE s.courseid = :courseid AND s.userid = :userid";
310
311        return $DB->count_records_sql($sql, array('courseid' => $courseid, 'userid' => $userid));
312    }
313
314    /**
315     * Get an array of subscriptions for a given user.
316     *
317     * @param int $limitfrom Limit from which to fetch rules.
318     * @param int $limitto  Limit to which rules need to be fetched.
319     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
320     * @param string $order Order to sort the subscriptions.
321     *
322     * @return array list of subscriptions
323     */
324    public static function get_user_subscriptions($limitfrom = 0, $limitto = 0, $userid = 0,
325                                                             $order = 's.courseid ASC, r.name' ) {
326        global $DB, $USER;
327        if ($userid == 0) {
328            $userid = $USER->id;
329        }
330        $sql = self::get_subscription_join_rule_sql();
331        $sql .= "WHERE s.userid = :userid ORDER BY $order";
332
333        return self::get_instances($DB->get_records_sql($sql, array('userid' => $userid), $limitfrom, $limitto));
334    }
335
336    /**
337     * Get count of subscriptions for a given user.
338     *
339     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
340     *
341     * @return int number of subscriptions
342     */
343    public static function count_user_subscriptions($userid = 0) {
344        global $DB, $USER;;
345        if ($userid == 0) {
346            $userid = $USER->id;
347        }
348        $sql = self::get_subscription_join_rule_sql(true);
349        $sql .= "WHERE s.userid = :userid";
350
351        return $DB->count_records_sql($sql, array('userid' => $userid));
352    }
353
354    /**
355     * Return a list of subscriptions for a given event.
356     *
357     * @param \stdClass $event the event object.
358     *
359     * @return array
360     */
361    public static function get_subscriptions_by_event(\stdClass $event) {
362        global $DB;
363
364        $sql = self::get_subscription_join_rule_sql();
365        if ($event->contextlevel == CONTEXT_MODULE && $event->contextinstanceid != 0) {
366            $sql .= "WHERE r.eventname = :eventname AND s.courseid = :courseid AND (s.cmid = :cmid OR s.cmid = 0)";
367            $params = array('eventname' => $event->eventname, 'courseid' => $event->courseid, 'cmid' => $event->contextinstanceid);
368        } else {
369            $sql .= "WHERE r.eventname = :eventname AND (s.courseid = :courseid OR s.courseid = 0)";
370            $params = array('eventname' => $event->eventname, 'courseid' => $event->courseid);
371        }
372        return self::get_instances($DB->get_records_sql($sql, $params));
373    }
374
375    /**
376     * Return sql to join rule and subscription table.
377     *
378     * @param bool $count Weather if this is a count query or not.
379     *
380     * @return string the sql.
381     */
382    protected static function get_subscription_join_rule_sql($count = false) {
383        if ($count) {
384            $select = "SELECT COUNT(s.id) ";
385        } else {
386            $select = "SELECT s.*, r.description, r.descriptionformat, r.name, r.userid as ruleuserid, r.courseid as rulecourseid,
387            r.plugin, r.eventname, r.template, r.templateformat, r.frequency, r.timewindow";
388        }
389        $sql = $select . "
390                  FROM {tool_monitor_rules} r
391                  JOIN {tool_monitor_subscriptions} s
392                        ON r.id = s.ruleid ";
393        return $sql;
394    }
395
396    /**
397     * Helper method to convert db records to instances.
398     *
399     * @param array $arr of subscriptions.
400     *
401     * @return array of subscriptions as instances.
402     */
403    protected static function get_instances($arr) {
404        $result = array();
405        foreach ($arr as $key => $sub) {
406            $result[$key] = new subscription($sub);
407        }
408        return $result;
409    }
410
411    /**
412     * Get count of subscriptions for a given rule.
413     *
414     * @param int $ruleid rule id of the subscription.
415     *
416     * @return int number of subscriptions
417     */
418    public static function count_rule_subscriptions($ruleid) {
419        global $DB;
420        $sql = self::get_subscription_join_rule_sql(true);
421        $sql .= "WHERE s.ruleid = :ruleid";
422
423        return $DB->count_records_sql($sql, array('ruleid' => $ruleid));
424    }
425
426    /**
427     * Returns true if an event in a particular course has a subscription.
428     *
429     * @param string $eventname the name of the event
430     * @param int $courseid the course id
431     * @return bool returns true if the event has subscriptions in a given course, false otherwise.
432     */
433    public static function event_has_subscriptions($eventname, $courseid) {
434        global $DB;
435
436        // Check if we can return these from cache.
437        $cache = \cache::make('tool_monitor', 'eventsubscriptions');
438
439        // The SQL we will be using to fill the cache if it is empty.
440        $sql = "SELECT DISTINCT(r.eventname)
441                  FROM {tool_monitor_subscriptions} s
442            INNER JOIN {tool_monitor_rules} r
443                    ON s.ruleid = r.id
444                 WHERE s.courseid = :courseid";
445
446        $sitesubscriptions = $cache->get(0);
447        // If we do not have the site subscriptions in the cache then return them from the DB.
448        if ($sitesubscriptions === false) {
449            // Set the array for the cache.
450            $sitesubscriptions = array();
451            if ($subscriptions = $DB->get_records_sql($sql, array('courseid' => 0))) {
452                foreach ($subscriptions as $subscription) {
453                    $sitesubscriptions[$subscription->eventname] = true;
454                }
455            }
456            $cache->set(0, $sitesubscriptions);
457        }
458
459        // Check if a subscription exists for this event site wide.
460        if (isset($sitesubscriptions[$eventname])) {
461            return true;
462        }
463
464        // If the course id is for the site, and we reached here then there is no site wide subscription for this event.
465        if (empty($courseid)) {
466            return false;
467        }
468
469        $coursesubscriptions = $cache->get($courseid);
470        // If we do not have the course subscriptions in the cache then return them from the DB.
471        if ($coursesubscriptions === false) {
472            // Set the array for the cache.
473            $coursesubscriptions = array();
474            if ($subscriptions = $DB->get_records_sql($sql, array('courseid' => $courseid))) {
475                foreach ($subscriptions as $subscription) {
476                    $coursesubscriptions[$subscription->eventname] = true;
477                }
478            }
479            $cache->set($courseid, $coursesubscriptions);
480        }
481
482        // Check if a subscription exists for this event in this course.
483        if (isset($coursesubscriptions[$eventname])) {
484            return true;
485        }
486
487        return false;
488    }
489
490    /**
491     * Activates a group of subscriptions based on an input array of ids.
492     *
493     * @since 3.2.0
494     * @param array $ids of subscription ids.
495     * @return bool true if the operation was successful, false otherwise.
496     */
497    public static function activate_subscriptions(array $ids) {
498        global $DB;
499        if (!empty($ids)) {
500            list($sql, $params) = $DB->get_in_or_equal($ids);
501            $success = $DB->set_field_select('tool_monitor_subscriptions', 'inactivedate', '0', 'id ' . $sql, $params);
502            return $success;
503        }
504        return false;
505    }
506
507    /**
508     * Deactivates a group of subscriptions based on an input array of ids.
509     *
510     * @since 3.2.0
511     * @param array $ids of subscription ids.
512     * @return bool true if the operation was successful, false otherwise.
513     */
514    public static function deactivate_subscriptions(array $ids) {
515        global $DB;
516        if (!empty($ids)) {
517            $inactivedate = time();
518            list($sql, $params) = $DB->get_in_or_equal($ids);
519            $success = $DB->set_field_select('tool_monitor_subscriptions', 'inactivedate', $inactivedate, 'id ' . $sql,
520                                             $params);
521            return $success;
522        }
523        return false;
524    }
525
526    /**
527     * Deletes subscriptions which have been inactive for a period of time.
528     *
529     * @since 3.2.0
530     * @param int $userid if provided, only this user's stale subscriptions will be deleted.
531     * @return bool true if the operation was successful, false otherwise.
532     */
533    public static function delete_stale_subscriptions($userid = 0) {
534        global $DB;
535        // Get the expiry duration, in days.
536        $cutofftime = strtotime("-" . self::INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS . " days", time());
537
538        if (!empty($userid)) {
539            // Remove any stale subscriptions for the desired user only.
540            $success = $DB->delete_records_select('tool_monitor_subscriptions',
541                                                  'userid = ? AND inactivedate < ? AND inactivedate <> 0',
542                                                  array($userid, $cutofftime));
543
544        } else {
545            // Remove all stale subscriptions.
546            $success = $DB->delete_records_select('tool_monitor_subscriptions',
547                                                  'inactivedate < ? AND inactivedate <> 0',
548                                                  array($cutofftime));
549        }
550        return $success;
551    }
552
553    /**
554     * Check whether a subscription is active.
555     *
556     * @since 3.2.0
557     * @param \tool_monitor\subscription $subscription instance.
558     * @return bool true if the subscription is active, false otherwise.
559     */
560    public static function subscription_is_active(subscription $subscription) {
561        return empty($subscription->inactivedate);
562    }
563}
564