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 * Condition main class.
19 *
20 * @package availability_grouping
21 * @copyright 2014 The Open University
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace availability_grouping;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Condition main class.
31 *
32 * @package availability_grouping
33 * @copyright 2014 The Open University
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36class condition extends \core_availability\condition {
37    /** @var array Array from grouping id => name */
38    protected static $groupingnames = array();
39
40    /** @var int ID of grouping that this condition requires */
41    protected $groupingid = 0;
42
43    /** @var bool If true, indicates that activity $cm->grouping is used */
44    protected $activitygrouping = false;
45
46    /**
47     * Constructor.
48     *
49     * @param \stdClass $structure Data structure from JSON decode
50     * @throws \coding_exception If invalid data structure.
51     */
52    public function __construct($structure) {
53        // Get grouping id.
54        if (isset($structure->id)) {
55            if (is_int($structure->id)) {
56                $this->groupingid = $structure->id;
57            } else {
58                throw new \coding_exception('Invalid ->id for grouping condition');
59            }
60        } else if (isset($structure->activity)) {
61            if (is_bool($structure->activity) && $structure->activity) {
62                $this->activitygrouping = true;
63            } else {
64                throw new \coding_exception('Invalid ->activity for grouping condition');
65            }
66        } else {
67            throw new \coding_exception('Missing ->id / ->activity for grouping condition');
68        }
69    }
70
71    public function save() {
72        $result = (object)array('type' => 'grouping');
73        if ($this->groupingid) {
74            $result->id = $this->groupingid;
75        } else {
76            $result->activity = true;
77        }
78        return $result;
79    }
80
81    public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
82        $context = \context_course::instance($info->get_course()->id);
83        $allow = true;
84        if (!has_capability('moodle/site:accessallgroups', $context, $userid)) {
85            // If the activity has 'group members only' and you don't have accessallgroups...
86            $groups = $info->get_modinfo()->get_groups($this->get_grouping_id($info));
87            if (!$groups) {
88                // ...and you don't belong to a group, then set it so you can't see/access it.
89                $allow = false;
90            }
91
92            // The NOT condition applies before accessallgroups (i.e. if you
93            // set something to be available to those NOT in grouping X,
94            // people with accessallgroups can still access it even if
95            // they are in grouping X).
96            if ($not) {
97                $allow = !$allow;
98            }
99        }
100        return $allow;
101    }
102
103    /**
104     * Gets the actual grouping id for the condition. This is either a specified
105     * id, or a special flag indicating that we use the one for the current cm.
106     *
107     * @param \core_availability\info $info Info about context cm
108     * @return int Grouping id
109     * @throws \coding_exception If it's set to use a cm but there isn't grouping
110     */
111    protected function get_grouping_id(\core_availability\info $info) {
112        if ($this->activitygrouping) {
113            $groupingid = $info->get_course_module()->groupingid;
114            if (!$groupingid) {
115                throw new \coding_exception(
116                        'Not supposed to be able to turn on activitygrouping when no grouping');
117            }
118            return $groupingid;
119        } else {
120            return $this->groupingid;
121        }
122    }
123
124    public function get_description($full, $not, \core_availability\info $info) {
125        global $DB;
126        $course = $info->get_course();
127
128        // Need to get the name for the grouping. Unfortunately this requires
129        // a database query. To save queries, get all groupings for course at
130        // once in a static cache.
131        $groupingid = $this->get_grouping_id($info);
132        if (!array_key_exists($groupingid, self::$groupingnames)) {
133            $coursegroupings = $DB->get_records(
134                    'groupings', array('courseid' => $course->id), '', 'id, name');
135            foreach ($coursegroupings as $rec) {
136                self::$groupingnames[$rec->id] = $rec->name;
137            }
138        }
139
140        // If it still doesn't exist, it must have been misplaced.
141        if (!array_key_exists($groupingid, self::$groupingnames)) {
142            $name = get_string('missing', 'availability_grouping');
143        } else {
144            // Not safe to call format_string here; use the special function to call it later.
145            $name = self::description_format_string(self::$groupingnames[$groupingid]);
146        }
147
148        return get_string($not ? 'requires_notgrouping' : 'requires_grouping',
149                'availability_grouping', $name);
150    }
151
152    protected function get_debug_string() {
153        if ($this->activitygrouping) {
154            return 'CM';
155        } else {
156            return '#' . $this->groupingid;
157        }
158    }
159
160    /**
161     * Include this condition only if we are including groups in restore, or
162     * if it's a generic 'same activity' one.
163     *
164     * @param int $restoreid The restore Id.
165     * @param int $courseid The ID of the course.
166     * @param base_logger $logger The logger being used.
167     * @param string $name Name of item being restored.
168     * @param base_task $task The task being performed.
169     *
170     * @return Integer groupid
171     */
172    public function include_after_restore($restoreid, $courseid, \base_logger $logger,
173            $name, \base_task $task) {
174        return !$this->groupingid || $task->get_setting_value('groups');
175    }
176
177    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
178        global $DB;
179        if (!$this->groupingid) {
180            // If using 'same as activity' option, no need to change it.
181            return false;
182        }
183        $rec = \restore_dbops::get_backup_ids_record($restoreid, 'grouping', $this->groupingid);
184        if (!$rec || !$rec->newitemid) {
185            // If we are on the same course (e.g. duplicate) then we can just
186            // use the existing one.
187            if ($DB->record_exists('groupings',
188                    array('id' => $this->groupingid, 'courseid' => $courseid))) {
189                return false;
190            }
191            // Otherwise it's a warning.
192            $this->groupingid = -1;
193            $logger->process('Restored item (' . $name .
194                    ') has availability condition on grouping that was not restored',
195                    \backup::LOG_WARNING);
196        } else {
197            $this->groupingid = (int)$rec->newitemid;
198        }
199        return true;
200    }
201
202    public function update_dependency_id($table, $oldid, $newid) {
203        if ($table === 'groupings' && (int)$this->groupingid === (int)$oldid) {
204            $this->groupingid = $newid;
205            return true;
206        } else {
207            return false;
208        }
209    }
210
211    /**
212     * Wipes the static cache used to store grouping names.
213     */
214    public static function wipe_static_cache() {
215        self::$groupingnames = array();
216    }
217
218    public function is_applied_to_user_lists() {
219        // Grouping conditions are assumed to be 'permanent', so they affect the
220        // display of user lists for activities.
221        return true;
222    }
223
224    public function filter_user_list(array $users, $not, \core_availability\info $info,
225            \core_availability\capability_checker $checker) {
226        global $CFG, $DB;
227
228        // If the array is empty already, just return it.
229        if (!$users) {
230            return $users;
231        }
232
233        // List users for this course who match the condition.
234        $groupingusers = $DB->get_records_sql("
235                SELECT DISTINCT gm.userid
236                  FROM {groupings_groups} gg
237                  JOIN {groups_members} gm ON gm.groupid = gg.groupid
238                 WHERE gg.groupingid = ?",
239                array($this->get_grouping_id($info)));
240
241        // List users who have access all groups.
242        $aagusers = $checker->get_users_by_capability('moodle/site:accessallgroups');
243
244        // Filter the user list.
245        $result = array();
246        foreach ($users as $id => $user) {
247            // Always include users with access all groups.
248            if (array_key_exists($id, $aagusers)) {
249                $result[$id] = $user;
250                continue;
251            }
252            // Other users are included or not based on grouping membership.
253            $allow = array_key_exists($id, $groupingusers);
254            if ($not) {
255                $allow = !$allow;
256            }
257            if ($allow) {
258                $result[$id] = $user;
259            }
260        }
261        return $result;
262    }
263
264    /**
265     * Returns a JSON object which corresponds to a condition of this type.
266     *
267     * Intended for unit testing, as normally the JSON values are constructed
268     * by JavaScript code.
269     *
270     * @param int $groupingid Required grouping id (0 = grouping linked to activity)
271     * @return stdClass Object representing condition
272     */
273    public static function get_json($groupingid = 0) {
274        $result = (object)array('type' => 'grouping');
275        if ($groupingid) {
276            $result->id = (int)$groupingid;
277        } else {
278            $result->activity = true;
279        }
280        return $result;
281    }
282
283    public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
284        global $DB;
285
286        // Get enrolled users with access all groups. These always are allowed.
287        list($aagsql, $aagparams) = get_enrolled_sql(
288                $info->get_context(), 'moodle/site:accessallgroups', 0, $onlyactive);
289
290        // Get all enrolled users.
291        list ($enrolsql, $enrolparams) =
292                get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
293
294        // Condition for specified or any group.
295        $matchparams = array();
296        $matchsql = "SELECT 1
297                       FROM {groups_members} gm
298                       JOIN {groupings_groups} gg ON gg.groupid = gm.groupid
299                      WHERE gm.userid = userids.id
300                            AND gg.groupingid = " .
301                self::unique_sql_parameter($matchparams, $this->get_grouping_id($info));
302
303        // Overall query combines all this.
304        $condition = $not ? 'NOT' : '';
305        $sql = "SELECT userids.id
306                  FROM ($enrolsql) userids
307                 WHERE (userids.id IN ($aagsql)) OR $condition EXISTS ($matchsql)";
308        return array($sql, array_merge($enrolparams, $aagparams, $matchparams));
309    }
310}
311