Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/mnet/ -> lib.php (source)

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

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