PluginProbe
Photonic Gallery & Lightbox for Flickr, SmugMug & Others / 3.36
Photonic Gallery & Lightbox for Flickr, SmugMug & Others v3.36
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.36, at Platforms/Instagram.php

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