cd716ef00846f87bfb43da285dcdd94fe3c098d8
[moodle.git] / lib / classes / hub / registration.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 /**
18  * Class registration
19  *
20  * @package    core
21  * @copyright  2017 Marina Glancy
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace core\hub;
26 defined('MOODLE_INTERNAL') || die();
28 use moodle_exception;
29 use moodle_url;
30 use context_system;
31 use stdClass;
32 use html_writer;
34 /**
35  * Methods to use when registering the site at the moodle sites directory.
36  *
37  * @package    core
38  * @copyright  2017 Marina Glancy
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class registration {
43     /** @var array Fields used in a site registration form.
44      * IMPORTANT: any new fields with non-empty defaults have to be added to CONFIRM_NEW_FIELDS */
45     const FORM_FIELDS = ['policyagreed', 'language', 'countrycode', 'privacy',
46         'contactemail', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail',
47         'contactname', 'name', 'description', 'imageurl', 'contactphone', 'regioncode', 'geolocation', 'street'];
49     /** @var List of new FORM_FIELDS or siteinfo fields added indexed by the version when they were added.
50      * If site was already registered, admin will be promted to confirm new registration data manually. Until registration is manually confirmed,
51      * the scheduled task updating registration will be paused.
52      * Keys of this array are not important as long as they increment, use current date to avoid confusions.
53      */
54     const CONFIRM_NEW_FIELDS = [
55         2017092200 => [
56             'commnews', // Receive communication news. This was added in 3.4 and is "On" by default. Admin must confirm or opt-out.
57             'mobileservicesenabled', 'mobilenotificationsenabled', 'registereduserdevices', 'registeredactiveuserdevices' // Mobile stats added in 3.4.
58         ],
59         // Analytics stats added in Moodle 3.7.
60         2019022200 => ['analyticsenabledmodels', 'analyticspredictions', 'analyticsactions', 'analyticsactionsnotuseful'],
61     ];
63     /** @var Site privacy: not displayed */
64     const HUB_SITENOTPUBLISHED = 'notdisplayed';
66     /** @var Site privacy: public */
67     const HUB_SITENAMEPUBLISHED = 'named';
69     /** @var Site privacy: public and global */
70     const HUB_SITELINKPUBLISHED = 'linked';
72     /** @var stdClass cached site registration information */
73     protected static $registration = null;
75     /**
76      * Get site registration
77      *
78      * @param bool $confirmed
79      * @return stdClass|null
80      */
81     protected static function get_registration($confirmed = true) {
82         global $DB;
84         if (self::$registration === null) {
85             self::$registration = $DB->get_record('registration_hubs', ['huburl' => HUB_MOODLEORGHUBURL]) ?: null;
86         }
88         if (self::$registration && (bool)self::$registration->confirmed == (bool)$confirmed) {
89             return self::$registration;
90         }
92         return null;
93     }
95     /**
96      * Same as get_registration except it throws exception if site not registered
97      *
98      * @return stdClass
99      * @throws \moodle_exception
100      */
101     public static function require_registration() {
102         if ($registration = self::get_registration()) {
103             return $registration;
104         }
105         if (has_capability('moodle/site:config', context_system::instance())) {
106             throw new moodle_exception('registrationwarning', 'admin', new moodle_url('/admin/registration/index.php'));
107         } else {
108             throw new moodle_exception('registrationwarningcontactadmin', 'admin');
109         }
110     }
112     /**
113      * Checks if site is registered
114      *
115      * @return bool
116      */
117     public static function is_registered() {
118         return self::get_registration() ? true : false;
119     }
121     /**
122      * Returns registration token
123      *
124      * @param int $strictness if set to MUST_EXIST and site is not registered will throw an exception
125      * @return null
126      * @throws moodle_exception
127      */
128     public static function get_token($strictness = IGNORE_MISSING) {
129         if ($strictness == MUST_EXIST) {
130             $registration = self::require_registration();
131         } else if (!$registration = self::get_registration()) {
132             return null;
133         }
134         return $registration->token;
135     }
137     /**
138      * When was the registration last updated
139      *
140      * @return int|null timestamp or null if site is not registered
141      */
142     public static function get_last_updated() {
143         if ($registration = self::get_registration()) {
144             return $registration->timemodified;
145         }
146         return null;
147     }
149     /**
150      * Calculates and prepares site information to send to the sites directory as a part of registration.
151      *
152      * @param array $defaults default values for inputs in the registration form (if site was never registered before)
153      * @return array site info
154      */
155     public static function get_site_info($defaults = []) {
156         global $CFG, $DB;
157         require_once($CFG->libdir . '/badgeslib.php');
158         require_once($CFG->dirroot . "/course/lib.php");
160         $siteinfo = array();
161         foreach (self::FORM_FIELDS as $field) {
162             $siteinfo[$field] = get_config('hub', 'site_'.$field);
163             if ($siteinfo[$field] === false) {
164                 $siteinfo[$field] = array_key_exists($field, $defaults) ? $defaults[$field] : null;
165             }
166         }
168         // Statistical data.
169         $siteinfo['courses'] = $DB->count_records('course') - 1;
170         $siteinfo['users'] = $DB->count_records('user', array('deleted' => 0));
171         $siteinfo['enrolments'] = $DB->count_records('role_assignments');
172         $siteinfo['posts'] = $DB->count_records('forum_posts');
173         $siteinfo['questions'] = $DB->count_records('question');
174         $siteinfo['resources'] = $DB->count_records('resource');
175         $siteinfo['badges'] = $DB->count_records_select('badge', 'status <> ' . BADGE_STATUS_ARCHIVED);
176         $siteinfo['issuedbadges'] = $DB->count_records('badge_issued');
177         $siteinfo['participantnumberaverage'] = average_number_of_participants();
178         $siteinfo['modulenumberaverage'] = average_number_of_courses_modules();
180         // Version and url.
181         $siteinfo['moodlerelease'] = $CFG->release;
182         $siteinfo['url'] = $CFG->wwwroot;
184         // Mobile related information.
185         $siteinfo['mobileservicesenabled'] = 0;
186         $siteinfo['mobilenotificationsenabled'] = 0;
187         $siteinfo['registereduserdevices'] = 0;
188         $siteinfo['registeredactiveuserdevices'] = 0;
189         if (!empty($CFG->enablewebservices) && !empty($CFG->enablemobilewebservice)) {
190             $siteinfo['mobileservicesenabled'] = 1;
191             $siteinfo['registereduserdevices'] = $DB->count_records('user_devices');
192             $airnotifierextpath = $CFG->dirroot . '/message/output/airnotifier/externallib.php';
193             if (file_exists($airnotifierextpath)) { // Maybe some one uninstalled the plugin.
194                 require_once($airnotifierextpath);
195                 $siteinfo['mobilenotificationsenabled'] = \message_airnotifier_external::is_system_configured();
196                 $siteinfo['registeredactiveuserdevices'] = $DB->count_records('message_airnotifier_devices', array('enable' => 1));
197             }
198         }
200         // Analytics related data follow.
201         $siteinfo['analyticsenabledmodels'] = \core_analytics\stats::enabled_models();
202         $siteinfo['analyticspredictions'] = \core_analytics\stats::predictions();
203         $siteinfo['analyticsactions'] = \core_analytics\stats::actions();
204         $siteinfo['analyticsactionsnotuseful'] = \core_analytics\stats::actions_not_useful();
206         // IMPORTANT: any new fields in siteinfo have to be added to the constant CONFIRM_NEW_FIELDS.
208         return $siteinfo;
209     }
211     /**
212      * Human-readable summary of data that will be sent to the sites directory.
213      *
214      * @param array $siteinfo result of get_site_info()
215      * @return string
216      */
217     public static function get_stats_summary($siteinfo) {
218         $fieldsneedconfirm = self::get_new_registration_fields();
219         $summary = html_writer::tag('p', get_string('sendfollowinginfo_help', 'hub')) .
220             html_writer::start_tag('ul');
222         $mobileservicesenabled = $siteinfo['mobileservicesenabled'] ? get_string('yes') : get_string('no');
223         $mobilenotificationsenabled = $siteinfo['mobilenotificationsenabled'] ? get_string('yes') : get_string('no');
224         $moodlerelease = $siteinfo['moodlerelease'];
225         if (preg_match('/^(\d+\.\d.*?)[\. ]/', $moodlerelease, $matches)) {
226             $moodlerelease = $matches[1];
227         }
228         $senddata = [
229             'moodlerelease' => get_string('sitereleasenum', 'hub', $moodlerelease),
230             'courses' => get_string('coursesnumber', 'hub', $siteinfo['courses']),
231             'users' => get_string('usersnumber', 'hub', $siteinfo['users']),
232             'enrolments' => get_string('roleassignmentsnumber', 'hub', $siteinfo['enrolments']),
233             'posts' => get_string('postsnumber', 'hub', $siteinfo['posts']),
234             'questions' => get_string('questionsnumber', 'hub', $siteinfo['questions']),
235             'resources' => get_string('resourcesnumber', 'hub', $siteinfo['resources']),
236             'badges' => get_string('badgesnumber', 'hub', $siteinfo['badges']),
237             'issuedbadges' => get_string('issuedbadgesnumber', 'hub', $siteinfo['issuedbadges']),
238             'participantnumberaverage' => get_string('participantnumberaverage', 'hub',
239                 format_float($siteinfo['participantnumberaverage'], 2)),
240             'modulenumberaverage' => get_string('modulenumberaverage', 'hub',
241                 format_float($siteinfo['modulenumberaverage'], 2)),
242             'mobileservicesenabled' => get_string('mobileservicesenabled', 'hub', $mobileservicesenabled),
243             'mobilenotificationsenabled' => get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsenabled),
244             'registereduserdevices' => get_string('registereduserdevices', 'hub', $siteinfo['registereduserdevices']),
245             'registeredactiveuserdevices' => get_string('registeredactiveuserdevices', 'hub', $siteinfo['registeredactiveuserdevices']),
246             'analyticsenabledmodels' => get_string('analyticsenabledmodels', 'hub', $siteinfo['analyticsenabledmodels']),
247             'analyticspredictions' => get_string('analyticspredictions', 'hub', $siteinfo['analyticspredictions']),
248             'analyticsactions' => get_string('analyticsactions', 'hub', $siteinfo['analyticsactions']),
249             'analyticsactionsnotuseful' => get_string('analyticsactionsnotuseful', 'hub', $siteinfo['analyticsactionsnotuseful']),
250         ];
252         foreach ($senddata as $key => $str) {
253             $class = in_array($key, $fieldsneedconfirm) ? ' needsconfirmation mark' : '';
254             $summary .= html_writer::tag('li', $str, ['class' => 'site' . $key . $class]);
255         }
256         $summary .= html_writer::end_tag('ul');
257         return $summary;
258     }
260     /**
261      * Save registration info locally so it can be retrieved when registration needs to be updated
262      *
263      * @param stdClass $formdata data from {@link site_registration_form}
264      */
265     public static function save_site_info($formdata) {
266         foreach (self::FORM_FIELDS as $field) {
267             set_config('site_' . $field, $formdata->$field, 'hub');
268         }
269         // Even if the connection with the sites directory fails, admin has manually submitted the form which means they don't need
270         // to be redirected to the site registration page any more.
271         set_config('site_regupdateversion', max(array_keys(self::CONFIRM_NEW_FIELDS)), 'hub');
272     }
274     /**
275      * Updates site registration when "Update reigstration" button is clicked by admin
276      */
277     public static function update_manual() {
278         global $DB;
280         if (!$registration = self::get_registration()) {
281             return false;
282         }
284         $siteinfo = self::get_site_info();
285         try {
286             api::update_registration($siteinfo);
287         } catch (moodle_exception $e) {
288             if (!self::is_registered()) {
289                 // Token was rejected during registration update and site and locally stored token was reset,
290                 // proceed to site registration. This method will redirect away.
291                 self::register('');
292             }
293             \core\notification::add(get_string('errorregistrationupdate', 'hub', $e->getMessage()),
294                 \core\output\notification::NOTIFY_ERROR);
295             return false;
296         }
297         $DB->update_record('registration_hubs', ['id' => $registration->id, 'timemodified' => time()]);
298         \core\notification::add(get_string('siteregistrationupdated', 'hub'),
299             \core\output\notification::NOTIFY_SUCCESS);
300         self::$registration = null;
301         return true;
302     }
304     /**
305      * Updates site registration via cron
306      *
307      * @throws moodle_exception
308      */
309     public static function update_cron() {
310         global $DB;
312         if (!$registration = self::get_registration()) {
313             mtrace(get_string('registrationwarning', 'admin'));
314             return;
315         }
317         if (self::get_new_registration_fields()) {
318             mtrace(get_string('pleaserefreshregistrationnewdata', 'admin'));
319             return;
320         }
322         $siteinfo = self::get_site_info();
323         api::update_registration($siteinfo);
324         $DB->update_record('registration_hubs', ['id' => $registration->id, 'timemodified' => time()]);
325         mtrace(get_string('siteregistrationupdated', 'hub'));
326         self::$registration = null;
327     }
329     /**
330      * Confirms registration by the sites directory.
331      *
332      * @param string $token
333      * @param string $newtoken
334      * @param string $hubname
335      * @throws moodle_exception
336      */
337     public static function confirm_registration($token, $newtoken, $hubname) {
338         global $DB;
340         $registration = self::get_registration(false);
341         if (!$registration || $registration->token !== $token) {
342             throw new moodle_exception('wrongtoken', 'hub', new moodle_url('/admin/registration/index.php'));
343         }
344         $record = ['id' => $registration->id];
345         $record['token'] = $newtoken;
346         $record['confirmed'] = 1;
347         $record['hubname'] = $hubname;
348         $record['timemodified'] = time();
349         $DB->update_record('registration_hubs', $record);
350         self::$registration = null;
352         $siteinfo = self::get_site_info();
353         if (strlen(http_build_query($siteinfo)) > 1800) {
354             // Update registration again because the initial request was too long and could have been truncated.
355             api::update_registration($siteinfo);
356             self::$registration = null;
357         }
358     }
360     /**
361      * Retrieve the options for site privacy form element to use in registration form
362      * @return array
363      */
364     public static function site_privacy_options() {
365         return [
366             self::HUB_SITENOTPUBLISHED => get_string('siteprivacynotpublished', 'hub'),
367             self::HUB_SITENAMEPUBLISHED => get_string('siteprivacypublished', 'hub'),
368             self::HUB_SITELINKPUBLISHED => get_string('siteprivacylinked', 'hub')
369         ];
370     }
372     /**
373      * Registers a site
374      *
375      * This method will make sure that unconfirmed registration record is created and then redirect to
376      * registration script on the sites directory.
377      * The sites directory will check that the site is accessible, register it and redirect back
378      * to /admin/registration/confirmregistration.php
379      *
380      * @param string $returnurl
381      * @throws \coding_exception
382      */
383     public static function register($returnurl) {
384         global $DB, $SESSION;
386         if (self::is_registered()) {
387             // Caller of this method must make sure that site is not registered.
388             throw new \coding_exception('Site already registered');
389         }
391         $hub = self::get_registration(false);
392         if (empty($hub)) {
393             // Create a new record in 'registration_hubs'.
394             $hub = new stdClass();
395             $hub->token = get_site_identifier();
396             $hub->secret = $hub->token;
397             $hub->huburl = HUB_MOODLEORGHUBURL;
398             $hub->hubname = 'moodle';
399             $hub->confirmed = 0;
400             $hub->timemodified = time();
401             $hub->id = $DB->insert_record('registration_hubs', $hub);
402             self::$registration = null;
403         }
405         $params = self::get_site_info();
407         // The most conservative limit for the redirect URL length is 2000 characters. Only pass parameters before
408         // we reach this limit. The next registration update will update all fields.
409         // We will also update registration after we receive confirmation from moodle.net.
410         $url = new moodle_url(HUB_MOODLEORGHUBURL . '/local/hub/siteregistration.php',
411             ['token' => $hub->token, 'url' => $params['url']]);
412         foreach ($params as $key => $value) {
413             if (strlen($url->out(false, [$key => $value])) > 2000) {
414                 break;
415             }
416             $url->param($key, $value);
417         }
419         $SESSION->registrationredirect = $returnurl;
420         redirect($url);
421     }
423     /**
424      * Unregister site
425      *
426      * @param bool $unpublishalladvertisedcourses
427      * @param bool $unpublishalluploadedcourses
428      * @return bool
429      */
430     public static function unregister($unpublishalladvertisedcourses, $unpublishalluploadedcourses) {
431         global $DB;
433         if (!$hub = self::get_registration()) {
434             return true;
435         }
437         // Course unpublish went ok, unregister the site now.
438         try {
439             api::unregister_site();
440         } catch (moodle_exception $e) {
441             \core\notification::add(get_string('unregistrationerror', 'hub', $e->getMessage()),
442                 \core\output\notification::NOTIFY_ERROR);
443             return false;
444         }
446         $DB->delete_records('registration_hubs', array('id' => $hub->id));
447         self::$registration = null;
448         return true;
449     }
451     /**
452      * Resets the registration token without changing site identifier so site can be re-registered
453      *
454      * @return bool
455      */
456     public static function reset_token() {
457         global $DB;
458         if (!$hub = self::get_registration()) {
459             return true;
460         }
461         $DB->delete_records('registration_hubs', array('id' => $hub->id));
462         self::$registration = null;
463     }
465     /**
466      * Generate a new token for the site that is not registered
467      *
468      * @param string $token
469      * @throws moodle_exception
470      */
471     public static function reset_site_identifier($token) {
472         global $DB, $CFG;
474         $registration = self::get_registration(false);
475         if (!$registration || $registration->token != $token) {
476             throw new moodle_exception('wrongtoken', 'hub',
477                new moodle_url('/admin/registration/index.php'));
478         }
480         $DB->delete_records('registration_hubs', array('id' => $registration->id));
481         self::$registration = null;
483         $CFG->siteidentifier = null;
484         get_site_identifier();
485     }
487     /**
488      * Returns information about the sites directory.
489      *
490      * Example of the return array:
491      * {
492      *     "courses": 384,
493      *     "description": "Official moodle sites directory",
494      *     "downloadablecourses": 0,
495      *     "enrollablecourses": 0,
496      *     "hublogo": 1,
497      *     "language": "en",
498      *     "name": "moodle",
499      *     "sites": 274175,
500      *     "url": "https://stats.moodle.org",
501      *     "imgurl": "https://stats.moodle.org/local/hub/webservice/download.php?filetype=hubscreenshot"
502      * }
503      *
504      * @return array|null
505      */
506     public static function get_moodlenet_info() {
507         try {
508             return api::get_hub_info();
509         } catch (moodle_exception $e) {
510             // Ignore error, we only need it for displaying information about the sites directory.
511             // If this request fails, it's not a big deal.
512             return null;
513         }
514     }
516     /**
517      * Does admin need to be redirected to the registration page after install?
518      *
519      * @param bool|null $markasviewed if set to true will mark the registration form as viewed and admin will not be redirected
520      *     to the registration form again (regardless of whether the site was registered or not).
521      * @return bool
522      */
523     public static function show_after_install($markasviewed = null) {
524         global $CFG;
525         if (self::is_registered()) {
526             $showregistration = false;
527             $markasviewed = true;
528         } else {
529             $showregistration = !empty($CFG->registrationpending);
530             if ($showregistration && !site_is_public()) {
531                 // If it's not a public site, don't redirect to registration, it won't work anyway.
532                 $showregistration = false;
533                 $markasviewed = true;
534             }
535         }
536         if ($markasviewed !== null) {
537             set_config('registrationpending', !$markasviewed);
538         }
539         return $showregistration;
540     }
542     /**
543      * Returns the list of the fields in the registration form that were added since registration or last manual update
544      *
545      * If this list is not empty the scheduled task will be paused and admin will be reminded to update registration manually.
546      *
547      * @return array
548      */
549     public static function get_new_registration_fields() {
550         $fieldsneedconfirm = [];
551         if (!self::is_registered()) {
552             // Nothing to update if site is not registered.
553             return $fieldsneedconfirm;
554         }
556         $lastupdated = (int)get_config('hub', 'site_regupdateversion');
557         foreach (self::CONFIRM_NEW_FIELDS as $version => $fields) {
558             if ($version > $lastupdated) {
559                 $fieldsneedconfirm = array_merge($fieldsneedconfirm, $fields);
560             }
561         }
562         return $fieldsneedconfirm;
563     }
565     /**
566      * Redirect to the site registration form if it's a new install or registration needs updating
567      *
568      * @param string|moodle_url $url
569      */
570     public static function registration_reminder($url) {
571         if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
572             // No redirection during behat runs.
573             return;
574         }
575         if (!has_capability('moodle/site:config', context_system::instance())) {
576             return;
577         }
578         if (self::show_after_install() || self::get_new_registration_fields()) {
579             $returnurl = new moodle_url($url);
580             redirect(new moodle_url('/admin/registration/index.php', ['returnurl' => $returnurl->out_as_local_url(false)]));
581         }
582     }