Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   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   // This file is part of Moodle - http://moodle.org/
  17  //
  18  // Moodle is free software: you can redistribute it and/or modify
  19  // it under the terms of the GNU General Public License as published by
  20  // the Free Software Foundation, either version 3 of the License, or
  21  // (at your option) any later version.
  22  //
  23  // Moodle is distributed in the hope that it will be useful,
  24  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  25  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  26  // GNU General Public License for more details.
  27  //
  28  // You should have received a copy of the GNU General Public License
  29  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  30  
  31  /**
  32   * This is a PHP library that handles calling reCAPTCHA v2.
  33   *
  34   *    - Documentation
  35   *          {@link https://developers.google.com/recaptcha/docs/display}
  36   *    - Get a reCAPTCHA API Key
  37   *          {@link https://www.google.com/recaptcha/admin}
  38   *    - Discussion group
  39   *          {@link http://groups.google.com/group/recaptcha}
  40   *
  41   * @package core
  42   * @copyright 2018 Jeff Webster
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  
  46  defined('MOODLE_INTERNAL') || die();
  47  
  48  /**
  49   * The reCAPTCHA URL's
  50   */
  51  define('RECAPTCHA_API_URL', 'https://www.recaptcha.net/recaptcha/api.js');
  52  define('RECAPTCHA_VERIFY_URL', 'https://www.recaptcha.net/recaptcha/api/siteverify');
  53  
  54  /**
  55   * Returns the language code the reCAPTCHA element should use.
  56   * Google reCAPTCHA uses different language codes than Moodle so we must convert.
  57   * https://developers.google.com/recaptcha/docs/language
  58   *
  59   * @param string $lang Language to use. If not provided, get current language.
  60   * @return string A language code
  61   */
  62  function recaptcha_lang($lang = null) {
  63  
  64      if (empty($lang)) {
  65          $lang = current_language();
  66      }
  67  
  68      $glang = $lang;
  69      switch ($glang) {
  70          case 'en':
  71              $glang = 'en-GB';
  72              break;
  73          case 'en_us':
  74              $glang = 'en';
  75              break;
  76          case 'zh_cn':
  77              $glang = 'zh-CN';
  78              break;
  79          case 'zh_tw':
  80              $glang = 'zh-TW';
  81              break;
  82          case 'fr_ca':
  83              $glang = 'fr-CA';
  84              break;
  85          case 'pt_br':
  86              $glang = 'pt-BR';
  87              break;
  88          case 'he':
  89              $glang = 'iw';
  90              break;
  91      }
  92      // For any language code that didn't change reduce down to the base language.
  93      if (($lang === $glang) and (strpos($lang, '_') !== false)) {
  94          list($glang, $trash) = explode('_', $lang, 2);
  95      }
  96      return $glang;
  97  }
  98  
  99  /**
 100   * Gets the challenge HTML
 101   * This is called from the browser, and the resulting reCAPTCHA HTML widget
 102   * is embedded within the HTML form it was called from.
 103   *
 104   * @param string $apiurl URL for reCAPTCHA API
 105   * @param string $pubkey The public key for reCAPTCHA
 106   * @param string $lang Language to use. If not provided, get current language.
 107   * @param bool $compactmode If true, use the compact widget.
 108   * @return string - The HTML to be embedded in the user's form.
 109   */
 110  function recaptcha_get_challenge_html($apiurl, $pubkey, $lang = null, bool $compactmode = false) {
 111      global $CFG, $PAGE;
 112  
 113      // To use reCAPTCHA you must have an API key.
 114      if ($pubkey === null || $pubkey === '') {
 115          return get_string('getrecaptchaapi', 'auth');
 116      }
 117  
 118      $jscode = "
 119          var recaptchacallback = function() {
 120              grecaptcha.render('recaptcha_element', {
 121                'sitekey' : '$pubkey'
 122              });
 123          }";
 124  
 125      $lang = recaptcha_lang($lang);
 126      $apicode = "\n<script type=\"text/javascript\" ";
 127      $apicode .= "src=\"$apiurl?onload=recaptchacallback&render=explicit&hl=$lang\" async defer>";
 128      $apicode .= "</script>\n";
 129  
 130      $return = html_writer::script($jscode, '');
 131      $return .= html_writer::div('', 'recaptcha_element', [
 132          'id' => 'recaptcha_element',
 133          'data-size' => ($compactmode ? 'compact' : 'normal'),
 134      ]);
 135      $return .= $apicode;
 136  
 137      return $return;
 138  }
 139  
 140  /**
 141   * Calls an HTTP POST function to verify if the user's response was correct
 142   *
 143   * @param string $verifyurl URL for reCAPTCHA verification
 144   * @param string $privkey The private key for reCAPTCHA
 145   * @param string $remoteip The user's IP
 146   * @param string $response The response from reCAPTCHA
 147   * @return ReCaptchaResponse
 148   */
 149  function recaptcha_check_response($verifyurl, $privkey, $remoteip, $response) {
 150      global $CFG;
 151      require_once($CFG->libdir.'/filelib.php');
 152  
 153      // Check response - isvalid boolean, error string.
 154      $checkresponse = array('isvalid' => false, 'error' => 'check-not-started');
 155  
 156      // To use reCAPTCHA you must have an API key.
 157      if ($privkey === null || $privkey === '') {
 158          $checkresponse['isvalid'] = false;
 159          $checkresponse['error'] = 'no-apikey';
 160          return $checkresponse;
 161      }
 162  
 163      // For security reasons, you must pass the remote ip to reCAPTCHA.
 164      if ($remoteip === null || $remoteip === '') {
 165          $checkresponse['isvalid'] = false;
 166          $checkresponse['error'] = 'no-remoteip';
 167          return $checkresponse;
 168      }
 169  
 170      // Discard spam submissions.
 171      if ($response === null || strlen($response) === 0) {
 172          $checkresponse['isvalid'] = false;
 173          $checkresponse['error'] = 'incorrect-captcha-sol';
 174          return $checkresponse;
 175      }
 176  
 177      $params = array('secret' => $privkey, 'remoteip' => $remoteip, 'response' => $response);
 178      $curl = new curl();
 179      $curlresponse = $curl->post($verifyurl, $params);
 180  
 181      if ($curl->get_errno() === 0) {
 182          $curldata = json_decode($curlresponse);
 183  
 184          if (isset($curldata->success) && $curldata->success === true) {
 185              $checkresponse['isvalid'] = true;
 186              $checkresponse['error'] = '';
 187          } else {
 188              $checkresponse['isvalid'] = false;
 189              $checkresponse['error'] = $curldata->{'error-codes'};
 190          }
 191      } else {
 192          $checkresponse['isvalid'] = false;
 193          $checkresponse['error'] = 'check-failed';
 194      }
 195      return $checkresponse;
 196  }
 197