MDL-52051 repository_dropbox: Migrate to v2 API

This commit is contained in:
Andrew Nicols 2016-07-14 13:12:40 +08:00
parent 3a4c497c15
commit 066963cd18
12 changed files with 1754 additions and 679 deletions

View File

@ -0,0 +1,38 @@
<?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/>.
/**
* Dropbox Authentication exception.
*
* @since Moodle 3.2
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace repository_dropbox;
defined('MOODLE_INTERNAL') || die();
/**
* Dropbox Authentication exception.
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class authentication_exception extends dropbox_exception {
}

View File

@ -0,0 +1,368 @@
<?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/>.
/**
* Dropbox V2 API.
*
* @since Moodle 3.2
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace repository_dropbox;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/oauthlib.php');
/**
* Dropbox V2 API.
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dropbox extends \oauth2_client {
/**
* Create the DropBox API Client.
*
* @param string $key The API key
* @param string $secret The API secret
* @param string $callback The callback URL
*/
public function __construct($key, $secret, $callback) {
parent::__construct($key, $secret, $callback, '');
}
/**
* Returns the auth url for OAuth 2.0 request.
*
* @return string the auth url
*/
protected function auth_url() {
return 'https://www.dropbox.com/oauth2/authorize';
}
/**
* Returns the token url for OAuth 2.0 request.
*
* @return string the auth url
*/
protected function token_url() {
return 'https://api.dropboxapi.com/oauth2/token';
}
/**
* Return the constructed API endpoint URL.
*
* @param string $endpoint The endpoint to be contacted
* @return moodle_url The constructed API URL
*/
protected function get_api_endpoint($endpoint) {
return new \moodle_url('https://api.dropboxapi.com/2/' . $endpoint);
}
/**
* Return the constructed content endpoint URL.
*
* @param string $endpoint The endpoint to be contacted
* @return moodle_url The constructed content URL
*/
protected function get_content_endpoint($endpoint) {
return new \moodle_url('https://api-content.dropbox.com/2/' . $endpoint);
}
/**
* Make an API call against the specified endpoint with supplied data.
*
* @param string $endpoint The endpoint to be contacted
* @param array $data Any data to pass to the endpoint
* @return object Content decoded from the endpoint
*/
protected function fetch_dropbox_data($endpoint, $data = []) {
$url = $this->get_api_endpoint($endpoint);
$this->cleanopt();
$this->resetHeader();
if ($data === null) {
// Some API endpoints explicitly expect a data submission of 'null'.
$options['CURLOPT_POSTFIELDS'] = 'null';
} else {
$options['CURLOPT_POSTFIELDS'] = json_encode($data);
}
$options['CURLOPT_POST'] = 1;
$this->setHeader('Content-Type: application/json');
$response = $this->request($url, $options);
$result = json_decode($response);
$this->check_and_handle_api_errors($result);
if ($this->has_additional_results($result)) {
// Any API endpoint returning 'has_more' will provide a cursor, and also have a matching endpoint suffixed
// with /continue which takes that cursor.
if (preg_match('_/continue$_', $endpoint) === 0) {
// Only add /continue if it is not already present.
$endpoint .= '/continue';
}
// Fetch the next page of results.
$additionaldata = $this->fetch_dropbox_data($endpoint, [
'cursor' => $result->cursor,
]);
// Merge the list of entries.
$result->entries = array_merge($result->entries, $additionaldata->entries);
}
if (isset($result->has_more)) {
// Unset the cursor and has_more flags.
unset($result->cursor);
unset($result->has_more);
}
return $result;
}
/**
* Whether the supplied result is paginated and not the final page.
*
* @param object $result The result of an operation
* @return boolean
*/
public function has_additional_results($result) {
return !empty($result->has_more) && !empty($result->cursor);
}
/**
* Fetch content from the specified endpoint with the supplied data.
*
* @param string $endpoint The endpoint to be contacted
* @param array $data Any data to pass to the endpoint
* @return string The returned data
*/
protected function fetch_dropbox_content($endpoint, $data = []) {
$url = $this->get_content_endpoint($endpoint);
$this->cleanopt();
$this->resetHeader();
$options['CURLOPT_POST'] = 1;
$this->setHeader('Content-Type: ');
$this->setHeader('Dropbox-API-Arg: ' . json_encode($data));
$response = $this->request($url, $options);
$this->check_and_handle_api_errors($response);
return $response;
}
/**
* Check for an attempt to handle API errors.
*
* This function attempts to deal with errors as per
* https://www.dropbox.com/developers/documentation/http/documentation#error-handling.
*
* @param string $data The returned content.
* @throws moodle_exception
*/
protected function check_and_handle_api_errors($data) {
if ($this->info['http_code'] == 200) {
// Dropbox only returns errors on non-200 response codes.
return;
}
switch($this->info['http_code']) {
case 400:
// Bad input parameter. Error message should indicate which one and why.
throw new \coding_exception('Invalid input parameter passed to DropBox API.');
break;
case 401:
// Bad or expired token. This can happen if the access token is expired or if the access token has been
// revoked by Dropbox or the user. To fix this, you should re-authenticate the user.
throw new authentication_exception('Authentication token expired');
break;
case 409:
// Endpoint-specific error. Look to the response body for the specifics of the error.
throw new \coding_exception('Endpoint specific error: ' . $data);
break;
case 429:
// Your app is making too many requests for the given user or team and is being rate limited. Your app
// should wait for the number of seconds specified in the "Retry-After" response header before trying
// again.
throw new rate_limit_exception();
break;
default:
break;
}
if ($this->info['http_code'] >= 500 && $this->info['http_code'] < 600) {
throw new \invalid_response_exception($this->info['http_code'] . ": " . $data);
}
}
/**
* Get file listing from dropbox.
*
* @param string $path The path to query
* @return object The returned directory listing, or null on failure
*/
public function get_listing($path = '') {
if ($path === '/') {
$path = '';
}
$data = $this->fetch_dropbox_data('files/list_folder', [
'path' => $path,
]);
return $data;
}
/**
* Get file search results from dropbox.
*
* @param string $query The search query
* @return object The returned directory listing, or null on failure
*/
public function search($query = '') {
$data = $this->fetch_dropbox_data('files/search', [
'path' => '',
'query' => $query,
]);
return $data;
}
/**
* Whether the entry is expected to have a thumbnail.
* See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail.
*
* @param object $entry The file entry received from the DropBox API
* @return boolean Whether dropbox has a thumbnail available
*/
public function supports_thumbnail($entry) {
if ($entry->{".tag"} !== "file") {
// Not a file. No thumbnail available.
return false;
}
// Thumbnails are available for files under 20MB with file extensions jpg, jpeg, png, tiff, tif, gif, and bmp.
if ($entry->size > 20 * 1024 * 1024) {
return false;
}
$supportedtypes = [
'jpg' => true,
'jpeg' => true,
'png' => true,
'tiff' => true,
'tif' => true,
'gif' => true,
'bmp' => true,
];
$extension = substr($entry->path_lower, strrpos($entry->path_lower, '.') + 1);
return isset($supportedtypes[$extension]) && $supportedtypes[$extension];
}
/**
* Retrieves the thumbnail for the content, as supplied by dropbox.
*
* @param string $path The path to fetch a thumbnail for
* @return string Thumbnail image content
*/
public function get_thumbnail($path) {
$content = $this->fetch_dropbox_content('files/get_thumbnail', [
'path' => $path,
]);
return $content;
}
/**
* Fetch a valid public share link for the specified file.
*
* @param string $id The file path or file id of the file to fetch information for.
* @return object An object containing the id, path, size, and URL of the entry
*/
public function get_file_share_info($id) {
// Attempt to fetch any existing shared link first.
$data = $this->fetch_dropbox_data('sharing/list_shared_links', [
'path' => $id,
]);
if (isset($data->links)) {
$link = reset($data->links);
if (isset($link->{".tag"}) && $link->{".tag"} === "file") {
return $this->normalize_file_share_info($link);
}
}
// No existing link available.
// Create a new one.
$link = $this->fetch_dropbox_data('sharing/create_shared_link_with_settings', [
'path' => $id,
'settings' => [
'requested_visibility' => 'public',
],
]);
if (isset($link->{".tag"}) && $link->{".tag"} === "file") {
return $this->normalize_file_share_info($link);
}
// Some kind of error we don't know how to handle at this stage.
return null;
}
/**
* Normalize the file share info.
*
* @param object $entry Information retrieved from share endpoints
* @return object Normalized entry information to store as repository information
*/
protected function normalize_file_share_info($entry) {
return (object) [
'id' => $entry->id,
'path' => $entry->path_lower,
'url' => $entry->url,
];
}
/**
* Process the callback.
*/
public function callback() {
$this->log_out();
$this->is_logged_in();
}
/**
* Revoke the current access token.
*
* @return string
*/
public function logout() {
try {
$this->fetch_dropbox_data('auth/token/revoke', null);
} catch(authentication_exception $e) {
// An authentication_exception may be expected if the token has
// already expired.
}
}
}

View File

@ -0,0 +1,38 @@
<?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/>.
/**
* General Dropbox Exception.
*
* @since Moodle 3.2
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace repository_dropbox;
defined('MOODLE_INTERNAL') || die();
/**
* General Dropbox Exception.
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dropbox_exception extends \moodle_exception {
}

View File

@ -0,0 +1,38 @@
<?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/>.
/**
* Upstream issue exception.
*
* @since Moodle 3.2
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace repository_dropbox;
defined('MOODLE_INTERNAL') || die();
/**
* Upstream issue exception.
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_exception extends dropbox_exception {
}

View File

@ -0,0 +1,44 @@
<?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/>.
/**
* Dropbox Rate Limit Encountered.
*
* @since Moodle 3.2
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace repository_dropbox;
defined('MOODLE_INTERNAL') || die();
/**
* Dropbox Rate Limit Encountered.
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rate_limit_exception extends dropbox_exception {
/**
* Constructor for rate_limit_exception.
*/
public function __construct() {
parent::__construct('Rate limit hit');
}
}

View File

@ -21,8 +21,6 @@ defined('MOODLE_INTERNAL') || die();
* @return bool result
*/
function xmldb_repository_dropbox_upgrade($oldversion) {
global $CFG;
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
@ -35,5 +33,9 @@ function xmldb_repository_dropbox_upgrade($oldversion) {
// Moodle v3.1.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2016091300) {
unset_config('legacyapi', 'dropbox');
upgrade_plugin_savepoint(true, 2016091300, 'repository', 'dropbox');
}
return true;
}

View File

@ -34,3 +34,4 @@ $string['cachelimit'] = 'Cache limit';
$string['cachelimit_info'] = 'Enter the maximum size of files (in bytes) to be cached on server for Dropbox aliases/shortcuts. Cached files will be served when the source is no longer available. Empty value or zero mean caching of all files regardless of size.';
$string['dropbox:view'] = 'View a Dropbox folder';
$string['logoutdesc'] = '(Logout when you finish using Dropbox)';
$string['oauth2redirecturi'] = 'OAuth 2 Redirect URI';

File diff suppressed because it is too large Load Diff

View File

@ -1,168 +0,0 @@
<?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/>.
/**
* A helper class to access dropbox resources
*
* @since Moodle 2.0
* @package repository_dropbox
* @copyright 2012 Marina Glancy
* @copyright 2010 Dongsheng Cai
* @author Dongsheng Cai <dongsheng@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/oauthlib.php');
/**
* Authentication class to access Dropbox API
*
* @package repository_dropbox
* @copyright 2010 Dongsheng Cai
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dropbox extends oauth_helper {
/** @var string dropbox access type, can be dropbox or sandbox */
private $mode = 'dropbox';
/** @var string dropbox api url*/
private $dropbox_api = 'https://api.dropbox.com/1';
/** @var string dropbox content api url*/
private $dropbox_content_api = 'https://api-content.dropbox.com/1';
/**
* Constructor for dropbox class
*
* @param array $args
*/
function __construct($args) {
parent::__construct($args);
}
/**
* Get file listing from dropbox
*
* @param string $path
* @param string $token
* @param string $secret
* @return array
*/
public function get_listing($path='/', $token='', $secret='') {
$url = $this->dropbox_api.'/metadata/'.$this->mode.$path;
$content = $this->get($url, array(), $token, $secret);
$data = json_decode($content);
return $data;
}
/**
* Prepares the filename to pass to Dropbox API as part of URL
*
* @param string $filepath
* @return string
*/
protected function prepare_filepath($filepath) {
$info = pathinfo($filepath);
$dirname = $info['dirname'];
$basename = $info['basename'];
$filepath = $dirname . rawurlencode($basename);
if ($dirname != '/') {
$filepath = $dirname . '/' . $basename;
$filepath = str_replace("%2F", "/", rawurlencode($filepath));
}
return $filepath;
}
/**
* Retrieves the default (64x64) thumbnail for dropbox file
*
* @throws moodle_exception when file could not be downloaded
*
* @param string $filepath local path in Dropbox
* @param string $saveas path to file to save the result
* @param int $timeout request timeout in seconds, 0 means no timeout
* @return array with attributes 'path' and 'url'
*/
public function get_thumbnail($filepath, $saveas, $timeout = 0) {
$url = $this->dropbox_content_api.'/thumbnails/'.$this->mode.$this->prepare_filepath($filepath);
if (!($fp = fopen($saveas, 'w'))) {
throw new moodle_exception('cannotwritefile', 'error', '', $saveas);
}
$this->setup_oauth_http_options(array('timeout' => $timeout, 'file' => $fp, 'BINARYTRANSFER' => true));
$result = $this->get($url);
fclose($fp);
if ($result === true) {
return array('path'=>$saveas, 'url'=>$url);
} else {
unlink($saveas);
throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
}
}
/**
* Downloads a file from Dropbox and saves it locally
*
* @throws moodle_exception when file could not be downloaded
*
* @param string $filepath local path in Dropbox
* @param string $saveas path to file to save the result
* @param int $timeout request timeout in seconds, 0 means no timeout
* @return array with attributes 'path' and 'url'
*/
public function get_file($filepath, $saveas, $timeout = 0) {
$url = $this->dropbox_content_api.'/files/'.$this->mode.$this->prepare_filepath($filepath);
if (!($fp = fopen($saveas, 'w'))) {
throw new moodle_exception('cannotwritefile', 'error', '', $saveas);
}
$this->setup_oauth_http_options(array('timeout' => $timeout, 'file' => $fp, 'BINARYTRANSFER' => true));
$result = $this->get($url);
fclose($fp);
if ($result === true) {
return array('path'=>$saveas, 'url'=>$url);
} else {
unlink($saveas);
throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
}
}
/**
* Returns direct link to Dropbox file
*
* @param string $filepath local path in Dropbox
* @param int $timeout request timeout in seconds, 0 means no timeout
* @return string|null information object or null if request failed with an error
*/
public function get_file_share_link($filepath, $timeout = 0) {
$url = $this->dropbox_api.'/shares/'.$this->mode.$this->prepare_filepath($filepath);
$this->setup_oauth_http_options(array('timeout' => $timeout));
$result = $this->post($url, array('short_url'=>0));
if (!$this->http->get_errno()) {
$data = json_decode($result);
if (isset($data->url)) {
return $data->url;
}
}
return null;
}
/**
* Sets Dropbox API mode (dropbox or sandbox, default dropbox)
*
* @param string $mode
*/
public function set_mode($mode) {
$this->mode = $mode;
}
}

View File

@ -0,0 +1,621 @@
<?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/>.
/**
* Tests for the Dropbox API (v2).
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Tests for the Dropbox API (v2).
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class repository_dropbox_api_testcase extends advanced_testcase {
/**
* Data provider for has_additional_results.
*
* @return array
*/
public function has_additional_results_provider() {
return [
'No more results' => [
(object) [
'has_more' => false,
'cursor' => '',
],
false
],
'Has more, No cursor' => [
(object) [
'has_more' => true,
'cursor' => '',
],
false
],
'Has more, Has cursor' => [
(object) [
'has_more' => true,
'cursor' => 'example_cursor',
],
true
],
'Missing has_more' => [
(object) [
'cursor' => 'example_cursor',
],
false
],
'Missing cursor' => [
(object) [
'has_more' => 'example_cursor',
],
false
],
];
}
/**
* Tests for the has_additional_results API function.
*
* @dataProvider has_additional_results_provider
* @param object $result The data to test
* @param bool $expected The expected result
*/
public function test_has_additional_results($result, $expected) {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods(null)
->getMock();
$this->assertEquals($expected, $mock->has_additional_results($result));
}
/**
* Data provider for check_and_handle_api_errors.
*
* @return array
*/
public function check_and_handle_api_errors_provider() {
return [
'200 http_code' => [
['http_code' => 200],
'',
null,
null,
],
'400 http_code' => [
['http_code' => 400],
'Unused',
'coding_exception',
'Invalid input parameter passed to DropBox API.',
],
'401 http_code' => [
['http_code' => 401],
'Unused',
\repository_dropbox\authentication_exception::class,
'Authentication token expired',
],
'409 http_code' => [
['http_code' => 409],
'Some data here',
'coding_exception',
'Endpoint specific error: Some data here',
],
'429 http_code' => [
['http_code' => 429],
'Unused',
\repository_dropbox\rate_limit_exception::class,
'Rate limit hit',
],
'500 http_code' => [
['http_code' => 500],
'Response body',
'invalid_response_exception',
'500: Response body',
],
'599 http_code' => [
['http_code' => 599],
'Response body',
'invalid_response_exception',
'599: Response body',
],
'600 http_code (invalid, but not officially an error)' => [
['http_code' => 600],
'',
null,
null,
],
];
}
/**
* Tests for check_and_handle_api_errors.
*
* @dataProvider check_and_handle_api_errors_provider
* @param object $info The response to test
* @param string $data The contented returned by the curl call
* @param string $exception The name of the expected exception
* @param string $exceptionmessage The expected message in the exception
*/
public function test_check_and_handle_api_errors($info, $data, $exception, $exceptionmessage) {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods(null)
->getMock();
$mock->info = $info;
$rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
$rcm = $rc->getMethod('check_and_handle_api_errors');
$rcm->setAccessible(true);
if ($exception) {
$this->expectException($exception);
}
if ($exceptionmessage) {
$this->expectExceptionMessage($exceptionmessage);
}
$result = $rcm->invoke($mock, $data);
$this->assertNull($result);
}
/**
* Data provider for the supports_thumbnail function.
*
* @return array
*/
public function supports_thumbnail_provider() {
$tests = [
'Only files support thumbnails' => [
(object) ['.tag' => 'folder'],
false,
],
'Dropbox currently only supports thumbnail generation for files under 20MB' => [
(object) [
'.tag' => 'file',
'size' => 21 * 1024 * 1024,
],
false,
],
'Unusual file extension containing a working format but ending in a non-working one' => [
(object) [
'.tag' => 'file',
'size' => 100 * 1024,
'path_lower' => 'Example.jpg.pdf',
],
false,
],
'Unusual file extension ending in a working extension' => [
(object) [
'.tag' => 'file',
'size' => 100 * 1024,
'path_lower' => 'Example.pdf.jpg',
],
true,
],
];
// See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail.
$types = [
'pdf' => false,
'doc' => false,
'docx' => false,
'jpg' => true,
'jpeg' => true,
'png' => true,
'tiff' => true,
'tif' => true,
'gif' => true,
'bmp' => true,
];
foreach ($types as $type => $result) {
$tests["Test support for {$type}"] = [
(object) [
'.tag' => 'file',
'size' => 100 * 1024,
'path_lower' => "example_filename.{$type}",
],
$result,
];
}
return $tests;
}
/**
* Test the supports_thumbnail function.
*
* @dataProvider supports_thumbnail_provider
* @param object $entry The entry to test
* @param bool $expected Whether this entry supports thumbnail generation
*/
public function test_supports_thumbnail($entry, $expected) {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods(null)
->getMock();
$this->assertEquals($expected, $mock->supports_thumbnail($entry));
}
/**
* Test that the logout makes a call to the correct revocation endpoint.
*/
public function test_logout_revocation() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods(['fetch_dropbox_data'])
->getMock();
$mock->expects($this->once())
->method('fetch_dropbox_data')
->with($this->equalTo('auth/token/revoke'), $this->equalTo(null));
$this->assertNull($mock->logout());
}
/**
* Test that the logout function catches authentication_exception exceptions and discards them.
*/
public function test_logout_revocation_catch_auth_exception() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods(['fetch_dropbox_data'])
->getMock();
$mock->expects($this->once())
->method('fetch_dropbox_data')
->will($this->throwException(new \repository_dropbox\authentication_exception('Exception should be caught')));
$this->assertNull($mock->logout());
}
/**
* Test that the logout function does not catch any other exception.
*/
public function test_logout_revocation_does_not_catch_other_exceptions() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods(['fetch_dropbox_data'])
->getMock();
$mock->expects($this->once())
->method('fetch_dropbox_data')
->will($this->throwException(new \repository_dropbox\rate_limit_exception));
$this->expectException(\repository_dropbox\rate_limit_exception::class);
$mock->logout();
}
/**
* Test basic fetch_dropbox_data function.
*/
public function test_fetch_dropbox_data_endpoint() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'request',
'get_api_endpoint',
'get_content_endpoint',
])
->getMock();
$endpoint = 'testEndpoint';
// The fetch_dropbox_data call should be called against the standard endpoint only.
$mock->expects($this->once())
->method('get_api_endpoint')
->with($endpoint)
->will($this->returnValue("https://example.com/api/2/{$endpoint}"));
$mock->expects($this->never())
->method('get_content_endpoint');
$mock->expects($this->once())
->method('request')
->will($this->returnValue(json_encode([])));
// Make the call.
$rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
$rcm = $rc->getMethod('fetch_dropbox_data');
$rcm->setAccessible(true);
$rcm->invoke($mock, $endpoint);
}
/**
* Some Dropbox endpoints require that the POSTFIELDS be set to null exactly.
*/
public function test_fetch_dropbox_data_postfields_null() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'request',
])
->getMock();
$endpoint = 'testEndpoint';
$mock->expects($this->once())
->method('request')
->with($this->anything(), $this->callback(function($d) {
return $d['CURLOPT_POSTFIELDS'] === 'null';
}))
->will($this->returnValue(json_encode([])));
// Make the call.
$rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
$rcm = $rc->getMethod('fetch_dropbox_data');
$rcm->setAccessible(true);
$rcm->invoke($mock, $endpoint, null);
}
/**
* When data is specified, it should be json_encoded in POSTFIELDS.
*/
public function test_fetch_dropbox_data_postfields_data() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'request',
])
->getMock();
$endpoint = 'testEndpoint';
$data = ['something' => 'somevalue'];
$mock->expects($this->once())
->method('request')
->with($this->anything(), $this->callback(function($d) use ($data) {
return $d['CURLOPT_POSTFIELDS'] === json_encode($data);
}))
->will($this->returnValue(json_encode([])));
// Make the call.
$rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
$rcm = $rc->getMethod('fetch_dropbox_data');
$rcm->setAccessible(true);
$rcm->invoke($mock, $endpoint, $data);
}
/**
* When more results are available, these should be fetched until there are no more.
*/
public function test_fetch_dropbox_data_recurse_on_additional_records() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'request',
'get_api_endpoint',
])
->getMock();
$endpoint = 'testEndpoint';
// We can't detect if fetch_dropbox_data was called twice because
// we can'
$mock->expects($this->exactly(3))
->method('request')
->will($this->onConsecutiveCalls(
json_encode(['has_more' => true, 'cursor' => 'Example', 'entries' => ['foo', 'bar']]),
json_encode(['has_more' => true, 'cursor' => 'Example', 'entries' => ['baz']]),
json_encode(['has_more' => false, 'cursor' => '', 'entries' => ['bum']])
));
// We automatically adjust for the /continue endpoint.
$mock->expects($this->exactly(3))
->method('get_api_endpoint')
->withConsecutive(['testEndpoint'], ['testEndpoint/continue'], ['testEndpoint/continue'])
->willReturn($this->onConsecutiveCalls(
'https://example.com/api/2/testEndpoint',
'https://example.com/api/2/testEndpoint/continue',
'https://example.com/api/2/testEndpoint/continue'
));
// Make the call.
$rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
$rcm = $rc->getMethod('fetch_dropbox_data');
$rcm->setAccessible(true);
$result = $rcm->invoke($mock, $endpoint, null);
$this->assertEquals([
'foo',
'bar',
'baz',
'bum',
], $result->entries);
$this->assertFalse(isset($result->cursor));
$this->assertFalse(isset($result->has_more));
}
/**
* Base tests for the fetch_dropbox_content function.
*/
public function test_fetch_dropbox_content() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'request',
'setHeader',
'get_content_endpoint',
'get_api_endpoint',
'check_and_handle_api_errors',
])
->getMock();
$data = ['exampledata' => 'examplevalue'];
$endpoint = 'getContent';
$url = "https://example.com/api/2/{$endpoint}";
$response = 'Example content';
// Only the content endpoint should be called.
$mock->expects($this->once())
->method('get_content_endpoint')
->with($endpoint)
->will($this->returnValue($url));
$mock->expects($this->never())
->method('get_api_endpoint');
$mock->expects($this->exactly(2))
->method('setHeader')
->withConsecutive(
[$this->equalTo('Content-Type: ')],
[$this->equalTo('Dropbox-API-Arg: ' . json_encode($data))]
);
// Only one request should be made, and it should forcibly be a POST.
$mock->expects($this->once())
->method('request')
->with($this->equalTo($url), $this->callback(function($options) {
return $options['CURLOPT_POST'] === 1;
}))
->willReturn($response);
$mock->expects($this->once())
->method('check_and_handle_api_errors')
->with($this->equalTo($response))
;
// Make the call.
$rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
$rcm = $rc->getMethod('fetch_dropbox_content');
$rcm->setAccessible(true);
$result = $rcm->invoke($mock, $endpoint, $data);
$this->assertEquals($response, $result);
}
/**
* Test that the get_file_share_info function returns an existing link if one is available.
*/
public function test_get_file_share_info_existing() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'fetch_dropbox_data',
'normalize_file_share_info',
])
->getMock();
$id = 'LifeTheUniverseAndEverything';
$file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
$sharelink = 'https://example.com/share/link';
// Mock fetch_dropbox_data to return an existing file.
$mock->expects($this->once())
->method('fetch_dropbox_data')
->with(
$this->equalTo('sharing/list_shared_links'),
$this->equalTo(['path' => $id])
)
->willReturn((object) ['links' => [$file]]);
$mock->expects($this->once())
->method('normalize_file_share_info')
->with($this->equalTo($file))
->will($this->returnValue($sharelink));
$this->assertEquals($sharelink, $mock->get_file_share_info($id));
}
/**
* Test that the get_file_share_info function creates a new link if one is not available.
*/
public function test_get_file_share_info_new() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'fetch_dropbox_data',
'normalize_file_share_info',
])
->getMock();
$id = 'LifeTheUniverseAndEverything';
$file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
$sharelink = 'https://example.com/share/link';
// Mock fetch_dropbox_data to return an existing file.
$mock->expects($this->exactly(2))
->method('fetch_dropbox_data')
->withConsecutive(
[$this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id])],
[$this->equalTo('sharing/create_shared_link_with_settings'), $this->equalTo([
'path' => $id,
'settings' => [
'requested_visibility' => 'public',
]
])]
)
->will($this->onConsecutiveCalls(
(object) ['links' => []],
$file
));
$mock->expects($this->once())
->method('normalize_file_share_info')
->with($this->equalTo($file))
->will($this->returnValue($sharelink));
$this->assertEquals($sharelink, $mock->get_file_share_info($id));
}
/**
* Test failure behaviour with get_file_share_info fails to create a new link.
*/
public function test_get_file_share_info_new_failure() {
$mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
->disableOriginalConstructor()
->setMethods([
'fetch_dropbox_data',
'normalize_file_share_info',
])
->getMock();
$id = 'LifeTheUniverseAndEverything';
// Mock fetch_dropbox_data to return an existing file.
$mock->expects($this->exactly(2))
->method('fetch_dropbox_data')
->withConsecutive(
[$this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id])],
[$this->equalTo('sharing/create_shared_link_with_settings'), $this->equalTo([
'path' => $id,
'settings' => [
'requested_visibility' => 'public',
]
])]
)
->will($this->onConsecutiveCalls(
(object) ['links' => []],
null
));
$mock->expects($this->never())
->method('normalize_file_share_info');
$this->assertNull($mock->get_file_share_info($id));
}
}

View File

@ -28,17 +28,21 @@
require(__DIR__.'/../../config.php');
require_once(__DIR__.'/lib.php');
$repo_id = optional_param('repo_id', 0, PARAM_INT); // Repository ID
$repoid = optional_param('repo_id', 0, PARAM_INT); // Repository ID
$contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // Context ID
$source = optional_param('source', '', PARAM_TEXT); // File path in current user's dropbox
if (isloggedin() && $repo_id && $source
&& ($repo = repository::get_repository_by_id($repo_id, $contextid))
&& method_exists($repo, 'send_thumbnail')) {
// try requesting thumbnail and outputting it. This function exits if thumbnail was retrieved
$thumbnailavailable = isloggedin();
$thumbnailavailable = $thumbnailavailable && $repoid;
$thumbnailavailable = $thumbnailavailable && $source;
$thumbnailavailable = $thumbnailavailable && ($repo = repository::get_repository_by_id($repoid, $contextid));
$thumbnailavailable = $thumbnailavailable && method_exists($repo, 'send_thumbnail');
if ($thumbnailavailable) {
// Try requesting thumbnail and outputting it.
// This function exits if thumbnail was retrieved.
$repo->send_thumbnail($source);
}
// send default icon for the file type
// Send default icon for the file type.
$fileicon = file_extension_icon($source, 64);
send_file($CFG->dirroot.'/pix/'.$fileicon.'.png', basename($fileicon).'.png');
send_file($CFG->dirroot . '/pix/' . $fileicon . '.png', basename($fileicon) . '.png');

View File

@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX)
$plugin->version = 2016091300; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2016051900; // Requires this Moodle version
$plugin->component = 'repository_dropbox'; // Full name of the plugin (used for diagnostics)