You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
12 KiB
PHP

<?php
/**
* The Video Parser
*
* @since 2.0.0
* @package RankMath
* @subpackage RankMath\Schema\Video
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Schema\Video;
use RankMath\Helper;
use RankMath\Schema\DB;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Parser class.
*/
class Parser {
/**
* Post.
*
* @var WP_Post
*/
private $post;
/**
* Stored Video URLs.
*
* @var array
*/
private $urls;
/**
* The Constructor.
*
* @param WP_Post $post Post to parse.
*/
public function __construct( $post ) {
$this->post = $post;
}
/**
* Save video object.
*/
public function save() {
if (
! ( $this->post instanceof \WP_Post ) ||
wp_is_post_revision( $this->post->ID ) ||
! Helper::get_settings( "titles.pt_{$this->post->post_type}_autodetect_video", 'on' )
) {
return;
}
$content = trim( $this->post->post_content . ' ' . $this->get_custom_fields_data() );
if ( empty( $content ) ) {
return;
}
$this->urls = $this->get_video_urls();
$content = apply_filters( 'the_content', $content );
$allowed_types = apply_filters( 'media_embedded_in_content_allowed_types', [ 'video', 'embed', 'iframe' ] );
$tags = implode( '|', $allowed_types );
$videos = [];
preg_match_all( '#<(?P<tag>' . $tags . ')[^<]*?(?:>[\s\S]*?<\/(?P=tag)>|\s*\/>)#', $content, $matches );
if ( ! empty( $matches ) && ! empty( $matches[0] ) ) {
foreach ( $matches[0] as $html ) {
$videos[] = $this->get_metadata( $html );
}
}
$videos = array_merge( $videos, $this->get_links_from_shortcode( $content ) );
$videos = array_filter(
$videos,
function( $video ) {
return ! empty( $video['src'] ) ? $video['src'] : false;
}
);
if ( empty( $videos ) ) {
return;
}
$schemas = $this->get_default_schema_data();
foreach ( $videos as $video ) {
$schemas[] = [
'@type' => 'VideoObject',
'metadata' => [
'title' => 'Video',
'type' => 'template',
'shortcode' => uniqid( 's-' ),
'isPrimary' => empty( DB::get_schemas( $this->post->ID ) ),
'reviewLocationShortcode' => '[rank_math_rich_snippet]',
'category' => '%categories%',
'tags' => '%tags%',
'isAutoGenerated' => true,
],
'name' => ! empty( $video['name'] ) ? $video['name'] : '%seo_title%',
'description' => ! empty( $video['description'] ) ? $video['description'] : '%seo_description%',
'uploadDate' => ! empty( $video['uploadDate'] ) ? $video['uploadDate'] : '%date(Y-m-dTH:i:sP)%',
'thumbnailUrl' => ! empty( $video['thumbnail'] ) ? $video['thumbnail'] : '%post_thumbnail%',
'embedUrl' => ! empty( $video['embed'] ) ? $video['src'] : '',
'contentUrl' => empty( $video['embed'] ) ? $video['src'] : '',
'duration' => ! empty( $video['duration'] ) ? $video['duration'] : '',
'width' => ! empty( $video['width'] ) ? $video['width'] : '',
'height' => ! empty( $video['height'] ) ? $video['height'] : '',
'isFamilyFriendly' => ! empty( $video['isFamilyFriendly'] ) ? (bool) $video['isFamilyFriendly'] : true,
];
}
foreach ( array_filter( $schemas ) as $schema ) {
add_post_meta( $this->post->ID, "rank_math_schema_{$schema['@type']}", $schema );
}
}
/**
* Get default schema data.
*/
private function get_default_schema_data() {
if ( ! empty( DB::get_schemas( $this->post->ID ) ) ) {
return [];
}
$default_type = Helper::get_default_schema_type( $this->post->ID, true );
if ( ! $default_type ) {
return [];
}
$is_article = in_array( $default_type, [ 'Article', 'NewsArticle', 'BlogPosting' ], true );
$schema_data = [];
if ( $is_article ) {
$schema_data = [
'headline' => Helper::get_settings( "titles.pt_{$this->post->post_type}_default_snippet_name" ),
'description' => Helper::get_settings( "titles.pt_{$this->post->post_type}_default_snippet_desc" ),
'datePublished' => '%date(Y-m-dTH:i:sP)%',
'dateModified' => '%modified(Y-m-dTH:i:sP)%',
'image' => [
'@type' => 'ImageObject',
'url' => '%post_thumbnail%',
],
'author' => [
'@type' => 'Person',
'name' => '%name%',
],
];
}
$schema_data['@type'] = $default_type;
$schema_data['metadata'] = [
'title' => Helper::sanitize_schema_title( $default_type ),
'type' => 'template',
'isPrimary' => true,
];
return [ $schema_data ];
}
/**
* Get Video source from the content.
*
* @param array $html Video Links.
*
* @return array
*/
public function get_metadata( $html ) {
preg_match_all( '@src=[\'"]([^"]+)[\'"]@', $html, $matches );
if ( empty( $matches ) || empty( $matches[1] ) ) {
return false;
}
return $this->get_video_metadata( $matches[1][0] );
}
/**
* Validate Video source.
*
* @param string $url Video Source.
* @return array
*/
private function get_video_metadata( $url ) {
$url = preg_replace( '/\?.*/', '', $url ); // Remove query string from URL.
if (
$url &&
(
is_array( $this->urls ) &&
(
in_array( $url, $this->urls, true ) ||
in_array( $url . '?feature=oembed', $this->urls, true )
)
)
) {
return false;
}
$this->urls[] = $url;
$networks = [
'Video\Youtube',
'Video\Vimeo',
'Video\DailyMotion',
'Video\TedVideos',
'Video\VideoPress',
'Video\WordPress',
];
$data = false;
foreach ( $networks as $network ) {
$data = \call_user_func( [ '\\RankMathPro\\Schema\\' . $network, 'match' ], $url );
if ( is_array( $data ) ) {
break;
}
}
// Save image locally.
if ( ! empty( $data['thumbnail'] ) ) {
$data['thumbnail'] = $this->save_video_thumbnail( $data );
}
return $data;
}
/**
* Get Video Links from YouTube Embed plugin.
*
* @param string $content Post Content.
* @return array
*
* Credit ridgerunner (https://stackoverflow.com/users/433790/ridgerunner)
*/
private function get_links_from_shortcode( $content ) {
preg_match_all(
'~
https?:// # Required scheme. Either http or https.
(?:[0-9A-Z-]+\.)? # Optional subdomain.
(?: # Group host alternatives.
youtu\.be/ # Either youtu.be,
| youtube # or youtube.com or
(?:-nocookie)? # youtube-nocookie.com
\.com # followed by
\S*? # Allow anything up to VIDEO_ID,
[^\w\s-] # but char before ID is non-ID char.
) # End host alternatives.
([\w-]{11}) # $1: VIDEO_ID is exactly 11 chars.
(?=[^\w-]|$) # Assert next char is non-ID or EOS.
(?! # Assert URL is not pre-linked.
[?=&+%\w.-]* # Allow URL (query) remainder.
(?: # Group pre-linked alternatives.
[\'"][^<>]*> # Either inside a start tag,
| </a> # or inside <a> element text contents.
) # End recognized pre-linked alts.
) # End negative lookahead assertion.
[?=&+%\w.-]* # Consume any URL (query) remainder.
~ix',
$content,
$matches
);
if ( empty( $matches ) || empty( $matches[1] ) ) {
return [];
}
$data = [];
foreach ( $matches[1] as $video_id ) {
$data[] = $this->get_video_metadata( "https://www.youtube.com/embed/{$video_id}" );
}
return $data;
}
/**
* Validate Video source.
*
* @param array $data Video data.
* @return array
*
* Credits to m1r0 @ https://gist.github.com/m1r0/f22d5237ee93bcccb0d9
*/
private function save_video_thumbnail( $data ) {
$url = $data['thumbnail'];
if ( ! Helper::get_settings( "titles.pt_{$this->post->post_type}_autogenerate_image", 'off' ) ) {
return false;
}
if ( ! class_exists( 'WP_Http' ) ) {
include_once( ABSPATH . WPINC . '/class-http.php' );
}
$url = explode( '?', $url )[0];
$http = new \WP_Http();
$response = $http->request( $url );
if ( 200 !== $response['response']['code'] ) {
return false;
}
$image_title = __( 'Video Thumbnail', 'rank-math-pro' );
if ( ! empty( $data['name'] ) ) {
$image_title = $data['name'];
} elseif ( ! empty( $this->post->post_title ) ) {
$image_title = $this->post->post_title;
}
$filename = substr( sanitize_title( $image_title, 'video-thumbnail' ), 0, 32 ) . '.jpg';
/**
* Filter the filename of the video thumbnail.
*
* @param string $filename The filename of the video thumbnail.
* @param array $data The video data.
* @param object $post The post object.
*/
$filename = apply_filters( 'rank_math/schema/video_thumbnail_filename', $filename, $data, $this->post );
$upload = wp_upload_bits( sanitize_file_name( $filename ), null, $response['body'] );
if ( ! empty( $upload['error'] ) ) {
return false;
}
$file_path = $upload['file'];
$file_name = basename( $file_path );
$file_type = wp_check_filetype( $file_name, null );
$wp_upload_dir = wp_upload_dir();
// Translators: Placeholder is the image title.
$attachment_title = sprintf( __( 'Video Thumbnail: %s', 'rank-math-pro' ), $image_title );
/**
* Filter the attachment title of the video thumbnail.
*
* @param string $attachment_title The attachment title of the video thumbnail.
* @param array $data The video data.
* @param object $post The post object.
*/
$attachment_title = apply_filters( 'rank_math/schema/video_thumbnail_attachment_title', $attachment_title, $data, $this->post );
$post_info = [
'guid' => $wp_upload_dir['url'] . '/' . $file_name,
'post_mime_type' => $file_type['type'],
'post_title' => $attachment_title,
'post_content' => '',
'post_status' => 'inherit',
];
$attach_id = wp_insert_attachment( $post_info, $file_path, $this->post->ID );
// Include image.php.
require_once( ABSPATH . 'wp-admin/includes/image.php' );
// Define attachment metadata.
$attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
// Assign metadata to attachment.
wp_update_attachment_metadata( $attach_id, $attach_data );
return wp_get_attachment_url( $attach_id );
}
/**
* Get Video URls stored in VideoObject schema.
*
* @return array
*/
private function get_video_urls() {
$schemas = DB::get_schemas( $this->post->ID );
if ( empty( $schemas ) ) {
return [];
}
$urls = [];
foreach ( $schemas as $schema ) {
if ( empty( $schema['@type'] ) || 'VideoObject' !== $schema['@type'] ) {
continue;
}
$urls[] = ! empty( $schema['embedUrl'] ) ? $schema['embedUrl'] : '';
$urls[] = ! empty( $schema['contentUrl'] ) ? $schema['contentUrl'] : '';
}
return array_filter( $urls );
}
/**
* Get Custom fields data.
*/
private function get_custom_fields_data() {
$custom_fields = Str::to_arr_no_empty( Helper::get_settings( 'sitemap.video_sitemap_custom_fields' ) );
if ( empty( $custom_fields ) ) {
return;
}
$content = '';
foreach ( $custom_fields as $custom_field ) {
$content = $content . ' ' . get_post_meta( $this->post->ID, $custom_field, true );
}
return trim( $content );
}
}