Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 39 and 402]

   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  }