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 * This file contains components used by the restore UI
19 *
20 * @package   core_backup
21 * @copyright 2010 Sam Hemelryk
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25/**
26 * A base class that can be used to build a specific search upon
27 *
28 * @package   core_backup
29 * @copyright 2010 Sam Hemelryk
30 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 */
32abstract class restore_search_base implements renderable {
33
34    /**
35     * The default values for this components params
36     */
37    const DEFAULT_SEARCH = '';
38
39    /**
40     * The param used to convey the current search string
41     * @var string
42     */
43    static $VAR_SEARCH = 'search';
44
45    /**
46     * The current search string
47     * @var string|null
48     */
49    private $search = null;
50    /**
51     * The URL for this page including required params to return to it
52     * @var moodle_url
53     */
54    private $url = null;
55    /**
56     * The results of the search
57     * @var array|null
58     */
59    private $results = null;
60    /**
61     * The total number of results available
62     * @var int
63     */
64    private $totalcount = null;
65    /**
66     * Array of capabilities required for each item in the search
67     * @var array
68     */
69    private $requiredcapabilities = array();
70    /**
71     * Max number of courses to return in a search.
72     * @var int
73     */
74    private $maxresults = null;
75    /**
76     * Indicates if we have more than maxresults found.
77     * @var boolean
78     */
79    private $hasmoreresults = false;
80
81    /**
82     * Constructor
83     * @param array $config Config options
84     */
85    public function __construct(array $config = array()) {
86
87        $this->search = optional_param($this->get_varsearch(), self::DEFAULT_SEARCH, PARAM_NOTAGS);
88        $this->maxresults = get_config('backup', 'import_general_maxresults');
89
90        foreach ($config as $name => $value) {
91            $method = 'set_'.$name;
92            if (method_exists($this, $method)) {
93                $this->$method($value);
94            }
95        }
96    }
97
98    /**
99     * The URL for this search
100     * @global moodle_page $PAGE
101     * @return moodle_url The URL for this page
102     */
103    final public function get_url() {
104        global $PAGE;
105        $params = array(
106            $this->get_varsearch()    => $this->get_search()
107        );
108        return ($this->url !== null) ? new moodle_url($this->url, $params) : new moodle_url($PAGE->url, $params);
109    }
110
111    /**
112     * The current search string
113     * @return string
114     */
115    final public function get_search() {
116        return ($this->search !== null) ? $this->search : self::DEFAULT_SEARCH;
117    }
118
119    /**
120     * The total number of results
121     * @return int
122     */
123    final public function get_count() {
124        if ($this->totalcount === null) {
125            $this->search();
126        }
127        return $this->totalcount;
128    }
129
130    /**
131     * Returns an array of results from the search
132     * @return array
133     */
134    final public function get_results() {
135        if ($this->results === null) {
136            $this->search();
137        }
138        return $this->results;
139    }
140
141    /**
142     * Sets the page URL
143     * @param moodle_url $url
144     */
145    final public function set_url(moodle_url $url) {
146        $this->url = $url;
147    }
148
149    /**
150     * Invalidates the results collected so far
151     */
152    final public function invalidate_results() {
153        $this->results = null;
154        $this->totalcount = null;
155    }
156
157    /**
158     * Adds a required capability which all results will be checked against
159     * @param string $capability
160     * @param int|null $user
161     */
162    final public function require_capability($capability, $user = null) {
163        if (!is_int($user)) {
164            $user = null;
165        }
166        $this->requiredcapabilities[] = array(
167            'capability' => $capability,
168            'user' => $user
169        );
170    }
171
172    /**
173     * Executes the search
174     *
175     * @global moodle_database $DB
176     * @return int The number of results
177     */
178    final public function search() {
179        global $DB;
180        if (!is_null($this->results)) {
181            return $this->results;
182        }
183
184        $this->results = array();
185        $this->totalcount = 0;
186        $contextlevel = $this->get_itemcontextlevel();
187        list($sql, $params) = $this->get_searchsql();
188        // Get total number, to avoid some incorrect iterations.
189        $countsql = preg_replace('/ORDER BY.*/', '', $sql);
190        $totalcourses = $DB->count_records_sql("SELECT COUNT(*) FROM ($countsql) sel", $params);
191        if ($totalcourses > 0) {
192            // User to be checked is always the same (usually null, get it from first element).
193            $firstcap = reset($this->requiredcapabilities);
194            $userid = isset($firstcap['user']) ? $firstcap['user'] : null;
195            // Extract caps to check, this saves us a bunch of iterations.
196            $requiredcaps = array();
197            foreach ($this->requiredcapabilities as $cap) {
198                $requiredcaps[] = $cap['capability'];
199            }
200            // Iterate while we have records and haven't reached $this->maxresults.
201            $resultset = $DB->get_recordset_sql($sql, $params);
202            foreach ($resultset as $result) {
203                context_helper::preload_from_record($result);
204                $classname = context_helper::get_class_for_level($contextlevel);
205                $context = $classname::instance($result->id);
206                if (count($requiredcaps) > 0) {
207                    if (!has_all_capabilities($requiredcaps, $context, $userid)) {
208                        continue;
209                    }
210                }
211                // Check if we are over the limit.
212                if ($this->totalcount + 1 > $this->maxresults) {
213                    $this->hasmoreresults = true;
214                    break;
215                }
216                // If not, then continue.
217                $this->totalcount++;
218                $this->results[$result->id] = $result;
219            }
220            $resultset->close();
221        }
222
223        return $this->totalcount;
224    }
225
226    /**
227     * Returns true if there are more search results.
228     * @return bool
229     */
230    final public function has_more_results() {
231        if ($this->results === null) {
232            $this->search();
233        }
234        return $this->hasmoreresults;
235    }
236
237    /**
238     * Returns an array containing the SQL for the search and the params
239     * @return array
240     */
241    abstract protected function get_searchsql();
242
243    /**
244     * Gets the context level associated with this components items
245     * @return CONTEXT_*
246     */
247    abstract protected function get_itemcontextlevel();
248
249    /**
250     * Formats the results
251     */
252    abstract protected function format_results();
253
254    /**
255     * Gets the string used to transfer the search string for this compontents requests
256     * @return string
257     */
258    abstract public function get_varsearch();
259}
260
261/**
262 * A course search component
263 *
264 * @package   core_backup
265 * @copyright 2010 Sam Hemelryk
266 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
267 */
268class restore_course_search extends restore_search_base {
269
270    /**
271     * @var string
272     */
273    static $VAR_SEARCH = 'search';
274
275    /**
276     * The current course id.
277     * @var int
278     */
279    protected $currentcourseid = null;
280
281    /**
282     * Determines if the current course is included in the results.
283     * @var bool
284     */
285    protected $includecurrentcourse;
286
287    /**
288     * Constructor
289     * @param array $config
290     * @param int $currentcouseid The current course id so it can be ignored
291     */
292    public function __construct(array $config = array(), $currentcouseid = null) {
293        parent::__construct($config);
294        $this->setup_restrictions();
295        $this->currentcourseid = $currentcouseid;
296        $this->includecurrentcourse = false;
297    }
298
299    /**
300     * Sets up any access restrictions for the courses to be displayed in the search.
301     *
302     * This will typically call $this->require_capability().
303     */
304    protected function setup_restrictions() {
305        $this->require_capability('moodle/restore:restorecourse');
306    }
307
308    /**
309     * Get the search SQL.
310     * @global moodle_database $DB
311     * @return array
312     */
313    protected function get_searchsql() {
314        global $DB;
315
316        $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
317        $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
318        $params = array(
319            'contextlevel' => CONTEXT_COURSE,
320            'fullnamesearch' => '%'.$this->get_search().'%',
321            'shortnamesearch' => '%'.$this->get_search().'%'
322        );
323
324        $select     = " SELECT c.id, c.fullname, c.shortname, c.visible, c.sortorder ";
325        $from       = " FROM {course} c ";
326        $where      = " WHERE (".$DB->sql_like('c.fullname', ':fullnamesearch', false)." OR ".
327            $DB->sql_like('c.shortname', ':shortnamesearch', false).")";
328        $orderby    = " ORDER BY c.sortorder";
329
330        if ($this->currentcourseid !== null && !$this->includecurrentcourse) {
331            $where .= " AND c.id <> :currentcourseid";
332            $params['currentcourseid'] = $this->currentcourseid;
333        }
334
335        return array($select.$ctxselect.$from.$ctxjoin.$where.$orderby, $params);
336    }
337
338    /**
339     * Gets the context level for the search result items.
340     * @return CONTEXT_|int
341     */
342    protected function get_itemcontextlevel() {
343        return CONTEXT_COURSE;
344    }
345
346    /**
347     * Formats results.
348     */
349    protected function format_results() {}
350
351    /**
352     * Returns the name the search variable should use
353     * @return string
354     */
355    public function get_varsearch() {
356        return self::$VAR_SEARCH;
357    }
358
359    /**
360     * Returns true if the current course should be included in the results.
361     */
362    public function set_include_currentcourse() {
363        $this->includecurrentcourse = true;
364    }
365}
366
367/**
368 * A category search component
369 *
370 * @package   core_backup
371 * @copyright 2010 Sam Hemelryk
372 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
373 */
374class restore_category_search extends restore_search_base  {
375
376    /**
377     * The search variable to use.
378     * @var string
379     */
380    static $VAR_SEARCH = 'catsearch';
381
382    /**
383     * Constructor
384     * @param array $config
385     */
386    public function __construct(array $config = array()) {
387        parent::__construct($config);
388        $this->require_capability('moodle/course:create');
389    }
390    /**
391     * Returns the search SQL.
392     * @global moodle_database $DB
393     * @return array
394     */
395    protected function get_searchsql() {
396        global $DB;
397
398        $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
399        $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
400        $params = array(
401            'contextlevel' => CONTEXT_COURSECAT,
402            'namesearch' => '%'.$this->get_search().'%',
403        );
404
405        $select     = " SELECT c.id, c.name, c.visible, c.sortorder, c.description, c.descriptionformat ";
406        $from       = " FROM {course_categories} c ";
407        $where      = " WHERE ".$DB->sql_like('c.name', ':namesearch', false);
408        $orderby    = " ORDER BY c.sortorder";
409
410        return array($select.$ctxselect.$from.$ctxjoin.$where.$orderby, $params);
411    }
412
413    /**
414     * Returns the context level of the search results.
415     * @return CONTEXT_COURSECAT
416     */
417    protected function get_itemcontextlevel() {
418        return CONTEXT_COURSECAT;
419    }
420
421    /**
422     * Formats the results.
423     */
424    protected function format_results() {}
425
426    /**
427     * Returns the name to use for the search variable.
428     * @return string
429     */
430    public function get_varsearch() {
431        return self::$VAR_SEARCH;
432    }
433}
434