Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This plugin is used to access Google Drive.
 *
 * @since Moodle 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 . '/filebrowser/file_browser.php');

use repository_googledocs\helper;
use repository_googledocs\googledocs_content_search;

/**
 * Google Docs Plugin
 *
 * @since Moodle 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
 */
class repository_googledocs extends repository {

    /**
     * OAuth 2 client
     * @var \core\oauth2\client
     */
    private $client = null;

    /**
     * OAuth 2 Issuer
     * @var \core\oauth2\issuer
     */
    private $issuer = null;

    /**
     * Additional scopes required for drive.
     */
    const SCOPES = 'https://www.googleapis.com/auth/drive';

    /** @var string Defines the path node identifier for the repository root. */
    const REPOSITORY_ROOT_ID = 'repository_root';

    /** @var string Defines the path node identifier for the my drive root. */
    const MY_DRIVE_ROOT_ID = 'root';

    /** @var string Defines the path node identifier for the shared drives root. */
    const SHARED_DRIVES_ROOT_ID = 'shared_drives_root';

    /** @var string Defines the path node identifier for the content search root. */
    const SEARCH_ROOT_ID = 'search';

    /**
     * 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);

        try {
            $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid'));
        } catch (dml_missing_record_exception $e) {
            $this->disabled = true;
        }

        if ($this->issuer && !$this->issuer->get('enabled')) {
            $this->disabled = true;
        }
    }

    /**
     * Get a cached user authenticated oauth client.
     *
     * @param moodle_url $overrideurl - Use this url instead of the repo callback.
     * @return \core\oauth2\client
     */
    protected function get_user_oauth_client($overrideurl = false) {
        if ($this->client) {
            return $this->client;
        }
        if ($overrideurl) {
            $returnurl = $overrideurl;
        } else {
            $returnurl = new moodle_url('/repository/repository_callback.php');
            $returnurl->param('callback', 'yes');
            $returnurl->param('repo_id', $this->id);
            $returnurl->param('sesskey', sesskey());
        }

        $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true);

        return $this->client;
    }

    /**
     * Checks whether the user is authenticate or not.
     *
     * @return bool true when logged in.
     */
    public function check_login() {
        $client = $this->get_user_oauth_client();
        return $client->is_logged_in();
    }

    /**
     * Print or return the login form.
     *
     * @return void|array for ajax.
     */
    public function print_login() {
        $client = $this->get_user_oauth_client();
        $url = $client->get_login_url();

        if ($this->options['ajax']) {
            $popup = new stdClass();
            $popup->type = 'popup';
            $popup->url = $url->out(false);
            return array('login' => array($popup));
        } else {
            echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
        }
    }

    /**
     * Print the login in a popup.
     *
     * @param array|null $attr Custom attributes to be applied to popup div.
     */
    public function print_login_popup($attr = null) {
        global $OUTPUT, $PAGE;

        $client = $this->get_user_oauth_client(false);
        $url = new moodle_url($client->get_login_url());
        $state = $url->get_param('state') . '&reloadparent=true';
        $url->param('state', $state);

        $PAGE->set_pagelayout('embedded');
        echo $OUTPUT->header();

        $repositoryname = get_string('pluginname', 'repository_googledocs');

< $button = new single_button($url, get_string('logintoaccount', 'repository', $repositoryname), 'post', true);
> $button = new single_button( > $url, > get_string('logintoaccount', 'repository', $repositoryname), > 'post', > single_button::BUTTON_PRIMARY > );
$button->add_action(new popup_action('click', $url, 'Login')); $button->class = 'mdl-align'; $button = $OUTPUT->render($button); echo html_writer::div($button, '', $attr); echo $OUTPUT->footer(); } /** * Build the breadcrumb from a path. * * @deprecated since Moodle 3.11. * @param string $path to create a breadcrumb from. * @return array containing name and path of each crumb. */ protected function build_breadcrumb($path) { debugging('The function build_breadcrumb() is deprecated, please use get_navigation() from the ' . 'googledocs repository content classes instead.', DEBUG_DEVELOPER); $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. * * @deprecated since Moodle 3.11. * @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 = '') { debugging('The function build_node_path() is deprecated, please use ' . '\repository_googledocs\helper::build_node_path() instead.', DEBUG_DEVELOPER); $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. * * @deprecated since Moodle 3.11. * @see self::build_node_path() * @param string $node to extrat information from. * @return array about the node. */ protected function explode_node_path($node) { debugging('The function explode_node_path() is deprecated, please use ' . '\repository_googledocs\helper::explode_node_path() instead.', DEBUG_DEVELOPER); 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 = '') { if (empty($path)) { $pluginname = get_string('pluginname', 'repository_googledocs'); $path = helper::build_node_path('repository_root', $pluginname); } if (!$this->issuer->get('enabled')) { // Empty list of files for disabled repository. return [ 'dynload' => false, 'list' => [], 'nologin' => true, ]; } // We analyse the path to extract what to browse. $trail = explode('/', $path); $uri = array_pop($trail); list($id, $name) = helper::explode_node_path($uri); $service = new repository_googledocs\rest($this->get_user_oauth_client()); // Define the content class object and query which will be used to get the contents for this path. if ($id === self::SEARCH_ROOT_ID) { // The special keyword 'search' is the ID of the node. This is possible as we can set up a breadcrumb in // the search results. Therefore, we should use the content search object to get the results from the // previously performed search. $contentobj = new googledocs_content_search($service, $path); // We need to deconstruct the node name in order to obtain the search term and use it as a query. $query = str_replace(get_string('searchfor', 'repository_googledocs'), '', $name); $query = trim(str_replace("'", "", $query)); } else { // Otherwise, return and use the appropriate (based on the path) content browser object. $contentobj = helper::get_browser($service, $path); // Use the node ID as a query. $query = $id; } return [ 'dynload' => true, 'defaultreturntype' => $this->default_returntype(), 'path' => $contentobj->get_navigation(), 'list' => $contentobj->get_content_nodes($query, [$this, 'filter']), 'manage' => 'https://drive.google.com/', ]; } /** * Search throughout the Google Drive. * * @param string $searchtext text to search for. * @param int $page search page. * @return array of results. */ public function search($searchtext, $page = 0) { // Construct the path to the repository root. $pluginname = get_string('pluginname', 'repository_googledocs'); $rootpath = helper::build_node_path(self::REPOSITORY_ROOT_ID, $pluginname); // Construct the path to the search results node. // Currently, when constructing the search node name, the search term is concatenated to the lang string. // This was done deliberately so that we can easily and accurately obtain the search term from the search node // name later when navigating to the search results through the breadcrumb navigation. $name = get_string('searchfor', 'repository_googledocs') . " '{$searchtext}'"; $path = helper::build_node_path(self::SEARCH_ROOT_ID, $name, $rootpath); $service = new repository_googledocs\rest($this->get_user_oauth_client()); $searchobj = new googledocs_content_search($service, $path); return [ 'dynload' => true, 'path' => $searchobj->get_navigation(), 'list' => $searchobj->get_content_nodes($searchtext, [$this, 'filter']), 'manage' => 'https://drive.google.com/', ]; } /** * 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(). * * @deprecated since Moodle 3.11. * @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) { debugging('The function query() is deprecated, please use get_content_nodes() from the ' . 'googledocs repository content classes instead.', DEBUG_DEVELOPER); global $OUTPUT; $files = array(); $folders = array(); $config = get_config('googledocs'); $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)"; $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive'); try { // Retrieving files and folders. $client = $this->get_user_oauth_client(); $service = new repository_googledocs\rest($client); $response = $service->call('list', $params); } catch (Exception $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; } } $gfiles = isset($response->files) ? $response->files : array(); foreach ($gfiles as $gfile) { if ($gfile->mimeType == 'application/vnd.google-apps.folder') { // This is a folder. $folders[$gfile->name . $gfile->id] = array( 'title' => $gfile->name, 'path' => $this->build_node_path($gfile->id, $gfile->name, $path), 'date' => strtotime($gfile->modifiedTime), 'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false), 'thumbnail_height' => 64, 'thumbnail_width' => 64, 'children' => array() ); } else { // This is a file. $link = isset($gfile->webViewLink) ? $gfile->webViewLink : ''; if (empty($link)) { $link = isset($gfile->webContentLink) ? $gfile->webContentLink : ''; } if (isset($gfile->fileExtension)) { // The file has an extension, therefore we can download it. $source = json_encode([ 'id' => $gfile->id, 'name' => $gfile->name, 'exportformat' => 'download', 'link' => $link ]); $title = $gfile->name; } 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.', '', $gfile->mimeType); $title = ''; $exporttype = ''; $types = get_mimetypes_array(); switch ($type){ case 'document': $ext = $config->documentformat; $title = $gfile->name . '.gdoc'; if ($ext === 'rtf') { // Moodle user 'text/rtf' as the MIME type for RTF files. // Google uses 'application/rtf' for the same type of file. // See https://developers.google.com/drive/v3/web/manage-downloads. $exporttype = 'application/rtf'; } else { $exporttype = $types[$ext]['type']; } break; case 'presentation': $ext = $config->presentationformat; $title = $gfile->name . '.gslides'; $exporttype = $types[$ext]['type']; break; case 'spreadsheet': $ext = $config->spreadsheetformat; $title = $gfile->name . '.gsheet'; $exporttype = $types[$ext]['type']; break; case 'drawing': $ext = $config->drawingformat; $title = $gfile->name . '.'. $ext; $exporttype = $types[$ext]['type']; break; } // Skips invalid/unknown types. if (empty($title)) { continue; } $source = json_encode([ 'id' => $gfile->id, 'exportformat' => $exporttype, 'link' => $link, 'name' => $gfile->name ]); } // Adds the file to the file list. Using the itemId along with the name as key // of the array because Google Drive allows files with identical names. $thumb = ''; if (isset($gfile->thumbnailLink)) { $thumb = $gfile->thumbnailLink; } else if (isset($gfile->iconLink)) { $thumb = $gfile->iconLink; } $files[$title . $gfile->id] = array( 'title' => $title, 'source' => $source, 'date' => strtotime($gfile->modifiedTime), 'size' => isset($gfile->size) ? $gfile->size : null, 'thumbnail' => $thumb, 'thumbnail_height' => 64, 'thumbnail_width' => 64, ); } } // Filter and order the results. $files = array_filter($files, array($this, 'filter')); core_collator::ksort($files, core_collator::SORT_NATURAL); core_collator::ksort($folders, core_collator::SORT_NATURAL); return array_merge(array_values($folders), array_values($files)); } /** * Logout. * * @return string */ public function logout() { $client = $this->get_user_oauth_client(); $client->log_out(); return parent::logout(); } /** * 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 = '') { global $CFG; if (!$this->issuer->get('enabled')) { throw new repository_exception('cannotdownload', 'repository'); } $source = json_decode($reference); $client = null; if (!empty($source->usesystem)) { $client = \core\oauth2\api::get_system_oauth_client($this->issuer); } else { $client = $this->get_user_oauth_client(); } $base = 'https://www.googleapis.com/drive/v3'; $newfilename = false; if ($source->exportformat == 'download') { $params = ['alt' => 'media']; $sourceurl = new moodle_url($base . '/files/' . $source->id, $params); $source = $sourceurl->out(false); } else { $params = ['mimeType' => $source->exportformat]; $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params); $types = get_mimetypes_array(); $checktype = $source->exportformat; if ($checktype == 'application/rtf') { $checktype = 'text/rtf'; } // Determine the relevant default import format config for the given file. switch ($source->googledoctype) { case 'document': $importformatconfig = get_config('googledocs', 'documentformat'); break; case 'presentation': $importformatconfig = get_config('googledocs', 'presentationformat'); break; case 'spreadsheet': $importformatconfig = get_config('googledocs', 'spreadsheetformat'); break; case 'drawing': $importformatconfig = get_config('googledocs', 'drawingformat'); break; default: $importformatconfig = null; } foreach ($types as $extension => $info) { if ($info['type'] == $checktype && $extension === $importformatconfig) { $newfilename = $source->name . '.' . $extension; break; } } $source = $sourceurl->out(false); } // We use download_one and not the rest API because it has special timeouts etc. $path = $this->prepare_file($filename); $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; $success = $client->download_one($source, null, $options); if ($success) { @chmod($path, $CFG->filepermissions); $result = [ 'path' => $path, 'url' => $reference, ]; if (!empty($newfilename)) { $result['newfilename'] = $newfilename; } return $result; } 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) { // We could do some magic upgrade code here. return $source; } /** * 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. * * @return int */ public function supported_returntypes() { // We can only support references if the system account is connected. if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) { $setting = get_config('googledocs', 'supportedreturntypes'); if ($setting == 'internal') { return FILE_INTERNAL; } else if ($setting == 'external') { return FILE_CONTROLLED_LINK; } else { return FILE_CONTROLLED_LINK | FILE_INTERNAL; } } else { return FILE_INTERNAL; } } /** * Which return type should be selected by default. * * @return int */ public function default_returntype() { $setting = get_config('googledocs', 'defaultreturntype'); $supported = get_config('googledocs', 'supportedreturntypes'); if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { return FILE_INTERNAL; } else { return FILE_CONTROLLED_LINK; } } /** * Return names of the general options. * By default: no general option name. * * @return array */ public static function get_type_option_names() { return array('issuerid', 'pluginname', 'documentformat', 'drawingformat', 'presentationformat', 'spreadsheetformat', 'defaultreturntype', 'supportedreturntypes'); } /** * Store the access token. */ public function callback() { $client = $this->get_user_oauth_client(); // This will upgrade to an access token if we have an authorization code and save the access token in the session. $client->is_logged_in(); } /** * Repository method to serve the referenced file * * @see send_stored_file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { if (!$this->issuer->get('enabled')) { throw new repository_exception('cannotdownload', 'repository'); } $source = json_decode($storedfile->get_reference()); $fb = get_file_browser(); $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST); $info = $fb->get_file_info($context, $storedfile->get_component(), $storedfile->get_filearea(), $storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename()); if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $systemservice = new repository_googledocs\rest($systemauth); // Get the user oauth so we can get the account to add. $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(), $storedfile->get_component(), $storedfile->get_filearea(), $storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename(), $forcedownload); $url->param('sesskey', sesskey()); $param = ($options['embed'] == true) ? false : $url; $userauth = $this->get_user_oauth_client($param); if (!$userauth->is_logged_in()) { if ($options['embed'] == true) { // Due to Same-origin policy, we cannot redirect to googledocs login page. // If the requested file is embed and the user is not logged in, add option to log in using a popup. $this->print_login_popup(['style' => 'margin-top: 250px']); exit; } redirect($userauth->get_login_url()); } if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $userinfo = $userauth->get_userinfo(); $useremail = $userinfo['email']; $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); } if (!empty($options['offline'])) { $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename()); $filename = $storedfile->get_filename(); if (isset($downloaded['newfilename'])) { $filename = $downloaded['newfilename']; } send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options); } else if ($source->link) { // Do not use redirect() here because is not compatible with webservice/pluginfile.php. header('Location: ' . $source->link); } else { $details = 'File is missing source link'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } } /** * See if a folder exists within a folder * * @param \repository_googledocs\rest $client Authenticated client. * @param string $foldername The folder we are looking for. * @param string $parentid The parent folder we are looking in. * @return string|boolean The file id if it exists or false. */ protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { $q = '\'' . addslashes($parentid) . '\' in parents and trashed = false and name = \'' . addslashes($foldername). '\''; $fields = 'files(id, name)'; $params = [ 'q' => $q, 'fields' => $fields]; $response = $client->call('list', $params); $missing = true; foreach ($response->files as $child) { if ($child->name == $foldername) { return $child->id; } } return false; } /** * Create a folder within a folder * * @param \repository_googledocs\rest $client Authenticated client. * @param string $foldername The folder we are creating. * @param string $parentid The parent folder we are creating in. * * @return string The file id of the new folder. */ protected function create_folder_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { $fields = 'id'; $params = ['fields' => $fields]; $folder = ['mimeType' => 'application/vnd.google-apps.folder', 'name' => $foldername, 'parents' => [$parentid]]; $created = $client->call('create', $params, json_encode($folder)); if (empty($created->id)) { $details = 'Cannot create folder:' . $foldername; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return $created->id; } /** * Get simple file info for humans. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are querying. * * @return stdClass */ protected function get_file_summary(\repository_googledocs\rest $client, $fileid) { $fields = "id,name,owners,parents"; $params = [ 'fileid' => $fileid, 'fields' => $fields ]; return $client->call('get', $params); } /** * Copy a file and return the new file details. A side effect of the copy * is that the owner will be the account authenticated with this oauth client. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are copying. * @param string $name The original filename (don't change it). * * @return stdClass file details. */ protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) { $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink"; $params = [ 'fileid' => $fileid, 'fields' => $fields, ]; // Keep the original name (don't put copy at the end of it). $copyinfo = []; if (!empty($name)) { $copyinfo = [ 'name' => $name ]; } $fileinfo = $client->call('copy', $params, json_encode($copyinfo)); if (empty($fileinfo->id)) { $details = 'Cannot copy file:' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return $fileinfo; } /** * Add a writer to the permissions on the file (temporary). * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) { // Expires in 7 days. $expires = new DateTime(); $expires->add(new DateInterval("P7D")); $updateeditor = [ 'emailAddress' => $email, 'role' => 'writer', 'type' => 'user', 'expirationTime' => $expires->format(DateTime::RFC3339) ]; $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Add a writer to the permissions on the file. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) { $updateeditor = [ 'emailAddress' => $email, 'role' => 'writer', 'type' => 'user' ]; $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Move from root to folder * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $folderid The id of the folder we are moving to * @return boolean */ protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) { // Set the parent. $params = [ 'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root' ]; $response = $client->call('update', $params, ' '); if (empty($response->id)) { $details = 'Cannot move the file to a folder: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Prevent writers from sharing. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) { // We don't want anyone but Moodle to change the sharing settings. $params = [ 'fileid' => $fileid ]; $update = [ 'writersCanShare' => false ]; $response = $client->call('update', $params, json_encode($update)); if (empty($response->id)) { $details = 'Cannot prevent writers from sharing document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Allow anyone with the link to read the file. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) { $updateread = [ 'type' => 'anyone', 'role' => 'reader', 'allowFileDiscovery' => 'false' ]; $params = ['fileid' => $fileid]; $response = $client->call('create_permission', $params, json_encode($updateread)); if (empty($response->id) || $response->id != 'anyoneWithLink') { $details = 'Cannot update link sharing for the document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Called when a file is selected as a "link". * Invoked at MOODLE/repository/repository_ajax.php * * This is called at the point the reference files are being copied from the draft area to the real area * (when the file has really really been selected. * * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. * @param string $component the target component for this new file. * @param string $filearea the target filearea for this new file. * @param string $itemid the target itemid for this new file. * @return string updated reference (final one before it's saved to db). */ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { global $CFG, $SITE; // What we need to do here is transfer ownership to the system user (or copy) // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not // already there (and point to new file id if we copied). // Get the details from the reference. $source = json_decode($reference); if (!empty($source->usesystem)) { // If we already copied this file to the system account - we are done. return $reference; } // Check this issuer is enabled. if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } // Get a system oauth client and a user oauth client. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } // Get the system user email so we can share the file with this user. $systemuserinfo = $systemauth->get_userinfo(); $systemuseremail = $systemuserinfo['email']; $userauth = $this->get_user_oauth_client(); if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $userservice = new repository_googledocs\rest($userauth); $systemservice = new repository_googledocs\rest($systemauth); // Add Moodle as writer. $this->add_writer_to_file($userservice, $source->id, $systemuseremail); // Now move it to a sensible folder. $contextlist = array_reverse($context->get_parent_contexts(true)); $cache = cache::make('repository_googledocs', 'folder'); $parentid = 'root'; $fullpath = 'root'; $allfolders = []; foreach ($contextlist as $context) { // Prepare human readable context folders names, making sure they are still unique within the site. $prevlang = force_current_language($CFG->lang); $foldername = $context->get_context_name(); force_current_language($prevlang); if ($context->contextlevel == CONTEXT_SYSTEM) { // Append the site short name to the root folder. $foldername .= ' ('.$SITE->shortname.')'; // Append the relevant object id. } else if ($context->instanceid) { $foldername .= ' (id '.$context->instanceid.')'; } else { // This does not really happen but just in case. $foldername .= ' (ctx '.$context->id.')'; } $foldername = clean_param($foldername, PARAM_PATH); $allfolders[] = $foldername; } $allfolders[] = clean_param($component, PARAM_PATH); $allfolders[] = clean_param($filearea, PARAM_PATH); $allfolders[] = clean_param($itemid, PARAM_PATH); // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder. foreach ($allfolders as $foldername) { // Make sure a folder exists here. $fullpath .= '/' . $foldername; $folderid = $cache->get($fullpath); if (empty($folderid)) { $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); } if ($folderid !== false) { $cache->set($fullpath, $folderid); $parentid = $folderid; } else { // Create it. $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid); $cache->set($fullpath, $parentid); } } // Copy the file so we get a snapshot file owned by Moodle. $newsource = $this->copy_file($systemservice, $source->id, $source->name); // Move the copied file to the correct folder. $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid); // Set the sharing options. $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); $this->prevent_writers_from_sharing_file($systemservice, $newsource->id); // Update the returned reference so that the stored_file in moodle points to the newly copied file. $source->id = $newsource->id; $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : ''; $source->usesystem = true; if (empty($source->link)) { $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : ''; } $reference = json_encode($source); return $reference; } /** * Get human readable file info from a the reference. * * @param string $reference * @param int $filestatus */ public function get_reference_details($reference, $filestatus = 0) { if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } if (empty($reference)) { return get_string('unknownsource', 'repository'); } $source = json_decode($reference); if (empty($source->usesystem)) { return ''; } $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { return ''; } $systemservice = new repository_googledocs\rest($systemauth); $info = $this->get_file_summary($systemservice, $source->id); $owner = ''; if (!empty($info->owners[0]->displayName)) { $owner = $info->owners[0]->displayName; } if ($owner) { return get_string('owner', 'repository_googledocs', $owner); } else { return $info->name; } } /** * 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') { $url = new moodle_url('/admin/tool/oauth2/issuers.php'); $url = $url->out(); $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url)); parent::type_config_form($mform); $options = []; $issuers = \core\oauth2\api::get_all_issuers(); foreach ($issuers as $issuer) { $options[$issuer->get('id')] = s($issuer->get('name')); } $strrequired = get_string('required'); $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options); $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs'); $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs')); $choices = [ 'internal' => get_string('internal', 'repository_googledocs'), 'external' => get_string('external', 'repository_googledocs'), 'both' => get_string('both', 'repository_googledocs') ]; $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices); $choices = [ FILE_INTERNAL => get_string('internal', 'repository_googledocs'), FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'), ]; $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices); $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs')); // Documents. $docsformat = array(); $docsformat['html'] = 'html'; $docsformat['docx'] = 'docx'; $docsformat['odt'] = 'odt'; $docsformat['pdf'] = 'pdf'; $docsformat['rtf'] = 'rtf'; $docsformat['txt'] = 'txt'; core_collator::ksort($docsformat, core_collator::SORT_NATURAL); $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat); $mform->setDefault('documentformat', $docsformat['rtf']); $mform->setType('documentformat', PARAM_ALPHANUM); // Drawing. $drawingformat = array(); $drawingformat['jpeg'] = 'jpeg'; $drawingformat['png'] = 'png'; $drawingformat['svg'] = 'svg'; $drawingformat['pdf'] = 'pdf'; core_collator::ksort($drawingformat, core_collator::SORT_NATURAL); $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat); $mform->setDefault('drawingformat', $drawingformat['pdf']); $mform->setType('drawingformat', PARAM_ALPHANUM); // Presentation. $presentationformat = array(); $presentationformat['pdf'] = 'pdf'; $presentationformat['pptx'] = 'pptx'; $presentationformat['txt'] = 'txt'; core_collator::ksort($presentationformat, core_collator::SORT_NATURAL); $str = get_string('presentationformat', 'repository_googledocs'); $mform->addElement('select', 'presentationformat', $str, $presentationformat); $mform->setDefault('presentationformat', $presentationformat['pptx']); $mform->setType('presentationformat', PARAM_ALPHANUM); // Spreadsheet. $spreadsheetformat = array(); $spreadsheetformat['csv'] = 'csv'; $spreadsheetformat['ods'] = 'ods'; $spreadsheetformat['pdf'] = 'pdf'; $spreadsheetformat['xlsx'] = 'xlsx'; core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL); $str = get_string('spreadsheetformat', 'repository_googledocs'); $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat); $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']); $mform->setType('spreadsheetformat', PARAM_ALPHANUM); } } /** * Callback to get the required scopes for system account. * * @param \core\oauth2\issuer $issuer * @return string */ function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) { if ($issuer->get('id') == get_config('googledocs', 'issuerid')) { return 'https://www.googleapis.com/auth/drive'; } return ''; }