2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Provides {@link flickr_client} class.
21 * @copyright 2017 David Mudrák <david@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') ||
die();
27 require_once($CFG->libdir
.'/oauthlib.php');
30 * Simple Flickr API client implementing the features needed by Moodle
32 * @copyright 2017 David Mudrak <david@moodle.com>
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 class flickr_client
extends oauth_helper
{
38 * Base URL for Flickr OAuth 1.0 API calls.
40 const OAUTH_ROOT
= 'https://www.flickr.com/services/oauth';
43 * Base URL for Flickr REST API calls.
45 const REST_ROOT
= 'https://api.flickr.com/services/rest';
48 * Base URL for Flickr Upload API call.
50 const UPLOAD_ROOT
= 'https://up.flickr.com/services/upload/';
53 * Set up OAuth and initialize the client.
55 * The callback URL specified here will override the one specified in the
56 * auth flow defined at Flickr Services.
58 * @param string $consumerkey
59 * @param string $consumersecret
60 * @param moodle_url|string $callbackurl
62 public function __construct($consumerkey, $consumersecret, $callbackurl = '') {
64 'api_root' => self
::OAUTH_ROOT
,
65 'oauth_consumer_key' => $consumerkey,
66 'oauth_consumer_secret' => $consumersecret,
67 'oauth_callback' => $callbackurl,
69 'CURLOPT_USERAGENT' => static::user_agent(),
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
80 public static function user_agent(): string {
83 $version = moodle_major_version();
85 return "MoodleSite/{$version} (+{$CFG->wwwroot})";
89 * Temporarily store the request token secret in the session.
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.
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.
102 * @param array $identifiers Identification of the call
103 * @param string $secret
105 public function set_request_token_secret(array $identifiers, $secret) {
107 if (empty($identifiers) ||
empty($identifiers['caller'])) {
108 throw new coding_exception('Invalid call identification');
111 $cache = cache
::make_from_params(cache_store
::MODE_SESSION
, 'core', 'flickrclient', $identifiers);
112 $cache->set('request_token_secret', $secret);
116 * Returns previously stored request token secret.
118 * See {@link self::set_request_token_secret()} for more details on the
119 * $identifiers argument.
121 * @param array $identifiers Identification of the call
122 * @return string|bool False on error, string secret otherwise.
124 public function get_request_token_secret(array $identifiers) {
126 if (empty($identifiers) ||
empty($identifiers['caller'])) {
127 throw new coding_exception('Invalid call identification');
130 $cache = cache
::make_from_params(cache_store
::MODE_SESSION
, 'core', 'flickrclient', $identifiers);
132 return $cache->get('request_token_secret');
136 * Call a Flickr API method.
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
143 public function call($function, array $params = [], $method = 'GET') {
145 if (strpos($function, 'flickr.') !== 0) {
146 $function = 'flickr.'.$function;
149 $params['method'] = $function;
150 $params['format'] = 'json';
151 $params['nojsoncallback'] = 1;
153 $rawresponse = $this->request($method, self
::REST_ROOT
, $params);
154 $response = json_decode($rawresponse);
156 if (!is_object($response) ||
!isset($response->stat
)) {
157 throw new moodle_exception('flickr_api_call_failed', 'core_error', '', $rawresponse);
160 if ($response->stat
=== 'ok') {
163 } else if ($response->stat
=== 'fail' && $response->code
== 98) {
164 // Authentication failure, give the caller a chance to re-authenticate.
168 throw new moodle_exception('flickr_api_call_failed', 'core_error', '', $response);
175 * Return the URL to fetch the given photo from.
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.
182 * @param string $photoid Flickr photo identifier
185 public function get_photo_url($photoid) {
187 $cache = cache
::make_from_params(cache_store
::MODE_APPLICATION
, 'core', 'flickrclient');
189 $url = $cache->get('photourl_'.$photoid);
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
;
204 if ($url === false) {
205 throw new repository_exception('cannotdownload', 'repository');
208 $cache->set('photourl_'.$photoid, $url);
215 * Upload a photo from Moodle file pool to Flickr.
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}.
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)
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
229 public function upload(stored_file
$photo, array $meta = []) {
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,
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');
246 $params['photo'] = $photo;
248 $response = $this->http
->post(self
::UPLOAD_ROOT
, $params);
250 // Reset http header and options to prepare for the next request.
251 $this->reset_state();
254 $xml = simplexml_load_string($response);
256 if ((string)$xml['stat'] === 'ok') {
257 return (int)$xml->photoid
;
259 } else if ((string)$xml['stat'] === 'fail' && (int)$xml->err
['code'] == 98) {
260 // Authentication failure.
264 throw new moodle_exception('flickr_upload_failed', 'core_error', '',
265 ['code' => (int)$xml->err
['code'], 'message' => (string)$xml->err
['msg']]);
269 throw new moodle_exception('flickr_upload_error', 'core_error', '', null, $response);
278 public function reset_state(): void
{
279 $this->http
->cleanopt();
280 $this->http
->resetHeader();