Merge branch 'MDL-36655-m23' of git://github.com/netspotau/moodle-mod_assign into...
[moodle.git] / lib / googleapi.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  * Simple implementation of some Google API functions for Moodle.
19  *
20  * @package   core
21  * @copyright Dan Poltawski <talktodan@gmail.com>
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once($CFG->libdir.'/filelib.php');
28 require_once($CFG->libdir.'/oauthlib.php');
30 /**
31  * Class for manipulating google documents through the google data api.
32  *
33  * Docs for this can be found here:
34  * {@link http://code.google.com/apis/documents/docs/2.0/developers_guide_protocol.html}
35  *
36  * @package    core
37  * @subpackage lib
38  * @copyright Dan Poltawski <talktodan@gmail.com>
39  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class google_docs {
42     /** @var string Realm for authentication, need both docs and spreadsheet realm */
43     const REALM            = 'https://docs.google.com/feeds/ https://spreadsheets.google.com/feeds/ https://docs.googleusercontent.com/';
44     /** @var string Document list url */
45     const DOCUMENTFEED_URL = 'https://docs.google.com/feeds/default/private/full';
46     /** @var string Upload url */
47     const UPLOAD_URL       = 'https://docs.google.com/feeds/upload/create-session/default/private/full?convert=false';
49     /** @var google_oauth oauth curl class for making authenticated requests */
50     private $googleoauth = null;
52     /**
53      * Constructor.
54      *
55      * @param google_oauth $googleoauth oauth curl class for making authenticated requests
56      */
57     public function __construct(google_oauth $googleoauth) {
58         $this->googleoauth = $googleoauth;
59         $this->reset_curl_state();
60     }
62     /**
63      * Resets state on oauth curl object and set GData protocol
64      * version
65      */
66     private function reset_curl_state() {
67         $this->googleoauth->reset_state();
68         $this->googleoauth->setHeader('GData-Version: 3.0');
69     }
71     /**
72      * Returns a list of files the user has formated for files api
73      *
74      * @param string $search A search string to do full text search on the documents
75      * @return mixed Array of files formated for fileapoi
76      */
77     public function get_file_list($search = '') {
78         global $CFG, $OUTPUT;
79         $url = self::DOCUMENTFEED_URL;
81         if ($search) {
82             $url.='?q='.urlencode($search);
83         }
85         $files = array();
86         $content = $this->googleoauth->get($url);
87         try {
88             if (strpos($content, '<?xml') !== 0) {
89                 throw new moodle_exception('invalidxmlresponse');
90             }
91             $xml = new SimpleXMLElement($content);
92         } catch (Exception $e) {
93             // An error occured while trying to parse the XML, let's just return nothing. SimpleXML does not
94             // return a more specific Exception, that's why the global Exception class is caught here.
95             return $files;
96         }
97         foreach ($xml->entry as $gdoc) {
98             $docid  = (string) $gdoc->children('http://schemas.google.com/g/2005')->resourceId;
99             list($type, $docid) = explode(':', $docid);
101             $title  = '';
102             $source = '';
103             // FIXME: We're making hard-coded choices about format here.
104             // If the repo api can support it, we could let the user
105             // chose.
106             switch($type){
107                 case 'document':
108                     $title = $gdoc->title.'.rtf';
109                     $source = 'https://docs.google.com/feeds/download/documents/Export?id='.$docid.'&exportFormat=rtf';
110                     break;
111                 case 'presentation':
112                     $title = $gdoc->title.'.ppt';
113                     $source = 'https://docs.google.com/feeds/download/presentations/Export?id='.$docid.'&exportFormat=ppt';
114                     break;
115                 case 'spreadsheet':
116                     $title = $gdoc->title.'.xls';
117                     $source = 'https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key='.$docid.'&exportFormat=xls';
118                     break;
119                 case 'pdf':
120                 case 'file':
121                     $title  = (string)$gdoc->title;
122                     // Some files don't have a content probably because the download has been restricted.
123                     if (isset($gdoc->content)) {
124                         $source = (string)$gdoc->content[0]->attributes()->src;
125                     }
126                     break;
127             }
129             $files[] =  array( 'title' => $title,
130                 'url' => "{$gdoc->link[0]->attributes()->href}",
131                 'source' => $source,
132                 'date'   => usertime(strtotime($gdoc->updated)),
133                 'thumbnail' => (string) $OUTPUT->pix_url(file_extension_icon($title, 32))
134             );
135         }
137         return $files;
138     }
140     /**
141      * Sends a file object to google documents
142      *
143      * @param object $file File object
144      * @return boolean True on success
145      */
146     public function send_file($file) {
147         // First we create the 'resumable upload request'.
148         $this->googleoauth->setHeader("Content-Length: 0");
149         $this->googleoauth->setHeader("X-Upload-Content-Length: ". $file->get_filesize());
150         $this->googleoauth->setHeader("X-Upload-Content-Type: ". $file->get_mimetype());
151         $this->googleoauth->setHeader("Slug: ". $file->get_filename());
152         $this->googleoauth->post(self::UPLOAD_URL);
154         if ($this->googleoauth->info['http_code'] !== 200) {
155             throw new moodle_exception('Cantpostupload');
156         }
158         // Now we http PUT the file in the location returned.
159         $location = $this->googleoauth->response['Location'];
160         if (empty($location)) {
161             throw new moodle_exception('Nouploadlocation');
162         }
164         // Reset the curl object for actually sending the file.
165         $this->reset_curl_state();
166         $this->googleoauth->setHeader("Content-Length: ". $file->get_filesize());
167         $this->googleoauth->setHeader("Content-Type: ". $file->get_mimetype());
169         // We can't get a filepointer, so have to copy the file..
170         $tmproot = make_temp_directory('googledocsuploads');
171         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
172         $file->copy_content_to($tmpfilepath);
174         // HTTP PUT the file.
175         $this->googleoauth->put($location, array('file'=>$tmpfilepath));
177         // Remove the temporary file we created..
178         unlink($tmpfilepath);
180         if ($this->googleoauth->info['http_code'] === 201) {
181             // Clear headers for further requests.
182             $this->reset_curl_state();
183             return true;
184         } else {
185             return false;
186         }
187     }
189     /**
190      * Downloads a file using authentication
191      *
192      * @param string $url url of file
193      * @param string $path path to save file to
194      * @param int $timeout request timeout, default 0 which means no timeout
195      * @return array stucture for repository download_file
196      */
197     public function download_file($url, $path, $timeout = 0) {
198         $result = $this->googleoauth->download_one($url, null, array('filepath' => $path, 'timeout' => $timeout));
199         if ($result === true) {
200             $info = $this->googleoauth->get_info();
201             if (isset($info['http_code']) && $info['http_code'] == 200) {
202                 return array('path'=>$path, 'url'=>$url);
203             } else {
204                 throw new moodle_exception('cannotdownload', 'repository');
205             }
206         } else {
207             throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
208         }
209     }
212 /**
213  * Class for manipulating picasa through the google data api.
214  *
215  * Docs for this can be found here:
216  * {@link http://code.google.com/apis/picasaweb/developers_guide_protocol.html}
217  *
218  * @package   core
219  * @copyright Dan Poltawski <talktodan@gmail.com>
220  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
221  */
222 class google_picasa {
223     /** @var string Realm for authentication */
224     const REALM             = 'http://picasaweb.google.com/data/';
225     /** @var string Upload url */
226     const UPLOAD_LOCATION   = 'https://picasaweb.google.com/data/feed/api/user/default/albumid/default';
227     /** @var string photo list url */
228     const ALBUM_PHOTO_LIST  = 'https://picasaweb.google.com/data/feed/api/user/default/albumid/';
229     /** @var string search url */
230     const PHOTO_SEARCH_URL  = 'https://picasaweb.google.com/data/feed/api/user/default?kind=photo&q=';
231     /** @var string album list url */
232     const LIST_ALBUMS_URL   = 'https://picasaweb.google.com/data/feed/api/user/default';
233     /** @var string manage files url */
234     const MANAGE_URL        = 'http://picasaweb.google.com/';
236     /** @var google_oauth oauth curl class for making authenticated requests */
237     private $googleoauth = null;
238     /** @var string Last album name retrievied */
239     private $lastalbumname = null;
241     /**
242      * Constructor.
243      *
244      * @param google_oauth $googleoauth oauth curl class for making authenticated requests
245      */
246     public function __construct(google_oauth $googleoauth) {
247         $this->googleoauth = $googleoauth;
248         $this->googleoauth->setHeader('GData-Version: 2');
249     }
251     /**
252      * Sends a file object to picasaweb
253      *
254      * @param object $file File object
255      * @return boolean True on success
256      */
257     public function send_file($file) {
258         $this->googleoauth->setHeader("Content-Length: ". $file->get_filesize());
259         $this->googleoauth->setHeader("Content-Type: ". $file->get_mimetype());
260         $this->googleoauth->setHeader("Slug: ". $file->get_filename());
262         $this->googleoauth->post(self::UPLOAD_LOCATION, $file->get_content());
264         if ($this->googleoauth->info['http_code'] === 201) {
265             return true;
266         } else {
267             return false;
268         }
269     }
271     /**
272      * Returns list of photos for file picker.
273      * If top level then returns list of albums, otherwise
274      * photos within an album.
275      *
276      * @param string $path The path to files (assumed to be albumid)
277      * @return mixed $files A list of files for the file picker
278      */
279     public function get_file_list($path = '') {
280         if (!$path) {
281             return $this->get_albums();
282         } else {
283             return $this->get_album_photos($path);
284         }
285     }
287     /**
288      * Returns list of photos in album specified
289      *
290      * @param int $albumid Photo album to list photos from
291      * @return mixed $files A list of files for the file picker
292      */
293     public function get_album_photos($albumid) {
294         $albumcontent = $this->googleoauth->get(self::ALBUM_PHOTO_LIST.$albumid);
296         return $this->get_photo_details($albumcontent);
297     }
299     /**
300      * Returns the name of the album for which get_photo_details was called last time.
301      *
302      * @return string
303      */
304     public function get_last_album_name() {
305         return $this->lastalbumname;
306     }
308     /**
309      * Does text search on the users photos and returns
310      * matches in format for picasa api
311      *
312      * @param string $query Search terms
313      * @return mixed $files A list of files for the file picker
314      */
315     public function do_photo_search($query) {
316         $content = $this->googleoauth->get(self::PHOTO_SEARCH_URL.htmlentities($query));
318         return $this->get_photo_details($content);
319     }
321     /**
322      * Gets all the users albums and returns them as a list of folders
323      * for the file picker
324      *
325      * @return mixes $files Array in the format get_listing uses for folders
326      */
327     public function get_albums() {
328         $files = array();
329         $content = $this->googleoauth->get(self::LIST_ALBUMS_URL);
331         try {
332             if (strpos($content, '<?xml') !== 0) {
333                 throw new moodle_exception('invalidxmlresponse');
334             }
335             $xml = new SimpleXMLElement($content);
336         } catch (Exception $e) {
337             // An error occured while trying to parse the XML, let's just return nothing. SimpleXML does not
338             // return a more specific Exception, that's why the global Exception class is caught here.
339             return $files;
340         }
342         foreach ($xml->entry as $album) {
343             $gphoto = $album->children('http://schemas.google.com/photos/2007');
345             $mediainfo = $album->children('http://search.yahoo.com/mrss/');
346             // Hacky...
347             $thumbnailinfo = $mediainfo->group->thumbnail[0]->attributes();
349             $files[] = array( 'title' => (string) $album->title,
350                 'date'  => userdate($gphoto->timestamp),
351                 'size'  => (int) $gphoto->bytesUsed,
352                 'path'  => (string) $gphoto->id,
353                 'thumbnail' => (string) $thumbnailinfo['url'],
354                 'thumbnail_width' => 160,  // 160 is the native maximum dimension.
355                 'thumbnail_height' => 160,
356                 'children' => array(),
357             );
358         }
360         return $files;
361     }
363     /**
364      * Recieves XML from a picasa list of photos and returns
365      * array in format for file picker.
366      *
367      * @param string $rawxml XML from picasa api
368      * @return mixed $files A list of files for the file picker
369      */
370     public function get_photo_details($rawxml) {
371         $files = array();
373         try {
374             if (strpos($rawxml, '<?xml') !== 0) {
375                 throw new moodle_exception('invalidxmlresponse');
376             }
377             $xml = new SimpleXMLElement($rawxml);
378         } catch (Exception $e) {
379             // An error occured while trying to parse the XML, let's just return nothing. SimpleXML does not
380             // return a more specific Exception, that's why the global Exception class is caught here.
381             return $files;
382         }
383         $this->lastalbumname = (string)$xml->title;
385         foreach ($xml->entry as $photo) {
386             $gphoto = $photo->children('http://schemas.google.com/photos/2007');
388             $mediainfo = $photo->children('http://search.yahoo.com/mrss/');
389             $fullinfo = $mediainfo->group->content->attributes();
390             // Hacky...
391             $thumbnailinfo = $mediainfo->group->thumbnail[0]->attributes();
393             // Derive the nicest file name we can.
394             if (!empty($mediainfo->group->description)) {
395                 $title = shorten_text((string)$mediainfo->group->description, 20, false, '');
396                 $title = clean_filename($title).'.jpg';
397             } else {
398                 $title = (string)$mediainfo->group->title;
399             }
401             $files[] = array(
402                 'title' => $title,
403                 'date'  => userdate($gphoto->timestamp),
404                 'size' => (int) $gphoto->size,
405                 'path' => $gphoto->albumid.'/'.$gphoto->id,
406                 'thumbnail' => (string) $thumbnailinfo['url'],
407                 'thumbnail_width' => 72,  // 72 is the native maximum dimension.
408                 'thumbnail_height' => 72,
409                 'source' => (string) $fullinfo['url'],
410                 'url' => (string) $fullinfo['url']
411             );
412         }
414         return $files;
415     }
418 /**
419  * OAuth 2.0 client for Google Services
420  *
421  * @package   core
422  * @copyright 2012 Dan Poltawski
423  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
424  */
425 class google_oauth extends oauth2_client {
426     /**
427      * Returns the auth url for OAuth 2.0 request
428      * @return string the auth url
429      */
430     protected function auth_url() {
431         return 'https://accounts.google.com/o/oauth2/auth';
432     }
434     /**
435      * Returns the token url for OAuth 2.0 request
436      * @return string the auth url
437      */
438     protected function token_url() {
439         return 'https://accounts.google.com/o/oauth2/token';
440     }
442     /**
443      * Resets headers and response for multiple requests
444      */
445     public function reset_state() {
446         $this->header = array();
447         $this->response = array();
448     }