MDL-37217 repository: Google Docs repository uses Google SDK
authorFrederic Massart <fred@moodle.com>
Tue, 8 Jan 2013 09:11:40 +0000 (17:11 +0800)
committerFred <fmcell@gmail.com>
Tue, 19 Feb 2013 05:36:23 +0000 (13:36 +0800)
lib/google/curlio.php
lib/google/local_config.php
repository/googledocs/lang/en/repository_googledocs.php
repository/googledocs/lib.php
repository/googledocs/version.php
version.php

index d9b4503..654b9de 100644 (file)
@@ -28,10 +28,10 @@ require_once($CFG->libdir . '/google/io/Google_IO.php');
 
 /**
  * Class moodle_google_curlio.
- * 
+ *
  * The initial purpose of this class is to add support for our
  * class curl in Google_CurlIO.
- * 
+ *
  * @package core_google
  * @copyright 2013 Frédéric Massart
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -125,31 +125,31 @@ class moodle_google_curlio extends Google_CurlIO {
             'CURLOPT_USERAGENT' => $request->getUserAgent()
         ));
 
-        $respData = $this->do_request($curl, $request);
+        $respdata = $this->do_request($curl, $request);
 
         // Retry if certificates are missing.
         if ($curl->get_errno() == CURLE_SSL_CACERT) {
             error_log('SSL certificate problem, verify that the CA cert is OK.' .
                     ' Retrying with the CA cert bundle from google-api-php-client.');
             $curl->setopt(array('CURLOPT_CAINFO' => dirname(__FILE__) . '/io/cacerts.pem'));
-            $respData = $this->do_request($curl, $request);
+            $respdata = $this->do_request($curl, $request);
         }
 
         $infos = $curl->get_info();
-        $respHeaderSize = $infos['header_size'];
-        $respHttpCode = (int) $infos['http_code'];
-        $curlErrorNum = $curl->get_errno();
-        $curlError = $curl->error;
+        $respheadersize = $infos['header_size'];
+        $resphttpcode = (int) $infos['http_code'];
+        $curlerrornum = $curl->get_errno();
+        $curlerror = $curl->error;
 
-        if ($curlErrorNum != CURLE_OK) {
-          throw new Google_IOException("HTTP Error: ($respHttpCode) $curlError");
+        if ($curlerrornum != CURLE_OK) {
+          throw new Google_IOException("HTTP Error: ($resphttpcode) $curlerror");
         }
 
         // Parse out the raw response into usable bits.
-        list($responseHeaders, $responseBody) = self::parseHttpResponse($respData, $respHeaderSize);
+        list($responseHeaders, $responseBody) = self::parseHttpResponse($respdata, $respheadersize);
 
         // Fill in the apiHttpRequest with the response values.
-        $request->setResponseHttpCode($respHttpCode);
+        $request->setResponseHttpCode($resphttpcode);
         $request->setResponseHeaders($responseHeaders);
         $request->setResponseBody($responseBody);
 
index 524b62a..b136b9f 100644 (file)
@@ -32,7 +32,7 @@ $GoogleConfigTempDir = $CFG->tempdir . '/googleapi';
 
 global $apiConfig;
 $apiConfig = array(
-    // The application_name is included in the User-Agent HTTP header.
+    // Application name.
     'application_name' => 'Moodle ' . $CFG->release,
 
     // Site name to show in the Google's OAuth 1 authentication screen.
@@ -41,7 +41,12 @@ $apiConfig = array(
     // Which HTTP IO classes to use.
     'ioClass' => 'moodle_google_curlio',
 
-    // IO Class dependent configuration, you only have to configure the values
-    // for the class that was configured as the ioClass above
-    'ioFileCache_directory' => $GoogleConfigTempDir
+    // Cache class directory, it should never be used but created just in case.
+    'ioFileCache_directory' => $GoogleConfigTempDir,
+
+    // Default Access Type for OAuth 2.0.
+    'oauth2_access_type' => 'online',
+
+    // Default Approval Prompt for OAuth 2.0.
+    'oauth2_approval_prompt' => 'auto'
 );
index 82fbc71..aeca306 100644 (file)
  */
 
 $string['clientid'] = 'Client ID';
-$string['configplugin'] = 'Configure Google Docs plugin';
-$string['googledocs:view'] = 'View google docs repository';
-$string['oauthinfo'] = '<p>To use this plugin, you must register your site with Google, as described in the documentation <a href="{$a->docsurl}">Google OAuth 2.0 setup</a>.</p><p>As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':</p><p>{$a->callbackurl}</p>Once registered, you will be provided with a client ID and secret which can be used to configure all Google Docs and Picasa plugins.</p>';
+$string['configplugin'] = 'Configure Google Drive plugin';
+$string['googledocs:view'] = 'View Google Drive repository';
+$string['oauthinfo'] = '<p>To use this plugin, you must register your site with Google, as described in the documentation <a href="{$a->docsurl}">Google OAuth 2.0 setup</a>.</p><p>As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':</p><p>{$a->callbackurl}</p>Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.</p><p>Please also note that you will have to enable the service \'Drive API\'.</p>';
 $string['oauth2upgrade_message_subject'] = 'Important information regarding Google Docs repository plugin';
 $string['oauth2upgrade_message_content'] = 'As part of the upgrade to Moodle 2.3, the Google Docs portfolio plugin has been disabled. To re-enable it, your Moodle site needs to be registered with Google, as described in the documentation {$a->docsurl}, in order to obtain a client ID and secret. The client ID and secret can then be used to configure all Google Docs and Picasa plugins.';
 $string['oauth2upgrade_message_small'] = 'This plugin has been disabled, as it requires configuration as described in the documentation Google OAuth 2.0 setup.';
-$string['pluginname'] = 'Google Docs';
+$string['pluginname'] = 'Google Drive';
 $string['secret'] = 'Secret';
-
+$string['servicenotenabled'] = 'Access not configured. Make sure the service \'Drive API\' is enabled.';
index 72ca7bb..62eb579 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * This plugin is used to access google docs
+ * This plugin is used to access Google Drive.
  *
  * @since 2.0
  * @package    repository_googledocs
  * @copyright  2009 Dan Poltawski <talktodan@gmail.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+
+defined('MOODLE_INTERNAL') || die();
+
 require_once($CFG->dirroot . '/repository/lib.php');
-require_once($CFG->libdir.'/googleapi.php');
+require_once($CFG->libdir . '/textlib.class.php');
+require_once($CFG->libdir . '/google/Google_Client.php');
+require_once($CFG->libdir . '/google/contrib/Google_DriveService.php');
 
 /**
  * Google Docs Plugin
@@ -34,30 +39,117 @@ require_once($CFG->libdir.'/googleapi.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class repository_googledocs extends repository {
-    private $googleoauth = null;
 
-    public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
-        parent::__construct($repositoryid, $context, $options);
+    /**
+     * Google Client.
+     * @var Google_Client
+     */
+    private $client = null;
 
-        $returnurl = new moodle_url('/repository/repository_callback.php');
-        $returnurl->param('callback', 'yes');
-        $returnurl->param('repo_id', $this->id);
-        $returnurl->param('sesskey', sesskey());
+    /**
+     * Google Drive Service.
+     * @var Google_DriveService
+     */
+    private $service = null;
+
+    /**
+     * Session key to store the accesstoken.
+     * @var string
+     */
+    const SESSIONKEY = 'googledrive_accesstoken';
 
-        $clientid = get_config('googledocs', 'clientid');
-        $secret = get_config('googledocs', 'secret');
-        $this->googleoauth = new google_oauth($clientid, $secret, $returnurl, google_docs::REALM);
+    /**
+     * URI to the callback file for OAuth.
+     * @var string
+     */
+    const CALLBACKURL = '/admin/oauth2callback.php';
+
+    /**
+     * Constructor.
+     *
+     * @param int $repositoryid repository instance id.
+     * @param int|stdClass $context a context id or context object.
+     * @param array $options repository options.
+     * @param int $readonly indicate this repo is readonly or not.
+     * @return void
+     */
+    public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
+        parent::__construct($repositoryid, $context, $options, $readonly = 0);
+
+        $callbackurl = new moodle_url(self::CALLBACKURL);
+
+        $this->client = new Google_Client();
+        $this->client->setClientId(get_config('googledocs', 'clientid'));
+        $this->client->setClientSecret(get_config('googledocs', 'secret'));
+        $this->client->setScopes(array('https://www.googleapis.com/auth/drive.readonly'));
+        $this->client->setRedirectUri($callbackurl->out(false));
+        $this->service = new Google_DriveService($this->client);
 
         $this->check_login();
     }
 
+    /**
+     * Returns the access token if any.
+     *
+     * @return string|null access token.
+     */
+    protected function get_access_token() {
+        global $SESSION;
+        if (isset($SESSION->{self::SESSIONKEY})) {
+            return $SESSION->{self::SESSIONKEY};
+        }
+        return null;
+    }
+
+    /**
+     * Store the access token in the session.
+     *
+     * @param string $token token to store.
+     * @return void
+     */
+    protected function store_access_token($token) {
+        global $SESSION;
+        $SESSION->{self::SESSIONKEY} = $token;
+    }
+
+    /**
+     * Callback method during authentication.
+     *
+     * @return void
+     */
+    public function callback() {
+        if ($code = optional_param('oauth2code', null, PARAM_RAW)) {
+            $this->client->authenticate($code);
+            $this->store_access_token($this->client->getAccessToken());
+        }
+    }
+
+    /**
+     * Checks whether the user is authenticate or not.
+     *
+     * @return bool true when logged in.
+     */
     public function check_login() {
-        return $this->googleoauth->is_logged_in();
+        if ($token = $this->get_access_token()) {
+            $this->client->setAccessToken($token);
+            return true;
+        }
+        return false;
     }
 
+    /**
+     * Print or return the login form.
+     *
+     * @return void|array for ajax.
+     */
     public function print_login() {
-        $url = $this->googleoauth->get_login_url();
+        $returnurl = new moodle_url('/repository/repository_callback.php');
+        $returnurl->param('callback', 'yes');
+        $returnurl->param('repo_id', $this->id);
+        $returnurl->param('sesskey', sesskey());
 
+        $url = new moodle_url($this->client->createAuthUrl());
+        $url->param('state', $returnurl->out_as_local_url(false));
         if ($this->options['ajax']) {
             $popup = new stdClass();
             $popup->type = 'popup';
@@ -68,54 +160,333 @@ class repository_googledocs extends repository {
         }
     }
 
+    /**
+    * Build the breadcrumb from a path.
+    *
+    * @param string $path to create a breadcrumb from.
+    * @return array containing name and path of each crumb.
+    */
+    protected function build_breadcrumb($path) {
+        $bread = explode('/', $path);
+        $crumbtrail = '';
+        foreach ($bread as $crumb) {
+            list($id, $name) = $this->explode_node_path($crumb);
+            $name = empty($name) ? $id : $name;
+            $breadcrumb[] = array(
+                'name' => $name,
+                'path' => $this->build_node_path($id, $name, $crumbtrail)
+            );
+            $tmp = end($breadcrumb);
+            $crumbtrail = $tmp['path'];
+        }
+        return $breadcrumb;
+    }
+
+    /**
+    * Generates a safe path to a node.
+    *
+    * Typically, a node will be id|Name of the node.
+    *
+    * @param string $id of the node.
+    * @param string $name of the node, will be URL encoded.
+    * @param string $root to append the node on, must be a result of this function.
+    * @return string path to the node.
+    */
+    protected function build_node_path($id, $name = '', $root = '') {
+        $path = $id;
+        if (!empty($name)) {
+            $path .= '|' . urlencode($name);
+        }
+        if (!empty($root)) {
+            $path = trim($root, '/') . '/' . $path;
+        }
+        return $path;
+    }
+
+    /**
+    * Returns information about a node in a path.
+    *
+    * @see self::build_node_path()
+    * @param string $node to extrat information from.
+    * @return array about the node.
+    */
+    protected function explode_node_path($node) {
+        if (strpos($node, '|') !== false) {
+            list($id, $name) = explode('|', $node, 2);
+            $name = urldecode($name);
+        } else {
+            $id = $node;
+            $name = '';
+        }
+        $id = urldecode($id);
+        return array(
+            0 => $id,
+            1 => $name,
+            'id' => $id,
+            'name' => $name
+        );
+    }
+
+
+    /**
+     * List the files and folders.
+     *
+     * @param  string $path path to browse.
+     * @param  string $page page to browse.
+     * @return array of result.
+     */
     public function get_listing($path='', $page = '') {
-        $gdocs = new google_docs($this->googleoauth);
+        if (empty($path)) {
+            $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
+        }
+
+        // We analyse the path to extract what to browse.
+        $trail = explode('/', $path);
+        $uri = array_pop($trail);
+        list($id, $name) = $this->explode_node_path($uri);
+
+        // Handle the special keyword 'search', which we defined in self::search() so that
+        // we could set up a breadcrumb in the search results. In any other case ID would be
+        // 'root' which is a special keyword set up by Google, or a parent (folder) ID.
+        if ($id === 'search') {
+            return $this->search($name);
+        }
+
+        // Query the Drive.
+        $q = "'" . str_replace("'", "\'", $id) . "' in parents";
+        $q .= ' AND trashed = false';
+        $results = $this->query($q, $path);
 
         $ret = array();
         $ret['dynload'] = true;
-        $ret['list'] = $gdocs->get_file_list();
+        $ret['path'] = $this->build_breadcrumb($path);
+        $ret['list'] = $results;
         return $ret;
     }
 
+    /**
+     * Search throughout the Google Drive.
+     *
+     * @param string $search_text text to search for.
+     * @param int $page search page.
+     * @return array of results.
+     */
     public function search($search_text, $page = 0) {
-        $gdocs = new google_docs($this->googleoauth);
+        $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
+        $path = $this->build_node_path('search', $search_text, $path);
+
+        // Query the Drive.
+        $q = "fullText contains '" . str_replace("'", "\'", $search_text) . "'";
+        $q .= ' AND trashed = false';
+        $results = $this->query($q, $path);
 
         $ret = array();
         $ret['dynload'] = true;
-        $ret['list'] = $gdocs->get_file_list($search_text);
+        $ret['path'] = $this->build_breadcrumb($path);
+        $ret['list'] = $results;
         return $ret;
     }
 
+    /**
+     * Query Google Drive for files and folders using a search query.
+     *
+     * Documentation about the query format can be found here:
+     *   https://developers.google.com/drive/search-parameters
+     *
+     * This returns a list of files and folders with their details as they should be
+     * formatted and returned by functions such as get_listing() or search().
+     *
+     * @param string $q search query as expected by the Google API.
+     * @param string $path parent path of the current files, will not be used for the query.
+     * @param int $page page.
+     * @return array of files and folders.
+     */
+    protected function query($q, $path = null, $page = 0) {
+        global $OUTPUT;
+
+        $files = array();
+        $folders = array();
+        $fields = "items(id,title,mimeType,downloadUrl,fileExtension,exportLinks,modifiedDate,fileSize,thumbnailLink)";
+        $params = array('q' => $q, 'fields' => $fields);
+
+        try {
+            // Retrieving files and folders.
+            $response = $this->service->files->listFiles($params);
+        } catch (Google_ServiceException $e) {
+            if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
+                // This is raised when the service Drive API has not been enabled on Google APIs control panel.
+                throw new repository_exception('servicenotenabled', 'repository_googledocs');
+            } else {
+                throw $e;
+            }
+        }
+
+        $items = isset($response['items']) ? $response['items'] : array();
+        foreach ($items as $item) {
+            if ($item['mimeType'] == 'application/vnd.google-apps.folder') {
+                // This is a folder.
+                $folders[$item['title'] . $item['id']] = array(
+                    'title' => $item['title'],
+                    'path' => $this->build_node_path($item['id'], $item['title'], $path),
+                    'date' => strtotime($item['modifiedDate']),
+                    'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false),
+                    'thumbnail_height' => 64,
+                    'thumbnail_width' => 64,
+                    'children' => array()
+                );
+            } else {
+                // This is a file.
+                if (isset($item['fileExtension'])) {
+                    // The file has an extension, therefore there is a download link.
+                    $title = $item['title'];
+                    $source = $item['downloadUrl'];
+                } else {
+                    // The file is probably a Google Doc file, we get the corresponding export link.
+                    // This should be improved by allowing the user to select the type of export they'd like.
+                    $type = str_replace('application/vnd.google-apps.', '', $item['mimeType']);
+                    $title = '';
+                    $exportType = '';
+                    switch ($type){
+                        case 'document':
+                            $title = $item['title'] . '.rtf';
+                            $exportType = 'application/rtf';
+                            break;
+                        case 'presentation':
+                            $title = $item['title'] . '.pptx';
+                            $exportType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
+                            break;
+                        case 'spreadsheet':
+                            $title = $item['title'] . '.xlsx';
+                            $exportType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+                            break;
+                    }
+                    // Skips invalid/unknown types.
+                    if (empty($title) || !isset($item['exportLinks'][$exportType])) {
+                        continue;
+                    }
+                    $source = $item['exportLinks'][$exportType];
+                }
+                // Adds the file to the file list. Using the itemId along with the title as key
+                // of the array because Google Drive allows files with identical names.
+                $files[$title . $item['id']] = array(
+                    'title' => $title,
+                    'source' => $source,
+                    'date' => strtotime($item['modifiedDate']),
+                    'size' => isset($item['fileSize']) ? $item['fileSize'] : null,
+                    'thumbnail' => $OUTPUT->pix_url(file_extension_icon($title, 64))->out(false),
+                    'thumbnail_height' => 64,
+                    'thumbnail_width' => 64,
+                    // Do not use real thumbnails as they wouldn't work if the user disabled 3rd party
+                    // plugins in his browser, or if they're not logged in their Google account.
+                );
+
+                // Sometimes the real thumbnails can't be displayed, for example if 3rd party cookies are disabled
+                // or if the user is not logged in Google anymore. But this restriction does not seem to be applied
+                // to a small subset of files.
+                $extension = strtolower(pathinfo($title, PATHINFO_EXTENSION));
+                if (isset($item['thumbnailLink']) && in_array($extension, array('jpg', 'png', 'txt', 'pdf'))) {
+                    $files[$title . $item['id']]['realthumbnail'] = $item['thumbnailLink'];
+                }
+            }
+        }
+
+        // Filter and order the results.
+        $files = array_filter($files, array($this, 'filter'));
+        collatorlib::ksort($files, collatorlib::SORT_NATURAL);
+        collatorlib::ksort($folders, collatorlib::SORT_NATURAL);
+        return array_merge(array_values($folders), array_values($files));
+    }
+
+    /**
+     * Logout.
+     *
+     * @return string
+     */
     public function logout() {
-        $this->googleoauth->log_out();
+        $this->store_access_token(null);
         return parent::logout();
     }
 
-    public function get_file($url, $file = '') {
-        if (empty($url)) {
-           throw new repository_exception('cannotdownload', 'repository');
+    /**
+     * Get a file.
+     *
+     * @param string $reference reference of the file.
+     * @param string $file name to save the file to.
+     * @return string JSON encoded array of information about the file.
+     */
+    public function get_file($reference, $filename = '') {
+        $request = new Google_HttpRequest($reference);
+        $httpRequest = Google_Client::$io->authenticatedRequest($request);
+        if ($httpRequest->getResponseHttpCode() == 200) {
+            $path = $this->prepare_file($filename);
+            $content = $httpRequest->getResponseBody();
+            if (file_put_contents($path, $content) !== false) {
+                return array(
+                    'path' => $path,
+                    'url' => $reference
+                );
+            }
         }
-        $gdocs = new google_docs($this->googleoauth);
-        $path = $this->prepare_file($file);
-        return $gdocs->download_file($url, $path, self::GETFILE_TIMEOUT);
+        throw new repository_exception('cannotdownload', 'repository');
+    }
+
+    /**
+     * Prepare file reference information.
+     *
+     * We are using this method to clean up the source to make sure that it
+     * is a valid source.
+     *
+     * @param string $source of the file.
+     * @return string file reference.
+     */
+    public function get_file_reference($source) {
+        return clean_param($source, PARAM_URL);
     }
 
+    /**
+     * What kind of files will be in this repository?
+     *
+     * @return array return '*' means this repository support any files, otherwise
+     *               return mimetypes of files, it can be an array
+     */
     public function supported_filetypes() {
         return '*';
     }
+
+    /**
+     * Tells how the file can be picked from this repository.
+     *
+     * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE.
+     *
+     * @return int
+     */
     public function supported_returntypes() {
         return FILE_INTERNAL;
     }
 
+    /**
+     * Return names of the general options.
+     * By default: no general option name.
+     *
+     * @return array
+     */
     public static function get_type_option_names() {
         return array('clientid', 'secret', 'pluginname');
     }
 
+    /**
+     * Edit/Create Admin Settings Moodle form.
+     *
+     * @param moodleform $mform Moodle form (passed by reference).
+     * @param string $classname repository class name.
+     */
     public static function type_config_form($mform, $classname = 'repository') {
 
+        $callbackurl = new moodle_url(self::CALLBACKURL);
+
         $a = new stdClass;
         $a->docsurl = get_docs_url('Google_OAuth_2.0_setup');
-        $a->callbackurl = google_oauth::callback_url()->out(false);
+        $a->callbackurl = $callbackurl->out(false);
 
         $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_googledocs', $a));
 
index a2382a9..8d75c70 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112900;        // The current plugin version (Date: YYYYMMDDXX).
-$plugin->requires  = 2012112900;        // Requires this Moodle version.
+$plugin->version   = 2013021200;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2013021400;        // Requires this Moodle version.
 $plugin->component = 'repository_googledocs'; // Full name of the plugin (used for diagnostics).
index 5ab02ab..f665d29 100644 (file)
@@ -30,7 +30,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2013021400.00;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2013021400.01;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes