MDL-38197 tool_generator: fix use of raw LIKE sql
[moodle.git] / admin / tool / generator / classes / backend.php
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/>.
17 defined('MOODLE_INTERNAL') || die();
19 /**
20  * Backend code for the 'make large course' tool.
21  *
22  * @package tool_generator
23  * @copyright 2013 The Open University
24  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
26 class tool_generator_backend {
27     /**
28      * @var int Lowest (smallest) size index
29      */
30     const MIN_SIZE = 0;
31     /**
32      * @var int Highest (largest) size index
33      */
34     const MAX_SIZE = 5;
35     /**
36      * @var int Default size index
37      */
38     const DEFAULT_SIZE = 3;
40     /**
41      * @var array Number of sections in course
42      */
43     private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
44     /**
45      * @var array Number of Page activities in course
46      */
47     private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
48     /**
49      * @var array Number of students enrolled in course
50      */
51     private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
52     /**
53      * Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
54      *
55      * @var array Number of small files created in a single file activity
56      */
57     private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
58     /**
59      * @var array Size of small files (to make the totals into nice numbers)
60      */
61     private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
62     /**
63      * Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
64      *
65      * @var array Number of big files created as individual file activities
66      */
67     private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
68     /**
69      * @var array Size of each large file
70      */
71     private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
72             858993459, 1717986918);
73     /**
74      * @var array Number of forum discussions
75      */
76     private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
77     /**
78      * @var array Number of forum posts per discussion
79      */
80     private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
82     /**
83      * @var string Course shortname
84      */
85     private $shortname;
87     /**
88      * @var int Size code (index in the above arrays)
89      */
90     private $size;
92     /**
93      * @var bool True if displaying progress
94      */
95     private $progress;
97     /**
98      * @var testing_data_generator Data generator
99      */
100     private $generator;
102     /**
103      * @var stdClass Course object
104      */
105     private $course;
107     /**
108      * @var int Epoch time at which last dot was displayed
109      */
110     private $lastdot;
112     /**
113      * @var int Epoch time at which last percentage was displayed
114      */
115     private $lastpercentage;
117     /**
118      * @var int Epoch time at which current step (current set of dots) started
119      */
120     private $starttime;
122     /**
123      * @var array Array from test user number (1...N) to userid in database
124      */
125     private $userids;
127     /**
128      * Constructs object ready to create course.
129      *
130      * @param string $shortname Course shortname
131      * @param int $size Size as numeric index
132      * @param bool $progress True if progress information should be displayed
133      * @return int Course id
134      * @throws coding_exception If parameters are invalid
135      */
136     public function __construct($shortname, $size, $progress = true) {
137         // Check parameter.
138         if ($size < self::MIN_SIZE || $size > self::MAX_SIZE) {
139             throw new coding_exception('Invalid size');
140         }
142         // Set parameters.
143         $this->shortname = $shortname;
144         $this->size = $size;
145         $this->progress = $progress;
146     }
148     /**
149      * Gets a list of size choices supported by this backend.
150      *
151      * @return array List of size (int) => text description for display
152      */
153     public static function get_size_choices() {
154         $options = array();
155         for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
156             $options[$size] = get_string('size_' . $size, 'tool_generator');
157         }
158         return $options;
159     }
161     /**
162      * Converts a size name into the numeric constant.
163      *
164      * @param string $sizename Size name e.g. 'L'
165      * @return int Numeric version
166      * @throws coding_exception If the size name is not known
167      */
168     public static function size_for_name($sizename) {
169         for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
170             if ($sizename == get_string('shortsize_' . $size, 'tool_generator')) {
171                 return $size;
172             }
173         }
174         throw new coding_exception("Unknown size name '$sizename'");
175     }
177     /**
178      * Checks that a shortname is available (unused).
179      *
180      * @param string $shortname Proposed course shortname
181      * @return string An error message if the name is unavailable or '' if OK
182      */
183     public static function check_shortname_available($shortname) {
184         global $DB;
185         $fullname = $DB->get_field('course', 'fullname',
186                 array('shortname' => $shortname), IGNORE_MISSING);
187         if ($fullname !== false) {
188             // I wanted to throw an exception here but it is not possible to
189             // use strings from moodle.php in exceptions, and I didn't want
190             // to duplicate the string in tool_generator, so I changed this to
191             // not use exceptions.
192             return get_string('shortnametaken', 'moodle', $fullname);
193         }
194         return '';
195     }
197     /**
198      * Runs the entire 'make' process.
199      *
200      * @return int Course id
201      */
202     public function make() {
203         global $DB, $CFG;
204         require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
206         raise_memory_limit(MEMORY_EXTRA);
208         if ($this->progress && !CLI_SCRIPT) {
209             echo html_writer::start_tag('ul');
210         }
212         $entirestart = microtime(true);
214         // Start transaction.
215         $transaction = $DB->start_delegated_transaction();
217         // Get generator.
218         $this->generator = phpunit_util::get_data_generator();
220         // Make course.
221         $this->course = $this->create_course();
222         $this->create_users();
223         $this->create_pages();
224         $this->create_small_files();
225         $this->create_big_files();
226         $this->create_forum();
228         // Log total time.
229         $this->log('complete', round(microtime(true) - $entirestart, 1));
231         if ($this->progress && !CLI_SCRIPT) {
232             echo html_writer::end_tag('ul');
233         }
235         // Commit transaction and finish.
236         $transaction->allow_commit();
237         return $this->course->id;
238     }
240     /**
241      * Creates the actual course.
242      *
243      * @return stdClass Course record
244      */
245     private function create_course() {
246         $this->log('createcourse', $this->shortname);
247         $courserecord = array('shortname' => $this->shortname,
248                 'fullname' => get_string('fullname', 'tool_generator',
249                     array('size' => get_string('shortsize_' . $this->size, 'tool_generator'))),
250                 'numsections' => self::$paramsections[$this->size]);
251         return $this->generator->create_course($courserecord, array('createsections' => true));
252     }
254     /**
255      * Creates a number of user accounts and enrols them on the course.
256      * Note: Existing user accounts that were created by this system are
257      * reused if available.
258      */
259     private function create_users() {
260         global $DB;
262         // Work out total number of users.
263         $count = self::$paramusers[$this->size];
265         // Get existing users in order. We will 'fill up holes' in this up to
266         // the required number.
267         $this->log('checkaccounts', $count);
268         $nextnumber = 1;
269         $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
270                 array('tool_generator_%'), 'username', 'id, username');
271         foreach ($rs as $rec) {
272             // Extract number from username.
273             $matches = array();
274             if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
275                 continue;
276             }
277             $number = (int)$matches[1];
279             // Create missing users in range up to this.
280             if ($number != $nextnumber) {
281                 $this->create_user_accounts($nextnumber, min($number - 1, $count));
282             } else {
283                 $this->userids[$number] = (int)$rec->id;
284             }
286             // Stop if we've got enough users.
287             $nextnumber = $number + 1;
288             if ($number >= $count) {
289                 break;
290             }
291         }
292         $rs->close();
294         // Create users from end of existing range.
295         if ($nextnumber <= $count) {
296             $this->create_user_accounts($nextnumber, $count);
297         }
299         // Assign all users to course.
300         $this->log('enrol', $count, true);
302         $enrolplugin = enrol_get_plugin('manual');
303         $instances = enrol_get_instances($this->course->id, true);
304         foreach ($instances as $instance) {
305             if ($instance->enrol === 'manual') {
306                 break;
307             }
308         }
309         if ($instance->enrol !== 'manual') {
310             throw new coding_exception('No manual enrol plugin in course');
311         }
312         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
314         for ($number = 1; $number <= $count; $number++) {
315             // Enrol user.
316             $enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
317             $this->dot($number, $count);
318         }
320         $this->end_log();
321     }
323     /**
324      * Creates user accounts with a numeric range.
325      *
326      * @param int $first Number of first user
327      * @param int $last Number of last user
328      */
329     private function create_user_accounts($first, $last) {
330         $this->log('createaccounts', (object)array('from' => $first, 'to' => $last), true);
331         $count = $last - $first + 1;
332         $done = 0;
333         for ($number = $first; $number <= $last; $number++, $done++) {
334             // Work out username with 6-digit number.
335             $textnumber = (string)$number;
336             while (strlen($textnumber) < 6) {
337                 $textnumber = '0' . $textnumber;
338             }
339             $username = 'tool_generator_' . $textnumber;
341             // Create user account.
342             $record = array('firstname' => get_string('firstname', 'tool_generator'),
343                     'lastname' => $number, 'username' => $username);
344             $user = $this->generator->create_user($record);
345             $this->userids[$number] = (int)$user->id;
346             $this->dot($done, $count);
347         }
348         $this->end_log();
349     }
351     /**
352      * Creates a number of Page activities.
353      */
354     private function create_pages() {
355         // Set up generator.
356         $pagegenerator = $this->generator->get_plugin_generator('mod_page');
358         // Create pages.
359         $number = self::$parampages[$this->size];
360         $this->log('createpages', $number, true);
361         for ($i=0; $i<$number; $i++) {
362             $record = array('course' => $this->course->id);
363             $options = array('section' => $this->get_random_section());
364             $pagegenerator->create_instance($record, $options);
365             $this->dot($i, $number);
366         }
368         $this->end_log();
369     }
371     /**
372      * Creates one resource activity with a lot of small files.
373      */
374     private function create_small_files() {
375         $count = self::$paramsmallfilecount[$this->size];
376         $this->log('createsmallfiles', $count, true);
378         // Create resource with default textfile only.
379         $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
380         $record = array('course' => $this->course->id,
381                 'name' => get_string('smallfiles', 'tool_generator'));
382         $options = array('section' => 0);
383         $resource = $resourcegenerator->create_instance($record, $options);
385         // Add files.
386         $fs = get_file_storage();
387         $context = context_module::instance($resource->cmid);
388         $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
389                 'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
390         for ($i = 0; $i < $count; $i++) {
391             $filerecord['filename'] = 'smallfile' . $i . '.dat';
393             // Generate random binary data (different for each file so it
394             // doesn't compress unrealistically).
395             $data = self::get_random_binary(self::$paramsmallfilesize[$this->size]);
397             $fs->create_file_from_string($filerecord, $data);
398             $this->dot($i, $count);
399         }
401         $this->end_log();
402     }
404     /**
405      * Creates a string of random binary data. The start of the string includes
406      * the current time, in an attempt to avoid large-scale repetition.
407      *
408      * @param int $length Number of bytes
409      * @return Random data
410      */
411     private static function get_random_binary($length) {
412         $data = microtime(true);
413         if (strlen($data) > $length) {
414             // Use last digits of data.
415             return substr($data, -$length);
416         }
417         $length -= strlen($data);
418         for ($j=0; $j < $length; $j++) {
419             $data .= chr(rand(1, 255));
420         }
421         return $data;
422     }
424     /**
425      * Creates a number of resource activities with one big file each.
426      */
427     private function create_big_files() {
428         global $CFG;
430         // Work out how many files and how many blocks to use (up to 64KB).
431         $count = self::$parambigfilecount[$this->size];
432         $blocks = ceil(self::$parambigfilesize[$this->size] / 65536);
433         $blocksize = floor(self::$parambigfilesize[$this->size] / $blocks);
435         $this->log('createbigfiles', $count, true);
437         // Prepare temp area.
438         $tempfolder = make_temp_directory('tool_generator');
439         $tempfile = $tempfolder . '/' . rand();
441         // Create resources and files.
442         $fs = get_file_storage();
443         $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
444         for ($i = 0; $i < $count; $i++) {
445             // Create resource.
446             $record = array('course' => $this->course->id,
447                     'name' => get_string('bigfile', 'tool_generator', $i));
448             $options = array('section' => $this->get_random_section());
449             $resource = $resourcegenerator->create_instance($record, $options);
451             // Write file.
452             $handle = fopen($tempfile, 'w');
453             if (!$handle) {
454                 throw new coding_exception('Failed to open temporary file');
455             }
456             for ($j = 0; $j < $blocks; $j++) {
457                 $data = self::get_random_binary($blocksize);
458                 fwrite($handle, $data);
459                 $this->dot($i * $blocks + $j, $count * $blocks);
460             }
461             fclose($handle);
463             // Add file.
464             $context = context_module::instance($resource->cmid);
465             $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
466                     'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
467                     'filename' => 'bigfile' . $i . '.dat');
468             $fs->create_file_from_pathname($filerecord, $tempfile);
469         }
471         unlink($tempfile);
472         $this->end_log();
473     }
475     /**
476      * Creates one forum activity with a bunch of posts.
477      */
478     private function create_forum() {
479         global $DB;
481         $discussions = self::$paramforumdiscussions[$this->size];
482         $posts = self::$paramforumposts[$this->size];
483         $totalposts = $discussions * $posts;
485         $this->log('createforum', $totalposts, true);
487         // Create empty forum.
488         $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
489         $record = array('course' => $this->course->id,
490                 'name' => get_string('pluginname', 'forum'));
491         $options = array('section' => 0);
492         $forum = $forumgenerator->create_instance($record, $options);
494         // Add discussions and posts.
495         $sofar = 0;
496         for ($i=0; $i < $discussions; $i++) {
497             $record = array('forum' => $forum->id, 'course' => $this->course->id,
498                     'userid' => $this->get_random_user());
499             $discussion = $forumgenerator->create_discussion($record);
500             $parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
501             $sofar++;
502             for ($j=0; $j < $posts - 1; $j++, $sofar++) {
503                 $record = array('discussion' => $discussion->id,
504                         'userid' => $this->get_random_user(), 'parent' => $parentid);
505                 $forumgenerator->create_post($record);
506                 $this->dot($sofar, $totalposts);
507             }
508         }
510         $this->end_log();
511     }
513     /**
514      * Gets a random section number.
515      *
516      * @return int A section number from 1 to the number of sections
517      */
518     private function get_random_section() {
519         return rand(1, self::$paramsections[$this->size]);
520     }
522     /**
523      * Gets a random user id.
524      *
525      * @return int A user id for a random created user
526      */
527     private function get_random_user() {
528         return $this->userids[rand(1, self::$paramusers[$this->size])];
529     }
531     /**
532      * Displays information as part of progress.
533      * @param string $langstring Part of langstring (after progress_)
534      * @param mixed $a Optional lang string parameters
535      * @param bool $leaveopen If true, doesn't close LI tag (ready for dots)
536      */
537     private function log($langstring, $a = null, $leaveopen = false) {
538         if (!$this->progress) {
539             return;
540         }
541         if (CLI_SCRIPT) {
542             echo '* ';
543         } else {
544             echo html_writer::start_tag('li');
545         }
546         echo get_string('progress_' . $langstring, 'tool_generator', $a);
547         if (!$leaveopen) {
548             if (CLI_SCRIPT) {
549                 echo "\n";
550             } else {
551                 echo html_writer::end_tag('li');
552             }
553         } else {
554             echo ': ';
555             $this->lastdot = time();
556             $this->lastpercentage = $this->lastdot;
557             $this->starttime = microtime(true);
558         }
559     }
561     /**
562      * Outputs dots. There is up to one dot per second. Once a minute, it
563      * displays a percentage.
564      * @param int $number Number of completed items
565      * @param int $total Total number of items to complete
566      */
567     private function dot($number, $total) {
568         if (!$this->progress) {
569             return;
570         }
571         $now = time();
572         if ($now == $this->lastdot) {
573             return;
574         }
575         $this->lastdot = $now;
576         if (CLI_SCRIPT) {
577             echo '.';
578         } else {
579             echo ' . ';
580         }
581         if ($now - $this->lastpercentage >= 30) {
582             echo round(100.0 * $number / $total, 1) . '%';
583             $this->lastpercentage = $now;
584         }
586         // Update time limit so PHP doesn't time out.
587         if (!CLI_SCRIPT) {
588             set_time_limit(120);
589         }
590     }
592     /**
593      * Ends a log string that was started using log function with $leaveopen.
594      */
595     private function end_log() {
596         if (!$this->progress) {
597             return;
598         }
599         echo get_string('done', 'tool_generator', round(microtime(true) - $this->starttime, 1));
600         if (CLI_SCRIPT) {
601             echo "\n";
602         } else {
603             echo html_writer::end_tag('li');
604         }
605     }