Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace tool_brickfield;
18
 
19
use context_system;
20
use moodle_exception;
21
use moodle_url;
22
use stdClass;
23
use tool_brickfield\local\tool\filter;
24
 
25
/**
26
 * Provides the Brickfield Accessibility toolkit API.
27
 *
28
 * @package    tool_brickfield
29
 * @copyright  2020 onward Brickfield Education Labs Ltd, https://www.brickfield.ie
30
 * @author     Mike Churchward (mike@brickfieldlabs.ie)
31
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 */
33
class accessibility {
34
 
35
    /** @var string The component sub path */
36
    private static $pluginpath = 'tool/brickfield';
37
 
38
    /** @var string Supported format of topics */
39
    const TOOL_BRICKFIELD_FORMAT_TOPIC = 'topics';
40
 
41
    /** @var string Supported format of weeks */
42
    const TOOL_BRICKFIELD_FORMAT_WEEKLY = 'weeks';
43
 
44
    /**
45
     * Return the state of the site enable condition.
46
     * @return bool
47
     */
48
    public static function is_accessibility_enabled(): bool {
49
        global $CFG;
50
 
51
        return !empty($CFG->enableaccessibilitytools);
52
    }
53
 
54
    /**
55
     * Throw an error if the toolkit is not enabled.
56
     * @return bool
57
     * @throws moodle_exception
58
     */
59
    public static function require_accessibility_enabled(): bool {
60
        if (!static::is_accessibility_enabled()) {
61
            throw new moodle_exception('accessibilitydisabled', manager::PLUGINNAME);
62
        }
63
        return true;
64
    }
65
 
66
    /**
67
     * Get a URL for a page within the plugin.
68
     *
69
     * This takes into account the value of the admin config value.
70
     *
71
     * @param   string $url The URL within the plugin
72
     * @return  moodle_url
73
     */
74
    public static function get_plugin_url(string $url = ''): moodle_url {
75
        $url = ($url == '') ? 'index.php' : $url;
76
        $pluginpath = self::$pluginpath;
77
        return new moodle_url("/admin/{$pluginpath}/{$url}");
78
    }
79
 
80
    /**
81
     * Get a file path for a file within the plugin.
82
     *
83
     * This takes into account the value of the admin config value.
84
     *
85
     * @param   string $path The path within the plugin
86
     * @return  string
87
     */
88
    public static function get_file_path(string $path): string {
89
        global $CFG;
90
 
91
        return implode(DIRECTORY_SEPARATOR, [$CFG->dirroot, $CFG->admin, self::$pluginpath, $path, ]);
92
    }
93
 
94
    /**
95
     * Get the canonicalised name of a capability.
96
     *
97
     * @param   string $capability
98
     * @return  string
99
     */
100
    public static function get_capability_name(string $capability): string {
101
        return self::$pluginpath . ':' . $capability;
102
    }
103
 
104
    /**
105
     * Get the relevant title.
106
     * @param filter $filter
107
     * @param int $countdata
108
     * @return string
109
     * @throws \coding_exception
110
     * @throws \dml_exception
111
     * @throws \moodle_exception
112
     */
113
    public static function get_title(filter $filter, int $countdata): string {
114
        global $DB;
115
 
116
        $tmp = new \stdClass();
117
        $tmp->count = $countdata;
118
        $langstr = 'title' . $filter->tab . 'partial';
119
 
120
        if ($filter->courseid != 0) {
121
            $thiscourse = get_fast_modinfo($filter->courseid)->get_course();
122
            $tmp->name = $thiscourse->fullname;
123
        } else {
124
            $langstr = 'title' . $filter->tab . 'all';
125
        }
126
        return get_string($langstr, manager::PLUGINNAME, $tmp);
127
    }
128
 
129
    /**
130
     * Function to be run periodically according to the scheduled task.
131
     * Return true if a process was completed. False if no process executed.
132
     * Finds all unprocessed courses for bulk batch processing and completes them.
133
     * @param int $batch
134
     * @return bool
135
     * @throws \ReflectionException
136
     * @throws \coding_exception
137
     * @throws \ddl_exception
138
     * @throws \ddl_table_missing_exception
139
     * @throws \dml_exception
140
     */
141
    public static function bulk_process_courses_cron(int $batch = 0): bool {
142
        global $PAGE;
143
 
144
        // Run a registration check.
145
        if (!(new registration())->validate()) {
146
            return false;
147
        }
148
 
149
        if (analysis::is_enabled()) {
150
            $PAGE->set_context(context_system::instance());
151
            mtrace("Starting cron for bulk_process_courses");
152
            // Do regular processing. True if full deployment type isn't selected as well.
153
            static::bulk_processing($batch);
154
            mtrace("Ending cron for bulk_process_courses");
155
            return true;
156
        } else {
157
            mtrace('Content analysis is currently disabled in settings.');
158
            return false;
159
        }
160
    }
161
 
162
    /**
163
     * Bulk processing.
164
     * @param int $batch
165
     * @return bool
166
     */
167
    protected static function bulk_processing(int $batch = 0): bool {
168
        manager::check_course_updates();
169
        mtrace("check_course_updates completed at " . time());
170
        $recordsprocessed = manager::check_scheduled_areas($batch);
171
        mtrace("check_scheduled_areas completed at " . time());
172
        manager::check_scheduled_deletions();
173
        mtrace("check_scheduled_deletions completed at " . time());
174
        manager::delete_historical_data();
175
        mtrace("delete_historical_data completed at " . time());
176
        return $recordsprocessed;
177
    }
178
 
179
    /**
180
     * Function to be run periodically according to the scheduled task.
181
     * Finds all unprocessed courses for cache processing and completes them.
182
     */
183
    public static function bulk_process_caches_cron() {
184
        global $DB;
185
 
186
        // Run a registration check.
187
        if (!(new registration())->validate()) {
188
            return;
189
        }
190
 
191
        if (analysis::is_enabled()) {
192
            mtrace("Starting cron for bulk_process_caches");
193
            // Monitor ongoing caching requests.
194
            $fields = 'DISTINCT courseid';
195
            $reruns = $DB->get_records(manager::DB_PROCESS, ['item' => 'cache'], '', $fields);
196
            foreach ($reruns as $rerun) {
197
                mtrace("Running rerun caching for Courseid " . $rerun->courseid);
198
                manager::store_result_summary($rerun->courseid);
199
                mtrace("rerun cache completed at " . time());
200
                $DB->delete_records(manager::DB_PROCESS, ['courseid' => $rerun->courseid, 'item' => 'cache']);
201
            }
202
            mtrace("Ending cron for bulk_process_caches at " . time());
203
        } else {
204
            mtrace('Content analysis is currently disabled in settings.');
205
        }
206
    }
207
 
208
    /**
209
     * This function runs the checks on the html item
210
     *
211
     * @param string $html The html string to be analysed; might be NULL.
212
     * @param int $contentid The content area ID
213
     * @param int $processingtime
214
     * @param int $resultstime
215
     */
216
    public static function run_check(string $html, int $contentid, int &$processingtime, int &$resultstime) {
217
        global $DB;
218
 
219
        // Change the limit if 10,000 is not appropriate.
220
        $bulkrecordlimit = manager::BULKRECORDLIMIT;
221
        $bulkrecordcount = 0;
222
 
223
        $checkids = static::checkids();
224
        $checknameids = array_flip($checkids);
225
 
226
        $testname = 'brickfield';
227
 
228
        $stime = time();
229
 
230
        // Swapping in new library.
231
        $htmlchecker = new local\htmlchecker\brickfield_accessibility($html, $testname, 'string');
232
        $htmlchecker->run_check();
233
        $tests = $htmlchecker->guideline->get_tests();
234
        $report = $htmlchecker->get_report();
235
        $processingtime += (time() - $stime);
236
 
237
        $records = [];
238
        foreach ($tests as $test) {
239
            $records[$test]['count'] = 0;
240
            $records[$test]['errors'] = [];
241
        }
242
 
243
        foreach ($report['report'] as $a) {
244
            if (!isset($a['type'])) {
245
                continue;
246
            }
247
            $type = $a['type'];
248
            $records[$type]['errors'][] = $a;
249
            if (!isset($records[$type]['count'])) {
250
                $records[$type]['count'] = 0;
251
            }
252
            $records[$type]['count']++;
253
        }
254
 
255
        $stime = time();
256
        $returnchecks = [];
257
        $errors = [];
258
 
259
        // Build up records for inserting.
260
        foreach ($records as $key => $rec) {
261
            $recordres = new stdClass();
262
            // Handling if checkid is unknown.
263
            $checkid = (isset($checknameids[$key])) ? $checknameids[$key] : 0;
264
            $recordres->contentid = $contentid;
265
            $recordres->checkid = $checkid;
266
            $recordres->errorcount = $rec['count'];
267
 
268
            // Build error inserts if needed.
269
            if ($rec['count'] > 0) {
270
                foreach ($rec['errors'] as $tmp) {
271
                    $error = new stdClass();
272
                    $error->resultid = 0;
273
                    $error->linenumber = $tmp['lineNo'];
274
                    $error->htmlcode = $tmp['html'];
275
                    $error->errordescription = $tmp['title'];
276
                    // Add contentid and checkid so that we can query for the results record id later.
277
                    $error->contentid = $contentid;
278
                    $error->checkid = $checkid;
279
                    $errors[] = $error;
280
                }
281
            }
282
            $returnchecks[] = $recordres;
283
            $bulkrecordcount++;
284
 
285
            // If we've hit the bulk limit, write the results records and reset.
286
            if ($bulkrecordcount > $bulkrecordlimit) {
287
                $DB->insert_records(manager::DB_RESULTS, $returnchecks);
288
                $bulkrecordcount = 0;
289
                $returnchecks = [];
290
                // Get the results id value for each error record and write the errors.
291
                foreach ($errors as $key2 => $error) {
292
                    $errors[$key2]->resultid = $DB->get_field(manager::DB_RESULTS, 'id',
293
                        ['contentid' => $error->contentid, 'checkid' => $error->checkid]);
294
                    unset($errors[$key2]->contentid);
295
                    unset($errors[$key2]->checkid);
296
                }
297
                $DB->insert_records(manager::DB_ERRORS, $errors);
298
                $errors = [];
299
            }
300
        }
301
 
302
        // Write any leftover records.
303
        if ($bulkrecordcount > 0) {
304
            $DB->insert_records(manager::DB_RESULTS, $returnchecks);
305
            // Get the results id value for each error record and write the errors.
306
            foreach ($errors as $key => $error) {
307
                $errors[$key]->resultid = $DB->get_field(manager::DB_RESULTS, 'id',
308
                    ['contentid' => $error->contentid, 'checkid' => $error->checkid]);
309
                unset($errors[$key]->contentid);
310
                unset($errors[$key]->checkid);
311
            }
312
            $DB->insert_records(manager::DB_ERRORS, $errors);
313
        }
314
 
315
        $resultstime += (time() - $stime);
316
    }
317
 
318
    /**
319
     * This function runs one specified check on the html item
320
     *
321
     * @param string|null $html The html string to be analysed; might be NULL.
322
     * @param int $contentid The content area ID
323
     * @param int $errid The error ID
324
     * @param string $check The check name to run
325
     * @param int $processingtime
326
     * @param int $resultstime
327
     * @throws \coding_exception
328
     * @throws \dml_exception
329
     */
330
    public static function run_one_check(
331
        ?string $html,
332
        int $contentid,
333
        int $errid,
334
        string $check,
335
        int &$processingtime,
336
        int &$resultstime
337
    ) {
338
        global $DB;
339
 
340
        $stime = time();
341
 
342
        $checkdata = $DB->get_record(manager::DB_CHECKS, ['shortname' => $check], 'id,shortname,severity');
343
 
344
        $testname = 'brickfield';
345
 
346
        // Swapping in new library.
347
        $htmlchecker = new local\htmlchecker\brickfield_accessibility($html, $testname, 'string');
348
        $htmlchecker->run_check();
349
        $report = $htmlchecker->get_test($check);
350
        $processingtime += (time() - $stime);
351
 
352
        $record = [];
353
        $record['count'] = 0;
354
        $record['errors'] = [];
355
 
356
        foreach ($report as $a) {
357
            $a->html = $a->get_html();
358
            $record['errors'][] = $a;
359
            $record['count']++;
360
        }
361
 
362
        // Build up record for inserting.
363
        $recordres = new stdClass();
364
        // Handling if checkid is unknown.
365
        $checkid = (isset($checkdata->id)) ? $checkdata->id : 0;
366
        $recordres->contentid = $contentid;
367
        $recordres->checkid = $checkid;
368
        $recordres->errorcount = $record['count'];
369
        if ($exists = $DB->get_record(manager::DB_RESULTS, ['contentid' => $contentid, 'checkid' => $checkid])) {
370
            $resultid = $exists->id;
371
            $DB->set_field(manager::DB_RESULTS, 'errorcount', $record['count'], ['id' => $resultid]);
372
            // Remove old error records for specific resultid, if existing.
373
            $DB->delete_records(manager::DB_ERRORS, ['id' => $errid]);
374
        } else {
375
            $resultid = $DB->insert_record(manager::DB_RESULTS, $recordres);
376
        }
377
        $errors = [];
378
 
379
        // Build error inserts if needed.
380
        if ($record['count'] > 0) {
381
            // Reporting all found errors for this check, so need to ignore existing other error records.
382
            foreach ($record['errors'] as $tmp) {
383
                // Confirm if error is reported separately.
384
                if ($DB->record_exists_select(manager::DB_ERRORS,
385
                    'resultid = ? AND ' . $DB->sql_compare_text('htmlcode', 255) . ' = ' . $DB->sql_compare_text('?', 255),
386
                    [$resultid, html_entity_decode($tmp->html, ENT_COMPAT)])) {
387
                    continue;
388
                }
389
                $error = new stdClass();
390
                $error->resultid = $resultid;
391
                $error->linenumber = $tmp->line;
392
                $error->htmlcode = html_entity_decode($tmp->html, ENT_COMPAT);
393
                $errors[] = $error;
394
            }
395
 
396
            $DB->insert_records(manager::DB_ERRORS, $errors);
397
        }
398
 
399
        $resultstime += (time() - $stime);
400
    }
401
 
402
    /**
403
     * Returns all of the id's and shortnames of all of the checks.
404
     * @param int $status
405
     * @return array
406
     * @throws \dml_exception
407
     */
408
    public static function checkids(int $status = 1): array {
409
        global $DB;
410
 
411
        $checks = $DB->get_records_menu(manager::DB_CHECKS, ['status' => $status], 'id ASC', 'id,shortname');
412
        return $checks;
413
    }
414
 
415
    /**
416
     * Returns an array of translations from htmlchecker of all of the checks, and their descriptions.
417
     * @return array
418
     * @throws \dml_exception
419
     */
420
    public static function get_translations(): array {
421
        global $DB;
422
 
423
        $htmlchecker = new local\htmlchecker\brickfield_accessibility('test', 'brickfield', 'string');
424
        $htmlchecker->run_check();
425
        ksort($htmlchecker->guideline->translations);
426
 
427
        // Need to limit to active checks.
428
        $activechecks = $DB->get_fieldset_select(manager::DB_CHECKS, 'shortname', 'status = :status', ['status' => 1]);
429
 
430
        $translations = [];
431
        foreach ($htmlchecker->guideline->translations as $key => $trans) {
432
            if (in_array($key, $activechecks)) {
433
                $translations[$key] = $trans;
434
            }
435
        }
436
 
437
        return $translations;
438
    }
439
 
440
    /**
441
     * Returns an array of all of the course id's for a given category.
442
     * @param int $categoryid
443
     * @return array|null
444
     * @throws \dml_exception
445
     */
446
    public static function get_category_courseids(int $categoryid): ?array {
447
        global $DB;
448
 
449
        if (!$DB->record_exists('course_categories', ['id' => $categoryid])) {
450
            return null;
451
        }
452
 
453
        $sql = "SELECT {course}.id
454
              FROM {course}, {course_categories}
455
             WHERE {course}.category = {course_categories}.id
456
               AND (
457
                " . $DB->sql_like('path', ':categoryid1') . "
458
             OR " . $DB->sql_like('path', ':categoryid2') . "
459
        )";
460
        $params = ['categoryid1' => "%/$categoryid/%", 'categoryid2' => "%/$categoryid"];
461
        $courseids = $DB->get_fieldset_sql($sql, $params);
462
 
463
        return $courseids;
464
    }
465
 
466
    /**
467
     * Get summary data for this site.
468
     * @param int $id
469
     * @return \stdClass
470
     * @throws \dml_exception
471
     */
472
    public static function get_summary_data(int $id): \stdClass {
473
        global $CFG, $DB;
474
 
475
        $summarydata = new \stdClass();
476
        $summarydata->siteurl = (substr($CFG->wwwroot, -1) !== '/') ? $CFG->wwwroot . '/' : $CFG->wwwroot;
477
        $summarydata->moodlerelease = (preg_match('/^(\d+\.\d.*?)[. ]/', $CFG->release, $matches)) ? $matches[1] : $CFG->release;
478
        $summarydata->numcourses = $DB->count_records('course') - 1;
479
        $summarydata->numusers = $DB->count_records('user', array('deleted' => 0));
480
        $summarydata->numfiles = $DB->count_records('files');
481
        $summarydata->numfactivities = $DB->count_records('course_modules');
482
        $summarydata->mobileservice = (int)$CFG->enablemobilewebservice === 1 ? true : false;
483
        $summarydata->usersmobileregistered = $DB->count_records('user_devices');
484
        $summarydata->contenttyperesults = static::get_contenttyperesults($id);
485
        $summarydata->contenttypeerrors = static::get_contenttypeerrors();
486
        $summarydata->percheckerrors = static::get_percheckerrors();
487
        return $summarydata;
488
    }
489
 
490
    /**
491
     * Get content type results.
492
     * @param int $id
493
     * @return \stdClass
494
     */
495
    private static function get_contenttyperesults(int $id): \stdClass {
496
        global $DB;
497
        $sql = 'SELECT component, COUNT(id) AS count
498
                  FROM {' . manager::DB_AREAS . '}
499
              GROUP BY component';
500
        $components = $DB->get_recordset_sql($sql);
501
        $contenttyperesults = new \stdClass();
502
        $contenttyperesults->id = $id;
503
        $contenttyperesults->contenttype = new \stdClass();
504
        foreach ($components as $component) {
505
            $componentname = $component->component;
506
            $contenttyperesults->contenttype->$componentname = $component->count;
507
        }
508
        $components->close();
509
        $contenttyperesults->summarydatastorage = static::get_summary_data_storage();
510
        $contenttyperesults->datachecked = time();
511
        return $contenttyperesults;
512
    }
513
 
514
 
515
    /**
516
     * Get per check errors.
517
     * @return stdClass
518
     * @throws dml_exception
519
     */
520
    private static function get_percheckerrors(): stdClass {
521
        global $DB;
522
 
523
        $sql = 'SELECT ' . $DB->sql_concat_join("'_'", ['courseid', 'checkid']) . ' as tmpid,
524
                       ca.courseid, ca.status, ca.checkid, ch.shortname, ca.checkcount, ca.errorcount
525
                  FROM {' . manager::DB_CACHECHECK . '} ca
526
            INNER JOIN {' . manager::DB_CHECKS . '} ch on ch.id = ca.checkid
527
              ORDER BY courseid, checkid ASC';
528
 
529
        $combo = $DB->get_records_sql($sql);
530
 
531
        return (object) [
532
            'percheckerrors' => $combo,
533
        ];
534
    }
535
 
536
    /**
537
     * Get content type errors.
538
     * @return stdClass
539
     * @throws dml_exception
540
     */
541
    private static function get_contenttypeerrors(): stdClass {
542
        global $DB;
543
 
544
        $fields = 'courseid, status, activities, activitiespassed, activitiesfailed,
545
                    errorschecktype1, errorschecktype2, errorschecktype3, errorschecktype4,
546
                    errorschecktype5, errorschecktype6, errorschecktype7,
547
                    failedchecktype1, failedchecktype2, failedchecktype3, failedchecktype4,
548
                    failedchecktype5, failedchecktype6, failedchecktype7,
549
                    percentchecktype1, percentchecktype2, percentchecktype3, percentchecktype4,
550
                    percentchecktype5, percentchecktype6, percentchecktype7';
551
        $combo = $DB->get_records(manager::DB_SUMMARY, null, 'courseid ASC', $fields);
552
 
553
        return (object) [
554
            'typeerrors' => $combo,
555
        ];
556
    }
557
 
558
    /**
559
     * Get summary data storage.
560
     * @return array
561
     * @throws dml_exception
562
     */
563
    private static function get_summary_data_storage(): array {
564
        global $DB;
565
 
566
        $fields = $DB->sql_concat_join("''", ['component', 'courseid']) . ' as tmpid,
567
                 courseid, component, errorcount, totalactivities, failedactivities, passedactivities';
568
        $combo = $DB->get_records(manager::DB_CACHEACTS, null, 'courseid, component ASC', $fields);
569
        return $combo;
570
    }
571
}