PageRenderTime 66ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/repository/googledocs/lib.php

https://bitbucket.org/moodle/moodle
PHP | 1206 lines | 722 code | 126 blank | 358 comment | 110 complexity | 6b7c14d87952a375c9bc457c53757ec0 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  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/>.
  16. /**
  17. * This plugin is used to access Google Drive.
  18. *
  19. * @since Moodle 2.0
  20. * @package repository_googledocs
  21. * @copyright 2009 Dan Poltawski <talktodan@gmail.com>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->dirroot . '/repository/lib.php');
  26. require_once($CFG->libdir . '/filebrowser/file_browser.php');
  27. use repository_googledocs\helper;
  28. use repository_googledocs\googledocs_content_search;
  29. /**
  30. * Google Docs Plugin
  31. *
  32. * @since Moodle 2.0
  33. * @package repository_googledocs
  34. * @copyright 2009 Dan Poltawski <talktodan@gmail.com>
  35. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36. */
  37. class repository_googledocs extends repository {
  38. /**
  39. * OAuth 2 client
  40. * @var \core\oauth2\client
  41. */
  42. private $client = null;
  43. /**
  44. * OAuth 2 Issuer
  45. * @var \core\oauth2\issuer
  46. */
  47. private $issuer = null;
  48. /**
  49. * Additional scopes required for drive.
  50. */
  51. const SCOPES = 'https://www.googleapis.com/auth/drive';
  52. /** @var string Defines the path node identifier for the repository root. */
  53. const REPOSITORY_ROOT_ID = 'repository_root';
  54. /** @var string Defines the path node identifier for the my drive root. */
  55. const MY_DRIVE_ROOT_ID = 'root';
  56. /** @var string Defines the path node identifier for the shared drives root. */
  57. const SHARED_DRIVES_ROOT_ID = 'shared_drives_root';
  58. /** @var string Defines the path node identifier for the content search root. */
  59. const SEARCH_ROOT_ID = 'search';
  60. /**
  61. * Constructor.
  62. *
  63. * @param int $repositoryid repository instance id.
  64. * @param int|stdClass $context a context id or context object.
  65. * @param array $options repository options.
  66. * @param int $readonly indicate this repo is readonly or not.
  67. * @return void
  68. */
  69. public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
  70. parent::__construct($repositoryid, $context, $options, $readonly = 0);
  71. try {
  72. $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid'));
  73. } catch (dml_missing_record_exception $e) {
  74. $this->disabled = true;
  75. }
  76. if ($this->issuer && !$this->issuer->get('enabled')) {
  77. $this->disabled = true;
  78. }
  79. }
  80. /**
  81. * Get a cached user authenticated oauth client.
  82. *
  83. * @param moodle_url $overrideurl - Use this url instead of the repo callback.
  84. * @return \core\oauth2\client
  85. */
  86. protected function get_user_oauth_client($overrideurl = false) {
  87. if ($this->client) {
  88. return $this->client;
  89. }
  90. if ($overrideurl) {
  91. $returnurl = $overrideurl;
  92. } else {
  93. $returnurl = new moodle_url('/repository/repository_callback.php');
  94. $returnurl->param('callback', 'yes');
  95. $returnurl->param('repo_id', $this->id);
  96. $returnurl->param('sesskey', sesskey());
  97. }
  98. $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true);
  99. return $this->client;
  100. }
  101. /**
  102. * Checks whether the user is authenticate or not.
  103. *
  104. * @return bool true when logged in.
  105. */
  106. public function check_login() {
  107. $client = $this->get_user_oauth_client();
  108. return $client->is_logged_in();
  109. }
  110. /**
  111. * Print or return the login form.
  112. *
  113. * @return void|array for ajax.
  114. */
  115. public function print_login() {
  116. $client = $this->get_user_oauth_client();
  117. $url = $client->get_login_url();
  118. if ($this->options['ajax']) {
  119. $popup = new stdClass();
  120. $popup->type = 'popup';
  121. $popup->url = $url->out(false);
  122. return array('login' => array($popup));
  123. } else {
  124. echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
  125. }
  126. }
  127. /**
  128. * Print the login in a popup.
  129. *
  130. * @param array|null $attr Custom attributes to be applied to popup div.
  131. */
  132. public function print_login_popup($attr = null) {
  133. global $OUTPUT, $PAGE;
  134. $client = $this->get_user_oauth_client(false);
  135. $url = new moodle_url($client->get_login_url());
  136. $state = $url->get_param('state') . '&reloadparent=true';
  137. $url->param('state', $state);
  138. $PAGE->set_pagelayout('embedded');
  139. echo $OUTPUT->header();
  140. $repositoryname = get_string('pluginname', 'repository_googledocs');
  141. $button = new single_button($url, get_string('logintoaccount', 'repository', $repositoryname), 'post', true);
  142. $button->add_action(new popup_action('click', $url, 'Login'));
  143. $button->class = 'mdl-align';
  144. $button = $OUTPUT->render($button);
  145. echo html_writer::div($button, '', $attr);
  146. echo $OUTPUT->footer();
  147. }
  148. /**
  149. * Build the breadcrumb from a path.
  150. *
  151. * @deprecated since Moodle 3.11.
  152. * @param string $path to create a breadcrumb from.
  153. * @return array containing name and path of each crumb.
  154. */
  155. protected function build_breadcrumb($path) {
  156. debugging('The function build_breadcrumb() is deprecated, please use get_navigation() from the ' .
  157. 'googledocs repository content classes instead.', DEBUG_DEVELOPER);
  158. $bread = explode('/', $path);
  159. $crumbtrail = '';
  160. foreach ($bread as $crumb) {
  161. list($id, $name) = $this->explode_node_path($crumb);
  162. $name = empty($name) ? $id : $name;
  163. $breadcrumb[] = array(
  164. 'name' => $name,
  165. 'path' => $this->build_node_path($id, $name, $crumbtrail)
  166. );
  167. $tmp = end($breadcrumb);
  168. $crumbtrail = $tmp['path'];
  169. }
  170. return $breadcrumb;
  171. }
  172. /**
  173. * Generates a safe path to a node.
  174. *
  175. * Typically, a node will be id|Name of the node.
  176. *
  177. * @deprecated since Moodle 3.11.
  178. * @param string $id of the node.
  179. * @param string $name of the node, will be URL encoded.
  180. * @param string $root to append the node on, must be a result of this function.
  181. * @return string path to the node.
  182. */
  183. protected function build_node_path($id, $name = '', $root = '') {
  184. debugging('The function build_node_path() is deprecated, please use ' .
  185. '\repository_googledocs\helper::build_node_path() instead.', DEBUG_DEVELOPER);
  186. $path = $id;
  187. if (!empty($name)) {
  188. $path .= '|' . urlencode($name);
  189. }
  190. if (!empty($root)) {
  191. $path = trim($root, '/') . '/' . $path;
  192. }
  193. return $path;
  194. }
  195. /**
  196. * Returns information about a node in a path.
  197. *
  198. * @deprecated since Moodle 3.11.
  199. * @see self::build_node_path()
  200. * @param string $node to extrat information from.
  201. * @return array about the node.
  202. */
  203. protected function explode_node_path($node) {
  204. debugging('The function explode_node_path() is deprecated, please use ' .
  205. '\repository_googledocs\helper::explode_node_path() instead.', DEBUG_DEVELOPER);
  206. if (strpos($node, '|') !== false) {
  207. list($id, $name) = explode('|', $node, 2);
  208. $name = urldecode($name);
  209. } else {
  210. $id = $node;
  211. $name = '';
  212. }
  213. $id = urldecode($id);
  214. return array(
  215. 0 => $id,
  216. 1 => $name,
  217. 'id' => $id,
  218. 'name' => $name
  219. );
  220. }
  221. /**
  222. * List the files and folders.
  223. *
  224. * @param string $path path to browse.
  225. * @param string $page page to browse.
  226. * @return array of result.
  227. */
  228. public function get_listing($path='', $page = '') {
  229. if (empty($path)) {
  230. $pluginname = get_string('pluginname', 'repository_googledocs');
  231. $path = helper::build_node_path('repository_root', $pluginname);
  232. }
  233. if (!$this->issuer->get('enabled')) {
  234. // Empty list of files for disabled repository.
  235. return [
  236. 'dynload' => false,
  237. 'list' => [],
  238. 'nologin' => true,
  239. ];
  240. }
  241. // We analyse the path to extract what to browse.
  242. $trail = explode('/', $path);
  243. $uri = array_pop($trail);
  244. list($id, $name) = helper::explode_node_path($uri);
  245. $service = new repository_googledocs\rest($this->get_user_oauth_client());
  246. // Define the content class object and query which will be used to get the contents for this path.
  247. if ($id === self::SEARCH_ROOT_ID) {
  248. // The special keyword 'search' is the ID of the node. This is possible as we can set up a breadcrumb in
  249. // the search results. Therefore, we should use the content search object to get the results from the
  250. // previously performed search.
  251. $contentobj = new googledocs_content_search($service, $path);
  252. // We need to deconstruct the node name in order to obtain the search term and use it as a query.
  253. $query = str_replace(get_string('searchfor', 'repository_googledocs'), '', $name);
  254. $query = trim(str_replace("'", "", $query));
  255. } else {
  256. // Otherwise, return and use the appropriate (based on the path) content browser object.
  257. $contentobj = helper::get_browser($service, $path);
  258. // Use the node ID as a query.
  259. $query = $id;
  260. }
  261. return [
  262. 'dynload' => true,
  263. 'defaultreturntype' => $this->default_returntype(),
  264. 'path' => $contentobj->get_navigation(),
  265. 'list' => $contentobj->get_content_nodes($query, [$this, 'filter']),
  266. 'manage' => 'https://drive.google.com/',
  267. ];
  268. }
  269. /**
  270. * Search throughout the Google Drive.
  271. *
  272. * @param string $searchtext text to search for.
  273. * @param int $page search page.
  274. * @return array of results.
  275. */
  276. public function search($searchtext, $page = 0) {
  277. // Construct the path to the repository root.
  278. $pluginname = get_string('pluginname', 'repository_googledocs');
  279. $rootpath = helper::build_node_path(self::REPOSITORY_ROOT_ID, $pluginname);
  280. // Construct the path to the search results node.
  281. // Currently, when constructing the search node name, the search term is concatenated to the lang string.
  282. // This was done deliberately so that we can easily and accurately obtain the search term from the search node
  283. // name later when navigating to the search results through the breadcrumb navigation.
  284. $name = get_string('searchfor', 'repository_googledocs') . " '{$searchtext}'";
  285. $path = helper::build_node_path(self::SEARCH_ROOT_ID, $name, $rootpath);
  286. $service = new repository_googledocs\rest($this->get_user_oauth_client());
  287. $searchobj = new googledocs_content_search($service, $path);
  288. return [
  289. 'dynload' => true,
  290. 'path' => $searchobj->get_navigation(),
  291. 'list' => $searchobj->get_content_nodes($searchtext, [$this, 'filter']),
  292. 'manage' => 'https://drive.google.com/',
  293. ];
  294. }
  295. /**
  296. * Query Google Drive for files and folders using a search query.
  297. *
  298. * Documentation about the query format can be found here:
  299. * https://developers.google.com/drive/search-parameters
  300. *
  301. * This returns a list of files and folders with their details as they should be
  302. * formatted and returned by functions such as get_listing() or search().
  303. *
  304. * @deprecated since Moodle 3.11.
  305. * @param string $q search query as expected by the Google API.
  306. * @param string $path parent path of the current files, will not be used for the query.
  307. * @param int $page page.
  308. * @return array of files and folders.
  309. */
  310. protected function query($q, $path = null, $page = 0) {
  311. debugging('The function query() is deprecated, please use get_content_nodes() from the ' .
  312. 'googledocs repository content classes instead.', DEBUG_DEVELOPER);
  313. global $OUTPUT;
  314. $files = array();
  315. $folders = array();
  316. $config = get_config('googledocs');
  317. $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)";
  318. $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive');
  319. try {
  320. // Retrieving files and folders.
  321. $client = $this->get_user_oauth_client();
  322. $service = new repository_googledocs\rest($client);
  323. $response = $service->call('list', $params);
  324. } catch (Exception $e) {
  325. if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
  326. // This is raised when the service Drive API has not been enabled on Google APIs control panel.
  327. throw new repository_exception('servicenotenabled', 'repository_googledocs');
  328. } else {
  329. throw $e;
  330. }
  331. }
  332. $gfiles = isset($response->files) ? $response->files : array();
  333. foreach ($gfiles as $gfile) {
  334. if ($gfile->mimeType == 'application/vnd.google-apps.folder') {
  335. // This is a folder.
  336. $folders[$gfile->name . $gfile->id] = array(
  337. 'title' => $gfile->name,
  338. 'path' => $this->build_node_path($gfile->id, $gfile->name, $path),
  339. 'date' => strtotime($gfile->modifiedTime),
  340. 'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
  341. 'thumbnail_height' => 64,
  342. 'thumbnail_width' => 64,
  343. 'children' => array()
  344. );
  345. } else {
  346. // This is a file.
  347. $link = isset($gfile->webViewLink) ? $gfile->webViewLink : '';
  348. if (empty($link)) {
  349. $link = isset($gfile->webContentLink) ? $gfile->webContentLink : '';
  350. }
  351. if (isset($gfile->fileExtension)) {
  352. // The file has an extension, therefore we can download it.
  353. $source = json_encode([
  354. 'id' => $gfile->id,
  355. 'name' => $gfile->name,
  356. 'exportformat' => 'download',
  357. 'link' => $link
  358. ]);
  359. $title = $gfile->name;
  360. } else {
  361. // The file is probably a Google Doc file, we get the corresponding export link.
  362. // This should be improved by allowing the user to select the type of export they'd like.
  363. $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType);
  364. $title = '';
  365. $exporttype = '';
  366. $types = get_mimetypes_array();
  367. switch ($type){
  368. case 'document':
  369. $ext = $config->documentformat;
  370. $title = $gfile->name . '.gdoc';
  371. if ($ext === 'rtf') {
  372. // Moodle user 'text/rtf' as the MIME type for RTF files.
  373. // Google uses 'application/rtf' for the same type of file.
  374. // See https://developers.google.com/drive/v3/web/manage-downloads.
  375. $exporttype = 'application/rtf';
  376. } else {
  377. $exporttype = $types[$ext]['type'];
  378. }
  379. break;
  380. case 'presentation':
  381. $ext = $config->presentationformat;
  382. $title = $gfile->name . '.gslides';
  383. $exporttype = $types[$ext]['type'];
  384. break;
  385. case 'spreadsheet':
  386. $ext = $config->spreadsheetformat;
  387. $title = $gfile->name . '.gsheet';
  388. $exporttype = $types[$ext]['type'];
  389. break;
  390. case 'drawing':
  391. $ext = $config->drawingformat;
  392. $title = $gfile->name . '.'. $ext;
  393. $exporttype = $types[$ext]['type'];
  394. break;
  395. }
  396. // Skips invalid/unknown types.
  397. if (empty($title)) {
  398. continue;
  399. }
  400. $source = json_encode([
  401. 'id' => $gfile->id,
  402. 'exportformat' => $exporttype,
  403. 'link' => $link,
  404. 'name' => $gfile->name
  405. ]);
  406. }
  407. // Adds the file to the file list. Using the itemId along with the name as key
  408. // of the array because Google Drive allows files with identical names.
  409. $thumb = '';
  410. if (isset($gfile->thumbnailLink)) {
  411. $thumb = $gfile->thumbnailLink;
  412. } else if (isset($gfile->iconLink)) {
  413. $thumb = $gfile->iconLink;
  414. }
  415. $files[$title . $gfile->id] = array(
  416. 'title' => $title,
  417. 'source' => $source,
  418. 'date' => strtotime($gfile->modifiedTime),
  419. 'size' => isset($gfile->size) ? $gfile->size : null,
  420. 'thumbnail' => $thumb,
  421. 'thumbnail_height' => 64,
  422. 'thumbnail_width' => 64,
  423. );
  424. }
  425. }
  426. // Filter and order the results.
  427. $files = array_filter($files, array($this, 'filter'));
  428. core_collator::ksort($files, core_collator::SORT_NATURAL);
  429. core_collator::ksort($folders, core_collator::SORT_NATURAL);
  430. return array_merge(array_values($folders), array_values($files));
  431. }
  432. /**
  433. * Logout.
  434. *
  435. * @return string
  436. */
  437. public function logout() {
  438. $client = $this->get_user_oauth_client();
  439. $client->log_out();
  440. return parent::logout();
  441. }
  442. /**
  443. * Get a file.
  444. *
  445. * @param string $reference reference of the file.
  446. * @param string $file name to save the file to.
  447. * @return string JSON encoded array of information about the file.
  448. */
  449. public function get_file($reference, $filename = '') {
  450. global $CFG;
  451. if (!$this->issuer->get('enabled')) {
  452. throw new repository_exception('cannotdownload', 'repository');
  453. }
  454. $source = json_decode($reference);
  455. $client = null;
  456. if (!empty($source->usesystem)) {
  457. $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
  458. } else {
  459. $client = $this->get_user_oauth_client();
  460. }
  461. $base = 'https://www.googleapis.com/drive/v3';
  462. $newfilename = false;
  463. if ($source->exportformat == 'download') {
  464. $params = ['alt' => 'media'];
  465. $sourceurl = new moodle_url($base . '/files/' . $source->id, $params);
  466. $source = $sourceurl->out(false);
  467. } else {
  468. $params = ['mimeType' => $source->exportformat];
  469. $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params);
  470. $types = get_mimetypes_array();
  471. $checktype = $source->exportformat;
  472. if ($checktype == 'application/rtf') {
  473. $checktype = 'text/rtf';
  474. }
  475. foreach ($types as $extension => $info) {
  476. if ($info['type'] == $checktype) {
  477. $newfilename = $source->name . '.' . $extension;
  478. break;
  479. }
  480. }
  481. $source = $sourceurl->out(false);
  482. }
  483. // We use download_one and not the rest API because it has special timeouts etc.
  484. $path = $this->prepare_file($filename);
  485. $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
  486. $success = $client->download_one($source, null, $options);
  487. if ($success) {
  488. @chmod($path, $CFG->filepermissions);
  489. $result = [
  490. 'path' => $path,
  491. 'url' => $reference,
  492. ];
  493. if (!empty($newfilename)) {
  494. $result['newfilename'] = $newfilename;
  495. }
  496. return $result;
  497. }
  498. throw new repository_exception('cannotdownload', 'repository');
  499. }
  500. /**
  501. * Prepare file reference information.
  502. *
  503. * We are using this method to clean up the source to make sure that it
  504. * is a valid source.
  505. *
  506. * @param string $source of the file.
  507. * @return string file reference.
  508. */
  509. public function get_file_reference($source) {
  510. // We could do some magic upgrade code here.
  511. return $source;
  512. }
  513. /**
  514. * What kind of files will be in this repository?
  515. *
  516. * @return array return '*' means this repository support any files, otherwise
  517. * return mimetypes of files, it can be an array
  518. */
  519. public function supported_filetypes() {
  520. return '*';
  521. }
  522. /**
  523. * Tells how the file can be picked from this repository.
  524. *
  525. * @return int
  526. */
  527. public function supported_returntypes() {
  528. // We can only support references if the system account is connected.
  529. if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) {
  530. $setting = get_config('googledocs', 'supportedreturntypes');
  531. if ($setting == 'internal') {
  532. return FILE_INTERNAL;
  533. } else if ($setting == 'external') {
  534. return FILE_CONTROLLED_LINK;
  535. } else {
  536. return FILE_CONTROLLED_LINK | FILE_INTERNAL;
  537. }
  538. } else {
  539. return FILE_INTERNAL;
  540. }
  541. }
  542. /**
  543. * Which return type should be selected by default.
  544. *
  545. * @return int
  546. */
  547. public function default_returntype() {
  548. $setting = get_config('googledocs', 'defaultreturntype');
  549. $supported = get_config('googledocs', 'supportedreturntypes');
  550. if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') {
  551. return FILE_INTERNAL;
  552. } else {
  553. return FILE_CONTROLLED_LINK;
  554. }
  555. }
  556. /**
  557. * Return names of the general options.
  558. * By default: no general option name.
  559. *
  560. * @return array
  561. */
  562. public static function get_type_option_names() {
  563. return array('issuerid', 'pluginname',
  564. 'documentformat', 'drawingformat',
  565. 'presentationformat', 'spreadsheetformat',
  566. 'defaultreturntype', 'supportedreturntypes');
  567. }
  568. /**
  569. * Store the access token.
  570. */
  571. public function callback() {
  572. $client = $this->get_user_oauth_client();
  573. // This will upgrade to an access token if we have an authorization code and save the access token in the session.
  574. $client->is_logged_in();
  575. }
  576. /**
  577. * Repository method to serve the referenced file
  578. *
  579. * @see send_stored_file
  580. *
  581. * @param stored_file $storedfile the file that contains the reference
  582. * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
  583. * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
  584. * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
  585. * @param array $options additional options affecting the file serving
  586. */
  587. public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
  588. if (!$this->issuer->get('enabled')) {
  589. throw new repository_exception('cannotdownload', 'repository');
  590. }
  591. $source = json_decode($storedfile->get_reference());
  592. $fb = get_file_browser();
  593. $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST);
  594. $info = $fb->get_file_info($context,
  595. $storedfile->get_component(),
  596. $storedfile->get_filearea(),
  597. $storedfile->get_itemid(),
  598. $storedfile->get_filepath(),
  599. $storedfile->get_filename());
  600. if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
  601. // Add the current user as an OAuth writer.
  602. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
  603. if ($systemauth === false) {
  604. $details = 'Cannot connect as system user';
  605. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  606. }
  607. $systemservice = new repository_googledocs\rest($systemauth);
  608. // Get the user oauth so we can get the account to add.
  609. $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(),
  610. $storedfile->get_component(),
  611. $storedfile->get_filearea(),
  612. $storedfile->get_itemid(),
  613. $storedfile->get_filepath(),
  614. $storedfile->get_filename(),
  615. $forcedownload);
  616. $url->param('sesskey', sesskey());
  617. $param = ($options['embed'] == true) ? false : $url;
  618. $userauth = $this->get_user_oauth_client($param);
  619. if (!$userauth->is_logged_in()) {
  620. if ($options['embed'] == true) {
  621. // Due to Same-origin policy, we cannot redirect to googledocs login page.
  622. // If the requested file is embed and the user is not logged in, add option to log in using a popup.
  623. $this->print_login_popup(['style' => 'margin-top: 250px']);
  624. exit;
  625. }
  626. redirect($userauth->get_login_url());
  627. }
  628. if ($userauth === false) {
  629. $details = 'Cannot connect as current user';
  630. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  631. }
  632. $userinfo = $userauth->get_userinfo();
  633. $useremail = $userinfo['email'];
  634. $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
  635. }
  636. if (!empty($options['offline'])) {
  637. $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename());
  638. $filename = $storedfile->get_filename();
  639. if (isset($downloaded['newfilename'])) {
  640. $filename = $downloaded['newfilename'];
  641. }
  642. send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options);
  643. } else if ($source->link) {
  644. // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
  645. header('Location: ' . $source->link);
  646. } else {
  647. $details = 'File is missing source link';
  648. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  649. }
  650. }
  651. /**
  652. * See if a folder exists within a folder
  653. *
  654. * @param \repository_googledocs\rest $client Authenticated client.
  655. * @param string $foldername The folder we are looking for.
  656. * @param string $parentid The parent folder we are looking in.
  657. * @return string|boolean The file id if it exists or false.
  658. */
  659. protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) {
  660. $q = '\'' . addslashes($parentid) . '\' in parents and trashed = false and name = \'' . addslashes($foldername). '\'';
  661. $fields = 'files(id, name)';
  662. $params = [ 'q' => $q, 'fields' => $fields];
  663. $response = $client->call('list', $params);
  664. $missing = true;
  665. foreach ($response->files as $child) {
  666. if ($child->name == $foldername) {
  667. return $child->id;
  668. }
  669. }
  670. return false;
  671. }
  672. /**
  673. * Create a folder within a folder
  674. *
  675. * @param \repository_googledocs\rest $client Authenticated client.
  676. * @param string $foldername The folder we are creating.
  677. * @param string $parentid The parent folder we are creating in.
  678. *
  679. * @return string The file id of the new folder.
  680. */
  681. protected function create_folder_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) {
  682. $fields = 'id';
  683. $params = ['fields' => $fields];
  684. $folder = ['mimeType' => 'application/vnd.google-apps.folder', 'name' => $foldername, 'parents' => [$parentid]];
  685. $created = $client->call('create', $params, json_encode($folder));
  686. if (empty($created->id)) {
  687. $details = 'Cannot create folder:' . $foldername;
  688. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  689. }
  690. return $created->id;
  691. }
  692. /**
  693. * Get simple file info for humans.
  694. *
  695. * @param \repository_googledocs\rest $client Authenticated client.
  696. * @param string $fileid The file we are querying.
  697. *
  698. * @return stdClass
  699. */
  700. protected function get_file_summary(\repository_googledocs\rest $client, $fileid) {
  701. $fields = "id,name,owners,parents";
  702. $params = [
  703. 'fileid' => $fileid,
  704. 'fields' => $fields
  705. ];
  706. return $client->call('get', $params);
  707. }
  708. /**
  709. * Copy a file and return the new file details. A side effect of the copy
  710. * is that the owner will be the account authenticated with this oauth client.
  711. *
  712. * @param \repository_googledocs\rest $client Authenticated client.
  713. * @param string $fileid The file we are copying.
  714. * @param string $name The original filename (don't change it).
  715. *
  716. * @return stdClass file details.
  717. */
  718. protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) {
  719. $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink";
  720. $params = [
  721. 'fileid' => $fileid,
  722. 'fields' => $fields,
  723. ];
  724. // Keep the original name (don't put copy at the end of it).
  725. $copyinfo = [];
  726. if (!empty($name)) {
  727. $copyinfo = [ 'name' => $name ];
  728. }
  729. $fileinfo = $client->call('copy', $params, json_encode($copyinfo));
  730. if (empty($fileinfo->id)) {
  731. $details = 'Cannot copy file:' . $fileid;
  732. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  733. }
  734. return $fileinfo;
  735. }
  736. /**
  737. * Add a writer to the permissions on the file (temporary).
  738. *
  739. * @param \repository_googledocs\rest $client Authenticated client.
  740. * @param string $fileid The file we are updating.
  741. * @param string $email The email of the writer account to add.
  742. * @return boolean
  743. */
  744. protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
  745. // Expires in 7 days.
  746. $expires = new DateTime();
  747. $expires->add(new DateInterval("P7D"));
  748. $updateeditor = [
  749. 'emailAddress' => $email,
  750. 'role' => 'writer',
  751. 'type' => 'user',
  752. 'expirationTime' => $expires->format(DateTime::RFC3339)
  753. ];
  754. $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
  755. $response = $client->call('create_permission', $params, json_encode($updateeditor));
  756. if (empty($response->id)) {
  757. $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
  758. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  759. }
  760. return true;
  761. }
  762. /**
  763. * Add a writer to the permissions on the file.
  764. *
  765. * @param \repository_googledocs\rest $client Authenticated client.
  766. * @param string $fileid The file we are updating.
  767. * @param string $email The email of the writer account to add.
  768. * @return boolean
  769. */
  770. protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
  771. $updateeditor = [
  772. 'emailAddress' => $email,
  773. 'role' => 'writer',
  774. 'type' => 'user'
  775. ];
  776. $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
  777. $response = $client->call('create_permission', $params, json_encode($updateeditor));
  778. if (empty($response->id)) {
  779. $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
  780. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  781. }
  782. return true;
  783. }
  784. /**
  785. * Move from root to folder
  786. *
  787. * @param \repository_googledocs\rest $client Authenticated client.
  788. * @param string $fileid The file we are updating.
  789. * @param string $folderid The id of the folder we are moving to
  790. * @return boolean
  791. */
  792. protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) {
  793. // Set the parent.
  794. $params = [
  795. 'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root'
  796. ];
  797. $response = $client->call('update', $params, ' ');
  798. if (empty($response->id)) {
  799. $details = 'Cannot move the file to a folder: ' . $fileid;
  800. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  801. }
  802. return true;
  803. }
  804. /**
  805. * Prevent writers from sharing.
  806. *
  807. * @param \repository_googledocs\rest $client Authenticated client.
  808. * @param string $fileid The file we are updating.
  809. * @return boolean
  810. */
  811. protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) {
  812. // We don't want anyone but Moodle to change the sharing settings.
  813. $params = [
  814. 'fileid' => $fileid
  815. ];
  816. $update = [
  817. 'writersCanShare' => false
  818. ];
  819. $response = $client->call('update', $params, json_encode($update));
  820. if (empty($response->id)) {
  821. $details = 'Cannot prevent writers from sharing document: ' . $fileid;
  822. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  823. }
  824. return true;
  825. }
  826. /**
  827. * Allow anyone with the link to read the file.
  828. *
  829. * @param \repository_googledocs\rest $client Authenticated client.
  830. * @param string $fileid The file we are updating.
  831. * @return boolean
  832. */
  833. protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) {
  834. $updateread = [
  835. 'type' => 'anyone',
  836. 'role' => 'reader',
  837. 'allowFileDiscovery' => 'false'
  838. ];
  839. $params = ['fileid' => $fileid];
  840. $response = $client->call('create_permission', $params, json_encode($updateread));
  841. if (empty($response->id) || $response->id != 'anyoneWithLink') {
  842. $details = 'Cannot update link sharing for the document: ' . $fileid;
  843. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  844. }
  845. return true;
  846. }
  847. /**
  848. * Called when a file is selected as a "link".
  849. * Invoked at MOODLE/repository/repository_ajax.php
  850. *
  851. * This is called at the point the reference files are being copied from the draft area to the real area
  852. * (when the file has really really been selected.
  853. *
  854. * @param string $reference this reference is generated by
  855. * repository::get_file_reference()
  856. * @param context $context the target context for this new file.
  857. * @param string $component the target component for this new file.
  858. * @param string $filearea the target filearea for this new file.
  859. * @param string $itemid the target itemid for this new file.
  860. * @return string updated reference (final one before it's saved to db).
  861. */
  862. public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
  863. global $CFG, $SITE;
  864. // What we need to do here is transfer ownership to the system user (or copy)
  865. // then set the permissions so anyone with the share link can view,
  866. // finally update the reference to contain the share link if it was not
  867. // already there (and point to new file id if we copied).
  868. // Get the details from the reference.
  869. $source = json_decode($reference);
  870. if (!empty($source->usesystem)) {
  871. // If we already copied this file to the system account - we are done.
  872. return $reference;
  873. }
  874. // Check this issuer is enabled.
  875. if ($this->disabled) {
  876. throw new repository_exception('cannotdownload', 'repository');
  877. }
  878. // Get a system oauth client and a user oauth client.
  879. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
  880. if ($systemauth === false) {
  881. $details = 'Cannot connect as system user';
  882. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  883. }
  884. // Get the system user email so we can share the file with this user.
  885. $systemuserinfo = $systemauth->get_userinfo();
  886. $systemuseremail = $systemuserinfo['email'];
  887. $userauth = $this->get_user_oauth_client();
  888. if ($userauth === false) {
  889. $details = 'Cannot connect as current user';
  890. throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
  891. }
  892. $userservice = new repository_googledocs\rest($userauth);
  893. $systemservice = new repository_googledocs\rest($systemauth);
  894. // Add Moodle as writer.
  895. $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
  896. // Now move it to a sensible folder.
  897. $contextlist = array_reverse($context->get_parent_contexts(true));
  898. $cache = cache::make('repository_googledocs', 'folder');
  899. $parentid = 'root';
  900. $fullpath = 'root';
  901. $allfolders = [];
  902. foreach ($contextlist as $context) {
  903. // Prepare human readable context folders names, making sure they are still unique within the site.
  904. $prevlang = force_current_language($CFG->lang);
  905. $foldername = $context->get_context_name();
  906. force_current_language($prevlang);
  907. if ($context->contextlevel == CONTEXT_SYSTEM) {
  908. // Append the site short name to the root folder.
  909. $foldername .= ' ('.$SITE->shortname.')';
  910. // Append the relevant object id.
  911. } else if ($context->instanceid) {
  912. $foldername .= ' (id '.$context->instanceid.')';
  913. } else {
  914. // This does not really happen but just in case.
  915. $foldername .= ' (ctx '.$context->id.')';
  916. }
  917. $foldername = clean_param($foldername, PARAM_PATH);
  918. $allfolders[] = $foldername;
  919. }
  920. $allfolders[] = clean_param($component, PARAM_PATH);
  921. $allfolders[] = clean_param($filearea, PARAM_PATH);
  922. $allfolders[] = clean_param($itemid, PARAM_PATH);
  923. // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder.
  924. foreach ($allfolders as $foldername) {
  925. // Make sure a folder exists here.
  926. $fullpath .= '/' . $foldername;
  927. $folderid = $cache->get($fullpath);
  928. if (empty($folderid)) {
  929. $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid);
  930. }
  931. if ($folderid !== false) {
  932. $cache->set($fullpath, $folderid);
  933. $parentid = $folderid;
  934. } else {
  935. // Create it.
  936. $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid);
  937. $cache->set($fullpath, $parentid);
  938. }
  939. }
  940. // Copy the file so we get a snapshot file owned by Moodle.
  941. $newsource = $this->copy_file($systemservice, $source->id, $source->name);
  942. // Move the copied file to the correct folder.
  943. $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid);
  944. // Set the sharing options.
  945. $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id);
  946. $this->prevent_writers_from_sharing_file($systemservice, $newsource->id);
  947. // Update the returned reference so that the stored_file in moodle points to the newly copied file.
  948. $source->id = $newsource->id;
  949. $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : '';
  950. $source->usesystem = true;
  951. if (empty($source->link)) {
  952. $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : '';
  953. }
  954. $reference = json_encode($source);
  955. return $reference;
  956. }
  957. /**
  958. * Get human readable file info from a the reference.
  959. *
  960. * @param string $reference
  961. * @param int $filestatus
  962. */
  963. public function get_reference_details($reference, $filestatus = 0) {
  964. if ($this->disabled) {
  965. throw new repository_exception('cannotdownload', 'repository');
  966. }
  967. if (empty($reference)) {
  968. return get_string('unknownsource', 'repository');
  969. }
  970. $source = json_decode($reference);
  971. if (empty($source->usesystem)) {
  972. return '';
  973. }
  974. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
  975. if ($systemauth === false) {
  976. return '';
  977. }
  978. $systemservice = new repository_googledocs\rest($systemauth);
  979. $info = $this->get_file_summary($systemservice, $source->id);
  980. $owner = '';
  981. if (!empty($info->owners[0]->displayName)) {
  982. $owner = $info->owners[0]->displayName;
  983. }
  984. if ($owner) {
  985. return get_string('owner', 'repository_googledocs', $owner);
  986. } else {
  987. return $info->name;
  988. }
  989. }
  990. /**
  991. * Edit/Create Admin Settings Moodle form.
  992. *
  993. * @param moodleform $mform Moodle form (passed by reference).
  994. * @param string $classname repository class name.
  995. */
  996. public static function type_config_form($mform, $classname = 'repository') {
  997. $url = new moodle_url('/admin/tool/oauth2/issuers.php');
  998. $url = $url->out();
  999. $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url));
  1000. parent::type_config_form($mform);
  1001. $options = [];
  1002. $issuers = \core\oauth2\api::get_all_issuers();
  1003. foreach ($issuers as $issuer) {
  1004. $options[$issuer->get('id')] = s($issuer->get('name'));
  1005. }
  1006. $strrequired = get_string('required');
  1007. $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options);
  1008. $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs');
  1009. $mform->addRule('issuerid', $strrequired, 'required', null, 'client');
  1010. $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs'));
  1011. $choices = [
  1012. 'internal' => get_string('internal', 'repository_googledocs'),
  1013. 'external' => get_string('external', 'repository_googledocs'),
  1014. 'both' => get_string('both', 'repository_googledocs')
  1015. ];
  1016. $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices);
  1017. $choices = [
  1018. FILE_INTERNAL => get_string('internal', 'repository_googledocs'),
  1019. FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'),
  1020. ];
  1021. $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices);
  1022. $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs'));
  1023. // Documents.
  1024. $docsformat = array();
  1025. $docsformat['html'] = 'html';
  1026. $docsformat['docx'] = 'docx';
  1027. $docsformat['odt'] = 'odt';
  1028. $docsformat['pdf'] = 'pdf';
  1029. $docsformat['rtf'] = 'rtf';
  1030. $docsformat['txt'] = 'txt';
  1031. core_collator::ksort($docsformat, core_collator::SORT_NATURAL);
  1032. $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat);
  1033. $mform->setDefault('documentformat', $docsformat['rtf']);
  1034. $mform->setType('documentformat', PARAM_ALPHANUM);
  1035. // Drawing.
  1036. $drawingformat = array();
  1037. $drawingformat['jpeg'] = 'jpeg';
  1038. $drawingformat['png'] = 'png';
  1039. $drawingformat['svg'] = 'svg';
  1040. $drawingformat['pdf'] = 'pdf';
  1041. core_collator::ksort($drawingformat, core_collator::SORT_NATURAL);
  1042. $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat);
  1043. $mform->setDefault('drawingformat', $drawingformat['pdf']);
  1044. $mform->setType('drawingformat', PARAM_ALPHANUM);
  1045. // Presentation.
  1046. $presentationformat = array();
  1047. $presentationformat['pdf'] = 'pdf';
  1048. $presentationformat['pptx'] = 'pptx';
  1049. $presentationformat['txt'] = 'txt';
  1050. core_collator::ksort($presentationformat, core_collator::SORT_NATURAL);
  1051. $str = get_string('presentationformat', 'repository_googledocs');
  1052. $mform->addElement('select', 'presentationformat', $str, $presentationformat);
  1053. $mform->setDefault('presentationformat', $presentationformat['pptx']);
  1054. $mform->setType('presentationformat', PARAM_ALPHANUM);
  1055. // Spreadsheet.
  1056. $spreadsheetformat = array();
  1057. $spreadsheetformat['csv'] = 'csv';
  1058. $spreadsheetformat['ods'] = 'ods';
  1059. $spreadsheetformat['pdf'] = 'pdf';
  1060. $spreadsheetformat['xlsx'] = 'xlsx';
  1061. core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL);
  1062. $str = get_string('spreadsheetformat', 'repository_googledocs');
  1063. $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat);
  1064. $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']);
  1065. $mform->setType('spreadsheetformat', PARAM_ALPHANUM);
  1066. }
  1067. }
  1068. /**
  1069. * Callback to get the required scopes for system account.
  1070. *
  1071. * @param \core\oauth2\issuer $issuer
  1072. * @return string
  1073. */
  1074. function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
  1075. if ($issuer->get('id') == get_config('googledocs', 'issuerid')) {
  1076. return 'https://www.googleapis.com/auth/drive';
  1077. }
  1078. return '';
  1079. }