MDL-38197 tool_generator: fix use of raw LIKE sql
[moodle.git] / admin / tool / generator / classes / backend.php
CommitLineData
bc4054f9 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
17defined('MOODLE_INTERNAL') || die();
18
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 */
26class 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;
39
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);
81
82 /**
83 * @var string Course shortname
84 */
85 private $shortname;
86
87 /**
88 * @var int Size code (index in the above arrays)
89 */
90 private $size;
91
92 /**
93 * @var bool True if displaying progress
94 */
95 private $progress;
96
97 /**
98 * @var testing_data_generator Data generator
99 */
100 private $generator;
101
102 /**
103 * @var stdClass Course object
104 */
105 private $course;
106
107 /**
108 * @var int Epoch time at which last dot was displayed
109 */
110 private $lastdot;
111
112 /**
113 * @var int Epoch time at which last percentage was displayed
114 */
115 private $lastpercentage;
116
117 /**
118 * @var int Epoch time at which current step (current set of dots) started
119 */
120 private $starttime;
121
122 /**
123 * @var array Array from test user number (1...N) to userid in database
124 */
125 private $userids;
126
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 }
141
142 // Set parameters.
143 $this->shortname = $shortname;
144 $this->size = $size;
145 $this->progress = $progress;
146 }
147
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 }
160
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 }
176
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 }
196
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');
205
206 raise_memory_limit(MEMORY_EXTRA);
207
208 if ($this->progress && !CLI_SCRIPT) {
209 echo html_writer::start_tag('ul');
210 }
211
212 $entirestart = microtime(true);
213
214 // Start transaction.
215 $transaction = $DB->start_delegated_transaction();
216
217 // Get generator.
218 $this->generator = phpunit_util::get_data_generator();
219
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();
227
228 // Log total time.
229 $this->log('complete', round(microtime(true) - $entirestart, 1));
230
231 if ($this->progress && !CLI_SCRIPT) {
232 echo html_writer::end_tag('ul');
233 }
234
235 // Commit transaction and finish.
236 $transaction->allow_commit();
237 return $this->course->id;
238 }
239
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 }
253
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;
261
262 // Work out total number of users.
263 $count = self::$paramusers[$this->size];
264
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;
faa2e68b 269 $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
bc4054f9 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];
278
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 }
285
286 // Stop if we've got enough users.
287 $nextnumber = $number + 1;
288 if ($number >= $count) {
289 break;
290 }
291 }
292 $rs->close();
293
294 // Create users from end of existing range.
295 if ($nextnumber <= $count) {
296 $this->create_user_accounts($nextnumber, $count);
297 }
298
299 // Assign all users to course.
300 $this->log('enrol', $count, true);
301
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);
313
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 }
319
320 $this->end_log();
321 }
322
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;
340
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 }
350
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');
357
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 }
367
368 $this->end_log();
369 }
370
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);
377
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);
384
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';
392
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]);
396
397 $fs->create_file_from_string($filerecord, $data);
398 $this->dot($i, $count);
399 }
400
401 $this->end_log();
402 }
403
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 }
423
424 /**
425 * Creates a number of resource activities with one big file each.
426 */
427 private function create_big_files() {
428 global $CFG;
429
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);
434
435 $this->log('createbigfiles', $count, true);
436
437 // Prepare temp area.
438 $tempfolder = make_temp_directory('tool_generator');
439 $tempfile = $tempfolder . '/' . rand();
440
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);
450
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);
462
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 }
470
471 unlink($tempfile);
472 $this->end_log();
473 }
474
475 /**
476 * Creates one forum activity with a bunch of posts.
477 */
478 private function create_forum() {
479 global $DB;
480
481 $discussions = self::$paramforumdiscussions[$this->size];
482 $posts = self::$paramforumposts[$this->size];
483 $totalposts = $discussions * $posts;
484
485 $this->log('createforum', $totalposts, true);
486
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);
493
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 }
509
510 $this->end_log();
511 }
512
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 }
521
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 }
530
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 }
560
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 }
585
586 // Update time limit so PHP doesn't time out.
587 if (!CLI_SCRIPT) {
588 set_time_limit(120);
589 }
590 }
591
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 }
606}