PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/repository/googledocs/lib.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 506 lines | 267 code | 46 blank | 193 comment | 29 complexity | 0dd077dfcc4ccb6d9658add5ab7d46c1 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-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 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 . '/google/Google_Client.php');
  27. require_once($CFG->libdir . '/google/contrib/Google_DriveService.php');
  28. /**
  29. * Google Docs Plugin
  30. *
  31. * @since 2.0
  32. * @package repository_googledocs
  33. * @copyright 2009 Dan Poltawski <talktodan@gmail.com>
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class repository_googledocs extends repository {
  37. /**
  38. * Google Client.
  39. * @var Google_Client
  40. */
  41. private $client = null;
  42. /**
  43. * Google Drive Service.
  44. * @var Google_DriveService
  45. */
  46. private $service = null;
  47. /**
  48. * Session key to store the accesstoken.
  49. * @var string
  50. */
  51. const SESSIONKEY = 'googledrive_accesstoken';
  52. /**
  53. * URI to the callback file for OAuth.
  54. * @var string
  55. */
  56. const CALLBACKURL = '/admin/oauth2callback.php';
  57. /**
  58. * Constructor.
  59. *
  60. * @param int $repositoryid repository instance id.
  61. * @param int|stdClass $context a context id or context object.
  62. * @param array $options repository options.
  63. * @param int $readonly indicate this repo is readonly or not.
  64. * @return void
  65. */
  66. public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
  67. parent::__construct($repositoryid, $context, $options, $readonly = 0);
  68. $callbackurl = new moodle_url(self::CALLBACKURL);
  69. $this->client = new Google_Client();
  70. $this->client->setClientId(get_config('googledocs', 'clientid'));
  71. $this->client->setClientSecret(get_config('googledocs', 'secret'));
  72. $this->client->setScopes(array('https://www.googleapis.com/auth/drive.readonly'));
  73. $this->client->setRedirectUri($callbackurl->out(false));
  74. $this->service = new Google_DriveService($this->client);
  75. $this->check_login();
  76. }
  77. /**
  78. * Returns the access token if any.
  79. *
  80. * @return string|null access token.
  81. */
  82. protected function get_access_token() {
  83. global $SESSION;
  84. if (isset($SESSION->{self::SESSIONKEY})) {
  85. return $SESSION->{self::SESSIONKEY};
  86. }
  87. return null;
  88. }
  89. /**
  90. * Store the access token in the session.
  91. *
  92. * @param string $token token to store.
  93. * @return void
  94. */
  95. protected function store_access_token($token) {
  96. global $SESSION;
  97. $SESSION->{self::SESSIONKEY} = $token;
  98. }
  99. /**
  100. * Callback method during authentication.
  101. *
  102. * @return void
  103. */
  104. public function callback() {
  105. if ($code = optional_param('oauth2code', null, PARAM_RAW)) {
  106. $this->client->authenticate($code);
  107. $this->store_access_token($this->client->getAccessToken());
  108. }
  109. }
  110. /**
  111. * Checks whether the user is authenticate or not.
  112. *
  113. * @return bool true when logged in.
  114. */
  115. public function check_login() {
  116. if ($token = $this->get_access_token()) {
  117. $this->client->setAccessToken($token);
  118. return true;
  119. }
  120. return false;
  121. }
  122. /**
  123. * Print or return the login form.
  124. *
  125. * @return void|array for ajax.
  126. */
  127. public function print_login() {
  128. $returnurl = new moodle_url('/repository/repository_callback.php');
  129. $returnurl->param('callback', 'yes');
  130. $returnurl->param('repo_id', $this->id);
  131. $returnurl->param('sesskey', sesskey());
  132. $url = new moodle_url($this->client->createAuthUrl());
  133. $url->param('state', $returnurl->out_as_local_url(false));
  134. if ($this->options['ajax']) {
  135. $popup = new stdClass();
  136. $popup->type = 'popup';
  137. $popup->url = $url->out(false);
  138. return array('login' => array($popup));
  139. } else {
  140. echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
  141. }
  142. }
  143. /**
  144. * Build the breadcrumb from a path.
  145. *
  146. * @param string $path to create a breadcrumb from.
  147. * @return array containing name and path of each crumb.
  148. */
  149. protected function build_breadcrumb($path) {
  150. $bread = explode('/', $path);
  151. $crumbtrail = '';
  152. foreach ($bread as $crumb) {
  153. list($id, $name) = $this->explode_node_path($crumb);
  154. $name = empty($name) ? $id : $name;
  155. $breadcrumb[] = array(
  156. 'name' => $name,
  157. 'path' => $this->build_node_path($id, $name, $crumbtrail)
  158. );
  159. $tmp = end($breadcrumb);
  160. $crumbtrail = $tmp['path'];
  161. }
  162. return $breadcrumb;
  163. }
  164. /**
  165. * Generates a safe path to a node.
  166. *
  167. * Typically, a node will be id|Name of the node.
  168. *
  169. * @param string $id of the node.
  170. * @param string $name of the node, will be URL encoded.
  171. * @param string $root to append the node on, must be a result of this function.
  172. * @return string path to the node.
  173. */
  174. protected function build_node_path($id, $name = '', $root = '') {
  175. $path = $id;
  176. if (!empty($name)) {
  177. $path .= '|' . urlencode($name);
  178. }
  179. if (!empty($root)) {
  180. $path = trim($root, '/') . '/' . $path;
  181. }
  182. return $path;
  183. }
  184. /**
  185. * Returns information about a node in a path.
  186. *
  187. * @see self::build_node_path()
  188. * @param string $node to extrat information from.
  189. * @return array about the node.
  190. */
  191. protected function explode_node_path($node) {
  192. if (strpos($node, '|') !== false) {
  193. list($id, $name) = explode('|', $node, 2);
  194. $name = urldecode($name);
  195. } else {
  196. $id = $node;
  197. $name = '';
  198. }
  199. $id = urldecode($id);
  200. return array(
  201. 0 => $id,
  202. 1 => $name,
  203. 'id' => $id,
  204. 'name' => $name
  205. );
  206. }
  207. /**
  208. * List the files and folders.
  209. *
  210. * @param string $path path to browse.
  211. * @param string $page page to browse.
  212. * @return array of result.
  213. */
  214. public function get_listing($path='', $page = '') {
  215. if (empty($path)) {
  216. $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
  217. }
  218. // We analyse the path to extract what to browse.
  219. $trail = explode('/', $path);
  220. $uri = array_pop($trail);
  221. list($id, $name) = $this->explode_node_path($uri);
  222. // Handle the special keyword 'search', which we defined in self::search() so that
  223. // we could set up a breadcrumb in the search results. In any other case ID would be
  224. // 'root' which is a special keyword set up by Google, or a parent (folder) ID.
  225. if ($id === 'search') {
  226. return $this->search($name);
  227. }
  228. // Query the Drive.
  229. $q = "'" . str_replace("'", "\'", $id) . "' in parents";
  230. $q .= ' AND trashed = false';
  231. $results = $this->query($q, $path);
  232. $ret = array();
  233. $ret['dynload'] = true;
  234. $ret['path'] = $this->build_breadcrumb($path);
  235. $ret['list'] = $results;
  236. return $ret;
  237. }
  238. /**
  239. * Search throughout the Google Drive.
  240. *
  241. * @param string $search_text text to search for.
  242. * @param int $page search page.
  243. * @return array of results.
  244. */
  245. public function search($search_text, $page = 0) {
  246. $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
  247. $path = $this->build_node_path('search', $search_text, $path);
  248. // Query the Drive.
  249. $q = "fullText contains '" . str_replace("'", "\'", $search_text) . "'";
  250. $q .= ' AND trashed = false';
  251. $results = $this->query($q, $path);
  252. $ret = array();
  253. $ret['dynload'] = true;
  254. $ret['path'] = $this->build_breadcrumb($path);
  255. $ret['list'] = $results;
  256. return $ret;
  257. }
  258. /**
  259. * Query Google Drive for files and folders using a search query.
  260. *
  261. * Documentation about the query format can be found here:
  262. * https://developers.google.com/drive/search-parameters
  263. *
  264. * This returns a list of files and folders with their details as they should be
  265. * formatted and returned by functions such as get_listing() or search().
  266. *
  267. * @param string $q search query as expected by the Google API.
  268. * @param string $path parent path of the current files, will not be used for the query.
  269. * @param int $page page.
  270. * @return array of files and folders.
  271. */
  272. protected function query($q, $path = null, $page = 0) {
  273. global $OUTPUT;
  274. $files = array();
  275. $folders = array();
  276. $fields = "items(id,title,mimeType,downloadUrl,fileExtension,exportLinks,modifiedDate,fileSize,thumbnailLink)";
  277. $params = array('q' => $q, 'fields' => $fields);
  278. try {
  279. // Retrieving files and folders.
  280. $response = $this->service->files->listFiles($params);
  281. } catch (Google_ServiceException $e) {
  282. if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
  283. // This is raised when the service Drive API has not been enabled on Google APIs control panel.
  284. throw new repository_exception('servicenotenabled', 'repository_googledocs');
  285. } else {
  286. throw $e;
  287. }
  288. }
  289. $items = isset($response['items']) ? $response['items'] : array();
  290. foreach ($items as $item) {
  291. if ($item['mimeType'] == 'application/vnd.google-apps.folder') {
  292. // This is a folder.
  293. $folders[$item['title'] . $item['id']] = array(
  294. 'title' => $item['title'],
  295. 'path' => $this->build_node_path($item['id'], $item['title'], $path),
  296. 'date' => strtotime($item['modifiedDate']),
  297. 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false),
  298. 'thumbnail_height' => 64,
  299. 'thumbnail_width' => 64,
  300. 'children' => array()
  301. );
  302. } else {
  303. // This is a file.
  304. if (isset($item['fileExtension'])) {
  305. // The file has an extension, therefore there is a download link.
  306. $title = $item['title'];
  307. $source = $item['downloadUrl'];
  308. } else {
  309. // The file is probably a Google Doc file, we get the corresponding export link.
  310. // This should be improved by allowing the user to select the type of export they'd like.
  311. $type = str_replace('application/vnd.google-apps.', '', $item['mimeType']);
  312. $title = '';
  313. $exportType = '';
  314. switch ($type){
  315. case 'document':
  316. $title = $item['title'] . '.rtf';
  317. $exportType = 'application/rtf';
  318. break;
  319. case 'presentation':
  320. $title = $item['title'] . '.pptx';
  321. $exportType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
  322. break;
  323. case 'spreadsheet':
  324. $title = $item['title'] . '.xlsx';
  325. $exportType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  326. break;
  327. }
  328. // Skips invalid/unknown types.
  329. if (empty($title) || !isset($item['exportLinks'][$exportType])) {
  330. continue;
  331. }
  332. $source = $item['exportLinks'][$exportType];
  333. }
  334. // Adds the file to the file list. Using the itemId along with the title as key
  335. // of the array because Google Drive allows files with identical names.
  336. $files[$title . $item['id']] = array(
  337. 'title' => $title,
  338. 'source' => $source,
  339. 'date' => strtotime($item['modifiedDate']),
  340. 'size' => isset($item['fileSize']) ? $item['fileSize'] : null,
  341. 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($title, 64))->out(false),
  342. 'thumbnail_height' => 64,
  343. 'thumbnail_width' => 64,
  344. // Do not use real thumbnails as they wouldn't work if the user disabled 3rd party
  345. // plugins in his browser, or if they're not logged in their Google account.
  346. );
  347. // Sometimes the real thumbnails can't be displayed, for example if 3rd party cookies are disabled
  348. // or if the user is not logged in Google anymore. But this restriction does not seem to be applied
  349. // to a small subset of files.
  350. $extension = strtolower(pathinfo($title, PATHINFO_EXTENSION));
  351. if (isset($item['thumbnailLink']) && in_array($extension, array('jpg', 'png', 'txt', 'pdf'))) {
  352. $files[$title . $item['id']]['realthumbnail'] = $item['thumbnailLink'];
  353. }
  354. }
  355. }
  356. // Filter and order the results.
  357. $files = array_filter($files, array($this, 'filter'));
  358. core_collator::ksort($files, core_collator::SORT_NATURAL);
  359. core_collator::ksort($folders, core_collator::SORT_NATURAL);
  360. return array_merge(array_values($folders), array_values($files));
  361. }
  362. /**
  363. * Logout.
  364. *
  365. * @return string
  366. */
  367. public function logout() {
  368. $this->store_access_token(null);
  369. return parent::logout();
  370. }
  371. /**
  372. * Get a file.
  373. *
  374. * @param string $reference reference of the file.
  375. * @param string $file name to save the file to.
  376. * @return string JSON encoded array of information about the file.
  377. */
  378. public function get_file($reference, $filename = '') {
  379. global $CFG;
  380. $request = new Google_HttpRequest($reference);
  381. $httpRequest = Google_Client::$io->authenticatedRequest($request);
  382. if ($httpRequest->getResponseHttpCode() == 200) {
  383. $path = $this->prepare_file($filename);
  384. $content = $httpRequest->getResponseBody();
  385. if (file_put_contents($path, $content) !== false) {
  386. @chmod($path, $CFG->filepermissions);
  387. return array(
  388. 'path' => $path,
  389. 'url' => $reference
  390. );
  391. }
  392. }
  393. throw new repository_exception('cannotdownload', 'repository');
  394. }
  395. /**
  396. * Prepare file reference information.
  397. *
  398. * We are using this method to clean up the source to make sure that it
  399. * is a valid source.
  400. *
  401. * @param string $source of the file.
  402. * @return string file reference.
  403. */
  404. public function get_file_reference($source) {
  405. return clean_param($source, PARAM_URL);
  406. }
  407. /**
  408. * What kind of files will be in this repository?
  409. *
  410. * @return array return '*' means this repository support any files, otherwise
  411. * return mimetypes of files, it can be an array
  412. */
  413. public function supported_filetypes() {
  414. return '*';
  415. }
  416. /**
  417. * Tells how the file can be picked from this repository.
  418. *
  419. * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE.
  420. *
  421. * @return int
  422. */
  423. public function supported_returntypes() {
  424. return FILE_INTERNAL;
  425. }
  426. /**
  427. * Return names of the general options.
  428. * By default: no general option name.
  429. *
  430. * @return array
  431. */
  432. public static function get_type_option_names() {
  433. return array('clientid', 'secret', 'pluginname');
  434. }
  435. /**
  436. * Edit/Create Admin Settings Moodle form.
  437. *
  438. * @param moodleform $mform Moodle form (passed by reference).
  439. * @param string $classname repository class name.
  440. */
  441. public static function type_config_form($mform, $classname = 'repository') {
  442. $callbackurl = new moodle_url(self::CALLBACKURL);
  443. $a = new stdClass;
  444. $a->docsurl = get_docs_url('Google_OAuth_2.0_setup');
  445. $a->callbackurl = $callbackurl->out(false);
  446. $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_googledocs', $a));
  447. parent::type_config_form($mform);
  448. $mform->addElement('text', 'clientid', get_string('clientid', 'repository_googledocs'));
  449. $mform->setType('clientid', PARAM_RAW_TRIMMED);
  450. $mform->addElement('text', 'secret', get_string('secret', 'repository_googledocs'));
  451. $mform->setType('secret', PARAM_RAW_TRIMMED);
  452. $strrequired = get_string('required');
  453. $mform->addRule('clientid', $strrequired, 'required', null, 'client');
  454. $mform->addRule('secret', $strrequired, 'required', null, 'client');
  455. }
  456. }
  457. // Icon from: http://www.iconspedia.com/icon/google-2706.html.