MDL-50891 useragent: Move web crawler checks to useragent class
[moodle.git] / lib / classes / update / checker.php
blob9b211ef7117a30e4232aa8394832f5b3b7c7f5cb
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.
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 * Defines classes used for updates.
20 * @package core
21 * @copyright 2011 David Mudrak <david@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 namespace core\update;
26 use html_writer, coding_exception, core_component;
28 defined('MOODLE_INTERNAL') || die();
30 /**
31 * Singleton class that handles checking for available updates
33 class checker {
35 /** @var \core\update\checker holds the singleton instance */
36 protected static $singletoninstance;
37 /** @var null|int the timestamp of when the most recent response was fetched */
38 protected $recentfetch = null;
39 /** @var null|array the recent response from the update notification provider */
40 protected $recentresponse = null;
41 /** @var null|string the numerical version of the local Moodle code */
42 protected $currentversion = null;
43 /** @var null|string the release info of the local Moodle code */
44 protected $currentrelease = null;
45 /** @var null|string branch of the local Moodle code */
46 protected $currentbranch = null;
47 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
48 protected $currentplugins = array();
50 /**
51 * Direct initiation not allowed, use the factory method {@link self::instance()}
53 protected function __construct() {
56 /**
57 * Sorry, this is singleton
59 protected function __clone() {
62 /**
63 * Factory method for this class
65 * @return \core\update\checker the singleton instance
67 public static function instance() {
68 if (is_null(self::$singletoninstance)) {
69 self::$singletoninstance = new self();
71 return self::$singletoninstance;
74 /**
75 * Reset any caches
76 * @param bool $phpunitreset
78 public static function reset_caches($phpunitreset = false) {
79 if ($phpunitreset) {
80 self::$singletoninstance = null;
84 /**
85 * Is automatic deployment enabled?
87 * @return bool
89 public function enabled() {
90 global $CFG;
92 // The feature can be prohibited via config.php.
93 return empty($CFG->disableupdateautodeploy);
96 /**
97 * Returns the timestamp of the last execution of {@link fetch()}
99 * @return int|null null if it has never been executed or we don't known
101 public function get_last_timefetched() {
103 $this->restore_response();
105 if (!empty($this->recentfetch)) {
106 return $this->recentfetch;
108 } else {
109 return null;
114 * Fetches the available update status from the remote site
116 * @throws checker_exception
118 public function fetch() {
120 $response = $this->get_response();
121 $this->validate_response($response);
122 $this->store_response($response);
124 // We need to reset plugin manager's caches - the currently existing
125 // singleton is not aware of eventually available updates we just fetched.
126 \core_plugin_manager::reset_caches();
130 * Returns the available update information for the given component
132 * This method returns null if the most recent response does not contain any information
133 * about it. The returned structure is an array of available updates for the given
134 * component. Each update info is an object with at least one property called
135 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
137 * For the 'core' component, the method returns real updates only (those with higher version).
138 * For all other components, the list of all known remote updates is returned and the caller
139 * (usually the {@link core_plugin_manager}) is supposed to make the actual comparison of versions.
141 * @param string $component frankenstyle
142 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
143 * @return null|array null or array of \core\update\info objects
145 public function get_update_info($component, array $options = array()) {
147 if (!isset($options['minmaturity'])) {
148 $options['minmaturity'] = 0;
151 if (!isset($options['notifybuilds'])) {
152 $options['notifybuilds'] = false;
155 if ($component === 'core') {
156 $this->load_current_environment();
159 $this->restore_response();
161 if (empty($this->recentresponse['updates'][$component])) {
162 return null;
165 $updates = array();
166 foreach ($this->recentresponse['updates'][$component] as $info) {
167 $update = new info($component, $info);
168 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
169 continue;
171 if ($component === 'core') {
172 if ($update->version <= $this->currentversion) {
173 continue;
175 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
176 continue;
179 $updates[] = $update;
182 if (empty($updates)) {
183 return null;
186 return $updates;
190 * The method being run via cron.php
192 public function cron() {
193 global $CFG;
195 if (!$this->cron_autocheck_enabled()) {
196 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
197 return;
200 $now = $this->cron_current_timestamp();
202 if ($this->cron_has_fresh_fetch($now)) {
203 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
204 return;
207 if ($this->cron_has_outdated_fetch($now)) {
208 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
209 $this->cron_execute();
210 return;
213 $offset = $this->cron_execution_offset();
214 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
215 if ($now > $start + $offset) {
216 $this->cron_mtrace('Regular daily check for available updates ... ', '');
217 $this->cron_execute();
218 return;
222 /* === End of public API === */
225 * Makes cURL request to get data from the remote site
227 * @return string raw request result
228 * @throws checker_exception
230 protected function get_response() {
231 global $CFG;
232 require_once($CFG->libdir.'/filelib.php');
234 $curl = new \curl(array('proxy' => true));
235 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
236 $curlerrno = $curl->get_errno();
237 if (!empty($curlerrno)) {
238 throw new checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
240 $curlinfo = $curl->get_info();
241 if ($curlinfo['http_code'] != 200) {
242 throw new checker_exception('err_response_http_code', $curlinfo['http_code']);
244 return $response;
248 * Makes sure the response is valid, has correct API format etc.
250 * @param string $response raw response as returned by the {@link self::get_response()}
251 * @throws checker_exception
253 protected function validate_response($response) {
255 $response = $this->decode_response($response);
257 if (empty($response)) {
258 throw new checker_exception('err_response_empty');
261 if (empty($response['status']) or $response['status'] !== 'OK') {
262 throw new checker_exception('err_response_status', $response['status']);
265 if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
266 throw new checker_exception('err_response_format_version', $response['apiver']);
269 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
270 throw new checker_exception('err_response_target_version', $response['forbranch']);
275 * Decodes the raw string response from the update notifications provider
277 * @param string $response as returned by {@link self::get_response()}
278 * @return array decoded response structure
280 protected function decode_response($response) {
281 return json_decode($response, true);
285 * Stores the valid fetched response for later usage
287 * This implementation uses the config_plugins table as the permanent storage.
289 * @param string $response raw valid data returned by {@link self::get_response()}
291 protected function store_response($response) {
293 set_config('recentfetch', time(), 'core_plugin');
294 set_config('recentresponse', $response, 'core_plugin');
296 if (defined('CACHE_DISABLE_ALL') and CACHE_DISABLE_ALL) {
297 // Very nasty hack to work around cache coherency issues on admin/index.php?cache=0 page,
298 // we definitely need to keep caches in sync when writing into DB at all times!
299 \cache_helper::purge_all(true);
302 $this->restore_response(true);
306 * Loads the most recent raw response record we have fetched
308 * After this method is called, $this->recentresponse is set to an array. If the
309 * array is empty, then either no data have been fetched yet or the fetched data
310 * do not have expected format (and thence they are ignored and a debugging
311 * message is displayed).
313 * This implementation uses the config_plugins table as the permanent storage.
315 * @param bool $forcereload reload even if it was already loaded
317 protected function restore_response($forcereload = false) {
319 if (!$forcereload and !is_null($this->recentresponse)) {
320 // We already have it, nothing to do.
321 return;
324 $config = get_config('core_plugin');
326 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
327 try {
328 $this->validate_response($config->recentresponse);
329 $this->recentfetch = $config->recentfetch;
330 $this->recentresponse = $this->decode_response($config->recentresponse);
331 } catch (checker_exception $e) {
332 // The server response is not valid. Behave as if no data were fetched yet.
333 // This may happen when the most recent update info (cached locally) has been
334 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
335 // to 2.y) or when the API of the response has changed.
336 $this->recentresponse = array();
339 } else {
340 $this->recentresponse = array();
345 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
347 * This method is used to populate potential update info to be sent to site admins.
349 * @param array $old
350 * @param array $new
351 * @throws checker_exception
352 * @return array parts of $new['updates'] that have changed
354 protected function compare_responses(array $old, array $new) {
356 if (empty($new)) {
357 return array();
360 if (!array_key_exists('updates', $new)) {
361 throw new checker_exception('err_response_format');
364 if (empty($old)) {
365 return $new['updates'];
368 if (!array_key_exists('updates', $old)) {
369 throw new checker_exception('err_response_format');
372 $changes = array();
374 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
375 if (empty($old['updates'][$newcomponent])) {
376 $changes[$newcomponent] = $newcomponentupdates;
377 continue;
379 foreach ($newcomponentupdates as $newcomponentupdate) {
380 $inold = false;
381 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
382 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
383 $inold = true;
386 if (!$inold) {
387 if (!isset($changes[$newcomponent])) {
388 $changes[$newcomponent] = array();
390 $changes[$newcomponent][] = $newcomponentupdate;
395 return $changes;
399 * Returns the URL to send update requests to
401 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
402 * to a custom URL that will be used. Otherwise the standard URL will be returned.
404 * @return string URL
406 protected function prepare_request_url() {
407 global $CFG;
409 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
410 return $CFG->config_php_settings['alternativeupdateproviderurl'];
411 } else {
412 return 'https://download.moodle.org/api/1.2/updates.php';
417 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
419 * @param bool $forcereload
421 protected function load_current_environment($forcereload=false) {
422 global $CFG;
424 if (!is_null($this->currentversion) and !$forcereload) {
425 // Nothing to do.
426 return;
429 $version = null;
430 $release = null;
432 require($CFG->dirroot.'/version.php');
433 $this->currentversion = $version;
434 $this->currentrelease = $release;
435 $this->currentbranch = moodle_major_version(true);
437 $pluginman = \core_plugin_manager::instance();
438 foreach ($pluginman->get_plugins() as $type => $plugins) {
439 foreach ($plugins as $plugin) {
440 if (!$plugin->is_standard()) {
441 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
448 * Returns the list of HTTP params to be sent to the updates provider URL
450 * @return array of (string)param => (string)value
452 protected function prepare_request_params() {
453 global $CFG;
455 $this->load_current_environment();
456 $this->restore_response();
458 $params = array();
459 $params['format'] = 'json';
461 if (isset($this->recentresponse['ticket'])) {
462 $params['ticket'] = $this->recentresponse['ticket'];
465 if (isset($this->currentversion)) {
466 $params['version'] = $this->currentversion;
467 } else {
468 throw new coding_exception('Main Moodle version must be already known here');
471 if (isset($this->currentbranch)) {
472 $params['branch'] = $this->currentbranch;
473 } else {
474 throw new coding_exception('Moodle release must be already known here');
477 $plugins = array();
478 foreach ($this->currentplugins as $plugin => $version) {
479 $plugins[] = $plugin.'@'.$version;
481 if (!empty($plugins)) {
482 $params['plugins'] = implode(',', $plugins);
485 return $params;
489 * Returns the list of cURL options to use when fetching available updates data
491 * @return array of (string)param => (string)value
493 protected function prepare_request_options() {
494 $options = array(
495 'CURLOPT_SSL_VERIFYHOST' => 2, // This is the default in {@link curl} class but just in case.
496 'CURLOPT_SSL_VERIFYPEER' => true,
499 return $options;
503 * Returns the current timestamp
505 * @return int the timestamp
507 protected function cron_current_timestamp() {
508 return time();
512 * Output cron debugging info
514 * @see mtrace()
515 * @param string $msg output message
516 * @param string $eol end of line
518 protected function cron_mtrace($msg, $eol = PHP_EOL) {
519 mtrace($msg, $eol);
523 * Decide if the autocheck feature is disabled in the server setting
525 * @return bool true if autocheck enabled, false if disabled
527 protected function cron_autocheck_enabled() {
528 global $CFG;
530 if (empty($CFG->updateautocheck)) {
531 return false;
532 } else {
533 return true;
538 * Decide if the recently fetched data are still fresh enough
540 * @param int $now current timestamp
541 * @return bool true if no need to re-fetch, false otherwise
543 protected function cron_has_fresh_fetch($now) {
544 $recent = $this->get_last_timefetched();
546 if (empty($recent)) {
547 return false;
550 if ($now < $recent) {
551 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
552 return true;
555 if ($now - $recent > 24 * HOURSECS) {
556 return false;
559 return true;
563 * Decide if the fetch is outadated or even missing
565 * @param int $now current timestamp
566 * @return bool false if no need to re-fetch, true otherwise
568 protected function cron_has_outdated_fetch($now) {
569 $recent = $this->get_last_timefetched();
571 if (empty($recent)) {
572 return true;
575 if ($now < $recent) {
576 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
577 return false;
580 if ($now - $recent > 48 * HOURSECS) {
581 return true;
584 return false;
588 * Returns the cron execution offset for this site
590 * The main {@link self::cron()} is supposed to run every night in some random time
591 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
592 * execution offset, that is the amount of time after 01:00 AM. The offset value is
593 * initially generated randomly and then used consistently at the site. This way, the
594 * regular checks against the download.moodle.org server are spread in time.
596 * @return int the offset number of seconds from range 1 sec to 5 hours
598 protected function cron_execution_offset() {
599 global $CFG;
601 if (empty($CFG->updatecronoffset)) {
602 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
605 return $CFG->updatecronoffset;
609 * Fetch available updates info and eventually send notification to site admins
611 protected function cron_execute() {
613 try {
614 $this->restore_response();
615 $previous = $this->recentresponse;
616 $this->fetch();
617 $this->restore_response(true);
618 $current = $this->recentresponse;
619 $changes = $this->compare_responses($previous, $current);
620 $notifications = $this->cron_notifications($changes);
621 $this->cron_notify($notifications);
622 $this->cron_mtrace('done');
623 } catch (checker_exception $e) {
624 $this->cron_mtrace('FAILED!');
629 * Given the list of changes in available updates, pick those to send to site admins
631 * @param array $changes as returned by {@link self::compare_responses()}
632 * @return array of \core\update\info objects to send to site admins
634 protected function cron_notifications(array $changes) {
635 global $CFG;
637 if (empty($changes)) {
638 return array();
641 $notifications = array();
642 $pluginman = \core_plugin_manager::instance();
643 $plugins = $pluginman->get_plugins();
645 foreach ($changes as $component => $componentchanges) {
646 if (empty($componentchanges)) {
647 continue;
649 $componentupdates = $this->get_update_info($component,
650 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
651 if (empty($componentupdates)) {
652 continue;
654 // Notify only about those $componentchanges that are present in $componentupdates
655 // to respect the preferences.
656 foreach ($componentchanges as $componentchange) {
657 foreach ($componentupdates as $componentupdate) {
658 if ($componentupdate->version == $componentchange['version']) {
659 if ($component == 'core') {
660 // In case of 'core', we already know that the $componentupdate
661 // is a real update with higher version ({@see self::get_update_info()}).
662 // We just perform additional check for the release property as there
663 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
664 // after the release). We can do that because we have the release info
665 // always available for the core.
666 if ((string)$componentupdate->release === (string)$componentchange['release']) {
667 $notifications[] = $componentupdate;
669 } else {
670 // Use the core_plugin_manager to check if the detected $componentchange
671 // is a real update with higher version. That is, the $componentchange
672 // is present in the array of {@link \core\update\info} objects
673 // returned by the plugin's available_updates() method.
674 list($plugintype, $pluginname) = core_component::normalize_component($component);
675 if (!empty($plugins[$plugintype][$pluginname])) {
676 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
677 if (!empty($availableupdates)) {
678 foreach ($availableupdates as $availableupdate) {
679 if ($availableupdate->version == $componentchange['version']) {
680 $notifications[] = $componentupdate;
691 return $notifications;
695 * Sends the given notifications to site admins via messaging API
697 * @param array $notifications array of \core\update\info objects to send
699 protected function cron_notify(array $notifications) {
700 global $CFG;
702 if (empty($notifications)) {
703 $this->cron_mtrace('nothing to notify about. ', '');
704 return;
707 $admins = get_admins();
709 if (empty($admins)) {
710 return;
713 $this->cron_mtrace('sending notifications ... ', '');
715 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
716 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
718 $coreupdates = array();
719 $pluginupdates = array();
721 foreach ($notifications as $notification) {
722 if ($notification->component == 'core') {
723 $coreupdates[] = $notification;
724 } else {
725 $pluginupdates[] = $notification;
729 if (!empty($coreupdates)) {
730 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
731 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
732 $html .= html_writer::start_tag('ul') . PHP_EOL;
733 foreach ($coreupdates as $coreupdate) {
734 $html .= html_writer::start_tag('li');
735 if (isset($coreupdate->release)) {
736 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
737 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
739 if (isset($coreupdate->version)) {
740 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
741 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
743 if (isset($coreupdate->maturity)) {
744 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
745 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
747 $text .= PHP_EOL;
748 $html .= html_writer::end_tag('li') . PHP_EOL;
750 $text .= PHP_EOL;
751 $html .= html_writer::end_tag('ul') . PHP_EOL;
753 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
754 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
755 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
756 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
758 $text .= PHP_EOL . get_string('updateavailablerecommendation', 'core_admin') . PHP_EOL;
759 $html .= html_writer::tag('p', get_string('updateavailablerecommendation', 'core_admin')) . PHP_EOL;
762 if (!empty($pluginupdates)) {
763 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
764 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
766 $html .= html_writer::start_tag('ul') . PHP_EOL;
767 foreach ($pluginupdates as $pluginupdate) {
768 $html .= html_writer::start_tag('li');
769 $text .= get_string('pluginname', $pluginupdate->component);
770 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
772 $text .= ' ('.$pluginupdate->component.')';
773 $html .= ' ('.$pluginupdate->component.')';
775 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
776 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
778 $text .= PHP_EOL;
779 $html .= html_writer::end_tag('li') . PHP_EOL;
781 $text .= PHP_EOL;
782 $html .= html_writer::end_tag('ul') . PHP_EOL;
784 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
785 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
786 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
787 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
790 $a = array('siteurl' => $CFG->wwwroot);
791 $text .= PHP_EOL . get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
792 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
793 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
794 array('style' => 'font-size:smaller; color:#333;')));
796 foreach ($admins as $admin) {
797 $message = new \stdClass();
798 $message->component = 'moodle';
799 $message->name = 'availableupdate';
800 $message->userfrom = get_admin();
801 $message->userto = $admin;
802 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
803 $message->fullmessage = $text;
804 $message->fullmessageformat = FORMAT_PLAIN;
805 $message->fullmessagehtml = $html;
806 $message->smallmessage = get_string('updatenotifications', 'core_admin');
807 $message->notification = 1;
808 message_send($message);
813 * Compare two release labels and decide if they are the same
815 * @param string $remote release info of the available update
816 * @param null|string $local release info of the local code, defaults to $release defined in version.php
817 * @return boolean true if the releases declare the same minor+major version
819 protected function is_same_release($remote, $local=null) {
821 if (is_null($local)) {
822 $this->load_current_environment();
823 $local = $this->currentrelease;
826 $pattern = '/^([0-9\.\+]+)([^(]*)/';
828 preg_match($pattern, $remote, $remotematches);
829 preg_match($pattern, $local, $localmatches);
831 $remotematches[1] = str_replace('+', '', $remotematches[1]);
832 $localmatches[1] = str_replace('+', '', $localmatches[1]);
834 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
835 return true;
836 } else {
837 return false;