Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
/**
18
 * Dropbox V2 API.
19
 *
20
 * @since       Moodle 3.2
21
 * @package     repository_dropbox
22
 * @copyright   Andrew Nicols <andrew@nicols.co.uk>
23
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace repository_dropbox;
27
 
28
use core\oauth2\client;
29
use core\oauth2\issuer;
30
 
31
/**
32
 * Dropbox V2 API.
33
 *
34
 * @package     repository_dropbox
35
 * @copyright   Andrew Nicols <andrew@nicols.co.uk>
36
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37
 */
38
class dropbox extends client {
39
 
40
    /**
41
     * @var array Custom continue endpoints that differ from the standard.
42
     */
43
    private $mappedcontinueoverides = [
44
        'files/search_v2' => 'files/search/continue_v2'
45
    ];
46
 
47
    /**
48
     * Create the DropBox API Client.
49
     *
50
     * @param   issuer      $issuer     The dropbox issuer
51
     * @param   string      $callback   The callback URL
52
     */
53
    public function __construct(issuer $issuer, $callback) {
54
        parent::__construct($issuer, $callback, '', false, true);
55
    }
56
 
57
    /**
58
     * Override - Return an empty string to override parent function.
59
     *
60
     * Dropbox does not require scopes to be provided and can function without them.
61
     * Additional information MDL-70268
62
     *
63
     * @return string
64
     */
65
    protected function get_login_scopes() {
66
        return '';
67
    }
68
 
69
    /**
70
     * Returns the auth url for OAuth 2.0 request.
71
     *
72
     * @return string the auth url
73
     */
74
    protected function auth_url() {
75
        return 'https://www.dropbox.com/oauth2/authorize';
76
    }
77
 
78
    /**
79
     * Returns the token url for OAuth 2.0 request.
80
     *
81
     * @return string the auth url
82
     */
83
    protected function token_url() {
84
        return 'https://api.dropboxapi.com/oauth2/token';
85
    }
86
 
87
    /**
88
     * Return the constructed API endpoint URL.
89
     *
90
     * @param   string      $endpoint   The endpoint to be contacted
91
     * @return  moodle_url              The constructed API URL
92
     */
93
    protected function get_api_endpoint($endpoint) {
94
        return new \moodle_url('https://api.dropboxapi.com/2/' . $endpoint);
95
    }
96
 
97
    /**
98
     * Return the constructed content endpoint URL.
99
     *
100
     * @param   string      $endpoint   The endpoint to be contacted
101
     * @return  moodle_url              The constructed content URL
102
     */
103
    protected function get_content_endpoint($endpoint) {
104
        return new \moodle_url('https://api-content.dropbox.com/2/' . $endpoint);
105
    }
106
 
107
    /**
108
     * Get the continue endpoint for the provided endpoint.
109
     *
110
     * @param string $endpoint The original endpoint
111
     * @return string $endpoint The generated/mapped continue link
112
     */
113
    protected function get_endpoint_for_continue(string $endpoint) {
114
        // Any API endpoint returning 'has_more' will provide a cursor, and also have a matching endpoint suffixed
115
        // with /continue which takes that cursor.
116
        if (preg_match('_/continue$_', $endpoint) === 0) {
117
            // First check if the API call uses a custom mapped continue endpoint.
118
            if (isset($this->mappedcontinueoverides[$endpoint])) {
119
                $endpoint = $this->mappedcontinueoverides[$endpoint];
120
            } else {
121
                // Only add /continue if it is not already present.
122
                $endpoint .= '/continue';
123
            }
124
        }
125
 
126
        return $endpoint;
127
    }
128
 
129
    /**
130
     * Make an API call against the specified endpoint with supplied data.
131
     *
132
     * @param   string      $endpoint   The endpoint to be contacted
133
     * @param   array       $data       Any data to pass to the endpoint
134
     * @param   string      $resultnode The name of the node that contains the data
135
     * @return  object                  Content decoded from the endpoint
136
     */
137
    protected function fetch_dropbox_data($endpoint, $data = [], string $resultnode = 'entries') {
138
        $url = $this->get_api_endpoint($endpoint);
139
        $this->cleanopt();
140
        $this->resetHeader();
141
 
142
        if ($data === null) {
143
            // Some API endpoints explicitly expect a data submission of 'null'.
144
            $options['CURLOPT_POSTFIELDS'] = 'null';
145
        } else {
146
            $options['CURLOPT_POSTFIELDS'] = json_encode($data);
147
        }
148
        $options['CURLOPT_POST'] = 1;
149
        $this->setHeader('Content-Type: application/json');
150
 
151
        $response = $this->request($url, $options);
152
        $result = json_decode($response);
153
 
154
        $this->check_and_handle_api_errors($result);
155
 
156
        if ($this->has_additional_results($result)) {
157
            $endpoint = $this->get_endpoint_for_continue($endpoint);
158
 
159
            // Fetch the next page of results.
160
            $additionaldata = $this->fetch_dropbox_data($endpoint, [
161
                    'cursor' => $result->cursor,
162
                ], $resultnode);
163
 
164
            // Merge the list of entries.
165
            $result->$resultnode = array_merge($result->$resultnode, $additionaldata->$resultnode);
166
        }
167
 
168
        if (isset($result->has_more)) {
169
            // Unset the cursor and has_more flags.
170
            unset($result->cursor);
171
            unset($result->has_more);
172
        }
173
 
174
        return $result;
175
    }
176
 
177
    /**
178
     * Whether the supplied result is paginated and not the final page.
179
     *
180
     * @param   object      $result     The result of an operation
181
     * @return  boolean
182
     */
183
    public function has_additional_results($result) {
184
        return !empty($result->has_more) && !empty($result->cursor);
185
    }
186
 
187
    /**
188
     * Fetch content from the specified endpoint with the supplied data.
189
     *
190
     * @param   string      $endpoint   The endpoint to be contacted
191
     * @param   array       $data       Any data to pass to the endpoint
192
     * @return  string                  The returned data
193
     */
194
    protected function fetch_dropbox_content($endpoint, $data = []) {
195
        $url = $this->get_content_endpoint($endpoint);
196
        $this->cleanopt();
197
        $this->resetHeader();
198
 
199
        $options['CURLOPT_POST'] = 1;
200
        $this->setHeader('Content-Type: ');
201
        $this->setHeader('Dropbox-API-Arg: ' . json_encode($data));
202
 
203
        $response = $this->request($url, $options);
204
 
205
        $this->check_and_handle_api_errors($response);
206
        return $response;
207
    }
208
 
209
    /**
210
     * Check for an attempt to handle API errors.
211
     *
212
     * This function attempts to deal with errors as per
213
     * https://www.dropbox.com/developers/documentation/http/documentation#error-handling.
214
     *
215
     * @param   mixed      $data       The returned content.
216
     * @throws  moodle_exception
217
     */
218
    protected function check_and_handle_api_errors($data) {
219
        if (!is_array($this->info) or $this->info['http_code'] == 200) {
220
            // Dropbox only returns errors on non-200 response codes.
221
            return;
222
        }
223
 
224
        switch($this->info['http_code']) {
225
            case 400:
226
                // Bad input parameter. Error message should indicate which one and why.
227
                throw new \coding_exception('Invalid input parameter passed to DropBox API.');
228
                break;
229
            case 401:
230
                // Bad or expired token. This can happen if the access token is expired or if the access token has been
231
                // revoked by Dropbox or the user. To fix this, you should re-authenticate the user.
232
                throw new authentication_exception('Authentication token expired');
233
                break;
234
            case 409:
235
                // Endpoint-specific error. Look to the JSON response body for the specifics of the error.
236
                throw new \coding_exception('Endpoint specific error: ' . $data->error_summary);
237
                break;
238
            case 429:
239
                // Your app is making too many requests for the given user or team and is being rate limited. Your app
240
                // should wait for the number of seconds specified in the "Retry-After" response header before trying
241
                // again.
242
                throw new rate_limit_exception();
243
                break;
244
            default:
245
                break;
246
        }
247
 
248
        if ($this->info['http_code'] >= 500 && $this->info['http_code'] < 600) {
249
            throw new \invalid_response_exception($this->info['http_code'] . ": " . $data);
250
        }
251
    }
252
 
253
    /**
254
     * Get file listing from dropbox.
255
     *
256
     * @param   string      $path       The path to query
257
     * @return  object                  The returned directory listing, or null on failure
258
     */
259
    public function get_listing($path = '') {
260
        if ($path === '/') {
261
            $path = '';
262
        }
263
 
264
        $data = $this->fetch_dropbox_data('files/list_folder', [
265
                'path' => $path,
266
            ]);
267
 
268
        return $data;
269
    }
270
 
271
    /**
272
     * Get file search results from dropbox.
273
     *
274
     * @param   string      $query      The search query
275
     * @return  object                  The returned directory listing, or null on failure
276
     */
277
    public function search($query = '') {
278
        // There is nothing to be searched. Return an empty array to mimic the response from Dropbox.
279
        if (!$query) {
280
            return [];
281
        }
282
 
283
        $data = $this->fetch_dropbox_data('files/search_v2', [
284
                'options' => [
285
                    'path' => '',
286
                    'filename_only' => true,
287
                ],
288
                'query' => $query,
289
            ], 'matches');
290
 
291
        return $data;
292
    }
293
 
294
    /**
295
     * Whether the entry is expected to have a thumbnail.
296
     * See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail.
297
     *
298
     * @param   object      $entry      The file entry received from the DropBox API
299
     * @return  boolean                 Whether dropbox has a thumbnail available
300
     */
301
    public function supports_thumbnail($entry) {
302
        if ($entry->{".tag"} !== "file") {
303
            // Not a file. No thumbnail available.
304
            return false;
305
        }
306
 
307
        // Thumbnails are available for files under 20MB with file extensions jpg, jpeg, png, tiff, tif, gif, and bmp.
308
        if ($entry->size > 20 * 1024 * 1024) {
309
            return false;
310
        }
311
 
312
        $supportedtypes = [
313
                'jpg'   => true,
314
                'jpeg'  => true,
315
                'png'   => true,
316
                'tiff'  => true,
317
                'tif'   => true,
318
                'gif'   => true,
319
                'bmp'   => true,
320
            ];
321
 
322
        $extension = substr($entry->path_lower, strrpos($entry->path_lower, '.') + 1);
323
        return isset($supportedtypes[$extension]) && $supportedtypes[$extension];
324
    }
325
 
326
    /**
327
     * Retrieves the thumbnail for the content, as supplied by dropbox.
328
     *
329
     * @param   string      $path       The path to fetch a thumbnail for
330
     * @return  string                  Thumbnail image content
331
     */
332
    public function get_thumbnail($path) {
333
        $content = $this->fetch_dropbox_content('files/get_thumbnail', [
334
                'path' => $path,
335
            ]);
336
 
337
        return $content;
338
    }
339
 
340
    /**
341
     * Fetch a valid public share link for the specified file.
342
     *
343
     * @param   string      $id         The file path or file id of the file to fetch information for.
344
     * @return  object                  An object containing the id, path, size, and URL of the entry
345
     */
346
    public function get_file_share_info($id) {
347
        // Attempt to fetch any existing shared link first.
348
        $data = $this->fetch_dropbox_data('sharing/list_shared_links', [
349
                'path'      => $id,
350
            ]);
351
 
352
        if (isset($data->links)) {
353
            $link = reset($data->links);
354
            if (isset($link->{".tag"}) && $link->{".tag"} === "file") {
355
                return $this->normalize_file_share_info($link);
356
            }
357
        }
358
 
359
        // No existing link available.
360
        // Create a new one.
361
        $link = $this->fetch_dropbox_data('sharing/create_shared_link_with_settings', [
362
                'path'      => $id,
363
                'settings'  => [
364
                    'requested_visibility'  => 'public',
365
                ],
366
            ]);
367
 
368
        if (isset($link->{".tag"}) && $link->{".tag"} === "file") {
369
            return $this->normalize_file_share_info($link);
370
        }
371
 
372
        // Some kind of error we don't know how to handle at this stage.
373
        return null;
374
    }
375
 
376
    /**
377
     * Normalize the file share info.
378
     *
379
     * @param   object $entry   Information retrieved from share endpoints
380
     * @return  object          Normalized entry information to store as repository information
381
     */
382
    protected function normalize_file_share_info($entry) {
383
        return (object) [
384
                'id'    => $entry->id,
385
                'path'  => $entry->path_lower,
386
                'url'   => $entry->url,
387
            ];
388
    }
389
 
390
    /**
391
     * Process the callback.
392
     */
393
    public function callback() {
394
        $this->log_out();
395
        $this->is_logged_in();
396
    }
397
 
398
    /**
399
     * Revoke the current access token.
400
     *
401
     * @return string
402
     */
403
    public function logout() {
404
        try {
405
            $this->fetch_dropbox_data('auth/token/revoke', null);
406
        } catch(authentication_exception $e) {
407
            // An authentication_exception may be expected if the token has
408
            // already expired.
409
        }
410
    }
411
}