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
 * Provides {@link flickr_client} class.
19
 *
20
 * @package     core
21
 * @copyright   2017 David Mudrák <david@moodle.com>
22
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
require_once($CFG->libdir.'/oauthlib.php');
28
 
29
/**
30
 * Simple Flickr API client implementing the features needed by Moodle
31
 *
32
 * @copyright 2017 David Mudrak <david@moodle.com>
33
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34
 */
35
class flickr_client extends oauth_helper {
36
 
37
    /**
38
     * Base URL for Flickr OAuth 1.0 API calls.
39
     */
40
    const OAUTH_ROOT = 'https://www.flickr.com/services/oauth';
41
 
42
    /**
43
     * Base URL for Flickr REST API calls.
44
     */
45
    const REST_ROOT = 'https://api.flickr.com/services/rest';
46
 
47
    /**
48
     * Base URL for Flickr Upload API call.
49
     */
50
    const UPLOAD_ROOT = 'https://up.flickr.com/services/upload/';
51
 
52
    /**
53
     * Set up OAuth and initialize the client.
54
     *
55
     * The callback URL specified here will override the one specified in the
56
     * auth flow defined at Flickr Services.
57
     *
58
     * @param string $consumerkey
59
     * @param string $consumersecret
60
     * @param moodle_url|string $callbackurl
61
     */
62
    public function __construct($consumerkey, $consumersecret, $callbackurl = '') {
63
        parent::__construct([
64
            'api_root' => self::OAUTH_ROOT,
65
            'oauth_consumer_key' => $consumerkey,
66
            'oauth_consumer_secret' => $consumersecret,
67
            'oauth_callback' => $callbackurl,
68
            'http_options' => [
69
                'CURLOPT_USERAGENT' => static::user_agent(),
70
            ],
71
        ]);
72
    }
73
 
74
    /**
75
     * Return User-Agent string suitable for calls to Flickr endpoint, avoiding problems caused by the string returned by
76
     * the {@see core_useragent::get_moodlebot_useragent} helper, which is often rejected due to presence of "Bot" within
77
     *
78
     * @return string
79
     */
80
    public static function user_agent(): string {
81
        global $CFG;
82
 
83
        $version = moodle_major_version();
84
 
85
        return "MoodleSite/{$version} (+{$CFG->wwwroot})";
86
    }
87
 
88
    /**
89
     * Temporarily store the request token secret in the session.
90
     *
91
     * The request token secret is returned by the oauth request_token method.
92
     * It needs to be stored in the session before the user is redirected to
93
     * the Flickr to authorize the client. After redirecting back, this secret
94
     * is used for exchanging the request token with the access token.
95
     *
96
     * The identifiers help to avoid collisions between multiple calls to this
97
     * method from different plugins in the same session. They are used as the
98
     * session cache identifiers. Provide an associative array identifying the
99
     * particular method call. At least, the array must contain the 'caller'
100
     * with the caller's component name. Use additional items if needed.
101
     *
102
     * @param array $identifiers Identification of the call
103
     * @param string $secret
104
     */
105
    public function set_request_token_secret(array $identifiers, $secret) {
106
 
107
        if (empty($identifiers) || empty($identifiers['caller'])) {
108
            throw new coding_exception('Invalid call identification');
109
        }
110
 
111
        $cache = cache::make_from_params(cache_store::MODE_SESSION, 'core', 'flickrclient', $identifiers);
112
        $cache->set('request_token_secret', $secret);
113
    }
114
 
115
    /**
116
     * Returns previously stored request token secret.
117
     *
118
     * See {@link self::set_request_token_secret()} for more details on the
119
     * $identifiers argument.
120
     *
121
     * @param array $identifiers Identification of the call
122
     * @return string|bool False on error, string secret otherwise.
123
     */
124
    public function get_request_token_secret(array $identifiers) {
125
 
126
        if (empty($identifiers) || empty($identifiers['caller'])) {
127
            throw new coding_exception('Invalid call identification');
128
        }
129
 
130
        $cache = cache::make_from_params(cache_store::MODE_SESSION, 'core', 'flickrclient', $identifiers);
131
 
132
        return $cache->get('request_token_secret');
133
    }
134
 
135
    /**
136
     * Call a Flickr API method.
137
     *
138
     * @param string $function API function name like 'flickr.photos.getSizes' or just 'photos.getSizes'
139
     * @param array $params Additional API call arguments.
140
     * @param string $method HTTP method to use (GET or POST).
141
     * @return object|bool Response as returned by the Flickr or false on invalid authentication
142
     */
143
    public function call($function, array $params = [], $method = 'GET') {
144
 
145
        if (strpos($function, 'flickr.') !== 0) {
146
            $function = 'flickr.'.$function;
147
        }
148
 
149
        $params['method'] = $function;
150
        $params['format'] = 'json';
151
        $params['nojsoncallback'] = 1;
152
 
153
        $rawresponse = $this->request($method, self::REST_ROOT, $params);
154
        $response = json_decode($rawresponse);
155
 
156
        if (!is_object($response) || !isset($response->stat)) {
157
            throw new moodle_exception('flickr_api_call_failed', 'core_error', '', $rawresponse);
158
        }
159
 
160
        if ($response->stat === 'ok') {
161
            return $response;
162
 
163
        } else if ($response->stat === 'fail' && $response->code == 98) {
164
            // Authentication failure, give the caller a chance to re-authenticate.
165
            return false;
166
 
167
        } else {
168
            throw new moodle_exception('flickr_api_call_failed', 'core_error', '', $response);
169
        }
170
 
171
        return $response;
172
    }
173
 
174
    /**
175
     * Return the URL to fetch the given photo from.
176
     *
177
     * Flickr photos are distributed via farm servers staticflickr.com in
178
     * various sizes (resolutions). The method tries to find the source URL of
179
     * the photo in the highest possible resolution. Results are cached so that
180
     * we do not need to query the Flickr API over and over again.
181
     *
182
     * @param string $photoid Flickr photo identifier
183
     * @return string URL
184
     */
185
    public function get_photo_url($photoid) {
186
 
187
        $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'flickrclient');
188
 
189
        $url = $cache->get('photourl_'.$photoid);
190
 
191
        if ($url === false) {
192
            $response = $this->call('photos.getSizes', ['photo_id' => $photoid]);
193
            // Sizes are returned from smallest to greatest.
194
            if (!empty($response->sizes->size) && is_array($response->sizes->size)) {
195
                while ($bestsize = array_pop($response->sizes->size)) {
196
                    if (isset($bestsize->source)) {
197
                        $url = $bestsize->source;
198
                        break;
199
                    }
200
                }
201
            }
202
        }
203
 
204
        if ($url === false) {
205
            throw new repository_exception('cannotdownload', 'repository');
206
 
207
        } else {
208
            $cache->set('photourl_'.$photoid, $url);
209
        }
210
 
211
        return $url;
212
    }
213
 
214
    /**
215
     * Upload a photo from Moodle file pool to Flickr.
216
     *
217
     * Optional meta information are title, description, tags, is_public,
218
     * is_friend, is_family, safety_level, content_type and hidden.
219
     * See {@link https://www.flickr.com/services/api/upload.api.html}.
220
     *
221
     * Upload can't be asynchronous because then the query would not return the
222
     * photo ID which we need to add the photo to a photoset (album)
223
     * eventually.
224
     *
225
     * @param stored_file $photo stored in Moodle file pool
226
     * @param array $meta optional meta information
227
     * @return int|bool photo id, false on authentication failure
228
     */
229
    public function upload(stored_file $photo, array $meta = []) {
230
 
231
        $args = [
232
            'title' => isset($meta['title']) ? $meta['title'] : null,
233
            'description' => isset($meta['description']) ? $meta['description'] : null,
234
            'tags' => isset($meta['tags']) ? $meta['tags'] : null,
235
            'is_public' => isset($meta['is_public']) ? $meta['is_public'] : 0,
236
            'is_friend' => isset($meta['is_friend']) ? $meta['is_friend'] : 0,
237
            'is_family' => isset($meta['is_family']) ? $meta['is_family'] : 0,
238
            'safety_level' => isset($meta['safety_level']) ? $meta['safety_level'] : 1,
239
            'content_type' => isset($meta['content_type']) ? $meta['content_type'] : 1,
240
            'hidden' => isset($meta['hidden']) ? $meta['hidden'] : 2,
241
        ];
242
 
243
        $this->sign_secret = $this->consumer_secret.'&'.$this->access_token_secret;
244
        $params = $this->prepare_oauth_parameters(self::UPLOAD_ROOT, ['oauth_token' => $this->access_token] + $args, 'POST');
245
 
246
        $params['photo'] = $photo;
247
 
248
        $response = $this->http->post(self::UPLOAD_ROOT, $params);
249
 
250
        // Reset http header and options to prepare for the next request.
251
        $this->reset_state();
252
 
253
        if ($response) {
254
            $xml = simplexml_load_string($response);
255
 
256
            if ((string)$xml['stat'] === 'ok') {
257
                return (int)$xml->photoid;
258
 
259
            } else if ((string)$xml['stat'] === 'fail' && (int)$xml->err['code'] == 98) {
260
                // Authentication failure.
261
                return false;
262
 
263
            } else {
264
                throw new moodle_exception('flickr_upload_failed', 'core_error', '',
265
                    ['code' => (int)$xml->err['code'], 'message' => (string)$xml->err['msg']]);
266
            }
267
 
268
        } else {
269
            throw new moodle_exception('flickr_upload_error', 'core_error', '', null, $response);
270
        }
271
    }
272
 
273
    /**
274
     * Resets curl state.
275
     *
276
     * @return void
277
     */
278
    public function reset_state(): void {
279
        $this->http->cleanopt();
280
        $this->http->resetHeader();
281
    }
282
}