last_api_request; if ( $delta < $this->api_grace_period ) { usleep( ( $this->api_grace_period - $delta ) * 1000000 ); } $result = array( 'final_url' => $url, 'redirect_count' => 0, 'timeout' => false, 'broken' => false, 'log' => "(Using YouTube API)\n\n", 'result_hash' => '', ); $components = @parse_url( $url ); if ( isset( $components['query'] ) ) { parse_str( $components['query'], $query ); } else { $query = array(); } //Extract the video or playlist ID from the URL $video_id = null; $playlist_id = null; if ( strtolower( $components['host'] ) === 'youtu.be' ) { $video_id = trim( $components['path'], '/' ); } elseif ( ( strpos( $components['path'], 'watch' ) !== false ) && isset( $query['v'] ) ) { $video_id = $query['v']; } elseif ( '/playlist' == $components['path'] ) { $playlist_id = $query['list']; } elseif ( '/view_play_list' == $components['path'] ) { $playlist_id = $query['p']; } if ( empty( $playlist_id ) && empty( $video_id ) ) { $result['status_text'] = 'Unsupported URL Syntax'; $result['status_code'] = BLC_LINK_STATUS_UNKNOWN; return $result; } //Fetch video or playlist from the YouTube API if ( ! empty( $video_id ) ) { $api_url = $this->get_video_resource_url( $video_id ); } else { $api_url = $this->get_playlist_resource_url( $playlist_id ); } $conf = blc_get_configuration(); $args = array( 'timeout' => $conf->options['timeout'] ); $start = microtime_float(); $response = wp_remote_get( $api_url, $args ); $result['request_duration'] = microtime_float() - $start; $this->last_api_request = $start; //Got anything? if ( is_wp_error( $response ) ) { $result['log'] .= "Error.\n" . $response->get_error_message(); //WP doesn't make it easy to distinguish between different internal errors. $result['broken'] = true; $result['http_code'] = 0; } else { $result['http_code'] = intval( $response['response']['code'] ); if ( ! empty( $video_id ) ) { $result = $this->check_video( $response, $result ); } else { $result = $this->check_playlist( $response, $result ); } } //The hash should contain info about all pieces of data that pertain to determining if the //link is working. $result['result_hash'] = implode( '|', array( 'youtube', $result['http_code'], $result['broken'] ? 'broken' : '0', $result['timeout'] ? 'timeout' : '0', isset( $result['state_name'] ) ? $result['state_name'] : '-', isset( $result['state_reason'] ) ? $result['state_reason'] : '-', ) ); return $result; } /** * Check API response for a single video. * * @param array $response WP HTTP API response. * @param array $result Current result array. * @return array New result array. */ protected function check_video( $response, $result ) { $api = json_decode( $response['body'], true ); $videoFound = ( 200 == $result['http_code'] ) && isset( $api['items'], $api['items'][0] ); if ( isset( $api['error'] ) && ( 404 !== $result['http_code'] ) ) { //404's are handled later. $result['status_code'] = BLC_LINK_STATUS_WARNING; $result['warning'] = true; if ( isset( $api['error']['errors'] ) ) { $result['status_text'] = $api['error']['errors'][0]['reason']; } else { $result['status_text'] = __( 'Unknown Error', 'broken-link-checker' ); } $result['log'] .= $this->format_api_error( $response, $api ); } elseif ( $videoFound ) { $result['log'] .= __( 'Video OK', 'broken-link-checker' ); $result['status_text'] = _x( 'OK', 'link status', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_OK; $result['http_code'] = 0; //Add the video title to the log, purely for information. if ( isset( $api['items'][0]['snippet']['title'] ) ) { $title = $api['items'][0]['snippet']['title']; $result['log'] .= "\n\nTitle : \"" . htmlentities( $title ) . '"'; } } else { $result['log'] .= __( 'Video Not Found', 'broken-link-checker' ); $result['broken'] = true; $result['http_code'] = 0; $result['status_text'] = __( 'Video Not Found', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_ERROR; } return $result; } /** * Check a YouTube API response that contains a single playlist. * * @param array $response * @param array $result * @return array */ protected function check_playlist( $response, $result ) { $api = json_decode( $response['body'], true ); if ( 404 === $result['http_code'] ) { //Not found. $result['log'] .= __( 'Playlist Not Found', 'broken-link-checker' ); $result['broken'] = true; $result['http_code'] = 0; $result['status_text'] = __( 'Playlist Not Found', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_ERROR; } elseif ( 403 === $result['http_code'] ) { //Forbidden. We're unlikely to see this code for playlists, but lets allow it. $result['log'] .= htmlentities( $response['body'] ); $result['broken'] = true; $result['status_text'] = __( 'Playlist Restricted', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_ERROR; } elseif ( ( 200 === $result['http_code'] ) && isset( $api['items'] ) && is_array( $api['items'] ) ) { //The playlist exists. if ( empty( $api['items'] ) ) { //An empty playlist. It is possible that all of the videos have been deleted. $result['log'] .= __( 'This playlist has no entries or all entries have been deleted.', 'broken-link-checker' ); $result['status_text'] = _x( 'Empty Playlist', 'link status', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_WARNING; $result['http_code'] = 0; $result['broken'] = true; } else { //Treat the playlist as broken if at least one video is inaccessible. foreach ( $api['items'] as $video ) { $is_private = isset( $video['status']['privacyStatus'] ) && ( 'private' == $video['status']['privacyStatus'] ); if ( $is_private ) { $result['log'] .= sprintf( __( 'Video status : %1$s%2$s', 'broken-link-checker' ), $video['status']['privacyStatus'], '' ); $result['broken'] = true; $result['status_text'] = __( 'Video Restricted', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_WARNING; $result['http_code'] = 0; break; } } if ( ! $result['broken'] ) { //All is well. $result['log'] .= __( 'Playlist OK', 'broken-link-checker' ); $result['status_text'] = _x( 'OK', 'link status', 'broken-link-checker' ); $result['status_code'] = BLC_LINK_STATUS_OK; $result['http_code'] = 0; } } } else { //Some other error. $result['status_code'] = BLC_LINK_STATUS_WARNING; $result['warning'] = true; if ( isset( $api['error']['message'] ) ) { $result['status_text'] = $api['error']['message']; } else { $result['status_text'] = __( 'Unknown Error', 'broken-link-checker' ); } $result['log'] .= $this->format_api_error( $response, $api ); } return $result; } protected function get_video_resource_url( $video_id ) { $params = array( 'part' => 'status,snippet', 'id' => $video_id, 'key' => $this->get_youtube_api_key(), ); $params = array_map( 'urlencode', $params ); return 'https://www.googleapis.com/youtube/v3/videos?' . build_query( $params ); } protected function get_playlist_resource_url( $playlist_id ) { if ( strpos( $playlist_id, 'PL' ) === 0 ) { $playlist_id = substr( $playlist_id, 2 ); } $params = array( 'key' => $this->get_youtube_api_key(), 'playlistId' => $playlist_id, 'part' => 'snippet,status', 'maxResults' => 10, //Playlists can be big. Lets just check the first few videos. ); $query = build_query( array_map( 'urlencode', $params ) ); return 'https://www.googleapis.com/youtube/v3/playlistItems?' . $query; } protected function format_api_error( $response, $api ) { $log = $response['response']['code'] . ' ' . $response['response']['message']; $log .= "\n" . __( 'Unknown YouTube API response received.' ); //Log error details. if ( isset( $api['error']['errors'] ) && is_array( $api['error']['errors'] ) ) { foreach ( $api['error']['errors'] as $error ) { $log .= "\n---\n"; if ( is_array( $error ) ) { foreach ( $error as $key => $value ) { $log .= sprintf( "%s: %s\n", htmlentities( $key ), htmlentities( $value ) ); } } } } return $log; } public function get_youtube_api_key() { $conf = blc_get_configuration(); $api_key = ! empty( $conf->options['youtube_api_key'] ) ? $conf->options['youtube_api_key'] : ''; return apply_filters( 'blc_youtube_api_key', $conf->options['youtube_api_key'] ); } }