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