// 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
* @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';
}
}
+ /**
+ * 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));