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.
/mnet/ -> lib.php (source)

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

   1  <?php
   2  /**
   3   * Library functions for mnet
   4   *
   5   * @author  Donal McMullan  donal@catalyst.net.nz
   6   * @version 0.0.1
   7   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
   8   * @package mnet
   9   */
  10  require_once $CFG->dirroot.'/mnet/xmlrpc/xmlparser.php';
  11  require_once $CFG->dirroot.'/mnet/peer.php';
  12  require_once $CFG->dirroot.'/mnet/environment.php';
  13  
  14  /// CONSTANTS ///////////////////////////////////////////////////////////
  15  
  16  define('RPC_OK',                0);
  17  define('RPC_NOSUCHFILE',        1);
  18  define('RPC_NOSUCHCLASS',       2);
  19  define('RPC_NOSUCHFUNCTION',    3);
  20  define('RPC_FORBIDDENFUNCTION', 4);
  21  define('RPC_NOSUCHMETHOD',      5);
  22  define('RPC_FORBIDDENMETHOD',   6);
  23  
  24  /**
  25   * Strip extraneous detail from a URL or URI and return the hostname
  26   *
  27   * @param  string  $uri  The URI of a file on the remote computer, optionally
  28   *                       including its http:// prefix like
  29   *                       http://www.example.com/index.html
  30   * @return string        Just the hostname
  31   */
  32  function mnet_get_hostname_from_uri($uri = null) {
  33      $count = preg_match("@^(?:http[s]?://)?([A-Z0-9\-\.]+).*@i", $uri, $matches);
  34      if ($count > 0) return $matches[1];
  35      return false;
  36  }
  37  
  38  /**
  39   * Get the remote machine's SSL Cert
  40   *
  41   * @param  string  $uri     The URI of a file on the remote computer, including
  42   *                          its http:// or https:// prefix
  43   * @return string           A PEM formatted SSL Certificate.
  44   */
  45  function mnet_get_public_key($uri, $application=null) {
  46      global $CFG, $DB;
  47      $mnet = get_mnet_environment();
  48      // The key may be cached in the mnet_set_public_key function...
  49      // check this first
  50      $key = mnet_set_public_key($uri);
  51      if ($key != false) {
  52          return $key;
  53      }
  54  
  55      if (empty($application)) {
  56          $application = $DB->get_record('mnet_application', array('name'=>'moodle'));
  57      }
  58  
  59      $params = [
  60          new \PhpXmlRpc\Value($CFG->wwwroot),
  61          new \PhpXmlRpc\Value($mnet->public_key),
  62          new \PhpXmlRpc\Value($application->name),
  63      ];
  64      $request = new \PhpXmlRpc\Request('system/keyswap', $params);
  65  
  66      // Let's create a client to handle the request and the response easily.
  67      $client = new \PhpXmlRpc\Client($uri . $application->xmlrpc_server_url);
  68      $client->setOption('use_curl', \PhpXmlRpc\Client::USE_CURL_ALWAYS);
  69      $client->setOption('user_agent', 'Moodle');
  70      $client->return_type = 'xmlrpcvals'; // This (keyswap) is not encrypted, so we can expect proper xmlrpc in this case.
  71      $client->setOption('request_charset_encoding', 'utf-8');
  72      $client->setOption('accepted_charset_encodings', ['utf-8']);
  73  
  74      // TODO: Link this to DEBUG DEVELOPER or with MNET debugging...
  75      // $client->setdebug(1); // See a good number of complete requests and responses.
  76  
  77      $client->setOption('verifyhost', 0);
  78      $client->setOption('verifypeer', false);
  79  
  80      // TODO: It's curious that this service (keyswap) that needs
  81      // a custom client, different from mnet_xmlrpc_client, because
  82      // this is not encrypted / signed, does support proxies and the
  83      // general one does not. Worth analysing if the support below
  84      // should be added to it.
  85  
  86      // Some curl options need to be set apart, accumulate them here.
  87      $extracurloptions = [];
  88  
  89      // Check for proxy.
  90      if (!empty($CFG->proxyhost) && !is_proxybypass($uri)) {
  91          // SOCKS supported in PHP5 only.
  92          if (!empty($CFG->proxytype) && ($CFG->proxytype == 'SOCKS5')) {
  93              if (defined('CURLPROXY_SOCKS5')) {
  94                  $extracurloptions[CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5;
  95              } else {
  96                  throw new \moodle_exception( 'socksnotsupported', 'mnet');
  97              }
  98          }
  99  
 100          $extracurloptions[CURLOPT_HTTPPROXYTUNNEL] = false;
 101  
 102          // Configure proxy host, port, user, pass and auth.
 103          $client->setProxy(
 104              $CFG->proxyhost,
 105              empty($CFG->proxyport) ? 0 : $CFG->proxyport,
 106              empty($CFG->proxyuser) ? '' : $CFG->proxyuser,
 107              empty($CFG->proxypassword) ? '' : $CFG->proxypassword,
 108              defined('CURLOPT_PROXYAUTH') ? CURLAUTH_BASIC | CURLAUTH_NTLM : 1);
 109      }
 110  
 111      // Finally, add the extra curl options we may have accumulated.
 112      $client->setCurlOptions($extracurloptions);
 113  
 114      $response = $client->send($request, 60);
 115  
 116      // Check curl / xmlrpc errors.
 117      if ($response->faultCode()) {
 118          debugging("Request for $uri failed with error {$response->faultCode()}: {$response->faultString()}");
 119          return false;
 120      }
 121  
 122      // Check HTTP error code.
 123      $status = $response->httpResponse()['status_code'];
 124      if (!empty($status) && ($status != 200)) {
 125          debugging("Request for $uri failed with HTTP code " . $status);
 126          return false;
 127      }
 128  
 129      // Get the peer actual public key from the response.
 130      $res = $response->value()->scalarval();
 131  
 132      if (!is_array($res)) { // ! error
 133          $public_certificate = $res;
 134          $credentials=array();
 135          if (strlen(trim($public_certificate))) {
 136              $credentials = openssl_x509_parse($public_certificate);
 137              $host = $credentials['subject']['CN'];
 138              if (array_key_exists( 'subjectAltName', $credentials['subject'])) {
 139                  $host = $credentials['subject']['subjectAltName'];
 140              }
 141              if (strpos($uri, $host) !== false) {
 142                  mnet_set_public_key($uri, $public_certificate);
 143                  return $public_certificate;
 144              }
 145              else {
 146                  debugging("Request for $uri returned public key for different URI - $host");
 147              }
 148          }
 149          else {
 150              debugging("Request for $uri returned empty response");
 151          }
 152      }
 153      else {
 154          debugging( "Request for $uri returned unexpected result");
 155      }
 156      return false;
 157  }
 158  
 159  /**
 160   * Store a URI's public key in a static variable, or retrieve the key for a URI
 161   *
 162   * @param  string  $uri  The URI of a file on the remote computer, including its
 163   *                       https:// prefix
 164   * @param  mixed   $key  A public key to store in the array OR null. If the key
 165   *                       is null, the function will return the previously stored
 166   *                       key for the supplied URI, should it exist.
 167   * @return mixed         A public key OR true/false.
 168   */
 169  function mnet_set_public_key($uri, $key = null) {
 170      static $keyarray = array();
 171      if (isset($keyarray[$uri]) && empty($key)) {
 172          return $keyarray[$uri];
 173      } elseif (!empty($key)) {
 174          $keyarray[$uri] = $key;
 175          return true;
 176      }
 177      return false;
 178  }
 179  
 180  /**
 181   * Sign a message and return it in an XML-Signature document
 182   *
 183   * This function can sign any content, but it was written to provide a system of
 184   * signing XML-RPC request and response messages. The message will be base64
 185   * encoded, so it does not need to be text.
 186   *
 187   * We compute the SHA1 digest of the message.
 188   * We compute a signature on that digest with our private key.
 189   * We link to the public key that can be used to verify our signature.
 190   * We base64 the message data.
 191   * We identify our wwwroot - this must match our certificate's CN
 192   *
 193   * The XML-RPC document will be parceled inside an XML-SIG document, which holds
 194   * the base64_encoded XML as an object, the SHA1 digest of that document, and a
 195   * signature of that document using the local private key. This signature will
 196   * uniquely identify the RPC document as having come from this server.
 197   *
 198   * See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c
 199   * site
 200   *
 201   * @param  string   $message              The data you want to sign
 202   * @param  resource $privatekey           The private key to sign the response with
 203   * @return string                         An XML-DSig document
 204   */
 205  function mnet_sign_message($message, $privatekey = null) {
 206      global $CFG;
 207      $digest = sha1($message);
 208  
 209      $mnet = get_mnet_environment();
 210      // If the user hasn't supplied a private key (for example, one of our older,
 211      //  expired private keys, we get the current default private key and use that.
 212      if ($privatekey == null) {
 213          $privatekey = $mnet->get_private_key();
 214      }
 215  
 216      // The '$sig' value below is returned by reference.
 217      // We initialize it first to stop my IDE from complaining.
 218      $sig  = '';
 219      $bool = openssl_sign($message, $sig, $privatekey);
 220  
 221      // Avoid passing null values to base64_encode.
 222      if ($bool === false) {
 223          throw new \moodle_exception('opensslsignerror');
 224      }
 225  
 226      $message = '<?xml version="1.0" encoding="iso-8859-1"?>
 227      <signedMessage>
 228          <Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
 229              <SignedInfo>
 230                  <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
 231                  <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
 232                  <Reference URI="#XMLRPC-MSG">
 233                      <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
 234                      <DigestValue>'.$digest.'</DigestValue>
 235                  </Reference>
 236              </SignedInfo>
 237              <SignatureValue>'.base64_encode($sig).'</SignatureValue>
 238              <KeyInfo>
 239                  <RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
 240              </KeyInfo>
 241          </Signature>
 242          <object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
 243          <wwwroot>'.$mnet->wwwroot.'</wwwroot>
 244          <timestamp>'.time().'</timestamp>
 245      </signedMessage>';
 246      return $message;
 247  }
 248  
 249  /**
 250   * Encrypt a message and return it in an XML-Encrypted document
 251   *
 252   * This function can encrypt any content, but it was written to provide a system
 253   * of encrypting XML-RPC request and response messages. The message will be
 254   * base64 encoded, so it does not need to be text - binary data should work.
 255   *
 256   * We compute the SHA1 digest of the message.
 257   * We compute a signature on that digest with our private key.
 258   * We link to the public key that can be used to verify our signature.
 259   * We base64 the message data.
 260   * We identify our wwwroot - this must match our certificate's CN
 261   *
 262   * The XML-RPC document will be parceled inside an XML-SIG document, which holds
 263   * the base64_encoded XML as an object, the SHA1 digest of that document, and a
 264   * signature of that document using the local private key. This signature will
 265   * uniquely identify the RPC document as having come from this server.
 266   *
 267   * See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c
 268   * site
 269   *
 270   * @param  string   $message              The data you want to sign
 271   * @param  string   $remote_certificate   Peer's certificate in PEM format
 272   * @return string                         An XML-ENC document
 273   */
 274  function mnet_encrypt_message($message, $remote_certificate) {
 275      $mnet = get_mnet_environment();
 276  
 277      // Generate a key resource from the remote_certificate text string
 278      $publickey = openssl_get_publickey($remote_certificate);
 279  
 280      if ($publickey === false) {
 281          // Remote certificate is faulty.
 282          return false;
 283      }
 284  
 285      // Initialize vars
 286      $encryptedstring = '';
 287      $symmetric_keys = array();
 288  
 289      //        passed by ref ->     &$encryptedstring &$symmetric_keys
 290      $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey), 'RC4');
 291  
 292      // Avoid passing null values to base64_encode.
 293      if ($bool === false) {
 294          throw new \moodle_exception('opensslsealerror');
 295      }
 296  
 297      $message = $encryptedstring;
 298      $symmetrickey = array_pop($symmetric_keys);
 299  
 300      $message = '<?xml version="1.0" encoding="iso-8859-1"?>
 301      <encryptedMessage>
 302          <EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
 303              <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
 304              <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
 305                  <ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
 306                  <ds:KeyName>XMLENC</ds:KeyName>
 307              </ds:KeyInfo>
 308              <CipherData>
 309                  <CipherValue>'.base64_encode($message).'</CipherValue>
 310              </CipherData>
 311          </EncryptedData>
 312          <EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
 313              <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
 314              <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
 315                  <ds:KeyName>SSLKEY</ds:KeyName>
 316              </ds:KeyInfo>
 317              <CipherData>
 318                  <CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
 319              </CipherData>
 320              <ReferenceList>
 321                  <DataReference URI="#ED"/>
 322              </ReferenceList>
 323              <CarriedKeyName>XMLENC</CarriedKeyName>
 324          </EncryptedKey>
 325          <wwwroot>'.$mnet->wwwroot.'</wwwroot>
 326      </encryptedMessage>';
 327      return $message;
 328  }
 329  
 330  /**
 331   * Get your SSL keys from the database, or create them (if they don't exist yet)
 332   *
 333   * Get your SSL keys from the database, or (if they don't exist yet) call
 334   * mnet_generate_keypair to create them
 335   *
 336   * @param   string  $string     The text you want to sign
 337   * @return  string              The signature over that text
 338   */
 339  function mnet_get_keypair() {
 340      global $CFG, $DB;
 341      static $keypair = null;
 342      if (!is_null($keypair)) return $keypair;
 343      if ($result = get_config('mnet', 'openssl')) {
 344          list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result);
 345          return $keypair;
 346      } else {
 347          $keypair = mnet_generate_keypair();
 348          return $keypair;
 349      }
 350  }
 351  
 352  /**
 353   * Generate public/private keys and store in the config table
 354   *
 355   * Use the distinguished name provided to create a CSR, and then sign that CSR
 356   * with the same credentials. Store the keypair you create in the config table.
 357   * If a distinguished name is not provided, create one using the fullname of
 358   * 'the course with ID 1' as your organization name, and your hostname (as
 359   * detailed in $CFG->wwwroot).
 360   *
 361   * @param   array  $dn  The distinguished name of the server
 362   * @return  string      The signature over that text
 363   */
 364  function mnet_generate_keypair($dn = null, $days=28) {
 365      global $CFG, $USER, $DB;
 366  
 367      // check if lifetime has been overriden
 368      if (!empty($CFG->mnetkeylifetime)) {
 369          $days = $CFG->mnetkeylifetime;
 370      }
 371  
 372      $host = strtolower($CFG->wwwroot);
 373      $host = preg_replace("~^http(s)?://~",'',$host);
 374      $break = strpos($host.'/' , '/');
 375      $host   = substr($host, 0, $break);
 376  
 377      $site = get_site();
 378      $organization = $site->fullname;
 379  
 380      $keypair = array();
 381  
 382      $country  = 'NZ';
 383      $province = 'Wellington';
 384      $locality = 'Wellington';
 385      $email    = !empty($CFG->noreplyaddress) ? $CFG->noreplyaddress : 'noreply@'.$_SERVER['HTTP_HOST'];
 386  
 387      if(!empty($USER->country)) {
 388          $country  = $USER->country;
 389      }
 390      if(!empty($USER->city)) {
 391          $province = $USER->city;
 392          $locality = $USER->city;
 393      }
 394      if(!empty($USER->email)) {
 395          $email    = $USER->email;
 396      }
 397  
 398      if (is_null($dn)) {
 399          $dn = array(
 400             "countryName" => $country,
 401             "stateOrProvinceName" => $province,
 402             "localityName" => $locality,
 403             "organizationName" => $organization,
 404             "organizationalUnitName" => 'Moodle',
 405             "commonName" => substr($CFG->wwwroot, 0, 64),
 406             "subjectAltName" => $CFG->wwwroot,
 407             "emailAddress" => $email
 408          );
 409      }
 410  
 411      $dnlimits = array(
 412             'countryName'            => 2,
 413             'stateOrProvinceName'    => 128,
 414             'localityName'           => 128,
 415             'organizationName'       => 64,
 416             'organizationalUnitName' => 64,
 417             'commonName'             => 64,
 418             'emailAddress'           => 128
 419      );
 420  
 421      foreach ($dnlimits as $key => $length) {
 422          $dn[$key] = core_text::substr($dn[$key], 0, $length);
 423      }
 424  
 425      // ensure we remove trailing slashes
 426      $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
 427      if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
 428          $new_key = openssl_pkey_new(array("config" => $CFG->opensslcnf));
 429      } else {
 430          $new_key = openssl_pkey_new();
 431      }
 432      if ($new_key === false) {
 433          // can not generate keys - missing openssl.cnf??
 434          return null;
 435      }
 436      if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
 437          $csr_rsc = openssl_csr_new($dn, $new_key, array("config" => $CFG->opensslcnf));
 438          $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days, array("config" => $CFG->opensslcnf));
 439      } else {
 440          $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
 441          $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
 442      }
 443      unset($csr_rsc); // Free up the resource
 444  
 445      // We export our self-signed certificate to a string.
 446      openssl_x509_export($selfSignedCert, $keypair['certificate']);
 447      // TODO: Remove this block once PHP 8.0 becomes required.
 448      if (PHP_MAJOR_VERSION < 8) {
 449          openssl_x509_free($selfSignedCert);
 450      }
 451  
 452      // Export your public/private key pair as a PEM encoded string. You
 453      // can protect it with an optional passphrase if you wish.
 454      if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
 455          $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'], null, array("config" => $CFG->opensslcnf));
 456      } else {
 457          $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
 458      }
 459      // TODO: Remove this block once PHP 8.0 becomes required.
 460      if (PHP_MAJOR_VERSION < 8) {
 461          openssl_pkey_free($new_key);
 462      }
 463      unset($new_key); // Free up the resource
 464  
 465      return $keypair;
 466  }
 467  
 468  
 469  function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
 470      global $DB;
 471  
 472      $mnethost = $DB->get_record('mnet_host', array('id'=>$mnet_host_id));
 473      if ($aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnet_host_id))) {
 474          // Update.
 475          $aclrecord->accessctrl = $accessctrl;
 476          $DB->update_record('mnet_sso_access_control', $aclrecord);
 477  
 478          // Trigger access control updated event.
 479          $params = array(
 480              'objectid' => $aclrecord->id,
 481              'context' => context_system::instance(),
 482              'other' => array(
 483                  'username' => $username,
 484                  'hostname' => $mnethost->name,
 485                  'accessctrl' => $accessctrl
 486              )
 487          );
 488          $event = \core\event\mnet_access_control_updated::create($params);
 489          $event->add_record_snapshot('mnet_host', $mnethost);
 490          $event->trigger();
 491      } else {
 492          // Insert.
 493          $aclrecord = new stdClass();
 494          $aclrecord->username = $username;
 495          $aclrecord->accessctrl = $accessctrl;
 496          $aclrecord->mnet_host_id = $mnet_host_id;
 497          $aclrecord->id = $DB->insert_record('mnet_sso_access_control', $aclrecord);
 498  
 499          // Trigger access control created event.
 500          $params = array(
 501              'objectid' => $aclrecord->id,
 502              'context' => context_system::instance(),
 503              'other' => array(
 504                  'username' => $username,
 505                  'hostname' => $mnethost->name,
 506                  'accessctrl' => $accessctrl
 507              )
 508          );
 509          $event = \core\event\mnet_access_control_created::create($params);
 510          $event->add_record_snapshot('mnet_host', $mnethost);
 511          $event->trigger();
 512      }
 513      return true;
 514  }
 515  
 516  function mnet_get_peer_host ($mnethostid) {
 517      global $DB;
 518      static $hosts;
 519      if (!isset($hosts[$mnethostid])) {
 520          $host = $DB->get_record('mnet_host', array('id' => $mnethostid));
 521          $hosts[$mnethostid] = $host;
 522      }
 523      return $hosts[$mnethostid];
 524  }
 525  
 526  /**
 527   * Inline function to modify a url string so that mnet users are requested to
 528   * log in at their mnet identity provider (if they are not already logged in)
 529   * before ultimately being directed to the original url.
 530   *
 531   * @param string $jumpurl the url which user should initially be directed to.
 532   *     This is a URL associated with a moodle networking peer when it
 533   *     is fulfiling a role as an identity provider (IDP). Different urls for
 534   *     different peers, the jumpurl is formed partly from the IDP's webroot, and
 535   *     partly from a predefined local path within that webwroot.
 536   *     The result of the user hitting this jump url is that they will be asked
 537   *     to login (at their identity provider (if they aren't already)), mnet
 538   *     will prepare the necessary authentication information, then redirect
 539   *     them back to somewhere at the content provider(CP) moodle (this moodle)
 540   * @param array $url array with 2 elements
 541   *     0 - context the url was taken from, possibly just the url, possibly href="url"
 542   *     1 - the destination url
 543   * @return string the url the remote user should be supplied with.
 544   */
 545  function mnet_sso_apply_indirection ($jumpurl, $url) {
 546      global $USER, $CFG;
 547  
 548      $localpart='';
 549      $urlparts = parse_url($url[1]);
 550      if($urlparts) {
 551          if (isset($urlparts['path'])) {
 552              $path = $urlparts['path'];
 553              // if our wwwroot has a path component, need to strip that path from beginning of the
 554              // 'localpart' to make it relative to moodle's wwwroot
 555              $wwwrootpath = parse_url($CFG->wwwroot, PHP_URL_PATH);
 556              if (!empty($wwwrootpath) && strpos($path, $wwwrootpath) === 0) {
 557                  $path = substr($path, strlen($wwwrootpath));
 558              }
 559              $localpart .= $path;
 560          }
 561          if (isset($urlparts['query'])) {
 562              $localpart .= '?'.$urlparts['query'];
 563          }
 564          if (isset($urlparts['fragment'])) {
 565              $localpart .= '#'.$urlparts['fragment'];
 566          }
 567      }
 568      $indirecturl = $jumpurl . urlencode($localpart);
 569      //If we matched on more than just a url (ie an html link), return the url to an href format
 570      if ($url[0] != $url[1]) {
 571          $indirecturl = 'href="'.$indirecturl.'"';
 572      }
 573      return $indirecturl;
 574  }
 575  
 576  function mnet_get_app_jumppath ($applicationid) {
 577      global $DB;
 578      static $appjumppaths;
 579      if (!isset($appjumppaths[$applicationid])) {
 580          $ssojumpurl = $DB->get_field('mnet_application', 'sso_jump_url', array('id' => $applicationid));
 581          $appjumppaths[$applicationid] = $ssojumpurl;
 582      }
 583      return $appjumppaths[$applicationid];
 584  }
 585  
 586  
 587  /**
 588   * Output debug information about mnet.  this will go to the <b>error_log</b>.
 589   *
 590   * @param mixed $debugdata this can be a string, or array or object.
 591   * @param int   $debuglevel optional , defaults to 1. bump up for very noisy debug info
 592   */
 593  function mnet_debug($debugdata, $debuglevel=1) {
 594      global $CFG;
 595      $setlevel = get_config('', 'mnet_rpcdebug');
 596      if (empty($setlevel) || $setlevel < $debuglevel) {
 597          return;
 598      }
 599      if (is_object($debugdata)) {
 600          $debugdata = (array)$debugdata;
 601      }
 602      if (is_array($debugdata)) {
 603          mnet_debug('DUMPING ARRAY');
 604          foreach ($debugdata as $key => $value) {
 605              mnet_debug("$key: $value");
 606          }
 607          mnet_debug('END DUMPING ARRAY');
 608          return;
 609      }
 610      $prefix = 'MNET DEBUG ';
 611      if (defined('MNET_SERVER')) {
 612          $prefix .= " (server $CFG->wwwroot";
 613          if ($peer = get_mnet_remote_client() && !empty($peer->wwwroot)) {
 614              $prefix .= ", remote peer " . $peer->wwwroot;
 615          }
 616          $prefix .= ')';
 617      } else {
 618          $prefix .= " (client $CFG->wwwroot) ";
 619      }
 620      error_log("$prefix $debugdata");
 621  }
 622  
 623  /**
 624   * Return an array of information about all moodle's profile fields
 625   * which ones are optional, which ones are forced.
 626   * This is used as the basis of providing lists of profile fields to the administrator
 627   * to pick which fields to import/export over MNET
 628   *
 629   * @return array(forced => array, optional => array)
 630   */
 631  function mnet_profile_field_options() {
 632      global $DB;
 633      static $info;
 634      if (!empty($info)) {
 635          return $info;
 636      }
 637  
 638      $excludes = array(
 639          'id',              // makes no sense
 640          'mnethostid',      // makes no sense
 641          'timecreated',     // will be set to relative to the host anyway
 642          'timemodified',    // will be set to relative to the host anyway
 643          'auth',            // going to be set to 'mnet'
 644          'deleted',         // we should never get deleted users sent over, but don't send this anyway
 645          'confirmed',       // unconfirmed users can't log in to their home site, all remote users considered confirmed
 646          'password',        // no password for mnet users
 647          'theme',           // handled separately
 648          'lastip',          // will be set to relative to the host anyway
 649      );
 650  
 651      // these are the ones that user_not_fully_set_up will complain about
 652      // and also special case ones
 653      $forced = array(
 654          'username',
 655          'email',
 656          'firstname',
 657          'lastname',
 658          'auth',
 659          'wwwroot',
 660          'session.gc_lifetime',
 661          '_mnet_userpicture_timemodified',
 662          '_mnet_userpicture_mimetype',
 663      );
 664  
 665      // these are the ones we used to send/receive (pre 2.0)
 666      $legacy = array(
 667          'username',
 668          'email',
 669          'auth',
 670          'deleted',
 671          'firstname',
 672          'lastname',
 673          'city',
 674          'country',
 675          'lang',
 676          'timezone',
 677          'description',
 678          'mailformat',
 679          'maildigest',
 680          'maildisplay',
 681          'htmleditor',
 682          'wwwroot',
 683          'picture',
 684      );
 685  
 686      // get a random user record from the database to pull the fields off
 687      $randomuser = $DB->get_record('user', array(), '*', IGNORE_MULTIPLE);
 688      foreach ($randomuser as $key => $discard) {
 689          if (in_array($key, $excludes) || in_array($key, $forced)) {
 690              continue;
 691          }
 692          $fields[$key] = $key;
 693      }
 694      $info = array(
 695          'forced'   => $forced,
 696          'optional' => $fields,
 697          'legacy'   => $legacy,
 698      );
 699      return $info;
 700  }
 701  
 702  
 703  /**
 704   * Returns information about MNet peers
 705   *
 706   * @param bool $withdeleted should the deleted peers be returned too
 707   * @return array
 708   */
 709  function mnet_get_hosts($withdeleted = false) {
 710      global $CFG, $DB;
 711  
 712      $sql = "SELECT h.id, h.deleted, h.wwwroot, h.ip_address, h.name, h.public_key, h.public_key_expires,
 713                     h.transport, h.portno, h.last_connect_time, h.last_log_id, h.applicationid,
 714                     a.name as app_name, a.display_name as app_display_name, a.xmlrpc_server_url
 715                FROM {mnet_host} h
 716                JOIN {mnet_application} a ON h.applicationid = a.id
 717               WHERE h.id <> ?";
 718  
 719      if (!$withdeleted) {
 720          $sql .= "  AND h.deleted = 0";
 721      }
 722  
 723      $sql .= " ORDER BY h.deleted, h.name, h.id";
 724  
 725      return $DB->get_records_sql($sql, array($CFG->mnet_localhost_id));
 726  }
 727  
 728  
 729  /**
 730   * return an array information about services enabled for the given peer.
 731   * in two modes, fulldata or very basic data.
 732   *
 733   * @param mnet_peer $mnet_peer the peer to get information abut
 734   * @param boolean   $fulldata whether to just return which services are published/subscribed, or more information (defaults to full)
 735   *
 736   * @return array  If $fulldata is false, an array is returned like:
 737   *                publish => array(
 738   *                    serviceid => boolean,
 739   *                    serviceid => boolean,
 740   *                ),
 741   *                subscribe => array(
 742   *                    serviceid => boolean,
 743   *                    serviceid => boolean,
 744   *                )
 745   *                If $fulldata is true, an array is returned like:
 746   *                servicename => array(
 747   *                   apiversion => array(
 748   *                        name           => string
 749   *                        offer          => boolean
 750   *                        apiversion     => int
 751   *                        plugintype     => string
 752   *                        pluginname     => string
 753   *                        hostsubscribes => boolean
 754   *                        hostpublishes  => boolean
 755   *                   ),
 756   *               )
 757   */
 758  function mnet_get_service_info(mnet_peer $mnet_peer, $fulldata=true) {
 759      global $CFG, $DB;
 760  
 761      $requestkey = (!empty($fulldata) ? 'fulldata' : 'mydata');
 762  
 763      static $cache = array();
 764      if (array_key_exists($mnet_peer->id, $cache)) {
 765          return $cache[$mnet_peer->id][$requestkey];
 766      }
 767  
 768      $id_list = $mnet_peer->id;
 769      if (!empty($CFG->mnet_all_hosts_id)) {
 770          $id_list .= ', '.$CFG->mnet_all_hosts_id;
 771      }
 772  
 773      $concat = $DB->sql_concat('COALESCE(h2s.id,0) ', ' \'-\' ', ' svc.id', '\'-\'', 'r.plugintype', '\'-\'', 'r.pluginname');
 774  
 775      $query = "
 776          SELECT DISTINCT
 777              $concat as id,
 778              svc.id as serviceid,
 779              svc.name,
 780              svc.offer,
 781              svc.apiversion,
 782              r.plugintype,
 783              r.pluginname,
 784              h2s.hostid,
 785              h2s.publish,
 786              h2s.subscribe
 787          FROM
 788              {mnet_service2rpc} s2r,
 789              {mnet_rpc} r,
 790              {mnet_service} svc
 791          LEFT JOIN
 792              {mnet_host2service} h2s
 793          ON
 794              h2s.hostid in ($id_list) AND
 795              h2s.serviceid = svc.id
 796          WHERE
 797              svc.offer = '1' AND
 798              s2r.serviceid = svc.id AND
 799              s2r.rpcid = r.id
 800          ORDER BY
 801              svc.name ASC";
 802  
 803      $resultset = $DB->get_records_sql($query);
 804  
 805      if (is_array($resultset)) {
 806          $resultset = array_values($resultset);
 807      } else {
 808          $resultset = array();
 809      }
 810  
 811      require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
 812  
 813      $remoteservices = array();
 814      if ($mnet_peer->id != $CFG->mnet_all_hosts_id) {
 815          // Create a new request object
 816          $mnet_request = new mnet_xmlrpc_client();
 817  
 818          // Tell it the path to the method that we want to execute
 819          $mnet_request->set_method('system/listServices');
 820          $mnet_request->send($mnet_peer);
 821          if (is_array($mnet_request->response)) {
 822              foreach($mnet_request->response as $service) {
 823                  $remoteservices[$service['name']][$service['apiversion']] = $service;
 824              }
 825          }
 826      }
 827  
 828      $myservices = array();
 829      $mydata = array();
 830      foreach($resultset as $result) {
 831          $result->hostpublishes  = false;
 832          $result->hostsubscribes = false;
 833          if (isset($remoteservices[$result->name][$result->apiversion])) {
 834              if ($remoteservices[$result->name][$result->apiversion]['publish'] == 1) {
 835                  $result->hostpublishes  = true;
 836              }
 837              if ($remoteservices[$result->name][$result->apiversion]['subscribe'] == 1) {
 838                  $result->hostsubscribes  = true;
 839              }
 840          }
 841  
 842          if (empty($myservices[$result->name][$result->apiversion])) {
 843              $myservices[$result->name][$result->apiversion] = array('serviceid' => $result->serviceid,
 844                                                                      'name' => $result->name,
 845                                                                      'offer' => $result->offer,
 846                                                                      'apiversion' => $result->apiversion,
 847                                                                      'plugintype' => $result->plugintype,
 848                                                                      'pluginname' => $result->pluginname,
 849                                                                      'hostsubscribes' => $result->hostsubscribes,
 850                                                                      'hostpublishes' => $result->hostpublishes
 851                                                                      );
 852          }
 853  
 854          // allhosts_publish allows us to tell the admin that even though he
 855          // is disabling a service, it's still available to the host because
 856          // he's also publishing it to 'all hosts'
 857          if ($result->hostid == $CFG->mnet_all_hosts_id && $CFG->mnet_all_hosts_id != $mnet_peer->id) {
 858              $myservices[$result->name][$result->apiversion]['allhosts_publish'] = $result->publish;
 859              $myservices[$result->name][$result->apiversion]['allhosts_subscribe'] = $result->subscribe;
 860          } elseif (!empty($result->hostid)) {
 861              $myservices[$result->name][$result->apiversion]['I_publish'] = $result->publish;
 862              $myservices[$result->name][$result->apiversion]['I_subscribe'] = $result->subscribe;
 863          }
 864          $mydata['publish'][$result->serviceid] = $result->publish;
 865          $mydata['subscribe'][$result->serviceid] = $result->subscribe;
 866  
 867      }
 868  
 869      $cache[$mnet_peer->id]['fulldata'] = $myservices;
 870      $cache[$mnet_peer->id]['mydata'] = $mydata;
 871  
 872      return $cache[$mnet_peer->id][$requestkey];
 873  }
 874  
 875  /**
 876   * return an array of the profile fields to send
 877   * with user information to the given mnet host.
 878   *
 879   * @param mnet_peer $peer the peer to send the information to
 880   *
 881   * @return array (like 'username', 'firstname', etc)
 882   */
 883  function mnet_fields_to_send(mnet_peer $peer) {
 884      return _mnet_field_helper($peer, 'export');
 885  }
 886  
 887  /**
 888   * return an array of the profile fields to import
 889   * from the given host, when creating/updating user accounts
 890   *
 891   * @param mnet_peer $peer the peer we're getting the information from
 892   *
 893   * @return array (like 'username', 'firstname', etc)
 894   */
 895  function mnet_fields_to_import(mnet_peer $peer) {
 896      return _mnet_field_helper($peer, 'import');
 897  }
 898  
 899  /**
 900   * helper for {@see mnet_fields_to_import} and {@mnet_fields_to_send}
 901   *
 902   * @access private
 903   *
 904   * @param mnet_peer $peer the peer object
 905   * @param string    $key 'import' or 'export'
 906   *
 907   * @return array (like 'username', 'firstname', etc)
 908   */
 909  function _mnet_field_helper(mnet_peer $peer, $key) {
 910      $tmp = mnet_profile_field_options();
 911      $defaults = explode(',', get_config('moodle', 'mnetprofile' . $key . 'fields'));
 912      if ('1' === get_config('mnet', 'host' . $peer->id . $key . 'default')) {
 913          return array_merge($tmp['forced'], $defaults);
 914      }
 915      $hostsettings = get_config('mnet', 'host' . $peer->id . $key . 'fields');
 916      if (false === $hostsettings) {
 917          return array_merge($tmp['forced'], $defaults);
 918      }
 919      return array_merge($tmp['forced'], explode(',', $hostsettings));
 920  }
 921  
 922  
 923  /**
 924   * given a user object (or array) and a list of allowed fields,
 925   * strip out all the fields that should not be included.
 926   * This can be used both for outgoing data and incoming data.
 927   *
 928   * @param mixed $user array or object representing a database record
 929   * @param array $fields an array of allowed fields (usually from mnet_fields_to_{send,import}
 930   *
 931   * @return mixed array or object, depending what type of $user object was passed (datatype is respected)
 932   */
 933  function mnet_strip_user($user, $fields) {
 934      if (is_object($user)) {
 935          $user = (array)$user;
 936          $wasobject = true; // so we can cast back before we return
 937      }
 938  
 939      foreach ($user as $key => $value) {
 940          if (!in_array($key, $fields)) {
 941              unset($user[$key]);
 942          }
 943      }
 944      if (!empty($wasobject)) {
 945          $user = (object)$user;
 946      }
 947      return $user;
 948  }