PluginProbe
Photonic Gallery & Lightbox for Flickr, SmugMug & Others / 3.33
Photonic Gallery & Lightbox for Flickr, SmugMug & Others v3.33
3.36 3.35 3.34 3.33 2.19 2.20 2.21 2.22 2.23 2.24 2.25 2.26 2.27 2.28 2.29 2.30 2.31 2.32 2.33 2.34 2.40 2.41 2.42 2.43 2.44 All 141 releases
photonic / Platforms / Instagram.php

Instagram.php in Photonic Gallery & Lightbox for Flickr, SmugMug & Others 3.33, at Platforms/Instagram.php

527 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Photonic_Plugin\Platforms;
4
5 use Photonic_Plugin\Components\Error;
6 use Photonic_Plugin\Components\Pagination;
7 use Photonic_Plugin\Components\Photo;
8 use Photonic_Plugin\Components\Photo_List;
9 use Photonic_Plugin\Components\Single_Photo;
10 use WP_Error;
11
12 require_once 'OAuth2.php';
13 require_once 'Level_One_Module.php';
14
15 /**
16 * Module to handle Instagram. Instagram uses OAuth2 authentication, and authentication is mandatory to display content.
17 * Instagram first issues a short-lived token. This is exchanged for a long-lived token right on an external site.
18 * The long-lived token is valid for 60 days, though, to be safe, Photonic swaps it out for a new token if there are less then
19 * 30 days of validity left on it.
20 */
21 class Instagram extends OAuth2 implements Level_One_Module {
22 public $response_type;
23 public $scope;
24 public $cached_token;
25 public $field_list;
26 public $token_valid;
27 private static $instance = null;
28
29 protected function __construct() {
30 parent::__construct();
31 global $photonic_instagram_disable_title_link, $photonic_instagram_access_token;
32 $this->provider = 'instagram';
33 $this->oauth_version = '2.0';
34 $this->response_type = 'token';
35 $this->scope = 'basic';
36 $this->api_key = 'not-required-but-not-empty';
37 $this->api_secret = 'not-required-but-not-empty';
38 $this->token = $photonic_instagram_access_token; // Used in the Authentication page to see if a token is set
39 $this->access_token = $photonic_instagram_access_token; // Used everywhere else. This is updated later based on the cached value in memory
40 $this->field_list = 'id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,username';
41 $this->soon_limit = 30;
42
43 $this->link_lightbox_title = empty($photonic_instagram_disable_title_link);
44 $this->doc_links = [
45 'general' => 'https://aquoid.com/plugins/photonic/instagram/',
46 ];
47 $this->authenticate($photonic_instagram_access_token);
48
49 $this->cached_token = $this->get_cached_token();
50 if (!empty($this->cached_token)) {
51 $this->access_token = $this->cached_token['oauth_token'];
52 }
53 }
54
55 /**
56 * Main function that fetches the images associated with the shortcode.
57 *
58 * @param array $attr
59 * @return array
60 */
61 public function get_gallery_images($attr = []): array {
62 global $photonic_instagram_media, $photonic_thumbnail_style, $photonic_instagram_carousel_caption_position;
63 $this->push_to_stack('Get Gallery Images');
64
65 $attr = array_merge(
66 $this->common_parameters,
67 [
68 // Common overrides ...
69 'caption' => 'title',
70
71 // Instagram-specific ...
72 'count' => 25,
73 'layout' => (empty($photonic_thumbnail_style) || 'square' === $photonic_thumbnail_style) ? 'random' : $photonic_thumbnail_style,
74 'media' => $photonic_instagram_media,
75 'embed_type' => 'embed',
76 'carousel_handling' => 'expand',
77 'carousel_caption' => $photonic_instagram_carousel_caption_position
78 ],
79 $attr
80 );
81
82 $attr = array_map('trim', $attr);
83
84 if (empty($this->token) || empty($this->cached_token) || !$this->token_valid) {
85 $components = [new Error(esc_html__("Instagram Access Token not valid. Please reauthenticate.", 'photonic'))];
86 return $components;
87 }
88
89 if (empty($attr['user_id'])) {
90 $user_id = 'me';
91 }
92 else {
93 $user_id = $attr['user_id'];
94 }
95
96 $base_url = 'https://graph.instagram.com/';
97
98 $display_what = 'media';
99 if (!empty($attr['media_id'])) {// Trumps all else. A single photo will be shown.
100 $id_format = preg_match('/^[A-Z][A-Z0-9_-]+/i', $attr['media_id']) ? 'old' : 'new';
101 if ('old' === $id_format) {
102 $components = [new Error(sprintf(esc_html__('The %1$s used, %2$s has been deprecated by Instagram. Please regenerate the gallery with a new id.', 'photonic'), "<code>media_id</code>", esc_attr("<code>" . $attr['media_id'] . "</code>")))];
103 return $components;
104 }
105 else {
106 $query_url = $base_url . $attr['media_id'] . '?fields=' . $this->field_list;
107 }
108 $display_what = 'single-media';
109 }
110 elseif (!empty($attr['carousel'])) {
111 $query_url = $base_url . $attr['carousel'] . '?fields=' . $this->field_list;
112 $display_what = 'carousel';
113 }
114 elseif (!empty($user_id)) {
115 $query_url = $base_url . $user_id . '/media?fields=' . $this->field_list; // Doesn't matter what the other values are. User's photos will be shown.
116 }
117 else {
118 if (empty($attr['view'])) {
119 $components = [new Error(sprintf(esc_html__('The %s parameter has to be defined.', 'photonic'), '<code>view</code>'))];
120 return $components;
121 }
122 else {
123 $components = [
124 new Error(sprintf(esc_html__('Malformed shortcode. Either %1$s or %2$s or %3$s has to be defined.', 'photonic'), '<code>media_id</code>', '<code>carousel</code>', '<code>view</code>'))
125 ];
126 return $components;
127 }
128 }
129
130 if (isset($attr['count'])) {
131 $query_url = add_query_arg(['limit' => $attr['count']], $query_url);
132 }
133
134 if (isset($attr['after'])) {
135 $query_url = add_query_arg(['after' => $attr['after']], $query_url);
136 }
137
138 $components = $this->make_call($query_url, $display_what, $attr);
139 $this->pop_from_stack();
140
141 if (!empty($this->stack_trace[$this->gallery_index])) {
142 $components[] = $this->stack_trace[$this->gallery_index];
143 }
144 return $components;
145 }
146
147 protected function make_call($query_url, $display_what, array &$shortcode_attr) {
148 $this->push_to_stack("Make call $query_url");
149
150 $components = [];
151
152 if (!empty($this->cached_token) && $this->token_valid) {
153 $query = add_query_arg(['access_token' => $this->access_token], $query_url);
154 }
155 else {
156 $this->pop_from_stack(); // Make call, error encountered
157 $components = [new Error(esc_html__("Instagram Access Token not valid. Please reauthenticate.", 'photonic'))];
158 return $components;
159 }
160
161 $this->push_to_stack('Send request');
162 $response = wp_remote_request(
163 $query,
164 [
165 'sslverify' => PHOTONIC_SSL_VERIFY,
166 ]
167 );
168 $this->pop_from_stack(); // Send request
169
170 $this->push_to_stack('Process response');
171 if (!is_wp_error($response)) {
172 if (isset($response['response']) && isset($response['response']['code'])) {
173 if (200 === $response['response']['code']) {
174 $body = json_decode($response['body']);
175
176 if (isset($body->paging) && isset($body->paging->next) && isset($body->paging->cursors) && isset($body->paging->cursors->after)) {
177 $shortcode_attr['after'] = $body->paging->cursors->after;
178 }
179 else {
180 if (isset($shortcode_attr['after'])) {
181 unset($shortcode_attr['after']);
182 }
183 }
184
185 if (isset($body->data) && 'media' === $display_what) {
186 $data = $body->data;
187 $components[] = $this->process_media($data, $shortcode_attr);
188 }
189 elseif ('single-media' === $display_what || 'carousel' === $display_what) {
190 // Special handling for cases where the user was passing a media_id that was really a carousel.
191 // Prior to Instagram disabling oEmbed, this would be displayed as an inline slideshow.
192 // Now we will make an additional call to fetch the carousel
193 if ('CAROUSEL_ALBUM' === $body->media_type) {
194 $carousel_query_url = 'https://graph.instagram.com/' . $body->id . '/children?fields=id,media_type,media_url,permalink,thumbnail_url&access_token=' . $this->access_token;
195 $carousel_contents = $this->get_carousel_contents($carousel_query_url);
196 if (!empty($carousel_contents)) {
197 if ('above' === $shortcode_attr['carousel_caption'] && !empty($body->caption)) {
198 $components[] = "<div class='photonic-caption photonic-instagram-caption'>{$body->caption}</div>";
199 }
200
201 $components[] = $this->process_media($carousel_contents, $shortcode_attr);
202
203 if ('below' === $shortcode_attr['carousel_caption'] && !empty($body->caption)) {
204 $components[] = "<div class='photonic-caption photonic-instagram-caption'>{$body->caption}</div>";
205 }
206 }
207 }
208 else {
209 $this->pop_from_stack(); // 'Process response'
210 $this->pop_from_stack(); // 'Make call'
211 $single_photo = new Single_Photo($body->media_url, $body->permalink, '', isset($body->caption) ? $body->caption : '');
212 $components[] = $single_photo;
213 return $components;
214 }
215 }
216 else {
217 $this->pop_from_stack(); // 'Process response'
218 $this->pop_from_stack(); // 'Make call'
219 $components[] = new Error(esc_html__('No data returned. Unknown error', 'photonic'));
220 return $components;
221 }
222 }
223 elseif (isset($response['body'])) {
224 $body = json_decode($response['body']);
225 if (isset($body->meta) && isset($body->meta->error_message)) {
226 $this->pop_from_stack(); // 'Process response'
227 $this->pop_from_stack(); // 'Make call'
228 $components[] = new Error(esc_html($body->meta->error_message));
229 return $components;
230 }
231 else {
232 $this->pop_from_stack(); // 'Process response'
233 $this->pop_from_stack(); // 'Make call'
234 $components[] = new Error(esc_html__('Unknown error', 'photonic'));
235 return $components;
236 }
237 }
238 elseif (isset($response['response']['message'])) {
239 $this->pop_from_stack(); // 'Process response'
240 $this->pop_from_stack(); // 'Make call'
241 $components[] = new Error(esc_html($response['response']['message']));
242 return $components;
243 }
244 else {
245 $this->pop_from_stack(); // 'Process response'
246 $this->pop_from_stack(); // 'Make call'
247 $components[] = new Error(esc_html__('Unknown error', 'photonic'));
248 return $components;
249 }
250 }
251 }
252 else {
253 $this->pop_from_stack(); // 'Process response'
254 $this->pop_from_stack(); // 'Make call'
255 $components[] = $this->wp_error_message($response);
256 return $components;
257 }
258
259 $this->pop_from_stack(); // 'Process response'
260 $this->pop_from_stack(); // 'Make call'
261 return $components;
262 }
263
264 private function process_media($data, $short_code) {
265 global $photonic_instagram_photo_title_display;
266
267 $photo_objects = $this->build_level_1_objects($data, $short_code);
268 $row_constraints = ['constraint-type' => null, 'padding' => 0, 'count' => 0];
269
270 $photo_list = new Photo_List($short_code);
271 $photo_list->photos = $photo_objects;
272 $photo_list->title_position = $photonic_instagram_photo_title_display;
273 $photo_list->row_constraints = $row_constraints;
274 $photo_list->parent = 'stream';
275 $pagination = new Pagination();
276 $pagination->end = 0;
277 $pagination->total = empty($short_code['after']) ? 0 : $short_code['count'];
278 $photo_list->pagination = $pagination;
279
280 return $photo_list;
281 }
282
283 public function build_level_1_objects($response, array $short_code, $module_parameters = [], $options = []): array {
284 $output = [];
285
286 $media = explode(',', $short_code['media']);
287 $videos_ok = in_array('videos', $media, true) || in_array('all', $media, true);
288 $photos_ok = in_array('photos', $media, true) || in_array('all', $media, true);
289
290 foreach ($response as $photo) {
291 if (isset($photo->media_type) && ((('image' === strtolower($photo->media_type) || 'carousel_album' === strtolower($photo->media_type)) && $photos_ok) || ('video' === strtolower($photo->media_type) && $videos_ok)) && isset($photo->media_url)) {
292 if ('carousel_album' === strtolower($photo->media_type) && 'expand' === $short_code['carousel_handling']) {
293 $query_url = 'https://graph.instagram.com/' . $photo->id . '/children?fields=id,media_type,media_url,permalink,thumbnail_url&access_token=' . $this->access_token;
294 $carousel_contents = $this->get_carousel_contents($query_url);
295 $carousel_caption = empty($photo->caption) ? null : $photo->caption;
296
297 foreach ($carousel_contents as $carousel_photo) {
298 $ret = $this->process_single_item($carousel_photo, $photos_ok, $videos_ok, $carousel_photo->permalink, $carousel_photo->id, $carousel_caption); // Carousel photos have no caption, so use the post caption
299 if (!empty($ret)) {
300 $output[] = $ret;
301 }
302 }
303 }
304 else {
305 $ret = $this->process_single_item($photo, $photos_ok, $videos_ok, $photo->permalink, $photo->id);
306 if (!empty($ret)) {
307 $output[] = $ret;
308 }
309 }
310 }
311 }
312 return $output;
313 }
314
315 public function renew_token($current_token): array {
316 $token = [];
317 $error = '';
318 $soon = $this->is_token_expiring_soon($this->soon_limit);
319
320 if (is_null($soon)) {
321 // No token exists. Do nothing, because the API secret is needed and authentication will need the back-end.
322 // The call to is_token_expired will return true, prompting validation.
323 }
324 else {
325 $transient_token = get_transient('photonic_' . $this->provider . '_token');
326 $photonic_authentication = get_option('photonic_authentication');
327
328 // Check for a transient first
329 $instagram_token = (false === $transient_token) ? $photonic_authentication['instagram'] : $transient_token;
330 if ($soon > 0) { // Token is expiring soon
331 if (!empty($current_token)) {
332 $response = wp_remote_request(
333 'https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=' . $current_token,
334 [
335 'sslverify' => PHOTONIC_SSL_VERIFY,
336 ]
337 );
338
339 if (!is_wp_error($response)) {
340 $token = $this->parse_token($response);
341 if (!empty($token)) {
342 $token['client_id'] = $instagram_token['client_id'];
343 $user_response = wp_remote_request(
344 'https://graph.instagram.com/me?fields=id,username&access_token=' . $token['oauth_token'],
345 [
346 'sslverify' => PHOTONIC_SSL_VERIFY,
347 ]
348 );
349 if (!is_wp_error($user_response)) {
350 $user_response = $user_response['body'];
351 $user_response = json_decode($user_response);
352 $token['user'] = $user_response->username;
353 }
354 }
355 // Replace the transient, or set it up anew.
356 set_transient('photonic_' . $this->provider . '_token', $token, $token['oauth_token_expires']);
357 }
358 else {
359 $error = $response->get_error_message();
360 }
361 }
362 }
363 elseif ($soon < 0) {
364 // Token has expired. Do nothing, because the API secret is needed and authentication will need the back-end.
365 // The call to is_token_expired will return true, prompting validation.
366 }
367 else {
368 // Token is still good. But, don't create a transient if none exists... transients have an expiry date; that will get messed up.
369 $token = $instagram_token;
370 }
371 }
372 return [$token, $error];
373 }
374
375 public function is_token_expired($token) {
376 if (empty($token)) {
377 return true;
378 }
379
380 if (!isset($token['oauth_token']) || !isset($token['oauth_token_created']) || !isset($token['oauth_token_expires'])) {
381 return true;
382 }
383
384 $current = time();
385 if ($token['oauth_token_created'] + $token['oauth_token_expires'] < $current) {
386 return true;
387 }
388
389 return false;
390 }
391
392 /**
393 * Checks if a token will expire soon. This is used to trigger a refresh for sources such as Instagram. Google uses a separate "Refresh Token",
394 * so this is not applicable to it. The <code>$soon_limit</code> defines how many days is "soon", and a refresh is triggered if the current date
395 * is in the "soon" range. E.g. If you have a soon limit of 30 days, and your token expires in 15 days when you load the page, this method will
396 * return <code>true</code>.
397 *
398 * For cases where the token does not exist yet, the method returns <code>null</code>.
399 *
400 * @param $soon_limit int Number of days to check the expiry limit for.
401 * @return int|null If there is no token, return <code>null</code>. Otherwise, if there are < <code>$soon_limit</code> days left, return 1, if token is expired return -1, and if there is time return 0.
402 */
403 public function is_token_expiring_soon($soon_limit) {
404 $transient_token = get_transient('photonic_' . $this->provider . '_token');
405 $photonic_authentication = get_option('photonic_authentication');
406
407 if (false === $transient_token &&
408 (empty($photonic_authentication) || empty($photonic_authentication[$this->provider]) ||
409 empty($photonic_authentication[$this->provider]['oauth_token']) || empty($photonic_authentication[$this->provider]['oauth_token_created']) || empty($photonic_authentication[$this->provider]['oauth_token_expires']))) {
410 return null; // There is no token!
411 }
412
413 $token = (false === $transient_token) ? $photonic_authentication[$this->provider] : $transient_token;
414 $token_expiry = $token['oauth_token_created'] + $token['oauth_token_expires'];
415
416 $current = time();
417 $test_expiry = $current + $soon_limit * 24 * 60 * 60;
418
419 $time_left = $token_expiry - $test_expiry;
420
421 if ($current >= $token_expiry) {
422 return -1; // already expired
423 }
424 elseif ($time_left <= 0) {
425 return 1; // Expiring soon
426 }
427 else {
428 return 0; // There is still time
429 }
430 }
431
432 /**
433 * @param $photo
434 * @param $photos_ok
435 * @param $videos_ok
436 * @param null $photo_link
437 * @param null $photo_id
438 * @param null $photo_caption
439 *
440 * @return Photo
441 */
442 private function process_single_item($photo, $photos_ok, $videos_ok, $photo_link = null, $photo_id = null, $photo_caption = null) {
443 if (isset($photo->media_type) && ((('image' === strtolower($photo->media_type) || 'carousel_album' === strtolower($photo->media_type)) && $photos_ok) || ('video' === strtolower($photo->media_type) && $videos_ok)) && isset($photo->media_url)) {
444 $photonic_photo = new Photo();
445 $main_image = esc_url($photo->media_url);
446 if (!empty($photo->thumbnail_url)) {
447 $photonic_photo->thumbnail = esc_url($photo->thumbnail_url);
448 $photonic_photo->tile_image = esc_url($photo->thumbnail_url);
449 }
450 else {
451 $photonic_photo->thumbnail = $main_image;
452 $photonic_photo->tile_image = $main_image;
453 }
454
455 $photonic_photo->main_image = $main_image;
456
457 if (isset($photo->caption)) {
458 $photonic_photo->title = wp_kses_post($photo->caption);
459 }
460 elseif (!empty($photo_caption)) {
461 $photonic_photo->title = wp_kses_post($photo_caption);
462 }
463 else {
464 $photonic_photo->title = '';
465 }
466
467 $photonic_photo->alt_title = $photonic_photo->title;
468 $photonic_photo->description = $photonic_photo->title;
469 $photonic_photo->main_page = $photo_link;
470 $photonic_photo->id = $photo_id;
471
472 if ('video' === strtolower($photo->media_type)) {
473 $photonic_photo->video = esc_url($photo->media_url);
474 $photonic_photo->main_image = esc_url($photo->thumbnail_url);
475
476 $parse = wp_parse_url($photonic_photo->video);
477 $parse = explode('.', $parse['path']);
478 $photonic_photo->mime = 'video/' . $parse[count($parse) - 1];
479 }
480
481 return $photonic_photo;
482 }
483 return null;
484 }
485
486 public function authentication_URL() {
487 // Not needed; placeholder for abstract class method stub
488 }
489
490 public function access_token_URL() {
491 // Not needed; placeholder for abstract class method stub
492 }
493
494 protected function set_token_validity($validity) {
495 $this->token_valid = $validity;
496 }
497
498 /**
499 * @param string $query_url
500 * @return array|WP_Error
501 */
502 private function get_carousel_contents($query_url) {
503 $this->push_to_stack('Fetch carousel');
504 $response = wp_remote_request(
505 $query_url,
506 [
507 'sslverify' => PHOTONIC_SSL_VERIFY,
508 ]
509 );
510
511 $carousel_contents = [];
512
513 if (!is_wp_error($response)) {
514 if (isset($response['response']) && isset($response['response']['code'])) {
515 if (200 === $response['response']['code']) {
516 $body = json_decode($response['body']);
517 if (isset($body->data)) {
518 $carousel_contents = $body->data;
519 }
520 }
521 }
522 }
523 $this->pop_from_stack(); // Fetch carousel
524 return $carousel_contents;
525 }
526 }
527