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