|\WP_Error */ public static function items( $source, $value, $count ) { $count = max( 1, min( 24, (int) $count ) ); // The Instagram token is part of the key so a new token shows its posts immediately. $key = 'akram_feed_' . md5( $source . '|' . $value . '|' . $count . ( 'instagram' === $source ? '|' . Options::get( 'instagram_token', '' ) : '' ) ); $hit = get_transient( $key ); if ( is_array( $hit ) ) { return isset( $hit['__err'] ) ? new \WP_Error( 'nx_feed', $hit['__err'] ) : $hit; } switch ( $source ) { case 'instagram': $items = self::instagram( $count ); break; case 'youtube': $id = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $value ); if ( ! $id ) { $items = new \WP_Error( 'nx_feed', __( 'Add a YouTube channel ID (it starts with UC).', 'akram' ) ); break; } // YouTube's feed sometimes answers 404 or 500 for a moment, so a failed request is tried once more. $items = self::rss( 'https://www.youtube.com/feeds/videos.xml?channel_id=' . $id, $count ); if ( is_wp_error( $items ) ) { $items = self::rss( 'https://www.youtube.com/feeds/videos.xml?channel_id=' . $id, $count ); } break; default: $items = self::rss( esc_url_raw( (string) $value ), $count ); } if ( is_wp_error( $items ) ) { // While a source is failing, the last good items (kept for a week) are shown instead. $last = get_transient( $key . '_ok' ); if ( is_array( $last ) && $last ) { $items = $last; } // Failures are remembered for a few minutes so a broken source isn't retried on every page view. set_transient( $key, is_wp_error( $items ) ? array( '__err' => $items->get_error_message() ) : $items, 5 * MINUTE_IN_SECONDS ); return $items; } set_transient( $key, $items, self::TTL ); set_transient( $key . '_ok', $items, WEEK_IN_SECONDS ); return $items; } private static function rss( $url, $count ) { if ( ! $url ) { return new \WP_Error( 'nx_feed', __( 'Add a feed URL.', 'akram' ) ); } require_once ABSPATH . WPINC . '/feed.php'; $feed = fetch_feed( $url ); if ( is_wp_error( $feed ) ) { return $feed; } $out = array(); foreach ( $feed->get_items( 0, $count ) as $item ) { $image = ''; $enc = $item->get_enclosure(); if ( $enc ) { $image = (string) ( $enc->get_thumbnail() ?: ( 0 === strpos( (string) $enc->get_type(), 'image' ) ? $enc->get_link() : '' ) ); } if ( ! $image ) { $media = $item->get_item_tags( 'http://search.yahoo.com/mrss/', 'group' ); if ( ! empty( $media[0]['child']['http://search.yahoo.com/mrss/']['thumbnail'][0]['attribs']['']['url'] ) ) { $image = $media[0]['child']['http://search.yahoo.com/mrss/']['thumbnail'][0]['attribs']['']['url']; } } if ( ! $image && preg_match( '/]+src=["\']([^"\']+)/i', (string) $item->get_content(), $m ) ) { $image = $m[1]; } $out[] = array( 'title' => wp_strip_all_tags( (string) $item->get_title() ), 'url' => esc_url_raw( (string) $item->get_permalink() ), 'date' => (int) $item->get_date( 'U' ), 'image' => esc_url_raw( $image ), 'text' => wp_trim_words( wp_strip_all_tags( (string) $item->get_description() ), 28 ), ); } return $out; } private static function instagram( $count ) { $token = (string) Options::get( 'instagram_token', '' ); if ( ! $token ) { return new \WP_Error( 'nx_feed', __( 'Add an Instagram access token in Akram → Settings → Integrations.', 'akram' ) ); } $res = wp_remote_get( add_query_arg( array( 'fields' => 'id,caption,media_type,media_url,thumbnail_url,permalink,timestamp', 'limit' => $count, 'access_token' => $token, ), 'https://graph.instagram.com/me/media' ), array( 'timeout' => 10 ) ); if ( is_wp_error( $res ) ) { return $res; } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( empty( $body['data'] ) ) { return new \WP_Error( 'nx_feed', ! empty( $body['error']['message'] ) ? (string) $body['error']['message'] : __( 'Instagram returned no posts.', 'akram' ) ); } $out = array(); foreach ( $body['data'] as $p ) { $out[] = array( 'title' => '', 'url' => esc_url_raw( (string) ( $p['permalink'] ?? '' ) ), 'date' => strtotime( (string) ( $p['timestamp'] ?? '' ) ) ?: 0, 'image' => esc_url_raw( (string) ( 'VIDEO' === ( $p['media_type'] ?? '' ) ? ( $p['thumbnail_url'] ?? '' ) : ( $p['media_url'] ?? '' ) ) ), 'text' => wp_trim_words( (string) ( $p['caption'] ?? '' ), 20 ), ); } return $out; } /** Long-lived Instagram tokens last 60 days; refresh weekly. */ public static function refresh_instagram_token() { $token = (string) Options::get( 'instagram_token', '' ); if ( ! $token ) { return; } $res = wp_remote_get( add_query_arg( array( 'grant_type' => 'ig_refresh_token', 'access_token' => $token, ), 'https://graph.instagram.com/refresh_access_token' ), array( 'timeout' => 10 ) ); $body = is_wp_error( $res ) ? array() : json_decode( wp_remote_retrieve_body( $res ), true ); if ( ! empty( $body['access_token'] ) ) { Options::update( array( 'instagram_token' => (string) $body['access_token'] ) ); } } /** Google Places (New) reviews: { rating, total, url, reviews: [{ author, photo, rating, text, time }] } */ public static function google_reviews( $place_id ) { $place_id = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $place_id ); $api_key = (string) Options::get( 'google_places_key', '' ); if ( ! $place_id || ! $api_key ) { return new \WP_Error( 'nx_reviews', __( 'Add a Google Places API key (Settings → Integrations) and a Place ID.', 'akram' ) ); } $key = 'akram_greviews_' . md5( $place_id . determine_locale() . $api_key ); $hit = get_transient( $key ); if ( is_array( $hit ) ) { return isset( $hit['__err'] ) ? new \WP_Error( 'nx_reviews', $hit['__err'] ) : $hit; } $res = wp_remote_get( 'https://places.googleapis.com/v1/places/' . rawurlencode( $place_id ) . '?languageCode=' . rawurlencode( substr( determine_locale(), 0, 2 ) ), array( 'timeout' => 10, 'headers' => array( 'X-Goog-Api-Key' => $api_key, 'X-Goog-FieldMask' => 'displayName,rating,userRatingCount,googleMapsUri,reviews', ), ) ); if ( is_wp_error( $res ) ) { set_transient( $key, array( '__err' => $res->get_error_message() ), 5 * MINUTE_IN_SECONDS ); return $res; } $b = json_decode( wp_remote_retrieve_body( $res ), true ); if ( ! is_array( $b ) || isset( $b['error'] ) ) { $msg = (string) ( $b['error']['message'] ?? __( 'Google returned an error.', 'akram' ) ); set_transient( $key, array( '__err' => $msg ), 5 * MINUTE_IN_SECONDS ); return new \WP_Error( 'nx_reviews', $msg ); } $out = array( 'name' => (string) ( $b['displayName']['text'] ?? '' ), 'rating' => (float) ( $b['rating'] ?? 0 ), 'total' => (int) ( $b['userRatingCount'] ?? 0 ), 'url' => esc_url_raw( (string) ( $b['googleMapsUri'] ?? '' ) ), 'reviews' => array(), ); foreach ( (array) ( $b['reviews'] ?? array() ) as $r ) { $out['reviews'][] = array( 'author' => (string) ( $r['authorAttribution']['displayName'] ?? '' ), 'photo' => esc_url_raw( (string) ( $r['authorAttribution']['photoUri'] ?? '' ) ), 'rating' => (int) ( $r['rating'] ?? 0 ), 'text' => (string) ( $r['text']['text'] ?? ( $r['originalText']['text'] ?? '' ) ), 'time' => (string) ( $r['relativePublishTimeDescription'] ?? '' ), ); } set_transient( $key, $out, 12 * HOUR_IN_SECONDS ); return $out; } }