Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/mnet/ -> lib.php (source)

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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      $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 ( gettype($publickey) != 'resource' ) {
 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));
 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          $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']);
 322          $keypair['publickey']  = openssl_pkey_get_public($keypair['certificate']);
 323          return $keypair;
 324      } else {
 325          $keypair = mnet_generate_keypair();
 326          return $keypair;
 327      }
 328  }
 329  
 330  /**
 331   * Generate public/private keys and store in the config table
 332   *
 333   * Use the distinguished name provided to create a CSR, and then sign that CSR
 334   * with the same credentials. Store the keypair you create in the config table.
 335   * If a distinguished name is not provided, create one using the fullname of
 336   * 'the course with ID 1' as your organization name, and your hostname (as
 337   * detailed in $CFG->wwwroot).
 338   *
 339   * @param   array  $dn  The distinguished name of the server
 340   * @return  string      The signature over that text
 341   */
 342  function mnet_generate_keypair($dn = null, $days=28) {
 343      global $CFG, $USER, $DB;
 344  
 345      // check if lifetime has been overriden
 346      if (!empty($CFG->mnetkeylifetime)) {
 347          $days = $CFG->mnetkeylifetime;
 348      }
 349  
 350      $host = strtolower($CFG->wwwroot);
 351      $host = preg_replace("~^http(s)?://~",'',$host);
 352      $break = strpos($host.'/' , '/');
 353      $host   = substr($host, 0, $break);
 354  
 355      $site = get_site();
 356      $organization = $site->fullname;
 357  
 358      $keypair = array();
 359  
 360      $country  = 'NZ';
 361      $province = 'Wellington';
 362      $locality = 'Wellington';
 363      $email    = !empty($CFG->noreplyaddress) ? $CFG->noreplyaddress : 'noreply@'.$_SERVER['HTTP_HOST'];
 364  
 365      if(!empty($USER->country)) {
 366          $country  = $USER->country;
 367      }
 368      if(!empty($USER->city)) {
 369          $province = $USER->city;
 370          $locality = $USER->city;
 371      }
 372      if(!empty($USER->email)) {
 373          $email    = $USER->email;
 374      }
 375  
 376      if (is_null($dn)) {
 377          $dn = array(
 378             "countryName" => $country,
 379             "stateOrProvinceName" => $province,
 380             "localityName" => $locality,
 381             "organizationName" => $organization,
 382             "organizationalUnitName" => 'Moodle',
 383             "commonName" => substr($CFG->wwwroot, 0, 64),
 384             "subjectAltName" => $CFG->wwwroot,
 385             "emailAddress" => $email
 386          );
 387      }
 388  
 389      $dnlimits = array(
 390             'countryName'            => 2,
 391             'stateOrProvinceName'    => 128,
 392             'localityName'           => 128,
 393             'organizationName'       => 64,
 394             'organizationalUnitName' => 64,
 395             'commonName'             => 64,
 396             'emailAddress'           => 128
 397      );
 398  
 399      foreach ($dnlimits as $key => $length) {
 400          $dn[$key] = core_text::substr($dn[$key], 0, $length);
 401      }
 402  
 403      // ensure we remove trailing slashes
 404      $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
 405      if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
 406          $new_key = openssl_pkey_new(array("config" => $CFG->opensslcnf));
 407      } else {
 408          $new_key = openssl_pkey_new();
 409      }
 410      if ($new_key === false) {
 411          // can not generate keys - missing openssl.cnf??
 412          return null;
 413      }
 414      if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
 415          $csr_rsc = openssl_csr_new($dn, $new_key, array("config" => $CFG->opensslcnf));
 416          $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days, array("config" => $CFG->opensslcnf));
 417      } else {
 418          $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
 419          $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
 420      }
 421      unset($csr_rsc); // Free up the resource
 422  
 423      // We export our self-signed certificate to a string.
 424      openssl_x509_export($selfSignedCert, $keypair['certificate']);
 425      openssl_x509_free($selfSignedCert);
 426  
 427      // Export your public/private key pair as a PEM encoded string. You
 428      // can protect it with an optional passphrase if you wish.
 429      if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
 430          $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'], null, array("config" => $CFG->opensslcnf));
 431      } else {
 432          $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
 433      }
 434      openssl_pkey_free($new_key);
 435      unset($new_key); // Free up the resource
 436  
 437      return $keypair;
 438  }
 439  
 440  
 441  function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
 442      global $DB;
 443  
 444      $mnethost = $DB->get_record('mnet_host', array('id'=>$mnet_host_id));
 445      if ($aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnet_host_id))) {
 446          // Update.
 447          $aclrecord->accessctrl = $accessctrl;
 448          $DB->update_record('mnet_sso_access_control', $aclrecord);
 449  
 450          // Trigger access control updated event.
 451          $params = array(
 452              'objectid' => $aclrecord->id,
 453              'context' => context_system::instance(),
 454              'other' => array(
 455                  'username' => $username,
 456                  'hostname' => $mnethost->name,
 457                  'accessctrl' => $accessctrl
 458              )
 459          );
 460          $event = \core\event\mnet_access_control_updated::create($params);
 461          $event->add_record_snapshot('mnet_host', $mnethost);
 462          $event->trigger();
 463      } else {
 464          // Insert.
 465          $aclrecord = new stdClass();
 466          $aclrecord->username = $username;
 467          $aclrecord->accessctrl = $accessctrl;
 468          $aclrecord->mnet_host_id = $mnet_host_id;
 469          $aclrecord->id = $DB->insert_record('mnet_sso_access_control', $aclrecord);
 470  
 471          // Trigger access control created event.
 472          $params = array(
 473              'objectid' => $aclrecord->id,
 474              'context' => context_system::instance(),
 475              'other' => array(
 476                  'username' => $username,
 477                  'hostname' => $mnethost->name,
 478                  'accessctrl' => $accessctrl
 479              )
 480          );
 481          $event = \core\event\mnet_access_control_created::create($params);
 482          $event->add_record_snapshot('mnet_host', $mnethost);
 483          $event->trigger();
 484      }
 485      return true;
 486  }
 487  
 488  function mnet_get_peer_host ($mnethostid) {
 489      global $DB;
 490      static $hosts;
 491      if (!isset($hosts[$mnethostid])) {
 492          $host = $DB->get_record('mnet_host', array('id' => $mnethostid));
 493          $hosts[$mnethostid] = $host;
 494      }
 495      return $hosts[$mnethostid];
 496  }
 497  
 498  /**
 499   * Inline function to modify a url string so that mnet users are requested to
 500   * log in at their mnet identity provider (if they are not already logged in)
 501   * before ultimately being directed to the original url.
 502   *
 503   * @param string $jumpurl the url which user should initially be directed to.
 504   *     This is a URL associated with a moodle networking peer when it
 505   *     is fulfiling a role as an identity provider (IDP). Different urls for
 506   *     different peers, the jumpurl is formed partly from the IDP's webroot, and
 507   *     partly from a predefined local path within that webwroot.
 508   *     The result of the user hitting this jump url is that they will be asked
 509   *     to login (at their identity provider (if they aren't already)), mnet
 510   *     will prepare the necessary authentication information, then redirect
 511   *     them back to somewhere at the content provider(CP) moodle (this moodle)
 512   * @param array $url array with 2 elements
 513   *     0 - context the url was taken from, possibly just the url, possibly href="url"
 514   *     1 - the destination url
 515   * @return string the url the remote user should be supplied with.
 516   */
 517  function mnet_sso_apply_indirection ($jumpurl, $url) {
 518      global $USER, $CFG;
 519  
 520      $localpart='';
 521      $urlparts = parse_url($url[1]);
 522      if($urlparts) {
 523          if (isset($urlparts['path'])) {
 524              $path = $urlparts['path'];
 525              // if our wwwroot has a path component, need to strip that path from beginning of the
 526              // 'localpart' to make it relative to moodle's wwwroot
 527              $wwwrootpath = parse_url($CFG->wwwroot, PHP_URL_PATH);
 528              if (!empty($wwwrootpath) and strpos($path, $wwwrootpath) === 0) {
 529                  $path = substr($path, strlen($wwwrootpath));
 530              }
 531              $localpart .= $path;
 532          }
 533          if (isset($urlparts['query'])) {
 534              $localpart .= '?'.$urlparts['query'];
 535          }
 536          if (isset($urlparts['fragment'])) {
 537              $localpart .= '#'.$urlparts['fragment'];
 538          }
 539      }
 540      $indirecturl = $jumpurl . urlencode($localpart);
 541      //If we matched on more than just a url (ie an html link), return the url to an href format
 542      if ($url[0] != $url[1]) {
 543          $indirecturl = 'href="'.$indirecturl.'"';
 544      }
 545      return $indirecturl;
 546  }
 547  
 548  function mnet_get_app_jumppath ($applicationid) {
 549      global $DB;
 550      static $appjumppaths;
 551      if (!isset($appjumppaths[$applicationid])) {
 552          $ssojumpurl = $DB->get_field('mnet_application', 'sso_jump_url', array('id' => $applicationid));
 553          $appjumppaths[$applicationid] = $ssojumpurl;
 554      }
 555      return $appjumppaths[$applicationid];
 556  }
 557  
 558  
 559  /**
 560   * Output debug information about mnet.  this will go to the <b>error_log</b>.
 561   *
 562   * @param mixed $debugdata this can be a string, or array or object.
 563   * @param int   $debuglevel optional , defaults to 1. bump up for very noisy debug info
 564   */
 565  function mnet_debug($debugdata, $debuglevel=1) {
 566      global $CFG;
 567      $setlevel = get_config('', 'mnet_rpcdebug');
 568      if (empty($setlevel) || $setlevel < $debuglevel) {
 569          return;
 570      }
 571      if (is_object($debugdata)) {
 572          $debugdata = (array)$debugdata;
 573      }
 574      if (is_array($debugdata)) {
 575          mnet_debug('DUMPING ARRAY');
 576          foreach ($debugdata as $key => $value) {
 577              mnet_debug("$key: $value");
 578          }
 579          mnet_debug('END DUMPING ARRAY');
 580          return;
 581      }
 582      $prefix = 'MNET DEBUG ';
 583      if (defined('MNET_SERVER')) {
 584          $prefix .= " (server $CFG->wwwroot";
 585          if ($peer = get_mnet_remote_client() && !empty($peer->wwwroot)) {
 586              $prefix .= ", remote peer " . $peer->wwwroot;
 587          }
 588          $prefix .= ')';
 589      } else {
 590          $prefix .= " (client $CFG->wwwroot) ";
 591      }
 592      error_log("$prefix $debugdata");
 593  }
 594  
 595  /**
 596   * Return an array of information about all moodle's profile fields
 597   * which ones are optional, which ones are forced.
 598   * This is used as the basis of providing lists of profile fields to the administrator
 599   * to pick which fields to import/export over MNET
 600   *
 601   * @return array(forced => array, optional => array)
 602   */
 603  function mnet_profile_field_options() {
 604      global $DB;
 605      static $info;
 606      if (!empty($info)) {
 607          return $info;
 608      }
 609  
 610      $excludes = array(
 611          'id',              // makes no sense
 612          'mnethostid',      // makes no sense
 613          'timecreated',     // will be set to relative to the host anyway
 614          'timemodified',    // will be set to relative to the host anyway
 615          'auth',            // going to be set to 'mnet'
 616          'deleted',         // we should never get deleted users sent over, but don't send this anyway
 617          'confirmed',       // unconfirmed users can't log in to their home site, all remote users considered confirmed
 618          'password',        // no password for mnet users
 619          'theme',           // handled separately
 620          'lastip',          // will be set to relative to the host anyway
 621      );
 622  
 623      // these are the ones that user_not_fully_set_up will complain about
 624      // and also special case ones
 625      $forced = array(
 626          'username',
 627          'email',
 628          'firstname',
 629          'lastname',
 630          'auth',
 631          'wwwroot',
 632          'session.gc_lifetime',
 633          '_mnet_userpicture_timemodified',
 634          '_mnet_userpicture_mimetype',
 635      );
 636  
 637      // these are the ones we used to send/receive (pre 2.0)
 638      $legacy = array(
 639          'username',
 640          'email',
 641          'auth',
 642          'deleted',
 643          'firstname',
 644          'lastname',
 645          'city',
 646          'country',
 647          'lang',
 648          'timezone',
 649          'description',
 650          'mailformat',
 651          'maildigest',
 652          'maildisplay',
 653          'htmleditor',
 654          'wwwroot',
 655          'picture',
 656      );
 657  
 658      // get a random user record from the database to pull the fields off
 659      $randomuser = $DB->get_record('user', array(), '*', IGNORE_MULTIPLE);
 660      foreach ($randomuser as $key => $discard) {
 661          if (in_array($key, $excludes) || in_array($key, $forced)) {
 662              continue;
 663          }
 664          $fields[$key] = $key;
 665      }
 666      $info = array(
 667          'forced'   => $forced,
 668          'optional' => $fields,
 669          'legacy'   => $legacy,
 670      );
 671      return $info;
 672  }
 673  
 674  
 675  /**
 676   * Returns information about MNet peers
 677   *
 678   * @param bool $withdeleted should the deleted peers be returned too
 679   * @return array
 680   */
 681  function mnet_get_hosts($withdeleted = false) {
 682      global $CFG, $DB;
 683  
 684      $sql = "SELECT h.id, h.deleted, h.wwwroot, h.ip_address, h.name, h.public_key, h.public_key_expires,
 685                     h.transport, h.portno, h.last_connect_time, h.last_log_id, h.applicationid,
 686                     a.name as app_name, a.display_name as app_display_name, a.xmlrpc_server_url
 687                FROM {mnet_host} h
 688                JOIN {mnet_application} a ON h.applicationid = a.id
 689               WHERE h.id <> ?";
 690  
 691      if (!$withdeleted) {
 692          $sql .= "  AND h.deleted = 0";
 693      }
 694  
 695      $sql .= " ORDER BY h.deleted, h.name, h.id";
 696  
 697      return $DB->get_records_sql($sql, array($CFG->mnet_localhost_id));
 698  }
 699  
 700  
 701  /**
 702   * return an array information about services enabled for the given peer.
 703   * in two modes, fulldata or very basic data.
 704   *
 705   * @param mnet_peer $mnet_peer the peer to get information abut
 706   * @param boolean   $fulldata whether to just return which services are published/subscribed, or more information (defaults to full)
 707   *
 708   * @return array  If $fulldata is false, an array is returned like:
 709   *                publish => array(
 710   *                    serviceid => boolean,
 711   *                    serviceid => boolean,
 712   *                ),
 713   *                subscribe => array(
 714   *                    serviceid => boolean,
 715   *                    serviceid => boolean,
 716   *                )
 717   *                If $fulldata is true, an array is returned like:
 718   *                servicename => array(
 719   *                   apiversion => array(
 720   *                        name           => string
 721   *                        offer          => boolean
 722   *                        apiversion     => int
 723   *                        plugintype     => string
 724   *                        pluginname     => string
 725   *                        hostsubscribes => boolean
 726   *                        hostpublishes  => boolean
 727   *                   ),
 728   *               )
 729   */
 730  function mnet_get_service_info(mnet_peer $mnet_peer, $fulldata=true) {
 731      global $CFG, $DB;
 732  
 733      $requestkey = (!empty($fulldata) ? 'fulldata' : 'mydata');
 734  
 735      static $cache = array();
 736      if (array_key_exists($mnet_peer->id, $cache)) {
 737          return $cache[$mnet_peer->id][$requestkey];
 738      }
 739  
 740      $id_list = $mnet_peer->id;
 741      if (!empty($CFG->mnet_all_hosts_id)) {
 742          $id_list .= ', '.$CFG->mnet_all_hosts_id;
 743      }
 744  
 745      $concat = $DB->sql_concat('COALESCE(h2s.id,0) ', ' \'-\' ', ' svc.id', '\'-\'', 'r.plugintype', '\'-\'', 'r.pluginname');
 746  
 747      $query = "
 748          SELECT DISTINCT
 749              $concat as id,
 750              svc.id as serviceid,
 751              svc.name,
 752              svc.offer,
 753              svc.apiversion,
 754              r.plugintype,
 755              r.pluginname,
 756              h2s.hostid,
 757              h2s.publish,
 758              h2s.subscribe
 759          FROM
 760              {mnet_service2rpc} s2r,
 761              {mnet_rpc} r,
 762              {mnet_service} svc
 763          LEFT JOIN
 764              {mnet_host2service} h2s
 765          ON
 766              h2s.hostid in ($id_list) AND
 767              h2s.serviceid = svc.id
 768          WHERE
 769              svc.offer = '1' AND
 770              s2r.serviceid = svc.id AND
 771              s2r.rpcid = r.id
 772          ORDER BY
 773              svc.name ASC";
 774  
 775      $resultset = $DB->get_records_sql($query);
 776  
 777      if (is_array($resultset)) {
 778          $resultset = array_values($resultset);
 779      } else {
 780          $resultset = array();
 781      }
 782  
 783      require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
 784  
 785      $remoteservices = array();
 786      if ($mnet_peer->id != $CFG->mnet_all_hosts_id) {
 787          // Create a new request object
 788          $mnet_request = new mnet_xmlrpc_client();
 789  
 790          // Tell it the path to the method that we want to execute
 791          $mnet_request->set_method('system/listServices');
 792          $mnet_request->send($mnet_peer);
 793          if (is_array($mnet_request->response)) {
 794              foreach($mnet_request->response as $service) {
 795                  $remoteservices[$service['name']][$service['apiversion']] = $service;
 796              }
 797          }
 798      }
 799  
 800      $myservices = array();
 801      $mydata = array();
 802      foreach($resultset as $result) {
 803          $result->hostpublishes  = false;
 804          $result->hostsubscribes = false;
 805          if (isset($remoteservices[$result->name][$result->apiversion])) {
 806              if ($remoteservices[$result->name][$result->apiversion]['publish'] == 1) {
 807                  $result->hostpublishes  = true;
 808              }
 809              if ($remoteservices[$result->name][$result->apiversion]['subscribe'] == 1) {
 810                  $result->hostsubscribes  = true;
 811              }
 812          }
 813  
 814          if (empty($myservices[$result->name][$result->apiversion])) {
 815              $myservices[$result->name][$result->apiversion] = array('serviceid' => $result->serviceid,
 816                                                                      'name' => $result->name,
 817                                                                      'offer' => $result->offer,
 818                                                                      'apiversion' => $result->apiversion,
 819                                                                      'plugintype' => $result->plugintype,
 820                                                                      'pluginname' => $result->pluginname,
 821                                                                      'hostsubscribes' => $result->hostsubscribes,
 822                                                                      'hostpublishes' => $result->hostpublishes
 823                                                                      );
 824          }
 825  
 826          // allhosts_publish allows us to tell the admin that even though he
 827          // is disabling a service, it's still available to the host because
 828          // he's also publishing it to 'all hosts'
 829          if ($result->hostid == $CFG->mnet_all_hosts_id && $CFG->mnet_all_hosts_id != $mnet_peer->id) {
 830              $myservices[$result->name][$result->apiversion]['allhosts_publish'] = $result->publish;
 831              $myservices[$result->name][$result->apiversion]['allhosts_subscribe'] = $result->subscribe;
 832          } elseif (!empty($result->hostid)) {
 833              $myservices[$result->name][$result->apiversion]['I_publish'] = $result->publish;
 834              $myservices[$result->name][$result->apiversion]['I_subscribe'] = $result->subscribe;
 835          }
 836          $mydata['publish'][$result->serviceid] = $result->publish;
 837          $mydata['subscribe'][$result->serviceid] = $result->subscribe;
 838  
 839      }
 840  
 841      $cache[$mnet_peer->id]['fulldata'] = $myservices;
 842      $cache[$mnet_peer->id]['mydata'] = $mydata;
 843  
 844      return $cache[$mnet_peer->id][$requestkey];
 845  }
 846  
 847  /**
 848   * return an array of the profile fields to send
 849   * with user information to the given mnet host.
 850   *
 851   * @param mnet_peer $peer the peer to send the information to
 852   *
 853   * @return array (like 'username', 'firstname', etc)
 854   */
 855  function mnet_fields_to_send(mnet_peer $peer) {
 856      return _mnet_field_helper($peer, 'export');
 857  }
 858  
 859  /**
 860   * return an array of the profile fields to import
 861   * from the given host, when creating/updating user accounts
 862   *
 863   * @param mnet_peer $peer the peer we're getting the information from
 864   *
 865   * @return array (like 'username', 'firstname', etc)
 866   */
 867  function mnet_fields_to_import(mnet_peer $peer) {
 868      return _mnet_field_helper($peer, 'import');
 869  }
 870  
 871  /**
 872   * helper for {@see mnet_fields_to_import} and {@mnet_fields_to_send}
 873   *
 874   * @access private
 875   *
 876   * @param mnet_peer $peer the peer object
 877   * @param string    $key 'import' or 'export'
 878   *
 879   * @return array (like 'username', 'firstname', etc)
 880   */
 881  function _mnet_field_helper(mnet_peer $peer, $key) {
 882      $tmp = mnet_profile_field_options();
 883      $defaults = explode(',', get_config('moodle', 'mnetprofile' . $key . 'fields'));
 884      if ('1' === get_config('mnet', 'host' . $peer->id . $key . 'default')) {
 885          return array_merge($tmp['forced'], $defaults);
 886      }
 887      $hostsettings = get_config('mnet', 'host' . $peer->id . $key . 'fields');
 888      if (false === $hostsettings) {
 889          return array_merge($tmp['forced'], $defaults);
 890      }
 891      return array_merge($tmp['forced'], explode(',', $hostsettings));
 892  }
 893  
 894  
 895  /**
 896   * given a user object (or array) and a list of allowed fields,
 897   * strip out all the fields that should not be included.
 898   * This can be used both for outgoing data and incoming data.
 899   *
 900   * @param mixed $user array or object representing a database record
 901   * @param array $fields an array of allowed fields (usually from mnet_fields_to_{send,import}
 902   *
 903   * @return mixed array or object, depending what type of $user object was passed (datatype is respected)
 904   */
 905  function mnet_strip_user($user, $fields) {
 906      if (is_object($user)) {
 907          $user = (array)$user;
 908          $wasobject = true; // so we can cast back before we return
 909      }
 910  
 911      foreach ($user as $key => $value) {
 912          if (!in_array($key, $fields)) {
 913              unset($user[$key]);
 914          }
 915      }
 916      if (!empty($wasobject)) {
 917          $user = (object)$user;
 918      }
 919      return $user;
 920  }