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