Merge branch 'wip-MDL-42089-master' of git://github.com/abgreeve/moodle
[moodle.git] / repository / equella / 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/>.
17 /**
18  * This plugin is used to access equella repositories.
19  *
20  * @since 2.3
21  * @package    repository_equella
22  * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/repository/lib.php');
30 /**
31  * repository_equella class implements equella_client
32  *
33  * @since 2.3
34  * @package    repository_equella
35  * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class repository_equella extends repository {
39     /** @var array mimetype filter */
40     private $mimetypes = array();
42     /**
43      * Constructor
44      *
45      * @param int $repositoryid repository instance id
46      * @param int|stdClass $context a context id or context object
47      * @param array $options repository options
48      */
49     public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
50         parent::__construct($repositoryid, $context, $options);
52         if (isset($this->options['mimetypes'])) {
53             $mt = $this->options['mimetypes'];
54             if (!empty($mt) && is_array($mt) && !in_array('*', $mt)) {
55                 $this->mimetypes = array_unique(array_map(array($this, 'to_mime_type'), $mt));
56             }
57         }
58     }
60     /**
61      * Display embedded equella interface
62      *
63      * @param string $path
64      * @param mixed $page
65      * @return array
66      */
67     public function get_listing($path = null, $page = null) {
68         global $COURSE;
69         $callbackurl = new moodle_url('/repository/equella/callback.php', array('repo_id'=>$this->id));
71         $mimetypesstr = '';
72         $restrict = '';
73         if (!empty($this->mimetypes)) {
74             $mimetypesstr = '&mimeTypes=' . implode(',', $this->mimetypes);
75             // We're restricting to a mime type, so we always restrict to selecting resources only.
76             $restrict = '&attachmentonly=true';
77         } else if ($this->get_option('equella_select_restriction') != 'none') {
78             // The option value matches the EQUELLA paramter name.
79             $restrict = '&' . $this->get_option('equella_select_restriction') . '=true';
80         }
82         $url = $this->get_option('equella_url')
83                 . '?method=lms'
84                 . '&returnurl='.urlencode($callbackurl)
85                 . '&returnprefix=tle'
86                 . '&template=standard'
87                 . '&token='.urlencode($this->getssotoken('write'))
88                 . '&courseId='.urlencode($COURSE->idnumber)
89                 . '&courseCode='.urlencode($COURSE->shortname)
90                 . '&action=searchThin'
91                 . '&forcePost=true'
92                 . '&cancelDisabled=true'
93                 . '&attachmentUuidUrls=true'
94                 . '&options='.urlencode($this->get_option('equella_options') . $mimetypesstr)
95                 . $restrict;
97         $manageurl = $this->get_option('equella_url');
98         $manageurl = str_ireplace('signon.do', 'logon.do', $manageurl);
99         $manageurl = $this->appendtoken($manageurl);
101         $list = array();
102         $list['object'] = array();
103         $list['object']['type'] = 'text/html';
104         $list['object']['src'] = $url;
105         $list['nologin']  = true;
106         $list['nosearch'] = true;
107         $list['norefresh'] = true;
108         $list['manage'] = $manageurl;
109         return $list;
110     }
112     /**
113      * Supported equella file types
114      *
115      * @return int
116      */
117     public function supported_returntypes() {
118         return FILE_REFERENCE;
119     }
121     /**
122      * Prepare file reference information
123      *
124      * @param string $source
125      * @return string file referece
126      */
127     public function get_file_reference($source) {
128         return $source;
129     }
131     /**
132      * Counts the number of failed connections.
133      *
134      * If we received the connection timeout more than 3 times in a row, we don't attemt to
135      * connect to the server any more during this request.
136      *
137      * This function is used by {@link repository_equella::get_file_by_reference()} that
138      * synchronises the file size of referenced files.
139      *
140      * @param int $errno omit if we just want to know the return value, the last curl_errno otherwise
141      * @return bool true if we had less than 3 failed connections, false if no more connections
142      * attempts recommended
143      */
144     private function connection_result($errno = null) {
145         static $countfailures = array();
146         $sess = sesskey();
147         if (!array_key_exists($sess, $countfailures)) {
148             $countfailures[$sess] = 0;
149         }
150         if ($errno !== null) {
151             if ($errno == 0) {
152                 // reset count of failed connections
153                 $countfailures[$sess] = 0;
154             } else if ($errno == 7 /*CURLE_COULDNT_CONNECT*/ || $errno == 9 /*CURLE_REMOTE_ACCESS_DENIED*/) {
155                 // problems with server
156                 $countfailures[$sess]++;
157             }
158         }
159         return ($countfailures[$sess] < 3);
160     }
162     /**
163      * Decide whether or not the file should be synced
164      *
165      * @param stored_file $storedfile
166      * @return bool
167      */
168     public function sync_individual_file(stored_file $storedfile) {
169         // if we had several unsuccessfull attempts to connect to server - do not try any more
170         return $this->connection_result();
171     }
174     /**
175      * Download a file, this function can be overridden by subclass. {@link curl}
176      *
177      * @param string $reference the source of the file
178      * @param string $filename filename (without path) to save the downloaded file in the
179      * temporary directory
180      * @return null|array null if download failed or array with elements:
181      *   path: internal location of the file
182      *   url: URL to the source (from parameters)
183      */
184     public function get_file($reference, $filename = '') {
185         global $USER;
186         $ref = @unserialize(base64_decode($reference));
187         if (!isset($ref->url) || !($url = $this->appendtoken($ref->url))) {
188             // Occurs when the user isn't known..
189             return null;
190         }
191         $path = $this->prepare_file($filename);
192         $cookiepathname = $this->prepare_file($USER->id. '_'. uniqid('', true). '.cookie');
193         $c = new curl(array('cookie'=>$cookiepathname));
194         $result = $c->download_one($url, null, array('filepath' => $path, 'followlocation' => true, 'timeout' => self::GETFILE_TIMEOUT));
195         // Delete cookie jar.
196         if (file_exists($cookiepathname)) {
197             unlink($cookiepathname);
198         }
199         if ($result !== true) {
200             throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
201         }
202         return array('path'=>$path, 'url'=>$url);
203     }
205     /**
206      * Returns information about file in this repository by reference
207      *
208      * If the file is an image we download the contents and save it in our filesystem
209      * so we can generate thumbnails. Otherwise we just request the file size.
210      * Returns null if file not found or can not be accessed
211      *
212      * @param stdClass $reference file reference db record
213      * @return stdClass|null contains one of the following:
214      *   - 'filesize' (for non-image files or files we failed to retrieve fully because of timeout)
215      *   - 'filepath' (for image files that we retrieived and saved)
216      */
217     public function get_file_by_reference($reference) {
218         global $USER;
219         $ref = @unserialize(base64_decode($reference->reference));
220         if (!isset($ref->url) || !($url = $this->appendtoken($ref->url))) {
221             // Occurs when the user isn't known..
222             return null;
223         }
225         $return = null;
226         $cookiepathname = $this->prepare_file($USER->id. '_'. uniqid('', true). '.cookie');
227         $c = new curl(array('cookie' => $cookiepathname));
228         if (file_extension_in_typegroup($ref->filename, 'web_image')) {
229             $path = $this->prepare_file('');
230             $result = $c->download_one($url, null, array('filepath' => $path, 'followlocation' => true, 'timeout' => self::SYNCIMAGE_TIMEOUT));
231             if ($result === true) {
232                 $return = (object)array('filepath' => $path);
233             }
234         } else {
235             $result = $c->head($url, array('followlocation' => true, 'timeout' => self::SYNCFILE_TIMEOUT));
236         }
237         // Delete cookie jar.
238         if (file_exists($cookiepathname)) {
239             unlink($cookiepathname);
240         }
242         $this->connection_result($c->get_errno());
243         $curlinfo = $c->get_info();
244         if ($return === null && isset($curlinfo['http_code']) && $curlinfo['http_code'] == 200
245                 && array_key_exists('download_content_length', $curlinfo)
246                 && $curlinfo['download_content_length'] >= 0) {
247             // we received a correct header and at least can tell the file size
248             $return = (object)array('filesize' => $curlinfo['download_content_length']);
249         }
250         return $return;
251     }
253     /**
254      * Repository method to serve the referenced file
255      *
256      * @param stored_file $storedfile the file that contains the reference
257      * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours)
258      * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
259      * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
260      * @param array $options additional options affecting the file serving
261      */
262     public function send_file($stored_file, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) {
263         $reference  = unserialize(base64_decode($stored_file->get_reference()));
264         $url = $this->appendtoken($reference->url);
265         if ($url) {
266             header('Location: ' . $url);
267         } else {
268             send_file_not_found();
269         }
270     }
272     /**
273      * Add Instance settings input to Moodle form
274      *
275      * @param moodleform $mform
276      */
277     public static function instance_config_form($mform) {
278         $mform->addElement('text', 'equella_url', get_string('equellaurl', 'repository_equella'));
279         $mform->setType('equella_url', PARAM_URL);
281         $strrequired = get_string('required');
282         $mform->addRule('equella_url', $strrequired, 'required', null, 'client');
284         $mform->addElement('text', 'equella_options', get_string('equellaoptions', 'repository_equella'));
285         $mform->setType('equella_options', PARAM_NOTAGS);
287         $choices = array(
288             'none' => get_string('restrictionnone', 'repository_equella'),
289             'itemonly' => get_string('restrictionitemsonly', 'repository_equella'),
290             'attachmentonly' => get_string('restrictionattachmentsonly', 'repository_equella'),
291         );
292         $mform->addElement('select', 'equella_select_restriction', get_string('selectrestriction', 'repository_equella'), $choices);
294         $mform->addElement('header', 'groupheader',
295             get_string('group', 'repository_equella', get_string('groupdefault', 'repository_equella')));
296         $mform->addElement('text', 'equella_shareid', get_string('sharedid', 'repository_equella'));
297         $mform->setType('equella_shareid', PARAM_RAW);
298         $mform->addRule('equella_shareid', $strrequired, 'required', null, 'client');
300         $mform->addElement('text', 'equella_sharedsecret', get_string('sharedsecrets', 'repository_equella'));
301         $mform->setType('equella_sharedsecret', PARAM_RAW);
302         $mform->addRule('equella_sharedsecret', $strrequired, 'required', null, 'client');
304         foreach (self::get_all_editing_roles() as $role) {
305             $mform->addElement('header', 'groupheader_'.$role->shortname, get_string('group', 'repository_equella',
306                 format_string($role->name)));
307             $mform->addElement('text', "equella_{$role->shortname}_shareid", get_string('sharedid', 'repository_equella'));
308             $mform->setType("equella_{$role->shortname}_shareid", PARAM_RAW);
309             $mform->addElement('text', "equella_{$role->shortname}_sharedsecret",
310                 get_string('sharedsecrets', 'repository_equella'));
311             $mform->setType("equella_{$role->shortname}_sharedsecret", PARAM_RAW);
312         }
313     }
315     /**
316      * Names of the instance settings
317      *
318      * @return array
319      */
320     public static function get_instance_option_names() {
321         $rv = array('equella_url', 'equella_select_restriction', 'equella_options',
322             'equella_shareid', 'equella_sharedsecret'
323         );
325         foreach (self::get_all_editing_roles() as $role) {
326             array_push($rv, "equella_{$role->shortname}_shareid");
327             array_push($rv, "equella_{$role->shortname}_sharedsecret");
328         }
330         return $rv;
331     }
333     /**
334      * Generate equella token
335      *
336      * @param string $username
337      * @param string $shareid
338      * @param string $sharedsecret
339      * @return string
340      */
341     private static function getssotoken_raw($username, $shareid, $sharedsecret) {
342         $time = time() . '000';
343         return urlencode($username)
344             . ':'
345             . $shareid
346             . ':'
347             . $time
348             . ':'
349             . base64_encode(pack('H*', md5($username . $shareid . $time . $sharedsecret)));
350     }
352     /**
353      * Append token
354      *
355      * @param string $url
356      * @param $readwrite
357      * @return string
358      */
359     private function appendtoken($url, $readwrite = null) {
360         $ssotoken = $this->getssotoken($readwrite);
361         if (!$ssotoken) {
362             return false;
363         }
364         return $url . (strpos($url, '?') != false ? '&' : '?') . 'token=' . urlencode($ssotoken);
365     }
367     /**
368      * Generate equella sso token
369      *
370      * @param string $readwrite
371      * @return string
372      */
373     private function getssotoken($readwrite = 'read') {
374         global $USER;
376         if (empty($USER->username)) {
377             return false;
378         }
380         if ($readwrite == 'write') {
382             foreach (self::get_all_editing_roles() as $role) {
383                 if (user_has_role_assignment($USER->id, $role->id, $this->context->id)) {
384                     // See if the user has a role that is linked to an equella role.
385                     $shareid = $this->get_option("equella_{$role->shortname}_shareid");
386                     if (!empty($shareid)) {
387                         return $this->getssotoken_raw($USER->username, $shareid,
388                             $this->get_option("equella_{$role->shortname}_sharedsecret"));
389                     }
390                 }
391             }
392         }
393         // If we are only reading, use the unadorned shareid and secret.
394         $shareid = $this->get_option('equella_shareid');
395         if (!empty($shareid)) {
396             return $this->getssotoken_raw($USER->username, $shareid, $this->get_option('equella_sharedsecret'));
397         }
398     }
400     private static function get_all_editing_roles() {
401         return get_roles_with_capability('moodle/course:manageactivities', CAP_ALLOW);
402     }
404     /**
405      * Convert moodle mimetypes list to equella format
406      *
407      * @param string $value
408      * @return string
409      */
410     private static function to_mime_type($value) {
411         return mimeinfo('type', $value);
412     }
414     /**
415      * Return the source information
416      *
417      * @param stdClass $url
418      * @return string|null
419      */
420     public function get_file_source_info($url) {
421         $ref = unserialize(base64_decode($url));
422         return 'EQUELLA: ' . $ref->filename;
423     }
425     /**
426      * Return human readable reference information
427      * {@link stored_file::get_reference()}
428      *
429      * @param string $reference
430      * @param int $filestatus status of the file, 0 - ok, 666 - source missing
431      * @return string
432      */
433     public function get_reference_details($reference, $filestatus = 0) {
434         if (!$filestatus) {
435             $ref = unserialize(base64_decode($reference));
436             return $this->get_name(). ': '. $ref->filename;
437         } else {
438             return get_string('lostsource', 'repository', '');
439         }
440     }
442     /**
443      * Is this repository accessing private data?
444      *
445      * @return bool
446      */
447     public function contains_private_data() {
448         return false;
449     }