mnet MDL-21261 large overhaul. This commit changes:
[moodle.git] / portfolio / mahara / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * This file contains the class definition for the mahara portfolio plugin
20  *
21  * @since 2.0
22  * @package moodlecore
23  * @subpackage portfolio
24  * @copyright 2009 Penny Leach
25  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
29 define('PORTFOLIO_MAHARA_ERR_NETWORKING_OFF', 'err_networkingoff');
30 define('PORTFOLIO_MAHARA_ERR_NOHOSTS', 'err_nomnethosts');
31 define('PORTFOLIO_MAHARA_ERR_INVALIDHOST', 'err_invalidhost');
32 define('PORTFOLIO_MAHARA_ERR_NOMNETAUTH', 'err_nomnetauth');
34 require_once($CFG->libdir . '/portfoliolib.php');
35 require_once($CFG->libdir . '/portfolio/plugin.php');
36 require_once($CFG->libdir . '/portfolio/exporter.php');
37 require_once($CFG->dirroot . '/mnet/lib.php');
39 define('PORTFOLIO_MAHARA_QUEUE', PORTFOLIO_TIME_HIGH);
40 define('PORTFOLIO_MAHARA_IMMEDIATE', PORTFOLIO_TIME_MODERATE);
42 class portfolio_plugin_mahara extends portfolio_plugin_pull_base {
44     private $hosts; // used in the admin config form
45     private $mnethost; // privately set during export from the admin config value (mnethostid)
46     private $hostrecord; // the host record that corresponds to the peer
47     private $token; // during-transfer token
48     private $sendtype; // whatever mahara has said it can handle (immediate or queued)
49     private $filesmanifest; // manifest of files to send to mahara (set during prepare_package and sent later)
50     private $totalsize; // total size of all included files added together
51     private $continueurl; // if we've been sent back a specific url to continue to (eg folder id)
53     public static function get_name() {
54         return get_string('pluginname', 'portfolio_mahara');
55     }
57     public static function get_allowed_config() {
58         return array('mnethostid');
59     }
61     public static function supported_formats() {
62         return array(PORTFOLIO_FORMAT_FILE);
63         // TODO remove above line once leap over mnet is tested
64         return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
65     }
67     public function expected_time($callertime) {
68         if ($this->sendtype == PORTFOLIO_MAHARA_QUEUE) {
69             return PORTFOLIO_TIME_FORCEQUEUE;
70         }
71         return $callertime;
72     }
74     public static function has_admin_config() {
75         return true;
76     }
78     public function admin_config_form(&$mform) {
79         $strrequired = get_string('required');
80         $hosts = self::get_mnet_hosts(); // this is called by sanity check but it's ok because it's cached
81         foreach ($hosts as $host) {
82             $hosts[$host->id] = $host->name;
83         }
84         $mform->addElement('select', 'mnethostid', get_string('mnethost', 'portfolio_mahara'), $hosts);
85         $mform->addRule('mnethostid', $strrequired, 'required', null, 'client');
86     }
88     public function instance_sanity_check() {
89         // make sure the host record exists since we don't have referential integrity
90         if (!is_enabled_auth('mnet')) {
91             return PORTFOLIO_MAHARA_ERR_NOMNETAUTH;
92         }
93         try {
94             $this->ensure_mnethost();
95         }
96         catch (portfolio_exception $e) {
97             return PORTFOLIO_MAHARA_ERR_INVALIDHOST;
98         }
99         // make sure we have the right services
100         $hosts = $this->get_mnet_hosts();
101         if (!array_key_exists($this->get_config('mnethostid'), $hosts)) {
102             return PORTFOLIO_MAHARA_ERR_INVALIDHOST;
103         }
104         return 0;
105     }
107     public static function plugin_sanity_check() {
108         global $CFG, $DB;
109         $errorcode = 0;
110         if (!isset($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode != 'strict') {
111             $errorcode =  PORTFOLIO_MAHARA_ERR_NETWORKING_OFF;
112         }
113         if (!is_enabled_auth('mnet')) {
114             $errorcode = PORTFOLIO_MAHARA_ERR_NOMNETAUTH;
115         }
116         if (!self::get_mnet_hosts()) {
117             $errorcode =  PORTFOLIO_MAHARA_ERR_NOHOSTS;
118         }
119         return $errorcode;
120     }
122     private static function get_mnet_hosts() {
123         global $DB, $CFG;
124         static $hosts;
125         if (isset($this) && is_object($this) && isset($this->hosts)) {
126             return $this->hosts;
127         } else if (!isset($this) && isset($hosts)) {
128             return $hosts;
129         }
130         $hosts = $DB->get_records_sql('  SELECT
131                                     h.id,
132                                     h.wwwroot,
133                                     h.ip_address,
134                                     h.name,
135                                     h.public_key,
136                                     h.public_key_expires,
137                                     h.transport,
138                                     h.portno,
139                                     h.last_connect_time,
140                                     h.last_log_id,
141                                     h.applicationid,
142                                     a.name as app_name,
143                                     a.display_name as app_display_name,
144                                     a.xmlrpc_server_url
145                                 FROM {mnet_host} h
146                                     JOIN {mnet_application} a ON h.applicationid=a.id
147                                     JOIN {mnet_host2service} hs1 ON hs1.hostid = h.id
148                                     JOIN {mnet_service} s1 ON hs1.serviceid = s1.id
149                                     JOIN {mnet_host2service} hs2 ON hs2.hostid = h.id
150                                     JOIN {mnet_service} s2 ON hs2.serviceid = s2.id
151                                     JOIN {mnet_host2service} hs3 ON hs3.hostid = h.id
152                                     JOIN {mnet_service} s3 ON hs3.serviceid = s3.id
153                                 WHERE
154                                     h.id <> ? AND
155                                     h.deleted = 0 AND
156                                     a.name = ? AND
157                                     s1.name = ? AND hs1.publish = ? AND
158                                     s2.name = ? AND hs2.subscribe = ? AND
159                                     s3.name = ? AND hs3.subscribe = ? AND
160                                     s3.name = ? AND hs3.publish = ?',
161                         array($CFG->mnet_localhost_id, 'mahara', 'sso_idp', 1, 'sso_sp', 1, 'pf', 1, 'pf', 1));
162         if (empty($hosts)) { $hosts = array(); }
163         if (isset($this) && is_object($this)) {
164             $this->hosts = $hosts;
165         }
166         return $hosts;
167     }
169     public function prepare_package() {
170         $files = $this->exporter->get_tempfiles();
171         $this->totalsize = 0;
172         foreach ($files as $f) {
173             $this->filesmanifest[$f->get_contenthash()] = array(
174                 'filename' => $f->get_filename(),
175                 'sha1'     => $f->get_contenthash(),
176                 'size'     => $f->get_filesize(),
177             );
178             $this->totalsize += $f->get_filesize();
179         }
181         $this->set('file', $this->exporter->zip_tempfiles());  // this will throw a file_exception which the exporter catches separately.
182     }
184     private function ensure_environment() {
185         global $MNET;
186         if (empty($MNET)) {
187             $MNET = new mnet_environment();
188             $MNET->init();
189         } // no idea why this happens :(
190     }
192     public function send_package() {
193         global $CFG;
194         $this->ensure_environment();
195         // send the 'content_ready' request to mahara
196         require_once($CFG->dirroot . '/mnet/xmlrpc/client.php');
197         $client = new mnet_xmlrpc_client();
198         $client->set_method('portfolio/mahara/lib.php/send_content_ready');
199         $client->add_param($this->token);
200         $client->add_param($this->get('user')->username);
201         $client->add_param($this->resolve_format());
202         $client->add_param(array(
203             'filesmanifest' => $this->filesmanifest,
204             'zipfilesha1'   => $this->get('file')->get_contenthash(),
205             'zipfilesize'   => $this->get('file')->get_filesize(),
206             'totalsize'     => $this->totalsize,
207         ));
208         $client->add_param($this->get_export_config('wait'));
209         $this->ensure_mnethost();
210         if (!$client->send($this->mnethost)) {
211             foreach ($client->error as $errormessage) {
212                 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
213                 $message .= "ERROR $code:<br/>$errormessage<br/>";
214             }
215             throw new portfolio_export_exception($this->get('exporter'), 'failedtoping', 'portfolio_mahara', '', $message);
216         }
217         // we should get back...  an ok and a status
218         // either we've been waiting a while and mahara has fetched the file or has queued it.
219         $response = (object)$client->response;
220         if (!$response->status) {
221             throw new portfolio_export_exception($this->get('exporter'), 'failedtoping', 'portfolio_mahara');
222         }
223         if ($response->type =='queued') {
224             $this->exporter->set_forcequeue();
225         }
226         if (isset($response->querystring)) {
227             $this->continueurl = $response->querystring;
228         }
229         // if we're not queuing the logging might have already happened
230         $this->exporter->update_log_url($this->get_static_continue_url());
231     }
233     public function get_static_continue_url() {
234         $remoteurl = '/artefact/file/';// @todo penny this might change later when we change formats.
235         if (isset($this->continueurl)) {
236             $remoteurl .= $this->continueurl;
237         }
238         return $remoteurl;
239     }
241     public function resolve_static_continue_url($remoteurl) {
242         static $sessions = array();
243         // if this is called mutliple times for the same host, stuff breaks
244         // so we have to keep track and just replace the wantsurl bit
245         // in case things go to different plugins or whatever
246         if (array_key_exists($this->get_config('mnethostid'), $sessions)) {
247             return preg_replace('/wantsurl=[^&]*&/', 'wantsurl=' . urlencode($remoteurl) . '&', $sessions[$this->get_config('mnethostid')]);
248         }
249         $this->ensure_mnethost();
250         $this->ensure_environment();
251         $mnetauth = get_auth_plugin('mnet');
252         if (!$url = $mnetauth->start_jump_session($this->get_config('mnethostid'), $remoteurl)) {
253             return false;
254         }
255         $sessions[$this->get_config('mnethostid')] = $url;
256         return $url;
257     }
259     public function get_interactive_continue_url() {
260         return $this->resolve_static_continue_url($this->get_static_continue_url());
261     }
263     public function steal_control($stage) {
264         if ($stage != PORTFOLIO_STAGE_CONFIG) {
265             return false;
266         }
267         global $CFG;
268         return $CFG->wwwroot . '/portfolio/mahara/preconfig.php?id=' . $this->exporter->get('id');
269     }
271     public function verify_file_request_params($params) {
272         return false;
273         // the data comes from an xmlrpc request,
274         // not a request to file.php
275     }
277     /**
278     * sends the 'content_intent' ping to mahara
279     * if all goes well, this will set the 'token' and 'sendtype' member variables.
280     */
281     public function send_intent() {
282         global $CFG, $DB;
283         require_once($CFG->dirroot . '/mnet/xmlrpc/client.php');
284         $client = new mnet_xmlrpc_client();
285         $client->set_method('portfolio/mahara/lib.php/send_content_intent');
286         $client->add_param($this->get('user')->username);
287         $this->ensure_mnethost();
288         if (!$client->send($this->mnethost)) {
289             foreach ($client->error as $errormessage) {
290                 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
291                 $message .= "ERROR $code:<br/>$errormessage<br/>";
292             }
293             throw new portfolio_export_exception($this->get('exporter'), 'failedtoping', 'portfolio_mahara', '', $message);
294         }
295         // we should get back... the send type and a shared token
296         $response = (object)$client->response;
297         if (empty($response->sendtype) || empty($response->token)) {
298             throw new portfolio_export_exception($this->get('exporter'), 'senddisallowed', 'portfolio_mahara');
299         }
300         switch ($response->sendtype) {
301             case 'immediate':
302                 $this->sendtype = PORTFOLIO_MAHARA_IMMEDIATE;
303                 break;
304             case 'queue':
305                 $this->sendtype = PORTFOLIO_MAHARA_QUEUE;
306                 break;
307             case 'none':
308             default:
309                 throw new portfolio_export_exception($this->get('exporter'), 'senddisallowed', 'portfolio_mahara');
310         }
311         $this->token = $response->token;
312         $this->get('exporter')->save();
313         // put the entry in the mahara queue table now too
314         $q = new stdClass;
315         $q->token = $this->token;
316         $q->transferid = $this->get('exporter')->get('id');
317         $DB->insert_record('portfolio_mahara_queue', $q);
318     }
320     private function ensure_mnethost() {
321         if (!empty($this->hostrecord) && !empty($this->mnethost)) {
322             return;
323         }
324         global $DB;
325         if (!$this->hostrecord = $DB->get_record('mnet_host', array('id' => $this->get_config('mnethostid')))) {
326             throw new portfolio_plugin_exception(PORTFOLIO_MAHARA_ERR_INVALIDHOST, 'portfolio_mahara');
327         }
328         $this->mnethost = new mnet_peer();
329         $this->mnethost->set_wwwroot($this->hostrecord->wwwroot);
330     }
332     /**
333     * xmlrpc (mnet) function to get the file.
334     * reads in the file and returns it base_64 encoded
335     * so that it can be enrypted by mnet.
336     *
337     * @param string $token the token recieved previously during send_content_intent
338     */
339     public static function fetch_file($token) {
340         global $DB, $MNET_REMOTE_CLIENT;;
341         try {
342             if (!$transferid = $DB->get_field('portfolio_mahara_queue', 'transferid', array('token' => $token))) {
343                 throw new mnet_server_exception(8009, get_string('mnet_notoken', 'portfolio_mahara'));
344             }
345             $exporter = portfolio_exporter::rewaken_object($transferid);
346         } catch (portfolio_exception $e) {
347             throw new mnet_server_exception(8010, get_string('mnet_noid', 'portfolio_mahara'));
348         }
349         if ($exporter->get('instance')->get_config('mnethostid') != $MNET_REMOTE_CLIENT->id) {
350             throw new mnet_server_exception(8011, get_string('mnet_wronghost', 'portfolio_mahara'));
351         }
352         global $CFG;
353         try {
354             $i = $exporter->get('instance');
355             $f = $i->get('file');
356             if (empty($f) || !($f instanceof stored_file)) {
357                 throw new mnet_server_exception(8012, get_string('mnet_nofile', 'portfolio_mahara'));
358             }
359             try {
360                 $c = $f->get_content();
361             } catch (file_exception $e) {
362                 throw new mnet_server_exception(8013, get_string('mnet_nofilecontents', 'portfolio_mahara', $e->getMessage()));
363             }
364             $contents = base64_encode($c);
365         } catch (Exception $e) {
366             throw new mnet_server_exception(8013, get_string('mnet_nofile', 'portfolio_mahara'));
367         }
368         $exporter->log_transfer();
369         $exporter->process_stage_cleanup(true);
370         return $contents;
371     }
373     public function cleanup() {
374         global $DB;
375         $DB->delete_records('portfolio_mahara_queue', array('transferid' => $this->get('exporter')->get('id'), 'token' => $this->token));
376     }
379     /**
380      * internal helper function, that converts between the format constant,
381      * which might be too specific (eg 'image') and the class in our *supported* list
382      * which might be higher up the format hierarchy tree (eg 'file')
383      */
384     private function resolve_format() {
385         global $CFG;
386         $thisformat = $this->get_export_config('format');
387         $allformats = portfolio_supported_formats();
388         require_once($CFG->libdir . '/portfolio/formats.php');
389         $thisobj = new $allformats[$thisformat];
390         foreach ($this->supported_formats() as $f) {
391             $class = $allformats[$f];
392             if ($thisobj instanceof $class) {
393                 return $f;
394             }
395         }
396     }