client_id = trim($photonic_google_client_id); $this->client_secret = trim($photonic_google_client_secret); } /* else if (empty($photonic_google_use_own_keys)) { $this->client_id = ''; $this->client_secret = ''; }*/ $this->provider = 'google'; $this->oauth_version = '2.0'; $this->response_type = 'code'; $this->scope = 'https://www.googleapis.com/auth/photoslibrary.readonly'; $this->link_lightbox_title = false; //empty($photonic_google_disable_title_link); // Documentation $this->doc_links = [ 'general' => 'https://aquoid.com/plugins/photonic/google-photos/', 'photos' => 'https://aquoid.com/plugins/photonic/google-photos/photos/', 'albums' => 'https://aquoid.com/plugins/photonic/google-photos/albums/', ]; $this->error_date_format = esc_html__('Dates must be entered in the format Y/M/D where Y is from 0 to 9999, M is from 0 to 12 and D is from 0 to 31. You entered %s.', 'photonic'); $this->oauth_done = false; $this->authenticate($photonic_google_refresh_token); } public static function get_instance() { if (self::$instance == null) { self::$instance = new Google_Photos(); } return self::$instance; } /** * Main function that fetches the images associated with the shortcode. * * @param array $attr * @param array $gallery_meta * @return string */ public function get_gallery_images($attr = [], &$gallery_meta = []) { global $photonic_google_refresh_token, $photonic_google_media, $photonic_google_title_caption; $this->gallery_index++; $this->push_to_stack('Get Gallery Images'); $attr = array_merge( $this->common_parameters, [ 'caption' => $photonic_google_title_caption, 'thumb_size' => '150', 'main_size' => '1600', 'tile_size' => '1600', 'crop_thumb' => 'crop', // Google ... 'count' => 100, 'media' => $photonic_google_media, 'video_size' => 'dv', 'date_filters' => '', 'content_filters' => '', 'access' => 'all', ], $attr); $attr = array_map('trim', $attr); $attr['overlay_size'] = empty($attr['overlay_size']) ? $attr['thumb_size'] : $attr['overlay_size']; $attr['overlay_video_size'] = empty($attr['overlay_video_size']) ? $attr['video_size'] : $attr['overlay_video_size']; $attr['overlay_crop'] = empty($attr['overlay_crop']) ? $attr['crop_thumb'] : $attr['overlay_crop']; if (empty($this->client_id)) { $this->pop_from_stack(); return $this->error(esc_html__('Google Photos Client ID not defined.', 'photonic').Photonic::doc_link($this->doc_links['general'])); } if (empty($this->client_secret)) { $this->pop_from_stack(); return $this->error(esc_html__('Google Photos Client Secret not defined.', 'photonic').Photonic::doc_link($this->doc_links['general'])); } if (empty($photonic_google_refresh_token)) { $this->pop_from_stack(); return $this->error(sprintf(esc_html__('Google Photos Refresh Token not defined. Please authenticate from %s.', 'photonic'), 'Photonic → Authentication').Photonic::doc_link($this->doc_links['general'])); } if (empty($attr['view'])) { $this->pop_from_stack(); return $this->error(sprintf(esc_html__('The %s parameter is mandatory for the shortcode.', 'photonic'), 'view')); } $query_urls = []; if ($attr['view'] == 'albums') { $additional = []; if (!empty($attr['count'])) { $additional['pageSize'] = intval($attr['count']) > 50 ? 50 : intval($attr['count']); } if (!empty($attr['next_token'])) { $additional['pageToken'] = $attr['next_token']; } $access = $this->access_all_or_shared($attr); if ($access['shared']) { $query_urls['https://photoslibrary.googleapis.com/v1/sharedAlbums'] = ['GET' => $additional]; } if ($access['all']) { $query_urls['https://photoslibrary.googleapis.com/v1/albums'] = ['GET' => $additional]; } } else if ($attr['view'] == 'photos') { $additional = []; if (!empty($attr['album_id'])) { $additional['albumId'] = $attr['album_id']; } else { $filters = []; $date_parameter = []; $range_parameter = []; if (!empty($attr['date_filters'])) { /* * Structure of $attr['date_filters']: comma-separated list of dates or date ranges. * Each date is represented by Y/M/D, where 0 <= Y <= 9999, 0 <= M <= 12, 0 <= D < 31 * Each range is represented as Y/M/D-Y/M/D */ $date_filters = explode(',', trim($attr['date_filters'])); foreach($date_filters as $date_filter) { $dates = explode('-', trim($date_filter)); if (count($dates) > 2) { $dates = array_slice($dates, 0, 2); } $range = []; foreach ($dates as $idx => $date) { $date_parts = explode('/', trim($date)); if (count($date_parts) != 3) { $this->pop_from_stack(); return $this->error(sprintf($this->error_date_format, $date)); } if (!is_numeric($date_parts[0]) || $date_parts[0] > 9999 || $date_parts[0] < 0 || !is_numeric($date_parts[1]) || $date_parts[1] > 12 || $date_parts[1] < 0 || !is_numeric($date_parts[2]) || $date_parts[2] > 31 || $date_parts[2] < 0) { $this->pop_from_stack(); return $this->error(sprintf($this->error_date_format, $date)); } $date_object = [ 'year' => intval($date_parts[0]), 'month' => intval($date_parts[1]), 'day' => intval($date_parts[2]), ]; if (count($dates) == 1) { $date_parameter[] = $date_object; } else if ($idx == 0) { $range['startDate'] = $date_object; } else { $range['endDate'] = $date_object; } } if (!empty($range)) { $range_parameter[] = $range; } } $date_filter_parameter = []; if (!empty($date_parameter)) { $date_filter_parameter['dates'] = $date_parameter; } if (!empty($range_parameter)) { $date_filter_parameter['ranges'] = $range_parameter; } if (!empty($date_filter_parameter)) { $filters['dateFilter'] = $date_filter_parameter; } } if (!empty($attr['content_filters'])) { $valid_filters = [ 'NONE' => 'Default content category. This category is ignored if any other category is also listed.', 'LANDSCAPES' => 'Media items containing landscapes.', 'RECEIPTS' => 'Media items containing receipts.', 'CITYSCAPES' =>'Media items containing cityscapes.', 'LANDMARKS' => 'Media items containing landmarks.', 'SELFIES' => 'Media items that are selfies.', 'PEOPLE' => 'Media items containing people.', 'PETS' => 'Media items containing pets.', 'WEDDINGS' => 'Media items from weddings.', 'BIRTHDAYS' => 'Media items from birthdays.', 'DOCUMENTS' => 'Media items containing documents.', 'TRAVEL' => 'Media items taken during travel.', 'ANIMALS' => 'Media items containing animals.', 'FOOD' => 'Media items containing food.', 'SPORT' => 'Media items from sporting events.', 'NIGHT' => 'Media items taken at night.', 'PERFORMANCES' => 'Media items from performances.', 'WHITEBOARDS' => 'Media items containing whiteboards.', 'SCREENSHOTS' => 'Media items that are screenshots.', 'UTILITY' => 'Media items that are considered to be utility. These include, but are not limited to documents, screenshots, whiteboards etc.', ]; /* * Structure of content_filters: C1,C2,-C3,C4,-C5. * The filters are specified as a comma-separated list. * A "-" before the filter's name indicates that the filter should be excluded rather than included. */ $content_filters = explode(',', $attr['content_filters']); $include = $exclude = []; foreach ($content_filters as $content_filter) { $content_filter = strtoupper($content_filter); if (stripos($content_filter, '-') == 0 && array_key_exists(substr($content_filter, 1), $valid_filters)) { $exclude[] = substr($content_filter, 1); } else if (array_key_exists($content_filter, $valid_filters)){ $include[] = $content_filter; } } $content_filter_parameter = []; if (!empty($include)) { $content_filter_parameter['includedContentCategories'] = $include; } if (!empty($exclude)) { $content_filter_parameter['excludedContentCategories'] = $exclude; } if (!empty($content_filter_parameter)) { $filters['contentFilter'] = $content_filter_parameter; } } $media_filters = explode(',', $attr['media']); $media_filter_parameter = []; if (in_array('all', $media_filters)) { $media_filter_parameter[] = 'ALL_MEDIA'; } else if (in_array('photos', $media_filters)) { $media_filter_parameter[] = 'PHOTO'; } else if (in_array('videos', $media_filters)) { $media_filter_parameter[] = 'VIDEO'; } if (!empty($media_filter_parameter)) { $filters['mediaTypeFilter'] = ['mediaTypes' => $media_filter_parameter]; } if (!empty($filters)) { $additional['filters'] = $filters; } } if (!empty($attr['count']) || !empty($attr['photo_count'])) { $additional['pageSize'] = !empty($attr['photo_count']) ? $attr['photo_count'] : $attr['count']; $additional['pageSize'] = intval($additional['pageSize']) > 100 ? 100 : intval($additional['pageSize']); } if (!empty($attr['next_token'])) { $additional['pageToken'] = $attr['next_token']; } $query_urls['https://photoslibrary.googleapis.com/v1/mediaItems:search'] = ['POST' => $additional]; } $out = $this->make_call($query_urls, $attr); $out = $this->finalize_markup($out, $attr); $this->pop_from_stack(); return $out.$this->get_stack_markup(); } /** * Makes calls to Google with the shortcode parameters. * * @param $query_urls - The URLs to be queried * @param $attr - The shortcode attributes * @return string */ private function make_call($query_urls, $attr) { global $photonic_google_refresh_token; $this->push_to_stack('Making calls'); $incremented = false; $out = ''; $access = $this->access_all_or_shared($attr); $all_ids = []; foreach ($query_urls as $query_url => $method_and_args) { $this->push_to_stack("Query $query_url"); if ($access['all'] && $query_url == 'https://photoslibrary.googleapis.com/v1/sharedAlbums') { $defer = true; } else { $defer = false; } if (!empty($photonic_google_refresh_token) && !empty($this->access_token)) { $query_url = add_query_arg('access_token', $this->access_token, $query_url); } foreach ($method_and_args as $method => $args) { $this->push_to_stack('Sending request'); if (empty($args['filters'])) { $call_args = []; $call_args['method'] = $method; $call_args['body'] = $args; $call_args['sslverify'] = PHOTONIC_SSL_VERIFY; $response = wp_remote_request($query_url, $call_args); } else { $headers = []; $headers[] = 'Accept: application/json'; $headers[] = 'Content-Type: application/json'; // This doesn't work for some reason while using 'filters'. Google always responds with an invalid JSON format error. // Various options have been tried, including sending 'body' without a json_encode, enclosing the json_encode in '[]' etc. // Falling back on cURL for cases where $args have 'filters' /* $response = wp_remote_request($query_url, [ 'method' => 'POST', 'headers' => $headers, 'httpversion' => '1.0', 'body' => json_encode($args), ]); */ $ch = curl_init($query_url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, PHOTONIC_SSL_VERIFY); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // If not set, this prints the output curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($args)); $curl_response = curl_exec($ch); curl_close($ch); // The following mimics a structure that can be handled by is_wp_error and wp_remote_retrieve_body $response = []; $response['body'] = $curl_response; } $this->pop_from_stack(); if (!is_wp_error($response)) { $this->push_to_stack('Processing Response'); $body = wp_remote_retrieve_body($response); if (!$incremented) { $incremented = true; } $output = $this->process_response($body, $attr, $defer, $all_ids); if ($defer && is_array($output)) { foreach ($output as $object) { $all_ids[] = $object['id_1']; } } else { $out .= $output; } $this->pop_from_stack(); } else { $this->pop_from_stack(); // "Query $query_url" $this->pop_from_stack(); // 'Making calls' return $this->error($response->get_error_message()); } } $this->pop_from_stack(); } $this->pop_from_stack(); return $out; } /** * @param $body * @param $short_code * @param bool|false $deferred * @param array $remove * @return mixed */ private function process_response($body, $short_code, $deferred = false, $remove = []) { global $photonic_google_photo_title_display, $photonic_google_photos_per_row_constraint, $photonic_google_photos_constrain_by_padding, $photonic_google_photos_constrain_by_count, $photonic_google_photos_pop_per_row_constraint, $photonic_google_photos_pop_constrain_by_padding, $photonic_google_photos_pop_constrain_by_count, $photonic_google_photo_pop_title_display, $photonic_google_hide_album_photo_count_display; $out = ''; if (!empty($body)) { $body = json_decode($body); $row_constraints = ['constraint-type' => $photonic_google_photos_per_row_constraint, 'padding' => $photonic_google_photos_constrain_by_padding, 'count' => $photonic_google_photos_constrain_by_count]; $display = $short_code['display']; if (isset($body->albums) || isset($body->sharedAlbums)) { $albums = isset($body->albums) ? $body->albums : $body->sharedAlbums; $pagination = []; if (!empty($body->nextPageToken)) { $pagination = [ 'total' => 10, 'start' => 0, 'end' => 1, 'per-page' => $short_code['count'], 'next-token' => $body->nextPageToken, ]; } $albums = $this->build_level_2_objects($albums, $short_code, $remove, $pagination); if ($deferred) { return $albums; } $out .= $this->layout_gallery($albums, [ 'row_constraints' => $row_constraints, 'type' => 'albums', 'singular_type' => 'album', 'title_position' => $photonic_google_photo_title_display, 'level_1_count_display' => !empty($photonic_google_hide_album_photo_count_display), 'pagination' => $pagination, ], $short_code, 2 ); } else if (isset($body->mediaItems)){ if ($display == 'in-page') { $title_position = $photonic_google_photo_title_display; } else { $row_constraints = ['constraint-type' => $photonic_google_photos_pop_per_row_constraint, 'padding' => $photonic_google_photos_pop_constrain_by_padding, 'count' => $photonic_google_photos_pop_constrain_by_count]; $title_position = $photonic_google_photo_pop_title_display; } $level_2_meta = []; if (!empty($body->nextPageToken)) { $level_2_meta = [ 'total' => 10, 'start' => 0, 'end' => 1, 'per-page' => $short_code['count'], 'next-token' => $body->nextPageToken, ]; } $photos = $body->mediaItems; $photos = $this->build_level_1_objects($photos, $short_code); $out .= $this->layout_gallery($photos, [ 'title_position' => $title_position, 'row_constraints' => $row_constraints, 'parent' => 'album', 'level_2_meta' => $level_2_meta, ], $short_code, 1); } else if (isset($body->error)) { $out .= esc_html__('Failed to get data. Error:', 'photonic')."
\n"; $out .= $body->error->message; $out .= "
\n"; return $this->error($out); } } else { $out .= esc_html__('Failed to get data. Error:', 'photonic')."
\n"; $out .= $body; $out .= "
\n"; return $this->error($out); } return $out; } function build_level_1_objects($photos, $short_code, $module_parameters = [], $options = []) { $objects = []; foreach ($photos as $photo) { $object = []; $is_video = false; // if (empty($photo->mimeType) || stripos($photo->mimeType, 'image') !== 0) { if (!empty($photo->mediaMetadata) && !empty($photo->mediaMetadata->video)) { $is_video = true; } $media = explode(',', $short_code['media']); $videos_ok = in_array('videos', $media) || in_array('all', $media); $photos_ok = in_array('photos', $media) || in_array('all', $media); if (($is_video && !$videos_ok) || (!$is_video && !$photos_ok)) { continue; } $object['id'] = $photo->id; $object['thumbnail'] = $photo->baseUrl . "=w{$short_code['thumb_size']}-h{$short_code['thumb_size']}" . ($short_code['crop_thumb'] == 'crop' ? '-c' : ''); $object['tile_image'] = $photo->baseUrl . "=w{$short_code['tile_size']}-h{$short_code['tile_size']}"; $object['main_image'] = $photo->baseUrl . "=w{$short_code['main_size']}-h{$short_code['main_size']}"; if ($is_video) { $object['video'] = $photo->baseUrl."={$short_code['video_size']}"; $object['mime'] = empty($photo->mimeType) ? 'video/mp4' : $photo->mimeType; } else { $object['download'] = $object['main_image'].'-d'; } if (!isset($photo->productUrl)) { $object['main_page'] = $object['main_image']; } else { $object['main_page'] = $photo->productUrl; } if (!empty($photo->description)) { $object['title'] = $photo->description; } else { $object['title'] = ''; } $object['alt_title'] = $object['title']; $object['description'] = $object['title']; $objects[] = $object; } return $objects; } /** * @param $albums * @param $short_code * @param $remove * @param $pagination * @return array */ private function build_level_2_objects($albums, $short_code, $remove, &$pagination) { $filter = $short_code['filter']; $filters = empty($filter) ? [] : explode(',', $filter); $processed = []; $objects = []; foreach ($albums as $album) { if (!empty($filters) && ((!in_array($album->id, $filters) && strtolower($short_code['filter_type']) !== 'exclude') || (in_array($album->id, $filters) && strtolower($short_code['filter_type']) === 'exclude'))) { continue; } if (in_array($album->id, $remove)) { continue; } $object = $this->process_album($album, $short_code); if (!empty($object)) { $objects[] = $object; $processed[] = $album->id; } } global $photonic_google_chain_queries; if (!empty($pagination['next-token']) && strtolower($short_code['filter_type']) !== 'exclude' && !empty($filters) && count($processed) < count($filters) && !empty($photonic_google_chain_queries)) { $additional = []; if (!empty($short_code['count'])) { $additional['pageSize'] = $short_code['count']; } if (!empty($pagination['next-token'])) { $additional['pageToken'] = $pagination['next-token']; } $access = $this->access_all_or_shared($short_code); if ($access['shared']) { $query_url = 'https://photoslibrary.googleapis.com/v1/sharedAlbums'; } if ($access['all']) { $query_url = 'https://photoslibrary.googleapis.com/v1/albums'; } if (!empty($query_url)) { global $photonic_google_refresh_token; if (!empty($photonic_google_refresh_token) && !empty($this->access_token)) { $query_url = add_query_arg('access_token', $this->access_token, $query_url); } $call_args = []; $call_args['method'] = 'GET'; $call_args['body'] = $additional; $call_args['sslverify'] = PHOTONIC_SSL_VERIFY; $response = wp_remote_request($query_url, $call_args); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $body = json_decode($body); $inner_albums = isset($body->albums) ? $body->albums : $body->sharedAlbums; if (!empty($body->nextPageToken)) { $pagination['next-token'] = $body->nextPageToken; } else { $pagination = []; } $remaining = array_diff($filters, $processed); $remaining = implode(',', $remaining); $inner_code = $short_code; $inner_code['filter'] = $remaining; $inner = $this->build_level_2_objects($inner_albums, $inner_code, $remove, $pagination); $objects = array_merge($objects, $inner); } } } return $objects; } /** * @param $album * @param $short_code * @return array */ private function process_album($album, $short_code) { if (empty($album->coverPhotoBaseUrl)) { return null; } $object = []; $internal_short_code = $short_code; $internal_short_code['layout'] = empty($short_code['photo_layout']) ? $short_code['layout'] : $short_code['photo_layout']; unset($internal_short_code['filter']); unset($internal_short_code['filter_type']); $internal_short_code['view'] = 'photos'; $internal_short_code['album_id'] = $album->id; $object['id_1'] = "{$album->id}"; $object['thumbnail'] = $album->coverPhotoBaseUrl . "=w{$short_code['thumb_size']}-h{$short_code['thumb_size']}" . ($short_code['crop_thumb'] == 'crop' ? '-c' : ''); $object['tile_image'] = $album->coverPhotoBaseUrl . "=w{$short_code['tile_size']}-h{$short_code['tile_size']}"; $object['main_page'] = ''; $object['title'] = esc_attr($album->title); $object['counter'] = empty($album->totalMediaItems) ? $album->mediaItemsCount : $album->totalMediaItems; $object['data_attributes'] = [ 'thumb-size' => $short_code['thumb_size'], 'photo-count' => empty($short_code['photo_count']) ? $short_code['count'] : $short_code['photo_count'], 'photo-more' => empty($short_code['photo_more']) ? '' : $short_code['photo_more'], 'overlay-size' => $short_code['overlay_size'], 'overlay-crop' => $short_code['overlay_crop'], 'overlay-video-size' => $short_code['overlay_video_size'], ]; global $photonic_gallery_template_page; if ($short_code['popup'] == 'page' && !empty($photonic_gallery_template_page) && is_string(get_post_status($photonic_gallery_template_page))) { $object['gallery_url'] = $this->get_gallery_url($internal_short_code, [ 'title' => $object['title'], ]); } return $object; } private function access_all_or_shared($short_code) { $all_or_shared = []; $access = explode(',', $short_code['access']); if (empty($access) || (in_array('shared', $access) && in_array('not-shared', $access)) || in_array('all', $access)) { $all_or_shared['all'] = true; $all_or_shared['shared'] = false; } else if (count($access) == 1 && in_array('not-shared', $access)) { $all_or_shared['all'] = true; $all_or_shared['shared'] = true; } else if (count($access) == 1 && in_array('shared', $access)) { $all_or_shared['all'] = false; $all_or_shared['shared'] = true; } return $all_or_shared; } public function authentication_url() { return 'https://accounts.google.com/o/oauth2/auth'; } public function access_token_url() { return 'https://accounts.google.com/o/oauth2/token'; } protected function set_token_validity($validity) { $this->refresh_token_valid = $validity; } function execute_helper($args) { $query_url = 'https://photoslibrary.googleapis.com/v1/albums'; $parameters = [ 'access_token' => $this->access_token, 'pageSize' => 50, ]; if (!empty($args['nextPageToken'])) { $parameters['pageToken'] = sanitize_text_field($args['nextPageToken']); } $call_args = []; $call_args['method'] = 'GET'; $call_args['body'] = $parameters; $call_args['sslverify'] = PHOTONIC_SSL_VERIFY; $response = wp_remote_request($query_url, $call_args); if (!is_wp_error($response)) { if (isset($response['response']) && isset($response['response']['code'])) { if ($response['response']['code'] == 200) { $body = json_decode(wp_remote_retrieve_body($response)); if (isset($body->albums) && !empty($body->albums) && is_array($body->albums)) { $albums = $body->albums; $ret = "\n"; $ret .= "\t\n"; $ret .= "\t\t\n"; $ret .= "\t\t\n"; $ret .= "\t\t\n"; $ret .= "\t\t\n"; $ret .= "\t\n"; foreach ($albums as $album) { $ret .= "\t\n"; $ret .= "\t\t\n"; $ret .= "\t\t\n"; $ret .= "\t\t\n"; $ret .= "\t\t\n"; $ret .= "\t\n"; } if (!empty($body->nextPageToken)) { $ret .= "\t\n"; $ret .= "\t\t\n"; $ret .= "\t\n"; } $ret .= "
Album TitleThumbnailAlbum IDMedia Count
{$album->title}thumbnail{$album->id}{$album->mediaItemsCount}
\n"; $ret .= ''; $ret .= "\t\t
\n"; return '
'.$ret.'
'; } else { return '
'.esc_html__('No albums found', 'photonic').'
'; } } else { Photonic::log($response['response']); return '
'.sprintf(esc_html__('No data returned. Error code %s', 'photonic'), $response['response']['code']).'
'; } } else { Photonic::log($response); return '
'.esc_html__('No data returned. Empty response, or empty error code.', 'photonic').'
'; } } else { return '
'.$response->get_error_message().'
'; } } function refresh_access_token($refresh_token, $save) { $token = []; $response = Photonic::http($this->access_token_url(), 'POST', [ 'client_id' => $this->client_id, 'client_secret' => $this->client_secret, 'refresh_token' => $refresh_token, 'grant_type' => 'refresh_token' ]); if (!is_wp_error($response)) { $token = $this->parse_token($response); if (!empty($token)) { $token['client_id'] = $this->client_id; } if ($save) { $this->save_token($token); } } return $token; } }