From e3a18e4458c92e75a040d63da1f862a7e55eca6a Mon Sep 17 00:00:00 2001 From: Jeff Webster Date: Sun, 4 Mar 2018 01:27:22 -0500 Subject: [PATCH 1/6] MDL-48501 lang: update reCAPTCHA language strings --- lang/en/admin.php | 4 ++-- lang/en/auth.php | 4 ++-- lang/en/moodle.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lang/en/admin.php b/lang/en/admin.php index 79b76214a58..4019b8da8c9 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -300,8 +300,8 @@ $string['configproxypassword'] = 'Password needed to access internet through pro $string['configproxyport'] = 'If this server needs to use a proxy computer, then provide the proxy port here.'; $string['configproxytype'] = 'Type of web proxy (PHP5 and cURL extension required for SOCKS5 support).'; $string['configproxyuser'] = 'Username needed to access internet through proxy if required, empty if none (PHP cURL extension required).'; -$string['configrecaptchaprivatekey'] = 'String of characters (private key) used to communicate between your Moodle server and the recaptcha server. ReCAPTCHA keys can be obtained from Google reCAPTCHA.'; -$string['configrecaptchapublickey'] = 'String of characters (public key) used to display the reCAPTCHA element in the signup form. ReCAPTCHA keys can be obtained from Google reCAPTCHA.'; +$string['configrecaptchaprivatekey'] = 'String of characters (secret key) used to communicate between your Moodle server and the recaptcha server. ReCAPTCHA keys can be obtained from Google reCAPTCHA.'; +$string['configrecaptchapublickey'] = 'String of characters (site key) used to display the reCAPTCHA element in the signup form. ReCAPTCHA keys can be obtained from Google reCAPTCHA.'; $string['configrequestcategoryselection'] = 'Allow the selection of a category when requesting a course.'; $string['configrequestedstudentname'] = 'Word for student used in requested courses'; $string['configrequestedstudentsname'] = 'Word for students used in requested courses'; diff --git a/lang/en/auth.php b/lang/en/auth.php index 19e6aec9686..fce3e764a47 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -138,9 +138,9 @@ $string['pluginnotenabled'] = 'Authentication plugin \'{$a}\' is not enabled.'; $string['pluginnotinstalled'] = 'Authentication plugin \'{$a}\' is not installed.'; $string['potentialidps'] = 'Log in using your account on:'; $string['recaptcha'] = 'reCAPTCHA'; -$string['recaptcha_help'] = 'The CAPTCHA is for preventing abuse from automated programs. Simply enter the words in the box, in order and separated by a space. +$string['recaptcha_help'] = 'The CAPTCHA is for preventing abuse from automated programs. Follow the instructions to verify you are a person. This could be a box to check, characters presented in an image you must enter or a set of images to select from. -If you are not sure what the words are, you can try getting another CAPTCHA or an audio CAPTCHA.'; +If you are not sure what the images are, you can try getting another CAPTCHA or an audio CAPTCHA.'; $string['recaptcha_link'] = 'auth/email'; $string['security_question'] = 'Security question'; $string['selfregistration'] = 'Self registration'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 9cb78d14489..2bba3d8de0e 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1175,7 +1175,7 @@ $string['missinglastname'] = 'Missing surname'; $string['missingname'] = 'Missing name'; $string['missingnewpassword'] = 'Missing new password'; $string['missingpassword'] = 'Missing password'; -$string['missingrecaptchachallengefield'] = 'Missing reCAPTCHA challenge field'; +$string['missingrecaptchachallengefield'] = 'Failed reCAPTCHA challenge, try again.'; $string['missingreqreason'] = 'Missing reason'; $string['missingshortname'] = 'Missing short name'; $string['missingshortsitename'] = 'Missing short site name'; From 68e6299010da2d2fa8f5c5690f0167ec6ef1ba08 Mon Sep 17 00:00:00 2001 From: Jeff Webster Date: Sun, 4 Mar 2018 01:27:22 -0500 Subject: [PATCH 2/6] MDL-48501 recaptchalib_v2: add new lib to support reCAPTCHA v2 --- lang/en/auth.php | 1 + lib/recaptchalib_v2.php | 189 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 lib/recaptchalib_v2.php diff --git a/lang/en/auth.php b/lang/en/auth.php index fce3e764a47..2e58676cc40 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -102,6 +102,7 @@ $string['forgottenpasswordurl'] = 'Forgotten password URL'; $string['getanaudiocaptcha'] = 'Get an audio CAPTCHA'; $string['getanimagecaptcha'] = 'Get an image CAPTCHA'; $string['getanothercaptcha'] = 'Get another CAPTCHA'; +$string['getrecaptchaapi'] = 'To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin'; $string['guestloginbutton'] = 'Guest login button'; $string['changepassword'] = 'Change password URL'; $string['changepasswordhelp'] = 'URL of lost password recovery page, which will be sent to users in an email. Note that this setting will have no effect if a forgotten password URL is set in the authentication common settings.'; diff --git a/lib/recaptchalib_v2.php b/lib/recaptchalib_v2.php new file mode 100644 index 00000000000..e14b345a568 --- /dev/null +++ b/lib/recaptchalib_v2.php @@ -0,0 +1,189 @@ +. + // This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * This is a PHP library that handles calling reCAPTCHA v2. + * + * - Documentation + * {@link https://developers.google.com/recaptcha/docs/display} + * - Get a reCAPTCHA API Key + * {@link https://www.google.com/recaptcha/admin} + * - Discussion group + * {@link http://groups.google.com/group/recaptcha} + * + * @package core + * @copyright 2018 Jeff Webster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * The reCAPTCHA URL's + */ +define('RECAPTCHA_API_URL', 'https://www.google.com/recaptcha/api.js'); +define('RECAPTCHA_VERIFY_URL', 'https://www.google.com/recaptcha/api/siteverify'); + +/** + * Returns the language code the reCAPTCHA element should use. + * Google reCAPTCHA uses different language codes than Moodle so we must convert. + * https://developers.google.com/recaptcha/docs/language + * + * @return string A language code + */ +function recaptcha_lang() { + + $mlang = current_language(); + + $glang = $mlang; + switch ($glang) { + case 'en': + $glang = 'en-GB'; + break; + case 'en_us': + $glang = 'en'; + break; + case 'zh_cn': + $glang = 'zh-CN'; + break; + case 'zh_tw': + $glang = 'zh-TW'; + break; + case 'fr_ca': + $glang = 'fr-CA'; + break; + case 'pt_br': + $glang = 'pt-BR'; + break; + case 'he': + $glang = 'iw'; + break; + } + // For any language code that didn't change reduce down to the base language. + if (($mlang === $glang) and (strpos($mlang, '_') !== false)) { + list($glang, $trash) = explode('_', $mlang, 2); + } + return $glang; +} + +/** + * Gets the challenge HTML + * This is called from the browser, and the resulting reCAPTCHA HTML widget + * is embedded within the HTML form it was called from. + * + * @param string $apiurl URL for reCAPTCHA API + * @param string $pubkey The public key for reCAPTCHA + * @return string - The HTML to be embedded in the user's form. + */ +function recaptcha_get_challenge_html($apiurl, $pubkey) { + global $CFG, $PAGE; + + // To use reCAPTCHA you must have an API key. + if ($pubkey === null || $pubkey === '') { + return get_string('getrecaptchaapi', 'auth'); + } + + $jscode = " + var recaptchacallback = function() { + grecaptcha.render('recaptcha_element', { + 'sitekey' : '$pubkey' + }); + }"; + + $lang = recaptcha_lang(); + $apicode = "\n\n"; + + $return = html_writer::script($jscode, ''); + $return .= html_writer::div('', 'recaptcha_element', array('id' => 'recaptcha_element')); + $return .= $apicode; + + return $return; +} + +/** + * Calls an HTTP POST function to verify if the user's response was correct + * + * @param string $verifyurl URL for reCAPTCHA verification + * @param string $privkey The private key for reCAPTCHA + * @param string $remoteip The user's IP + * @param string $response The response from reCAPTCHA + * @return ReCaptchaResponse + */ +function recaptcha_check_response($verifyurl, $privkey, $remoteip, $response) { + global $CFG; + require_once($CFG->libdir.'/filelib.php'); + + // Check response - isvalid boolean, error string. + $checkresponse = array('isvalid' => false, 'error' => 'check-not-started'); + + // To use reCAPTCHA you must have an API key. + if ($privkey === null || $privkey === '') { + $checkresponse['isvalid'] = false; + $checkresponse['error'] = 'no-apikey'; + return $checkresponse; + } + + // For security reasons, you must pass the remote ip to reCAPTCHA. + if ($remoteip === null || $remoteip === '') { + $checkresponse['isvalid'] = false; + $checkresponse['error'] = 'no-remoteip'; + return $checkresponse; + } + + // Discard spam submissions. + if ($response === null || strlen($response) === 0) { + $checkresponse['isvalid'] = false; + $checkresponse['error'] = 'incorrect-captcha-sol'; + return $checkresponse; + } + + $params = array('secret' => $privkey, 'remoteip' => $remoteip, 'response' => $response); + $curl = new curl(); + $curlresponse = $curl->post($verifyurl, $params); + + if ($curl->get_errno() === 0) { + $curldata = json_decode($curlresponse); + + if (isset($curldata->success) && $curldata->success === true) { + $checkresponse['isvalid'] = true; + $checkresponse['error'] = ''; + } else { + $checkresponse['isvalid'] = false; + $checkresponse['error'] = $curldata->{error-codes}; + } + } else { + $checkresponse['isvalid'] = false; + $checkresponse['error'] = 'check-failed'; + } + return $checkresponse; +} + From 2f0be313b373ad6689738aab1f85d9c3db81f067 Mon Sep 17 00:00:00 2001 From: Jeff Webster Date: Sun, 4 Mar 2018 01:27:23 -0500 Subject: [PATCH 3/6] MDL-48501 form/recaptcha: update to use reCAPTCHA v2 --- lib/form/recaptcha.php | 78 +++++++++--------------------------------- 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/lib/form/recaptcha.php b/lib/form/recaptcha.php index 87b3d2a8a8d..2eb732624e8 100644 --- a/lib/form/recaptcha.php +++ b/lib/form/recaptcha.php @@ -46,9 +46,6 @@ class MoodleQuickForm_recaptcha extends HTML_QuickForm_input implements templata /** @var string html for help button, if empty then no help */ var $_helpbutton=''; - /** @var bool if true, recaptcha will be servered from https */ - var $_https=false; - /** * constructor * @@ -58,14 +55,8 @@ class MoodleQuickForm_recaptcha extends HTML_QuickForm_input implements templata * or an associative array */ public function __construct($elementName = null, $elementLabel = null, $attributes = null) { - global $CFG; parent::__construct($elementName, $elementLabel, $attributes); $this->_type = 'recaptcha'; - if (is_https()) { - $this->_https = true; - } else { - $this->_https = false; - } } /** @@ -79,49 +70,15 @@ class MoodleQuickForm_recaptcha extends HTML_QuickForm_input implements templata } /** - * Returns the recaptcha element in HTML + * Returns the reCAPTCHA element in HTML * - * @return string + * @return string The HTML to render */ - function toHtml() { - global $CFG, $PAGE; - require_once $CFG->libdir . '/recaptchalib.php'; + public function toHtml() { + global $CFG; + require_once($CFG->libdir . '/recaptchalib_v2.php'); - $recaptureoptions = Array('theme'=>'custom', 'custom_theme_widget'=>'recaptcha_widget'); - $html = html_writer::script(js_writer::set_variable('RecaptchaOptions', $recaptureoptions)); - - $attributes = $this->getAttributes(); - if (empty($attributes['error_message'])) { - $attributes['error_message'] = null; - $this->setAttributes($attributes); - } - $error = $attributes['error_message']; - unset($attributes['error_message']); - - $strincorrectpleasetryagain = get_string('incorrectpleasetryagain', 'auth'); - $strenterthewordsabove = get_string('enterthewordsabove', 'auth'); - $strenterthenumbersyouhear = get_string('enterthenumbersyouhear', 'auth'); - $strgetanothercaptcha = get_string('getanothercaptcha', 'auth'); - $strgetanaudiocaptcha = get_string('getanaudiocaptcha', 'auth'); - $strgetanimagecaptcha = get_string('getanimagecaptcha', 'auth'); - - $html .= ' -'; - - return $html . recaptcha_get_html($CFG->recaptchapublickey, $error, $this->_https); + return recaptcha_get_challenge_html(RECAPTCHA_API_URL, $CFG->recaptchapublickey); } /** @@ -134,25 +91,22 @@ class MoodleQuickForm_recaptcha extends HTML_QuickForm_input implements templata } /** - * Checks input and challenged field + * Checks recaptcha response with Google. * - * @param string $challenge_field recaptcha shown to user - * @param string $response_field input value by user + * @param string $responsestr * @return bool */ - function verify($challenge_field, $response_field) { + public function verify($responsestr) { global $CFG; - require_once $CFG->libdir . '/recaptchalib.php'; - $response = recaptcha_check_answer($CFG->recaptchaprivatekey, - getremoteaddr(), - $challenge_field, - $response_field, - $this->_https); - if (!$response->is_valid) { + require_once($CFG->libdir . '/recaptchalib_v2.php'); + + $response = recaptcha_check_response(RECAPTCHA_VERIFY_URL, $CFG->recaptchaprivatekey, + getremoteaddr(), $responsestr); + if (!$response['isvalid']) { $attributes = $this->getAttributes(); - $attributes['error_message'] = $response->error; + $attributes['error_message'] = $response['error']; $this->setAttributes($attributes); - return $response->error; + return $response['error']; } return true; } From 8d7d722fb03e0615bea8936b97aa82114e59e3dc Mon Sep 17 00:00:00 2001 From: Jeff Webster Date: Sun, 4 Mar 2018 01:27:23 -0500 Subject: [PATCH 4/6] MDL-48501 login: update signup_form to use reCAPTCHA v2 --- login/signup_form.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/login/signup_form.php b/login/signup_form.php index c15c0c4c3cc..69c2b342d00 100644 --- a/login/signup_form.php +++ b/login/signup_form.php @@ -1,5 +1,4 @@ value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK (true allowed for backwards compatibility too). + */ + public function validation($data, $files) { $errors = parent::validation($data, $files); if (signup_captcha_enabled()) { - $recaptcha_element = $this->_form->getElement('recaptcha_element'); - if (!empty($this->_form->_submitValues['recaptcha_challenge_field'])) { - $challenge_field = $this->_form->_submitValues['recaptcha_challenge_field']; - $response_field = $this->_form->_submitValues['recaptcha_response_field']; - if (true !== ($result = $recaptcha_element->verify($challenge_field, $response_field))) { + $recaptchaelement = $this->_form->getElement('recaptcha_element'); + if (!empty($this->_form->_submitValues['g-recaptcha-response'])) { + $response = $this->_form->_submitValues['g-recaptcha-response']; + if (!$recaptchaelement->verify($response)) { $errors['recaptcha_element'] = get_string('incorrectpleasetryagain', 'auth'); } } else { @@ -140,7 +146,6 @@ class login_signup_form extends moodleform implements renderable, templatable { $errors += signup_validate_data($data, $files); return $errors; - } /** From 3c9a5ae1cc6f148788bb84dbde7dd3b3c3130380 Mon Sep 17 00:00:00 2001 From: Jeff Webster Date: Sun, 4 Mar 2018 01:27:23 -0500 Subject: [PATCH 5/6] MDL-48501 mod_feedback: update feedback to use reCAPTCHA v2 --- mod/feedback/item/captcha/lib.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/mod/feedback/item/captcha/lib.php b/mod/feedback/item/captcha/lib.php index 8c44deb2868..47a07eaed47 100644 --- a/mod/feedback/item/captcha/lib.php +++ b/mod/feedback/item/captcha/lib.php @@ -25,13 +25,13 @@ class feedback_item_captcha extends feedback_item_base { $editurl = new moodle_url('/mod/feedback/edit.php', array('id'=>$cm->id)); - //ther are no settings for recaptcha + // There are no settings for recaptcha. if (isset($item->id) AND $item->id > 0) { notice(get_string('there_are_no_settings_for_recaptcha', 'feedback'), $editurl->out()); exit; } - //only one recaptcha can be in a feedback + // Only one recaptcha can be in a feedback. $params = array('feedback' => $feedback->id, 'typ' => $this->type); if ($DB->record_exists('feedback_item', $params)) { notice(get_string('only_one_captcha_allowed', 'feedback'), $editurl->out()); @@ -39,7 +39,7 @@ class feedback_item_captcha extends feedback_item_base { } $this->item = $item; - $this->item_form = true; //dummy + $this->item_form = true; // Dummy. $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); @@ -140,16 +140,13 @@ class feedback_item_captcha extends feedback_item_base { $form->add_validation_rule(function($values, $files) use ($item, $form) { $elementname = $item->typ . '_' . $item->id . 'recaptcha'; $recaptchaelement = $form->get_form_element($elementname); - if (empty($values['recaptcha_response_field'])) { + if (empty($values['g-recaptcha-response'])) { return array($elementname => get_string('required')); - } else if (!empty($values['recaptcha_challenge_field'])) { - $challengefield = $values['recaptcha_challenge_field']; - $responsefield = $values['recaptcha_response_field']; - if (true !== ($result = $recaptchaelement->verify($challengefield, $responsefield))) { + } else { + $response = $values['g-recaptcha-response']; + if (true !== ($result = $recaptchaelement->verify($response))) { return array($elementname => $result); } - } else { - return array($elementname => get_string('missingrecaptchachallengefield')); } return true; }); @@ -164,7 +161,7 @@ class feedback_item_captcha extends feedback_item_base { public function get_hasvalue() { global $CFG; - //is recaptcha configured in moodle? + // Is recaptcha configured in moodle? if (empty($CFG->recaptchaprivatekey) OR empty($CFG->recaptchapublickey)) { return 0; } @@ -196,9 +193,10 @@ class feedback_item_captcha extends feedback_item_base { return null; } - require_once($CFG->libdir . '/recaptchalib.php'); - // We return the public key, maybe we want to use the javascript api to get the image. - $data = recaptcha_get_challenge_hash_and_urls(RECAPTCHA_API_SECURE_SERVER, $CFG->recaptchapublickey); + // With reCAPTCHA v2 the captcha will be rendered by the mobile client using just the publickey. + // For now include placeholders for the v1 paramaters to support older mobile app versions. + // recaptchachallengehash, recaptchachallengeimage and recaptchachallengejs. + $data = array('', '', ''); $data[] = $CFG->recaptchapublickey; return json_encode($data); } From 934220228afb003bd564cc0bc0033ce52a584f3f Mon Sep 17 00:00:00 2001 From: Jeff Webster Date: Mon, 5 Mar 2018 19:27:52 -0500 Subject: [PATCH 6/6] MDL-48501 auth/email: update external class to use reCAPTCHA v2 --- auth/email/classes/external.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/auth/email/classes/external.php b/auth/email/classes/external.php index a36ceb39c8f..76bba9b466f 100644 --- a/auth/email/classes/external.php +++ b/auth/email/classes/external.php @@ -112,11 +112,11 @@ class auth_email_external extends external_api { } if (signup_captcha_enabled()) { - require_once($CFG->libdir . '/recaptchalib.php'); - // We return the public key, maybe we want to use the javascript api to get the image. + // With reCAPTCHA v2 the captcha will be rendered by the mobile client using just the publickey. + // For now include placeholders for the v1 paramaters to support older mobile app versions. $result['recaptchapublickey'] = $CFG->recaptchapublickey; list($result['recaptchachallengehash'], $result['recaptchachallengeimage'], $result['recaptchachallengejs']) = - recaptcha_get_challenge_hash_and_urls(RECAPTCHA_API_SECURE_SERVER, $CFG->recaptchapublickey); + array('', '', ''); } $result['warnings'] = array(); @@ -307,11 +307,11 @@ class auth_email_external extends external_api { // Validate recaptcha. if (signup_captcha_enabled()) { - require_once($CFG->libdir . '/recaptchalib.php'); - $response = recaptcha_check_answer($CFG->recaptchaprivatekey, getremoteaddr(), $params['recaptchachallengehash'], - $params['recaptcharesponse'], true); - if (!$response->is_valid) { - $errors['recaptcharesponse'] = $response->error; + require_once($CFG->libdir . '/recaptchalib_v2.php'); + $response = recaptcha_check_response(RECAPTCHA_VERIFY_URL, $CFG->recaptchaprivatekey, + getremoteaddr(), $params['recaptcharesponse']); + if (!$response['isvalid']) { + $errors['recaptcharesponse'] = $response['error']; } }