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