first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for core_mnet.
*
* @package core_mnet
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_mnet\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for core_mnet implementing null_provider.
*
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
/**
* Info about the local environment, wrt RPC
*
* This should really be a singleton. A PHP5 Todo I guess.
*/
class mnet_environment {
var $id = 0;
var $wwwroot = '';
var $ip_address = '';
var $public_key = '';
var $public_key_expires = 0;
var $last_connect_time = 0;
var $last_log_id = 0;
var $keypair = array();
var $deleted = 0;
/** @var string mnet host name. */
public $name;
/** @var int mnet host transport. */
public $transport;
/** @var int mnet host port number. */
public $portno;
/** @var int mnet host force theme. */
public $force_theme;
/** @var string mnet host theme. */
public $theme;
/** @var int mnet host application ID. */
public $applicationid;
/** @var int mnet host SSL verification. */
public $sslverification;
function init() {
global $CFG, $DB;
// Bootstrap the object data on first load.
if (!$hostobject = $DB->get_record('mnet_host', array('id'=>$CFG->mnet_localhost_id))) {
return false;
}
$temparr = get_object_vars($hostobject);
foreach($temparr as $key => $value) {
$this->$key = $value;
}
unset($hostobject, $temparr);
// Unless this is an install/upgrade, generate the SSL keys.
if (empty($this->public_key)) {
$this->get_keypair();
}
// We need to set up a record that represents 'all hosts'. Any rights
// granted to this host will be conferred on all hosts.
if (empty($CFG->mnet_all_hosts_id) ) {
$hostobject = new stdClass();
$hostobject->wwwroot = '';
$hostobject->ip_address = '';
$hostobject->public_key = '';
$hostobject->public_key_expires = 0;
$hostobject->last_connect_time = 0;
$hostobject->last_log_id = 0;
$hostobject->deleted = 0;
$hostobject->name = 'All Hosts';
$hostobject->id = $DB->insert_record('mnet_host',$hostobject);
set_config('mnet_all_hosts_id', $hostobject->id);
$CFG->mnet_all_hosts_id = $hostobject->id;
unset($hostobject);
}
}
function get_keypair() {
global $DB, $CFG;
// We don't generate keys on install/upgrade because we want the USER
// record to have an email address, city and country already.
if (during_initial_install()) return true;
if ($CFG->mnet_dispatcher_mode == 'off') return true;
if (!extension_loaded("openssl")) return true;
if (!empty($this->keypair)) return true;
$this->keypair = array();
$keypair = get_config('mnet', 'openssl');
if (!empty($keypair)) {
// Explode/Implode is faster than Unserialize/Serialize
list($this->keypair['certificate'], $this->keypair['keypair_PEM']) = explode('@@@@@@@@', $keypair);
}
if ($this->public_key_expires <= time()) {
// Key generation/rotation
// 1. Archive the current key (if there is one).
$result = get_config('mnet', 'openssl_history');
if(empty($result)) {
set_config('openssl_history', serialize(array()), 'mnet');
$openssl_history = array();
} else {
$openssl_history = unserialize($result);
}
if(count($this->keypair)) {
$this->keypair['expires'] = $this->public_key_expires;
array_unshift($openssl_history, $this->keypair);
}
// 2. How many old keys do we want to keep? Use array_slice to get
// rid of any we don't want
$openssl_generations = get_config('mnet', 'openssl_generations');
if(empty($openssl_generations)) {
set_config('openssl_generations', 3, 'mnet');
$openssl_generations = 3;
}
if(count($openssl_history) > $openssl_generations) {
$openssl_history = array_slice($openssl_history, 0, $openssl_generations);
}
set_config('openssl_history', serialize($openssl_history), 'mnet');
// 3. Generate fresh keys
$this->replace_keys();
}
return true;
}
function replace_keys() {
global $DB, $CFG;
$keypair = mnet_generate_keypair();
if (empty($keypair)) {
error_log('Can not generate keypair, sorry');
return;
}
$this->keypair = array();
$this->keypair = $keypair;
$this->public_key = $this->keypair['certificate'];
$details = openssl_x509_parse($this->public_key);
$this->public_key_expires = $details['validTo_time_t'];
$this->wwwroot = $CFG->wwwroot;
if (empty($_SERVER['SERVER_ADDR'])) {
// SERVER_ADDR is only returned by Apache-like webservers
$my_hostname = mnet_get_hostname_from_uri($CFG->wwwroot);
$my_ip = gethostbyname($my_hostname); // Returns unmodified hostname on failure. DOH!
if ($my_ip == $my_hostname) {
$this->ip_address = 'UNKNOWN';
} else {
$this->ip_address = $my_ip;
}
} else {
$this->ip_address = $_SERVER['SERVER_ADDR'];
}
set_config('openssl', implode('@@@@@@@@', $this->keypair), 'mnet');
$DB->update_record('mnet_host', $this);
error_log('New public key has been generated. It expires ' . date('Y/m/d h:i:s', $this->public_key_expires));
}
function get_private_key() {
if (empty($this->keypair)) $this->get_keypair();
return openssl_pkey_get_private($this->keypair['keypair_PEM']);
}
function get_public_key() {
if (!isset($this->keypair)) $this->get_keypair();
return openssl_pkey_get_public($this->keypair['certificate']);
}
}
+948
View File
@@ -0,0 +1,948 @@
<?php
/**
* Library functions for mnet
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
require_once $CFG->dirroot.'/mnet/xmlrpc/xmlparser.php';
require_once $CFG->dirroot.'/mnet/peer.php';
require_once $CFG->dirroot.'/mnet/environment.php';
/// CONSTANTS ///////////////////////////////////////////////////////////
define('RPC_OK', 0);
define('RPC_NOSUCHFILE', 1);
define('RPC_NOSUCHCLASS', 2);
define('RPC_NOSUCHFUNCTION', 3);
define('RPC_FORBIDDENFUNCTION', 4);
define('RPC_NOSUCHMETHOD', 5);
define('RPC_FORBIDDENMETHOD', 6);
/**
* Strip extraneous detail from a URL or URI and return the hostname
*
* @param string $uri The URI of a file on the remote computer, optionally
* including its http:// prefix like
* http://www.example.com/index.html
* @return string Just the hostname
*/
function mnet_get_hostname_from_uri($uri = null) {
$count = preg_match("@^(?:http[s]?://)?([A-Z0-9\-\.]+).*@i", $uri, $matches);
if ($count > 0) return $matches[1];
return false;
}
/**
* Get the remote machine's SSL Cert
*
* @param string $uri The URI of a file on the remote computer, including
* its http:// or https:// prefix
* @return string A PEM formatted SSL Certificate.
*/
function mnet_get_public_key($uri, $application=null) {
global $CFG, $DB;
$mnet = get_mnet_environment();
// The key may be cached in the mnet_set_public_key function...
// check this first
$key = mnet_set_public_key($uri);
if ($key != false) {
return $key;
}
if (empty($application)) {
$application = $DB->get_record('mnet_application', array('name'=>'moodle'));
}
$params = [
new \PhpXmlRpc\Value($CFG->wwwroot),
new \PhpXmlRpc\Value($mnet->public_key),
new \PhpXmlRpc\Value($application->name),
];
$request = new \PhpXmlRpc\Request('system/keyswap', $params);
// Let's create a client to handle the request and the response easily.
$client = new \PhpXmlRpc\Client($uri . $application->xmlrpc_server_url);
$client->setOption('use_curl', \PhpXmlRpc\Client::USE_CURL_ALWAYS);
$client->setOption('user_agent', 'Moodle');
$client->return_type = 'xmlrpcvals'; // This (keyswap) is not encrypted, so we can expect proper xmlrpc in this case.
$client->setOption('request_charset_encoding', 'utf-8');
$client->setOption('accepted_charset_encodings', ['utf-8']);
// TODO: Link this to DEBUG DEVELOPER or with MNET debugging...
// $client->setdebug(1); // See a good number of complete requests and responses.
$client->setOption('verifyhost', 0);
$client->setOption('verifypeer', false);
// TODO: It's curious that this service (keyswap) that needs
// a custom client, different from mnet_xmlrpc_client, because
// this is not encrypted / signed, does support proxies and the
// general one does not. Worth analysing if the support below
// should be added to it.
// Some curl options need to be set apart, accumulate them here.
$extracurloptions = [];
// Check for proxy.
if (!empty($CFG->proxyhost) && !is_proxybypass($uri)) {
// SOCKS supported in PHP5 only.
if (!empty($CFG->proxytype) && ($CFG->proxytype == 'SOCKS5')) {
if (defined('CURLPROXY_SOCKS5')) {
$extracurloptions[CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5;
} else {
throw new \moodle_exception( 'socksnotsupported', 'mnet');
}
}
$extracurloptions[CURLOPT_HTTPPROXYTUNNEL] = false;
// Configure proxy host, port, user, pass and auth.
$client->setProxy(
$CFG->proxyhost,
empty($CFG->proxyport) ? 0 : $CFG->proxyport,
empty($CFG->proxyuser) ? '' : $CFG->proxyuser,
empty($CFG->proxypassword) ? '' : $CFG->proxypassword,
defined('CURLOPT_PROXYAUTH') ? CURLAUTH_BASIC | CURLAUTH_NTLM : 1);
}
// Finally, add the extra curl options we may have accumulated.
$client->setCurlOptions($extracurloptions);
$response = $client->send($request, 60);
// Check curl / xmlrpc errors.
if ($response->faultCode()) {
debugging("Request for $uri failed with error {$response->faultCode()}: {$response->faultString()}");
return false;
}
// Check HTTP error code.
$status = $response->httpResponse()['status_code'];
if (!empty($status) && ($status != 200)) {
debugging("Request for $uri failed with HTTP code " . $status);
return false;
}
// Get the peer actual public key from the response.
$res = $response->value()->scalarval();
if (!is_array($res)) { // ! error
$public_certificate = $res;
$credentials=array();
if (strlen(trim($public_certificate))) {
$credentials = openssl_x509_parse($public_certificate);
$host = $credentials['subject']['CN'];
if (array_key_exists( 'subjectAltName', $credentials['subject'])) {
$host = $credentials['subject']['subjectAltName'];
}
if (strpos($uri, $host) !== false) {
mnet_set_public_key($uri, $public_certificate);
return $public_certificate;
}
else {
debugging("Request for $uri returned public key for different URI - $host");
}
}
else {
debugging("Request for $uri returned empty response");
}
}
else {
debugging( "Request for $uri returned unexpected result");
}
return false;
}
/**
* Store a URI's public key in a static variable, or retrieve the key for a URI
*
* @param string $uri The URI of a file on the remote computer, including its
* https:// prefix
* @param mixed $key A public key to store in the array OR null. If the key
* is null, the function will return the previously stored
* key for the supplied URI, should it exist.
* @return mixed A public key OR true/false.
*/
function mnet_set_public_key($uri, $key = null) {
static $keyarray = array();
if (isset($keyarray[$uri]) && empty($key)) {
return $keyarray[$uri];
} elseif (!empty($key)) {
$keyarray[$uri] = $key;
return true;
}
return false;
}
/**
* Sign a message and return it in an XML-Signature document
*
* This function can sign any content, but it was written to provide a system of
* signing XML-RPC request and response messages. The message will be base64
* encoded, so it does not need to be text.
*
* We compute the SHA1 digest of the message.
* We compute a signature on that digest with our private key.
* We link to the public key that can be used to verify our signature.
* We base64 the message data.
* We identify our wwwroot - this must match our certificate's CN
*
* The XML-RPC document will be parceled inside an XML-SIG document, which holds
* the base64_encoded XML as an object, the SHA1 digest of that document, and a
* signature of that document using the local private key. This signature will
* uniquely identify the RPC document as having come from this server.
*
* See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c
* site
*
* @param string $message The data you want to sign
* @param resource $privatekey The private key to sign the response with
* @return string An XML-DSig document
*/
function mnet_sign_message($message, $privatekey = null) {
global $CFG;
$digest = sha1($message);
$mnet = get_mnet_environment();
// If the user hasn't supplied a private key (for example, one of our older,
// expired private keys, we get the current default private key and use that.
if ($privatekey == null) {
$privatekey = $mnet->get_private_key();
}
// The '$sig' value below is returned by reference.
// We initialize it first to stop my IDE from complaining.
$sig = '';
$bool = openssl_sign($message, $sig, $privatekey);
// Avoid passing null values to base64_encode.
if ($bool === false) {
throw new \moodle_exception('opensslsignerror');
}
$message = '<?xml version="1.0" encoding="iso-8859-1"?>
<signedMessage>
<Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#XMLRPC-MSG">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>'.$digest.'</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>'.base64_encode($sig).'</SignatureValue>
<KeyInfo>
<RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
</KeyInfo>
</Signature>
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
<wwwroot>'.$mnet->wwwroot.'</wwwroot>
<timestamp>'.time().'</timestamp>
</signedMessage>';
return $message;
}
/**
* Encrypt a message and return it in an XML-Encrypted document
*
* This function can encrypt any content, but it was written to provide a system
* of encrypting XML-RPC request and response messages. The message will be
* base64 encoded, so it does not need to be text - binary data should work.
*
* We compute the SHA1 digest of the message.
* We compute a signature on that digest with our private key.
* We link to the public key that can be used to verify our signature.
* We base64 the message data.
* We identify our wwwroot - this must match our certificate's CN
*
* The XML-RPC document will be parceled inside an XML-SIG document, which holds
* the base64_encoded XML as an object, the SHA1 digest of that document, and a
* signature of that document using the local private key. This signature will
* uniquely identify the RPC document as having come from this server.
*
* See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c
* site
*
* @param string $message The data you want to sign
* @param string $remote_certificate Peer's certificate in PEM format
* @return string An XML-ENC document
*/
function mnet_encrypt_message($message, $remote_certificate) {
$mnet = get_mnet_environment();
// Generate a key resource from the remote_certificate text string
$publickey = openssl_get_publickey($remote_certificate);
if ($publickey === false) {
// Remote certificate is faulty.
return false;
}
// Initialize vars
$encryptedstring = '';
$symmetric_keys = array();
// passed by ref -> &$encryptedstring &$symmetric_keys
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey), 'RC4');
// Avoid passing null values to base64_encode.
if ($bool === false) {
throw new \moodle_exception('opensslsealerror');
}
$message = $encryptedstring;
$symmetrickey = array_pop($symmetric_keys);
$message = '<?xml version="1.0" encoding="iso-8859-1"?>
<encryptedMessage>
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
<ds:KeyName>XMLENC</ds:KeyName>
</ds:KeyInfo>
<CipherData>
<CipherValue>'.base64_encode($message).'</CipherValue>
</CipherData>
</EncryptedData>
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:KeyName>SSLKEY</ds:KeyName>
</ds:KeyInfo>
<CipherData>
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
</CipherData>
<ReferenceList>
<DataReference URI="#ED"/>
</ReferenceList>
<CarriedKeyName>XMLENC</CarriedKeyName>
</EncryptedKey>
<wwwroot>'.$mnet->wwwroot.'</wwwroot>
</encryptedMessage>';
return $message;
}
/**
* Get your SSL keys from the database, or create them (if they don't exist yet)
*
* Get your SSL keys from the database, or (if they don't exist yet) call
* mnet_generate_keypair to create them
*
* @param string $string The text you want to sign
* @return string The signature over that text
*/
function mnet_get_keypair() {
global $CFG, $DB;
static $keypair = null;
if (!is_null($keypair)) return $keypair;
if ($result = get_config('mnet', 'openssl')) {
list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result);
return $keypair;
} else {
$keypair = mnet_generate_keypair();
return $keypair;
}
}
/**
* Generate public/private keys and store in the config table
*
* Use the distinguished name provided to create a CSR, and then sign that CSR
* with the same credentials. Store the keypair you create in the config table.
* If a distinguished name is not provided, create one using the fullname of
* 'the course with ID 1' as your organization name, and your hostname (as
* detailed in $CFG->wwwroot).
*
* @param array $dn The distinguished name of the server
* @return string The signature over that text
*/
function mnet_generate_keypair($dn = null, $days=28) {
global $CFG, $USER, $DB;
// check if lifetime has been overriden
if (!empty($CFG->mnetkeylifetime)) {
$days = $CFG->mnetkeylifetime;
}
$host = strtolower($CFG->wwwroot);
$host = preg_replace("~^http(s)?://~",'',$host);
$break = strpos($host.'/' , '/');
$host = substr($host, 0, $break);
$site = get_site();
$organization = $site->fullname;
$keypair = array();
$country = 'NZ';
$province = 'Wellington';
$locality = 'Wellington';
$email = !empty($CFG->noreplyaddress) ? $CFG->noreplyaddress : 'noreply@'.$_SERVER['HTTP_HOST'];
if(!empty($USER->country)) {
$country = $USER->country;
}
if(!empty($USER->city)) {
$province = $USER->city;
$locality = $USER->city;
}
if(!empty($USER->email)) {
$email = $USER->email;
}
if (is_null($dn)) {
$dn = array(
"countryName" => $country,
"stateOrProvinceName" => $province,
"localityName" => $locality,
"organizationName" => $organization,
"organizationalUnitName" => 'Moodle',
"commonName" => substr($CFG->wwwroot, 0, 64),
"subjectAltName" => $CFG->wwwroot,
"emailAddress" => $email
);
}
$dnlimits = array(
'countryName' => 2,
'stateOrProvinceName' => 128,
'localityName' => 128,
'organizationName' => 64,
'organizationalUnitName' => 64,
'commonName' => 64,
'emailAddress' => 128
);
foreach ($dnlimits as $key => $length) {
$dn[$key] = core_text::substr($dn[$key], 0, $length);
}
// ensure we remove trailing slashes
$dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
$new_key = openssl_pkey_new(array("config" => $CFG->opensslcnf));
} else {
$new_key = openssl_pkey_new();
}
if ($new_key === false) {
// can not generate keys - missing openssl.cnf??
return null;
}
if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
$csr_rsc = openssl_csr_new($dn, $new_key, array("config" => $CFG->opensslcnf));
$selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days, array("config" => $CFG->opensslcnf));
} else {
$csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
$selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
}
unset($csr_rsc); // Free up the resource
// We export our self-signed certificate to a string.
openssl_x509_export($selfSignedCert, $keypair['certificate']);
// TODO: Remove this block once PHP 8.0 becomes required.
if (PHP_MAJOR_VERSION < 8) {
openssl_x509_free($selfSignedCert);
}
// Export your public/private key pair as a PEM encoded string. You
// can protect it with an optional passphrase if you wish.
if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
$export = openssl_pkey_export($new_key, $keypair['keypair_PEM'], null, array("config" => $CFG->opensslcnf));
} else {
$export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
}
// TODO: Remove this block once PHP 8.0 becomes required.
if (PHP_MAJOR_VERSION < 8) {
openssl_pkey_free($new_key);
}
unset($new_key); // Free up the resource
return $keypair;
}
function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
global $DB;
$mnethost = $DB->get_record('mnet_host', array('id'=>$mnet_host_id));
if ($aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnet_host_id))) {
// Update.
$aclrecord->accessctrl = $accessctrl;
$DB->update_record('mnet_sso_access_control', $aclrecord);
// Trigger access control updated event.
$params = array(
'objectid' => $aclrecord->id,
'context' => context_system::instance(),
'other' => array(
'username' => $username,
'hostname' => $mnethost->name,
'accessctrl' => $accessctrl
)
);
$event = \core\event\mnet_access_control_updated::create($params);
$event->add_record_snapshot('mnet_host', $mnethost);
$event->trigger();
} else {
// Insert.
$aclrecord = new stdClass();
$aclrecord->username = $username;
$aclrecord->accessctrl = $accessctrl;
$aclrecord->mnet_host_id = $mnet_host_id;
$aclrecord->id = $DB->insert_record('mnet_sso_access_control', $aclrecord);
// Trigger access control created event.
$params = array(
'objectid' => $aclrecord->id,
'context' => context_system::instance(),
'other' => array(
'username' => $username,
'hostname' => $mnethost->name,
'accessctrl' => $accessctrl
)
);
$event = \core\event\mnet_access_control_created::create($params);
$event->add_record_snapshot('mnet_host', $mnethost);
$event->trigger();
}
return true;
}
function mnet_get_peer_host ($mnethostid) {
global $DB;
static $hosts;
if (!isset($hosts[$mnethostid])) {
$host = $DB->get_record('mnet_host', array('id' => $mnethostid));
$hosts[$mnethostid] = $host;
}
return $hosts[$mnethostid];
}
/**
* Inline function to modify a url string so that mnet users are requested to
* log in at their mnet identity provider (if they are not already logged in)
* before ultimately being directed to the original url.
*
* @param string $jumpurl the url which user should initially be directed to.
* This is a URL associated with a moodle networking peer when it
* is fulfiling a role as an identity provider (IDP). Different urls for
* different peers, the jumpurl is formed partly from the IDP's webroot, and
* partly from a predefined local path within that webwroot.
* The result of the user hitting this jump url is that they will be asked
* to login (at their identity provider (if they aren't already)), mnet
* will prepare the necessary authentication information, then redirect
* them back to somewhere at the content provider(CP) moodle (this moodle)
* @param array $url array with 2 elements
* 0 - context the url was taken from, possibly just the url, possibly href="url"
* 1 - the destination url
* @return string the url the remote user should be supplied with.
*/
function mnet_sso_apply_indirection ($jumpurl, $url) {
global $USER, $CFG;
$localpart='';
$urlparts = parse_url($url[1]);
if($urlparts) {
if (isset($urlparts['path'])) {
$path = $urlparts['path'];
// if our wwwroot has a path component, need to strip that path from beginning of the
// 'localpart' to make it relative to moodle's wwwroot
$wwwrootpath = parse_url($CFG->wwwroot, PHP_URL_PATH);
if (!empty($wwwrootpath) && strpos($path, $wwwrootpath) === 0) {
$path = substr($path, strlen($wwwrootpath));
}
$localpart .= $path;
}
if (isset($urlparts['query'])) {
$localpart .= '?'.$urlparts['query'];
}
if (isset($urlparts['fragment'])) {
$localpart .= '#'.$urlparts['fragment'];
}
}
$indirecturl = $jumpurl . urlencode($localpart);
//If we matched on more than just a url (ie an html link), return the url to an href format
if ($url[0] != $url[1]) {
$indirecturl = 'href="'.$indirecturl.'"';
}
return $indirecturl;
}
function mnet_get_app_jumppath ($applicationid) {
global $DB;
static $appjumppaths;
if (!isset($appjumppaths[$applicationid])) {
$ssojumpurl = $DB->get_field('mnet_application', 'sso_jump_url', array('id' => $applicationid));
$appjumppaths[$applicationid] = $ssojumpurl;
}
return $appjumppaths[$applicationid];
}
/**
* Output debug information about mnet. this will go to the <b>error_log</b>.
*
* @param mixed $debugdata this can be a string, or array or object.
* @param int $debuglevel optional , defaults to 1. bump up for very noisy debug info
*/
function mnet_debug($debugdata, $debuglevel=1) {
global $CFG;
$setlevel = get_config('', 'mnet_rpcdebug');
if (empty($setlevel) || $setlevel < $debuglevel) {
return;
}
if (is_object($debugdata)) {
$debugdata = (array)$debugdata;
}
if (is_array($debugdata)) {
mnet_debug('DUMPING ARRAY');
foreach ($debugdata as $key => $value) {
mnet_debug("$key: $value");
}
mnet_debug('END DUMPING ARRAY');
return;
}
$prefix = 'MNET DEBUG ';
if (defined('MNET_SERVER')) {
$prefix .= " (server $CFG->wwwroot";
if ($peer = get_mnet_remote_client() && !empty($peer->wwwroot)) {
$prefix .= ", remote peer " . $peer->wwwroot;
}
$prefix .= ')';
} else {
$prefix .= " (client $CFG->wwwroot) ";
}
error_log("$prefix $debugdata");
}
/**
* Return an array of information about all moodle's profile fields
* which ones are optional, which ones are forced.
* This is used as the basis of providing lists of profile fields to the administrator
* to pick which fields to import/export over MNET
*
* @return array(forced => array, optional => array)
*/
function mnet_profile_field_options() {
global $DB;
static $info;
if (!empty($info)) {
return $info;
}
$excludes = array(
'id', // makes no sense
'mnethostid', // makes no sense
'timecreated', // will be set to relative to the host anyway
'timemodified', // will be set to relative to the host anyway
'auth', // going to be set to 'mnet'
'deleted', // we should never get deleted users sent over, but don't send this anyway
'confirmed', // unconfirmed users can't log in to their home site, all remote users considered confirmed
'password', // no password for mnet users
'theme', // handled separately
'lastip', // will be set to relative to the host anyway
);
// these are the ones that user_not_fully_set_up will complain about
// and also special case ones
$forced = array(
'username',
'email',
'firstname',
'lastname',
'auth',
'wwwroot',
'session.gc_lifetime',
'_mnet_userpicture_timemodified',
'_mnet_userpicture_mimetype',
);
// these are the ones we used to send/receive (pre 2.0)
$legacy = array(
'username',
'email',
'auth',
'deleted',
'firstname',
'lastname',
'city',
'country',
'lang',
'timezone',
'description',
'mailformat',
'maildigest',
'maildisplay',
'htmleditor',
'wwwroot',
'picture',
);
// get a random user record from the database to pull the fields off
$randomuser = $DB->get_record('user', array(), '*', IGNORE_MULTIPLE);
foreach ($randomuser as $key => $discard) {
if (in_array($key, $excludes) || in_array($key, $forced)) {
continue;
}
$fields[$key] = $key;
}
$info = array(
'forced' => $forced,
'optional' => $fields,
'legacy' => $legacy,
);
return $info;
}
/**
* Returns information about MNet peers
*
* @param bool $withdeleted should the deleted peers be returned too
* @return array
*/
function mnet_get_hosts($withdeleted = false) {
global $CFG, $DB;
$sql = "SELECT h.id, h.deleted, h.wwwroot, h.ip_address, h.name, h.public_key, h.public_key_expires,
h.transport, h.portno, h.last_connect_time, h.last_log_id, h.applicationid,
a.name as app_name, a.display_name as app_display_name, a.xmlrpc_server_url
FROM {mnet_host} h
JOIN {mnet_application} a ON h.applicationid = a.id
WHERE h.id <> ?";
if (!$withdeleted) {
$sql .= " AND h.deleted = 0";
}
$sql .= " ORDER BY h.deleted, h.name, h.id";
return $DB->get_records_sql($sql, array($CFG->mnet_localhost_id));
}
/**
* return an array information about services enabled for the given peer.
* in two modes, fulldata or very basic data.
*
* @param mnet_peer $mnet_peer the peer to get information abut
* @param boolean $fulldata whether to just return which services are published/subscribed, or more information (defaults to full)
*
* @return array If $fulldata is false, an array is returned like:
* publish => array(
* serviceid => boolean,
* serviceid => boolean,
* ),
* subscribe => array(
* serviceid => boolean,
* serviceid => boolean,
* )
* If $fulldata is true, an array is returned like:
* servicename => array(
* apiversion => array(
* name => string
* offer => boolean
* apiversion => int
* plugintype => string
* pluginname => string
* hostsubscribes => boolean
* hostpublishes => boolean
* ),
* )
*/
function mnet_get_service_info(mnet_peer $mnet_peer, $fulldata=true) {
global $CFG, $DB;
$requestkey = (!empty($fulldata) ? 'fulldata' : 'mydata');
static $cache = array();
if (array_key_exists($mnet_peer->id, $cache)) {
return $cache[$mnet_peer->id][$requestkey];
}
$id_list = $mnet_peer->id;
if (!empty($CFG->mnet_all_hosts_id)) {
$id_list .= ', '.$CFG->mnet_all_hosts_id;
}
$concat = $DB->sql_concat('COALESCE(h2s.id,0) ', ' \'-\' ', ' svc.id', '\'-\'', 'r.plugintype', '\'-\'', 'r.pluginname');
$query = "
SELECT DISTINCT
$concat as id,
svc.id as serviceid,
svc.name,
svc.offer,
svc.apiversion,
r.plugintype,
r.pluginname,
h2s.hostid,
h2s.publish,
h2s.subscribe
FROM
{mnet_service2rpc} s2r,
{mnet_rpc} r,
{mnet_service} svc
LEFT JOIN
{mnet_host2service} h2s
ON
h2s.hostid in ($id_list) AND
h2s.serviceid = svc.id
WHERE
svc.offer = '1' AND
s2r.serviceid = svc.id AND
s2r.rpcid = r.id
ORDER BY
svc.name ASC";
$resultset = $DB->get_records_sql($query);
if (is_array($resultset)) {
$resultset = array_values($resultset);
} else {
$resultset = array();
}
require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
$remoteservices = array();
if ($mnet_peer->id != $CFG->mnet_all_hosts_id) {
// Create a new request object
$mnet_request = new mnet_xmlrpc_client();
// Tell it the path to the method that we want to execute
$mnet_request->set_method('system/listServices');
$mnet_request->send($mnet_peer);
if (is_array($mnet_request->response)) {
foreach($mnet_request->response as $service) {
$remoteservices[$service['name']][$service['apiversion']] = $service;
}
}
}
$myservices = array();
$mydata = array();
foreach($resultset as $result) {
$result->hostpublishes = false;
$result->hostsubscribes = false;
if (isset($remoteservices[$result->name][$result->apiversion])) {
if ($remoteservices[$result->name][$result->apiversion]['publish'] == 1) {
$result->hostpublishes = true;
}
if ($remoteservices[$result->name][$result->apiversion]['subscribe'] == 1) {
$result->hostsubscribes = true;
}
}
if (empty($myservices[$result->name][$result->apiversion])) {
$myservices[$result->name][$result->apiversion] = array('serviceid' => $result->serviceid,
'name' => $result->name,
'offer' => $result->offer,
'apiversion' => $result->apiversion,
'plugintype' => $result->plugintype,
'pluginname' => $result->pluginname,
'hostsubscribes' => $result->hostsubscribes,
'hostpublishes' => $result->hostpublishes
);
}
// allhosts_publish allows us to tell the admin that even though he
// is disabling a service, it's still available to the host because
// he's also publishing it to 'all hosts'
if ($result->hostid == $CFG->mnet_all_hosts_id && $CFG->mnet_all_hosts_id != $mnet_peer->id) {
$myservices[$result->name][$result->apiversion]['allhosts_publish'] = $result->publish;
$myservices[$result->name][$result->apiversion]['allhosts_subscribe'] = $result->subscribe;
} elseif (!empty($result->hostid)) {
$myservices[$result->name][$result->apiversion]['I_publish'] = $result->publish;
$myservices[$result->name][$result->apiversion]['I_subscribe'] = $result->subscribe;
}
$mydata['publish'][$result->serviceid] = $result->publish;
$mydata['subscribe'][$result->serviceid] = $result->subscribe;
}
$cache[$mnet_peer->id]['fulldata'] = $myservices;
$cache[$mnet_peer->id]['mydata'] = $mydata;
return $cache[$mnet_peer->id][$requestkey];
}
/**
* return an array of the profile fields to send
* with user information to the given mnet host.
*
* @param mnet_peer $peer the peer to send the information to
*
* @return array (like 'username', 'firstname', etc)
*/
function mnet_fields_to_send(mnet_peer $peer) {
return _mnet_field_helper($peer, 'export');
}
/**
* return an array of the profile fields to import
* from the given host, when creating/updating user accounts
*
* @param mnet_peer $peer the peer we're getting the information from
*
* @return array (like 'username', 'firstname', etc)
*/
function mnet_fields_to_import(mnet_peer $peer) {
return _mnet_field_helper($peer, 'import');
}
/**
* helper for {@see mnet_fields_to_import} and {@mnet_fields_to_send}
*
* @access private
*
* @param mnet_peer $peer the peer object
* @param string $key 'import' or 'export'
*
* @return array (like 'username', 'firstname', etc)
*/
function _mnet_field_helper(mnet_peer $peer, $key) {
$tmp = mnet_profile_field_options();
$defaults = explode(',', get_config('moodle', 'mnetprofile' . $key . 'fields'));
if ('1' === get_config('mnet', 'host' . $peer->id . $key . 'default')) {
return array_merge($tmp['forced'], $defaults);
}
$hostsettings = get_config('mnet', 'host' . $peer->id . $key . 'fields');
if (false === $hostsettings) {
return array_merge($tmp['forced'], $defaults);
}
return array_merge($tmp['forced'], explode(',', $hostsettings));
}
/**
* given a user object (or array) and a list of allowed fields,
* strip out all the fields that should not be included.
* This can be used both for outgoing data and incoming data.
*
* @param mixed $user array or object representing a database record
* @param array $fields an array of allowed fields (usually from mnet_fields_to_{send,import}
*
* @return mixed array or object, depending what type of $user object was passed (datatype is respected)
*/
function mnet_strip_user($user, $fields) {
if (is_object($user)) {
$user = (array)$user;
$wasobject = true; // so we can cast back before we return
}
foreach ($user as $key => $value) {
if (!in_array($key, $fields)) {
unset($user[$key]);
}
}
if (!empty($wasobject)) {
$user = (object)$user;
}
return $user;
}
+332
View File
@@ -0,0 +1,332 @@
<?php
/**
* An object to represent lots of information about an RPC-peer machine
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
require_once($CFG->libdir . '/filelib.php'); // download_file_content() used here
class mnet_peer {
/** No SSL verification. */
const SSL_NONE = 0;
/** SSL verification for host. */
const SSL_HOST = 1;
/** SSL verification for host and peer. */
const SSL_HOST_AND_PEER = 2;
var $id = 0;
var $wwwroot = '';
var $ip_address = '';
var $name = '';
var $public_key = '';
var $public_key_expires = 0;
var $last_connect_time = 0;
var $last_log_id = 0;
var $force_theme = 0;
var $theme = '';
var $applicationid = 1; // Default of 1 == Moodle
var $keypair = array();
var $error = array();
var $bootstrapped = false; // set when the object is populated
/** @var int $sslverification The level of SSL verification to apply. */
public $sslverification = self::SSL_HOST_AND_PEER;
/** @var int deleted status. */
public $deleted;
/** @var stdClass data from mnet_application table in DB. */
public $application;
/**
* Current SSL public key
*
* MNet need to compare the remote machine's SSL Cert and the public key to warn users of any mismatch.
* The property is the remote machine's SSL Cert.
*
* @see admin/mnet/peers.php
* @var string
*/
public $currentkey;
/*
* Fetch information about a peer identified by wwwroot
* If information does not preexist in db, collect it together based on
* supplied information
*
* @param string $wwwroot - address of peer whose details we want
* @param string $pubkey - to use if we add a record to db for new peer
* @param int $application - table id - what kind of peer are we talking to
* @return bool - indication of success or failure
*/
function bootstrap($wwwroot, $pubkey, $application) {
global $DB;
if (substr($wwwroot, -1, 1) == '/') {
$wwwroot = substr($wwwroot, 0, -1);
}
// If a peer record already exists for this address,
// load that info and return
if ($this->set_wwwroot($wwwroot)) {
return true;
}
$hostname = mnet_get_hostname_from_uri($wwwroot);
// Get the IP address for that host - if this fails, it will return the hostname string
$ip_address = gethostbyname($hostname);
// Couldn't find the IP address?
if ($ip_address === $hostname && !preg_match('/^\d+\.\d+\.\d+.\d+$/',$hostname)) {
throw new moodle_exception('noaddressforhost', 'mnet', '', $hostname);
}
$this->name = $wwwroot;
// TODO: In reality, this will be prohibitively slow... need another
// default - maybe blank string
$homepage = download_file_content($wwwroot);
if (!empty($homepage)) {
$count = preg_match("@<title>(.*)</title>@siU", $homepage, $matches);
if ($count > 0) {
$this->name = $matches[1];
}
}
$this->wwwroot = $wwwroot;
$this->ip_address = $ip_address;
$this->deleted = 0;
$this->application = $DB->get_record('mnet_application', array('name'=>$application));
if (empty($this->application)) {
$this->application = $DB->get_record('mnet_application', array('name'=>'moodle'));
}
$this->applicationid = $this->application->id;
if(empty($pubkey)) {
$this->public_key = clean_param(mnet_get_public_key($this->wwwroot, $this->application), PARAM_PEM);
} else {
$this->public_key = clean_param($pubkey, PARAM_PEM);
}
$this->public_key_expires = $this->check_common_name($this->public_key);
$this->last_connect_time = 0;
$this->last_log_id = 0;
if ($this->public_key_expires == false) {
$this->public_key == '';
return false;
}
$this->bootstrapped = true;
return true;
}
/*
* Delete mnet peer
* the peer is marked as deleted in the database
* we delete current sessions.
* @return bool - success
*/
function delete() {
global $DB;
if ($this->deleted) {
return true;
}
$this->delete_all_sessions();
$this->deleted = 1;
return $this->commit();
}
function count_live_sessions() {
global $DB;
$obj = $this->delete_expired_sessions();
return $DB->count_records('mnet_session', array('mnethostid'=>$this->id));
}
function delete_expired_sessions() {
global $DB;
$now = time();
return $DB->delete_records_select('mnet_session', " mnethostid = ? AND expires < ? ", array($this->id, $now));
}
function delete_all_sessions() {
global $CFG, $DB;
// TODO: Expires each PHP session individually
$sessions = $DB->get_records('mnet_session', array('mnethostid'=>$this->id));
if (count($sessions) > 0 && file_exists($CFG->dirroot.'/auth/mnet/auth.php')) {
require_once($CFG->dirroot.'/auth/mnet/auth.php');
$auth = new auth_plugin_mnet();
$auth->end_local_sessions($sessions);
}
$deletereturn = $DB->delete_records('mnet_session', array('mnethostid'=>$this->id));
return true;
}
function check_common_name($key) {
$credentials = $this->check_credentials($key);
return $credentials['validTo_time_t'];
}
function check_credentials($key) {
$credentials = openssl_x509_parse($key);
if ($credentials == false) {
$this->error[] = array('code' => 3, 'text' => get_string("nonmatchingcert", 'mnet', array('subject' => '','host' => '')));
return false;
} elseif (array_key_exists('subjectAltName', $credentials['subject']) && $credentials['subject']['subjectAltName'] != $this->wwwroot) {
$a['subject'] = $credentials['subject']['subjectAltName'];
$a['host'] = $this->wwwroot;
$this->error[] = array('code' => 5, 'text' => get_string("nonmatchingcert", 'mnet', $a));
return false;
} else if ($credentials['subject']['CN'] !== substr($this->wwwroot, 0, 64)) {
$a['subject'] = $credentials['subject']['CN'];
$a['host'] = $this->wwwroot;
$this->error[] = array('code' => 4, 'text' => get_string("nonmatchingcert", 'mnet', $a));
return false;
} else {
if (array_key_exists('subjectAltName', $credentials['subject'])) {
$credentials['wwwroot'] = $credentials['subject']['subjectAltName'];
} else {
$credentials['wwwroot'] = $credentials['subject']['CN'];
}
return $credentials;
}
}
function commit() {
global $DB;
$obj = new stdClass();
$obj->wwwroot = $this->wwwroot;
$obj->ip_address = $this->ip_address;
$obj->name = $this->name;
$obj->public_key = $this->public_key;
$obj->public_key_expires = $this->public_key_expires;
$obj->deleted = $this->deleted;
$obj->last_connect_time = $this->last_connect_time;
$obj->last_log_id = $this->last_log_id;
$obj->force_theme = $this->force_theme;
$obj->theme = $this->theme;
$obj->applicationid = $this->applicationid;
$obj->sslverification = $this->sslverification;
if (isset($this->id) && $this->id > 0) {
$obj->id = $this->id;
return $DB->update_record('mnet_host', $obj);
} else {
$this->id = $DB->insert_record('mnet_host', $obj);
return $this->id > 0;
}
}
function touch() {
$this->last_connect_time = time();
$this->commit();
}
function set_name($newname) {
if (is_string($newname) && strlen($newname <= 80)) {
$this->name = $newname;
return true;
}
return false;
}
function set_applicationid($applicationid) {
if (is_numeric($applicationid) && $applicationid == intval($applicationid)) {
$this->applicationid = $applicationid;
return true;
}
return false;
}
/**
* Load information from db about an mnet peer into this object's properties
*
* @param string $wwwroot - address of peer whose details we want to load
* @return bool - indication of success or failure
*/
function set_wwwroot($wwwroot) {
global $CFG, $DB;
$hostinfo = $DB->get_record('mnet_host', array('wwwroot'=>$wwwroot));
if ($hostinfo != false) {
$this->populate($hostinfo);
return true;
}
return false;
}
function set_id($id) {
global $CFG, $DB;
if (clean_param($id, PARAM_INT) != $id) {
$this->error[] = ['code' => 1, 'text' => 'Your id ('.$id.') is not legal'];
return false;
}
$sql = "
SELECT
h.*
FROM
{mnet_host} h
WHERE
h.id = ?";
if ($hostinfo = $DB->get_record_sql($sql, array($id))) {
$this->populate($hostinfo);
return true;
}
return false;
}
/**
* Several methods can be used to get an 'mnet_host' record. They all then
* send it to this private method to populate this object's attributes.
*
* @param object $hostinfo A database record from the mnet_host table
* @return void
*/
function populate($hostinfo) {
global $DB;
$this->id = $hostinfo->id;
$this->wwwroot = $hostinfo->wwwroot;
$this->ip_address = $hostinfo->ip_address;
$this->name = $hostinfo->name;
$this->deleted = $hostinfo->deleted;
$this->public_key = $hostinfo->public_key;
$this->public_key_expires = $hostinfo->public_key_expires;
$this->last_connect_time = $hostinfo->last_connect_time;
$this->last_log_id = $hostinfo->last_log_id;
$this->force_theme = $hostinfo->force_theme;
$this->theme = $hostinfo->theme;
$this->applicationid = $hostinfo->applicationid;
$this->sslverification = $hostinfo->sslverification;
$this->application = $DB->get_record('mnet_application', array('id'=>$this->applicationid));
$this->bootstrapped = true;
}
/**
* Get public key.
*
* @deprecated since Moodle 4.3
* @todo MDL-78304 Final deprecation.
*/
function get_public_key() {
debugging('Function get_public_key() is deprecated.', DEBUG_DEVELOPER);
if (isset($this->public_key_ref)) return $this->public_key_ref;
$this->public_key_ref = openssl_pkey_get_public($this->public_key);
return $this->public_key_ref;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* Print this server's public key and exit
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
require_once(__DIR__ . '/../config.php');
require_once $CFG->dirroot.'/mnet/lib.php';
if ($CFG->mnet_dispatcher_mode === 'off') {
throw new \moodle_exception('mnetdisabled', 'mnet');
}
header("Content-type: text/plain; charset=utf-8");
$keypair = mnet_get_keypair();
echo $keypair['certificate'];
+95
View File
@@ -0,0 +1,95 @@
<?php
/**
* An object to represent lots of information about an RPC-peer machine
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
class mnet_remote_client extends mnet_peer {
// If the remote client is trying to execute a method on an object instead
// of just a function, we'll instantiate the proper class and store it in
// this 'object_to_call' property, or 'static_location' if it wants to be called statically
var $object_to_call = false;
var $static_location = false;
var $request_was_encrypted = false;
var $request_was_signed = false;
var $signatureok = false; // True if we have successfully verified that the request was signed by an established peer
var $pushkey = false; // True if we need to tell the remote peer about our current public key
var $useprivatekey = ''; // The private key we should use to sign pushkey response
function was_encrypted() {
$this->request_was_encrypted = true;
}
/* Record private key to use in pushkey response
* Called when we have decrypted a request using an old (but still acceptable) keypair
* @param $keyresource the private key we should use to sign the response.
*/
function encrypted_to($keyresource) {
$this->useprivatekey = $keyresource;
}
function set_pushkey() {
$this->pushkey = true;
}
function was_signed() {
$this->request_was_signed = true;
}
function signature_verified() {
$this->signatureok = true;
}
function object_to_call($object) {
$this->object_to_call = $object;
}
function static_location($location) {
$this->static_location = $location;
}
function plaintext_is_ok() {
global $CFG;
$trusted_hosts = explode(',', get_config('mnet', 'mnet_trusted_hosts'));
foreach($trusted_hosts as $host) {
if (address_in_subnet(getremoteaddr(), $host)) {
return true;
}
}
return false;
}
function refresh_key() {
mnet_debug("remote client refreshing key");
global $CFG;
// set up an RPC request
require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
$mnetrequest = new mnet_xmlrpc_client();
// Use any method - listServices is pretty lightweight.
$mnetrequest->set_method('system/listServices');
// Do RPC call and store response
if ($mnetrequest->send($this) === true) {
mnet_debug("refresh key request complete");
// Ok - we actually don't care about the result
$temp = new mnet_peer();
$temp->set_id($this->id);
if($this->public_key != $temp->public_key) {
$newkey = clean_param($temp->public_key, PARAM_PEM);
if(!empty($newkey)) {
$this->public_key = $newkey;
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,221 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for mnetservice_enrol.
*
* @package mnetservice_enrol
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mnetservice_enrol\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\context;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
/**
* Privacy Subsystem for mnetservice_enrol implementing metadata and plugin providers.
*
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table(
'mnetservice_enrol_enrolments',
[
'hostid' => 'privacy:metadata:mnetservice_enrol_enrolments:hostid',
'userid' => 'privacy:metadata:mnetservice_enrol_enrolments:userid',
'remotecourseid' => 'privacy:metadata:mnetservice_enrol_enrolments:remotecourseid',
'rolename' => 'privacy:metadata:mnetservice_enrol_enrolments:rolename',
'enroltime' => 'privacy:metadata:mnetservice_enrol_enrolments:enroltime',
'enroltype' => 'privacy:metadata:mnetservice_enrol_enrolments:enroltype'
],
'privacy:metadata:mnetservice_enrol_enrolments:tableexplanation'
);
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$sql = "SELECT c.id
FROM {context} c
JOIN {mnetservice_enrol_enrolments} me
ON me.userid = c.instanceid
AND c.contextlevel = :contextlevel
WHERE me.userid = :userid";
$params = [
'contextlevel' => CONTEXT_USER,
'userid' => $userid
];
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!$context instanceof \context_user) {
return;
}
$params = ['userid' => $context->instanceid];
$sql = "SELECT userid
FROM {mnetservice_enrol_enrolments}
WHERE userid = :userid";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
if (empty($contextlist->count())) {
return;
}
$userid = $contextlist->get_user()->id;
$contextuser = \context_user::instance($userid);
list($insql, $inparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$params = [
'userid' => $userid,
'contextlevel' => CONTEXT_USER
];
$params += $inparams;
$sql = "SELECT me.id,
me.rolename,
me.enroltime,
me.enroltype,
mh.name as hostname,
mc.fullname
FROM {mnetservice_enrol_enrolments} me
JOIN {context} ctx
ON ctx.instanceid = me.userid
AND ctx.contextlevel = :contextlevel
JOIN {mnet_host} mh
ON mh.id = me.hostid
JOIN {mnetservice_enrol_courses} mc
ON mc.remoteid = me.remotecourseid
WHERE me.userid = :userid
AND ctx.id {$insql}";
$mnetenrolments = $DB->get_records_sql($sql, $params);
foreach ($mnetenrolments as $mnetenrolment) {
// The core_enrol data export is organised in:
// {User Context}/User enrolments/data.json.
$data[] = (object) [
'host' => $mnetenrolment->hostname,
'remotecourseid' => $mnetenrolment->fullname,
'rolename' => $mnetenrolment->rolename,
'enroltime' => transform::datetime($mnetenrolment->enroltime),
'enroltype' => $mnetenrolment->enroltype
];
}
writer::with_context($contextuser)->export_data(
[get_string('privacy:metadata:mnetservice_enrol_enrolments', 'mnetservice_enrol')],
(object)$data
);
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// Sanity check that context is at the User context level.
if ($context->contextlevel == CONTEXT_USER) {
static::delete_user_data($context->instanceid);
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
$context = $userlist->get_context();
if ($context instanceof \context_user) {
static::delete_user_data($context->instanceid);
}
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
if (empty($contextlist->count())) {
return;
}
$user = $contextlist->get_user();
foreach ($contextlist->get_contexts() as $context) {
// Verify the context is a user context and that the instanceid matches the userid of the contextlist.
if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) {
// Get the data and write it.
static::delete_user_data($user->id);
}
}
}
/**
* This does the deletion of user data for the mnetservice_enrolments.
*
* @param int $userid The user ID
*/
protected static function delete_user_data(int $userid) {
global $DB;
// Because we only use user contexts the instance ID is the user ID.
$DB->delete_records('mnetservice_enrol_enrolments', ['userid' => $userid]);
}
}
+200
View File
@@ -0,0 +1,200 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Page to enrol our users into remote courses
*
* @package plugintype
* @subpackage pluginname
* @copyright 2010 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require(__DIR__.'/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->dirroot.'/mnet/service/enrol/locallib.php');
require_sesskey();
$hostid = required_param('host', PARAM_INT); // remote host id in our mnet_host table
$courseid = required_param('course', PARAM_INT); // id of the course in our cache table
$usecache = optional_param('usecache', true, PARAM_BOOL); // use cached list of enrolments
admin_externalpage_setup('mnetenrol', '', array('host'=>$hostid, 'course'=>$courseid, 'usecache'=>1, 'sesskey'=>sesskey()),
new moodle_url('/mnet/service/enrol/course.php'));
$service = mnetservice_enrol::get_instance();
if (!$service->is_available()) {
echo $OUTPUT->box(get_string('mnetdisabled','mnet'), 'noticebox');
echo $OUTPUT->footer();
die();
}
// remote hosts that may publish remote enrolment service and we are subscribed to it
$hosts = $service->get_remote_publishers();
if (empty($hosts[$hostid])) {
throw new \moodle_exception('wearenotsubscribedtothishost', 'mnetservice_enrol');
}
$host = $hosts[$hostid];
$course = $DB->get_record('mnetservice_enrol_courses', array('id'=>$courseid, 'hostid'=>$host->id), '*', MUST_EXIST);
echo $OUTPUT->header();
// course name
$icon = $OUTPUT->pix_icon('i/course', get_string('category'));
echo $OUTPUT->heading($icon . s($course->fullname));
// collapsible course summary
if (!empty($course->summary)) {
$options = new stdClass();
$options->trusted = false;
$options->para = false;
$options->filter = false;
$options->noclean = false;
$options->overflowdiv = true;
print_collapsible_region_start('remotecourse summary', 'remotecourse-summary', get_string('coursesummary'), false, true);
echo format_text($course->summary, $course->summaryformat, $options);
print_collapsible_region_end();
}
$error = '';
$lastfetchenrolments = get_config('mnetservice_enrol', 'lastfetchenrolments');
if (!$usecache or empty($lastfetchenrolments) or (time()-$lastfetchenrolments > 600)) {
// fetch fresh data from remote if we just came from the course selection screen
// or every 10 minutes
$usecache = false;
$result = $service->req_course_enrolments($host->id, $course->remoteid, $usecache);
if ($result !== true) {
$error .= $service->format_error_message($result);
}
}
// user selectors
$currentuserselector = new mnetservice_enrol_existing_users_selector('removeselect', array('hostid'=>$host->id, 'remotecourseid'=>$course->remoteid));
$potentialuserselector = new mnetservice_enrol_potential_users_selector('addselect', array('hostid'=>$host->id, 'remotecourseid'=>$course->remoteid));
// process incoming enrol request
if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
$userstoassign = $potentialuserselector->get_selected_users();
if (!empty($userstoassign)) {
foreach($userstoassign as $adduser) {
$user = $DB->get_record('user', array('id'=>$adduser->id));
$result = $service->req_enrol_user($user, $course);
if ($result !== true) {
$error .= $service->format_error_message($result);
}
}
$potentialuserselector->invalidate_selected_users();
$currentuserselector->invalidate_selected_users();
}
}
// process incoming unenrol request
if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
$userstounassign = $currentuserselector->get_selected_users();
if (!empty($userstounassign)) {
foreach($userstounassign as $removeuser) {
$user = $DB->get_record('user', array('id'=>$removeuser->id));
$result = $service->req_unenrol_user($user, $course);
if ($result !== true) {
$error .= $service->format_error_message($result);
}
}
$potentialuserselector->invalidate_selected_users();
$currentuserselector->invalidate_selected_users();
}
}
if (!empty($error)) {
echo $OUTPUT->box($error, 'generalbox error');
}
// print form to enrol our students
?>
<form id="assignform" method="post" action="<?php echo $PAGE->url ?>">
<div>
<input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
<input type="hidden" name="hostid" value="<?php echo $host->id ?>" />
<input type="hidden" name="courseid" value="<?php echo $course->id ?>" />
<table summary="" class="roleassigntable generaltable generalbox boxaligncenter" cellspacing="0">
<tr>
<td id="existingcell">
<p><label for="removeselect"><?php print_string('enrolledusers', 'enrol'); ?></label></p>
<?php $currentuserselector->display() ?>
</td>
<td id="buttonscell">
<div id="addcontrols">
<input name="add" id="add" type="submit" value="<?php echo $OUTPUT->larrow().'&nbsp;'.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br />
<div class="enroloptions">
<p><?php echo get_string('assignrole', 'role') .': '. s($course->rolename); ?></p>
</div>
</div>
<div id="removecontrols">
<input name="remove" id="remove" type="submit" value="<?php echo get_string('remove').'&nbsp;'.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
</div>
</td>
<td id="potentialcell">
<p><label for="addselect"><?php print_string('enrolcandidates', 'enrol'); ?></label></p>
<?php $potentialuserselector->display() ?>
</td>
</tr>
</table>
</div>
</form>
<?php
// eventually display other enrolments of our users (manual, self etc.) in the remote course
list($sort, $params) = users_order_by_sql('u');
$sql = "SELECT e.id,e.enroltype AS plugin, u.firstname, u.lastname, u.email, u.id AS userid,
e.enroltime AS timemodified, e.rolename
FROM {mnetservice_enrol_enrolments} e
JOIN {user} u ON u.id = e.userid
WHERE e.hostid = :hostid AND e.remotecourseid = :remotecourseid AND e.enroltype != 'mnet'
ORDER BY $sort";
$params['hostid'] = $host->id;
$params['remotecourseid'] = $course->remoteid;
if ($enrolments = $DB->get_records_sql($sql, $params)) {
echo $OUTPUT->heading(get_string('otherenrolledusers', 'mnetservice_enrol'), 3);
$table = new html_table();
$table->attributes['class'] = 'generaltable otherenrolledusers';
$table->head = array(get_string('fullnameuser'), get_string('role'), get_string('plugin'));
foreach ($enrolments as $enrolleduser) {
$table->data[] = array(fullname($enrolleduser), s($enrolleduser->rolename), s($enrolleduser->plugin));
}
echo html_writer::table($table);
}
if ($usecache) {
echo $OUTPUT->single_button(new moodle_url($PAGE->url, array('usecache'=>0, 'sesskey'=>sesskey())),
get_string('refetch', 'mnetservice_enrol'), 'get');
}
echo $OUTPUT->single_button(new moodle_url('/mnet/service/enrol/host.php', array('id'=>$host->id)),
get_string('availablecourseson', 'mnetservice_enrol', s($host->hostname)), 'get');
echo $OUTPUT->footer();
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mnet/service/enrol/db" VERSION="20120122" COMMENT="XMLDB file for MNet service plugin mnet/service/enrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="mnetservice_enrol_courses" COMMENT="Caches the information fetched via XML-RPC about courses on remote hosts that are offered for our users">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Unique remote-course ID"/>
<FIELD NAME="hostid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the remote MNet host"/>
<FIELD NAME="remoteid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of course on its home server"/>
<FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the category on the remote server"/>
<FIELD NAME="categoryname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="fullname" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="shortname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="idnumber" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="summary" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="summaryformat" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Format of the summary field"/>
<FIELD NAME="startdate" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the role at the remote server that our users will get when we enrol them there"/>
<FIELD NAME="rolename" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The name of the role at the remote server that our users will get when we enrol them there"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the mnet_course table"/>
</KEYS>
<INDEXES>
<INDEX NAME="uq_hostid_remoteid" UNIQUE="true" FIELDS="hostid, remoteid" COMMENT="The id of the course on its host must be unique"/>
</INDEXES>
</TABLE>
<TABLE NAME="mnetservice_enrol_enrolments" COMMENT="Caches the information about enrolments of our local users in courses on remote hosts">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Unique enrollment ID"/>
<FIELD NAME="hostid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the remote MNet host"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of our local user on this server"/>
<FIELD NAME="remotecourseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the course at the remote server. Note that this may and may not be cached in our mnetservice_enrol_courses table, depends of whether the course is opened for remote enrolments or our student is the enrolled there via other plugin"/>
<FIELD NAME="rolename" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="enroltime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="enroltype" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false" COMMENT="The name of the enrol plugin at the remote server that was used to enrol our student into their course"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key"/>
<KEY NAME="fk_user" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="fk_mnet_host" TYPE="foreign" FIELDS="hostid" REFTABLE="mnet_host" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+119
View File
@@ -0,0 +1,119 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Displays a list of remote courses offered by a given host for our students
*
* By default the courses information is cached in our local DB table. Parameter
* $usecache can be used to force re-fetching up to date state from remote
* hosts (session key required in such case).
*
* @package mnetservice
* @subpackage enrol
* @copyright 2010 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require(__DIR__.'/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->dirroot.'/mnet/service/enrol/locallib.php');
$hostid = required_param('id', PARAM_INT); // remote host id
$usecache = optional_param('usecache', true, PARAM_BOOL); // use cached list of courses
admin_externalpage_setup('mnetenrol', '', array('id'=>$hostid, 'usecache'=>1),
new moodle_url('/mnet/service/enrol/host.php'));
$service = mnetservice_enrol::get_instance();
if (!$service->is_available()) {
echo $OUTPUT->box(get_string('mnetdisabled','mnet'), 'noticebox');
echo $OUTPUT->footer();
die();
}
// remote hosts that may publish remote enrolment service and we are subscribed to it
$hosts = $service->get_remote_publishers();
if (empty($hosts[$hostid])) {
throw new \moodle_exception('wearenotsubscribedtothishost', 'mnetservice_enrol');
}
$host = $hosts[$hostid];
if (!$usecache) {
// our local database will be changed
require_sesskey();
}
$courses = $service->get_remote_courses($host->id, $usecache);
if (is_string($courses)) {
throw new \moodle_exception('fetchingcourses', 'mnetservice_enrol', '', null, $service->format_error_message($courses));
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('availablecourseson', 'mnetservice_enrol', s($host->hostname)));
if (empty($courses)) {
$a = (object)array('hostname' => s($host->hostname), 'hosturl' => s($host->hosturl));
echo $OUTPUT->box(get_string('availablecoursesonnone','mnetservice_enrol', $a), 'noticebox');
if ($usecache) {
echo $OUTPUT->single_button(new moodle_url($PAGE->url, array('usecache'=>0, 'sesskey'=>sesskey())),
get_string('refetch', 'mnetservice_enrol'), 'get');
}
echo $OUTPUT->footer();
die();
}
$table = new html_table();
$table->head = array(
get_string('shortnamecourse'),
get_string('fullnamecourse'),
get_string('role'),
get_string('action')
);
$table->attributes['class'] = 'generaltable remotecourses';
$icon = $OUTPUT->pix_icon('i/course', get_string('category'));
$prevcat = null;
foreach ($courses as $course) {
$course = (object)$course;
if ($prevcat !== $course->categoryid) {
$row = new html_table_row();
$cell = new html_table_cell($icon . s($course->categoryname));
$cell->header = true;
$cell->attributes['class'] = 'categoryname';
$cell->colspan = 4;
$row->cells = array($cell);
$table->data[] = $row;
$prevcat = $course->categoryid;
}
$editbtn = $OUTPUT->single_button(new moodle_url('/mnet/service/enrol/course.php',
array('host'=>$host->id, 'course'=>$course->id, 'usecache'=>0, 'sesskey'=>sesskey())),
get_string('editenrolments', 'mnetservice_enrol'), 'get');
$row = new html_table_row();
$row->cells = array(
s($course->shortname),
s($course->fullname),
s($course->rolename),
$editbtn
);
$table->data[] = $row;
}
echo html_writer::table($table);
if ($usecache) {
echo $OUTPUT->single_button(new moodle_url($PAGE->url, array('usecache'=>0, 'sesskey'=>sesskey())),
get_string('refetch', 'mnetservice_enrol'), 'get');
}
echo $OUTPUT->footer();
+76
View File
@@ -0,0 +1,76 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Displays the list of remote peers we can enrol our users to
*
* @package mnetservice
* @subpackage enrol
* @copyright 2010 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require(__DIR__.'/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->dirroot.'/mnet/service/enrol/locallib.php');
admin_externalpage_setup('mnetenrol');
$service = mnetservice_enrol::get_instance();
echo $OUTPUT->header();
echo $OUTPUT->heading_with_help(get_string('clientname', 'mnetservice_enrol'), 'clientname', 'mnetservice_enrol');
if (!$service->is_available()) {
echo $OUTPUT->box(get_string('mnetdisabled','mnet'), 'noticebox');
echo $OUTPUT->footer();
die();
}
$roamingusers = get_users_by_capability(context_system::instance(), 'moodle/site:mnetlogintoremote', 'u.id');
if (empty($roamingusers)) {
$capname = get_string('site:mnetlogintoremote', 'role');
$url = new moodle_url('/admin/roles/manage.php');
echo notice(get_string('noroamingusers', 'mnetservice_enrol', $capname), $url);
}
unset($roamingusers);
// remote hosts that may publish remote enrolment service and we are subscribed to it
$hosts = $service->get_remote_publishers();
if (empty($hosts)) {
echo $OUTPUT->box(get_string('nopublishers', 'mnetservice_enrol'), 'noticebox');
echo $OUTPUT->footer();
die();
}
$table = new html_table();
$table->attributes['class'] = 'generaltable remotehosts';
$table->head = array(
get_string('hostappname', 'mnetservice_enrol'),
get_string('hostname', 'mnetservice_enrol'),
get_string('hosturl', 'mnetservice_enrol'),
get_string('action')
);
foreach ($hosts as $host) {
$hostlink = html_writer::link(new moodle_url($host->hosturl), s($host->hosturl));
$editbtn = $OUTPUT->single_button(new moodle_url('/mnet/service/enrol/host.php', array('id'=>$host->id)),
get_string('editenrolments', 'mnetservice_enrol'), 'get');
$table->data[] = array(s($host->appname), s($host->hostname), $hostlink, $editbtn);
}
echo html_writer::table($table);
echo $OUTPUT->footer();
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package mnetservice
* @subpackage enrolment
* @copyright 2010 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['availablecourseson'] = 'Available courses on {$a}';
$string['availablecoursesonnone'] = 'Remote host <a href="{$a->hosturl}">{$a->hostname}</a> does not offer any courses for our users.';
$string['clientname'] = 'Remote enrolments client';
$string['clientname_help'] = 'This tool allows you to enrol and unenrol your local users on remote hosts that allow you to do so via the \'MNet remote enrolments\' plugin.';
$string['editenrolments'] = 'Edit enrolments';
$string['hostappname'] = 'Application';
$string['hostname'] = 'Host name';
$string['hosturl'] = 'Remote host URL';
$string['nopublishers'] = 'No remote peers available.';
$string['noroamingusers'] = 'Users require the capability \'{$a}\' in the system context to be enrolled to remote courses, however there are currently no users with this capability. Click the continue button to assign the required capability to one or more roles on your site.';
$string['otherenrolledusers'] = 'Other enrolled users';
$string['pluginname'] = 'Remote enrolment service';
$string['refetch'] = 'Re-fetch up to date state from remote hosts';
$string['privacy:metadata:mnetservice_enrol_enrolments'] = 'Remote enrolment service';
$string['privacy:metadata:mnetservice_enrol_enrolments:enroltime'] = 'The time when the enrolment was modified';
$string['privacy:metadata:mnetservice_enrol_enrolments:enroltype'] = 'The enrolment type on the remote server used to enrol the user in their course';
$string['privacy:metadata:mnetservice_enrol_enrolments:hostid'] = 'The ID of the remote MNet host';
$string['privacy:metadata:mnetservice_enrol_enrolments:remotecourseid'] = 'The ID of the course on the remote server';
$string['privacy:metadata:mnetservice_enrol_enrolments:rolename'] = 'The name of role on the remote server';
$string['privacy:metadata:mnetservice_enrol_enrolments:tableexplanation'] = 'The Remote enrolment service stores information about enrolments of local users in courses on remote hosts.';
$string['privacy:metadata:mnetservice_enrol_enrolments:userid'] = 'The ID of the local user on this server';
+614
View File
@@ -0,0 +1,614 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides various useful functionality to plugins that offer or use this MNet service
*
* Remote enrolment service is used by enrol_mnet plugin which publishes the server side
* methods. The client side is accessible from the admin tree.
*
* @package mnetservice
* @subpackage enrol
* @copyright 2010 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/user/selector/lib.php');
/**
* Singleton providing various functionality usable by plugin(s) implementing this MNet service
*/
class mnetservice_enrol {
/** @var mnetservice_enrol holds the singleton instance. */
protected static $singleton;
/** @var caches the result of {@link self::get_remote_subscribers()} */
protected $cachesubscribers = null;
/** @var caches the result of {@link self::get_remote_publishers()} */
protected $cachepublishers = null;
/**
* This is singleton, use {@link mnetservice_enrol::get_instance()}
*/
protected function __construct() {
}
/**
* @return mnetservice_enrol singleton instance
*/
public static function get_instance() {
if (is_null(self::$singleton)) {
self::$singleton = new self();
}
return self::$singleton;
}
/**
* Is this service enabled?
*
* Currently, this checks if whole MNet is available. In the future, additional
* checks can be done. Probably the field 'offer' should be checked but it does
* not seem to be used so far.
*
* @todo move this to some parent class once we have such
* @return bool
*/
public function is_available() {
global $CFG;
if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
return false;
}
return true;
}
/**
* Returns a list of remote servers that can enrol their users into our courses
*
* We must publish MNet service 'mnet_enrol' for the peers to allow them to enrol
* their users into our courses.
*
* @todo once the MNet core is refactored this may be part of a parent class
* @todo the name of the service should be changed to the name of this plugin
* @return array
*/
public function get_remote_subscribers() {
global $DB;
if (is_null($this->cachesubscribers)) {
$sql = "SELECT DISTINCT h.id, h.name AS hostname, h.wwwroot AS hosturl,
a.display_name AS appname
FROM {mnet_host} h
JOIN {mnet_host2service} hs ON h.id = hs.hostid
JOIN {mnet_service} s ON hs.serviceid = s.id
JOIN {mnet_application} a ON h.applicationid = a.id
WHERE s.name = 'mnet_enrol'
AND h.deleted = 0
AND hs.publish = 1";
$this->cachesubscribers = $DB->get_records_sql($sql);
}
return $this->cachesubscribers;
}
/**
* Returns a list of remote servers that offer their courses for our users
*
* We must subscribe MNet service 'mnet_enrol' for the peers to allow our users to enrol
* into their courses.
*
* @todo once the MNet core is refactored this may be part of a parent class
* @todo the name of the service should be changed to the name of this plugin
* @return array
*/
public function get_remote_publishers() {
global $DB;
if (is_null($this->cachepublishers)) {
$sql = "SELECT DISTINCT h.id, h.name AS hostname, h.wwwroot AS hosturl,
a.display_name AS appname
FROM {mnet_host} h
JOIN {mnet_host2service} hs ON h.id = hs.hostid
JOIN {mnet_service} s ON hs.serviceid = s.id
JOIN {mnet_application} a ON h.applicationid = a.id
WHERE s.name = 'mnet_enrol'
AND h.deleted = 0
AND hs.subscribe = 1";
$this->cachepublishers = $DB->get_records_sql($sql);
}
return $this->cachepublishers;
}
/**
* Fetches the information about the courses available on remote host for our students
*
* The information about remote courses available for us is cached in {mnetservice_enrol_courses}.
* This method either returns the cached information (typically when displaying the list to
* students) or fetch fresh data via new XML-RPC request (which updates the local cache, too).
* The lifetime of the cache is 1 day, so even if $usecache is set to true, the cache will be
* re-populated if we did not fetch from any server (not only the currently requested one)
* for some time.
*
* @param id $mnethostid MNet remote host id
* @param bool $usecache use cached data or invoke new XML-RPC?
* @uses mnet_xmlrpc_client Invokes XML-RPC request if the cache is not used
* @return array|string returned list or serialized array of mnet error messages
*/
public function get_remote_courses($mnethostid, $usecache=true) {
global $CFG, $DB; // $CFG needed!
$lastfetchcourses = get_config('mnetservice_enrol', 'lastfetchcourses');
if (empty($lastfetchcourses) or (time()-$lastfetchcourses > DAYSECS)) {
$usecache = false;
}
if ($usecache) {
return $DB->get_records('mnetservice_enrol_courses', array('hostid' => $mnethostid), 'sortorder, shortname');
}
// do not use cache - fetch fresh list from remote MNet host
require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
$peer = new mnet_peer();
if (!$peer->set_id($mnethostid)) {
return serialize(array('unknown mnet peer'));
}
$request = new mnet_xmlrpc_client();
$request->set_method('enrol/mnet/enrol.php/available_courses');
if ($request->send($peer)) {
$list = array();
$response = $request->response;
// get the currently cached courses key'd on remote id - only need remoteid and id fields
$cachedcourses = $DB->get_records('mnetservice_enrol_courses', array('hostid' => $mnethostid), 'remoteid', 'remoteid, id');
foreach ($response as &$remote) {
$course = new stdclass(); // record in our local cache
$course->hostid = $mnethostid;
$course->remoteid = (int)$remote['remoteid'];
$course->categoryid = (int)$remote['cat_id'];
$course->categoryname = substr($remote['cat_name'], 0, 255);
$course->sortorder = (int)$remote['sortorder'];
$course->fullname = substr($remote['fullname'], 0, 254);
$course->shortname = substr($remote['shortname'], 0, 100);
$course->idnumber = substr($remote['idnumber'], 0, 100);
$course->summary = $remote['summary'];
$course->summaryformat = empty($remote['summaryformat']) ? FORMAT_MOODLE : (int)$remote['summaryformat'];
$course->startdate = (int)$remote['startdate'];
$course->roleid = (int)$remote['defaultroleid'];
$course->rolename = substr($remote['defaultrolename'], 0, 255);
// We do not cache the following fields returned from peer in 2.0 any more
// not cached: cat_description
// not cached: cat_descriptionformat
// not cached: cost
// not cached: currency
if (empty($cachedcourses[$course->remoteid])) {
$course->id = $DB->insert_record('mnetservice_enrol_courses', $course);
} else {
$course->id = $cachedcourses[$course->remoteid]->id;
$DB->update_record('mnetservice_enrol_courses', $course);
}
$list[$course->remoteid] = $course;
}
// prune stale data from cache
if (!empty($cachedcourses)) {
foreach ($cachedcourses as $cachedcourse) {
if (!empty($list[$cachedcourse->remoteid])) {
unset($cachedcourses[$cachedcourse->remoteid]);
}
}
$staleremoteids = array_keys($cachedcourses);
if (!empty($staleremoteids)) {
list($sql, $params) = $DB->get_in_or_equal($staleremoteids, SQL_PARAMS_NAMED);
$select = "hostid=:hostid AND remoteid $sql";
$params['hostid'] = $mnethostid;
$DB->delete_records_select('mnetservice_enrol_courses', $select, $params);
}
}
// and return the fresh data
set_config('lastfetchcourses', time(), 'mnetservice_enrol');
return $list;
} else {
return serialize($request->error);
}
}
/**
* Updates local cache about enrolments of our users in remote courses
*
* The remote course must allow enrolments via our Remote enrolment service client.
* Because of legacy design of data structure returned by XML-RPC code, only one
* user enrolment per course is returned by 1.9 MNet servers. This may be an issue
* if the user is enrolled multiple times by various enrolment plugins. MNet 2.0
* servers do not use user name as array keys - they do not need to due to side
* effect of MDL-19219.
*
* @param id $mnethostid MNet remote host id
* @param int $remotecourseid ID of the course at the remote host
* @param bool $usecache use cached data or invoke new XML-RPC?
* @uses mnet_xmlrpc_client Invokes XML-RPC request
* @return bool|string true if success or serialized array of mnet error messages
*/
public function req_course_enrolments($mnethostid, $remotecourseid) {
global $CFG, $DB; // $CFG needed!
require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
if (!$DB->record_exists('mnetservice_enrol_courses', array('hostid'=>$mnethostid, 'remoteid'=>$remotecourseid))) {
return serialize(array('course not available for remote enrolments'));
}
$peer = new mnet_peer();
if (!$peer->set_id($mnethostid)) {
return serialize(array('unknown mnet peer'));
}
$request = new mnet_xmlrpc_client();
$request->set_method('enrol/mnet/enrol.php/course_enrolments');
$request->add_param($remotecourseid, 'int');
if ($request->send($peer)) {
$list = array();
$response = $request->response;
// prepare a table mapping usernames of our users to their ids
$usernames = array();
foreach ($response as $unused => $remote) {
if (!isset($remote['username'])) {
// see MDL-19219
return serialize(array('remote host running old version of mnet server - does not return username attribute'));
}
if ($remote['username'] == 'guest') { // we can not use $CFG->siteguest here
// do not try nasty things you bastard!
continue;
}
$usernames[$remote['username']] = $remote['username'];
}
if (!empty($usernames)) {
list($usql, $params) = $DB->get_in_or_equal($usernames, SQL_PARAMS_NAMED);
list($sort, $sortparams) = users_order_by_sql();
$params['mnetlocalhostid'] = $CFG->mnet_localhost_id;
$sql = "SELECT username,id
FROM {user}
WHERE mnethostid = :mnetlocalhostid
AND username $usql
AND deleted = 0
AND confirmed = 1
ORDER BY $sort";
$usersbyusername = $DB->get_records_sql($sql, array_merge($params, $sortparams));
} else {
$usersbyusername = array();
}
// populate the returned list and update local cache of enrolment records
foreach ($response as $remote) {
if (empty($usersbyusername[$remote['username']])) {
// we do not know this user or she is deleted or not confirmed or is 'guest'
continue;
}
$enrolment = new stdclass();
$enrolment->hostid = $mnethostid;
$enrolment->userid = $usersbyusername[$remote['username']]->id;
$enrolment->remotecourseid = $remotecourseid;
$enrolment->rolename = $remote['name']; // $remote['shortname'] not used
$enrolment->enroltime = $remote['timemodified'];
$enrolment->enroltype = $remote['enrol'];
$current = $DB->get_record('mnetservice_enrol_enrolments', array('hostid'=>$enrolment->hostid, 'userid'=>$enrolment->userid,
'remotecourseid'=>$enrolment->remotecourseid, 'enroltype'=>$enrolment->enroltype), 'id, enroltime');
if (empty($current)) {
$enrolment->id = $DB->insert_record('mnetservice_enrol_enrolments', $enrolment);
} else {
$enrolment->id = $current->id;
if ($current->enroltime != $enrolment->enroltime) {
$DB->update_record('mnetservice_enrol_enrolments', $enrolment);
}
}
$list[$enrolment->id] = $enrolment;
}
// prune stale enrolment records
if (empty($list)) {
$DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$mnethostid, 'remotecourseid'=>$remotecourseid));
} else {
list($isql, $params) = $DB->get_in_or_equal(array_keys($list), SQL_PARAMS_NAMED, 'param', false);
$params['hostid'] = $mnethostid;
$params['remotecourseid'] = $remotecourseid;
$select = "hostid = :hostid AND remotecourseid = :remotecourseid AND id $isql";
$DB->delete_records_select('mnetservice_enrol_enrolments', $select, $params);
}
// store the timestamp of the recent fetch, can be used for cache invalidate purposes
set_config('lastfetchenrolments', time(), 'mnetservice_enrol');
// local cache successfully updated
return true;
} else {
return serialize($request->error);
}
}
/**
* Send request to enrol our user to the remote course
*
* Updates our remote enrolments cache if the enrolment was successful.
*
* @uses mnet_xmlrpc_client Invokes XML-RPC request
* @param object $user our user
* @param object $remotecourse record from mnetservice_enrol_courses table
* @return true|string true if success, error message from the remote host otherwise
*/
public function req_enrol_user(stdclass $user, stdclass $remotecourse) {
global $CFG, $DB;
require_once($CFG->dirroot.'/mnet/xmlrpc/client.php');
$peer = new mnet_peer();
$peer->set_id($remotecourse->hostid);
$request = new mnet_xmlrpc_client();
$request->set_method('enrol/mnet/enrol.php/enrol_user');
$request->add_param(mnet_strip_user((array)$user, mnet_fields_to_send($peer)));
$request->add_param($remotecourse->remoteid);
if ($request->send($peer) === true) {
if ($request->response === true) {
// cache the enrolment information in our table
$enrolment = new stdclass();
$enrolment->hostid = $peer->id;
$enrolment->userid = $user->id;
$enrolment->remotecourseid = $remotecourse->remoteid;
$enrolment->enroltype = 'mnet';
// $enrolment->rolename not known now, must be re-fetched
// $enrolment->enroltime not known now, must be re-fetched
$DB->insert_record('mnetservice_enrol_enrolments', $enrolment);
return true;
} else {
return serialize(array('invalid response: '.print_r($request->response, true)));
}
} else {
return serialize($request->error);
}
}
/**
* Send request to unenrol our user from the remote course
*
* Updates our remote enrolments cache if the unenrolment was successful.
*
* @uses mnet_xmlrpc_client Invokes XML-RPC request
* @param object $user our user
* @param object $remotecourse record from mnetservice_enrol_courses table
* @return true|string true if success, error message from the remote host otherwise
*/
public function req_unenrol_user(stdclass $user, stdclass $remotecourse) {
global $CFG, $DB;
require_once($CFG->dirroot.'/mnet/xmlrpc/client.php');
$peer = new mnet_peer();
$peer->set_id($remotecourse->hostid);
$request = new mnet_xmlrpc_client();
$request->set_method('enrol/mnet/enrol.php/unenrol_user');
$request->add_param($user->username);
$request->add_param($remotecourse->remoteid);
if ($request->send($peer) === true) {
if ($request->response === true) {
// clear the cached information
$DB->delete_records('mnetservice_enrol_enrolments',
array('hostid'=>$peer->id, 'userid'=>$user->id, 'remotecourseid'=>$remotecourse->remoteid, 'enroltype'=>'mnet'));
return true;
} else {
return serialize(array('invalid response: '.print_r($request->response, true)));
}
} else {
return serialize($request->error);
}
}
/**
* Prepares error messages returned by our XML-RPC requests to be send as debug info to {@see \moodle_exception()}
*
* MNet client-side methods in this class return request error as serialized array.
*
* @param string $error serialized array
* @return string
*/
public function format_error_message($errormsg) {
$errors = unserialize($errormsg);
$output = 'mnet_xmlrpc_client request returned errors:'."\n";
foreach ($errors as $error) {
$output .= "$error\n";
}
return $output;
}
}
/**
* Selector of our users enrolled into remote course via enrol_mnet plugin
*/
class mnetservice_enrol_existing_users_selector extends user_selector_base {
/** @var id of the MNet peer */
protected $hostid;
/** @var id of the course at the remote server */
protected $remotecourseid;
public function __construct($name, $options) {
$this->hostid = $options['hostid'];
$this->remotecourseid = $options['remotecourseid'];
parent::__construct($name, $options);
}
/**
* Find our users currently enrolled into the remote course
*
* @param string $search
* @return array
*/
public function find_users($search) {
global $DB;
list($wherecondition, $params) = $this->search_sql($search, 'u');
$params['hostid'] = $this->hostid;
$params['remotecourseid'] = $this->remotecourseid;
$fields = "SELECT ".$this->required_fields_sql("u");
$countfields = "SELECT COUNT(1)";
$sql = " FROM {user} u
JOIN {mnetservice_enrol_enrolments} e ON e.userid = u.id
WHERE e.hostid = :hostid AND e.remotecourseid = :remotecourseid
AND e.enroltype = 'mnet'
AND $wherecondition";
list($sort, $sortparams) = users_order_by_sql('u');
$order = " ORDER BY $sort";
if (!$this->is_validating()) {
$potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
if ($potentialmemberscount > 100) {
return $this->too_many_results($search, $potentialmemberscount);
}
}
$availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams));
if (empty($availableusers)) {
return array();
}
if ($search) {
$groupname = get_string('enrolledusersmatching', 'enrol', $search);
} else {
$groupname = get_string('enrolledusers', 'enrol');
}
return array($groupname => $availableusers);
}
protected function get_options() {
$options = parent::get_options();
$options['hostid'] = $this->hostid;
$options['remotecourseid'] = $this->remotecourseid;
$options['file'] = 'mnet/service/enrol/locallib.php';
return $options;
}
}
/**
* Selector of our users who could be enrolled into a remote course via their enrol_mnet
*/
class mnetservice_enrol_potential_users_selector extends user_selector_base {
/** @var id of the MNet peer */
protected $hostid;
/** @var id of the course at the remote server */
protected $remotecourseid;
public function __construct($name, $options) {
$this->hostid = $options['hostid'];
$this->remotecourseid = $options['remotecourseid'];
parent::__construct($name, $options);
}
/**
* Find our users who could be enrolled into the remote course
*
* Our users must have 'moodle/site:mnetlogintoremote' capability assigned.
* Remote users, guests, deleted and not confirmed users are not returned.
*
* @param string $search
* @return array
*/
public function find_users($search) {
global $CFG, $DB;
$systemcontext = context_system::instance();
$userids = get_users_by_capability($systemcontext, 'moodle/site:mnetlogintoremote', 'u.id');
if (empty($userids)) {
return array();
}
list($usql, $uparams) = $DB->get_in_or_equal(array_keys($userids), SQL_PARAMS_NAMED, 'uid');
list($wherecondition, $params) = $this->search_sql($search, 'u');
$params = array_merge($params, $uparams);
$params['hostid'] = $this->hostid;
$params['remotecourseid'] = $this->remotecourseid;
$params['mnetlocalhostid'] = $CFG->mnet_localhost_id;
$fields = "SELECT ".$this->required_fields_sql("u");
$countfields = "SELECT COUNT(1)";
$sql = " FROM {user} u
WHERE $wherecondition
AND u.mnethostid = :mnetlocalhostid
AND u.id $usql
AND u.id NOT IN (SELECT e.userid
FROM {mnetservice_enrol_enrolments} e
WHERE (e.hostid = :hostid AND e.remotecourseid = :remotecourseid))";
list($sort, $sortparams) = users_order_by_sql('u');
$order = " ORDER BY $sort";
if (!$this->is_validating()) {
$potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
if ($potentialmemberscount > 100) {
return $this->too_many_results($search, $potentialmemberscount);
}
}
$availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams));
if (empty($availableusers)) {
return array();
}
if ($search) {
$groupname = get_string('enrolcandidatesmatching', 'enrol', $search);
} else {
$groupname = get_string('enrolcandidates', 'enrol');
}
return array($groupname => $availableusers);
}
protected function get_options() {
$options = parent::get_options();
$options['hostid'] = $this->hostid;
$options['remotecourseid'] = $this->remotecourseid;
$options['file'] = 'mnet/service/enrol/locallib.php';
return $options;
}
}
+33
View File
@@ -0,0 +1,33 @@
.path-admin-mnet-service-enrol .singlebutton {
text-align: center;
}
.path-admin-mnet-service-enrol table.remotehosts,
.path-admin-mnet-service-enrol table.otherenrolledusers,
.path-admin-mnet-service-enrol table.remotecourses {
margin: 0 auto 1em auto;
}
.path-admin-mnet-service-enrol table.remotecourses th.categoryname {
text-align: left;
background-color: #f6f6f6;
}
.path-admin-mnet-service-enrol table.remotecourses td.c1 {
font-weight: bold;
}
.path-admin-mnet-service-enrol table.remotecourses th.categoryname img {
margin-right: 1em;
}
.path-admin-mnet-service-enrol .collapsibleregioncaption {
font-size: 110%;
font-weight: bold;
text-align: center;
}
.path-admin-mnet-service-enrol .collapsibleregioninner {
border: 1px solid #ddd;
padding: 1em;
}
.path-admin-mnet-service-enrol .collapsibleregion.remotecourse.summary {
margin: 0 10em;
}
.path-admin-mnet-service-enrol .roleassigntable {
margin: 1em auto;
}
@@ -0,0 +1,344 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy test for the mnetservice_enrol implementation of the privacy API.
*
* @package mnetservice_enrol
* @category test
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mnetservice_enrol\privacy;
defined('MOODLE_INTERNAL') || die();
use mnetservice_enrol\privacy\provider;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\writer;
use core_privacy\local\request\transform;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\approved_userlist;
/**
* Privacy test for the mnetservice_enrol.
*
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/** @var stdClass the mnet host we are using to test. */
protected $mnethost;
/** @var stdClass the mnet service enrolment to test. */
protected $enrolment;
/**
* Test set up.
*
* This is executed before running any test in this file.
*/
public function setUp(): void {
global $DB;
// Add a mnet host.
$this->mnethost = new \stdClass();
$this->mnethost->name = 'A mnet host';
$this->mnethost->public_key = 'A random public key!';
$this->mnethost->id = $DB->insert_record('mnet_host', $this->mnethost);
}
/**
* Check that a user context is returned if there is any user data for this user.
*/
public function test_get_contexts_for_userid(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->assertEmpty(provider::get_contexts_for_userid($user->id));
// Create a test MNet service enrol enrolments.
$remotecourseid = 101;
$this->insert_mnetservice_enrol_courses($remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user->id, $remotecourseid);
$contextlist = provider::get_contexts_for_userid($user->id);
// Check that we only get back two context.
$this->assertCount(1, $contextlist);
// Check that the contexts are returned are the expected.
$usercontext = \context_user::instance($user->id);
$this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]);
}
/**
* Test that user data is exported correctly.
*/
public function test_export_user_data(): void {
global $DB;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->assertEmpty(provider::get_contexts_for_userid($user->id));
// Create a test MNet service enrol enrolments.
$remotecourseid = 101;
$this->insert_mnetservice_enrol_courses($remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user->id, $remotecourseid);
$subcontexts = [
get_string('privacy:metadata:mnetservice_enrol_enrolments', 'mnetservice_enrol')
];
$usercontext = \context_user::instance($user->id);
$writer = writer::with_context($usercontext);
$this->assertFalse($writer->has_any_data());
$approvedlist = new approved_contextlist($user, 'mnetservice_enrol', [$usercontext->id]);
provider::export_user_data($approvedlist);
$data = (array)$writer->get_data($subcontexts);
$this->assertCount(1, $data);
$this->assertEquals($this->mnethost->name, reset($data)->host);
$remotecoursename = $DB->get_field('mnetservice_enrol_courses', 'fullname',
array('remoteid' => $this->enrolment->remotecourseid));
$this->assertEquals($remotecoursename, reset($data)->remotecourseid);
$this->assertEquals($this->enrolment->rolename, reset($data)->rolename);
$this->assertEquals($this->enrolment->enroltype, reset($data)->enroltype);
$this->assertEquals(transform::datetime($this->enrolment->enroltime), reset($data)->enroltime);
}
/**
* Test deleting all user data for a specific context.
*/
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$this->assertEmpty(provider::get_contexts_for_userid($user->id));
// Create a test MNet service enrol enrolments.
$remotecourseid = 101;
$this->insert_mnetservice_enrol_courses($remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user->id, $remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user2->id, $remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user3->id, $remotecourseid);
$usercontext = \context_user::instance($user->id);
// Get all user enrolments.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', array());
$this->assertCount(3, $userenrolments);
// Get all user enrolments match with user.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', array('userid' => $user->id));
$this->assertCount(1, $userenrolments);
// Delete everything for the first user context.
provider::delete_data_for_all_users_in_context($usercontext);
// Get all user enrolments match with user.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', ['userid' => $user->id]);
$this->assertCount(0, $userenrolments);
// Get all user enrolments.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', array());
$this->assertCount(2, $userenrolments);
}
/**
* This should work identical to the above test.
*/
public function test_delete_data_for_user(): void {
global $DB;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$this->assertEmpty(provider::get_contexts_for_userid($user->id));
$remotecourseid = 101;
$this->insert_mnetservice_enrol_courses($remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user->id, $remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user2->id, $remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user3->id, $remotecourseid);
$remotecourseid2 = 102;
$this->insert_mnetservice_enrol_courses($remotecourseid2);
$this->insert_mnetservice_enrol_enrolments($user->id, $remotecourseid2);
$usercontext = \context_user::instance($user->id);
// Get all user enrolments.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', array());
$this->assertCount(4, $userenrolments);
// Get all user enrolments match with user.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', array('userid' => $user->id));
$this->assertCount(2, $userenrolments);
// Delete everything for the first user.
$approvedlist = new approved_contextlist($user, 'mnetservice_enrol', [$usercontext->id]);
provider::delete_data_for_user($approvedlist);
// Get all user enrolments match with user.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', ['userid' => $user->id]);
$this->assertCount(0, $userenrolments);
// Get all user enrolments accounts.
$userenrolments = $DB->get_records('mnetservice_enrol_enrolments', array());
$this->assertCount(2, $userenrolments);
}
/**
* Test that only users within a course context are fetched.
*/
public function test_get_users_in_context(): void {
$this->resetAfterTest();
$component = 'mnetservice_enrol';
// Create a user.
$user = $this->getDataGenerator()->create_user();
$usercontext = \context_user::instance($user->id);
// Create user2.
$user2 = $this->getDataGenerator()->create_user();
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
provider::get_users_in_context($userlist);
$this->assertCount(0, $userlist);
// Create a test MNet service enrol enrolments.
$remotecourseid = 101;
$this->insert_mnetservice_enrol_courses($remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user->id, $remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user2->id, $remotecourseid);
// The list of users within the user context should contain only user.
provider::get_users_in_context($userlist);
$this->assertCount(1, $userlist);
$this->assertFalse(in_array($user2->id, $userlist->get_userids()));
$this->assertTrue(in_array($user->id, $userlist->get_userids()));
// The list of users within the system context should be empty.
$systemcontext = \context_system::instance();
$userlist2 = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(0, $userlist2);
}
/**
* Test that data for users in approved userlist is deleted.
*/
public function test_delete_data_for_users(): void {
$this->resetAfterTest();
$component = 'mnetservice_enrol';
// Create user1.
$user1 = $this->getDataGenerator()->create_user();
$usercontext1 = \context_user::instance($user1->id);
// Create user2.
$user2 = $this->getDataGenerator()->create_user();
$usercontext2 = \context_user::instance($user2->id);
// Create a test MNet service enrol enrolments.
$remotecourseid = 101;
$this->insert_mnetservice_enrol_courses($remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user1->id, $remotecourseid);
$this->insert_mnetservice_enrol_enrolments($user2->id, $remotecourseid);
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(1, $userlist1);
$expected = [$user1->id];
$actual = $userlist1->get_userids();
$this->assertEquals($expected, $actual);
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(1, $userlist2);
$expected = [$user2->id];
$actual = $userlist2->get_userids();
$this->assertEquals($expected, $actual);
// Convert $userlist1 into an approved_contextlist.
$approvedlist1 = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist1);
// Re-fetch users in usercontext1.
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
provider::get_users_in_context($userlist1);
// The user data in usercontext1 should be deleted.
$this->assertCount(0, $userlist1);
// Re-fetch users in usercontext2.
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
provider::get_users_in_context($userlist2);
// The user data in usercontext2 should be still present.
$this->assertCount(1, $userlist2);
// Convert $userlist2 into an approved_contextlist in the system context.
$systemcontext = \context_system::instance();
$approvedlist3 = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist3);
// Re-fetch users in usercontext2.
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
provider::get_users_in_context($userlist2);
// The user data in systemcontext should not be deleted.
$this->assertCount(1, $userlist2);
}
/**
* Help function to create a simulation of MNet service enrol.
* Create a Dummy Enrol into mnetservice_enrol_enrolments.
*
* @param int $userid Userid.
* @param int $remotecourseid Remotecourseid.
*/
protected function insert_mnetservice_enrol_enrolments($userid, $remotecourseid) {
global $DB;
// Create a test MNet service enrol enrolments.
$this->enrolment = new \stdClass();
$this->enrolment->hostid = $this->mnethost->id;
$this->enrolment->userid = $userid;
$this->enrolment->remotecourseid = $remotecourseid;
$this->enrolment->rolename = 'student';
$this->enrolment->enroltime = time();
$this->enrolment->enroltype = 'mnet';
$DB->insert_record('mnetservice_enrol_enrolments', $this->enrolment);
}
/**
* Help function to create a simualtion of MNet service enrol.
* Create a Dummy Course into mnetservice_enrol_courses.
* Important: The real course is on the host.
*
* @param int $remoteid Remote courseid.
*/
protected function insert_mnetservice_enrol_courses($remoteid) {
global $DB;
// Create a Dummy Remote Course to test.
$course = new \stdClass();
$course->hostid = $this->mnethost->id;
$course->remoteid = $remoteid;
$course->categoryid = 1;
$course->categoryname = get_string('defaultcategoryname');
$course->sortorder = 10001;
$course->fullname = 'Test Remote Course '.$remoteid;
$course->shortname = 'testremotecourse '.$remoteid;
$course->idnumber = 'IdnumberRemote '.$remoteid;
$course->summary = 'TestSummaryRemote '.$remoteid;
$course->summaryformat = FORMAT_MOODLE;
$course->startdate = time();
$course->roleid = 5;
$course->rolename = 'student';
$DB->insert_record('mnetservice_enrol_courses', $course);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package mnetservice
* @subpackage enrol
* @copyright 2010 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'mnetservice_enrol'; // Full name of the plugin (used for diagnostics)
+100
View File
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Events tests.
*
* @package core_mnet
* @category test
* @copyright 2013 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_mnet\event;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mnet/lib.php');
class events_test extends \advanced_testcase {
/** @var stdClass the mnet host we are using to test */
protected $mnethost;
/**
* Test set up.
*
* This is executed before running any test in this file.
*/
public function setUp(): void {
global $DB;
$this->resetAfterTest();
// Add a mnet host.
$this->mnethost = new \stdClass();
$this->mnethost->name = 'A mnet host';
$this->mnethost->public_key = 'A random public key!';
$this->mnethost->id = $DB->insert_record('mnet_host', $this->mnethost);
}
/**
* Test the mnet access control created event.
*/
public function test_mnet_access_control_created(): void {
// Trigger and capture the event.
$sink = $this->redirectEvents();
mnet_update_sso_access_control('username', $this->mnethost->id, 'enabled');
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\mnet_access_control_created', $event);
$this->assertEquals(\context_system::instance(), $event->get_context());
$this->assertEventContextNotUsed($event);
$url = new \moodle_url('/admin/mnet/access_control.php');
$this->assertEquals($url, $event->get_url());
}
/**
* Test the mnet access control updated event.
*/
public function test_mnet_access_control_updated(): void {
global $DB;
// Create a mnet access control.
$mnetaccesscontrol = new \stdClass();
$mnetaccesscontrol->username = 'username';
$mnetaccesscontrol->mnet_host_id = $this->mnethost->id;
$mnetaccesscontrol->accessctrl = 'enabled';
$mnetaccesscontrol->id = $DB->insert_record('mnet_sso_access_control', $mnetaccesscontrol);
// Trigger and capture the event.
$sink = $this->redirectEvents();
mnet_update_sso_access_control('username', $this->mnethost->id, 'enabled');
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\mnet_access_control_updated', $event);
$this->assertEquals(\context_system::instance(), $event->get_context());
$this->assertEventContextNotUsed($event);
$url = new \moodle_url('/admin/mnet/access_control.php');
$this->assertEquals($url, $event->get_url());
}
}
+404
View File
@@ -0,0 +1,404 @@
<?php
/**
* An XML-RPC client
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
require_once $CFG->dirroot.'/mnet/lib.php';
/**
* Class representing an XMLRPC request against a remote machine
*/
class mnet_xmlrpc_client {
var $method = '';
var $params = array();
var $timeout = 60;
var $error = array();
var $response = '';
var $mnet = null;
/**
* Constructor
*/
public function __construct() {
// make sure we've got this set up before we try and do anything else
$this->mnet = get_mnet_environment();
}
/**
* Allow users to override the default timeout
* @param int $timeout Request timeout in seconds
* $return bool True if param is an integer or integer string
*/
public function set_timeout($timeout) {
if (!is_integer($timeout)) {
if (is_numeric($timeout)) {
$this->timeout = (integer)$timeout;
return true;
}
return false;
}
$this->timeout = $timeout;
return true;
}
/**
* Set the path to the method or function we want to execute on the remote
* machine. Examples:
* mod/scorm/functionname
* auth/mnet/methodname
* In the case of auth and enrolment plugins, an object will be created and
* the method on that object will be called
*/
public function set_method($xmlrpcpath) {
if (is_string($xmlrpcpath)) {
$this->method = $xmlrpcpath;
$this->params = array();
return true;
}
$this->method = '';
$this->params = array();
return false;
}
/**
* Add a parameter to the array of parameters.
*
* @param string $argument A transport ID, as defined in lib.php
* @param string $type The argument type, can be one of:
* i4
* i8
* int
* double
* string
* boolean
* datetime | dateTime.iso8601
* base64
* null
* array
* struct
* @return bool True on success
*/
public function add_param($argument, $type = 'string') {
// Convert any use of the old 'datetime' to the correct 'dateTime.iso8601' one.
$type = ($type === 'datetime' ? 'dateTime.iso8601' : $type);
// BC fix, if some argument is array and comes as string, change type to array (sequentials)
// or struct (associative).
// This is the behavior of the encode_request() method from the xmlrpc extension.
// Note that uses in core have been fixed, but there may be others using that.
if (is_array($argument) && $type === 'string') {
if (array_keys($argument) === range(0, count($argument) - 1)) {
$type = 'array';
} else {
$type = 'struct';
}
mnet_debug('Incorrect ' . $type . ' param passed as string in mnet_xmlrpc_client->add_param(): ' .
json_encode($argument));
}
if (!isset(\PhpXmlRpc\Value::$xmlrpcTypes[$type])) { // Arrived here, still erong type? Let's stop.
return false;
}
// If we are array or struct, we need to ensure that, recursively, all the elements are proper values.
// or serialize, used later on send() won't work with them. Encoder::encode() provides us with that.
if ($type === 'array' || $type === 'struct') {
$encoder = new \PhpXmlRpc\Encoder();
$this->params[] = $encoder->encode($argument);
} else {
// Normal scalar case.
$this->params[] = new \PhpXmlRpc\Value($argument, $type);
}
return true;
}
/**
* Send the request to the server - decode and return the response
*
* @param object $mnet_peer A mnet_peer object with details of the
* remote host we're connecting to
* @param bool $rekey The rekey attribute stops us from
* getting into a loop.
* @return mixed A PHP variable, as returned by the
*/
public function send($mnet_peer, bool $rekey = false) {
global $CFG, $DB;
if (!$this->permission_to_call($mnet_peer)) {
mnet_debug("tried and wasn't allowed to call a method on $mnet_peer->wwwroot");
return false;
}
$request = new \PhpXmlRpc\Request($this->method, $this->params);
$requesttext = $request->serialize('utf-8');
$signedrequest = mnet_sign_message($requesttext);
$encryptedrequest = mnet_encrypt_message($signedrequest, $mnet_peer->public_key);
$client = $this->prepare_http_request($mnet_peer);
$timestamp_send = time();
mnet_debug("about to send the xmlrpc request");
$response = $client->send($encryptedrequest, $this->timeout);
mnet_debug("managed to complete a xmlrpc request");
$timestamp_receive = time();
if ($response->faultCode()) {
$this->error[] = $response->faultCode() .':'. $response->faultString();
return false;
}
$rawresponse = trim($response->value()); // Because MNet responses ARE NOT valid xmlrpc, don't try any PhpXmlRpc facility.
$mnet_peer->touch();
$crypt_parser = new mnet_encxml_parser();
$crypt_parser->parse($rawresponse);
// If we couldn't parse the message, or it doesn't seem to have encrypted contents,
// give the most specific error msg available & return
if (!$crypt_parser->payload_encrypted) {
if (! empty($crypt_parser->remoteerror)) {
$this->error[] = '4: remote server error: ' . $crypt_parser->remoteerror;
} else if (! empty($crypt_parser->error)) {
$crypt_parser_error = $crypt_parser->error[0];
$message = '3:XML Parse error in payload: '.$crypt_parser_error['string']."\n";
if (array_key_exists('lineno', $crypt_parser_error)) {
$message .= 'At line number: '.$crypt_parser_error['lineno']."\n";
}
if (array_key_exists('line', $crypt_parser_error)) {
$message .= 'Which reads: '.$crypt_parser_error['line']."\n";
}
$this->error[] = $message;
} else {
$this->error[] = '1:Payload not encrypted ';
}
$crypt_parser->free_resource();
return false;
}
$key = array_pop($crypt_parser->cipher);
$data = array_pop($crypt_parser->cipher);
$crypt_parser->free_resource();
// Initialize payload var
$decryptedenvelope = '';
// &$decryptedenvelope
$isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key),
$this->mnet->get_private_key(), 'RC4');
if (!$isOpen) {
// Decryption failed... let's try our archived keys
$openssl_history = get_config('mnet', 'openssl_history');
if(empty($openssl_history)) {
$openssl_history = array();
set_config('openssl_history', serialize($openssl_history), 'mnet');
} else {
$openssl_history = unserialize($openssl_history);
}
foreach($openssl_history as $keyset) {
$keyresource = openssl_pkey_get_private($keyset['keypair_PEM']);
$isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $keyresource, 'RC4');
if ($isOpen) {
// It's an older code, sir, but it checks out
break;
}
}
}
if (!$isOpen) {
trigger_error("None of our keys could open the payload from host {$mnet_peer->wwwroot} with id {$mnet_peer->id}.");
$this->error[] = '3:No key match';
return false;
}
if (strpos(substr($decryptedenvelope, 0, 100), '<signedMessage>')) {
$sig_parser = new mnet_encxml_parser();
$sig_parser->parse($decryptedenvelope);
} else {
$this->error[] = '2:Payload not signed: ' . $decryptedenvelope;
return false;
}
// Margin of error is the time it took the request to complete.
$margin_of_error = $timestamp_receive - $timestamp_send;
// Guess the time gap between sending the request and the remote machine
// executing the time() function. Marginally better than nothing.
$hysteresis = ($margin_of_error) / 2;
$remote_timestamp = $sig_parser->remote_timestamp - $hysteresis;
$time_offset = $remote_timestamp - $timestamp_send;
if ($time_offset > 0) {
$threshold = get_config('mnet', 'drift_threshold');
if(empty($threshold)) {
// We decided 15 seconds was a pretty good arbitrary threshold
// for time-drift between servers, but you can customize this in
// the config_plugins table. It's not advised though.
set_config('drift_threshold', 15, 'mnet');
$threshold = 15;
}
if ($time_offset > $threshold) {
$this->error[] = '6:Time gap with '.$mnet_peer->name.' ('.$time_offset.' seconds) is greater than the permitted maximum of '.$threshold.' seconds';
return false;
}
}
$xmlrpcresponse = base64_decode($sig_parser->data_object);
// Let's convert the xmlrpc back to PHP structure.
$response = null;
$encoder = new \PhpXmlRpc\Encoder();
$oresponse = $encoder->decodeXML($xmlrpcresponse); // First, to internal PhpXmlRpc\Response structure.
if ($oresponse instanceof \PhpXmlRpc\Response) {
// Special handling of fault responses (because value() doesn't handle them properly).
if ($oresponse->faultCode()) {
$response = ['faultCode' => $oresponse->faultCode(), 'faultString' => $oresponse->faultString()];
} else {
$response = $encoder->decode($oresponse->value()); // Normal Response conversion to PHP.
}
} else {
// Maybe this is just a param, let's convert it too.
$response = $encoder->decode($oresponse);
}
$this->response = $response;
// xmlrpc errors are pushed onto the $this->error stack
if (is_array($this->response) && array_key_exists('faultCode', $this->response)) {
// The faultCode 7025 means we tried to connect with an old SSL key
// The faultString is the new key - let's save it and try again
// The rekey attribute stops us from getting into a loop
if($this->response['faultCode'] == 7025 && empty($rekey)) {
mnet_debug('recieved an old-key fault, so trying to get the new key and update our records');
// If the new certificate doesn't come thru clean_param() unmolested, error out
if($this->response['faultString'] != clean_param($this->response['faultString'], PARAM_PEM)) {
$this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'];
}
$record = new stdClass();
$record->id = $mnet_peer->id;
$record->public_key = $this->response['faultString'];
$details = openssl_x509_parse($record->public_key);
if(!isset($details['validTo_time_t'])) {
$this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'];
}
$record->public_key_expires = $details['validTo_time_t'];
$DB->update_record('mnet_host', $record);
// Create a new peer object populated with the new info & try re-sending the request
$rekeyed_mnet_peer = new mnet_peer();
$rekeyed_mnet_peer->set_id($record->id);
return $this->send($rekeyed_mnet_peer, true); // Re-send mnet_peer with the new key.
}
if (!empty($CFG->mnet_rpcdebug)) {
if (get_string_manager()->string_exists('error'.$this->response['faultCode'], 'mnet')) {
$guidance = get_string('error'.$this->response['faultCode'], 'mnet');
} else {
$guidance = '';
}
} else {
$guidance = '';
}
$this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'] ."\n".$guidance;
}
// ok, it's signed, but is it signed with the right certificate ?
// do this *after* we check for an out of date key
$verified = openssl_verify($xmlrpcresponse, base64_decode($sig_parser->signature), $mnet_peer->public_key);
if ($verified != 1) {
$this->error[] = 'Invalid signature';
}
return empty($this->error);
}
/**
* Check that we are permitted to call method on specified peer
*
* @param object $mnet_peer A mnet_peer object with details of the remote host we're connecting to
* @return bool True if we permit calls to method on specified peer, False otherwise.
*/
public function permission_to_call($mnet_peer) {
global $DB, $CFG, $USER;
// Executing any system method is permitted.
$system_methods = array('system/listMethods', 'system/methodSignature', 'system/methodHelp', 'system/listServices');
if (in_array($this->method, $system_methods) ) {
return true;
}
$hostids = array($mnet_peer->id);
if (!empty($CFG->mnet_all_hosts_id)) {
$hostids[] = $CFG->mnet_all_hosts_id;
}
// At this point, we don't care if the remote host implements the
// method we're trying to call. We just want to know that:
// 1. The method belongs to some service, as far as OUR host knows
// 2. We are allowed to subscribe to that service on this mnet_peer
list($hostidsql, $hostidparams) = $DB->get_in_or_equal($hostids);
$sql = "SELECT r.id
FROM {mnet_remote_rpc} r
INNER JOIN {mnet_remote_service2rpc} s2r ON s2r.rpcid = r.id
INNER JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid
WHERE r.xmlrpcpath = ?
AND h2s.subscribe = ?
AND h2s.hostid $hostidsql";
$params = array($this->method, 1);
$params = array_merge($params, $hostidparams);
if ($DB->record_exists_sql($sql, $params)) {
return true;
}
$this->error[] = '7:User with ID '. $USER->id .
' attempted to call unauthorised method '.
$this->method.' on host '.
$mnet_peer->wwwroot;
return false;
}
/**
* Generate a \PhpXmlRpc\Client handle and prepare it for sending to an mnet host
*
* @param object $mnet_peer A mnet_peer object with details of the remote host the request will be sent to
* @return \PhpXmlRpc\Client handle - the almost-ready-to-send http request
*/
public function prepare_http_request($mnet_peer) {
$uri = $mnet_peer->wwwroot . $mnet_peer->application->xmlrpc_server_url;
// Instantiate the xmlrpc client to be used for the client request
// and configure it the way we want.
$client = new \PhpXmlRpc\Client($uri);
$client->setUseCurl(\PhpXmlRpc\Client::USE_CURL_ALWAYS);
$client->setUserAgent('Moodle');
$client->return_type = 'xml'; // Because MNet responses ARE NOT valid xmlrpc, don't try any validation.
// TODO: Link this to DEBUG DEVELOPER or with MNET debugging...
// $client->setdebug(1); // See a good number of complete requests and responses.
$verifyhost = 0;
$verifypeer = false;
if ($mnet_peer->sslverification == mnet_peer::SSL_HOST_AND_PEER) {
$verifyhost = 2;
$verifypeer = true;
} else if ($mnet_peer->sslverification == mnet_peer::SSL_HOST) {
$verifyhost = 2;
}
$client->setSSLVerifyHost($verifyhost);
$client->setSSLVerifyPeer($verifypeer);
return $client;
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
/**
* An XML-RPC server
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
// Make certain that config.php doesn't display any errors, and that it doesn't
// override our do-not-display-errors setting:
// disable moodle specific debug messages and any errors in output
define('NO_DEBUG_DISPLAY', true);
// cookies are not used, makes sure there is empty global $USER
define('NO_MOODLE_COOKIES', true);
define('MNET_SERVER', true);
require(__DIR__.'/../../config.php');
$mnet = get_mnet_environment();
// Include MNET stuff:
require_once $CFG->dirroot.'/mnet/lib.php';
require_once $CFG->dirroot.'/mnet/remote_client.php';
require_once $CFG->dirroot.'/mnet/xmlrpc/serverlib.php';
if ($CFG->mnet_dispatcher_mode === 'off') {
throw new \moodle_exception('mnetdisabled', 'mnet');
}
// Content type for output is not html:
header('Content-type: text/xml; charset=utf-8');
$rawpostdata = file_get_contents("php://input");
mnet_debug("RAW POST DATA", 2);
mnet_debug($rawpostdata, 2);
if (!isset($_SERVER)) {
exit(mnet_server_fault(712, get_string('phperror', 'mnet')));
}
// New global variable which ONLY gets set in this server page, so you know that
// if you've been called by a remote Moodle, this should be set:
$remoteclient = new mnet_remote_client();
set_mnet_remote_client($remoteclient);
try {
$plaintextmessage = mnet_server_strip_encryption($rawpostdata);
$xmlrpcrequest = mnet_server_strip_signature($plaintextmessage);
} catch (Exception $e) {
mnet_debug('encryption strip exception thrown: ' . $e->getMessage());
exit(mnet_server_fault($e->getCode(), $e->getMessage(), $e->a));
}
mnet_debug('XMLRPC Payload', 2);
mnet_debug($xmlrpcrequest, 2);
if($remoteclient->pushkey == true) {
// The peer used one of our older public keys, we will return a
// signed/encrypted error message containing our new public key
// Sign message with our old key, and encrypt to the peer's private key.
mnet_debug('sending back new key');
exit(mnet_server_fault_xml(7025, $mnet->public_key, $remoteclient->useprivatekey));
}
// Have a peek at what the request would be if we were to process it
$encoder = new \PhpXmlRpc\Encoder();
$orequest = $encoder->decodeXML($xmlrpcrequest); // First, to internal.
$method = $orequest->method(); // We just need the method.
mnet_debug("incoming mnet request $method");
// One of three conditions need to be met before we continue processing this request:
// 1. Request is properly encrypted and signed
// 2. Request is for a keyswap (we don't mind enencrypted or unsigned requests for a public key)
// 3. Request is properly signed and we're happy with it being unencrypted
if ((($remoteclient->request_was_encrypted == true) && ($remoteclient->signatureok == true))
|| (($method == 'system.keyswap') || ($method == 'system/keyswap'))
|| (($remoteclient->signatureok == true) && ($remoteclient->plaintext_is_ok() == true))) {
try {
// main dispatch call. will echo the response directly
mnet_server_dispatch($xmlrpcrequest);
mnet_debug('exiting cleanly');
exit;
} catch (Exception $e) {
mnet_debug('dispatch exception thrown: ' . $e->getMessage());
exit(mnet_server_fault($e->getCode(), $e->getMessage(), $e->a));
}
}
// if we get to here, something is wrong
// so detect a few common cases and send appropriate errors
if (($remoteclient->request_was_encrypted == false) && ($remoteclient->plaintext_is_ok() == false)) {
mnet_debug('non encrypted request');
exit(mnet_server_fault(7021, get_string('forbidden-transport', 'mnet')));
}
if ($remoteclient->request_was_signed == false) {
// Request was not signed
mnet_debug('non signed request');
exit(mnet_server_fault(711, get_string('verifysignature-error', 'mnet')));
}
if ($remoteclient->signatureok == false) {
// We were unable to verify the signature
mnet_debug('non verified signature');
exit(mnet_server_fault(710, get_string('verifysignature-invalid', 'mnet')));
}
mnet_debug('unknown error');
exit(mnet_server_fault(7000, get_string('unknownerror', 'mnet')));
+700
View File
@@ -0,0 +1,700 @@
<?php
/**
* -----XML-Envelope---------------------------------
* | |
* | Encrypted-Symmetric-key---------------- |
* | |_____________________________________| |
* | |
* | Encrypted data------------------------- |
* | | | |
* | | -XML-Envelope------------------ | |
* | | | | | |
* | | | --Signature------------- | | |
* | | | |______________________| | | |
* | | | | | |
* | | | --Signed-Payload-------- | | |
* | | | | | | | |
* | | | | XML-RPC Request | | | |
* | | | |______________________| | | |
* | | | | | |
* | | |_____________________________| | |
* | |_____________________________________| |
* | |
* |________________________________________________|
*
*/
/* Strip encryption envelope (if present) and decrypt data
*
* @param string $rawpostdata The XML that the client sent
*
* @throws mnet_server_exception
*
* @return string XML with any encryption envolope removed
*/
function mnet_server_strip_encryption($rawpostdata) {
$remoteclient = get_mnet_remote_client();
$crypt_parser = new mnet_encxml_parser();
$crypt_parser->parse($rawpostdata);
$mnet = get_mnet_environment();
if (!$crypt_parser->payload_encrypted) {
return $rawpostdata;
}
// Make sure we know who we're talking to
$host_record_exists = $remoteclient->set_wwwroot($crypt_parser->remote_wwwroot);
if (false == $host_record_exists) {
throw new mnet_server_exception(7020, 'wrong-wwwroot', $crypt_parser->remote_wwwroot);
}
// This key is symmetric, and is itself encrypted. Can be decrypted using our private key
$key = array_pop($crypt_parser->cipher);
// This data is symmetrically encrypted, can be decrypted using the above key
$data = array_pop($crypt_parser->cipher);
$crypt_parser->free_resource();
$payload = ''; // Initialize payload var
// &$payload
$isOpen = openssl_open(base64_decode($data), $payload, base64_decode($key), $mnet->get_private_key(), 'RC4');
if ($isOpen) {
$remoteclient->was_encrypted();
return $payload;
}
// Decryption failed... let's try our archived keys
$openssl_history = get_config('mnet', 'openssl_history');
if(empty($openssl_history)) {
$openssl_history = array();
set_config('openssl_history', serialize($openssl_history), 'mnet');
} else {
$openssl_history = unserialize($openssl_history);
}
foreach($openssl_history as $keyset) {
$keyresource = openssl_pkey_get_private($keyset['keypair_PEM']);
$isOpen = openssl_open(base64_decode($data), $payload, base64_decode($key), $keyresource, 'RC4');
if ($isOpen) {
// It's an older code, sir, but it checks out
$remoteclient->was_encrypted();
$remoteclient->encrypted_to($keyresource);
$remoteclient->set_pushkey();
return $payload;
}
}
//If after all that we still couldn't decrypt the message, error out.
throw new mnet_server_exception(7023, 'encryption-invalid');
}
/* Strip signature envelope (if present), try to verify any signature using our record of remote peer's public key.
*
* @param string $plaintextmessage XML envelope containing XMLRPC request and signature
*
* @return string XMLRPC request
*/
function mnet_server_strip_signature($plaintextmessage) {
$remoteclient = get_mnet_remote_client();
$sig_parser = new mnet_encxml_parser();
$sig_parser->parse($plaintextmessage);
if ($sig_parser->signature == '') {
return $plaintextmessage;
}
// Record that the request was signed in some way
$remoteclient->was_signed();
// Load any information we have about this mnet peer
$remoteclient->set_wwwroot($sig_parser->remote_wwwroot);
$payload = base64_decode($sig_parser->data_object);
$signature = base64_decode($sig_parser->signature);
$certificate = $remoteclient->public_key;
// If we don't have any certificate for the host, don't try to check the signature
// Just return the parsed request
if ($certificate == false) {
return $payload;
}
// Does the signature match the data and the public cert?
$signature_verified = openssl_verify($payload, $signature, $certificate);
if ($signature_verified == 0) {
// $signature was not generated for $payload using $certificate
// Get the key the remote peer is currently publishing:
$currkey = mnet_get_public_key($remoteclient->wwwroot, $remoteclient->application);
// If the key the remote peer is currently publishing is different to $certificate
if($currkey != $certificate) {
// if pushkey is already set, it means the request was encrypted to an old key
// in mnet_server_strip_encryption.
// if we call refresh_key() here before pushing out our new key,
// and the other site ALSO has a new key,
// we'll get into an infinite keyswap loop
// so push just bail here, and push out the new key.
// the next request will get through to refresh_key
if ($remoteclient->pushkey) {
return false;
}
// Try and get the server's new key through trusted means
$remoteclient->refresh_key();
// If we did manage to re-key, try to verify the signature again using the new public key.
$certificate = $remoteclient->public_key;
$signature_verified = openssl_verify($payload, $signature, $certificate);
}
}
if ($signature_verified == 1) {
$remoteclient->signature_verified();
$remoteclient->touch();
}
$sig_parser->free_resource();
return $payload;
}
/**
* Return the proper XML-RPC content to report an error in the local language.
*
* @param int $code The ID code of the error message
* @param string $text The full string of the error message (get_string will <b>not be called</b>)
* @param string $param The $a param for the error message in the lang file
* @return string $text The text of the error message
*/
function mnet_server_fault($code, $text, $param = null) {
if (!is_numeric($code)) {
$code = 0;
}
$code = intval($code);
return mnet_server_fault_xml($code, $text);
}
/**
* Return the proper XML-RPC content to report an error.
*
* @param int $code The ID code of the error message
* @param string $text The error message
* @param resource $privatekey The private key that should be used to sign the response
* @return string $text The XML text of the error message
*/
function mnet_server_fault_xml($code, $text, $privatekey = null) {
global $CFG;
// Replace illegal XML chars - is this already in a lib somewhere?
$text = str_replace(array('<','>','&','"',"'"), array('&lt;','&gt;','&amp;','&quot;','&apos;'), $text);
$return = mnet_server_prepare_response('<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>'.$code.'</int></value>
</member>
<member>
<name>faultString</name>
<value><string>'.$text.'</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>', $privatekey);
if ($code != 7025) { // new key responses
mnet_debug("XMLRPC Error Response $code: $text");
//mnet_debug($return);
}
return $return;
}
/**
* Package a response in any required envelope, and return it to the client
*
* @param string $response The XMLRPC response string
* @param resource $privatekey The private key to sign the response with
* @return string The encoded response string
*/
function mnet_server_prepare_response($response, $privatekey = null) {
$remoteclient = get_mnet_remote_client();
if ($remoteclient->request_was_signed) {
$response = mnet_sign_message($response, $privatekey);
}
if ($remoteclient->request_was_encrypted) {
$response = mnet_encrypt_message($response, $remoteclient->public_key);
}
return $response;
}
/**
* If security checks are passed, dispatch the request to the function/method
*
* The config variable 'mnet_dispatcher_mode' can be:
* strict: Only execute functions that are in specific files
* off: The default - don't execute anything
*
* @param string $payload The XML-RPC request
*
* @throws mnet_server_exception
*
* @return No return val - just echo the response
*/
function mnet_server_dispatch($payload) {
global $CFG, $DB;
$remoteclient = get_mnet_remote_client();
// Decode the request to method + params.
$method = null;
$params = null;
$encoder = new \PhpXmlRpc\Encoder();
$orequest = $encoder->decodeXML($payload); // First, to internal PhpXmlRpc\Response structure.
if ($orequest && $orequest instanceof \PhpXmlRpc\Request) {
$method = $orequest->method();
$numparams = $orequest->getNumParams();
for ($i = 0; $i < $numparams; $i++) {
$params[] = $encoder->decode($orequest->getParam($i));
}
}
// $method is something like: "mod/forum/lib.php/forum_add_instance"
// $params is an array of parameters. A parameter might itself be an array.
// Check that the method name consists of allowed characters only.
// The method name must not begin with a / - avoid absolute paths
// A dot character . is only allowed in the filename, i.e. something.php
if (0 == preg_match("@^[A-Za-z0-9]+/[A-Za-z0-9/_\.-]+(\.php/)?[A-Za-z0-9_-]+$@",$method)) {
throw new mnet_server_exception(713, 'nosuchfunction');
}
if(preg_match("/^system\./", $method)) {
$callstack = explode('.', $method);
} else {
$callstack = explode('/', $method);
// callstack will look like array('mod', 'forum', 'lib.php', 'forum_add_instance');
}
/**
* What has the site administrator chosen as his dispatcher setting?
* strict: Only execute functions that are in specific files
* off: The default - don't execute anything
*/
////////////////////////////////////// OFF
if (!isset($CFG->mnet_dispatcher_mode) ) {
set_config('mnet_dispatcher_mode', 'off');
throw new mnet_server_exception(704, 'nosuchservice');
} elseif ('off' == $CFG->mnet_dispatcher_mode) {
throw new mnet_server_exception(704, 'nosuchservice');
////////////////////////////////////// SYSTEM METHODS
} elseif ($callstack[0] == 'system') {
$functionname = $callstack[1];
$xmlrpcserver = new \PhpXmlRpc\Server();
$xmlrpcserver->functions_parameters_type = 'epivals';
$xmlrpcserver->compress_response = false;
// register all the system methods
$systemmethods = array('listMethods', 'methodSignature', 'methodHelp', 'listServices', 'listFiles', 'retrieveFile', 'keyswap');
foreach ($systemmethods as $m) {
// I'm adding the canonical xmlrpc references here, however we've
// already forbidden that the period (.) should be allowed in the call
// stack, so if someone tries to access our XMLRPC in the normal way,
// they'll already have received a RPC server fault message.
// Maybe we should allow an easement so that regular XMLRPC clients can
// call our system methods, and find out what we have to offer?
$handler = 'mnet_system';
if ($m == 'keyswap') {
$handler = 'mnet_keyswap';
}
if ($method == 'system.' . $m || $method == 'system/' . $m) {
$xmlrpcserver->add_to_map($method, $handler);
$xmlrpcserver->user_data = $remoteclient;
$response = $xmlrpcserver->service($payload, true);
$response = mnet_server_prepare_response($response);
echo $response;
return;
}
}
throw new mnet_server_exception(7018, 'nosuchfunction');
//////////////////////////////////// NORMAL PLUGIN DISPATCHER
} else {
// anything else comes from some sort of plugin
if ($rpcrecord = $DB->get_record('mnet_rpc', array('xmlrpcpath' => $method))) {
$response = mnet_server_invoke_plugin_method($method, $callstack, $rpcrecord, $payload);
$response = mnet_server_prepare_response($response);
echo $response;
return;
// if the rpc record isn't found, check to see if dangerous mode is on
////////////////////////////////////// DANGEROUS
} else if ('dangerous' == $CFG->mnet_dispatcher_mode && $remoteclient->plaintext_is_ok()) {
$functionname = array_pop($callstack);
$filename = clean_param(implode('/',$callstack), PARAM_PATH);
if (0 == preg_match("/php$/", $filename)) {
// Filename doesn't end in 'php'; possible attack?
// Generate error response - unable to locate function
throw new mnet_server_exception(7012, 'nosuchfunction');
}
// The call stack holds the path to any include file
$includefile = $CFG->dirroot.'/'.$filename;
$response = mnet_server_invoke_dangerous_method($includefile, $functionname, $method, $payload);
echo $response;
return;
}
}
throw new mnet_server_exception(7012, 'nosuchfunction');
}
/**
* Execute the system functions - mostly for introspection
*
* @param string $method XMLRPC method name, e.g. system.listMethods
* @param array $params Array of parameters from the XMLRPC request
* @param string $hostinfo Hostinfo object from the mnet_host table
*
* @throws mnet_server_exception
*
* @return mixed Response data - any kind of PHP variable
*/
function mnet_system($method, $params, $hostinfo) {
global $CFG, $DB;
if (empty($hostinfo)) return array();
$id_list = $hostinfo->id;
if (!empty($CFG->mnet_all_hosts_id)) {
$id_list .= ', '.$CFG->mnet_all_hosts_id;
}
if ('system.listMethods' == $method || 'system/listMethods' == $method) {
$query = '
SELECT DISTINCT
rpc.functionname,
rpc.xmlrpcpath
FROM
{mnet_host2service} h2s
JOIN {mnet_service2rpc} s2r ON h2s.serviceid = s2r.serviceid
JOIN {mnet_rpc} rpc ON s2r.rpcid = rpc.id
JOIN {mnet_service} svc ON svc.id = s2r.serviceid
WHERE
h2s.hostid in ('.$id_list .') AND
h2s.publish = 1 AND rpc.enabled = 1
' . ((count($params) > 0) ? 'AND svc.name = ? ' : '') . '
ORDER BY
rpc.xmlrpcpath ASC';
if (count($params) > 0) {
$params = array($params[0]);
}
$methods = array();
foreach ($DB->get_records_sql($query, $params) as $result) {
$methods[] = $result->xmlrpcpath;
}
return $methods;
} elseif (in_array($method, array('system.methodSignature', 'system/methodSignature', 'system.methodHelp', 'system/methodHelp'))) {
$query = '
SELECT DISTINCT
rpc.functionname,
rpc.help,
rpc.profile
FROM
{mnet_host2service} h2s,
{mnet_service2rpc} s2r,
{mnet_rpc} rpc
WHERE
rpc.xmlrpcpath = ? AND
s2r.rpcid = rpc.id AND
h2s.publish = 1 AND rpc.enabled = 1 AND
h2s.serviceid = s2r.serviceid AND
h2s.hostid in ('.$id_list .')';
$params = array($params[0]);
if (!$result = $DB->get_record_sql($query, $params)) {
return false;
}
if (strpos($method, 'methodSignature') !== false) {
return unserialize($result->profile);
}
return $result->help;
} elseif ('system.listServices' == $method || 'system/listServices' == $method) {
$query = '
SELECT DISTINCT
s.id,
s.name,
s.apiversion,
h2s.publish,
h2s.subscribe
FROM
{mnet_host2service} h2s,
{mnet_service} s
WHERE
h2s.serviceid = s.id AND
(h2s.publish = 1 OR h2s.subscribe = 1) AND
h2s.hostid in ('.$id_list .')
ORDER BY
s.name ASC';
$params = array();
$result = $DB->get_records_sql($query, $params);
$services = array();
if (is_array($result)) {
foreach($result as $service) {
$services[] = array('name' => $service->name,
'apiversion' => $service->apiversion,
'publish' => $service->publish,
'subscribe' => $service->subscribe);
}
}
return $services;
}
throw new mnet_server_exception(7019, 'nosuchfunction');
}
/**
* Invoke a normal style plugin method
* This will verify permissions first.
*
* @param string $method the full xmlrpc method that was called eg auth/mnet/auth.php/user_authorise
* @param array $callstack the exploded callstack
* @param stdclass $rpcrecord the record from mnet_rpc
*
* @return mixed the response from the invoked method
*/
function mnet_server_invoke_plugin_method($method, $callstack, $rpcrecord, $payload) {
mnet_verify_permissions($rpcrecord); // will throw exceptions
mnet_setup_dummy_method($method, $callstack, $rpcrecord);
$methodname = array_pop($callstack);
$xmlrpcserver = new \PhpXmlRpc\Server();
$xmlrpcserver->functions_parameters_type = 'epivals';
$xmlrpcserver->compress_response = false;
$xmlrpcserver->add_to_map($method, 'mnet_server_dummy_method');
$xmlrpcserver->user_data = $methodname;
$response = $xmlrpcserver->service($payload, true);
return $response;
}
/**
* Initialize the object (if necessary), execute the method or function, and
* return the response
*
* @param string $includefile The file that contains the object definition
* @param string $methodname The name of the method to execute
* @param string $method The full path to the method
* @param string $payload The XML-RPC request payload
* @param string $class The name of the class to instantiate (or false)
*
* @throws mnet_server_exception
*
* @return string The XML-RPC response
*/
function mnet_server_invoke_dangerous_method($includefile, $methodname, $method, $payload) {
if (file_exists($CFG->dirroot . $includefile)) {
require_once $CFG->dirroot . $includefile;
// $callprefix matches the rpc convention
// of not having a leading slash
$callprefix = preg_replace('!^/!', '', $includefile);
} else {
throw new mnet_server_exception(705, "nosuchfile");
}
if ($functionname != clean_param($functionname, PARAM_PATH)) {
throw new mnet_server_exception(7012, "nosuchfunction");
}
if (!function_exists($functionname)) {
throw new mnet_server_exception(7012, "nosuchfunction");
}
$xmlrpcserver = new \PhpXmlRpc\Server();
$xmlrpcserver->functions_parameters_type = 'epivals';
$xmlrpcserver->compress_response = false;
$xmlrpcserver->add_to_map($method, 'mnet_server_dummy_method');
$xmlrpcserver->user_data = $methodname;
$response = $xmlrpcserver->service($payload, true);
return $response;
}
/**
* Accepts a public key from a new remote host and returns the public key for
* this host. If 'register all hosts' is turned on, it will bootstrap a record
* for the remote host in the mnet_host table (if it's not already there)
*
* @param string $function XML-RPC requires this but we don't... discard!
* @param array $params Array of parameters
* $params[0] is the remote wwwroot
* $params[1] is the remote public key
* @return string The XML-RPC response
*/
function mnet_keyswap($function, $params) {
global $CFG;
$return = array();
$mnet = get_mnet_environment();
if (!empty($CFG->mnet_register_allhosts)) {
$mnet_peer = new mnet_peer();
list($wwwroot, $pubkey, $application) = $params;
$keyok = $mnet_peer->bootstrap($wwwroot, $pubkey, $application);
if ($keyok) {
$mnet_peer->commit();
}
}
return $mnet->public_key;
}
/**
* Verify that the requested xmlrpc method can be called
* This just checks the method exists in the rpc table and is enabled.
*
* @param stdclass $rpcrecord the record from mnet_rpc
*
* @throws mnet_server_exception
*/
function mnet_verify_permissions($rpcrecord) {
global $CFG, $DB;
$remoteclient = get_mnet_remote_client();
$id_list = $remoteclient->id;
if (!empty($CFG->mnet_all_hosts_id)) {
$id_list .= ', '.$CFG->mnet_all_hosts_id;
}
// TODO: Change this to avoid the first column duplicate debugging, keeping current behaviour 100%.
$sql = "SELECT
r.*, h2s.publish
FROM
{mnet_rpc} r
JOIN {mnet_service2rpc} s2r ON s2r.rpcid = r.id
LEFT JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid
WHERE
r.id = ? AND
h2s.hostid in ($id_list)";
$params = array($rpcrecord->id);
if (!$permission = $DB->get_record_sql($sql, $params)) {
throw new mnet_server_exception(7012, "nosuchfunction");
} else if (!$permission->publish || !$permission->enabled) {
throw new mnet_server_exception(707, "nosuchfunction");
}
}
/**
* Figure out exactly what needs to be called and stashes it in $remoteclient
* Does some further verification that the method is callable
*
* @param string $method the full xmlrpc method that was called eg auth/mnet/auth.php/user_authorise
* @param array $callstack the exploded callstack
* @param stdclass $rpcrecord the record from mnet_rpc
*
* @throws mnet_server_exception
*/
function mnet_setup_dummy_method($method, $callstack, $rpcrecord) {
global $CFG;
$remoteclient = get_mnet_remote_client();
// verify that the callpath in the stack matches our records
// callstack will look like array('mod', 'forum', 'lib.php', 'forum_add_instance');
$path = core_component::get_plugin_directory($rpcrecord->plugintype, $rpcrecord->pluginname);
$path = substr($path, strlen($CFG->dirroot)+1); // this is a bit hacky and fragile, it is not guaranteed that plugins are in dirroot
array_pop($callstack);
$providedpath = implode('/', $callstack);
if ($providedpath != $path . '/' . $rpcrecord->filename) {
throw new mnet_server_exception(705, "nosuchfile");
}
if (!file_exists($CFG->dirroot . '/' . $providedpath)) {
throw new mnet_server_exception(705, "nosuchfile");
}
require_once($CFG->dirroot . '/' . $providedpath);
if (!empty($rpcrecord->classname)) {
if (!class_exists($rpcrecord->classname)) {
throw new mnet_server_exception(708, 'nosuchclass');
}
if (!$rpcrecord->static) {
try {
$object = new $rpcrecord->classname;
} catch (Exception $e) {
throw new mnet_server_exception(709, "classerror");
}
if (!is_callable(array($object, $rpcrecord->functionname))) {
throw new mnet_server_exception(706, "nosuchfunction");
}
$remoteclient->object_to_call($object);
} else {
if (!is_callable(array($rpcrecord->classname, $rpcrecord->functionname))) {
throw new mnet_server_exception(706, "nosuchfunction");
}
$remoteclient->static_location($rpcrecord->classname);
}
}
}
/**
* Dummy function for the XML-RPC dispatcher - use to call a method on an object
* or to call a function
*
* Translate XML-RPC's strange function call syntax into a more straightforward
* PHP-friendly alternative. This dummy function will be called by the
* dispatcher, and can be used to call a method on an object, or just a function
*
* The methodName argument (eg. mnet/testlib/mnet_concatenate_strings)
* is ignored.
*
* @throws mnet_server_exception
*
* @param string $methodname We discard this - see 'functionname'
* @param array $argsarray Each element is an argument to the real
* function
* @param string $functionname The name of the PHP function you want to call
* @return mixed The return value will be that of the real
* function, whatever it may be.
*/
function mnet_server_dummy_method($methodname, $argsarray, $functionname) {
$remoteclient = get_mnet_remote_client();
try {
if (is_object($remoteclient->object_to_call)) {
return @call_user_func_array(array($remoteclient->object_to_call,$functionname), $argsarray);
} else if (!empty($remoteclient->static_location)) {
return @call_user_func_array(array($remoteclient->static_location, $functionname), $argsarray);
} else {
return @call_user_func_array($functionname, $argsarray);
}
} catch (Exception $e) {
exit(mnet_server_fault($e->getCode(), $e->getMessage()));
}
}
/**
* mnet server exception. extends moodle_exception, but takes slightly different arguments.
* and unlike the rest of moodle, the actual int error code is used.
* this exception should only be used during an xmlrpc server request, ie, not for client requests.
*/
class mnet_server_exception extends moodle_exception {
/**
* @param int $intcode the numerical error associated with this fault. this is <b>not</b> the string errorcode
* @param string $langkey the error message in full (<b>get_string will not be used</b>)
* @param string $module the language module, defaults to 'mnet'
* @param mixed $a params for get_string
*/
public function __construct($intcode, $languagekey, $module='mnet', $a=null) {
parent::__construct($languagekey, $module, '', $a);
$this->code = $intcode;
}
}
+357
View File
@@ -0,0 +1,357 @@
<?php
/**
* Custom XML parser for signed and/or encrypted XML Docs
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
/**
* Custom XML parser class for signed and/or encrypted XML Docs
*/
class mnet_encxml_parser {
/** @var resource|false|XMLParser — a resource handle for the new XML parser. */
private $parser;
/** @var int unique ID for each tag. */
private $tag_number;
/** @var string digest string. */
private $digest;
/** @var string remote_timestamp string. */
public $remote_timestamp;
/** @var string remote_wwwroot string. */
public $remote_wwwroot;
/** @var string signature string. */
public $signature;
/** @var string data_object string. */
public $data_object;
/** @var string URI value inside the RETRIEVALMETHOD xml tag. */
private $key_URI;
/** @var bool true if $chiper has a value, otherwise false. */
public $payload_encrypted;
/** @var array the chiper string. */
public $cipher = [];
/** @var array error information with code and string keys. */
public $error = [];
/** @var string The remote error string, specified in the discard_data(). */
public $remoteerror;
/** @var stdClass error started status. */
private $errorstarted;
/**
* Constructor creates and initialises parser resource and calls initialise
*
* @return bool True
*/
public function __construct() {
return $this->initialise();
}
/**
* Old syntax of class constructor. Deprecated in PHP7.
*
* @deprecated since Moodle 3.1
*/
public function mnet_encxml_parser() {
debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
self::__construct();
}
/**
* Set default element handlers and initialise properties to empty.
*
* @return bool True
*/
function initialise() {
$this->parser = xml_parser_create();
xml_set_object($this->parser, $this);
xml_set_element_handler($this->parser, "start_element", "end_element");
xml_set_character_data_handler($this->parser, "discard_data");
$this->tag_number = 0; // Just a unique ID for each tag
$this->digest = '';
$this->remote_timestamp = '';
$this->remote_wwwroot = '';
$this->signature = '';
$this->data_object = '';
$this->key_URI = '';
$this->payload_encrypted = false;
$this->cipher = array();
$this->error = array();
$this->remoteerror = null;
$this->errorstarted = false;
return true;
}
/**
* Parse a block of XML text
*
* The XML Text will be an XML-RPC request which is wrapped in an XML doc
* with a signature from the sender. This envelope may be encrypted and
* delivered within another XML envelope with a symmetric key. The parser
* should first decrypt this XML, and then place the XML-RPC request into
* the data_object property, and the signature into the signature property.
*
* See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing}
* and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing}
* guidelines for more detail on the XML.
*
* -----XML-Envelope---------------------------------
* | |
* | Symmetric-key-------------------------- |
* | |_____________________________________| |
* | |
* | Encrypted data------------------------- |
* | | | |
* | | -XML-Envelope------------------ | |
* | | | | | |
* | | | --Signature------------- | | |
* | | | |______________________| | | |
* | | | | | |
* | | | --Signed-Payload-------- | | |
* | | | | | | | |
* | | | | XML-RPC Request | | | |
* | | | |______________________| | | |
* | | | | | |
* | | |_____________________________| | |
* | |_____________________________________| |
* | |
* |________________________________________________|
*
* @param string $data The XML that you want to parse
* @return bool True on success - false on failure
*/
function parse($data) {
$p = xml_parse($this->parser, $data);
if ($p == 0) {
// Parse failed
$errcode = xml_get_error_code($this->parser);
$errstring = xml_error_string($errcode);
$lineno = xml_get_current_line_number($this->parser);
if ($lineno !== false) {
$error = array('lineno' => $lineno);
$lineno--; // Line numbering starts at 1.
while ($lineno > 0) {
$data = strstr($data, "\n");
$lineno--;
}
$data .= "\n"; // In case there's only one line (no newline)
$line = substr($data, 0, strpos($data, "\n"));
$error['code'] = $errcode;
$error['string'] = $errstring;
$error['line'] = $line;
$this->error[] = $error;
} else {
$this->error[] = array('code' => $errcode, 'string' => $errstring);
}
}
if (!empty($this->remoteerror)) {
return false;
}
if (count($this->cipher) > 0) {
$this->cipher = array_values($this->cipher);
$this->payload_encrypted = true;
}
return (bool)$p;
}
/**
* Destroy the parser and free up any related resource.
*/
function free_resource() {
$free = xml_parser_free($this->parser);
}
/**
* Set the character-data handler to the right function for each element
*
* For each tag (element) name, this function switches the character-data
* handler to the function that handles that element. Note that character
* data is referred to the handler in blocks of 1024 bytes.
*
* @param mixed $parser The XML parser
* @param string $name The name of the tag, e.g. method_call
* @param array $attrs The tag's attributes (if any exist).
* @return bool True
*/
function start_element($parser, $name, $attrs) {
$this->tag_number++;
$handler = 'discard_data';
switch(strtoupper($name)) {
case 'DIGESTVALUE':
$handler = 'parse_digest';
break;
case 'SIGNATUREVALUE':
$handler = 'parse_signature';
break;
case 'OBJECT':
$handler = 'parse_object';
break;
case 'RETRIEVALMETHOD':
$this->key_URI = $attrs['URI'];
break;
case 'TIMESTAMP':
$handler = 'parse_timestamp';
break;
case 'WWWROOT':
$handler = 'parse_wwwroot';
break;
case 'CIPHERVALUE':
$this->cipher[$this->tag_number] = '';
$handler = 'parse_cipher';
break;
case 'FAULT':
$handler = 'parse_fault';
default:
break;
}
xml_set_character_data_handler($this->parser, $handler);
return true;
}
/**
* Add the next chunk of character data to the remote_timestamp string
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function parse_timestamp($parser, $data) {
$this->remote_timestamp .= $data;
return true;
}
/**
* Add the next chunk of character data to the cipher string for that tag
*
* The XML parser calls the character-data handler with 1024-character
* chunks of data. This means that the handler may be called several times
* for a single tag, so we use the concatenate operator (.) to build the
* tag content into a string.
* We should not encounter more than one of each tag type, except for the
* cipher tag. We will often see two of those. We prevent the content of
* these two tags being concatenated together by counting each tag, and
* using its 'number' as the key to an array of ciphers.
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function parse_cipher($parser, $data) {
$this->cipher[$this->tag_number] .= $data;
return true;
}
/**
* Add the next chunk of character data to the remote_wwwroot string
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function parse_wwwroot($parser, $data) {
$this->remote_wwwroot .= $data;
return true;
}
/**
* Add the next chunk of character data to the digest string
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function parse_digest($parser, $data) {
$this->digest .= $data;
return true;
}
/**
* Add the next chunk of character data to the signature string
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function parse_signature($parser, $data) {
$this->signature .= $data;
return true;
}
/**
* Add the next chunk of character data to the data_object string
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function parse_object($parser, $data) {
$this->data_object .= $data;
return true;
}
/**
* Discard the next chunk of character data
*
* This is used for tags that we're not interested in.
*
* @param mixed $parser The XML parser
* @param string $data The content of the current tag (1024 byte chunk)
* @return bool True
*/
function discard_data($parser, $data) {
if (!$this->errorstarted) {
// Not interested
return true;
}
$data = trim($data);
if (isset($this->errorstarted->faultstringstarted) && !empty($data)) {
$this->remoteerror .= ', message: ' . $data;
} else if (isset($this->errorstarted->faultcodestarted)) {
$this->remoteerror = 'code: ' . $data;
unset($this->errorstarted->faultcodestarted);
} else if ($data == 'faultCode') {
$this->errorstarted->faultcodestarted = true;
} else if ($data == 'faultString') {
$this->errorstarted->faultstringstarted = true;
}
return true;
}
function parse_fault($parser, $data) {
$this->errorstarted = new StdClass;
return true;
}
/**
* Switch the character-data handler to ignore the next chunk of data
*
* @param mixed $parser The XML parser
* @param string $name The name of the tag, e.g. method_call
* @return bool True
*/
function end_element($parser, $name) {
$ok = xml_set_character_data_handler($this->parser, "discard_data");
return true;
}
}