diff --git a/assets/play-button.png b/assets/play-button.png new file mode 100644 index 0000000..7c86dc8 Binary files /dev/null and b/assets/play-button.png differ diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..adaba2c --- /dev/null +++ b/changelog.md @@ -0,0 +1,697 @@ +## Changelog for newer versions can be found in readme.md + +### 4.9: June 7th, 2017 +* Compatibility with Yoast SEO 4.9. + +### 4.8: May 23rd, 2017 +* Compatibility with Yoast SEO 4.8. + +### 4.7: May 2nd, 2017 +* Compatibility with Yoast SEO 4.7. + +### 4.6: April 11th, 2017 +* Compatibility with Yoast SEO 4.6. + +### 4.5: March 21st, 2017 +* Only invalidate sitemaps on configured post types. +* Fixes a bug where there was a fatal error thrown when the plugin was active without Yoast SEO or Yoast SEO Premium. + +### 4.4: February 28th, 2017 + +* Adds a minimum and maximum value to the video rating field. +* Adds the `og:video:secure_url` meta tag. + +### 4.3: February 14th, 2017 + +* Compatibility with Yoast SEO 4.3. + +### 4.2.1: February 3rd, 2017 + +* Bugfixes + * Fixes "Fatal error: Class 'yoast_i18n' not found". + +### 4.2: January 31st, 2017 + +* Bugfixes + * Fixes translator comments that were missing or didn't follow the guidelines. + +### 4.1: January 17th, 2017 + +* Bugfixes + * Fixes link to google article about video sitemaps. + * Fixes a bug where the video-seo menu would overwrite the go premium menu item. + * Fixes: If a post uses a custom title/description template with variables, the variables were not being replaced correctly for the Video sitemap. + * Minor spelling & grammar fixes. + * If a video was previously detected, but the post type has since been excluded from VideoSEO, the video opengraph and schema tags would still be added to the front-end page. This has been fixed now. + * Fix case-sensitivity issues with video object meta tags. + * Minor XHTML syntax fix. + +* Enhancements + * Improves styling for notices. + * Minor improvements for compatibility with Yoast SEO. + * Minor UI improvements for buttons and translations. + * Add the video:duration tag to video page headers. + * Clarify what effect the option to allow videos to be embedded by other sites has. + * Clarify the description of the "Family Friendly" option used in the metabox. + * Improve support for the Yandex search engine by adding some Yandex specific tags. This can be turned off using the new wpseo_video_yandex_support filter (return false to turn it off). + * Allow for adding additional schema meta tags - such as transcript to a video object using the new wpseo_video_object_meta_content filter. + * Clarify the description for the family friendly checkbox. + +### 4.0: December 13th, 2016 + +* Fixes the YouTube video player URL to always use a protocol. This solves issues where the Google invalidates the sitemap and where Facebook does not recognize the player. (needs force re-index for existing posts) + +### 3.9: November 29th, 2016 + +* Enhancements + * Added support for the additional Wistia video urls and embed codes. If you use the Wistia video service, re-indexing your videos is highly recommended. + * Added fallback for the detail retrieval of private Vimeo videos. This will allow these to be recognized. (needs force re-index for existing posts). + * Added recognition of //player.vimeo.com/... type URLs. (needs force re-index for existing posts). + * Change the 'og:type' meta value to the more accurate 'video.other'. + * Change the 'og:video:type' meta value HTML5 which is now more accurate than Flash in most cases. + * Minor improvements in behaviour when installed on WP multi-site. + +* Bugfixes + * Fixed the YouTube video player URL. This should solve black screens and/or "Unable to resolve DNS" errors when embedding videos on Facebook and other sites. (needs force re-index for existing posts) + * Updated the Vimeo video player URL to the new HTML5 player format (with Flash fallback). This should solve black screens and/or "Unable to resolve DNS" errors when embedding these videos on Facebook and other sites. (needs force re-index for existing posts). + +### 3.8: November 8th, 2016 + +* Enhancements + * The wpseo_sitemaps_base_url filter will now be respected by the VideoSEO plugin. + * Makes the oEmbed recognition compatible with the upcoming WP 4.7. + +* Bugfixes + * Minor improvements in video URL recognition. + * Fixes a fatal error on PHP 5.2 when adding a YouTube video (_undefined method DateTime::add()_ / _undefined class DateInterval()_). + * Fixes a bug where adding a video in a custom post type would show an undefined index `content_width` when used in combination with non-compliant themes + * Fixes support for Advanced Responsive Video Embedder plugin. + * Fixes support for Automatic YouTube Video Post plugin. + * Fixes a bug where the sitemap had the wrong style when a custom post type 'video' exists. + * Makes sure that the video sitemap will be available as soon as this plugin is activated and unavailable after deactivation. + * Fixes "Disable video for this post" per-post setting not being respected for the og: meta tags which led to Facebook still displaying the video even if the video for the post was disabled. + * If an invalid date is encountered for the publication date of a video post, the publication date will be re-evaluated. + * If a video post title or content/excerpt is - or has been - updated, this will now be reflected in the sitemap and the video metadata. (needs force re-index for existing posts) + * If a video post SEO title or SEO description is - or has been - added/adjusted, this will now be reflected in the sitemap. (needs force re-index for existing posts) + * If a SEO description template had been set for the post type which includes the video, this will now be respected. (needs force re-index for existing posts) + * If a video post was first saved as draft and only published later, the publication date would be stuck on the draft date in the sitemap, this has been fixed. (needs force re-index for existing posts) + * The "Force re-index" functionality was broken with the implementation of the progress bar. This has now been fixed. Checking the "Force re-index" checkbox will now work again as expected, including the regeneration of thumbnails. + * The "Re-index" functionality did not properly respect the post types to be indexed for the Video sitemap as set on the VideoSEO settings page, which unintentionally led to fewer items being re-indexed than they should. This has now been fixed. + * The re-index functionality has been made more efficient and should now - for the same number of posts - be faster. + * The sitemap cache was not automatically cleared after a re-index. This has now been fixed. + * Fixes the minimum requirement checks on activation of the plugin. + +### 3.7: October 11th, 2016 + +* Enhancements + * Added iframe-based support for uStudio videos. + * Added missing index.php files. + +### 3.6: September 27th, 2016 + +* Changes + * Updated translations. + +### 3.5: September 7th, 2016 + +* Changes + * Adds support for Featured Video Plugin, props [ahoereth](https://github.com/ahoereth) + + +### 3.4: July 19th, 2016 + +* Changes + * Updated translations. + +### 3.3: June 14th, 2016 + +* Enhancements + * Adds the Yoast i18n module to the Yoast SEO Video settings page, which informs users the plugin isn't available in their language and what they can do about it. + +* Bugfixes + * Fixes a bug where the support beacon for Yoast SEO Video was added to all Yoast SEO settings pages. + * Fixes a bug where updates were not working reliably when multiple paid Yoast plugins were active. + +### 3.2: April 20th, 2016 + +* Fixes a bug where the video sitemap cache wasn't cleared on activation. +* Fixes a bug where video specific checks that were added to the content analysis would no longer work in combination with Yoast SEO 3.2 and higher. +* Fixes a bug where clicking the 'Update now' button on the plugin page didn't update correctly. + +### 3.1: March 1st, 2016 + +* Bug fixes + * Fixes a JS error on the post edit page causing the content analysis to break in combination with Yoast SEO versions higher than 3.0.7. + * Fixes a bug where our license manager could sometimes not reach our licensing system due to problems with ssl. + +* Enhancements + * Makes sure users don't have to reactivate their license after updating or disabling/enabling the plugin. + * Adds a support beacon on the Video SEO settings page enabling users to ask for support from the WordPress backend. + +### 3.0: November 18th, 2015 + +* Synchronized plugin version with all other Yoast SEO plugins for WordPress. + +* Bug fixes + * Fixes a fatal error that could occur while reïndexing the video sitemap. + * Fixes the video metabox that was broken in combination with Yoast SEO 3.0. + * Fixes deprecation warnings for filters that have been removed in Yoast SEO 3.0 + +* Enhancements + * Made sure video specific content analysis checks work well with the Real Time content analysis tool in Yoast SEO 3.0. + +2.0.4: June 23rd, 2015 +----- + +* Bug fixes + * Fixes a bug where https YouTube URLs weren't recognized. + * Fixes a bug where the sitemap cache wouldn't be cleared when saving options. + * Changed to the YouTube v3 API, making the YouTube integration work again. + +2.0.3: March 25th, 2015 +----- + +* Bug fixes + * Fixes a bug where the video sitemap could contain wrongly formatted date times. + * Fixes an undefined index notice for the $post global that was fired when creating a new product in WooCommerce. + * Fixes a bug where title variables weren't parsed well in the Video Sitemap. + * Fixes a bug where the video thumbnails were saved without an extension. +* Enhancement + * Added 5 new languages: en_GB, es_MX, fr_FR, nl_NL and tr_TR. + + +2.0.2: Dec 17th, 2014 +----- + +* Bug fixes: + * Fix for notice on the snippet preview + +* Enhancements: + * Showing progressbar on re-indexing the video sitemap + +2.0.1: Nov 11th, 2014 +----- + +* Bug fixes: + * Fixed: Vixxy shortcode/url combi not recognized + * Fixed: Missing stylesheet error + * Fixed: Limiting issue on sitemap + +* Enhancements: + * Added translations for Persian and Brazilian Portuguese + * Removed translations for French, Dutch and Swedish. If you would like to help translate these languages, please sign up at translate.yoast.com! + * Improved translations for Danish, German, Hungarian and Italian + +2.0: Oct 7th, 2014 +----- + +* Bug fixes: + * Fixed: shortcode list would often not be reset properly. + * Fixed: escaped shortcodes would still be searched for video. + * Fixed: no name shortcode attributes wouldn't always be recognized. + * Fixed: Flickr video detail retrieval was failing, SSL now required. + * Fixed: Compatibility issue between support for the [JW Player](http://wordpress.org/extend/plugins/jw-player-plugin-for-wordpress/) plugin and fitvids.js. + * Fixed: only the first shortcode found would be checked to see if it was a video shortcode, then it would fall back to other methods, now all shortcodes are checked until a video shortcode is found. If none is found, it will still fall back to other methods of finding video. + * Fixed: small regression where wordpress.tv video details would not always be retrieved. + * Fixed: `[videopress]` shortcode - while supported by plugins - was still not recognized. + * Fixed: regression where numeric video ids would sometimes prevent video detail retrieval. + * Fixed: most of vidyard detail retrieval failed. + * Fixed: bug where content of the last recognized meta field would overrule earlier found information. + * Fixed: bug where a meta field containing a mixture of html/text and a url at the end could be accepted as content_loc. + * Fixed: the VideoSEO plugin would auto-de-activate on an upgrade of WPSEO. This should no longer happen. + +* Enhancements: + * Added support for recognizing video attachments without additional plugins. + * Added support for recognizing `.ogv` files as video files. + * Added support for custom Wistia domains. + * A lot more video URLs will be recognized as such. + * Better support for protocol-less urls all round. + * Add Video SEO menu item to the admin bar + * WP 4.0 removes oembed support for Viddler videos as [Viddler no longer supports free personal accounts](https://gigaom.com/2014/02/07/viddler-gets-ready-to-delete-personal-videos/). For those users who still use Viddler, Video SEO will continue to support both the OEmbedding as well as - of course - the SEO aspect. + +* Supported Services: + * Added support for 23Video videos (retrieval of video details). + * Added support for Archive.org videos (retrieval of video details). + * Added support for CollegeHumor.com videos (retrieval of video details). + * Added support for Funnyordie.com videos (retrieval of video details - unfortunately this does not (yet) work for short urls). + * Added support for Hulu.com videos (retrieval of video details). + * Added support for Revision3 videos (retrieval of video details). + * Added support for TED videos (retrieval of video details). + * Added support for VideoJug videos (retrieval of video details). + * Added support for Snotr videos via Embedly (limited video details). + * Added support for Spike.com/IFilm videos via Embedly (retrieval of video details). + * Added support for Vine videos via Embedly (retrieval of video details). + * If no video detail retrieval is available, Embedly will be used to try and retrieve details anyway. + * Much improved support for uploaded/locally hosted videos (retrieval of video details). + * Improved support for YouTube (country) sub-domains and alternative protocols (httpvhd, httpvhp, youtube::). Removed support for audio-only embeds as, well, audio is not video. + * Improved support for Animoto videos (recognition of urls). + * Improved support for Blip.tv videos (improved recognition leading to better retrieval of video details). + * Improved support for Dailymotion.com videos (recognition of short urls). + * Improved support for Flickr videos (recognition of short urls and better retrieval of video details). + * Improved support for Viddler videos (retrieval of video details). + * Improved support for VideoPress and WordPress.tv (retrieval of video details). + * Improved support for Vimeo videos (url recognition and retrieval of video details). + * Improved support for Vzaar videos (url recognition and retrieval of video details). + * Improved support for Wistia videos (recognition of urls and retrieval of video details). + * Slightly improved support for YouTube videos (retrieval of video details). + +* Supported Plugins: + * Added support for the [Flowplayer HTML5](http://wordpress.org/plugins/flowplayer5/) plugin. + * Added support for the [JetPack](http://wordpress.org/plugins/jetpack/) plugin shortcodes module. + * Added support for the [VideoPress](http://wordpress.org/plugins/video/) plugin. + * Added support for the [YouTube Embed Plus](http://wordpress.org/plugins/youtube-embed-plus/) plugin. + * Improved support for the [Advanced Responsive Video Embedder](http://wordpress.org/plugins/advanced-responsive-video-embedder/) plugin - a large number of shortcodes were not recognized. + * Improved support for the [IFrame Embed for YouTube](http://wordpress.org/extend/plugins/iframe-embed-for-youtube/) plugin - shortcode was not recognized. + * Improved support for the [Simple Video Embedder](http://wordpress.org/plugins/simple-video-embedder/) plugin - shortcode was recognized, better handling of custom fields. + * Improved support for the [Sublime Video](http://wordpress.org/extend/plugins/sublimevideo-official/) plugin - not all possible video sources were recognized. + * Improved support for the [TubePress](http://wordpress.org/extend/plugins/tubepress/) plugin - added Vimeo support. + * Improved support for the [Viper Video Quicktags](http://wordpress.org/extend/plugins/vipers-video-quicktags/) plugin - a large number of shortcodes were not recognized. + * Improved support for the [WP Video Lightbox](http://wordpress.org/extend/plugins/wp-video-lightbox/) plugin - thumbnail image was not supported. + * Improved support for the [WP YouTube Player](http://wordpress.org/extend/plugins/wp-youtube-player/) plugin - added support for id instead of url and for width, height attributes. + * Improved support for the [YouTuber](http://wordpress.org/extend/plugins/youtuber/) plugin - shortcode was not supported. + * Improved support for the [YouTube Embed](http://wordpress.org/extend/plugins/youtube-embed/) plugin - alternative protocols recognition. + * Improved support for the [YouTube with Style](http://wordpress.org/extend/plugins/youtube-with-style/) plugin - playlist syntax would break support. + * Removed support for the [Better Youtube Embeds](http://wordpress.org/extend/plugins/dirtysuds-embed-youtube-iframe/) plugin as the plugin functionality is now included in WP core and the plugin is no longer active. + * Removed support for the [Instabuilder](http://instabuilder.com/) plugin. + * Removed explicit support for the [Premise](http://getpremise.com/) plugin. + * Removed explicit support for the [Youtube Brackets](http://wordpress.org/extend/plugins/youtube-brackets/) plugin as the plugin hasn't been updated in eight years. + +* Other: + * Minimum requirement for WP now 3.6. + * Added license information + * Applied some best practices + +1.7.2: July 17th, 2014 +----- + +Fix added whitespace after content cause in 1.7 update. + +1.7.1: July 15th, 2014 +----- + +Fix error in update caused by missing the version number update in 1.7. + +1.7: July 14th, 2014 +----- + +* Bug fixes: + * Fixed: bug where `$content` would be empty for an `mrss_item`. + * Fixed: minor bug in upgrade routine. + * Fixed: bugs in Animoto and Screenr oembed provider addition. + * Fixed: issue with sitemap errors when conflicting http protocols were given. + * Fixed: video sitemap could show in sitemap index even when no posts with videos were found. + * Fixed: video description generated from content could break off in the middle of a word or html entity. + * Fixed: error on plugin activation. + * Fixed: sitemap conflict when a custom post type named 'video' would exist. + * Fixed: issue where durations would not be shown correctly in the metabox. + +* Enhancements: + * Add oembed support for wistia.net domain and wistia protocol-relative urls. + * Moved language file loading to the init hook to allow for translation overloading. + * Improved clean-up of uploaded files. + * Update snippet preview to use latest Google design changes in line with the earlier update to WP SEO. This fixes the javascript error some people were experiencing. + * Auto-deactivate plugin in circumstances that it can't work. + * Increased size of YouTube thumbnail image being retrieved. + +1.6.3: March 31st, 2014 +----- + +* Bug fixes: + * Fixed a warning for a missing variable in sanitize_rating. + +1.6.2: March 17th, 2014 +----- + +* Bug fixes: + * Fixed a warning for a missing variable. + * Updated Fitvids.js to fix some issues with it. + +* Enhancements: + * Fitvids will now be included un-minified when `SCRIPT_DEBUG` is on. + +1.6.1: March 11th, 2014 +----- + +Fix wrong boolean check. + +1.6: March 11th, 2014 +----- + +Compatibility with WPSEO 1.5 and implementation of the same options & meta philosophy + +* Bug fixes + * Fixed: Non-static methods should not be called statically + * Fixed: noindex setting wasn't being respected properly + * Fixed: some inconsistent admin form texts + * Fixed: Warning when loading new post. + * Fixed: Always re-validate license key on change. + +* i18n + * Updated .pot file + * Updated it_IT + +1.5.5.1 +----- + +* Bug fixes + * Make sure thumbnail image is available. + * Move initialisation of plugin to earlier hook to make sure it's there when XML sitemap is generated. + +1.5.5 +----- + +* Bug fixes + * Remove dependency on `WPSEO_URL` constant. + * Fix use of wrong image in OpenGraph and Schema.org output when a thumbnail is manually selected. + * Restore $shortcode_tags to original after `index_content()`. + +* Enhancements + * Use media uploader to change video thumbnail. + * Add setting to allow video playback directly on Facebook (defaults to on). + +1.5.4.6 +----- + +* Bug fixes + * Prevent warning on line 4169, for unset video taxonomies. + * Prevent issues with custom fields that have spaces in their keys. + * Added support for more Dailymotion URLs. + +* Enhancements + * Remove CDATA in favor of proper encoding of entities. + * Force 200 status codes and proper caching on both video sitemap XML and XSL. + * Add support for [WP YouTube Lyte](http://wordpress.org/extend/plugins/wp-youtube-lyte/) shortcode. + +* i18n + * Renamed wpseo-video.pot to yoast-video-seo.pot + * Updated fr_FR + * Added hu_HU + + +1.5.4.5 +----- + +* To make best use of the new features in this update, please reindex your videos. + +* Bug fixes + * Several i18n namespace fixes. + * Make videos in taxonomy descriptions pick up properly again. + * Fix for Wistia popover embeds and Wistia https URLs. + * Prevent output of hd attribute for videos in XML Video sitemap. + * Make sure opengraph image is always set to "full" size. + * Add width and height for Youtube videos. + * Prevent notice in sitemap when video from taxonomy term is displayed. + * Prevent wrong or empty dates in XML video sitemap. +* Enhancements + * Add option to manually add tags per video. + * Add option to override video category (normally defaults to first post category). + * Order videos in XML video sitemap by date modified, ascending. + * Add "proper" Facebook video integration. + * Added support for [Advanced Responsive Video Embedder](http://wordpress.org/plugins/advanced-responsive-video-embedder/). + * Added support for muzu.tv. + * Allow for custom fields that hold arrays to be detected too. + * Add support for custom Vimeo URLs. (eg http://vimeo.com/yoast/video-seo) + * Make sure the video thumbnail is always put out as an og:image too. + * Added support for Instabuilder video shortcodes + * Added support for Vidyard + * Set license key with a constant + * Added support for Cincopa + * Added support for Brightcove + * Added support for videos in the 'Archive Intro Text' (Genesis) in the video sitemap + * Added support for [WP OS FLV plugin](http://wordpress.org/plugins/wp-os-flv/) + * Added support for [Wordpress Automatic Youtube Video Post] (http://wordpress.org/plugins/automatic-youtube-video-posts/) + +1.5.4.4 +----- + +* Bug fixes + * Spaces in custom fields settings are now properly trimmed. + * Fix for Vzaar URLs. + * Wistia embed with extra classes now properly detected. +* Enhancements + * Video sitemap now adheres to same pagination as post sitemap. + * Video XML Sitemap date now properly retrieved from last modified post with movie. + +1.5.4.3 +----- + +* Enhancements + * Add support for `fvplayer` shortcode. + * Add option to manually change or enter duration. + +1.5.4.2 +----- + +* Bug fixes: + * Properly allow normal meta description length when video has been disabled for post. +* Enhancements: + * Added option to disable RSS enhancements, to prevent clashes with podcasting plugins. + +1.5.4.1 +----- + +* Move loading of the plugin to prio 20, in line with upgrades of the core WordPress SEO plugin. + +1.5.4 +----- + +* Enhancements: + * Added support for [fitvids.js](http://fitvidsjs.com/), enable it in the Video SEO settings to make your Youtube / Vimeo / Blip.tv / Viddler / Wistia videos responsive, meaning they'll become fluid. This might not work with all embed codes, let us know when it doesn't work for a particular one. + * Removed the ping functionality as that's fixed within the core plugin. + * Added code that forces you to update WordPress to 3.4 or higher and the WordPress SEO plugin to 1.4 or higher to use the plugin. +* Bug fixes: + * Fixed a bug that would prevent the time last modified of the video sitemap to update. + +1.5.3 +----- + +* Enhancements: + * Improved defaults: now enables all public post-types by default on install. + * Option to change the basename of the video sitemap, from video-sitemap.xml to whatever-sitemap.xml by setting the `YOAST_VIDEO_SITEMAP_BASENAME` constant. + * If post meta values are encoded, the plugin now decodes them. +* Bug fixes: + * No longer override opengraph image when one has already been set. + * Add extra newlines before video schema to allow oEmbed to work. + * No longer depends on response from Vzaar servers to create sitemap, properly uses the referrer to authenticate requests and adds option in settings to add your Vzaar CNAME. + * When there's a post-type with the slug `video`, the plugin now automatically changes the basename to `yoast-video`. + * No longer print empty `

` for empty description in meta box. + * Improve logic whereby "this image" link is shown correctly and only when the video thumb is not overridden. + +1.5.2 +----- + +* Enhancements: + * Added support for Vzaar videos, embedded with either iframe, object embed or shortcode through 1 of 2 plugins. + * Added [TubePress](https://wordpress.org/plugins/tubepress/) support. +* Bug fixes + * Wistia.net support added (not just .com). + * Fixed bug in parsing youtube_sc shortcodes. + +1.5.1 +----- + +* Bug fixes: + * Improved activation. +* Enhancements: + * Add support for titan lightbox. + * Prevented some notices. + +1.5 +----- + +* Bug fixes: + * Make `mrss_gallery_lookup` public to prevent notices. + * Fix some forms of object detection for youtube and others. + * Fix detection of [video] shortcodes. +* Enhancements: + * Allow deactivation of license key so it can be used on another domain. + * Add link to detected thumbnail on video tab. + * Changed text-domain from `wordpress-seo` to `yoast-video-seo`. + * Made sure all the strings are translatable. + * Touch up admin sections styling. +* i18n: + * You can now translate the plugin to your native language should you need a translation, check [translate.yoast.com](http://translate.yoast.com/projects/yoast-video-seo) for details. + * Changed text-domain from `wordpress-seo` to `yoast-video-seo`. + * Added .pot file to repository. + * Added Dutch translation. + +1.4.4 +----- + +* Bug fixes: + * Prevent issues with content_width global. + * Prevent trying to activate an already activated license. + * Prevent a notice for custom fields. + * A fix for wistia popover embeds. +* New features: + * Add PluginBuddy VidEmbed support. + +1.4.3 +----- + +* Bug fixes: + * Now matches multiple iframes / objects on a page. + * Fix several bugs where embeds without quotes around the URL wouldn't be recognized. +* New features: + * Added an option to set the content width for your theme if your theme doesn't set it. + * Added support for Sublime video and its [official WordPress plugin](https://wordpress.org/plugins/sublimevideo-official/). + * Added SEO & oEmbed support for Animoto. + * Added ping for Bing with the video sitemap. + * Added a _bunch_ of supported plugins & shortcodes for YouTube embeds. + +1.4.2 +----- + +* Bug fixes / Enhancements: + * Try to prevent timeout on license validation. + * Clean up of a lot of regexes in the plugin. + * Prevent relative image URL paths and images set as just 'h'. + * Prevent double output of posts. + * Fixed small bug that would prevent youtube URLs with the video ID in a weird place in the URL from working. + * Improve Wistia embed support. + * Lengthen timeout for video info requests. +* New features: + * Added support for html5 video elements (d'0h!). + * Add support for [vimeo id= and [youtube id= embed codes + * Added support for self-hosted videos with just a file URL in custom field. In these cases the featured image is used as thumbnail. + * Added generic fallback to post thumbnail image if there is no video thumbnail. + +1.4 +--- + +* Bug fixes / Enhancements: + * Fix Vimeo embed detection. + * Switch Vimeo to oEmbed API. + * When available, use html5_file for jwplayer embeds. +* New features: + * Added video content optimization tips in the page analysis tab of WordPress SEO. + * Added support for [WP Video Lightbox plugin](https://wordpress.org/plugins/wp-video-lightbox/). + * Added initial support for [Flowplayer plugin](http://wordpress.org/plugins/fv-wordpress-flowplayer/). + * Added support for Wistia video hosting platform. + * Added support for Vippy video hosting platform (thanks to Ronald Huereca). + * Added support for shortcodes from [Weaver theme](https://wordpress.org/themes/weaver-ii). + +1.3.4 +----- + +* Bug fixes: + * Fixed Viddler check. + * Fix strip tags for videoObject output. + * Don't filter content when in a feed. + * Improve parsing of VideoPress embed ID's. +* Enhancements: + * Added support for checking custom fields for videos. + * Added support for Press75's Simple Video Embedder (and thus for all their themes). + +1.3.3 +----- + +* Bug fixes: + * Properly catch thumbnail images when the path is relative instead of absolute. + * Strip shortcodes for plugins that don't register them properly as well. + * Prevent empty titles. + * Wrap XML sitemap and MediaRSS textual content in CDATA tags, this solves about 900.000 issues with encoding. + * Fixed [Veoh](http://www.veoh.com/) support. +* Enhancements: + * When a post is in more than one category, the excess categories are now used as tags. + * Don't print sitemap lines for videos that have no thumbnail and either a content location or a player location. + * If the description and excerpt are empty, use the title for the description, as an empty description is invalid. + * Changed the name of the family friendly variable, so it can't go "wrong" with old data. + * Added support for the `video:uploader` tag. This automatically links to the post authors posts page. + * Make terms use their own name as category in XML sitemap. + * Added support for jwplayer shortcode embeds with file and image attributes instead of mediaid. + * Added support for the [WordPress Video Plugin](http://wordpress.org/plugins/wordpress-video-plugin/). + * Added support for the [MediaElements.js](http://wordpress.org/plugins/media-element-html5-video-and-audio-player/) plugin. + * Added support for the [WP YouTube Player](http://wordpress.org/plugins/wp-youtube-player/) plugin. + * Added support for the [Advanced YouTube Embed Plugin by Embed Plus](http://wordpress.org/plugins/embedplus-for-wordpress/) plugin. + * Added support for the [VideoJS - HTML5 Video Player for WordPress](http://wordpress.org/plugins/videojs-html5-video-player-for-wordpress/) plugin. + * Added support for the [YouTube Shortcode](http://wordpress.org/plugins/youtube-shortcode/) plugin. + +1.3.2 +----- + +* Bug fixes: + * Fix XSLT URL issue, for real this time. Sometimes you have to ignore WordPress internals because they are just + plain wrong. This is such a time. The path to the XSL file should now always be correct. Note the word "should" + though. + * Improve matching of Youtube ID's, apparently those can contain underscores too. + * Improve re-indexation process by running through consecutive loops of 100 posts, to avoid memory issues. + * Fixed very annoying bug where videos would be mark as non-family-friendly by default. + * Force view count to be an integer. +* Enhancements: + * Switched around the logic for family friendliness. It now assumes all videos are family friendly by default and + you have to check the box to make it NON family friendly. + +1.3.1 +----- + +* Bug fixes: + * Prevent relative paths to images + * Prevent post_id from showing up in XML Video Sitemap + * Fix wrong URL to XSLT +* Enhancements: + * Added support for [JW Player Plugin](http://wordpress.org/plugins/jw-player-plugin-for-wordpress/) embeds (only embeds with `mediaid=` will work for now). + +1.3 +--- + +* Bug fixes: + * Even more YouTube embed fixes, also fixes empty Youtube ID issue. + * Properly grab thumbnail from YouTube instead of "assuming" a URL. + * Improve code that grabs duration from YouTube API. +* Enhancements: + * Add support for searching through category / tag / term descriptions for video content. + * Get view count from YouTube API. + * Add option to hide sitemap from everyone except admins and Googlebot. + * Add option to disable the video integration on a single post and page by adding a checkbox on the Video tab. + * Changed the way reindex gets called, so the admin keeps working immediately after a reindex without a refresh. + * Added option to force re-indexation of old posts that have already been indexed as having video (normally + they're just refreshed but no external calls are being done). + +1.2.2 +----- + +* Bug fixes: + * Properly work with [youtube]video-id[/youtube] type embed shortcodes. +* Enhancements: + * Option to only show the XML video sitemap to admins and to googlebot, not to any other visitors. This prevents + other visitors from downloading your video files. + +1.2.1 +----- + +* Bug fixes: + * Properly works with index.php URLs. + * Sends right URL for video sitemap on Google ping at all times. + * Correctly clean up video descriptions & tags for display in the XML sitemap. +* Enhancements: + * Added support for [Smart Youtube Pro](http://wordpress.org/plugins/smart-youtube/). + * Added support for Viddler iframe embeds. + * Added support for youtu.be oEmbeds. + * Preliminary Brightcove support. + +1.2 +--- + +* The Video tab in the meta box now works, so you can change the preview image. +* The plugin now adds full support for the videoObject schema. +* Several fixes to video recognition, especially for youtube iframe embeds, be sure to click re-index on the Video SEO page if you have those. + +1.1 +--- + +* This version should work better on activation. +* The plugin settings are now moved into its own SEO -> Video SEO admin page and out of the XML Sitemaps page. +* The plugin now recognizes youtube and vimeo embeds with an object tag or an iframe, to use this just click reindex videos. +* Improved the snippet preview date display. +* Fixed a few notices. + +1.0 +--- + +* Initial version + +0.2 +--- + +* First private beta release diff --git a/classes/class-wpseo-meta-video.php b/classes/class-wpseo-meta-video.php new file mode 100644 index 0000000..98895d6 --- /dev/null +++ b/classes/class-wpseo-meta-video.php @@ -0,0 +1,320 @@ + tag. + * @type string $description Optional. Description to show underneath the field. + * @type string $expl Optional. Label for a checkbox. + * @type string $help Optional. Help text to show on mouse over ? image. + * @type int $rows Optional. Number of rows for a textarea, defaults to 3. + * @type string $placeholder Optional. Currently not used in this class. + * } + * + * {@internal + * - Titles, help texts, description text and option labels are added via a translate_meta_boxes() method + * in the relevant child classes (WPSEO_Metabox and WPSEO_Social_admin) as they are only needed there. + * - Beware: even though the meta keys are divided into subsets, they still have to be uniquely named!} + */ + public static $meta_fields = [ + 'video' => [ + 'videositemap-disable' => [ + 'type' => 'hidden', + 'title' => '', // Translation added later. + 'default_value' => 'off', + 'expl' => '', // Translation added later. + ], + 'videositemap-thumbnail' => [ + 'type' => 'hidden', + 'title' => '', // Translation added later. + 'default_value' => '', + 'description' => '', + 'placeholder' => '', // Translation added later. + ], + 'videositemap-duration' => [ + 'type' => 'hidden', + 'title' => '', // Translation added later. + 'default_value' => '0', + 'description' => '', // Translation added later. + 'options' => [ + 'min_value' => '0', + 'max_value' => null, + 'step' => '1', + ], + ], + + /* + * {@internal Not used directly anywhere except for storage and retrieval of the meta box + * form values. The real use is in the video_meta key which retrieves the value + * of this from $_POST. + * However removing this would cause the meta box field to go blank as the info + * can no longer be retrieved, so leaving it as is for now.} + */ + 'videositemap-tags' => [ + 'type' => 'hidden', + 'title' => '', // Translation added later. + 'default_value' => '', + 'description' => '', // Translation added later. + ], + + /* + * {@internal Not used directly anywhere except for storage and retrieval of the meta box + * form values. The real use is in the video_meta key which retrieves the value + * of this from $_POST. + * However removing this would cause the meta box field to go blank as the info + * can no longer be retrieved, so leaving it as is for now.} + */ + 'videositemap-rating' => [ + 'type' => 'hidden', + 'title' => '', // Translation added later. + 'default_value' => 0, + 'description' => '', // Translation added later. + 'options' => [ + 'min_value' => '0', + 'max_value' => '5', + 'step' => '0.1', + ], + ], + 'videositemap-not-family-friendly' => [ + 'type' => 'hidden', + 'title' => '', // Translation added later. + 'default_value' => 'off', + 'expl' => '', // Translation added later. + 'description' => '', // Translation added later. + ], + ], + + + /* Fields we should validate & save, but not show on any form */ + 'non_form' => [ + 'video_meta' => [ + 'type' => null, + 'default_value' => 'none', + 'serialized' => true, + ], + ], + ]; + + /** + * Hook into WPSEO_Meta + * + * @return void + */ + public static function init() { + add_filter( 'add_extra_wpseo_meta_fields', [ __CLASS__, 'register_video_meta_fields' ] ); + add_filter( 'wpseo_metabox_entries_video', [ __CLASS__, 'adjust_video_meta_field_defs' ], 10, 2 ); + + add_filter( 'wpseo_sanitize_post_meta_' . WPSEO_Meta::$meta_prefix . 'videositemap-duration', [ __CLASS__, 'sanitize_duration' ], 10, 3 ); + add_filter( 'wpseo_sanitize_post_meta_' . WPSEO_Meta::$meta_prefix . 'videositemap-rating', [ __CLASS__, 'sanitize_rating' ], 10, 3 ); + add_filter( 'wpseo_sanitize_post_meta_' . WPSEO_Meta::$meta_prefix . 'videositemap-thumbnail', [ __CLASS__, 'sanitize_thumbnail_upload' ], 10, 3 ); + add_filter( 'wpseo_sanitize_post_meta_' . WPSEO_Meta::$meta_prefix . 'video_meta', [ __CLASS__, 'sanitize_video_meta' ], 10, 2 ); + } + + /** + * Add the video meta fields to the WPSEO_Meta::$meta_fields definitions + * + * @param array $fields Fields already in place (possibly from other add-on plugins). + * + * @return array + */ + public static function register_video_meta_fields( $fields ) { + return WPSEO_Meta::array_merge_recursive_distinct( $fields, self::$meta_fields ); + } + + /** + * Prepare the video meta field definitions for display in the metabox + * + * @param string $field_defs Field definitions for the requested tab. + * @param string $post_type Post type of the current post. + * + * @return array Array containing the meta box field definitions + */ + public static function adjust_video_meta_field_defs( $field_defs, $post_type ) { + $post = ( ! empty( $GLOBALS['post'] ) ) ? $GLOBALS['post'] : null; + + $field_defs['videositemap-disable']['expl'] = sprintf( $field_defs['videositemap-disable']['expl'], $post_type ); + + $video = []; + if ( isset( $post->ID ) ) { + $video = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + } + + if ( ( ! isset( $post->ID ) || WPSEO_Meta::get_value( 'videositemap-thumbnail', $post->ID ) === '' ) + && ( isset( $video['thumbnail_loc'] ) && $video['thumbnail_loc'] !== '' ) + ) { + $field_defs['videositemap-thumbnail']['description'] = sprintf( $field_defs['videositemap-thumbnail']['description'], '', '' ); + } + else { + $field_defs['videositemap-thumbnail']['description'] = ''; + } + + if ( isset( $video['duration'] ) ) { + $field_defs['videositemap-duration']['default_value'] = $video['duration']; + } + + return $field_defs; + } + + /** + * Sanitize the video thumbnail upload post meta + * + * @param mixed $clean Potentially pre-cleaned version of the new meta value. + * @param mixed $meta_value The new value. + * @param string $field_def The field definition for the current meta field. + * + * @return string Cleaned value + */ + public static function sanitize_thumbnail_upload( $clean, $meta_value, $field_def ) { + // Validate as url. + $clean = $field_def['default_value']; + + $url = WPSEO_Video_Wrappers::yoast_wpseo_video_sanitize_url( $meta_value ); + + if ( $url !== '' ) { + $clean = $url; + } + + return $clean; + } + + /** + * Sanitize the video duration post meta + * + * @param mixed $clean Potentially pre-cleaned version of the new meta value. + * @param mixed $meta_value The new value. + * @param string $field_def The field definition for the current meta field. + * + * @return string Cleaned value + */ + public static function sanitize_duration( $clean, $meta_value, $field_def ) { + $field_def = WPSEO_Meta::get_meta_field_defs( 'video' ); + $field_def = $field_def['videositemap-duration']; + $clean = $field_def['default_value']; + + $int = WPSEO_Video_Wrappers::yoast_wpseo_video_validate_int( $meta_value ); + + if ( $int !== false && $int > 0 ) { + $clean = strval( $int ); + } + + return $clean; + } + + /** + * Sanitize the video rating post meta + * + * @param mixed $clean Potentially pre-cleaned version of the new meta value. + * @param mixed $meta_value The new value. + * @param string $field_def The field definition for the current meta field. + * + * @return string Cleaned value + */ + public static function sanitize_rating( $clean, $meta_value, $field_def ) { + $clean = $field_def['default_value']; + if ( is_numeric( $meta_value ) && ( $meta_value >= 0 && $meta_value <= 5 ) ) { + $clean = $meta_value; + } + + return $clean; + } + + /** + * Sanitize the video meta post meta - set in function, not from user input so no extra validation done + * + * @param mixed $clean Potentially pre-cleaned version of the new meta value. + * @param mixed $meta_value The new value. + * + * @return string Cleaned value + */ + public static function sanitize_video_meta( $clean, $meta_value ) { + if ( is_array( $meta_value ) && $meta_value !== [] ) { + $clean = $meta_value; + } + + return $clean; + } + + /** + * Upgrade routine to deal with the fall-out of issue #102 + */ + public static function re_add_durations() { + global $wpdb; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Only runs on upgrade, only gets data. + $query = $wpdb->prepare( + "SELECT post_id, meta_value + FROM {$wpdb->postmeta} + WHERE meta_key = %s", + WPSEO_Meta::$meta_prefix . 'video_meta' + ); + $video_metas = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL -- Correctly prepared above. + + $query = $wpdb->prepare( + "SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key = %s", + WPSEO_Meta::$meta_prefix . 'videositemap-duration' + ); + $known_durations = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL -- Correctly prepared above. + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + + if ( is_array( $video_metas ) && $video_metas !== [] ) { + foreach ( $video_metas as $video ) { + if ( $known_durations === [] || ! in_array( $video->post_id, $known_durations, true ) ) { + $meta = maybe_unserialize( $video->meta_value ); + if ( isset( $meta['duration'] ) ) { + WPSEO_Meta::set_value( 'videositemap-duration', $meta['duration'], $video->post_id ); + } + unset( $meta ); + } + } + } + unset( $query, $video_metas, $known_durations, $video ); + } + } +} diff --git a/classes/class-wpseo-option-video.php b/classes/class-wpseo-option-video.php new file mode 100644 index 0000000..15eb851 --- /dev/null +++ b/classes/class-wpseo-option-video.php @@ -0,0 +1,187 @@ +get_defaults(); + * + * @var array + */ + protected $defaults = [ + // Non-form fields, set via validation routine / license activation method. + // Leave default as 0 to ensure activation/upgrade works. + 'video_dbversion' => 0, + + // Form fields. + 'video_cloak_sitemap' => false, + 'video_disable_rss' => false, + 'video_custom_fields' => '', + 'video_facebook_embed' => true, // N.B.: The name of this property is outdated, should be `allow_external_embeds`. + 'video_fitvids' => false, + 'video_content_width' => '', + 'video_wistia_domain' => '', + 'video_embedly_api_key' => '', + 'videositemap_posttypes' => [], + 'videositemap_taxonomies' => [], + 'video_youtube_faster_embed' => false, + ]; + + /** + * Get the singleton instance of this class + * + * @return self + */ + public static function get_instance() { + if ( ! ( self::$instance instanceof self ) ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Registers the option with the options framework. + */ + public static function register_option() { + WPSEO_Options::register_option( self::get_instance() ); + } + + /** + * Add dynamically created default option based on available post types + * + * @return void + */ + public function enrich_defaults() { + $this->defaults['videositemap_posttypes'] = get_post_types( [ 'public' => true ] ); + } + + /** + * Validate the option + * + * @param array $dirty New value for the option. + * @param array $clean Clean value for the option, normally the defaults. + * @param array $old Old value of the option. + * + * @return array Validated clean value for the option to be saved to the database + */ + protected function validate_option( $dirty, $clean, $old ) { + + foreach ( $clean as $key => $value ) { + switch ( $key ) { + case 'video_dbversion': + $clean[ $key ] = WPSEO_VIDEO_VERSION; + break; + + case 'videositemap_posttypes': + $clean[ $key ] = []; + $valid_post_types = get_post_types( [ 'public' => true ] ); + if ( isset( $dirty[ $key ] ) && ( is_array( $dirty[ $key ] ) && $dirty[ $key ] !== [] ) ) { + foreach ( $dirty[ $key ] as $k => $v ) { + if ( in_array( $k, $valid_post_types, true ) ) { + $clean[ $key ][ $k ] = $v; + } + elseif ( sanitize_title_with_dashes( $k ) === $k ) { + // Allow post types which may not be registered yet. + $clean[ $key ][ $k ] = $v; + } + } + } + break; + + case 'videositemap_taxonomies': + $clean[ $key ] = []; + $valid_taxonomies = get_taxonomies( [ 'public' => true ] ); + if ( isset( $dirty[ $key ] ) && ( is_array( $dirty[ $key ] ) && $dirty[ $key ] !== [] ) ) { + foreach ( $dirty[ $key ] as $k => $v ) { + if ( in_array( $k, $valid_taxonomies, true ) ) { + $clean[ $key ][ $k ] = $v; + } + elseif ( sanitize_title_with_dashes( $k ) === $k ) { + // Allow taxonomies which may not be registered yet. + $clean[ $key ][ $k ] = $v; + } + } + } + break; + + // Text field - may not be in form. + // @todo - validate custom fields against meta table? + case 'video_custom_fields': + if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { + $clean[ $key ] = sanitize_text_field( $dirty[ $key ] ); + } + break; + + // @todo - validate domains in some way? + case 'video_wistia_domain': + if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { + $clean[ $key ] = sanitize_text_field( urldecode( $dirty[ $key ] ) ); + $clean[ $key ] = preg_replace( [ '`^http[s]?://`', '`^//`', '`/$`' ], '', $clean[ $key ] ); + } + break; + + case 'video_embedly_api_key': + if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' && preg_match( '`^[a-f0-9]{32}$`', $dirty[ $key ] ) ) { + $clean[ $key ] = sanitize_text_field( $dirty[ $key ] ); + } + break; + + // Numeric text field - may not be in form. + case 'video_content_width': + if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { + $int = WPSEO_Video_Wrappers::yoast_wpseo_video_validate_int( $dirty[ $key ] ); + + if ( $int !== false && $int > 0 ) { + $clean[ $key ] = $int; + } + } + break; + + // Boolean (checkbox) field - may not be in form. + case 'video_cloak_sitemap': + case 'video_disable_rss': + case 'video_facebook_embed': + case 'video_fitvids': + case 'video_youtube_faster_embed': + $clean[ $key ] = false; + if ( isset( $dirty[ $key ] ) ) { + $clean[ $key ] = WPSEO_Video_Wrappers::validate_bool( $dirty[ $key ] ); + } + break; + } + } + + return $clean; + } + } +} diff --git a/classes/class-wpseo-video-admin-page.php b/classes/class-wpseo-video-admin-page.php new file mode 100644 index 0000000..76eb1d1 --- /dev/null +++ b/classes/class-wpseo-video-admin-page.php @@ -0,0 +1,164 @@ +is_video_page( $page ) ) { + add_action( 'wpseo_admin_footer', [ $this, 'reindex_videos_form' ] ); + } + + Yoast_Form::get_instance()->admin_header( true, 'wpseo_video' ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- only loads a page, doesn't do any actions by itself. + if ( isset( $_POST['reindex'] ) ) { + /* + * Load the reindex page, shows a progressbar and sents ajax calls to the server with + * small amounts of posts to reindex. + */ + require plugin_dir_path( WPSEO_VIDEO_FILE ) . 'views/reindex-page.php'; + } + else { + if ( WPSEO_Options::get( 'enable_xml_sitemap', false ) !== true ) { + printf( + '

%s

', + sprintf( + /* translators: 1: link open tag; 2: link close tag. */ + esc_html__( 'Please enable XML sitemaps in Yoast SEO > Settings > %1$sSite features%2$s.', 'yoast-video-seo' ), + '', + '' + ) + ); + } + else { + echo '

' . esc_html__( 'General Settings', 'yoast-video-seo' ) . '

'; + if ( $sitemap_url !== null ) { + echo '

' . esc_html__( 'Please find your video sitemap here:', 'yoast-video-seo' ) . ' ' . esc_html__( 'XML Video Sitemap', 'yoast-video-seo' ) . '

'; + } + else { + echo '

' . esc_html__( 'Select at least one post type to enable the video sitemap.', 'yoast-video-seo' ) . '

'; + } + + Yoast_Form::get_instance()->checkbox( 'video_cloak_sitemap', esc_html__( 'Hide the sitemap from normal visitors?', 'yoast-video-seo' ) ); + Yoast_Form::get_instance()->checkbox( 'video_disable_rss', esc_html__( 'Disable Media RSS Enhancement', 'yoast-video-seo' ) ); + echo '
'; + + Yoast_Form::get_instance()->textinput( 'video_custom_fields', esc_html__( 'Custom fields', 'yoast-video-seo' ) ); + echo '

' . esc_html__( 'Custom fields the plugin should check for video content (comma separated)', 'yoast-video-seo' ) . '

'; + Yoast_Form::get_instance()->textinput( 'video_embedly_api_key', esc_html__( '(Optional) Embedly API Key', 'yoast-video-seo' ) ); + /* translators: 1,3: link open tag; 2: link close tag. */ + echo '

' . sprintf( esc_html__( 'The video SEO plugin provides where possible enriched information about your videos. A lot of %1$svideo services%2$s are supported by default. For those services which aren\'t supported, we can try to retrieve enriched video information using %3$sEmbedly%2$s. If you want to use this option, you\'ll need to sign up for a (free) %3$sEmbedly%2$s account and provide the API key you receive.', 'yoast-video-seo' ), '', '', '' ) . '

'; + + + echo '

' . esc_html__( 'Embed Settings', 'yoast-video-seo' ) . '

'; + + Yoast_Form::get_instance()->checkbox( 'video_facebook_embed', esc_html__( 'Allow videos to be played directly on other websites, such as Facebook or Twitter?', 'yoast-video-seo' ) ); + /* translators: 1: link open tag, 2: link close tag. */ + Yoast_Form::get_instance()->checkbox( 'video_fitvids', sprintf( esc_html__( 'Try to make videos responsive using %1$sFitVids.js%2$s?', 'yoast-video-seo' ), '
', '' ) ); + + Yoast_Form::get_instance()->checkbox( 'video_youtube_faster_embed', esc_html__( 'YouTube embeds: make pages load faster by only loading the YouTube player when the user clicks play.', 'yoast-video-seo' ) ); + echo '
'; + + Yoast_Form::get_instance()->textinput( 'video_content_width', esc_html__( 'Content width', 'yoast-video-seo' ) ); + echo '

' . esc_html__( 'This defaults to your themes content width, but if it\'s empty, setting a value here will make sure videos are embedded in the right width.', 'yoast-video-seo' ) . '

'; + + Yoast_Form::get_instance()->textinput( 'video_wistia_domain', esc_html__( 'Wistia domain', 'yoast-video-seo' ) ); + echo '

' . esc_html__( 'If you use Wistia in combination with a custom domain, set this to the domain name you use for your Wistia videos, no http: or slashes needed.', 'yoast-video-seo' ) . '

'; + + + echo '

' . esc_html__( 'Post Types for which to enable the Video SEO plugin', 'yoast-video-seo' ) . '

'; + echo '

' . esc_html__( 'Determine which post types on your site might contain video.', 'yoast-video-seo' ) . '

'; + + + $post_types = get_post_types( [ 'public' => true ], 'objects' ); + $post_types_list = []; + foreach ( $post_types as $post_type ) { + $post_types_list[ $post_type->name ] = $post_type->labels->name; + } + + Yoast_Form::get_instance()->checkbox_list( 'videositemap_posttypes', $post_types_list ); + + echo '

' . esc_html__( 'Taxonomies to include in XML Video Sitemap', 'yoast-video-seo' ) . '

'; + echo '

' . esc_html__( 'You can also include your taxonomy archives, for instance, if you have videos on a category page.', 'yoast-video-seo' ) . '

'; + + $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' ); + $taxonomies_list = []; + foreach ( $taxonomies as $taxonomy ) { + $taxonomies_list[ $taxonomy->name ] = $taxonomy->labels->name; + } + + Yoast_Form::get_instance()->checkbox_list( 'videositemap_taxonomies', $taxonomies_list ); + } + } + // Add debug info. + Yoast_Form::get_instance()->admin_footer( true, false ); + } + + /** + * Adds the reindexing form for videos. + */ + public function reindex_videos_form() { + ?> +

+ +

+ +

+ +
+ + +
+

+ +

+
+ can_activate(); + if ( ! $can_activate ) { + $this->add_admin_notices_hook(); + + return; + } + + $this->add_integration_hooks(); + + // If called via WP-CLI, register additional commands. + if ( defined( 'WP_CLI' ) && WP_CLI ) { + add_action( 'wp_loaded', [ $this, 'register_cli_commands' ] ); + } + } + + /** + * Shows any queued admin notices. + * + * @return void + */ + public function show_admin_notices() { + if ( empty( $this->admin_notices ) ) { + return; + } + + if ( $this->is_iframe_request() ) { + return; + } + + foreach ( $this->admin_notices as $admin_notice ) { + $this->display_admin_notice( $admin_notice ); + } + } + + /** + * Adds hooks to load the video integrations. + * + * @return void + */ + protected function add_integration_hooks() { + add_action( 'plugins_loaded', [ $this, 'load_metabox_integration' ], 10 ); + add_action( 'plugins_loaded', [ $this, 'load_sitemap_integration' ], 20 ); + add_action( 'plugins_loaded', [ $this, 'load_schema_integration' ], 20 ); + add_action( 'plugins_loaded', [ $this, 'load_embed_optimization' ], 20 ); + // Add opengraph presenter. + add_filter( 'wpseo_frontend_presenters', [ $this, 'add_frontend_presenters' ], 10, 2 ); + + $editor_reactification_alert = new WPSEO_Video_Editor_Reactification_Alert(); + $editor_reactification_alert->register_hooks(); + } + + /** + * Adds presenters for presenting the opengraph metatags for the video metadata. + * + * @param Abstract_Indexable_Presenter[] $presenters The presenter instances. + * @param Meta_Tags_Context $context The meta tags context. + * + * @return Abstract_Indexable_Presenter[] The extended presenters. + */ + public function add_frontend_presenters( $presenters, $context ) { + if ( ! is_array( $presenters ) ) { + return $presenters; + } + + // Bail out when opengraph video tags are switched off. + if ( WPSEO_Options::get( 'video_facebook_embed' ) !== true ) { + return $presenters; + } + + // Retrieve the video metadata. + $video = $this->get_video( $context ); + + // Bail out if the video metadata is not there or malformed. + if ( ! isset( $video['player_loc'] ) || ! is_array( $video ) ) { + return $presenters; + } + + // Always output location (URL) and type of the video. + $presenters[] = new WPSEO_Video_Location_Presenter( $video ); + $presenters[] = new WPSEO_Video_Type_Presenter( $video ); + $presenters[] = new WPSEO_Video_Duration_Presenter( $video ); + $presenters[] = new WPSEO_Video_Width_Presenter( $video ); + $presenters[] = new WPSEO_Video_Height_Presenter( $video ); + + /** This filter is documented in classes/class-wpseo-video-sitemap.php */ + $yandex_support_enabled = apply_filters( 'wpseo_video_yandex_support', true ); + + // Add Yandex-supported metatags, if enabled. They are enabled by default. + if ( $yandex_support_enabled ) { + $presenters[] = new WPSEO_Video_Yandex_Adult_Presenter( $video ); + $presenters[] = new WPSEO_Video_Yandex_Upload_Date_Presenter( $video ); + $presenters[] = new WPSEO_Video_Yandex_Allow_Embed_Presenter( $video ); + } + + return $presenters; + } + + /** + * Retrieves the video metadata of the given post or term. + * + * @param Meta_Tags_Context|null $context The meta tags context. + * + * @return array|false The video metadata or `false` if no metadata are available. + */ + protected function get_video( $context = null ) { + // This is not executed on a REST API call. + if ( is_a( $context, Meta_Tags_Context::class ) ) { + // Check if we're on a singular post page. + if ( $context->indexable->object_type === 'post' ) { + $the_post = get_post( $context->indexable->object_id ); + + return WPSEO_Video_Utils::get_video_for_post( $the_post ); + } + // Check if we're on a singular term page. + // Note that this code won't work until https://yoast.atlassian.net/browse/QAK-2443 is fixed. + if ( $context->indexable->object_type === 'term' ) { + $the_term = get_term( $context->indexable->object_id ); + + return WPSEO_Video_Utils::get_video_for_term( $the_term ); + } + } + + // Fallback for posts in REST API calls. + global $post; + if ( ! empty( $post ) ) { + return WPSEO_Video_Utils::get_video_for_post( $post ); + } + + // Known issue: video meta tags for terms are not working: https://yoast.atlassian.net/browse/QAK-2443. + // To do: once QAK-2443 is fixed, implement fallback for terms for REST API calls. + return false; + } + + /** + * Loads the metabox integration. + * + * @return void + */ + public function load_metabox_integration() { + WPSEO_Meta_Video::init(); + } + + /** + * Loads the sitemap integration. + * + * @return void + */ + public function load_sitemap_integration() { + $GLOBALS['wpseo_video_xml'] = new WPSEO_Video_Sitemap(); + } + + /** + * Loads the Schema integration. + * + * @return void + */ + public function load_schema_integration() { + $GLOBALS['wpseo_video_schema'] = new WPSEO_Video_Schema(); + } + + /** + * Loads the Embed optimization. + * + * @return void + */ + public function load_embed_optimization() { + if ( WPSEO_Options::get( 'video_youtube_faster_embed' ) !== true ) { + return; + } + + $GLOBALS['wpseo_video_embed'] = new WPSEO_Video_Embed(); + } + + /** + * Checks if the plugin can be activated. + * + * @return bool True if the plugin has the environment to work in. + */ + protected function can_activate() { + if ( ! $this->is_spl_autoload_available() ) { + $this->add_admin_notice( + esc_html__( + 'The PHP SPL extension seems to be unavailable. Please ask your web host to enable it.', + 'yoast-video-seo' + ), + true + ); + } + + if ( ! $this->is_wordpress_up_to_date() ) { + $this->add_admin_notice( + esc_html__( + 'Please upgrade WordPress to the latest version to allow WordPress and the Video SEO module to work properly.', + 'yoast-video-seo' + ) + ); + } + + if ( ! $this->is_yoast_seo_active() ) { + $this->add_admin_notice( $this->get_wpseo_missing_error() ); + } + + // Allow beta version. + if ( $this->is_yoast_seo_active() && ! $this->is_yoast_seo_up_to_date() ) { + $this->add_admin_notice( + sprintf( + /* translators: $1$s expands to Yoast SEO. */ + esc_html__( + 'Please upgrade the %1$s plugin to the latest version to allow the Video SEO module to work.', + 'yoast-video-seo' + ), + 'Yoast SEO' + ) + ); + } + + return empty( $this->admin_notices ); + } + + /** + * Retrieves the message to show to make sure Yoast SEO gets activated. + * + * @return string The message to present to the user. + */ + protected function get_wpseo_missing_error() { + if ( ! $this->user_can_activate_plugins() ) { + return $this->get_install_by_admin_message(); + } + + return $this->get_install_plugin_message(); + } + + /** + * Queues an admin notification. + * + * @param string $message Message to be shown. + * @param bool $use_prefix Optional. Use the default prefix or not. + * + * @return void + */ + protected function add_admin_notice( $message, $use_prefix = false ) { + $prefix = ''; + + if ( $use_prefix ) { + $prefix = esc_html( $this->get_admin_notice_prefix() ) . ' '; + } + + $this->admin_notices[] = $prefix . $message; + } + + /** + * Registers the admin notices hooks to display messages. + * + * @return void + */ + protected function add_admin_notices_hook() { + $hook = 'admin_notices'; + if ( $this->use_multisite_notifications() ) { + $hook = 'network_' . $hook; + } + + add_action( $hook, [ $this, 'show_admin_notices' ] ); + } + + /** + * Displays an admin notice. + * + * @param string $admin_notice Notice to display, pre-escaped. + * + * @return void + */ + protected function display_admin_notice( $admin_notice ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Notices are passed in with variables already escaped. + echo '

' . $admin_notice . '

'; + } + + /** + * Checks if the user can install or activate plugins. + * + * @return bool True if the user can activate plugins. + */ + protected function user_can_activate_plugins() { + return current_user_can( 'install_plugins' ) || current_user_can( 'activate_plugins' ); + } + + /** + * Checks if the current request is an iFrame request. + * + * @return bool True if this request is an iFrame request. + */ + protected function is_iframe_request() { + return defined( 'IFRAME_REQUEST' ) && IFRAME_REQUEST !== false; + } + + /** + * Checks whether we should use multisite notifications or not. + * + * @return bool True if we want to use multisite notifications. + */ + protected function use_multisite_notifications() { + return is_multisite() && is_network_admin(); + } + + /** + * Retrieves the plugin page URL to use. + * + * @return string Plugin page URL to use. + */ + protected function get_plugin_page_url() { + $page_slug = 'plugin-install.php'; + + if ( $this->use_multisite_plugin_page() ) { + return network_admin_url( $page_slug ); + } + + return admin_url( $page_slug ); + } + + /** + * Checks if we should use the multisite plugin page. + * + * @return bool True if we are on multisite and super admin. + */ + protected function use_multisite_plugin_page() { + return is_multisite() === true && is_super_admin(); + } + + /** + * Checks if SPL Autoload is available. + * + * @return bool True if SPL Autoload is available. + */ + protected function is_spl_autoload_available() { + return function_exists( 'spl_autoload_register' ); + } + + /** + * Checks if WordPress is at a mimimal required version. + * + * @return bool True if WordPress is at a minimal required version. + */ + protected function is_wordpress_up_to_date() { + return version_compare( $GLOBALS['wp_version'], '6.1', '>=' ); + } + + /** + * Checks if Yoast SEO is active. + * + * @return bool True if Yoast SEO is active. + */ + public function is_yoast_seo_active() { + return defined( 'WPSEO_VERSION' ); + } + + /** + * Checks if Yoast SEO is at a minimum required version. + * + * @return bool True if Yoast SEO is at a minimal required version. + */ + protected function is_yoast_seo_up_to_date() { + return $this->is_yoast_seo_active() && version_compare( WPSEO_VERSION, '20.13-RC0', '>=' ); + } + + /** + * Returns the admin notice prefix string. + * + * @return string The string to prefix the admin notice with. + */ + protected function get_admin_notice_prefix() { + return __( 'Activation of Video SEO failed:', 'yoast-video-seo' ); + } + + /** + * Generates the message to display to install Yoast SEO by proxy. + * + * @return string The message to show to inform the user to install Yoast SEO. + */ + protected function get_install_by_admin_message() { + return sprintf( + /* translators: %1$s expands to Yoast SEO. */ + esc_html__( + 'Please ask the (network) admin to install & activate %1$s and then enable its XML sitemap functionality to allow the Video SEO module to work.', + 'yoast-video-seo' + ), + 'Yoast SEO' + ); + } + + /** + * Generates the message to display to install Yoast SEO. + * + * @return string The message to show to inform the user to install Yoast SEO. + */ + protected function get_install_plugin_message() { + $url = add_query_arg( + [ + 'tab' => 'search', + 'type' => 'term', + 's' => 'wordpress+seo', + 'plugin-search-input' => 'Search+Plugins', + ], + $this->get_plugin_page_url() + ); + + return sprintf( + /* translators: %1$s and %3$s expand to anchor tags with a link to the download page for Yoast SEO . %2$s expands to Yoast SEO. */ + esc_html__( + 'Please %1$sinstall & activate %2$s%3$s and then enable its XML sitemap functionality to allow the Video SEO module to work.', + 'yoast-video-seo' + ), + '', + 'Yoast SEO', + '' + ); + } + + /** + * Loads the CLI commands. + * + * @return void + */ + public function register_cli_commands() { + if ( defined( 'WP_CLI' ) && WP_CLI ) { + $sitemap = new WPSEO_Video_Sitemap(); + $post_indexation_action = new WPSEO_Video_Post_Indexation_Action( $sitemap ); + $term_indexation_action = new WPSEO_Video_Term_Indexation_Action( $sitemap ); + $index_command = new WPSEO_Video_Index_Command( $post_indexation_action, $term_indexation_action ); + WP_CLI::add_command( WPSEO_Video_Index_Command::get_namespace(), $index_command ); + } + } +} diff --git a/classes/class-wpseo-video-embed.php b/classes/class-wpseo-video-embed.php new file mode 100644 index 0000000..206cc84 --- /dev/null +++ b/classes/class-wpseo-video-embed.php @@ -0,0 +1,166 @@ + + document.addEventListener( "DOMContentLoaded", function() { + var div, i, + youtubePlayers = document.getElementsByClassName( "video-seo-youtube-player" ); + for ( i = 0; i < youtubePlayers.length; i++ ) { + div = document.createElement( "div" ); + div.className = "video-seo-youtube-embed-loader"; + div.setAttribute( "data-id", youtubePlayers[ i ].dataset.id ); + div.setAttribute( "tabindex", "0" ); + div.setAttribute( "role", "button" ); + div.setAttribute( "aria-label", "' . esc_attr__( 'Load YouTube video', 'yoast-video-seo' ) . '" ); + div.innerHTML = videoSEOGenerateYouTubeThumbnail( youtubePlayers[ i ].dataset.id ); + div.addEventListener( "click", videoSEOGenerateYouTubeIframe ); + div.addEventListener( "keydown", videoSEOYouTubeThumbnailHandleKeydown ); + div.addEventListener( "keyup", videoSEOYouTubeThumbnailHandleKeyup ); + youtubePlayers[ i ].appendChild( div ); + } + } ); + + function videoSEOGenerateYouTubeThumbnail( id ) { + var thumbnail = \'\n\' + + \'\n\' + + \'\n\' + + \'\n\' + + \'\n\', + play = \'
\'; + return thumbnail.replace( "ID", id ) + play; + } + + function videoSEOMaybeReplaceMaxResSourceWithHqSource( event ) { + var sourceMaxRes, + sourceHighQuality, + loadedThumbnail = event.target, + parent = loadedThumbnail.parentNode; + + if ( loadedThumbnail.naturalWidth < 150 ) { + sourceMaxRes = parent.querySelector(".video-seo-source-to-maybe-replace"); + sourceHighQuality = parent.querySelector(".video-seo-source-hq"); + sourceMaxRes.srcset = sourceHighQuality.srcset; + parent.className = "video-seo-youtube-picture video-seo-youtube-picture-replaced-srcset"; + } + } + + function videoSEOYouTubeThumbnailHandleKeydown( event ) { + if ( event.keyCode !== 13 && event.keyCode !== 32 ) { + return; + } + + if ( event.keyCode === 13 ) { + videoSEOGenerateYouTubeIframe( event ); + } + + if ( event.keyCode === 32 ) { + event.preventDefault(); + } + } + + function videoSEOYouTubeThumbnailHandleKeyup( event ) { + if ( event.keyCode !== 32 ) { + return; + } + + videoSEOGenerateYouTubeIframe( event ); + } + + function videoSEOGenerateYouTubeIframe( event ) { + var el = ( event.type === "click" ) ? this : event.target, + iframe = document.createElement( "iframe" ); + + iframe.setAttribute( "src", "https://www.youtube.com/embed/" + el.dataset.id + "?autoplay=1&enablejsapi=1&origin=' . rawurlencode( home_url() ) . '" ); + iframe.setAttribute( "frameborder", "0" ); + iframe.setAttribute( "allowfullscreen", "1" ); + iframe.setAttribute( "allow", "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" ); + el.parentNode.replaceChild( iframe, el ); + } + '; + } + + /** + * Replaces the YouTube block HTML with our custom HTML. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * + * @return string The filtered block content. + */ + public function replace_youtube_block_html( $block_content, $block ) { + /* + * In WordPress 5.6 the YouTube block name changed from `core-embed/youtube` + * to `core/embed` as the embed block was refactored to use only one block + * type with variations. The old name `core-embed/youtube` is used here + * for backwards compatibility. + */ + if ( + ( $block['blockName'] === 'core/embed' || $block['blockName'] === 'core-embed/youtube' ) + && $block['attrs']['providerNameSlug'] === 'youtube' + ) { + wp_enqueue_style( + 'videoseo-youtube-embed-css', + plugins_url( 'css/dist/videoseo-youtube-embed.css', WPSEO_VIDEO_FILE ), + [], + WPSEO_VIDEO_VERSION + ); + + if ( strpos( $block['attrs']['url'], 'youtu.be' ) ) { + $data_id = str_replace( 'https://youtu.be/', '', $block['attrs']['url'] ); + } + else { + $data_id = str_replace( 'https://www.youtube.com/watch?v=', '', $block['attrs']['url'] ); + } + + $class = ''; + if ( isset( $block['attrs']['align'] ) ) { + $class = 'align' . $block['attrs']['align'] . ' '; + } + + if ( isset( $block['attrs']['className'] ) ) { + $class .= $block['attrs']['className'] . ' '; + } + + $block_content = '
'; + $block_content .= '
'; + $block_content .= '
'; + $block_content .= '
'; + + return $block_content; + } + + return $block_content; + } +} diff --git a/classes/class-wpseo-video-metabox.php b/classes/class-wpseo-video-metabox.php new file mode 100644 index 0000000..6b1eab2 --- /dev/null +++ b/classes/class-wpseo-video-metabox.php @@ -0,0 +1,276 @@ +ID ) ) { + $video = WPSEO_Meta::get_value( 'video_meta', $GLOBALS['post']->ID ); + if ( is_array( $video ) && $video !== [] ) { + return true; + } + } + + return false; + } + + /** + * Adds a video section to the metabox sections array. + * + * @param array $sections The sections to add. + * + * @return array + */ + public function add_metabox_section( $sections ) { + if ( ! $this->should_show_metabox() ) { + return $sections; + } + + $content = '
'; + + $sections[] = [ + 'name' => 'video', + 'link_content' => '' . esc_html__( 'Video', 'yoast-video-seo' ), + 'content' => '
' . $content . '
', + 'options' => [ 'content_class' => '' ], + ]; + + return $sections; + } + + /** + * Appends the Video editor hidden fields to the content, if applicable. + * + * @param string $content The content to add hidden fields to. + * + * @return string The content. + */ + public function add_hidden_fields( $content ) { + if ( ! $this->should_show_metabox() ) { + return $content; + } + + return $content . new Meta_Fields_Presenter( $GLOBALS['post'], 'video' ); + } + + /** + * Filter over the meta boxes to save, this function adds the Video meta box fields. + * + * @param array $field_defs Array of metaboxes to save. + * + * @return array + */ + public function save_meta_boxes( $field_defs ) { + return array_merge( $field_defs, WPSEO_Meta::get_meta_field_defs( 'video' ) ); + } + + /** + * Form field generator for number fields in WPSEO metabox + * + * @param string $content The current content of the metabox. + * @param mixed $meta_value The meta value to use for the form field. + * @param string $esc_form_key The pre-escaped key for the form field. + * @param array $options Contains the min and max value of the number field, if relevant. + * + * @return string + */ + public function do_number_field( $content, $meta_value, $esc_form_key, $options = [] ) { + $options = $options['options']; + $minvalue = ''; + $maxvalue = ''; + $step = ''; + + if ( isset( $options['min_value'] ) ) { + $minvalue = ' min="' . $options['min_value'] . '" '; + } + + if ( isset( $options['max_value'] ) ) { + $maxvalue = ' max="' . $options['max_value'] . '" '; + } + + if ( isset( $options['step'] ) ) { + $step = ' step="' . $options['step'] . '" '; + } + + $content .= '
'; + + return $content; + } + + /** + * Flattens a version number for use in a filename + * + * @param string $version The original version number. + * + * @return string The flattened version number. + */ + public function flatten_version( $version ) { + $parts = explode( '.', $version ); + if ( count( $parts ) === 2 && preg_match( '/^\d+$/', $parts[1] ) === 1 ) { + $parts[] = '0'; + } + return implode( '', $parts ); + } + + /** + * Enqueues the plugin scripts. + */ + public function enqueue_scripts() { + if ( ! $this->should_show_metabox() ) { + return; + } + + $dependencies = [ + 'wp-components', + 'wp-compose', + 'wp-data', + 'wp-element', + 'wp-hooks', + 'wp-i18n', + 'yoast-seo-api', + 'yoast-seo-editor-modules', + 'yoast-seo-yoast-components', + ]; + + wp_enqueue_script( 'wp-seo-video-seo', plugins_url( 'js/yoast-video-seo-plugin-' . $this->flatten_version( WPSEO_VIDEO_VERSION ) . WPSEO_CSSJS_SUFFIX . '.js', WPSEO_VIDEO_FILE ), $dependencies, WPSEO_VERSION, true ); + + wp_localize_script( 'wp-seo-video-seo', 'wpseoVideoL10n', $this->localize_video_script() ); + } + + /** + * Check if the post type the user is currently editing is shown in the sitemaps. If so, the video metabox should be shown. + * + * @return bool + */ + private function should_show_metabox() { + return WPSEO_Video_Utils::is_videoseo_active_for_posttype( get_post_type() ); + } + + /** + * Localizes scripts for the videoplugin. + * + * @return array + */ + private function localize_video_script() { + $action = YoastSEO()->classes->get( \Yoast\WP\SEO\Actions\Alert_Dismissal_Action::class ); + $video_reactification_alert = new WPSEO_Video_Editor_Reactification_Alert(); + $is_dismissed = $action->is_dismissed( $video_reactification_alert->alert_identifier ); + $video_meta = WPSEO_Meta::get_value( 'video_meta', $GLOBALS['post']->ID ); + + return [ + 'has_video' => $this->has_video(), + 'react_alert_is_dismissed' => $is_dismissed, + 'script_url' => plugins_url( 'js/yoast-video-seo-worker-' . $this->flatten_version( WPSEO_VIDEO_VERSION ) . WPSEO_CSSJS_SUFFIX . '.js', WPSEO_VIDEO_FILE ), + 'video' => __( 'video', 'yoast-video-seo' ), + 'video_title_ok' => __( 'You should consider adding the word "video" in your title, to optimize your ability to be found by people searching for video.', 'yoast-video-seo' ), + 'video_title_good' => __( 'You\'re using the word "video" in your title, this optimizes your ability to be found by people searching for video.', 'yoast-video-seo' ), + 'video_body_short' => __( 'Your body copy is too short for Search Engines to understand the topic of your video, add some more content describing the contents of the video.', 'yoast-video-seo' ), + 'video_body_good' => __( 'Your body copy is at optimal length for your video to be recognized by Search Engines.', 'yoast-video-seo' ), + /* translators: 1: links to https://yoast.com/video-not-showing-search-results, 2: closing link tag */ + 'video_body_long' => __( 'Your body copy is quite long, make sure that the video is the most important asset on the page, read %1$sthis post%2$s for more info.', 'yoast-video-seo' ), + 'video_body_long_url' => '', + 'yoast-video-seo' => $this->get_translations( 'yoast-video-seojs' ), + 'shortlinks.configuration_guide' => WPSEO_Shortlinker::get( 'https://yoa.st/video-config-guide' ), + 'shortlinks.video_changes' => WPSEO_Shortlinker::get( 'https://yoa.st/video-changes' ), + 'default_thumbnail' => ( isset( $video_meta['thumbnail_loc'] ) ) ? $video_meta['thumbnail_loc'] : '', + ]; + } + + /** + * Returns translations necessary for JS files. + * + * @param string $component The component to retrieve the translations for. + * + * @return object|null The translations in a Jed format for JS files or null + * if the translation file could not be found. + */ + protected function get_translations( $component ) { + $locale = \get_user_locale(); + + $file = plugin_dir_path( WPSEO_VIDEO_FILE ) . 'languages/' . $component . '-' . $locale . '.json'; + if ( file_exists( $file ) ) { + $file = file_get_contents( $file ); + if ( is_string( $file ) && $file !== '' ) { + return json_decode( $file, true ); + } + } + + return null; + } + } +} diff --git a/classes/class-wpseo-video-schema-videoobject.php b/classes/class-wpseo-video-schema-videoobject.php new file mode 100644 index 0000000..9fccf3e --- /dev/null +++ b/classes/class-wpseo-video-schema-videoobject.php @@ -0,0 +1,201 @@ +context = $context; + } + + /** + * Determines whether we need to run our VideoObject piece. + * + * @return bool True if it should be run, false if not. + */ + public function is_needed() { + if ( ! is_singular() ) { + return false; + } + + if ( WPSEO_Video_Utils::is_videoseo_active_for_posttype( get_post_type() ) === false ) { + return false; + } + + $this->get_video_data(); + if ( ! $this->should_have_video_schema() ) { + return false; + } + + return true; + } + + /** + * Generates the Schema data for a video. + * + * @link https://schema.org/VideoObject + * @link https://developers.google.com/search/docs/data-types/video + * + * @return array VideoObject Schema data for video. + */ + public function generate() { + $post = get_post( $this->context->id ); + + $this->data = [ + '@type' => 'VideoObject', + '@id' => $this->context->canonical . WPSEO_Video_Schema::VIDEO_HASH, + 'name' => $this->context->title, + 'isPartOf' => [ '@id' => $this->context->main_schema_id ], + 'thumbnailUrl' => $this->video['thumbnail_loc'], + 'description' => $this->context->description, + 'uploadDate' => gmdate( 'Y-m-d', strtotime( $post->post_date ) ), + ]; + + if ( $this->context->has_article ) { + $this->data['isPartOf'] = [ '@id' => $this->context->main_schema_id . Schema_IDs::ARTICLE_HASH ]; + } + + $this->check_description( $post ); + $this->add_video_size(); + $this->add_video_urls(); + $this->add_duration(); + $this->add_video_family_friendly(); + + $this->data = YoastSEO()->helpers->schema->language->add_piece_language( $this->data ); + + return $this->data; + } + + /** + * Checks if the post should have video schema. + * + * @return bool True if video schema should be output for the post, false if not. + */ + private function should_have_video_schema() { + if ( ! is_array( $this->video ) || $this->video === [] ) { + return false; + } + + $disable = WPSEO_Meta::get_value( 'videositemap-disable', $this->context->id ); + if ( $disable === 'on' ) { + return false; + } + + return true; + } + + /** + * Grabs the video data from meta data. + * + * @return void + */ + private function get_video_data() { + $this->video = WPSEO_Meta::get_value( 'video_meta', $this->context->id ); + // We add on video details to the same array. Ugly. + $this->video = WPSEO_Video_Utils::get_video_image( $this->context->id, $this->video ); + } + + /** + * Adds the video size. + * + * @return void + */ + private function add_video_size() { + if ( ! empty( $this->video['width'] ) && ! empty( $this->video['height'] ) ) { + $this->data['width'] = $this->video['width']; + $this->data['height'] = $this->video['height']; + } + } + + /** + * Adds the video's URLs. + * + * @return void + */ + private function add_video_urls() { + if ( isset( $this->video['player_loc'] ) ) { + $this->data['embedUrl'] = $this->video['player_loc']; + } + if ( isset( $this->video['content_loc'] ) ) { + $this->data['contentUrl'] = $this->video['content_loc']; + } + } + + /** + * Adds the video's duration. + * + * @return void + */ + private function add_duration() { + $video_duration = WPSEO_Video_Utils::get_video_duration( $this->video, $this->context->id ); + if ( $video_duration !== 0 ) { + $this->data['duration'] = WPSEO_Video_Utils::iso_8601_duration( $video_duration ); + } + } + + /** + * Adds the family friendly attribute. + * + * @return void + */ + private function add_video_family_friendly() { + $this->data['isFamilyFriendly'] = (bool) WPSEO_Video_Utils::is_video_family_friendly( $this->context->id ); + } + + /** + * Checks whether the description is empty and if so fixes that. + * + * @param WP_Post $post The post object. + */ + private function check_description( WP_Post $post ) { + if ( empty( $this->data['description'] ) ) { + $content = trim( wp_html_excerpt( strip_shortcodes( $post->post_content ), 300 ) ); + if ( ! empty( $content ) ) { + $this->data['description'] = $content; + return; + } + $this->data['description'] = __( 'No description', 'yoast-video-seo' ); + } + } +} diff --git a/classes/class-wpseo-video-schema.php b/classes/class-wpseo-video-schema.php new file mode 100644 index 0000000..60b8ae5 --- /dev/null +++ b/classes/class-wpseo-video-schema.php @@ -0,0 +1,110 @@ +object = new WPSEO_Video_Schema_VideoObject( $context ); + $pieces[] = $this->object; + + return $pieces; + } + + /** + * Changes Article Schema output. + * + * @param array $data Article Schema data. + * @param \Yoast\WP\Free\Context\Meta_Tags_Context $context The meta tags context. + * + * @return array Article Schema data. + */ + public function filter_article( $data, $context ) { + if ( $this->object->is_needed() ) { + $data['video'] = [ $this->get_video_id( $context->canonical ) ]; + } + + return $data; + } + + /** + * Changes WebPage Schema output. + * + * @param array $data WebPage Schema data. + * @param \Yoast\WP\Free\Context\Meta_Tags_Context $context The meta tags context. + * + * @return array WebPage Schema data. + */ + public function filter_webpage( $data, $context ) { + if ( ! is_singular() ) { + return $data; + } + + /** + * Filter: 'wpseo_schema_article_post_types' - Allow changing for which post types we output Article schema. + * + * @api string[] $post_types The post types for which we output Article. + */ + $post_types = apply_filters( 'wpseo_schema_article_post_types', [ 'post' ] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using a YoastSEO Free hook. + + if ( in_array( get_post_type(), $post_types, true ) ) { + return $data; + } + + if ( $this->object->is_needed() ) { + $data['video'] = [ $this->get_video_id( $context->canonical ) ]; + } + + return $data; + } + + /** + * Returns an array with the video identifier. + * + * @param string $canonical The canonical to current page. + * + * @return string[] Array with the video identifier. + */ + private function get_video_id( $canonical ) { + return [ '@id' => $canonical . self::VIDEO_HASH ]; + } +} diff --git a/classes/class-wpseo-video-sitemap.php b/classes/class-wpseo-video-sitemap.php new file mode 100644 index 0000000..36c1ae4 --- /dev/null +++ b/classes/class-wpseo-video-sitemap.php @@ -0,0 +1,2070 @@ +upgrade(); + + add_filter( 'wpseo_tax_meta_special_term_id_validation__video', [ $this, 'validate_video_tax_meta' ] ); + + // Set content_width based on theme content_width or our option value if either is available. + $content_width = $this->get_content_width(); + if ( $content_width !== false ) { + $GLOBALS['content_width'] = $content_width; + } + unset( $content_width ); + + add_action( 'setup_theme', [ $this, 'init' ] ); + add_action( 'admin_init', [ $this, 'init' ] ); + add_action( 'init', [ $this, 'register_sitemap' ], 20 ); // Register sitemap after cpts have been added. + add_action( 'admin_bar_menu', [ $this, 'add_admin_bar_item' ], 97 ); + add_filter( 'oembed_providers', [ $this, 'sync_oembed_providers' ] ); + + if ( is_admin() ) { + + add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_pages' ] ); + + // Check if we are in our Elementor AJAX request. + $post_action = false; + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. + if ( isset( $_POST['action'] ) && is_string( $_POST['action'] ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. + $post_action = sanitize_text_field( wp_unslash( $_POST['action'] ) ); + } + $doing_ajax = \wp_doing_ajax(); + $is_elementor_ajax_save = $doing_ajax && $post_action === 'elementor_ajax'; + $is_our_elementor_ajax_save = $doing_ajax && $post_action === 'wpseo_elementor_save'; + + // Update video post meta in Elementor save, after our WordPress SEO save. + if ( $is_our_elementor_ajax_save ) { + \add_action( 'wpseo_saved_postdata', [ $this, 'update_video_post_meta' ], 10 ); + \add_action( 'wpseo_saved_postdata', [ $this, 'invalidate_sitemap' ], 12 ); + } + // Update video meta on normal save. But prevent updates in Elementor's own save request, as we have our own. + elseif ( ! $is_elementor_ajax_save ) { + \add_action( 'wp_insert_post', [ $this, 'update_video_post_meta' ], 12, 3 ); + \add_action( 'wp_insert_post', [ $this, 'invalidate_sitemap' ], 13 ); + } + + $valid_pages = [ + 'edit.php', + 'post.php', + 'post-new.php', + ]; + if ( in_array( $GLOBALS['pagenow'], $valid_pages, true ) + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using a YoastSEO Free hook. + || apply_filters( 'wpseo_always_register_metaboxes_on_admin', false ) + || $doing_ajax + ) { + $this->metabox_tab = new WPSEO_Video_Metabox(); + $this->metabox_tab->register_hooks(); + } + + add_action( 'admin_enqueue_scripts', [ $this, 'admin_video_enqueue_scripts' ] ); + + add_action( 'admin_init', [ $this, 'admin_video_enqueue_styles' ] ); + + add_action( 'wp_ajax_index_posts', [ $this, 'index_posts_callback' ] ); + + // Maybe show 'Recommend re-index' admin notice. + if ( get_transient( 'video_seo_recommend_reindex' ) === '1' ) { + add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts_ignore' ] ); + add_action( 'all_admin_notices', [ $this, 'recommend_force_index' ] ); + add_action( 'wp_ajax_videoseo_set_ignore', [ $this, 'set_ignore' ] ); + } + } + else { + + // OpenGraph. + add_action( 'wpseo_add_opengraph_additional_images', [ $this, 'opengraph_image' ], 15, 1 ); + add_filter( 'wpseo_html_namespaces', [ $this, 'add_video_namespaces' ] ); + + // XML Sitemap Index addition. + add_filter( 'wpseo_sitemap_index', [ $this, 'add_to_index' ] ); + + if ( WPSEO_Options::get( 'video_fitvids' ) === true ) { + // Fitvids scripting. + add_action( 'wp_head', [ $this, 'fitvids' ] ); + } + + if ( WPSEO_Options::get( 'video_disable_rss' ) !== true ) { + // MRSS. + add_action( 'rss2_ns', [ $this, 'mrss_namespace' ] ); + add_action( 'rss2_item', [ $this, 'mrss_item' ], 10, 1 ); + add_filter( 'mrss_media', [ $this, 'mrss_add_video' ] ); + } + } + + $this->date = new WPSEO_Date_Helper(); + } + + /** + * Retrieve a value to use for content_width. + * + * @since 3.8.0 + * + * @param int $default_value Optional. Default value to use if value could not be determined. + * + * @return int|false Integer content width value or false if it could not be determined + * and no default was provided. + */ + public function get_content_width( $default_value = 0 ) { + // If the theme or WP has set it, use what's already available. + if ( ! empty( $GLOBALS['content_width'] ) ) { + return (int) $GLOBALS['content_width']; + } + + // If the user has set it in options, use that. + $option_content_width = (int) WPSEO_Options::get( 'video_content_width' ); + if ( $option_content_width > 0 ) { + return $option_content_width; + } + + // Otherwise fall back to an arbitrary default if provided. + // WP itself uses 500 for embeds, 640 for playlists and video shortcodes. + if ( $default_value > 0 ) { + return $default_value; + } + + return false; + } + + /** + * Method to invalidate the sitemap + * + * @param int $post_id Post ID. + */ + public function invalidate_sitemap( $post_id ) { + // If this is just a revision, don't invalidate the sitemap cache yet. + if ( wp_is_post_revision( $post_id ) ) { + return; + } + + // Bail if this is a multisite installation and the site has been switched. + if ( is_multisite() && ms_is_switched() ) { + return; + } + + if ( ! WPSEO_Video_Utils::is_videoseo_active_for_posttype( get_post_type( $post_id ) ) ) { + return; + } + + WPSEO_Video_Wrappers::invalidate_sitemap( self::get_video_sitemap_basename() ); + } + + /** + * When sitemap is coming out of the cache there is no stylesheet. Normally it will take the default stylesheet. + * + * This method is called by a filter that will set the video stylesheet. + * + * @param object $target_object Target object. + * + * @return object + */ + public function set_stylesheet_cache( $target_object ) { + if ( property_exists( $target_object, 'renderer' ) ) { + $target_object->renderer->set_stylesheet( $this->get_stylesheet_line() ); + } + + return $target_object; + } + + /** + * Getter for stylesheet url + * + * @return string + */ + public function get_stylesheet_line() { + $stylesheet_url = "\n" . 'get_xsl_url() ) . '"?>'; + + return $stylesheet_url; + } + + /** + * Adds the fitvids JavaScript to the output if there's a video on the page that's supported by this script. + * Prevents fitvids being added when the JWPlayer plugin is active as they are incompatible. + * + * @todo - check if we can remove the JW6. The JWP plugin does some checking and deactivating + * themselves, so if we can rely on that, all the better. + * + * @since 1.5.4 + */ + public function fitvids() { + if ( ! is_singular() || defined( 'JWP6' ) ) { + return; + } + + global $post; + + if ( WPSEO_Video_Utils::is_videoseo_active_for_posttype( $post->post_type ) === false ) { + return; + } + + $video = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + + if ( ! is_array( $video ) || $video === [] ) { + return; + } + + // Check if the current post contains a YouTube, Vimeo, Blip.tv or Viddler video, if it does, add the fitvids code. + if ( in_array( $video['type'], [ 'youtube', 'vimeo', 'blip.tv', 'viddler', 'wistia' ], true ) ) { + $file = 'js/jquery.fitvids.min.js'; + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { + $file = 'js/jquery.fitvids.js'; + } + + wp_enqueue_script( + 'fitvids', + plugins_url( $file, WPSEO_VIDEO_FILE ), + [ 'jquery' ], + WPSEO_VIDEO_VERSION, + true // Load in footer. + ); + } + + add_action( 'wp_footer', [ $this, 'fitvids_footer' ] ); + } + + /** + * The fitvids instantiation code. + * + * @since 1.5.4 + */ + public function fitvids_footer() { + global $post; + + // Try and use the post class to determine the container. + $classes = get_post_class( '', $post->ID ); + $class_name = 'post'; + if ( is_array( $classes ) && $classes !== [] ) { + $class_name = $classes[0]; + } + $script = sprintf( + 'jQuery( document ).ready( function ( $ ) { $( ".%s" ).fitVids( {customSelector: "iframe.wistia_embed"} ); } );', + esc_attr( $class_name ) + ); + + wp_add_inline_script( 'fitvids', $script ); + } + + /** + * Registers the Video SEO submenu. + * + * @param array $submenu_pages Currently registered submenu pages. + * + * @return array Submenu pages with our submenu added. + */ + public function add_submenu_pages( $submenu_pages ) { + $submenu_pages[] = [ + 'wpseo_dashboard', + 'Yoast SEO: Video SEO', + 'Video SEO', + 'wpseo_manage_options', + 'wpseo_video', + [ $this, 'admin_panel' ], + ]; + + return $submenu_pages; + } + + /** + * Adds the rewrite for the video XML sitemap + * + * @since 0.1 + */ + public function init() { + $this->max_entries = $this->get_entries_per_page(); + $this->add_oembed(); + + add_filter( 'wpseo_helpscout_beacon_settings', [ $this, 'filter_helpscout_beacon' ] ); + } + + /** + * Makes sure the News settings page has a HelpScout beacon. + * + * @param array $helpscout_settings The HelpScout settings. + * + * @return array $helpscout_settings The HelpScout settings with the News SEO beacon added. + */ + public function filter_helpscout_beacon( $helpscout_settings ) { + $helpscout_settings['pages_ids']['wpseo_video'] = '4e7489db-f907-41b3-9e86-93a01b4df9b0'; + $helpscout_settings['products'][] = WPSEO_Addon_Manager::VIDEO_SLUG; + + return $helpscout_settings; + } + + /** + * Add VideoSeo Admin bar menu item + * + * @param object $wp_admin_bar Current admin bar. + */ + public function add_admin_bar_item( $wp_admin_bar ) { + if ( $this->can_manage_options() === true ) { + $wp_admin_bar->add_menu( + [ + 'parent' => 'wpseo-settings', + 'id' => 'wpseo-video', + 'title' => __( 'Video SEO', 'yoast-video-seo' ), + 'href' => admin_url( 'admin.php?page=wpseo_video' ), + ] + ); + } + } + + /** + * Register the video sitemap in the WPSEO sitemap class + * + * @since 1.7 + */ + public function register_sitemap() { + $basename = self::get_video_sitemap_basename(); + + // Register the sitemap. + WPSEO_Video_Wrappers::register_sitemap( $basename, [ $this, 'build_video_sitemap' ] ); + WPSEO_Video_Wrappers::register_xsl( 'video', [ $this, 'build_video_sitemap_xsl' ] ); + + if ( is_admin() ) { + // Setting action for removing the transient on update options. + WPSEO_Video_Wrappers::register_cache_clear_option( 'wpseo_video', $basename ); + } + else { + // Setting stylesheet for cached sitemap. + add_action( 'wpseo_sitemap_stylesheet_cache_' . $basename, [ $this, 'set_stylesheet_cache' ] ); + } + } + + /** + * Execute upgrade actions when needed + */ + public function upgrade() { + $options = get_option( 'wpseo_video' ); + $current_version = '0'; + if ( ! empty( $options['video_dbversion'] ) ) { + $current_version = $options['video_dbversion']; + } + + if ( $current_version === '0' && ! empty( $options['dbversion'] ) ) { + $current_version = $options['dbversion']; + } + + // Early bail if dbversion is equal to current version. + if ( version_compare( $current_version, WPSEO_VIDEO_VERSION, '==' ) ) { + return; + } + + // Upgrade to new option & meta classes. + if ( version_compare( $current_version, '1.6', '<' ) ) { + WPSEO_Option_Video::get_instance()->clean(); + // Make sure our meta values are cleaned up even if WP SEO would have been upgraded already. + WPSEO_Meta::clean_up(); + } + + // Re-add missing durations. + if ( $current_version === '0' || ( version_compare( $current_version, '1.7', '<' ) && version_compare( $current_version, '1.6', '>' ) ) ) { + WPSEO_Meta_Video::re_add_durations(); + } + + // Recommend force re-index. + if ( $current_version !== '0' && version_compare( $current_version, '4.0', '<' ) ) { + set_transient( 'video_seo_recommend_reindex', 1 ); + } + + // Rename the option values. + if ( $current_version !== '0' && version_compare( $current_version, '12.4-RC1', '<=' ) ) { + $fields_to_convert = [ + 'dbversion' => 'video_dbversion', + 'cloak_sitemap' => 'video_cloak_sitemap', + 'disable_rss' => 'video_disable_rss', + 'custom_fields' => 'video_custom_fields', + 'facebook_embed' => 'video_facebook_embed', + 'fitvids' => 'video_fitvids', + 'content_width' => 'video_content_width', + 'wistia_domain' => 'video_wistia_domain', + 'embedly_api_key' => 'video_embedly_api_key', + ]; + + foreach ( $fields_to_convert as $current_field => $new_field ) { + if ( ! isset( $options[ $current_field ] ) ) { + continue; + } + + $options[ $new_field ] = $options[ $current_field ]; + } + + update_option( 'wpseo_video', $options ); + } + + // Make sure version nr gets updated for any version without specific upgrades. + // Re-get to make sure we have the latest version. + if ( version_compare( $current_version, WPSEO_VIDEO_VERSION, '<' ) ) { + WPSEO_Options::set( 'video_dbversion', WPSEO_VIDEO_VERSION ); + } + } + + /** + * Recommend re-index with force index checked + * + * @since 1.8.0 + */ + public function recommend_force_index() { + if ( ! $this->can_manage_options() ) { + return; + } + + printf( + ' +
+

%2$s

+

%3$s

+
', + esc_js( wp_create_nonce( 'videoseo-ignore' ) ), // #1. + esc_html__( 'Ignore.', 'yoast-video-seo' ), // #2. + sprintf( + /* translators: 1: link open tag, 2: link close tag. */ + esc_html__( 'The VideoSEO upgrade which was just applied contains a lot of improvements. It is strongly recommended that you %1$sre-index the video content on your website%2$s with the \'force reindex\' option checked.', 'yoast-video-seo' ), + '', + '' + ) // #3. + ); + } + + /** + * Function used to remove the temporary admin notices for several purposes, dies on exit. + */ + public function set_ignore() { + if ( ! $this->can_manage_options() || ! isset( $_POST['option'] ) ) { + die( '-1' ); + } + + check_ajax_referer( 'videoseo-ignore' ); + delete_transient( 'video_seo_' . sanitize_text_field( wp_unslash( $_POST['option'] ) ) ); + die( '1' ); + } + + /** + * Load other scripts for the admin in the Video SEO plugin + */ + public function admin_video_enqueue_scripts() { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Loads a page, doesn't perform any action yet. + if ( isset( $_POST['reindex'] ) ) { + wp_enqueue_script( + 'videoseo-admin-progress-bar', + plugins_url( 'js/videoseo-admin-progressbar' . WPSEO_CSSJS_SUFFIX . '.js', WPSEO_VIDEO_FILE ), + [ 'jquery' ], + WPSEO_VIDEO_VERSION, + true + ); + } + } + + /** + * Load styles for the admin in Video SEO + */ + public function admin_video_enqueue_styles() { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Loads a page, doesn't perform any action yet. + if ( isset( $_POST['reindex'] ) ) { + wp_enqueue_style( + 'videoseo-admin-progress-bar-css', + plugins_url( 'css/dist/videoseo-admin-progressbar.css', WPSEO_VIDEO_FILE ), + [], + WPSEO_VIDEO_VERSION + ); + } + } + + /** + * Load a small js file to facilitate ignoring admin messages + */ + public function admin_enqueue_scripts_ignore() { + if ( ! $this->can_manage_options() ) { + return; + } + + wp_enqueue_script( 'videoseo-admin-global-script', plugins_url( 'js/videoseo-admin-global' . WPSEO_CSSJS_SUFFIX . '.js', WPSEO_VIDEO_FILE ), [ 'jquery' ], WPSEO_VIDEO_VERSION, true ); + } + + /** + * AJAX request handler for reindex posts + */ + public function index_posts_callback() { + if ( isset( $_POST['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'videoseo-ajax-nonce-for-reindex' ) ) { + if ( isset( $_POST['type'] ) && $_POST['type'] === 'total_posts' ) { + $total = 0; + + $sitemap_post_types = (array) WPSEO_Options::get( 'videositemap_posttypes', [] ); + foreach ( $sitemap_post_types as $post_type ) { + $total += wp_count_posts( $post_type )->publish; + } + echo (int) $total; + } + elseif ( isset( $_POST['type'] ) && $_POST['type'] === 'index' ) { + $start_time = time(); + + $post_defaults = [ + 'portion' => 5, + 'start' => 0, + 'total' => 0, + ]; + + foreach ( $post_defaults as $key => $default ) { + if ( isset( $_POST[ $key ] ) && is_numeric( $_POST[ $key ] ) ) { + ${$key} = (int) $_POST[ $key ]; + } + else { + ${$key} = $default; + } + } + + $this->reindex( $portion, $start, $total ); + + $end_time = time(); + + // Return time in seconds that we've needed to index. + echo (int) ( ( $end_time - $start_time ) + 1 ); + } + } + + exit; + } + + /** + * Returns the basename of the video-sitemap, the first portion of the name of the sitemap "file". + * + * Retrieves the video sitemap basename. + * + * @since 1.5.3 + * + * @return string + */ + public function video_sitemap_basename() { + return self::get_video_sitemap_basename(); + } + + /** + * Defaults to video, but it's possible to override it by using the YOAST_VIDEO_SITEMAP_BASENAME constant. + * + * @return string The sitemap basename. + */ + public static function get_video_sitemap_basename() { + $basename = 'video'; + + if ( post_type_exists( 'video' ) ) { + $basename = 'yoast-video'; + } + + if ( defined( 'YOAST_VIDEO_SITEMAP_BASENAME' ) ) { + $basename = YOAST_VIDEO_SITEMAP_BASENAME; + } + + return $basename; + } + + /** + * Return the Video Sitemap URL + * + * @since 1.2.1 + * @since 3.8.0 The $extra parameter was added. + * + * @param string $extra Optionally suffix to add to the filename part of the sitemap url. + * + * @return string The URL to the video Sitemap. + */ + public function sitemap_url( $extra = '' ) { + $sitemap = self::get_video_sitemap_basename() . '-sitemap' . $extra . '.xml'; + + return WPSEO_Video_Wrappers::xml_sitemaps_base_url( $sitemap ); + } + + /** + * Adds the video XML sitemap to the Index Sitemap. + * + * @since 0.1 + * + * @param string $str String with the filtered additions to the index sitemap in it. + * + * @return string String with the Video XML sitemap additions to the index sitemap in it. + */ + public function add_to_index( $str ) { + $base = $GLOBALS['wp_rewrite']->using_index_permalinks() ? 'index.php/' : ''; + + $sitemap_post_types = WPSEO_Options::get( 'videositemap_posttypes', [] ); + if ( is_array( $sitemap_post_types ) && $sitemap_post_types !== [] ) { + // Use fields => ids to limit the overhead of fetching entire post objects, fetch only an array of ids instead to count. + // phpcs:disable WordPress.DB.SlowDBQuery -- no other way. + $args = [ + 'post_type' => $sitemap_post_types, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_key' => '_yoast_wpseo_video_meta', + 'meta_compare' => '!=', + 'meta_value' => 'none', + 'fields' => 'ids', + ]; + // phpcs:enable WordPress.DB.SlowDBQuery -- no other way. + // Copy these args to be used and modify later. + $date_args = $args; + + $video_ids = get_posts( $args ); + $count = count( $video_ids ); + + if ( $count > 0 ) { + $n = ( $count > $this->max_entries ) ? (int) ceil( $count / $this->max_entries ) : 1; + for ( $i = 0; $i < $n; $i++ ) { + $count = ( $n > 1 ) ? ( $i + 1 ) : ''; + + if ( empty( $count ) || $count === $n ) { + $date_args['fields'] = 'all'; + $date_args['posts_per_page'] = 1; + $date_args['offset'] = 0; + $date_args['order'] = 'DESC'; + $date_args['orderby'] = 'modified'; + } + else { + $date_args['fields'] = 'all'; + $date_args['posts_per_page'] = 1; + $date_args['offset'] = ( ( $this->max_entries * ( $i + 1 ) ) - 1 ); + $date_args['order'] = 'ASC'; + $date_args['orderby'] = 'modified'; + } + + $posts = get_posts( $date_args ); + $date = $this->date->format( $posts[0]->post_modified_gmt ); + + $text = ( $count > 1 ) ? $count : ''; + $str .= '' . "\n"; + $str .= '' . $this->sitemap_url( $text ) . '' . "\n"; + $str .= '' . $date . '' . "\n"; + $str .= '' . "\n"; + } + } + } + + return $str; + } + + /** + * Adds oembed endpoints for supported video platforms that are not supported by core. + * + * @since 1.3.5 + */ + public function add_oembed() { + // @todo - check with official plugin. + // Wistia. + $wistia_regex = '`(?:http[s]?:)?//[^/]*(wistia\.(com|net)|wi\.st#CUSTOM_URL#)/(medias|embed)/.*`i'; + $wistia_domain = WPSEO_Options::get( 'video_wistia_domain', '' ); + if ( $wistia_domain !== '' ) { + $wistia_regex = str_replace( '#CUSTOM_URL#', '|' . preg_quote( $wistia_domain, '`' ), $wistia_regex ); + } + else { + $wistia_regex = str_replace( '#CUSTOM_URL#', '', $wistia_regex ); + } + wp_oembed_add_provider( $wistia_regex, 'http://fast.wistia.com/oembed', true ); + + // Viddler - WP native support removed in WP 4.0. + wp_oembed_add_provider( '`http[s]?://(?:www\.)?viddler\.com/.*`i', 'http://lab.viddler.com/services/oembed/', true ); + + // Screenr. + wp_oembed_add_provider( '`http[s]?://(?:www\.)?screenr\.com/.*`i', 'http://www.screenr.com/api/oembed.{format}', true ); + + // EVS. + $evs_location = get_option( 'evs_location' ); + if ( $evs_location && ! empty( $evs_location ) ) { + wp_oembed_add_provider( $evs_location . '/*', $evs_location . '/oembed.php', false ); + } + } + + /** + * Synchronize the WP native oembed providers list for various WP versions. + * + * If VideoSEO users choose to stay on a lower WP version, they will still get the benefit of improved + * oembed regexes and provider compatibility this way. + * + * @param string[] $providers Providers. + * + * @return string[] + */ + public function sync_oembed_providers( $providers ) { + + // Support SSL urls for flick shortdomain (natively added in WP4.0). + if ( isset( $providers['http://flic.kr/*'] ) ) { + unset( $providers['http://flic.kr/*'] ); + $providers['#https?://flic\.kr/.*#i'] = [ 'https://www.flickr.com/services/oembed/', true ]; + } + + // Change to SSL for oembed provider domain (natively changed in WP4.0). + if ( isset( $providers['#https?://(www\.)?flickr\.com/.*#i'] ) && strpos( $providers['#https?://(www\.)?flickr\.com/.*#i'][0], 'https' ) !== 0 ) { + $providers['#https?://(www\.)?flickr\.com/.*#i'] = [ 'https://www.flickr.com/services/oembed/', true ]; + } + + // Allow any vimeo subdomain (natively changed in WP3.9). + if ( isset( $providers['#https?://(www\.)?vimeo\.com/.*#i'] ) ) { + unset( $providers['#https?://(www\.)?vimeo\.com/.*#i'] ); + $providers['#https?://(.+\.)?vimeo\.com/.*#i'] = [ 'http://vimeo.com/api/oembed.{format}', true ]; + } + + // Support SSL urls for wordpress.tv (natively added in WP4.0). + if ( isset( $providers['http://wordpress.tv/*'] ) ) { + unset( $providers['http://wordpress.tv/*'] ); + $providers['#https?://wordpress.tv/.*#i'] = [ 'http://wordpress.tv/oembed/', true ]; + } + + return $providers; + } + + /** + * Add the MRSS namespace to the RSS feed. + * + * @since 0.1 + */ + public function mrss_namespace() { + echo ' xmlns:media="http://search.yahoo.com/mrss/" '; + } + + /** + * Add the MRSS info to the feed + * + * Based upon the MRSS plugin {@link https://wordpress.org/plugins/mrss/} developed by Andy Skelton + * + * @since 0.1 + * @copyright Andy Skelton + */ + public function mrss_item() { + global $mrss_gallery_lookup; + $media = []; + $lookup = []; + + // Honor the feed settings. Don't include any media that isn't in the feed. + if ( get_option( 'rss_use_excerpt' ) || ! strlen( get_the_content() ) ) { + ob_start(); + the_excerpt_rss(); + $content = ob_get_clean(); + } + else { + // If any galleries are processed, we need to capture the attachment IDs. + add_filter( 'wp_get_attachment_link', [ $this, 'mrss_gallery_lookup' ], 10, 5 ); + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using a WP Core hook. + $content = apply_filters( 'the_content', get_the_content() ); + remove_filter( 'wp_get_attachment_link', [ $this, 'mrss_gallery_lookup' ], 10, 5 ); + $lookup = $mrss_gallery_lookup; + unset( $mrss_gallery_lookup ); + } + + $images = 0; + if ( preg_match_all( '`]+)>`', $content, $matches ) ) { + foreach ( $matches[1] as $attrs ) { + $item = []; + $img = []; + // Construct $img array from attributes. + $attributes = wp_kses_hair( $attrs, [ 'http' ] ); + foreach ( $attributes as $attr ) { + $img[ $attr['name'] ] = $attr['value']; + } + unset( $attributes ); + + // Skip emoticons and images without source attribute. + if ( ! isset( $img['src'] ) || ( isset( $img['class'] ) && strpos( $img['class'], 'wp-smiley' ) !== false ) ) { + continue; + } + + $img['src'] = $this->mrss_url( $img['src'] ); + + $id = false; + if ( isset( $lookup[ $img['src'] ] ) ) { + $id = $lookup[ $img['src'] ]; + } + elseif ( isset( $img['class'] ) && preg_match( '`wp-image-(\d+)`', $img['class'], $match ) ) { + $id = $match[1]; + } + if ( $id ) { + // It's an attachment, so we will get the URLs, title, and description from functions. + $attachment = get_post( $id ); + $src = wp_get_attachment_image_src( $id, 'full' ); + if ( ! empty( $src[0] ) ) { + $img['src'] = $src[0]; + } + $thumbnail = wp_get_attachment_image_src( $id, 'thumbnail' ); + if ( ! empty( $thumbnail[0] ) && $thumbnail[0] !== $img['src'] ) { + $img['thumbnail'] = $thumbnail[0]; + } + $title = get_the_title( $id ); + if ( ! empty( $title ) ) { + $img['title'] = trim( $title ); + } + if ( ! empty( $attachment->post_excerpt ) ) { + $img['description'] = trim( $attachment->post_excerpt ); + } + } + // If this is the first image in the markup, make it the post thumbnail. + if ( ++$images === 1 ) { + if ( isset( $img['thumbnail'] ) ) { + $media[]['thumbnail']['attr']['url'] = $img['thumbnail']; + } + else { + $media[]['thumbnail']['attr']['url'] = $img['src']; + } + } + + $item['content']['attr']['url'] = $img['src']; + $item['content']['attr']['medium'] = 'image'; + if ( ! empty( $img['title'] ) ) { + $item['content']['children']['title']['attr']['type'] = 'html'; + $item['content']['children']['title']['children'][] = $img['title']; + } + elseif ( ! empty( $img['alt'] ) ) { + $item['content']['children']['title']['attr']['type'] = 'html'; + $item['content']['children']['title']['children'][] = $img['alt']; + } + if ( ! empty( $img['description'] ) ) { + $item['content']['children']['description']['attr']['type'] = 'html'; + $item['content']['children']['description']['children'][] = $img['description']; + } + if ( ! empty( $img['thumbnail'] ) ) { + $item['content']['children']['thumbnail']['attr']['url'] = $img['thumbnail']; + } + $media[] = $item; + } + } + + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using a hook from the MediaRSS plugin. + $media = apply_filters( 'mrss_media', $media ); + $this->mrss_print( $media ); + } + + /** + * Create an absolute URL for use in the MRSS info. + * + * @param string $url Variable to evaluate for URL. + * + * @return string + */ + public function mrss_url( $url ) { + if ( preg_match( '`^(?:http[s]?:)//`', $url ) ) { + return $url; + } + else { + return home_url( $url ); + } + } + + /** + * Add attachments to the MRSS gallery lookup array. + * + * @param string $link Link tag. + * @param string|int $id ID to lookup. + * + * @return string + */ + public function mrss_gallery_lookup( $link, $id ) { + if ( preg_match( '` src="([^"]+)"`', $link, $matches ) ) { + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- not our global var. + $GLOBALS['mrss_gallery_lookup'][ $matches[1] ] = $id; + } + + return $link; + } + + /** + * Print an MRSS item. + * + * @param array $media Media. + */ + public function mrss_print( $media ) { + if ( ! empty( $media ) ) { + foreach ( (array) $media as $element ) { + $this->mrss_print_element( $element ); + } + } + echo "\n"; + } + + /** + * Print an MRSS element. + * + * @param array $element Element. + * @param int $indent Ident. + */ + public function mrss_print_element( $element, $indent = 2 ) { + echo "\n"; + foreach ( (array) $element as $name => $data ) { + // phpcs:ignore WordPress.Security.EscapeOutput -- This str_repeat() is safe. + echo str_repeat( "\t", $indent ); + echo ' $value ) { + echo ' ' . esc_attr( $attr ) . '="' . esc_attr( ent2ncr( $value ) ) . '"'; + } + } + if ( ! empty( $data['children'] ) && is_array( $data['children'] ) ) { + $nl = false; + echo '>'; + foreach ( $data['children'] as $_name => $_data ) { + if ( is_int( $_name ) ) { + echo ent2ncr( esc_html( $_data ) ); + } + else { + $nl = true; + $this->mrss_print_element( [ $_name => $_data ], ( $indent + 1 ) ); + } + } + if ( $nl ) { + // phpcs:ignore WordPress.Security.EscapeOutput -- This str_repeat() is safe. + echo "\n" . str_repeat( "\t", $indent ); + } + echo ''; + } + else { + echo ' />'; + } + } + } + + /** + * Add the video output to the MRSS feed. + * + * @since 0.1 + * + * @param array $media Media. + * + * @return array + */ + public function mrss_add_video( $media ) { + global $post; + + if ( WPSEO_Video_Utils::is_videoseo_active_for_posttype( $post->post_type ) === false ) { + return $media; + } + + $video = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + + if ( ! is_array( $video ) || $video === [] ) { + return $media; + } + + $video_duration = WPSEO_Meta::get_value( 'videositemap-duration', $post->ID ); + if ( $video_duration === '0' && isset( $video['duration'] ) ) { + $video_duration = $video['duration']; + } + + $item = []; + $item['content']['attr']['url'] = $video['player_loc']; + $item['content']['attr']['duration'] = $video_duration; + $item['content']['children']['player']['attr']['url'] = $video['player_loc']; + $item['content']['children']['title']['attr']['type'] = 'html'; + $item['content']['children']['title']['children'][] = esc_html( $video['title'] ); + $item['content']['children']['description']['attr']['type'] = 'html'; + $item['content']['children']['description']['children'][] = esc_html( $video['description'] ); + $item['content']['children']['thumbnail']['attr']['url'] = $video['thumbnail_loc']; + + if ( array_key_exists( 'tag', $video ) ) { + $item['content']['children']['keywords']['children'][] = is_array( $video['tag'] ) ? implode( ',', $video['tag'] ) : $video['tag']; + } + else { + $item['content']['children']['keywords']['children'][] = ''; + } + + array_unshift( $media, $item ); + + return $media; + } + + /** + * Parse the content of a post or term description. + * + * @since 1.3 + * @see WPSEO_Video_Analyse_Post + * + * @param string $content The content to parse for videos. + * @param array $vid The video array to update. + * @param array $old_vid The former video array. + * @param object|int|null $post The post object or the post id of the post to analyse. + * + * @return array + */ + public function index_content( $content, $vid, $old_vid = [], $post = null ) { + $index = new WPSEO_Video_Analyse_Post( $content, $vid, $old_vid, $post ); + + return $index->get_vid_info(); + } + + /** + * Check and, if applicable, update video details for a term description + * + * @since 1.3 + * + * @param object $term The term to check the description and possibly update the video details for. + * @param bool $send_to_screen Whether or not to echo the performed actions. + * + * @return array|string|false The video array that was just stored, or "none" if nothing + * was stored or false if not applicable. + */ + public function update_video_term_meta( $term, $send_to_screen = false ) { + $sitemap_taxonomies = WPSEO_Options::get( 'videositemap_taxonomies', [] ); + if ( ! is_array( $sitemap_taxonomies ) || $sitemap_taxonomies === [] ) { + return false; + } + + if ( ! in_array( $term->taxonomy, $sitemap_taxonomies, true ) ) { + return false; + } + + $tax_meta = get_option( 'wpseo_taxonomy_meta' ); + $old_vid = []; + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Check done elsewhere. + if ( ! isset( $_POST['force'] ) ) { + if ( isset( $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] ) ) { + $old_vid = $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ]; + } + } + + $vid = []; + + $title = WPSEO_Taxonomy_Meta::get_term_meta( $term->term_id, $term->taxonomy, 'wpseo_title' ); + if ( empty( $title ) ) { + $default_title = WPSEO_Options::get( 'title-' . $term->taxonomy, '' ); + if ( $default_title !== '' ) { + $title = wpseo_replace_vars( $default_title, (array) $term ); + } + } + if ( empty( $title ) ) { + $title = $term->name; + } + $vid['title'] = htmlspecialchars( $title, ENT_COMPAT, get_bloginfo( 'charset' ), true ); + + $vid['description'] = WPSEO_Taxonomy_Meta::get_term_meta( $term->term_id, $term->taxonomy, 'wpseo_metadesc' ); + if ( ! $vid['description'] ) { + $vid['description'] = esc_attr( preg_replace( '`\s+`', ' ', wp_html_excerpt( strip_shortcodes( get_term_field( 'description', $term->term_id, $term->taxonomy ) ), 300 ) ) ); + } + + $vid['publication_date'] = $this->date->format_timestamp( time() ); + + // Concatenate genesis intro text and term description to index the videos for both. + $genesis_term_meta = get_option( 'genesis-term-meta' ); + + $content = ''; + if ( isset( $genesis_term_meta[ $term->term_id ]['intro_text'] ) && $genesis_term_meta[ $term->term_id ]['intro_text'] ) { + $content .= $genesis_term_meta[ $term->term_id ]['intro_text']; + } + + $content .= "\n" . $term->description; + $content = stripslashes( $content ); + + $vid = $this->index_content( $content, $vid, $old_vid, null ); + + if ( $vid !== 'none' ) { + $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] = $vid; + // Don't bother with the complete tax meta validation. + $tax_meta['wpseo_already_validated'] = true; + update_option( 'wpseo_taxonomy_meta', $tax_meta ); + + if ( $send_to_screen ) { + $link = get_term_link( $term ); + if ( ! is_wp_error( $link ) ) { + echo 'Updated ' . esc_html( $vid['title'] ) . ' - ' . esc_html( $vid['type'] ) . '
'; + } + } + } + + return $vid; + } + + /** + * (Don't) validate the _video taxonomy metadata array + * Doesn't actually validate it atm, but having this function hooked in *does* make sure that the + * _video taxonomy metadata is not removed as it otherwise would be (by the normal taxonomy meta validation). + * + * @since 1.6 + * + * @param array $tax_meta_data Received _video tax metadata. + * + * @return array Validated _video tax metadata + */ + public function validate_video_tax_meta( $tax_meta_data ) { + return $tax_meta_data; + } + + /** + * Check and, if applicable, update video details for a post + * + * @since 0.1 + * @since 3.8 The $echo parameter was removed and the $post and $update parameters + * added to be in line with the parameters received from the hook this + * method is tied to. + * @since 11.x Removed the $update parameter as it was never used. + * + * @param int $post_id The post ID to check and possibly update the video details for. + * @param \WP_Post|null $post The post object. + * + * @return array|string|false The video array that was just stored, string "none" if nothing + * was stored or false if not applicable. + */ + public function update_video_post_meta( $post_id, $post = null ) { + // phpcs:disable WordPress.Security.NonceVerification.Missing -- Check done elsewhere. + + global $wp_query; + + // Bail if this is a multisite installation and the site has been switched. + if ( is_multisite() && ms_is_switched() ) { + return false; + } + + if ( ! is_numeric( $post_id ) ) { + // Get post ID from the request. Added this for our Elementor save hook. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Nonce has been verified earlier in the pipeline, We are not processing form information, We are casting to an integer. + $post_id = isset( $_POST['post'] ) && is_string( $_POST['post'] ) ? (int) wp_unslash( $_POST['post'] ) : 0; + } + + if ( ( ! isset( $post ) || ! ( $post instanceof WP_Post ) ) && is_numeric( $post_id ) ) { + $post = get_post( $post_id ); + } + + if ( isset( $post ) && ( ! ( $post instanceof WP_Post ) || ! isset( $post->ID ) ) ) { + return false; + } + + if ( WPSEO_Video_Utils::is_videoseo_active_for_posttype( $post->post_type ) === false ) { + return false; + } + + $old_vid = []; + if ( ! isset( $_POST['force'] ) ) { + $old_vid = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + } + + $title = WPSEO_Meta::get_value( 'title', $post->ID ); + if ( ! is_string( $title ) || $title === '' ) { + $default_title = WPSEO_Options::get( 'title-' . $post->post_type, '' ); + if ( $default_title !== '' ) { + $title = wpseo_replace_vars( $default_title, (array) $post ); + } + else { + $title = wpseo_replace_vars( '%%title%% - %%sitename%%', (array) $post ); + } + } + + if ( ! is_string( $title ) || $title === '' ) { + $title = $post->post_title; + } + + $vid = []; + + // @todo [JRF->Yoast] Verify if this is really what we want. What about non-hierarchical custom post types ? and are we adjusting the main query output now ? could this cause bugs for others ? + if ( $post->post_type === 'post' ) { + $wp_query->is_single = true; + $wp_query->is_page = false; + } + else { + $wp_query->is_single = false; + $wp_query->is_page = true; + } + + $vid['post_id'] = $post->ID; + $vid['title'] = htmlspecialchars( $title, ENT_COMPAT, get_bloginfo( 'charset' ), true ); + + $vid['publication_date'] = $this->date->format( $post->post_date_gmt ); + + $vid['description'] = WPSEO_Meta::get_value( 'metadesc', $post->ID ); + if ( ! is_string( $vid['description'] ) || $vid['description'] === '' ) { + $default_description = WPSEO_Options::get( 'metadesc-' . $post->post_type, '' ); + if ( $default_description !== '' ) { + $vid['description'] = wpseo_replace_vars( $default_description, (array) $post ); + } + else { + $vid['description'] = esc_attr( preg_replace( '`\s+`', ' ', wp_html_excerpt( strip_shortcodes( $post->post_content ), 300 ) ) ); + } + } + + $vid = $this->index_content( $post->post_content, $vid, $old_vid, $post ); + + if ( $vid !== 'none' ) { + // Shouldn't be needed, but just in case. + if ( isset( $vid['__add_to_content'] ) ) { + unset( $vid['__add_to_content'] ); + } + + if ( ! isset( $vid['thumbnail_loc'] ) || empty( $vid['thumbnail_loc'] ) ) { + $img = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'single-post-thumbnail' ); + if ( strpos( $img[0], 'http' ) !== 0 ) { + $vid['thumbnail_loc'] = get_site_url( null, $img[0] ); + } + else { + $vid['thumbnail_loc'] = $img[0]; + } + } + + // Grab the metadata from the post. + $tags = wp_get_object_terms( $post->ID, 'post_tag', [ 'fields' => 'names' ] ); + + if ( isset( $_POST['yoast_wpseo_videositemap-tags'] ) && ! empty( $_POST['yoast_wpseo_videositemap-tags'] ) ) { + $extra_tags = explode( ',', sanitize_text_field( wp_unslash( $_POST['yoast_wpseo_videositemap-tags'] ) ) ); + $tags = array_merge( $extra_tags, $tags ); + } + + $tag = []; + if ( is_array( $tags ) ) { + foreach ( $tags as $t ) { + $tag[] = $t; + } + } + elseif ( isset( $cats[0] ) ) { + $tag[] = $cats[0]->name; + } + + $focuskw = WPSEO_Meta::get_value( 'focuskw', $post->ID ); + if ( ! empty( $focuskw ) ) { + $tag[] = $focuskw; + } + $vid['tag'] = $tag; + + if ( WPSEO_Video_Wrappers::is_development_mode() ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- we're in development mode. + error_log( 'Updated [' . esc_html( $post->post_title ) . '](' . esc_url( add_query_arg( [ 'p' => $post->ID ], home_url() ) ) . ') - ' . esc_html( $vid['type'] ) ); + } + } + + WPSEO_Meta::set_value( 'video_meta', $vid, $post->ID ); + + // phpcs:enable WordPress.Security.NonceVerification.Missing -- Check done elsewhere. + + return $vid; + } + + /** + * Check whether the current visitor is really Google or Bing's bot by doing a reverse DNS lookup + * + * @since 1.2.2 + * + * @return bool + */ + public function is_valid_bot() { + if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && isset( $_SERVER['REMOTE_ADDR'] ) && preg_match( '`(Google|bing)bot`', sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), $match ) ) { + $hostname = gethostbyaddr( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ); + + if ( + ( $match[1] === 'Google' && preg_match( '`googlebot\.com$`', $hostname ) && gethostbyname( $hostname ) === $_SERVER['REMOTE_ADDR'] ) + || ( $match[1] === 'bing' && preg_match( '`search\.msn\.com$`', $hostname ) && gethostbyname( $hostname ) === $_SERVER['REMOTE_ADDR'] ) + ) { + return true; + } + } + + return false; + } + + /** + * Get the server protocol. + * + * @since 4.1.0 + * + * @return string + */ + protected function get_server_protocol() { + $protocol = 'HTTP/1.1'; + if ( isset( $_SERVER['SERVER_PROTOCOL'] ) && $_SERVER['SERVER_PROTOCOL'] !== '' ) { + $protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); + } + + return $protocol; + } + + /** + * Outputs the XSL file + */ + public function build_video_sitemap_xsl() { + $protocol = $this->get_server_protocol(); + + // Force a 200 header and replace other status codes. + header( $protocol . ' 200 OK', true, 200 ); + + // Set the right content / mime type. + header( 'Content-Type: text/xml' ); + + // Prevent the search engines from indexing the XML Sitemap. + header( 'X-Robots-Tag: noindex, follow', true ); + + // Make the browser cache this file properly. + header( 'Pragma: public' ); + header( 'Cache-Control: maxage=' . YEAR_IN_SECONDS ); + header( 'Expires: ' . $this->date->format_timestamp( ( time() + YEAR_IN_SECONDS ), 'D, d M Y H:i:s' ) . ' GMT' ); + + global $wp_filesystem; + require_once ABSPATH . '/wp-admin/includes/file.php'; + WP_Filesystem(); + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This file is straight from this plugin. + echo $wp_filesystem->get_contents( plugin_dir_path( WPSEO_VIDEO_FILE ) . 'xml-video-sitemap.xsl' ); + + die(); + } + + /** + * The main function of this class: it generates the XML sitemap's contents. + * + * @since 0.1 + */ + public function build_video_sitemap() { + $protocol = $this->get_server_protocol(); + + // Restrict access to the video sitemap to admins and valid bots. + if ( WPSEO_Options::get( 'video_cloak_sitemap' ) === true && ( ! $this->can_manage_options() && ! $this->is_valid_bot() ) ) { + header( $protocol . ' 403 Forbidden', true, 403 ); + wp_die( "We're sorry, access to our video sitemap is restricted to site admins and valid Google & Bing bots." ); + } + + // Force a 200 header and replace other status codes. + header( $protocol . ' 200 OK', true, 200 ); + + $output = '' . "\n"; + + $printed_post_ids = []; + + $steps = $this->max_entries; + $n = (int) get_query_var( 'sitemap_n' ); + $offset = ( $n > 1 ) ? ( ( $n - 1 ) * $this->max_entries ) : 0; + $total = ( $offset + $this->max_entries ); + + $sitemap_post_types = WPSEO_Options::get( 'videositemap_posttypes', [] ); + if ( is_array( $sitemap_post_types ) && $sitemap_post_types !== [] ) { + // Set the initial args array to get videos in chunks. + // phpcs:disable WordPress.DB.SlowDBQuery -- Ain't no other way. + $args = [ + 'post_type' => $sitemap_post_types, + 'post_status' => 'publish', + 'posts_per_page' => $steps, + 'offset' => $offset, + 'meta_key' => '_yoast_wpseo_video_meta', + 'meta_compare' => '!=', + 'meta_value' => 'none', + 'order' => 'ASC', + 'orderby' => 'post_modified', + ]; + // phpcs:enable WordPress.DB.SlowDBQuery -- Ain't no other way. + + /* + * @TODO: add support to tax video to honor pages + * add a bool to the while loop to see if tax has been processed + * if $items is empty the posts are done so move on to tax + * + * do some math between $printed_post_ids and $this-max_entries to figure out + * how many from tax to add to this pagination + */ + + // Add entries to the sitemap until the total is hit (rounded up by nearest $steps). + $items = get_posts( $args ); + while ( ( $total > $offset ) && $items ) { + + if ( is_array( $items ) && $items !== [] ) { + foreach ( $items as $item ) { + if ( ! is_object( $item ) || in_array( $item->ID, $printed_post_ids, true ) ) { + continue; + } + else { + $printed_post_ids[] = $item->ID; + } + + if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $item->ID ) === '1' ) { + continue; + } + + $disable = WPSEO_Meta::get_value( 'videositemap-disable', $item->ID ); + if ( $disable === 'on' ) { + continue; + } + + $video = WPSEO_Meta::get_value( 'video_meta', $item->ID ); + + $video = WPSEO_Video_Utils::get_video_image( $item->ID, $video ); + + // When we don't have a thumbnail and either a player_loc or a content_loc, skip this video. + if ( ! isset( $video['thumbnail_loc'] ) + || ( ! isset( $video['player_loc'] ) && ! isset( $video['content_loc'] ) ) + ) { + continue; + } + + $video_duration = WPSEO_Meta::get_value( 'videositemap-duration', $item->ID ); + if ( $video_duration > 0 ) { + $video['duration'] = $video_duration; + } + + $video['permalink'] = get_permalink( $item ); + + /** + * Filter: 'wpseo_video_rating' - Allow changing the rating for a video on output. + * + * @api float $rating A rating between 0 and 5. + * + * @param int $post_id The ID of the post the video is in. + */ + $rating = apply_filters( 'wpseo_video_rating', WPSEO_Meta::get_value( 'videositemap-rating', $item->ID ) ); + if ( $rating && WPSEO_Meta_Video::sanitize_rating( null, $rating, WPSEO_Meta_Video::$meta_fields['video']['videositemap-rating'] ) ) { + $video['rating'] = number_format( $rating, 1 ); + } + + $video['family_friendly'] = 'yes'; + if ( WPSEO_Video_Utils::is_video_family_friendly( $item->ID ) === false ) { + $video['family_friendly'] = 'no'; + } + + $video['author'] = $item->post_author; + + $output .= $this->print_sitemap_line( $video, $item ); + } + } + + // Update these args for the next iteration. + $offset = ( $offset + $steps ); + $args['offset'] += $steps; + $items = get_posts( $args ); + } + } + + $tax_meta = get_option( 'wpseo_taxonomy_meta' ); + $terms = []; + + $sitemap_taxonomies = WPSEO_Options::get( 'videositemap_taxonomies', [] ); + if ( is_array( $sitemap_taxonomies ) && $sitemap_taxonomies !== [] ) { + $terms = get_terms( + [ + 'taxonomy' => array_values( $sitemap_taxonomies ), + ] + ); + } + + if ( is_array( $terms ) && $terms !== [] ) { + foreach ( $terms as $term ) { + if ( is_object( $term ) && isset( $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] ) ) { + $video = $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ]; + if ( is_array( $video ) ) { + $video['permalink'] = get_term_link( $term, $term->taxonomy ); + $video['tag'] = $term->name; + $output .= $this->print_sitemap_line( $video, $term ); + } + } + } + } + + $output .= ''; + + WPSEO_Video_Wrappers::set_sitemap( $output ); + WPSEO_Video_Wrappers::set_stylesheet( $this->get_stylesheet_line() ); + } + + /** + * Print a full line in the sitemap. + * + * @since 1.3 + * + * @param array $video The video object to print out. + * @param object $post_or_tax_object The post/tax object this video relates to. + * + * @return string The output generated + */ + public function print_sitemap_line( $video, $post_or_tax_object ) { + if ( ! is_array( $video ) || $video === [] ) { + return ''; + } + + $output = "\t\n"; + $output .= "\t\t" . esc_url( $video['permalink'] ) . '' . "\n"; + $output .= "\t\t\n"; + + + if ( empty( $video['publication_date'] ) || WPSEO_Video_Wrappers::is_valid_datetime( $video['publication_date'] ) === false ) { + $post = $post_or_tax_object; + if ( is_object( $post ) && $post->post_date_gmt !== '0000-00-00 00:00:00' && WPSEO_Video_Wrappers::is_valid_datetime( $post->post_date_gmt ) ) { + $video['publication_date'] = $this->date->format( $post->post_date_gmt ); + } + elseif ( is_object( $post ) && $post->post_date !== '0000-00-00 00:00:00' && WPSEO_Video_Wrappers::is_valid_datetime( $post->post_date ) ) { + $video['publication_date'] = $this->date->format( get_gmt_from_date( $post->post_date ) ); + } + else { + return ''; + } // If we have no valid date for the post, skip the video and don't print it in the XML Video Sitemap. + } + + // @todo - We should really switch to whitelist format, rather than blacklist + $video_keys_to_skip = [ + 'id', + 'url', + 'type', + 'permalink', + 'post_id', + 'hd', + 'maybe_local', + 'attachment_id', + 'file_path', + 'file_url', + 'last_fetched', + ]; + + foreach ( $video as $key => $val ) { + if ( in_array( $key, $video_keys_to_skip, true ) ) { + continue; + } + + if ( $key === 'author' ) { + $output .= "\t\t\t" . ent2ncr( esc_html( get_the_author_meta( 'display_name', $val ) ) ) . "\n"; + continue; + } + + if ( $key === 'description' && empty( $val ) ) { + $val = $video['title']; + } + + if ( is_scalar( $val ) && ! empty( $val ) ) { + $prepare_sitemap_line = $this->get_single_sitemap_line( $val, $key, '', $post_or_tax_object ); + + if ( ! is_null( $prepare_sitemap_line ) ) { + $output .= $prepare_sitemap_line; + } + } + elseif ( is_array( $val ) && $val !== [] ) { + $i = 1; + foreach ( $val as $v ) { + // Only 32 tags are allowed. + if ( $key === 'tag' && $i > 32 ) { + break; + } + $prepare_sitemap_line = $this->get_single_sitemap_line( $v, $key, '', $post_or_tax_object ); + + if ( ! is_null( $prepare_sitemap_line ) ) { + $output .= $prepare_sitemap_line; + } + + ++$i; + } + } + } + + // Allow custom implementations with extra tags here. + $output .= apply_filters( 'wpseo_video_item', '', isset( $video['post_id'] ) ? $video['post_id'] : 0 ); + + $output .= "\t\t\n"; + + $output .= "\t\n"; + + return $output; + } + + /** + * Cleans a string for XML display purposes. + * + * @since 1.2.1 + * + * @link http://php.net/html-entity-decode#98697 Modified for WP from here. + * + * @param string $in The string to clean. + * @param int|null $offset Offset of the string to start the cleaning at. + * + * @return string Cleaned string. + */ + public function clean_string( $in, $offset = 0 ) { + $out = trim( $in ); + $out = strip_shortcodes( $out ); + $out = html_entity_decode( $out, ENT_QUOTES, 'ISO-8859-15' ); + $out = html_entity_decode( $out, ENT_QUOTES, get_bloginfo( 'charset' ) ); + if ( ! empty( $out ) ) { + $entity_start = strpos( $out, '&', $offset ); + if ( $entity_start === false ) { + return _wp_specialchars( $out ); + } + else { + $entity_end = strpos( $out, ';', $entity_start ); + if ( $entity_end === false ) { + return _wp_specialchars( $out ); + } + elseif ( $entity_end > ( $entity_start + 7 ) ) { + $out = $this->clean_string( $out, ( $entity_start + 1 ) ); + } + else { + $clean = substr( $out, 0, $entity_start ); + $subst = substr( $out, ( $entity_start + 1 ), 1 ); + $clean .= ( $subst !== '#' ) ? $subst : '_'; + $clean .= substr( $out, ( $entity_end + 1 ) ); + $out = $this->clean_string( $clean, ( $entity_start + 1 ) ); + } + } + } + + return _wp_specialchars( $out ); + } + + /** + * Roughly calculate the length of an FLV video. + * + * @since 1.3.1 + * + * @param string $file The path to the video file to calculate the length for. + * + * @return int Duration of the video + */ + public function get_flv_duration( $file ) { + // phpcs:disable WordPress.WP.AlternativeFunctions -- rewriting this as WP filesystem isn't worth it. + if ( is_file( $file ) && is_readable( $file ) ) { + $flv = fopen( $file, 'rb' ); + if ( is_resource( $flv ) ) { + fseek( $flv, -4, SEEK_END ); + $arr = unpack( 'N', fread( $flv, 4 ) ); + $last_tag_offset = $arr[1]; + fseek( $flv, -( $last_tag_offset + 4 ), SEEK_END ); + fseek( $flv, 4, SEEK_CUR ); + $t0 = fread( $flv, 3 ); + $t1 = fread( $flv, 1 ); + $arr = unpack( 'N', $t1 . $t0 ); + $milliseconds_duration = $arr[1]; + + return $milliseconds_duration; + } + } + // phpcs:enable WordPress.WP.AlternativeFunctions -- rewriting this as WP filesystem isn't worth it. + return 0; + } + + /** + * Outputs the admin panel for the Video Sitemaps on the XML Sitemaps page with the WP SEO admin + * + * @since 0.1 + */ + public function admin_panel() { + $sitemap_url = null; + $sitemap_post_types = WPSEO_Options::get( 'videositemap_posttypes', [] ); + if ( is_array( $sitemap_post_types ) && $sitemap_post_types !== [] ) { + // Use fields => ids to limit the overhead of fetching entire post objects, fetch only an array of ids instead to count. + // phpcs:disable WordPress.DB.SlowDBQuery -- no other way to do this. + $args = [ + 'post_type' => $sitemap_post_types, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_key' => '_yoast_wpseo_video_meta', + 'meta_compare' => '!=', + 'meta_value' => 'none', + 'fields' => 'ids', + ]; + // phpcs:enable WordPress.DB.SlowDBQuery -- no other way to do this. + $video_ids = get_posts( $args ); + $count = count( $video_ids ); + $n = ( $count > $this->max_entries ) ? (int) ceil( $count / $this->max_entries ) : ''; + $sitemap_url = $this->sitemap_url( $n ); + } + + $admin_page = new WPSEO_Video_Admin_Page(); + $admin_page->display( $sitemap_url ); + } + + /** + * A better strip tags that leaves spaces intact (and rips out more code) + * + * @since 1.3.4 + * + * @link http://php.net/strip-tags#110280 + * + * @param string $text Text string to strip tags from. + * + * @return string + */ + public function strip_tags( $text ) { + + // ----- remove HTML TAGs ----- + $text = preg_replace( '/<[^>]*>/', ' ', $text ); + + // ----- remove control characters ----- + $text = str_replace( "\r", '', $text ); // --- replace with empty space + $text = str_replace( "\n", ' ', $text ); // --- replace with space + $text = str_replace( "\t", ' ', $text ); // --- replace with space + + // ----- remove multiple spaces ----- + $text = trim( preg_replace( '/ {2,}/', ' ', $text ) ); + + return $text; + } + + /** + * Add the video and yandex namespaces to the namespaces in the html prefix attribute. + * + * @since 4.1.0 + * + * @link http://ogp.me/#type_video + * @link https://yandex.com/support/webmaster/video/open-graph.xml + * + * @param string[] $namespaces Currently registered namespaces. + * + * @return string[] + */ + public function add_video_namespaces( $namespaces ) { + $namespaces[] = 'video: http://ogp.me/ns/video#'; + + /** + * Allow for turning off Yandex support. + * + * @since 4.1.0 + * + * @param bool Whether or not to support (add) Yandex specific video SEO + * meta tags. Defaults to `true`. + * Return `false` to disable Yandex support. + */ + if ( apply_filters( 'wpseo_video_yandex_support', true ) === true ) { + $namespaces[] = 'ya: http://webmaster.yandex.ru/vocabularies/'; + } + + return $namespaces; + } + + /** + * Switch the Twitter card type to player if needed. + * + * {@internal [JRF] This method does not seem to be hooked in anywhere.} + * + * @param string $type The Twitter card type. + * + * @return string + */ + public function card_type( $type ) { + return $this->type_filter( $type, 'player' ); + } + + /** + * Helper function for Twitter and OpenGraph card types + * + * @param string $type The card type. + * @param string $video_output Output. + * + * @return string + */ + public function type_filter( $type, $video_output ) { + global $post; + + if ( is_singular() ) { + if ( is_object( $post ) ) { + if ( WPSEO_Video_Utils::is_videoseo_active_for_posttype( $post->post_type ) === false ) { + return $type; + } + + $video = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + if ( ! is_array( $video ) || $video === [] ) { + return $type; + } + + $disable = WPSEO_Meta::get_value( 'videositemap-disable', $post->ID ); + if ( $disable === 'on' ) { + return $type; + } + + return $video_output; + } + } + elseif ( is_tax() || is_category() || is_tag() ) { + $term = get_queried_object(); + + $sitemap_taxonomies = WPSEO_Options::get( 'videositemap_taxonomies', [] ); + if ( is_array( $sitemap_taxonomies ) && in_array( $term->taxonomy, $sitemap_taxonomies, true ) ) { + $tax_meta = get_option( 'wpseo_taxonomy_meta' ); + if ( isset( $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] ) ) { + return $video_output; + } + } + } + + return $type; + } + + /** + * Filter the OpenGraph image for the post and sets it to the video thumbnail + * + * @param Images $image_container The WPSEO OpenGraph image object. + * + * @return void + */ + public function opengraph_image( Images $image_container ) { + if ( is_singular() ) { + $post = get_queried_object(); + + if ( is_object( $post ) ) { + // If there are images already, the video still is probably not going the be the best image, so bail. + if ( $image_container->get_images() !== [] ) { + return; + } + + if ( WPSEO_Video_Utils::is_videoseo_active_for_posttype( $post->post_type ) === false ) { + return; + } + + $disable = WPSEO_Meta::get_value( 'videositemap-disable', $post->ID ); + if ( $disable === 'on' ) { + return; + } + + $video = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + if ( ! is_array( $video ) || $video === [] ) { + return; + } + + $image_container->add_image_by_url( $video['thumbnail_loc'] ); + + return; + } + + return; + } + + if ( is_tax() || is_category() || is_tag() ) { + $term = get_queried_object(); + + $sitemap_taxonomies = WPSEO_Options::get( 'videositemap_taxonomies', [] ); + if ( is_array( $sitemap_taxonomies ) && in_array( $term->taxonomy, $sitemap_taxonomies, true ) ) { + $tax_meta = get_option( 'wpseo_taxonomy_meta' ); + if ( isset( $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] ) ) { + $video = $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ]; + $image_container->add_image_by_url( $video['thumbnail_loc'] ); + } + } + } + } + + /** + * Make the get_terms query only return terms with a non-empty description. + * + * @since 1.3 + * + * @param array $pieces The separate pieces of the terms query to filter. + * + * @return string[] + */ + public function filter_terms_clauses( $pieces ) { + $pieces['where'] .= " AND tt.description != ''"; + + return $pieces; + } + + /** + * Get a single sitemap line to output in the xml sitemap + * + * @param string $val Value. + * @param string $key Key. + * @param string $xtra Extra. + * @param object $post_or_tax_object The post/tax object this value relates to. + * + * @return string|null + */ + private function get_single_sitemap_line( $val, $key, $xtra, $post_or_tax_object ) { + $val = $this->clean_string( $val ); + if ( in_array( $key, [ 'description', 'category', 'tag', 'title' ], true ) ) { + $val = ent2ncr( esc_html( $val ) ); + } + if ( ! empty( $val ) ) { + $val = wpseo_replace_vars( $val, $post_or_tax_object ); + $val = _wp_specialchars( html_entity_decode( $val, ENT_QUOTES, 'UTF-8' ) ); + + if ( in_array( $key, [ 'description', 'category', 'tag', 'title' ], true ) ) { + $val = ''; + } + + return "\t\t\t' . $val . '\n"; + } + + return null; + } + + /** + * Reindex the video info from posts + * + * @since 0.1 + * @since 3.8 $total parameter was added. + * + * @param int $portion Number of posts. + * @param int $start Offset. + * @param int $total Total number of posts which will be re-indexed. + */ + private function reindex( $portion, $start, $total ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $sitemap_post_types = WPSEO_Options::get( 'videositemap_posttypes', [] ); + if ( is_array( $sitemap_post_types ) && $sitemap_post_types !== [] ) { + $args = [ + 'post_type' => $sitemap_post_types, + 'post_status' => 'publish', + 'numberposts' => $portion, + 'offset' => $start, + ]; + + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- we don't have to verify this with a nonce, we have to verify the overall action. + if ( ! isset( $_POST['force'] ) ) { + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- no other way to do this. + $args['meta_query'] = [ + 'key' => '_yoast_wpseo_video_meta', + 'compare' => 'NOT EXISTS', + ]; + } + + + $results = get_posts( $args ); + $result_count = count( $results ); + + if ( is_array( $results ) && $result_count > 0 ) { + foreach ( $results as $post ) { + if ( $post instanceof WP_Post ) { + $this->update_video_post_meta( $post->ID, $post ); + } + elseif ( is_numeric( $post ) ) { + $this->update_video_post_meta( $post ); + } + flush(); // Clear system output buffer if any exist. + } + } + } + + if ( ( $start + $portion ) >= $total ) { + // Get all the non-empty terms. + add_filter( 'terms_clauses', [ $this, 'filter_terms_clauses' ] ); + $terms = []; + $sitemap_taxonomies = WPSEO_Options::get( 'videositemap_taxonomies', [] ); + if ( is_array( $sitemap_taxonomies ) && $sitemap_taxonomies !== [] ) { + foreach ( $sitemap_taxonomies as $val ) { + $new_terms = get_terms( $val ); + if ( is_array( $new_terms ) ) { + $terms = array_merge( $terms, $new_terms ); + } + } + } + remove_filter( 'terms_clauses', [ $this, 'filter_terms_clauses' ] ); + + if ( count( $terms ) > 0 ) { + + foreach ( $terms as $term ) { + $this->update_video_term_meta( $term, false ); + flush(); + } + } + + // As this is used from within an AJAX call, we don't queue the cache clearing, + // but do a hard reset. + WPSEO_Video_Wrappers::invalidate_cache_storage( self::get_video_sitemap_basename() ); + + // Ping the search engines with our updated XML sitemap, we ping with the index sitemap because + // we don't know which video sitemap, or sitemaps, have been updated / added. + WPSEO_Video_Wrappers::ping_search_engines(); + + // Remove the admin notice. + delete_transient( 'video_seo_recommend_reindex' ); + } + } + + /** + * Retrieves the XSL URL that should be used in the current environment + * + * When home_url and site_url are not the same, the home_url should be used. + * This is because the XSL needs to be served from the same domain, protocol and port + * as the XML file that is loading it. + * + * @return string The XSL URL that needs to be used. + */ + protected function get_xsl_url() { + if ( home_url() !== site_url() ) { + return home_url( 'video-sitemap.xsl' ); + } + + return plugin_dir_url( WPSEO_VIDEO_FILE ) . 'xml-video-sitemap.xsl'; + } + + /** + * Checks if the user can manage options. + * + * @since 5.6.0 + * + * @return bool True if the user can manage options. + */ + protected function can_manage_options() { + if ( class_exists( 'WPSEO_Capability_Utils' ) ) { + return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + } + + return false; + } + + /** + * Retrieves the maximum number of entries per XML sitemap. + * + * @return int The maximum number of entries. + */ + protected function get_entries_per_page() { + /** + * Filter the maximum number of entries per XML sitemap. + * + * @param int $entries The maximum number of entries per XML sitemap. + */ + return (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using a YoastSEO Free hook. + } +} diff --git a/classes/class-wpseo-video-utils.php b/classes/class-wpseo-video-utils.php new file mode 100644 index 0000000..ea0bcf7 --- /dev/null +++ b/classes/class-wpseo-video-utils.php @@ -0,0 +1,248 @@ +post_type ) === false ) { + return false; + } + + $disable = WPSEO_Meta::get_value( 'videositemap-disable', $post->ID ); + if ( $disable === 'on' ) { + return false; + } + + $video = WPSEO_Meta::get_value( 'video_meta', $post->ID ); + + // For Youtube, refresh the video data every 30 days. + $thirty_days = ( DAY_IN_SECONDS * 30 ); + + if ( is_array( $video ) && $video !== [] ) { + $video = self::get_video_image( $post->ID, $video ); + $video['duration'] = self::get_video_duration( $video, $post->ID ); + + $needs_refresh = isset( $video['last_fetched'] ) ? ( $video['last_fetched'] < ( time() - $thirty_days ) ) : true; + if ( $video['type'] === 'youtube' && $needs_refresh ) { + $video_details = new WPSEO_Video_Details_Youtube( $video ); + $video = $video_details->get_details(); + WPSEO_Meta::set_value( 'video_meta', $video, $post->ID ); + } + } + + return $video; + } + + /** + * Retrieves the video meta data for a term page. + * + * @param WP_Term $term The term for which to retrieve the video meta data. + * + * @return array|false The video metadata, or `false` if not meta data could be retrieved. + */ + public static function get_video_for_term( $term ) { + $video = false; + $sitemap_taxonomies = WPSEO_Options::get( 'videositemap_taxonomies', [] ); + if ( is_array( $sitemap_taxonomies ) && in_array( $term->taxonomy, $sitemap_taxonomies, true ) ) { + $video = []; + + $tax_meta = get_option( 'wpseo_taxonomy_meta' ); + if ( isset( $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] ) ) { + $video = $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ]; + } + + // For Youtube, refresh the video data every 30 days. + $thirty_days = ( DAY_IN_SECONDS * 30 ); + $needs_refresh = isset( $video['last_fetched'] ) ? ( $video['last_fetched'] < ( time() - $thirty_days ) ) : true; + if ( $video['type'] === 'youtube' && $needs_refresh ) { + $video_details = new WPSEO_Video_Details_Youtube( $video ); + $video = $video_details->get_details(); + $tax_meta[ $term->taxonomy ]['_video'][ $term->term_id ] = $video; + update_option( 'wpseo_taxonomy_meta', $tax_meta ); + } + + $video['duration'] = self::get_video_duration( $video ); + } + + return $video; + } + + /** + * Check to see if the video thumbnail was manually set, if so, update the $video array. + * + * @since 11.1 + * + * @param int $post_id The post to check for. + * @param array $video The video array. + * + * @return array + */ + public static function get_video_image( $post_id, $video ) { + // Allow for the video's thumbnail to be overridden by the meta box input. + $videoimg = WPSEO_Meta::get_value( 'videositemap-thumbnail', $post_id ); + if ( ( $video !== 'none' ) && ( $videoimg !== '' ) ) { + $video['thumbnail_loc'] = $videoimg; + } + + return $video; + } + + /** + * Retrieve the duration of a video. + * + * Use a user provided duration if available, fall back to the available video data + * as previously retrieved through an API call. + * + * @since 11.1 + * + * @param array $video Data about the video being evaluated. + * @param int|null $post_id Optional. Post ID. + * + * @return int Duration in seconds or 0 if no duration could be determined. + */ + public static function get_video_duration( $video, $post_id = null ) { + $video_duration = 0; + + if ( isset( $post_id ) ) { + $video_duration = (int) WPSEO_Meta::get_value( 'videositemap-duration', $post_id ); + } + + if ( $video_duration === 0 && isset( $video['duration'] ) ) { + $video_duration = (int) $video['duration']; + } + + return $video_duration; + } + + /** + * Converts the duration in seconds to an ISO 8601 compatible output. Assumes the length is not over 24 hours. + * + * @link https://en.wikipedia.org/wiki/ISO_8601 + * + * @param int $duration The duration in seconds. + * + * @return string ISO 8601 compatible output. + */ + public static function iso_8601_duration( $duration ) { + if ( $duration <= 0 ) { + return ''; + } + + $out = 'PT'; + if ( $duration > HOUR_IN_SECONDS ) { + $hours = floor( $duration / HOUR_IN_SECONDS ); + $out .= $hours . 'H'; + $duration = ( $duration - ( $hours * HOUR_IN_SECONDS ) ); + } + if ( $duration > MINUTE_IN_SECONDS ) { + $minutes = floor( $duration / MINUTE_IN_SECONDS ); + $out .= $minutes . 'M'; + $duration = ( $duration - ( $minutes * MINUTE_IN_SECONDS ) ); + } + if ( $duration > 0 ) { + $out .= $duration . 'S'; + } + + return $out; + } + + /** + * Determine whether a video is family friendly or not. + * + * @since 11.1 + * + * @param int $post_id Post ID. + * + * @return bool True if family friendly, false if not. + */ + public static function is_video_family_friendly( $post_id ) { + $family_friendly = true; + + // We store this inverted which is incredibly annoying. + $not_family_friendly = WPSEO_Meta::get_value( 'videositemap-not-family-friendly', $post_id ); + if ( is_string( $not_family_friendly ) && $not_family_friendly === 'on' ) { + $family_friendly = false; + } + + /** + * Filter: 'wpseo_video_family_friendly' - Allow changing the family friendly setting for a video. + * + * @api bool $family_friendly Set to `false` to mark a video as _not_ family friendly. + * + * @param int $post_id Post ID. + */ + $filter_return = apply_filters( 'wpseo_video_family_friendly', $family_friendly, $post_id ); + + // For legacy reasons, this filter used to be quite ugly. + if ( is_string( $filter_return ) ) { + if ( $filter_return === 'on' ) { + return true; + } + return false; + } + + if ( is_bool( $filter_return ) ) { + return $filter_return; + } + + return $family_friendly; + } + + /** + * Return the plugin file + * + * @since 11.1 + * + * @return string + */ + public static function get_plugin_file() { + return WPSEO_VIDEO_FILE; + } + + /** + * Load translations + * + * @since 11.1 + */ + public static function load_textdomain() { + load_plugin_textdomain( 'yoast-video-seo', false, dirname( plugin_basename( WPSEO_VIDEO_FILE ) ) . '/languages/' ); + } +} diff --git a/classes/class-wpseo-video-wrappers.php b/classes/class-wpseo-video-wrappers.php new file mode 100644 index 0000000..7f51c0d --- /dev/null +++ b/classes/class-wpseo-video-wrappers.php @@ -0,0 +1,314 @@ +register_sitemap( $name, $callback, $rewrite ); + } + } + + /** + * Call WPSEO_Sitemaps::register_xsl() if the method exists. + * + * @since 4.1 + * + * @param string $name The name of the XSL file. + * @param callable $callback Function to build your XSL file. + * @param string $rewrite Optional. Regular expression to match your sitemap with. + */ + public static function register_xsl( $name, $callback, $rewrite = '' ) { + // WPSEO 1.4.23+. + if ( isset( $GLOBALS['wpseo_sitemaps'] ) && is_object( $GLOBALS['wpseo_sitemaps'] ) && method_exists( 'WPSEO_Sitemaps', 'register_xsl' ) ) { + $GLOBALS['wpseo_sitemaps']->register_xsl( $name, $callback, $rewrite ); + } + } + + /** + * Call WPSEO_Sitemaps::set_sitemap() if the method exists. + * + * @since 4.1 + * + * @param string $sitemap The generated sitemap to output. + */ + public static function set_sitemap( $sitemap ) { + // WPSEO 1.4.23+. + if ( isset( $GLOBALS['wpseo_sitemaps'] ) && is_object( $GLOBALS['wpseo_sitemaps'] ) && method_exists( 'WPSEO_Sitemaps', 'set_sitemap' ) ) { + $GLOBALS['wpseo_sitemaps']->set_sitemap( $sitemap ); + } + } + + /** + * Call WPSEO_Sitemaps::set_stylesheet() if the method exists. + * + * @since 4.1 + * + * @param string $stylesheet Full xml-stylesheet declaration. + */ + public static function set_stylesheet( $stylesheet ) { + if ( isset( $GLOBALS['wpseo_sitemaps'] ) && is_object( $GLOBALS['wpseo_sitemaps'] ) ) { + + // WPSEO 3.2+. + if ( method_exists( 'WPSEO_Sitemaps_Renderer', 'set_stylesheet' ) && property_exists( $GLOBALS['wpseo_sitemaps'], 'renderer' ) && ( $GLOBALS['wpseo_sitemaps']->renderer instanceof WPSEO_Sitemaps_Renderer ) ) { + $GLOBALS['wpseo_sitemaps']->renderer->set_stylesheet( $stylesheet ); + return; + } + + // WPSEO 1.4.23+. + if ( method_exists( $GLOBALS['wpseo_sitemaps'], 'set_stylesheet' ) ) { + $GLOBALS['wpseo_sitemaps']->set_stylesheet( $stylesheet ); + return; + } + } + } + + /** + * Returns the result of WPSEO_Utils::is_development_mode() if the method exists. + * + * @since 4.1 + * + * @return bool + */ + public static function is_development_mode() { + // WPSEO 3.0+. + if ( method_exists( 'WPSEO_Utils', 'is_development_mode' ) ) { + return WPSEO_Utils::is_development_mode(); + } + + return false; + } + + /** + * Returns the result of get_base_url from WPSEO_Sitemaps_Router if the method exists, + * otherwise it will return the result from the deprecated wpseo_xml_sitemaps_base_url() function. + * + * @since 4.1 + * + * @param string $sitemap Sitemap file name. + * + * @return string + */ + public static function xml_sitemaps_base_url( $sitemap ) { + // WPSEO 3.2+. + if ( method_exists( 'WPSEO_Sitemaps_Router', 'get_base_url' ) ) { + return WPSEO_Sitemaps_Router::get_base_url( $sitemap ); + } + + if ( function_exists( 'wpseo_xml_sitemaps_base_url' ) ) { + return wpseo_xml_sitemaps_base_url( $sitemap ); + } + } + + /** + * Call WPSEO_Sitemaps::ping_search_engines() if the method exists, + * otherwise it will call the deprecated wpseo_ping_search_engines() function. + * + * @since 4.1 + * + * @param string|null $sitemapurl Sitemap URL. + * + * @return void + */ + public static function ping_search_engines( $sitemapurl = null ) { + // WPSEO 19.2+. + if ( method_exists( 'WPSEO_Sitemaps_Admin', 'ping_search_engines' ) ) { + $admin = new WPSEO_Sitemaps_Admin(); + $admin->ping_search_engines(); + return; + } + + // WPSEO 3.2+. + if ( method_exists( 'WPSEO_Sitemaps', 'ping_search_engines' ) ) { + WPSEO_Sitemaps::ping_search_engines( $sitemapurl ); + return; + } + + if ( function_exists( 'wpseo_ping_search_engines' ) ) { + wpseo_ping_search_engines( $sitemapurl ); + return; + } + } + + /** + * Wrapper function to invalidate a cached sitemap. + * + * @since 4.1 + * + * @param string|null $type The type to get the key for. Null for all caches. + * + * @return void + */ + public static function invalidate_cache_storage( $type = null ) { + // WPSEO 3.2+. + if ( method_exists( 'WPSEO_Sitemaps_Cache_Validator', 'invalidate_storage' ) ) { + WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type ); + return; + } + + // WPSEO 1.8.0+. + if ( method_exists( 'WPSEO_Utils', 'clear_sitemap_cache' ) ) { + WPSEO_Utils::clear_sitemap_cache( $type ); + return; + } + } + + /** + * Wrapper function to invalidate a sitemap type. + * + * @since 4.1 + * + * @param string $type Sitemap type to invalidate. + * + * @return void + */ + public static function invalidate_sitemap( $type ) { + // WPSEO 3.2+. + if ( method_exists( 'WPSEO_Sitemaps_Cache', 'invalidate' ) ) { + WPSEO_Sitemaps_Cache::invalidate( $type ); + return; + } + + // WPSEO 1.5.4+. + if ( function_exists( 'wpseo_invalidate_sitemap_cache' ) ) { + wpseo_invalidate_sitemap_cache( $type ); + return; + } + } + + /** + * Call WPSEO_Sitemaps_Cache::register_clear_on_option_update() if the method exists, + * otherwise it will call the deprecated WPSEO_Utils::register_cache_clear_option() function. + * + * @since 4.1 + * + * @param string $option Option name. + * @param string $type Sitemap type. + * + * @return void + */ + public static function register_cache_clear_option( $option, $type = '' ) { + // WPSEO 3.2+. + if ( method_exists( 'WPSEO_Sitemaps_Cache', 'register_clear_on_option_update' ) ) { + WPSEO_Sitemaps_Cache::register_clear_on_option_update( $option, $type ); + return; + } + + // WPSEO 2.2+. + if ( method_exists( 'WPSEO_Utils', 'register_cache_clear_option' ) ) { + WPSEO_Utils::register_cache_clear_option( $option, $type ); + return; + } + } +} diff --git a/classes/index.php b/classes/index.php new file mode 100644 index 0000000..9070b02 --- /dev/null +++ b/classes/index.php @@ -0,0 +1,4 @@ +indexation_actions = $indexation_actions; + $this->plugin_basename = plugin_basename( WPSEO_VIDEO_FILE ); + } + + /** + * Gets the namespace. + * + * @return string + */ + public static function get_namespace() { + return Main::WP_CLI_NAMESPACE . ' video'; + } + + /** + * Indexation of videos in your content. + * + * ## OPTIONS + * + * [--reindex] + * : Force reindex of already indexed videos. + * + * [--limit=] + * : The number of database records to per SQL query. + * --- + * default: 25 + * --- + * + * [--interval=] + * : The number of microseconds (millionths of a second) to wait between index actions. + * --- + * default: 50000 + * --- + * + * ## EXAMPLES + * + * wp yoast video index + * + * @when after_wp_load + * + * @param array|null $args The arguments. + * @param array|null $assoc_args The associative arguments. + * + * @return void + */ + public function index( $args = null, $assoc_args = null ) { + $reindex = isset( $assoc_args['reindex'] ); + $limit = isset( $assoc_args['limit'] ) ? (int) $assoc_args['limit'] : 25; + if ( $limit < 1 ) { + WP_CLI::error( 'The value for \'limit\' must be a positive integer, larger than 0.' ); + } + $interval = isset( $assoc_args['interval'] ) ? (int) $assoc_args['interval'] : 50000; + if ( $interval < 0 ) { + WP_CLI::error( 'The value for \'interval\' must be a positive integer.' ); + } + + $this->index_current_site( $reindex, $limit, $interval ); + + WP_CLI::success( 'Done!' ); + } + + /** + * Performs the indexation for the current site. + * + * @param bool $reindex Whether to force reindex. + * @param int $limit The limit per query. + * @param int $interval The number of microseconds to sleep. + */ + private function index_current_site( $reindex, $limit, $interval ) { + if ( ! is_plugin_active( $this->plugin_basename ) ) { + WP_CLI::warning( sprintf( 'Skipping %1$s. Yoast SEO Video is not active on this site.', site_url() ) ); + + return; + } + + foreach ( $this->indexation_actions as $indexation_action ) { + $this->run_indexation_action( $indexation_action, $limit, $interval, $reindex ); + } + + $this->index_complete(); + } + + /** + * Runs an indexation action. + * + * @param WPSEO_Video_Indexation_Action_Interface $indexation_action The indexation action. + * @param int $limit The limit per query. + * @param int $interval Number of microseconds to sleep. + * @param bool $reindex Whether to force reindex. + * + * @return void + */ + private function run_indexation_action( + WPSEO_Video_Indexation_Action_Interface $indexation_action, + $limit, + $interval, + $reindex + ) { + $total = $indexation_action->get_total(); + if ( $total <= 0 ) { + return; + } + $offset = 0; + $progress = Utils\make_progress_bar( 'Indexing ' . $indexation_action->get_name(), $total ); + do { + $indexation_action->index( $limit, $offset, $reindex ); + $progress->tick( $limit ); + $offset += $limit; + \usleep( $interval ); + Utils\wp_clear_object_cache(); + } while ( $offset <= $total ); + $progress->finish(); + } + + /** + * Cleans up after completing indexation on a site. + * + * @return void + */ + private function index_complete() { + // As this is used from within a CLI command, we don't queue the cache clearing, but do a hard reset. + WPSEO_Video_Wrappers::invalidate_cache_storage( WPSEO_Video_Sitemap::get_video_sitemap_basename() ); + + // Ping the search engines with our updated XML sitemap, we ping with the index sitemap because + // we don't know which video sitemap, or sitemaps, have been updated / added. + WPSEO_Video_Wrappers::ping_search_engines(); + + // Remove the admin notice. + delete_transient( 'video_seo_recommend_reindex' ); + } +} diff --git a/classes/indexation/class-wpseo-video-post-indexation-action.php b/classes/indexation/class-wpseo-video-post-indexation-action.php new file mode 100644 index 0000000..150f0af --- /dev/null +++ b/classes/indexation/class-wpseo-video-post-indexation-action.php @@ -0,0 +1,109 @@ +sitemap = $sitemap; + } + + /** + * Returns the name of the represented indexation action. + * + * @return string The name of the represented indexation action. + */ + public function get_name() { + return 'posts'; + } + + /** + * Returns the total number of posts. + * + * @return int The total number of posts. + */ + public function get_total() { + $total = 0; + $post_types = $this->get_post_types(); + foreach ( $post_types as $post_type ) { + $total += (int) wp_count_posts( $post_type )->publish; + } + + return $total; + } + + /** + * Index the video info from posts. + * + * @param int $limit The limit per query. + * @param int $offset The offset of the query. + * @param bool $reindex Whether to force reindex. + */ + public function index( $limit, $offset, $reindex ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $post_types = $this->get_post_types(); + if ( $post_types === [] ) { + return; + } + + $query = [ + 'post_type' => $post_types, + 'post_status' => 'publish', + 'numberposts' => $limit, + 'offset' => $offset, + ]; + if ( ! $reindex ) { + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- no other way to do this. + $query['meta_query'] = [ + 'key' => '_yoast_wpseo_video_meta', + 'compare' => 'NOT EXISTS', + ]; + } + + $results = get_posts( $query ); + if ( is_array( $results ) && count( $results ) > 0 ) { + if ( $reindex ) { + // Do this ugly thing instead of refactoring `update_video_post_meta`. + $_POST['force'] = $reindex; + } + + foreach ( $results as $post ) { + if ( $post instanceof WP_Post ) { + $this->sitemap->update_video_post_meta( $post->ID, $post ); + } + } + } + } + + /** + * Retrieves the post types to include in the sitemap. + * + * @return array The post types to include in the sitemap. + */ + private function get_post_types() { + return (array) WPSEO_Options::get( 'videositemap_posttypes', [] ); + } +} diff --git a/classes/indexation/class-wpseo-video-term-indexation-action.php b/classes/indexation/class-wpseo-video-term-indexation-action.php new file mode 100644 index 0000000..7d4eef8 --- /dev/null +++ b/classes/indexation/class-wpseo-video-term-indexation-action.php @@ -0,0 +1,111 @@ +sitemap = $sitemap; + } + + /** + * Returns the name of the represented indexation action. + * + * @return string The name of the represented indexation action. + */ + public function get_name() { + return 'terms'; + } + + /** + * Returns the total number of terms. + * + * @return int The total number of terms. + */ + public function get_total() { + // Get all the non-empty terms. + add_filter( 'terms_clauses', [ $this->sitemap, 'filter_terms_clauses' ] ); + $taxonomies = $this->get_taxonomies(); + $total = 0; + if ( $taxonomies !== [] ) { + $count = wp_count_terms( + [ + 'taxonomy' => array_values( $taxonomies ), + ] + ); + if ( is_string( $count ) ) { + $total = (int) $count; + } + } + remove_filter( 'terms_clauses', [ $this->sitemap, 'filter_terms_clauses' ] ); + + return $total; + } + + /** + * Index the video info from terms. + * + * @param int $limit The limit per query. + * @param int $offset The offset of the query. + * @param bool $reindex Whether to force reindex. + */ + public function index( $limit, $offset, $reindex ) { + // Get all the non-empty terms. + add_filter( 'terms_clauses', [ $this->sitemap, 'filter_terms_clauses' ] ); + $terms = []; + $taxonomies = $this->get_taxonomies(); + if ( $taxonomies !== [] ) { + $new_terms = get_terms( + [ + 'taxonomy' => array_values( $taxonomies ), + 'number' => $limit, + 'offset' => $offset, + ] + ); + if ( is_array( $new_terms ) ) { + $terms = $new_terms; + } + } + remove_filter( 'terms_clauses', [ $this->sitemap, 'filter_terms_clauses' ] ); + + if ( count( $terms ) > 0 ) { + if ( $reindex ) { + // Do this ugly thing instead of refactoring `update_video_term_meta`. + $_POST['force'] = $reindex; + } + + foreach ( $terms as $term ) { + $this->sitemap->update_video_term_meta( $term, false ); + flush(); + } + } + } + + /** + * Retrieves the taxonomies to include in the sitemap. + * + * @return array The taxonomies to include in the sitemap. + */ + private function get_taxonomies() { + return (array) WPSEO_Options::get( 'videositemap_taxonomies', [] ); + } +} diff --git a/classes/indexation/indexation-action-interface.php b/classes/indexation/indexation-action-interface.php new file mode 100644 index 0000000..5030725 --- /dev/null +++ b/classes/indexation/indexation-action-interface.php @@ -0,0 +1,36 @@ +video = $video; + } +} diff --git a/classes/presenters/class-wpseo-video-duration-presenter.php b/classes/presenters/class-wpseo-video-duration-presenter.php new file mode 100644 index 0000000..99ef27b --- /dev/null +++ b/classes/presenters/class-wpseo-video-duration-presenter.php @@ -0,0 +1,31 @@ +video['duration'] === 0 ) { + return ''; + } + return (string) $this->video['duration']; + } +} diff --git a/classes/presenters/class-wpseo-video-height-presenter.php b/classes/presenters/class-wpseo-video-height-presenter.php new file mode 100644 index 0000000..6c64765 --- /dev/null +++ b/classes/presenters/class-wpseo-video-height-presenter.php @@ -0,0 +1,31 @@ +video['height'] ) ) { + return ''; + } + return (string) $this->video['height']; + } +} diff --git a/classes/presenters/class-wpseo-video-location-presenter.php b/classes/presenters/class-wpseo-video-location-presenter.php new file mode 100644 index 0000000..0a99555 --- /dev/null +++ b/classes/presenters/class-wpseo-video-location-presenter.php @@ -0,0 +1,28 @@ +video['player_loc']; + } +} diff --git a/classes/presenters/class-wpseo-video-type-presenter.php b/classes/presenters/class-wpseo-video-type-presenter.php new file mode 100644 index 0000000..1a890a0 --- /dev/null +++ b/classes/presenters/class-wpseo-video-type-presenter.php @@ -0,0 +1,28 @@ +video['width'] ) ) { + return ''; + } + return (string) $this->video['width']; + } +} diff --git a/classes/presenters/yandex/class-wpseo-video-yandex-adult-presenter.php b/classes/presenters/yandex/class-wpseo-video-yandex-adult-presenter.php new file mode 100644 index 0000000..0d81325 --- /dev/null +++ b/classes/presenters/yandex/class-wpseo-video-yandex-adult-presenter.php @@ -0,0 +1,35 @@ +presentation->source; + if ( ! $post instanceof WP_Post ) { + return 'false'; + } + if ( WPSEO_Video_Utils::is_video_family_friendly( $post->ID ) === false ) { + return 'true'; + } + return 'false'; + } +} diff --git a/classes/presenters/yandex/class-wpseo-video-yandex-allow-embed-presenter.php b/classes/presenters/yandex/class-wpseo-video-yandex-allow-embed-presenter.php new file mode 100644 index 0000000..08f1dde --- /dev/null +++ b/classes/presenters/yandex/class-wpseo-video-yandex-allow-embed-presenter.php @@ -0,0 +1,28 @@ +presentation->source; + + if ( ! $post instanceof WP_Post ) { + return ''; + } + + return $this->helpers->date->format( $post->post_date_gmt ); + } +} diff --git a/css/dist/videoseo-admin-progressbar-rtl.css b/css/dist/videoseo-admin-progressbar-rtl.css new file mode 100644 index 0000000..e09078b --- /dev/null +++ b/css/dist/videoseo-admin-progressbar-rtl.css @@ -0,0 +1 @@ +#video_seo_progressbar{width:99%;border-radius:4px;border:1px solid #000;padding:0}#video_seo_progressbar .bar{background-color:#006691;height:100%;margin:0;width:0;padding:0}#video_seo_progressbar .bar p{margin:0;padding:0 10px;max-width:none;line-height:30px;text-align:center;color:#fff}#video_seo_progressbar .bar_status{width:1%}.video_seo_timetogo{font-size:1.1em} \ No newline at end of file diff --git a/css/dist/videoseo-admin-progressbar.css b/css/dist/videoseo-admin-progressbar.css new file mode 100644 index 0000000..e09078b --- /dev/null +++ b/css/dist/videoseo-admin-progressbar.css @@ -0,0 +1 @@ +#video_seo_progressbar{width:99%;border-radius:4px;border:1px solid #000;padding:0}#video_seo_progressbar .bar{background-color:#006691;height:100%;margin:0;width:0;padding:0}#video_seo_progressbar .bar p{margin:0;padding:0 10px;max-width:none;line-height:30px;text-align:center;color:#fff}#video_seo_progressbar .bar_status{width:1%}.video_seo_timetogo{font-size:1.1em} \ No newline at end of file diff --git a/css/dist/videoseo-youtube-embed-rtl.css b/css/dist/videoseo-youtube-embed-rtl.css new file mode 100644 index 0000000..3a752bc --- /dev/null +++ b/css/dist/videoseo-youtube-embed-rtl.css @@ -0,0 +1 @@ +.video-seo-youtube-player{position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;margin:0;background:transparent}.video-seo-youtube-player .video-seo-youtube-embed-loader,.video-seo-youtube-player embed,.video-seo-youtube-player iframe,.video-seo-youtube-player object{position:absolute;top:0;right:0;width:100%;height:100%;z-index:100;background:transparent}.wp-block-embed__wrapper.video-seo-youtube-embed-wrapper:before{display:none!important}.video-seo-youtube-player .video-seo-youtube-picture{display:block;margin:auto;max-width:100%;width:100%;position:absolute;left:0;right:0;top:0;bottom:0;border:none;height:auto;cursor:pointer}.video-seo-youtube-player .video-seo-youtube-picture img{width:100%}.video-seo-youtube-picture-replaced-srcset img{margin-top:-8.7%}@media screen and (max-width:800px){.video-seo-youtube-player .video-seo-youtube-picture img{margin-top:-8.7%}}.video-seo-youtube-embed-loader:focus,.video-seo-youtube-embed-loader:hover{-webkit-filter:brightness(75%)}.video-seo-youtube-player .video-seo-youtube-player-play{height:72px;width:72px;right:50%;top:50%;margin-right:-36px;margin-top:-36px;position:absolute;background:url(../../assets/play-button.png) no-repeat;cursor:pointer} \ No newline at end of file diff --git a/css/dist/videoseo-youtube-embed.css b/css/dist/videoseo-youtube-embed.css new file mode 100644 index 0000000..ae1d17d --- /dev/null +++ b/css/dist/videoseo-youtube-embed.css @@ -0,0 +1 @@ +.video-seo-youtube-player{position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;margin:0;background:transparent}.video-seo-youtube-player .video-seo-youtube-embed-loader,.video-seo-youtube-player embed,.video-seo-youtube-player iframe,.video-seo-youtube-player object{position:absolute;top:0;left:0;width:100%;height:100%;z-index:100;background:transparent}.wp-block-embed__wrapper.video-seo-youtube-embed-wrapper:before{display:none!important}.video-seo-youtube-player .video-seo-youtube-picture{display:block;margin:auto;max-width:100%;width:100%;position:absolute;right:0;left:0;top:0;bottom:0;border:none;height:auto;cursor:pointer}.video-seo-youtube-player .video-seo-youtube-picture img{width:100%}.video-seo-youtube-picture-replaced-srcset img{margin-top:-8.7%}@media screen and (max-width:800px){.video-seo-youtube-player .video-seo-youtube-picture img{margin-top:-8.7%}}.video-seo-youtube-embed-loader:focus,.video-seo-youtube-embed-loader:hover{-webkit-filter:brightness(75%)}.video-seo-youtube-player .video-seo-youtube-player-play{height:72px;width:72px;left:50%;top:50%;margin-left:-36px;margin-top:-36px;position:absolute;background:url(../../assets/play-button.png) no-repeat;cursor:pointer} \ No newline at end of file diff --git a/detail-retrieval/abstract-class-oembed.php b/detail-retrieval/abstract-class-oembed.php new file mode 100644 index 0000000..6776ed8 --- /dev/null +++ b/detail-retrieval/abstract-class-oembed.php @@ -0,0 +1,100 @@ + '', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * Match whether old data can be used on url rather than video id + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data + * Defaults to 'url' for this implementation. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'id' ) { + if ( $this->id_regex !== '' ) { + return parent::maybe_use_old_video_data( $match_on ); + } + else { + return parent::maybe_use_old_video_data( 'url' ); + } + } + + /** + * Check to see if this is really a video. + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) && isset( $this->decoded_response->type ) && $this->decoded_response->type === 'video' ); + } + + /** + * Set the video duration + */ + protected function set_duration() { + $this->set_duration_from_json_object(); + } + + /** + * Set the video height + */ + protected function set_height() { + $this->set_height_from_json_object(); + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + $this->set_thumbnail_loc_from_json_object(); + } + + /** + * Set the video width + */ + protected function set_width() { + $this->set_width_from_json_object(); + } + } +} diff --git a/detail-retrieval/abstract-class-wpseo-video-details.php b/detail-retrieval/abstract-class-wpseo-video-details.php new file mode 100644 index 0000000..486f9d3 --- /dev/null +++ b/detail-retrieval/abstract-class-wpseo-video-details.php @@ -0,0 +1,758 @@ +vid is currently done with empty(). + * This is fine as long as the defaults in the $vid array are all either empty strings, + * 0 integers or null values. If at any point those defaults would change, checking with + * empty() will start to cause serious issues and all those checks will need to be + * rewritten.} + * + * {@internal If you add a service, don't forget to add the class to the autoload list and + * the associated URLs to the verify_service_type() method in class-analyse-post.php. + * Oh, and adding some tests would not go amiss either.} + * + * {@internal If you remove a service, make sure you check that no other services where extending its class.} + * + * @package WordPress\Plugins\Video-seo + * @subpackage Internals + * @since 1.7.0 + * @version 1.7.0 + */ + abstract class WPSEO_Video_Details { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * This property must be set in the concrete implementation class. + * Leaving it empty will disable the standard determine_video_id_from_url() functionality. + * You can still implement your own version of this functionality by adding a + * determine_video_id_from_url() method to the concrete class. + * + * @see WPSEO_Video_Details::determine_video_id_from_url() + * + * @var string + */ + protected $id_regex = ''; + + /** + * Sprintf template to create a URL from an ID. + * + * This property must be set in the concrete implementation class. + * Leaving it empty will disable the standard determine_video_url_from_id() functionality. + * You can still implement your own version of this functionality by adding a + * determine_video_url_from_id() method to the concrete class. + * + * @see WPSEO_Video_Details::determine_video_url_from_id() + * + * @var string + */ + protected $url_template = ''; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * This property must be set in the concrete implementation class. + * Leaving 'pattern' and 'replace_key' empty will disable the get_remote_video_info() + * functionality. + * You can still implement you own version of this functionality by adding this method to + * the concrete class. + * Similarly leaving 'response_type' empty will disable the decode_remote_video_info() + * functionality. + * + * @see WPSEO_Video_Details::get_remote_video_info() + * @see WPSEO_Video_Details::decode_remote_video_info() + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => '', // Remote url pattern with one (!) %s placeholder. + 'replace_key' => '', // Key in the $vid array with which to replace the placeholder in the url. + + /* + * Expected response type for use in decoding the response... + * - should be one of the following: 'json', 'serial' or 'simplexml'; + * - if you need another type of decoding, implement your own version of + * decode_remote_video_info() in the concrete class; + * - leaving it empty will disable decoding and pass the received response + * unchanged to decoded_response. + */ + 'response_type' => '', + ]; + + /** + * In some cases there is a need for an API key + * + * @var string + */ + protected $api_key = ''; + + /** + * The details retrieved for this video. + * + * @var array + */ + protected $vid = [ + // Should/will always be set after retrieving the details. + 'id' => null, + 'url' => '', + 'type' => '', + 'player_loc' => '', + 'thumbnail_loc' => '', + 'last_fetched' => '', + + // Might be set after retrieving the details. + 'content_loc' => '', + 'duration' => 0, + 'view_count' => 0, + 'width' => 0, + 'height' => 0, + + /* + * Might come in via old_vid / update_meta method + * 'title' => '', + * 'description' => '', + * 'publication_date' => null, + * 'post_ID' => null, + * 'tag' => null, + */ + ]; + + /** + * The video array with all the data of the previous "fetch", if available. + * + * @var array + */ + protected $old_vid = []; + + /** + * Storage for response retrieved from external server upon video detail request. + * + * @var string + */ + protected $remote_response; + + /** + * Storage for the decoded version of the remote response. + * + * @var mixed + */ + protected $decoded_response; + + /** + * Storage for a SimpleXML object created from response. + * + * Only used when $remote_response['type'] has been set to 'simpleXML'. + * + * @var object + */ + protected $xml; + + /** + * Instantiate the class, main routine. + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + $vid = (array) $vid; + $this->vid = array_merge( $this->vid, array_filter( $vid ) ); + + if ( is_array( $old_vid ) && $old_vid !== [] ) { + $this->old_vid = $old_vid; + } + + if ( ! isset( $this->vid['id'] ) || empty( $this->vid['id'] ) ) { + $this->determine_video_id_from_url(); + } + + if ( ! isset( $this->vid['url'] ) || empty( $this->vid['url'] ) ) { + $this->determine_video_url_from_id(); + } + + if ( $this->maybe_use_old_video_data() === false ) { + + $this->get_remote_video_info(); + + if ( isset( $this->remote_response ) ) { + $this->decode_remote_video_info(); + } + + if ( $this->is_video_response() ) { + $this->put_video_details(); + } + + /* + * @todo - if it's not a video - should we reset the $vid array ? or maybe add a key + * 'video' => false, so we can avoid checking the item again? + */ + } + } + + /** + * Get the enriched video details without empties + * + * @return array + */ + public function get_details() { + return array_filter( $this->vid ); + } + + /** + * Retrieve the video id from a known video url based on a regex match + * + * @uses WPSEO_Video_Details::$id_regex + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) && $this->id_regex !== '' ) { + if ( preg_match( $this->id_regex, $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + } + } + + /** + * Create a video url based on a known video id and url template + * + * @uses WPSEO_Video_Details::$url_template + * + * @return void + */ + protected function determine_video_url_from_id() { + if ( ! empty( $this->vid['id'] ) && $this->url_template !== '' ) { + $this->vid['url'] = sprintf( $this->url_template, rawurlencode( $this->vid['id'] ) ); + } + } + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * @since 0.1 + * + * @todo Big check on whether this works properly in this new implementation !!! + * What about overwritting a title with an empty title ? Probably can't be done this way, so probably + * needs alternative solution! + * + * @todo [JRF -> Yoast] The remote info may change over time (most notably view count), should we maybe + * set a cache time for this kind of data ? Only retrieve once a month ? Once every six months ? + * Cache check could be done by adding a $this->vid['remote_retrieve_date'] key and checking against that + * + * @todo [JRF/Yoast] Re-visit this method - what about if we have improved the retrieval methods ? + * (like we have) - shouldn't we also check that either a content_loc or player_loc has been set and if not, + * try a remote call anyway ? + * After all, it's not as if video retrieval is done *that* often, only on post/page/term save when a + * video has been found in that item and on manual request for re-index. + * So it shouldn't really slow people down. + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data + * Defaults to 'id'. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'id' ) { + if ( ( is_array( $this->old_vid ) && $this->old_vid !== [] ) && ( ( isset( $this->old_vid[ $match_on ] ) && isset( $this->vid[ $match_on ] ) ) && $this->vid[ $match_on ] === $this->old_vid[ $match_on ] ) ) { + + // Filter out any empty values so as to not overwrite a real value with an empty. + $this->vid = array_merge( array_filter( $this->old_vid ), array_filter( $this->vid ) ); + + return true; + } + + return false; + } + + /** + * Retrieve information on a video via a remote API call + * + * @uses WPSEO_Video_Details::$remote_url + * + * @return void|string + */ + protected function get_remote_video_info() { + if ( ( is_string( $this->remote_url['pattern'] ) && $this->remote_url['pattern'] !== '' ) + && ( ( is_string( $this->vid[ $this->remote_url['replace_key'] ] ) + || is_int( $this->vid[ $this->remote_url['replace_key'] ] ) ) + && ! empty( $this->vid[ $this->remote_url['replace_key'] ] ) ) + ) { + + $replace_key = $this->vid[ $this->remote_url['replace_key'] ]; + // Fix protocol-less urls in parameters as the remote get call most often will not work with them. + if ( $this->remote_url['replace_key'] === 'url' && strpos( $this->vid['url'], '//' ) === 0 ) { + $replace_key = 'http:' . $this->vid['url']; + } + + $url = sprintf( $this->remote_url['pattern'], $replace_key, $this->api_key ); + $url = $this->url_encode( $url ); + + $response = $this->remote_get( $url ); + if ( is_string( $response ) && $response !== '' && $response !== 'null' ) { + $this->remote_response = $response; + } + + // Only needed for child classes to catch the response and handle it differently. + return $response; + } + } + + /** + * Wrapper for the WordPress internal wp_remote_get function, making sure a proper user-agent is sent along. + * + * @since 0.1 + * + * @param string $url The URL to retrieve. + * @param string[] $headers Optional headers to send. + * + * @return string[]|bool Returns the body of the post when successful, false when unsuccessful + */ + protected function remote_get( $url, $headers = [] ) { + // Fix protocol-less urls as the remote get call will not work with them (mainly needed for wistia frame source). + if ( strpos( $url, '//' ) === 0 ) { + $url = 'http:' . $url; + } + + $response = wp_remote_get( + $url, + [ + 'redirection' => 1, + 'httpversion' => '1.1', + 'user-agent' => 'WordPress Video SEO plugin ' . WPSEO_VERSION . '; WordPress (' . home_url( '/' ) . ')', + 'timeout' => 15, + 'headers' => $headers, + ] + ); + + if ( ! is_wp_error( $response ) && $response['response']['code'] === 200 && isset( $response['body'] ) ) { + return $response['body']; + } + + return false; + } + + /** + * Decode a remote response for a number of typical response types + * + * @uses WPSEO_Video_Details::$remote_url + * + * @return void + */ + protected function decode_remote_video_info() { + if ( ( ! empty( $this->remote_url['response_type'] ) && is_string( $this->remote_url['response_type'] ) ) && ! empty( $this->remote_response ) ) { + + switch ( $this->remote_url['response_type'] ) { + + case 'json': + $this->decode_as_json(); + break; + + case 'serial': + $this->decode_as_serialized(); + break; + + case 'simplexml': + $this->decode_as_simplexml(); + break; + } + } + else { + $this->decoded_response = $this->remote_response; + } + } + + /** + * Decode a remote response as json + */ + protected function decode_as_json() { + $response = json_decode( $this->remote_response ); + if ( is_object( $response ) ) { + $this->decoded_response = $response; + } + } + + /** + * Decode a remote response as serialized + */ + protected function decode_as_serialized() { + $response = unserialize( $this->remote_response ); + if ( $response !== false ) { + $this->decoded_response = $response; + } + } + + /** + * Decode a remote response as simpleXML + */ + protected function decode_as_simplexml() { + $this->xml = new SimpleXMLElement( $this->remote_response ); + $response = $this->xml->channel->item->children( 'http://search.yahoo.com/mrss/' ); + if ( is_object( $response ) && ! empty( $response ) ) { + $this->decoded_response = $response; + } + } + + /** + * Check to see if this is really a video. + * Meant to be overloaded from child classes. Defaults to true. + * + * @return bool + */ + protected function is_video_response() { + return true; + } + + /** + * Set video details to their new values + * + * The actual setting is done via methods in the concrete classes. + * + * @return void + */ + protected function put_video_details() { + // {@internal Keep set_id() first, if it does need changing, it needs to be done before anything else.} + if ( method_exists( $this, 'set_id' ) ) { + $this->set_id(); + } + $this->set_type(); + $this->set_player_loc(); + + if ( method_exists( $this, 'set_duration' ) ) { + $this->set_duration(); + } + if ( method_exists( $this, 'set_view_count' ) ) { + $this->set_view_count(); + } + if ( method_exists( $this, 'set_content_loc' ) ) { + $this->set_content_loc(); + } + if ( method_exists( $this, 'set_width' ) ) { + $this->set_width(); + } + if ( method_exists( $this, 'set_height' ) ) { + $this->set_height(); + } + if ( method_exists( $this, 'set_last_fetched' ) ) { + $this->set_last_fetched(); + } + + /* + * Only override the thumbnail if it hasn't been set already. + * This is in contrast to all the other methods, where the info retrieved from the remote + * service is leading. For the thumbnail, the user preference is leading. + */ + if ( empty( $this->vid['thumbnail_loc'] ) ) { + $this->set_thumbnail_loc(); + } + + /* + * Add protocol if the resulting player_loc URL would be protocol-less to prevent invalid sitemaps. + * Default to http as not all video services support https. + */ + if ( isset( $this->vid['player_loc'] ) && strpos( $this->vid['player_loc'], '//' ) === 0 ) { + $this->vid['player_loc'] = 'http:' . $this->vid['player_loc']; + } + } + + /** + * Set the player location + */ + abstract protected function set_player_loc(); + + /** + * Set the thumbnail location + */ + abstract protected function set_thumbnail_loc(); + + /** + * Set the video type based on the concrete class name + */ + protected function set_type() { + $type = str_ireplace( 'WPSEO_Video_Details_', '', static::class, $count ); + if ( $count === 1 ) { + $this->vid['type'] = strtolower( $type ); + } + } + + /* ********* HELPER METHODS ******** */ + + /** + * Set the video duration based on a typical json response + * + * @uses WPSEO_Video_Details::$decoded_response + */ + protected function set_duration_from_json_object() { + if ( ! empty( $this->decoded_response->duration ) ) { + $this->vid['duration'] = $this->decoded_response->duration; + } + } + + /** + * Set the video height based on a typical json response + * + * @uses WPSEO_Video_Details::$decoded_response + */ + protected function set_height_from_json_object() { + if ( ! empty( $this->decoded_response->height ) ) { + $this->vid['height'] = $this->decoded_response->height; + } + } + + /** + * Set the video thumbnail url based on a typical json response + * + * @uses WPSEO_Video_Details::$decoded_response + */ + protected function set_thumbnail_loc_from_json_object() { + if ( ! empty( $this->decoded_response->thumbnail_url ) ) { + $image = $this->make_image_local( $this->decoded_response->thumbnail_url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video width based on a typical json response + * + * @uses WPSEO_Video_Details::$decoded_response + */ + protected function set_width_from_json_object() { + if ( ! empty( $this->decoded_response->width ) ) { + $this->vid['width'] = $this->decoded_response->width; + } + } + + /** + * Downloads an externally hosted thumbnail image to the local server + * + * @since 0.1 + * + * @todo - revisit this whole function, a lot depends on $vid['id'] being set, while it might very well not be + * + * @todo - also: why not check whether the url is already local (not just an attachment) before doing anything else ? + * + * @param string $url The remote URL of the image. + * @param string $ext Extension to use for the image, optional. + * + * @return bool|string The link to the now locally hosted image. + */ + protected function make_image_local( $url, $ext = '' ) { + + $vid = $this->vid; + + // Remove query parameters from the URL. + $url = strtok( $url, '?' ); + + if ( isset( $vid['post_id'] ) ) { + $att = get_posts( + [ + 'numberposts' => 1, + 'post_type' => 'attachment', + 'meta_key' => 'wpseo_video_id', + 'meta_value' => isset( $vid['id'] ) ? $vid['id'] : '', + 'post_parent' => $vid['post_id'], + 'fields' => 'ids', + ] + ); + + if ( is_array( $att ) && count( $att ) > 0 ) { + $img = wp_get_attachment_image_src( $att[0], 'full' ); + + if ( $img ) { + if ( strpos( $img[0], 'http' ) !== 0 ) { + return get_site_url( null, $img[0] ); + } + + return $img[0]; + } + } + } + + /* + * Disable wp smush.it to speed up the process. + * @todo - should this filter maybe be added back at the end ? If so we need to test whether it existed and + * only add it back if it did. + */ + remove_filter( 'wp_generate_attachment_metadata', 'wp_smushit_resize_from_meta_data' ); + + $tmp = download_url( $url ); + + if ( ! is_wp_error( $tmp ) ) { + + if ( preg_match( '`[^\?]+\.(' . WPSEO_Video_Sitemap::$image_ext_pattern . ')$`i', $url, $matches ) ) { + $ext = $matches[1]; + } + + if ( ( ! isset( $vid['title'] ) || empty( $vid['title'] ) ) && isset( $vid['post_id'] ) ) { + $vid['title'] = get_the_title( $vid['post_id'] ); + } + else { + $vid['title'] = strtolower( $vid['id'] ); + } + $title = sanitize_title( strtolower( $vid['title'] ) ); + + $file_array = [ + 'name' => sanitize_file_name( preg_replace( '`[^a-z0-9\s_-]`i', '', $title ) ) . '.' . $ext, + 'tmp_name' => $tmp, + ]; + + if ( isset( $vid['post_id'] ) && ! defined( 'WPSEO_VIDEO_NO_ATTACHMENTS' ) ) { + + $ret = media_handle_sideload( $file_array, $vid['post_id'], 'Video thumbnail for ' . $vid['type'] . ' video ' . $vid['title'] ); + if ( is_wp_error( $ret ) ) { + @unlink( $tmp ); + return false; + } + + if ( isset( $vid['id'] ) ) { + update_post_meta( $ret, 'wpseo_video_id', $vid['id'] ); + } + + $img = wp_get_attachment_image_src( $ret, 'full' ); + + if ( $img ) { + // Try and prevent relative paths to images. + if ( strpos( $img[0], 'http' ) !== 0 ) { + $img = get_site_url( null, $img[0] ); + } + else { + $img = $img[0]; + } + + return $img; + } + } + else { + $file = wp_handle_sideload( $file_array, [ 'test_form' => false ] ); + + if ( ! isset( $file['error'] ) ) { + return $file['url']; + } + else { + @unlink( $file ); + } + } + + return false; + } + } + + /** + * Encode a url according to the specs + * + * Based on a function by Lucas Gonze -- lucas@gonze.com + * 07-Jan-2005 07:01 + * http://nl2.php.net/manual/nl/function.rawurlencode.php + * + * @param string $url URL to be encoded (or part thereof). + * + * @return string Correctly encoded url + */ + protected function url_encode( $url ) { + + $defaults = [ + 'scheme' => '', + 'pass' => '', + 'user' => '', + 'port' => '', + 'host' => '', + 'path' => '', + 'query' => '', + 'fragment' => '', + ]; + + $parsed_url = WPSEO_Video_Analyse_Post::wp_parse_url( $url ); + $parsed_url = array_merge( $defaults, $parsed_url ); + + if ( empty( $parsed_url['scheme'] ) === false ) { + $parsed_url['scheme'] .= '://'; + } + + if ( empty( $parsed_url['pass'] ) === false && empty( $parsed_url['user'] ) === false ) { + $parsed_url['user'] = rawurlencode( $parsed_url['user'] ) . ':'; + $parsed_url['pass'] = rawurlencode( $parsed_url['pass'] ) . '@'; + } + elseif ( empty( $parsed_url['user'] ) === false ) { + $parsed_url['user'] .= '@'; + } + + if ( empty( $parsed_url['port'] ) === false && empty( $parsed_url['host'] ) === false ) { + $parsed_url['host'] .= ':'; + } + + if ( empty( $parsed_url['path'] ) === false ) { + $arr = preg_split( '`([/;=])`', $parsed_url['path'], -1, PREG_SPLIT_DELIM_CAPTURE ); + $path = ''; + foreach ( $arr as $var ) { + switch ( $var ) { + case '/': + case ';': + case '=': + $path .= $var; + break; + + default: + $path .= rawurlencode( $var ); + break; + } + } + // Legacy patch for servers that need a literal /~username. + $parsed_url['path'] = str_replace( '/%7E', '/~', $path ); + unset( $path ); + } + + if ( empty( $parsed_url['query'] ) === false ) { + $arr = preg_split( '`([&=])`', $parsed_url['query'], -1, PREG_SPLIT_DELIM_CAPTURE ); + $query = '?'; + foreach ( $arr as $var ) { + if ( $var === '&' || $var === '=' ) { + $query .= $var; + } + else { + $query .= urlencode( $var ); + } + } + $parsed_url['query'] = $query; + unset( $query ); + } + + if ( empty( $parsed_url['fragment'] ) === false ) { + $parsed_url['fragment'] = '#' . urlencode( $parsed_url['fragment'] ); + } + + $encoded_url = $parsed_url['scheme']; + $encoded_url .= $parsed_url['user']; + $encoded_url .= $parsed_url['pass']; + $encoded_url .= $parsed_url['host']; + $encoded_url .= $parsed_url['port']; + $encoded_url .= $parsed_url['path']; + $encoded_url .= $parsed_url['query']; + $encoded_url .= $parsed_url['fragment']; + + return $encoded_url; + } + } +} diff --git a/detail-retrieval/class-23video.php b/detail-retrieval/class-23video.php new file mode 100644 index 0000000..3858d14 --- /dev/null +++ b/detail-retrieval/class-23video.php @@ -0,0 +1,269 @@ + 'http://videos.23video.com/api/photo/list?format=json&photo_id=%s&video_p=1', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Alternate remote URL for when the video ID is unknown. + * + * @var string[] + */ + protected $alternate_remote = [ + 'pattern' => 'http://videos.23video.com/api/photo/list?format=json&search=%s&video_p=1', + 'replace_key' => 'permalink', + 'response_type' => 'json', + ]; + + /** + * Instantiate the class, main routine. + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + // @todo Deal with custom domains. + parent::__construct( $vid, $old_vid ); + } + + /** + * Retrieve the video id or the permalink from a known video url based on a regex match + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 2 ) { + if ( ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) && $this->id_regex !== '' ) { + if ( preg_match( $this->id_regex, $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + $this->vid['subdomain'] = $match[1]; + } + elseif ( preg_match( $this->permalink_regex, $this->vid['url'], $match ) ) { + $this->vid['permalink'] = $match[ $match_nr ]; + $this->vid['subdomain'] = $match[1]; + } + } + } + + /** + * Retrieve information on a video via a remote API call + */ + protected function get_remote_video_info() { + if ( empty( $this->vid['id'] ) && ! empty( $this->vid['permalink'] ) ) { + $this->remote_url = $this->alternate_remote; + } + if ( ! empty( $this->vid['subdomain'] ) ) { + $replace = '://' . $this->vid['subdomain'] . '.'; + $this->remote_url['pattern'] = str_replace( '://videos.', $replace, $this->remote_url['pattern'] ); + } + parent::get_remote_video_info(); + } + + /** + * Decode a remote response for a number of typical response types + */ + protected function decode_remote_video_info() { + if ( ! empty( $this->remote_response ) ) { + // Get rid of the 'var visual = ' string before the actual json output. + $this->remote_response = substr( $this->remote_response, strpos( $this->remote_response, '{' ) ); + } + parent::decode_remote_video_info(); + } + + /** + * Check to see if this is really a video and for the right item. + * + * @return bool + */ + protected function is_video_response() { + $valid = false; + + if ( ! empty( $this->decoded_response ) ) { + + // Check whether we received a valid response based on how we retrieved it. + switch ( $this->remote_url['replace_key'] ) { + + case 'id': + if ( ! empty( $this->decoded_response->photo->photo_id ) && $this->decoded_response->photo->photo_id === $this->vid['id'] ) { + $valid = true; + } + break; + + case 'permalink': + if ( ! empty( $this->decoded_response->photo->one ) && $this->decoded_response->photo->one === '/' . $this->vid['permalink'] ) { + $valid = true; + } + elseif ( isset( $this->decoded_response->photos ) && is_array( $this->decoded_response->photos ) && $this->decoded_response->photos !== [] ) { + // Walk through the (first page of the) search results and see if we can find a match. + foreach ( $this->decoded_response->photos as $photo ) { + if ( $photo->one === '/' . $this->vid['permalink'] ) { + $this->decoded_response->photo = $photo; + $valid = true; + break; + } + } + } + break; + } + } + return $valid; + } + + /** + * Set video details to their new values + */ + protected function put_video_details() { + $this->set_subdomain(); + parent::put_video_details(); + } + + /** + * Set the content location + * + * {@internal If this is changed to another property, the width/height properties need to change too. + * Alternative set could be video_medium_download / video_medium_width / video_medium_height.} + */ + protected function set_content_loc() { + if ( ! empty( $this->decoded_response->photo->standard_download ) && ! empty( $this->vid['subdomain'] ) ) { + // Extension-less, could add .mp4, should work most of the time. + $this->vid['content_loc'] = 'http://' . rawurlencode( $this->vid['subdomain'] ) . '.23video.com' . $this->decoded_response->photo->standard_download; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response->photo->video_length ) ) { + $this->vid['duration'] = ( $this->decoded_response->photo->video_length ); + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response->photo->standard_height ) ) { + $this->vid['height'] = $this->decoded_response->photo->standard_height; + } + } + + /** + * Set the video id (as it might not be set - permalink based retrieval) + */ + protected function set_id() { + if ( ! empty( $this->decoded_response->photo->photo_id ) ) { + $this->vid['id'] = $this->decoded_response->photo->photo_id; + } + } + + /** + * Set the player location + * + * {@internal Alternative options: + * https://[subdomain].23video.com/v.swf?photo_id=[photo->photo_id]&autoPlay=1 + * https://[subdomain].23video.com/[photo->tree_id].ihtml?photo_id=[photo->photo_id]&token=[photo->token]&autoPlay=1&defaultQuality=high } + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) && ! empty( $this->vid['subdomain'] ) ) { + $this->vid['player_loc'] = 'http://' . rawurlencode( $this->vid['subdomain'] ) . '.23video.com/v.ihtml/player.html?source=share&photo_id=' . urlencode( $this->vid['id'] ) . '&autoPlay=0'; + } + } + + /** + * Verify and set the subdomain + */ + protected function set_subdomain() { + if ( ! empty( $this->decoded_response->site->domain ) && $this->decoded_response->site->domain !== $this->vid['subdomain'] . '.23video.com' ) { + $this->vid['subdomain'] = str_replace( '.23video.com', '', $this->decoded_response->site->domain ); + } + } + + /** + * Set the thumbnail location + * + * {@internal Possible alternative: + * https://[subdomain].23video.com/[photo->tree_id]/[photo->photo_id]/[photo->token]/large } + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response->photo->video_frames_download ) && ( is_string( $this->decoded_response->photo->video_frames_download ) && $this->decoded_response->photo->video_frames_download !== '' ) && ! empty( $this->vid['subdomain'] ) ) { + $url = 'http://' . rawurlencode( $this->vid['subdomain'] ) . '.23video.com' . $this->decoded_response->photo->video_frames_download; + $image = $this->make_image_local( $url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->photo->view_count ) ) { + $this->vid['view_count'] = $this->decoded_response->photo->view_count; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response->photo->standard_width ) ) { + $this->vid['width'] = $this->decoded_response->photo->standard_width; + } + } + } +} diff --git a/detail-retrieval/class-animoto.php b/detail-retrieval/class-animoto.php new file mode 100644 index 0000000..e980d6a --- /dev/null +++ b/detail-retrieval/class-animoto.php @@ -0,0 +1,105 @@ +", + * "provider_name":"Animoto", + * "thumbnail_width":648, + * "icon_url":"https://s3.amazonaws.com/s3-p.animoto.com/Video/JzwsBn5FRVxS0qoqcBP5zA/cover_432x240.jpg", + * "author_name":"Chris Korhonen", + * "type":"video", + * "width":640, + * "video_url":"https://d150hyw1dtprld.cloudfront.net/swf/w.swf?w=swf/production/vp1&e=1406063431&f=JzwsBn5FRVxS0qoqcBP5zA&d=0&m=p&r=240p&i=m&ct=&cu=&asset_domain=s3-p.animoto.com&animoto_domain=animoto.com&options=", + * "version":1.0, + * "thumbnail_url":"https://s3.amazonaws.com/s3-p.animoto.com/Video/JzwsBn5FRVxS0qoqcBP5zA/cover_432x240.jpg", + * "icon_height":54, + * "description":"", + * "height":360, + * "cache_age":{}, + * "icon_width":54 + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Animoto' ) ) { + + /** + * Class WPSEO_Video_Details_Animoto + * + * {@internal Animoto doesn't provide duration in the oembed API, unfortunately.} + */ + class WPSEO_Video_Details_Animoto extends WPSEO_Video_Details_Oembed { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`animoto\.com/play/(.+)$`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'https://animoto.com/play/%s'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://animoto.com/services/oembed?format=json&url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Instantiate the class + * + * Adjust the video url before passing off to the parent constructor + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + if ( ! empty( $vid['url'] ) ) { + if ( preg_match( '`http://static\.animoto\.com/swf/.*?&f=([^&]+)`', $vid['url'], $match ) ) { + $vid['url'] = sprintf( $this->url_template, rawurlencode( $match[1] ) ); + } + } + parent::__construct( $vid, $old_vid ); + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response->video_url ) ) { + $this->vid['player_loc'] = $this->decoded_response->video_url; + } + } + } +} diff --git a/detail-retrieval/class-archiveorg.php b/detail-retrieval/class-archiveorg.php new file mode 100644 index 0000000..bfe4a73 --- /dev/null +++ b/detail-retrieval/class-archiveorg.php @@ -0,0 +1,238 @@ + 'https://archive.org/details/%s?output=json', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * The file from the files array which contains most data we need. + * + * @var object + */ + private $video_file; + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) && ( ( isset( $this->decoded_response->metadata->mediatype[0] ) && $this->decoded_response->metadata->mediatype[0] === 'movies' ) || ( isset( $this->decoded_response->misc->css ) && $this->decoded_response->misc->css === 'movies' ) ) ); + } + + /** + * Set video details to their new values + */ + protected function put_video_details() { + $this->get_video_file_data(); + parent::put_video_details(); + } + + /** + * Determine which file in the files array contains the information we need. + */ + protected function get_video_file_data() { + $video_files = []; + + // Get the video files from the files object. + if ( ! empty( $this->decoded_response->files ) ) { + foreach ( $this->decoded_response->files as $key => $value ) { + if ( preg_match( '`\.(' . WPSEO_Video_Sitemap::$video_ext_pattern . ')$`', $key, $match ) ) { + $video_files[ $match[1] ] = $value; + + // Strip off the '/' at the start. + $video_files[ $match[1] ]->file_name = substr( $key, 1 ); + } + } + } + + // Find a file with enriched data. + if ( $video_files !== [] ) { + // Preferred extensions (sort of) in order of preference. + $video_exts = explode( '|', WPSEO_Video_Sitemap::$video_ext_pattern ); + + foreach ( $video_exts as $ext ) { + if ( isset( $video_files[ $ext ] ) ) { + if ( ! isset( $this->video_file ) ) { + // Set to the file with the first matched (most preferred) extension. + $this->video_file = $video_files[ $ext ]; + + if ( ( ! empty( $video_files[ $ext ]->length ) || ! empty( $this->decoded_response->metadata->runtime[0] ) ) && ( ! empty( $video_files[ $ext ]->height ) || ! empty( $this->decoded_response->metadata->width[0] ) ) && ( ! empty( $video_files[ $ext ]->width ) || ! empty( $this->decoded_response->metadata->height[0] ) ) ) { + // We got all the data we need. + break; + } + } + else { + if ( empty( $this->video_file->length ) && ! empty( $video_files[ $ext ]->length ) ) { + $this->video_file->length = $video_files[ $ext ]->length; + } + + if ( empty( $this->video_file->width ) && empty( $this->video_file->height ) ) { + if ( ! empty( $video_files[ $ext ]->width ) ) { + $this->video_file->width = $video_files[ $ext ]->width; + } + if ( ! empty( $video_files[ $ext ]->height ) ) { + $this->video_file->height = $video_files[ $ext ]->height; + } + } + + if ( ! empty( $this->video_file->length ) && ( ! empty( $this->video_file->width ) || ! empty( $this->video_file->height ) ) ) { + // We have as much data as we can have. + break; + } + } + } + } + } + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->vid['id'] ) && ! empty( $this->video_file->file_name ) ) { + $this->vid['content_loc'] = sprintf( + 'https://archive.org/download/%s/%s', + rawurlencode( $this->vid['id'] ), + rawurlencode( $this->video_file->file_name ) + ); + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response->metadata->runtime[0] ) ) { + // 31 seconds + $this->vid['duration'] = str_replace( ' seconds', '', $this->decoded_response->metadata->runtime[0] ); + } + elseif ( ! empty( $this->video_file->length ) ) { + $this->vid['duration'] = $this->video_file->length; + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response->metadata->height[0] ) ) { + $this->vid['height'] = $this->decoded_response->metadata->height[0]; + } + elseif ( ! empty( $this->video_file->height ) ) { + $this->vid['height'] = $this->video_file->height; + } + } + + /** + * Set the video id + */ + protected function set_id() { + if ( ! empty( $this->decoded_response->metadata->identifier[0] ) ) { + $this->vid['id'] = $this->decoded_response->metadata->identifier[0]; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'https://archive.org/embed/' . rawurlencode( $this->vid['id'] ); + } + } + + /** + * Set the thumbnail location + * + * @todo decide whether the order is correct - should the thumb permalink be tried first or the misc image ? + */ + protected function set_thumbnail_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $image_url = sprintf( 'https://archive.org/download/%s/format=Thumbnail', $this->vid['id'] ); + $image = $this->make_image_local( $image_url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + elseif ( isset( $this->decoded_response->misc->image ) && ( is_string( $this->decoded_response->misc->image ) && $this->decoded_response->misc->image !== '' ) ) { + $image = $this->make_image_local( $this->decoded_response->misc->image ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + } + + /** + * Set the video view count + * + * @todo [JRF -> Yoast] is using the download count acceptable here ? + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->item->downloads ) ) { + $this->vid['view_count'] = $this->decoded_response->item->downloads; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response->metadata->width[0] ) ) { + $this->vid['width'] = $this->decoded_response->metadata->width[0]; + } + elseif ( ! empty( $this->video_file->width ) ) { + $this->vid['width'] = $this->video_file->width; + } + } + } +} diff --git a/detail-retrieval/class-blip.php b/detail-retrieval/class-blip.php new file mode 100644 index 0000000..8c81706 --- /dev/null +++ b/detail-retrieval/class-blip.php @@ -0,0 +1,264 @@ +", + * "type":"video", + * "title":"Nostalgia Critic: Sailor Moon" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Blip' ) ) { + + /** + * Class WPSEO_Video_Details_Blip + * + * {@internal Blip videos have an embedLookup in the format 'gbk7g5S1HwI' and a numeric ID. + * These two have no discernable relation to each other. Detail lookup can be done + * via two distinct methods, for one the embedLookup is usable, for the other the ID.} + */ + class WPSEO_Video_Details_Blip extends WPSEO_Video_Details_Oembed { + + /** + * Different remote information retrieval sets to be used depending on the information available. + * + * @var array + */ + private $remotes = [ + 'rss' => [ + 'pattern' => 'http://blip.tv/rss/view/%s', + 'replace_key' => 'id', + 'response_type' => '', + ], + 'oembed' => [ + 'pattern' => 'http://blip.tv/oembed/?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ], + ]; + + /** + * OEmbed or RSS. + * + * @var string + */ + private $retrieve_method = ''; + + /** + * Instantiate the class and determine which remote retrieval method we can use before + * passing of to the parent constructor. + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + if ( isset( $vid['url'] ) ) { + $this->retrieve_method = 'oembed'; + } + elseif ( isset( $vid['embedlookup'] ) ) { + $vid['url'] = 'http://blip.tv/play/' . $vid['embedlookup']; + $this->retrieve_method = 'oembed'; + } + elseif ( isset( $vid['id'] ) ) { + $this->retrieve_method = 'rss'; + } + + if ( isset( $this->remotes[ $this->retrieve_method ] ) ) { + $this->remote_url = $this->remotes[ $this->retrieve_method ]; + } + + parent::__construct( $vid, $old_vid ); + } + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + $valid = false; + switch ( $this->retrieve_method ) { + case 'oembed': + $valid = parent::is_video_response(); + break; + + case 'rss': + // No way from the base rss info to determine whether it is video, but most info should not match anyway if it isn't. + if ( ! empty( $this->decoded_response ) ) { + $valid = true; + } + break; + } + return $valid; + } + + /** + * Set video details to their new values + */ + protected function put_video_details() { + $this->set_embedlookup(); + parent::put_video_details(); + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( $this->retrieve_method === 'rss' && preg_match( '``', $this->decoded_response, $match ) ) { + $this->vid['content_loc'] = $match[1]; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + switch ( $this->retrieve_method ) { + case 'oembed': + parent::set_duration(); + break; + + case 'rss': + if ( preg_match( '`(\d+)`', $this->decoded_response, $match ) ) { + $this->vid['duration'] = $match[1]; + } + break; + } + } + + /** + * Grab the embedlookup so we can use the faster oembed method next time. + */ + protected function set_embedlookup() { + switch ( $this->retrieve_method ) { + case 'oembed': + // Do this for oembed too as the embedlookup is used for the player loc. + if ( empty( $this->vid['embedlookup'] ) && ! empty( $this->decoded_response->html ) ) { + $this->decoded_response->html = stripslashes( $this->decoded_response->html ); + if ( preg_match( '`src="http://blip\.tv/play/([A-Za-z0-9-]{5,})(?:"|[&%\./])`i', $this->decoded_response->html, $match ) ) { + $this->vid['embedlookup'] = $match[1]; + } + } + break; + + case 'rss': + if ( preg_match( '`([A-Za-z0-9-]{5,})`', $this->decoded_response, $match ) ) { + $this->vid['embedlookup'] = $match[1]; + } + break; + } + } + + /** + * Set the video height + */ + protected function set_height() { + switch ( $this->retrieve_method ) { + case 'oembed': + parent::set_height(); + break; + + case 'rss': + if ( preg_match( '`decoded_response, $match ) ) { + $this->vid['height'] = $match[1]; + } + break; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + switch ( $this->retrieve_method ) { + case 'oembed': + if ( ! empty( $this->vid['embedlookup'] ) ) { + // @todo [JRF => Yoast] Review if this could be the correct player loc + $this->vid['player_loc'] = 'http://a.blip.tv/api.swf#' . urlencode( $this->vid['embedlookup'] ); + } + break; + + case 'rss': + if ( preg_match( '``', $this->decoded_response, $match ) ) { + $this->vid['player_loc'] = $match[1]; + } + break; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + switch ( $this->retrieve_method ) { + case 'oembed': + parent::set_thumbnail_loc(); + break; + + case 'rss': + if ( preg_match( '``', $this->decoded_response, $match ) ) { + $image = $this->make_image_local( $match[1] ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + break; + } + } + + /** + * Set the video type + * + * @todo - chould this be changed to blip ? or does that impact something else ? + */ + protected function set_type() { + $this->vid['type'] = 'blip.tv'; + } + + /** + * Set the video width + */ + protected function set_width() { + switch ( $this->retrieve_method ) { + case 'oembed': + parent::set_width(); + break; + + case 'rss': + if ( preg_match( '`decoded_response, $match ) ) { + $this->vid['width'] = $match[1]; + } + break; + } + } + } +} diff --git a/detail-retrieval/class-brightcove.php b/detail-retrieval/class-brightcove.php new file mode 100644 index 0000000..8ca76b5 --- /dev/null +++ b/detail-retrieval/class-brightcove.php @@ -0,0 +1,264 @@ + 'http://api.brightcove.com/services/library?command=find_video_by_id&video_id=%s&video_fields=name,playsTotal,videoStillURL,thumbnailURL,length,FLVURL,videoFullLength&media_delivery=http', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Brightcove token. + * + * @var string + */ + private $bc_token; + + /** + * Instantiate the class + * + * Retrieve the Brightcove token and only pass of to the parent constructor if we find one + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + // Grab Brightcove api key from wp_options. + $this->bc_token = get_option( 'bc_api_key' ); + + if ( ! empty( $this->bc_token ) ) { + $this->remote_url['pattern'] .= '&token=' . $this->bc_token; + + // Set the class properties before the parent constructor so they're available to maybe_use_old. + $vid = (array) $vid; + $this->vid = array_merge( $this->vid, array_filter( $vid ) ); + + if ( is_array( $old_vid ) && $old_vid !== [] ) { + $this->old_vid = $old_vid; + } + + // Bail out as early as possible to avoid extra API call. + $this->maybe_use_old_video_data(); + parent::__construct( $this->vid, $this->old_vid ); + } + else { + // @todo [JRF -> Yoast] Why not use (merge with) oldvid data here if available ? The api key might be removed, but old data might still be better than none. + $this->vid = $vid; + } + } + + /** + * Retrieve the video id based on a known video url via an external API call. + * + * @param int $match_nr Not used in this implementation. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) { + $parse = WPSEO_Video_Analyse_Post::wp_parse_url( $this->vid['url'] ); + $query_vars = []; + + if ( ! empty( $parse['query'] ) ) { + + parse_str( $parse['query'], $query_vars ); + + if ( isset( $query_vars['vidID'] ) && ( is_string( $query_vars['vidID'] ) && $query_vars['vidID'] !== '' ) ) { + $this->vid['id'] = $query_vars['ID']; + } + elseif ( isset( $query_vars['playerID'] ) && ( is_string( $query_vars['playerID'] ) && $query_vars['playerID'] !== '' ) ) { + $this->vid['player_id'] = $query_vars['playerID']; + } + + // Player id is given which means this is a playlist so grab the first video from the playlist. + if ( isset( $this->vid['player_id'] ) && $this->vid['player_id'] ) { + $this->determine_video_id_from_playlist(); + } + } + } + } + + /** + * Retrieve the video id of the first video of a playlist via an external API call. + * + * @return void + */ + private function determine_video_id_from_playlist() { + $url = 'http://api.brightcove.com/services/library?command=find_playlists_for_player_id&player_id=%s&video_fields=id&token=%s'; + $url = sprintf( $url, $this->vid['player_id'], $this->bc_token ); + $url = $this->url_encode( $url ); + + $response = $this->remote_get( $url ); + if ( is_string( $response ) && $response !== 'null' ) { + $decoded_response = json_decode( $response ); + + if ( is_object( $decoded_response ) && ! isset( $decoded_response->error ) ) { + if ( isset( $decoded_response->items[0]->videoIds[0] ) && ( is_string( $decoded_response->items[0]->videoIds[0] ) && $decoded_response->items[0]->videoIds[0] !== '' ) ) { + $this->vid['id'] = $decoded_response->items[0]->videoIds[0]; + } + } + } + } + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * Match whether old data can be used on video id or on video url if id is not available + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data + * Defaults to 'url' for this implementation. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'url' ) { + if ( ( isset( $this->old_vid['id'] ) && isset( $this->vid['id'] ) ) && $this->old_vid['id'] === $this->vid['id'] ) { + $match_on = 'id'; + } + return parent::maybe_use_old_video_data( $match_on ); + } + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + return ( isset( $this->decoded_response ) && ( is_object( $this->decoded_response ) && ! isset( $this->decoded_response->error ) ) ); + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->decoded_response->FLVURL ) ) { + $this->vid['content_loc'] = $this->decoded_response->FLVURL; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response->length ) && $this->decoded_response->length > 0 ) { + $this->vid['duration'] = ( $this->decoded_response->length / 1000 ); + } + elseif ( ! empty( $this->decoded_response->videoFullLength->videoDuration ) && $this->decoded_response->videoFullLength->videoDuration > 0 ) { + $this->vid['duration'] = ( $this->decoded_response->videoFullLength->videoDuration / 1000 ); + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response->videoFullLength->frameHeight ) ) { + $this->vid['height'] = $this->decoded_response->videoFullLength->frameHeight; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + /* + * @todo - find out what the player_loc should be - this method is set by (nearly) + * every other video class, so why not in this one ? + */ + return; + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response->videoStillURL ) && is_string( $this->decoded_response->videoStillURL ) && $this->decoded_response->videoStillURL !== '' ) { + $image = $this->make_image_local( $this->decoded_response->videoStillURL ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + elseif ( isset( $this->decoded_response->thumbnailURL ) && is_string( $this->decoded_response->thumbnailURL ) && $this->decoded_response->thumbnailURL !== '' ) { + $image = $this->make_image_local( $this->decoded_response->thumbnailURL ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->playsTotal ) ) { + $this->vid['view_count'] = $this->decoded_response->playsTotal; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response->videoFullLength->frameWidth ) ) { + $this->vid['width'] = $this->decoded_response->videoFullLength->frameWidth; + } + } + } +} diff --git a/detail-retrieval/class-cincopa.php b/detail-retrieval/class-cincopa.php new file mode 100644 index 0000000..1e06a75 --- /dev/null +++ b/detail-retrieval/class-cincopa.php @@ -0,0 +1,147 @@ + 'http://www.cincopa.com/media-platform/runtime/rss200.aspx?fid=%s', + 'replace_key' => 'id', + 'response_type' => 'simplexml', + ]; + + /** + * Retrieve the video id from a known video url based on parsing the url and a regex match. + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( isset( $this->vid['url'] ) && ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) && $this->id_regex !== '' ) { + $parse = WPSEO_Video_Analyse_Post::wp_parse_url( $this->vid['url'] ); + if ( isset( $parse['query'] ) && preg_match( $this->id_regex, $parse['query'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + $url = $this->decoded_response->content->attributes()->url; + if ( ! empty( $url ) ) { + $this->vid['player_loc'] = (string) $url; + } + } + + /** + * Set the thumbnail location + * + * @todo thumbnails are not working currently b/c $this->make_image_local() strips query parameters + * and this video service needs query params to generate thumbnails, look for a more direct approach + */ + protected function set_thumbnail_loc() { + $url = $this->decoded_response->content->attributes()->url; + if ( ! empty( $url ) ) { + $image = $this->make_image_local( (string) $url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + } +} + +// phpcs:disable -- API response format documentation doesn't need to comply with CS. + +/* + * RSS response format [2014/7/22]: + + + http://www.cincopa.com/cmp/start.aspx?fid=10464405!25bdfbaa-bc56-4242-a77b-5d53f55faf4b!Zrt-FdXYq8927VNWnam5dB + + National Geographic wpplugin site + + + + Praesent Feugiat + Praesent feugiat nulla at lectus lacinia auctor. Ut sed lacus. Sed cursus, metus non ornare mollis, justo tortor mattis turpis, eu malesuada lectus est id elit. Vivamus posuere pulvinar massa. + + + http://www.cincopa.com/cmp/start.aspx?fid=10464405!25bdfbaa-bc56-4242-a77b-5d53f55faf4b!Zrt-FdXYq8927VNWnam5dB + + + + .... + + + + * + * Example JSON response format [2014/7/22]: + * ( + * "", + * { + * "items": [ + * { + * "id":"47860448", + * "description":"Praesent feugiat nulla at lectus lacinia auctor. Ut sed lacus. Sed cursus, metus non ornare mollis, justo tortor mattis turpis, eu malesuada lectus est id elit. Vivamus posuere pulvinar massa.", + * "link":"http://www.cincopa.com/cmp/start.aspx?fid=10464405!25bdfbaa-bc56-4242-a77b-5d53f55faf4b!Zrt-FdXYq8927VNWnam5dB", + * "aspect_ratio":"1.33", + * "title":"Praesent Feugiat", + * "storage":"71", + * "content_url":"http://ec10.cdn.cincopa.com/1024x768_15.jpg?o=1\u0026amp;res=152\u0026amp;h2=3j4booooj1vsp43hmzqdiu1u40matybn\u0026amp;cdn=ec\u0026amp;p=y\u0026amp;pid=66267\u0026amp;ph3=kcal5kmi4g2351lhmj5s5i0jqt0cqxwv\u0026amp;d=AsDA7AAFBAAAVy6nAYbOIDO\u0026amp;as=mp3", + * "thumbnail_url":"http://ec10.cdn.cincopa.com/1024x768_15.jpg?o=2\u0026amp;res=152\u0026amp;h2=3j4booooj1vsp43hmzqdiu1u40matybn\u0026amp;cdn=ec\u0026amp;p=y\u0026amp;pid=66267\u0026amp;ph3=kcal5kmi4g2351lhmj5s5i0jqt0cqxwv\u0026amp;d=AsDA7AAFBAAAVy6nAYbOIDO", + * "content_type":"image/jpeg" + * }, + * { + * ... + * } + * ], + * "title":"National Geographic wpplugin site", + * "description":"" + * } + * ) + */ diff --git a/detail-retrieval/class-collegehumor.php b/detail-retrieval/class-collegehumor.php new file mode 100644 index 0000000..2d9afdc --- /dev/null +++ b/detail-retrieval/class-collegehumor.php @@ -0,0 +1,83 @@ +<\/embed><\/object>

CollegeHumor's Favorite Funny Videos<\/a><\/p><\/div>", + * "thumbnail_url":"http:\/\/0.media.collegehumor.cvcdn.com\/14\/81\/46ed8b408e8c586b0fad03ccd968aaa5.jpg", + * "thumbnail_width":"175", + * "thumbnail_height":"98" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Collegehumor' ) ) { + + /** + * Class WPSEO_Video_Details_Collegehumor + */ + class WPSEO_Video_Details_Collegehumor extends WPSEO_Video_Details_Oembed { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`[/\.]collegehumor\.com/(?:video|embed)/([0-9]+)`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * {@internal Set to embed as it gives better retrieval results compared to video!} + * + * @var string + */ + protected $url_template = 'http://www.collegehumor.com/embed/%s/'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://www.collegehumor.com/oembed.json?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Set the player location + * + * @todo - or should we parse the embed url from decoded_response->html ? + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://0.static.collegehumor.cvcdn.com/moogaloop/moogaloop.1.0.31.swf?clip_id=' . urlencode( $this->vid['id'] ) . '&fullscreen=1'; + } + } + } +} diff --git a/detail-retrieval/class-dailymotion.php b/detail-retrieval/class-dailymotion.php new file mode 100644 index 0000000..e8c0c7c --- /dev/null +++ b/detail-retrieval/class-dailymotion.php @@ -0,0 +1,102 @@ + 'https://api.dailymotion.com/video/%s?fields=duration,embed_url,thumbnail_large_url,views_total', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + return ( isset( $this->decoded_response ) && ( is_object( $this->decoded_response ) && ! isset( $this->decoded_response->error ) ) ); + } + + /** + * Set the video duration + */ + protected function set_duration() { + $this->set_duration_from_json_object(); + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response->embed_url ) ) { + $this->vid['player_loc'] = $this->decoded_response->embed_url; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response->thumbnail_large_url ) && is_string( $this->decoded_response->thumbnail_large_url ) && $this->decoded_response->thumbnail_large_url !== '' ) { + $image = $this->make_image_local( $this->decoded_response->thumbnail_large_url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->views_total ) ) { + $this->vid['view_count'] = $this->decoded_response->views_total; + } + } + } +} diff --git a/detail-retrieval/class-embedly.php b/detail-retrieval/class-embedly.php new file mode 100644 index 0000000..d7df122 --- /dev/null +++ b/detail-retrieval/class-embedly.php @@ -0,0 +1,152 @@ +", + * "author_url": "http://www.youtube.com/user/OldSpice", + * "version": "1.0", + * "provider_name": "YouTube", + * "thumbnail_url": "http://i.ytimg.com/vi/-oElH6M_5i4/hqdefault.jpg", + * "type": "video", + * "thumbnail_height": 360 + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Embedly' ) ) { + + /** + * Class WPSEO_Video_Details_Embedly + * + * Retrieve details via the embedly service. Can be used for nearly services. + */ + class WPSEO_Video_Details_Embedly extends WPSEO_Video_Details_Oembed { + + /** + * Information on the remote URL to use for retrieving the video details. + * + * Alternative url: http://api.embed.ly/v1/api/oembed + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'https://api.embed.ly/1/oembed?&url=%s&format=json', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Embedly API key from saved options. + * + * @var string + */ + protected $api_key; + + /** + * Whether or not we're still getting a response when retrieval is done without API key. + * After a few calls the IP address is cut off for a limited period if you're not using an API key. + * + * @var bool + */ + public static $functional = true; + + /** + * Instantiate the class + * + * Retrieve the Embedly API key if there is any and prevent unnecessary API calls if there + * isn't one and Embedly has started to cut off the ip address + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + if ( empty( $this->api_key ) ) { + // Grab Embedly api key if it's set. + $embedly_api_key = WPSEO_Options::get( 'video_embedly_api_key', '' ); + if ( $embedly_api_key !== '' ) { + $this->api_key = $embedly_api_key; + } + } + if ( ! empty( $this->api_key ) ) { + $this->remote_url['pattern'] .= '&key=' . $this->api_key; + } + + // Prevent further API calls if the user has been cut off by Embedly. + if ( self::$functional === true ) { + parent::__construct( $vid, $old_vid ); + } + else { + // @todo [JRF -> Yoast] Why not use (merge with) oldvid data here if available? + $this->vid = $vid; + } + } + + /** + * Retrieve information on a video via a remote API call and prevent further API calls + * if the user has been cut off. + */ + protected function get_remote_video_info() { + $response = parent::get_remote_video_info(); + if ( ! isset( $this->remote_response ) && intval( wp_remote_retrieve_response_code( $response ) ) === 403 ) { + // User has been cut off, prevent further calls from this class instance and other instances. + self::$functional = false; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response->html ) ) { + $this->decoded_response->html = stripslashes( $this->decoded_response->html ); + if ( preg_match( '` src="([^"]+)"`i', $this->decoded_response->html, $match ) ) { + $this->vid['player_loc'] = $match[1]; + } + } + } + + /** + * Set the video type + */ + protected function set_type() { + if ( ! empty( $this->decoded_response->provider_name ) ) { + // When needed, change service.com to service. + $provider = explode( '.', $this->decoded_response->provider_name ); + $this->vid['type'] = strtolower( $provider[0] ); + } + else { + parent::set_type(); + } + } + } +} diff --git a/detail-retrieval/class-evs.php b/detail-retrieval/class-evs.php new file mode 100644 index 0000000..b0cea98 --- /dev/null +++ b/detail-retrieval/class-evs.php @@ -0,0 +1,140 @@ + 'json', + ]; + + /** + * EVS location. + * + * @var string + */ + protected $evs_location; + + /** + * Instantiate the class + * + * Retrieve the EVS location and only pass of to the parent constructor if we find one + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + $this->evs_location = get_option( 'evs_location' ); + + if ( $this->evs_location ) { + parent::__construct( $vid, $old_vid ); + } + else { + /* + * @todo [JRF -> Yoast] Why not use (merge with) oldvid data here if available? + * The api key might be removed, but old data might still be better than none. + */ + $this->vid = $vid; + } + } + + /** + * Set the video id to a known video url. + * + * @param int $match_nr Not used in this implementation. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( ! empty( $this->vid['url'] ) ) { + $this->vid['id'] = $this->vid['url']; + } + } + + /** + * Retrieve information on a video via a remote API call + * + * @return void + */ + protected function get_remote_video_info() { + if ( ! empty( $this->vid['id'] ) ) { + // Retrieve evs thumbnail info. + $api = $this->evs_location . '/api.php'; + $response = wp_remote_post( + $api, + [ + 'method' => 'POST', + 'timeout' => 45, + 'redirection' => 5, + 'httpversion' => '1.0', + 'blocking' => true, + 'headers' => [], + 'cookies' => [], + 'body' => [ + 'page_url' => $this->vid['id'], + 'method' => 'public-file-images', + ], + ] + ); + + if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) ) { + $this->remote_response = $response['body']; + } + } + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->vid['url'] ) ) { + $this->vid['content_loc'] = $this->vid['url']; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['url'] ) ) { + $this->vid['player_loc'] = $this->vid['url']; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( ( isset( $this->decoded_response->success ) && $this->decoded_response->success === true ) && ! empty( $this->decoded_response->thumbnail ) ) { + $image = $this->make_image_local( $this->decoded_response->thumbnail ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + } +} diff --git a/detail-retrieval/class-flickr.php b/detail-retrieval/class-flickr.php new file mode 100644 index 0000000..6f52d29 --- /dev/null +++ b/detail-retrieval/class-flickr.php @@ -0,0 +1,354 @@ + 'https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=2d2985adb59d21e6933368e41e5ca3b0&photo_id=%s&format=json&nojsoncallback=1', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Deal with potentially wrong ids from short url format and instantiate the class + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + // Check for wrongly set short id as id. + if ( ! empty( $vid['id'] ) && ! preg_match( '`^[0-9]+$`', $vid['id'] ) ) { + $vid['short_id'] = $vid['id']; + unset( $vid['id'] ); + } + // Make sure we use the short id if it's available and there's no id. + if ( empty( $vid['id'] ) && ! empty( $vid['short_id'] ) ) { + $this->remote_url['replace_key'] = 'short_id'; + } + + parent::__construct( $vid, $old_vid ); + } + + /** + * Retrieve the video id or short id from a known video url based on a regex match + * + * @uses WPSEO_Video_Details_Flickr::$id_regex + * @uses WPSEO_Video_Details_Flickr::$short_id_regex + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) { + if ( preg_match( $this->id_regex, $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + elseif ( preg_match( $this->short_id_regex, $this->vid['url'], $match ) ) { + $this->vid['short_id'] = $match[ $match_nr ]; + $this->remote_url['replace_key'] = 'short_id'; + } + } + } + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) && isset( $this->decoded_response->photo->media ) && $this->decoded_response->photo->media === 'video' ); + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response->photo->video->duration ) ) { + $this->vid['duration'] = $this->decoded_response->photo->video->duration; + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response->photo->video->height ) ) { + $this->vid['height'] = $this->decoded_response->photo->video->height; + } + } + + /** + * Set the video id + */ + protected function set_id() { + if ( ! empty( $this->decoded_response->photo->id ) ) { + $this->vid['id'] = $this->decoded_response->photo->id; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response->photo->secret ) && ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = $this->url_encode( 'http://www.flickr.com/apps/video/stewart.swf?v=109786&intl_lang=en_us&photo_secret=' . $this->decoded_response->photo->secret . '&photo_id=' . $this->vid['id'] ); + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( ( ! empty( $this->decoded_response->photo->farm ) && ! empty( $this->decoded_response->photo->server ) ) + && ( ! empty( $this->decoded_response->photo->secret ) && ! empty( $this->vid['id'] ) ) ) { + + $url = 'http://farm' . $this->decoded_response->photo->farm . '.staticflickr.com/' + . $this->decoded_response->photo->server . '/' + . $this->vid['id'] . '_' . $this->decoded_response->photo->secret . '.jpg'; + $url = $this->url_encode( $url ); + $image = $this->make_image_local( $url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->photo->views ) ) { + $this->vid['view_count'] = $this->decoded_response->photo->views; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response->photo->video->width ) ) { + $this->vid['width'] = $this->decoded_response->photo->video->width; + } + } + } +} + +// phpcs:disable -- API response format documentation doesn't need to comply with CS. + +/* + * Full JSON response format [2014/7/22]: + * { + * "photo": + * { + * "id":"3989150138", + * "secret":"de7a31ba19", + * "server":"2624", + * "farm":3, + * "dateuploaded":"1254882213", + * "isfavorite":0, + * "license":"0", + * "safety_level":"0", + * "rotation":0, + * "originalsecret":"eb04f37e5a", + * "originalformat":"jpg", + * "owner": + * { + * "nsid":"36587311@N08", + * "username":"\u25baCubaGallery", + * "realname":"Cuba Gallery", + * "location":"Auckland, New Zealand", + * "iconserver":"3705", + * "iconfarm":4, + * "path_alias":"cubagallery" + * }, + * "title": + * { + * "_content":"Lightroom Tutorial Video" + * }, + * "description": + * { + * "_content":"Lightroom Tutorial Video: <\/b>Yes there is more on this but I'm not allowed to even whisper where it could be! :) Shh Check out my Lightroom blog for more before & after shots.<\/b> The link is on my profile page.\n\n \u25ba Follow me on Tumblr<\/b><\/a>" + * }, + * "visibility": + * { + * "ispublic":1, + * "isfriend":0, + * "isfamily":0 + * }, + * "dates": + * { + * "posted":"1254882213", + * "taken":"2009-10-06 19:23:33", + * "takengranularity":"0", + * "lastupdate":"1342897955" + * }, + * "views":"42692", + * "editability": + * { + * "cancomment":0, + * "canaddmeta":0 + * }, + * "publiceditability": + * { + * "cancomment":1, + * "canaddmeta":0 + * }, + * "usage": + * { + * "candownload":1, + * "canblog":0, + * "canprint":0, + * "canshare":1 + * }, + * "comments": + * { + * "_content":"36" + * }, + * "notes": + * { + * "note":[] + * }, + * "people": + * { + * "haspeople":0 + * }, + * "tags": + * { + * "tag":[ + * { + * "id":"36494498-3989150138-152587", + * "author":"36587311@N08", + * "authorname":"\u25baCubaGallery", + * "raw":"Lightroom", + * "_content":"lightroom", + * "machine_tag":0 + * }, + * { + * "id":"36494498-3989150138-61264", + * "author":"36587311@N08", + * "authorname":"\u25baCubaGallery", + * "raw":"Tutorial", + * "_content":"tutorial", + * "machine_tag":0 + * }, + * { + * "id":"36494498-3989150138-2546", + * "author":"36587311@N08", + * "authorname":"\u25baCubaGallery", + * "raw":"Video", + * "_content":"video", + * "machine_tag":0 + * } + * ] + * }, + * "location": + * { + * "latitude":-36.830181, + * "longitude":174.428497, + * "accuracy":"13", + * "context":"0", + * "locality": + * { + * "_content":"Muriwai", + * "place_id":"zMBe8sZUVLqzrrZ.UQ", + * "woeid":"56023026" + * }, + * "county": + * { + * "_content":"Rodney District", + * "place_id":"fBUgSc5UV7JuHDMGsA", + * "woeid":"55875887" + * }, + * "region": + * { + * "_content":"Auckland", + * "place_id":"XIVOdI5QV7pepC5JCA", + * "woeid":"15021756" + * }, + * "country":{ + * "_content":"New Zealand", + * "place_id":"X_2zAGVTUb5..jhXDw", + * "woeid":"23424916" + * }, + * "place_id":"zMBe8sZUVLqzrrZ.UQ", + * "woeid":"56023026" + * }, + * "geoperms": + * { + * "ispublic":1, + * "iscontact":0, + * "isfriend":0, + * "isfamily":0 + * }, + * "urls": + * { + * "url":[ + * { + * "type":"photopage", + * "_content":"https:\/\/www.flickr.com\/photos\/cubagallery\/3989150138\/" + * } + * ] + * }, + * "media":"video", + * "video": + * { + * "ready":1, + * "failed":0, + * "pending":0, + * "duration":"20", + * "width":"600", + * "height":"600" + * } + * }, + * "stat":"ok" + * } + */ diff --git a/detail-retrieval/class-funnyordie.php b/detail-retrieval/class-funnyordie.php new file mode 100644 index 0000000..a8b2866 --- /dev/null +++ b/detail-retrieval/class-funnyordie.php @@ -0,0 +1,105 @@ + 'http://www.funnyordie.com/oembed.json?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Instantiate the class and determine which remote retrieval method we can use before + * passing of to the parent constructor. + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + if ( isset( $vid['url'] ) ) { + // Fix it as FoD oembed does not work with embed urls. + $vid['url'] = str_replace( 'funnyordie.com/embed/', 'funnyordie.com/videos/', $vid['url'] ); + } + + parent::__construct( $vid, $old_vid ); + } + + /** + * Create a video url based on a known video id and url template + */ + protected function determine_video_url_from_id() { + if ( ( ! empty( $this->vid['id'] ) && strlen( $this->vid['id'] ) > 4 ) && $this->url_template !== '' ) { + $this->vid['url'] = sprintf( $this->url_template, $this->vid['id'] ); + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.funnyordie.com/embed/' . rawurlencode( $this->vid['id'] ); + } + } + } +} diff --git a/detail-retrieval/class-hulu.php b/detail-retrieval/class-hulu.php new file mode 100644 index 0000000..0b9f2a6 --- /dev/null +++ b/detail-retrieval/class-hulu.php @@ -0,0 +1,109 @@ + ", + * "version":"1.0", + * "large_thumbnail_width":512, + * "thumbnail_url":"http://ib.huluim.com/video/60420730?size=145x80&caller=h1o&img=i", + * "air_date":"Mon Jul 21 00:00:00 UTC 2014", + * "large_thumbnail_height":288, + * "author_name":"Comedy Central", + * "provider_name":"Hulu" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Hulu' ) ) { + + /** + * Class WPSEO_Video_Details_Hulu + */ + class WPSEO_Video_Details_Hulu extends WPSEO_Video_Details_Oembed { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`[/\.]hulu\.com/watch/([0-9]+)(?:$|[/#\?])`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'http://hulu.com/watch/%s'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://www.hulu.com/api/oembed.json?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Check if the response is for a valid video - Filter out "404s" + * + * Hulu does not provide proper 404s, but just returns the details of the first video in + * their library if passed in invalid video id in the url. + * Test for this known first video to filter out 404s. This will give an issue for someone + * embedding this very first video, but that outweights the alternative. + * + * @return bool + */ + protected function is_video_response() { + $title_404 = 'Cop in a Cage (Kojak)'; + $embed_url_404 = 'http://www.hulu.com/embed.html?eid=VY_l7Yi0kCop3y-NtMAFaA'; + + return ( ! empty( $this->decoded_response ) && ( ( ! isset( $this->decoded_response->title ) || $this->decoded_response->title !== $title_404 ) && ( ! isset( $this->decoded_response->embed_url ) || $this->decoded_response->embed_url !== $embed_url_404 ) ) ); + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response->embed_url ) ) { + $this->vid['player_loc'] = $this->decoded_response->embed_url; + } + } + } +} diff --git a/detail-retrieval/class-localfile.php b/detail-retrieval/class-localfile.php new file mode 100644 index 0000000..a0dbd4f --- /dev/null +++ b/detail-retrieval/class-localfile.php @@ -0,0 +1,522 @@ + bool : ( = false ) + * [bitrate (string)] => int : 76266 + * [bitrate_mode (string)] => string[3] : �cbr� + * [filesize (string)] => int : 388042 + * [mime_type (string)] => string[14] : �video/x-ms-wmv� + * [length (string)] => int : 34 + * [length_formatted (string)] => string[4] : �0:33� + * [width (string)] => int : 320 + * [height (string)] => int : 240 + * [fileformat (string)] => string[3] : �asf� + * [dataformat (string)] => string[3] : �wmv� + * [encoder (string)] => string[21] : �Windows Media Video 9� + * [audio (string)] => Array: + * ( + * [codec (string)] => string[21] : �Windows Media Audio 9� + * [channels (string)] => int : 1 + * [sample_rate (string)] => int : 16000 + * [bitrate (string)] => int : 17396 + * [bits_per_sample (string)] => int : 16 + * [dataformat (string)] => string[3] : �wma� + * [bitrate_mode (string)] => string[3] : �cbr� + * [lossless (string)] => bool : ( = false ) + * [encoder (string)] => string[21] : �Windows Media Audio 9� + * [encoder_options (string)] => string[32] : �16 kbps, 16 kHz, mono 1-pass CBR� + * [channelmode (string)] => string[4] : �mono� + * [compression_ratio (string)] => float : 0.067953125 + * ) + * ) + */ +if ( ! class_exists( 'WPSEO_Video_Details_Localfile' ) ) { + + /** + * Class WPSEO_Video_Details_Local_File + * + * {@internal This class works slightly different from the other detail retrieval service classes + * in that no remote call is done, but that the details are retrieved are file metadata. + * + * This also means that this class uses a few extra $vid keys to a) pass things to this + * class and b) retain the information found. + * + * The 'maybe_local' (bool) key is used to determine whether to call this class. It would + * be nicer to use 'type' = 'localfile' for this to be in line with the other detail + * retrieval classes, but that could break existing filters on types 'jwplayer', + * 'mediaelementjs' etc which have been used up to now for local files from various sources. + * + * The 'attachment_id' (int) key is used to refer to local attachment posts. + * The 'file_path' (string) key is used to remember the path to the file we determined exists. + * + * The local property $file_url always *has* to be set, of the local properties $file_path + * and $attachment_id only one or the other is expected.} + */ + class WPSEO_Video_Details_Localfile extends WPSEO_Video_Details { + + /** + * File path to a local file, which *may be* a video file (unconfirmed). + * + * @var string + */ + protected $file_path = ''; + + /** + * URL for a local file, which *may be* a video file (unconfirmed). + * + * @var string + */ + protected $file_url = ''; + + /** + * Attachment ID, which *may be* a video file (unconfirmed). + * + * @var string + */ + protected $attachment_id = 0; + + /** + * Instantiate the class, main routine. + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + if ( $this->could_be_local_video_file( $vid ) === true ) { + parent::__construct( $vid, $old_vid ); + } + else { + // @todo [JRF -> Yoast] Why not use (merge with) oldvid data here if available ? The api key might be removed, but old data might still be better than none. + $this->vid = $vid; + } + } + + /** + * Determine whether an absolute or relative url is a local file and possibly a video file. + * + * @param array $vid Currently available video info. + * + * @return bool + */ + protected function could_be_local_video_file( $vid ) { + $is_local = false; + + if ( ! empty( $vid['file_path'] ) ) { + $this->file_path = $vid['file_path']; + if ( isset( $vid['url'] ) ) { + $this->file_url = $vid['url']; + } + else { + $this->file_url = str_replace( ABSPATH, site_url( '/' ), $this->file_path ); + } + $is_local = true; + } + elseif ( ! empty( $vid['attachment_id'] ) ) { + $this->attachment_id = $vid['attachment_id']; + $is_local = true; + } + elseif ( isset( $vid['url'] ) && is_string( $vid['url'] ) && $vid['url'] !== '' ) { + $is_local = $this->is_attachment_or_local_file( $vid['url'] ); + } + + return $is_local; + } + + /** + * Try and determine if a url refers to a local file. + * + * For relative urls, this method recurses onto itself while trying to find the file with a variety + * of absolute versions of the relative url. + * + * @todo This one could do with some refactoring, but at least got it working ;-) + * + * @param string $url The url to test. + * + * @return bool + */ + private function is_attachment_or_local_file( $url ) { + static $uploads; + static $site_url; + static $network_url; + static $search; + static $extensions; + + // Set statics. + if ( ! isset( $uploads ) ) { + $uploads = wp_upload_dir(); + } + if ( ! isset( $site_url ) ) { + $site_url = preg_replace( '`^http[s]?:`', '', site_url() ); + } + if ( ! isset( $network_url ) ) { + if ( is_multisite() ) { + $network_url = preg_replace( '`^http[s]?:`', '', network_site_url() ); + } + else { + $network_url = false; + } + } + if ( ! isset( $search ) ) { + $search = [ $site_url . '/' ]; + if ( ! empty( $network_url ) ) { + $search[] = $network_url . '/'; + } + } + if ( ! isset( $extensions ) ) { + $extensions = explode( '|', WPSEO_Video_Sitemap::$video_ext_pattern ); + } + + + /** + * Absolute url + */ + if ( strpos( $url, 'http' ) === 0 || strpos( $url, '//' ) === 0 ) { + + $is_local = false; + + // Make it protocol relative so we don't have to worry about that. + $url = preg_replace( '`^http[s]?:`', '', $url ); + $url = rtrim( $url, '\/' ); + + // Is this a url on our site/network ? + if ( strpos( $url, $site_url ) === 0 || ( ! empty( $network_url ) && strpos( $url, $network_url ) === 0 ) ) { + $parsed_url = WPSEO_Video_Analyse_Post::parse_url( $url ); + + if ( $parsed_url['file'] !== '' ) { + $ext = strrchr( $parsed_url['file'], '.' ); + + if ( $ext !== false && in_array( substr( $ext, 1 ), $extensions, true ) ) { + $base_url = preg_replace( '`^http[s]?:`', '', $uploads['baseurl'] ); + if ( strpos( $url, $base_url ) === 0 ) { + $this->file_path = str_replace( $base_url, $uploads['basedir'], $url ); + } + else { + $this->file_path = str_replace( $search, ABSPATH, $url ); + } + + if ( file_exists( $this->file_path ) ) { + $this->file_url = 'http:' . $url; + $is_local = true; + } + } + elseif ( $ext === false ) { + /* + * {@internal At some point in the future we may want to switch this over + * to the attachment_url_to_postid( $url ) function which is + * introduced in WP 4.0.} + */ + $path_parts = explode( '/', trim( $parsed_url['path'], '\/' ) ); + $last_bit = array_pop( $path_parts ); + $query_arg = [ + 'post_status' => 'any', + 'post_type' => 'attachment', + 'name' => $last_bit, + ]; + $query = new WP_Query( $query_arg ); + + if ( $query->post_count === 1 ) { + $this->attachment_id = $query->post->ID; + $is_local = true; + } + else { + // Last ditch effort - can we find the file if we add an extension? + $base_url = preg_replace( '`^http[s]?:`', '', $uploads['baseurl'] ); + if ( strpos( $url, $base_url ) === 0 ) { + $file_path = str_replace( $base_url, $uploads['basedir'], $url ); + } + else { + $file_path = str_replace( $search, ABSPATH, $url ); + } + + foreach ( $extensions as $extension ) { + if ( file_exists( $file_path . '.' . $extension ) ) { + $this->file_path = $file_path . '.' . $extension; + $this->file_url = 'http:' . $url . '.' . $extension; + $is_local = true; + break; + } + } + } + } + } + elseif ( $parsed_url['query'] !== '' ) { + parse_str( $parsed_url['query'], $query ); + if ( ! empty( $query['attachment_id'] ) ) { + $post_id = $query['attachment_id']; + } + elseif ( ! empty( $query['p'] ) ) { + $post_id = $query['p']; + } + + if ( isset( $post_id ) && get_post_type( $post_id ) === 'attachment' ) { + $this->attachment_id = $post_id; + $is_local = true; + } + } + } + return $is_local; + } + else { + /** + * Relative path - try and see if we can find the absolute url + */ + if ( $this->is_attachment_or_local_file( site_url( $url ) ) === true ) { + return true; + } + elseif ( is_multisite() && $this->is_attachment_or_local_file( network_site_url( $url ) ) === true ) { + return true; + } + elseif ( $this->is_attachment_or_local_file( $uploads['baseurl'] . '/' . ltrim( $url, '\/' ) ) === true ) { + return true; + } + elseif ( $this->is_attachment_or_local_file( $uploads['url'] . '/' . ltrim( $url, '\/' ) ) === true ) { + return true; + } + elseif ( $this->is_attachment_or_local_file( content_url( $url ) ) === true ) { + return true; + } + elseif ( $this->is_attachment_or_local_file( get_stylesheet_directory_uri() . '/' . ltrim( $url, '\/' ) ) === true ) { + return true; + } + elseif ( $this->is_attachment_or_local_file( get_template_directory_uri() . '/' . ltrim( $url, '\/' ) ) === true ) { + return true; + } + elseif ( $this->is_attachment_or_local_file( plugins_url( $url ) ) === true ) { + return true; + } + + return false; + } + } + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * Match whether old data can be used on url rather than video id + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data + * Defaults to 'url' for this implementation. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'url' ) { + return parent::maybe_use_old_video_data( $match_on ); + } + + /** + * Retrieve information on a local video via GetID3. + * + * @uses WPSEO_Video_Details::$remote_url + * + * @return void|string + */ + protected function get_remote_video_info() { + $response = null; + + if ( ! empty( $this->attachment_id ) ) { + $response = wp_get_attachment_metadata( $this->attachment_id ); + if ( is_array( $response ) && $response !== [] ) { + $this->remote_response = $response; + } + } + elseif ( ! empty( $this->file_path ) ) { + if ( ! function_exists( 'wp_read_video_metadata' ) ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + } + + $response = wp_read_video_metadata( $this->file_path ); + + if ( is_array( $response ) && $response !== [] ) { + $this->remote_response = $response; + } + } + return $response; + } + + /** + * Check to see if this is really a video. + * + * @return bool + */ + protected function is_video_response() { + if ( isset( $this->decoded_response['mime_type'] ) && strpos( $this->decoded_response['mime_type'], 'video' ) !== false ) { + if ( ! empty( $this->attachment_id ) ) { + if ( empty( $this->file_url ) ) { + $this->file_url = wp_get_attachment_url( $this->attachment_id ); + } + if ( empty( $this->file_path ) ) { + $this->file_path = get_attached_file( $this->attachment_id ); + } + } + return true; + } + else { + unset( $this->vid['attachment_id'], $this->vid['file_path'] ); + return false; + } + } + + /** + * Set video details to their new values + */ + protected function put_video_details() { + // Only save the determined details to the vid array if we're sure it's a video. + $this->set_file_path(); + $this->set_file_url(); + $this->set_attachment_id(); + + parent::put_video_details(); + } + + /** + * Set the attachment id + */ + protected function set_attachment_id() { + if ( ! empty( $this->attachment_id ) ) { + $this->vid['attachment_id'] = $this->attachment_id; + } + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->file_url ) ) { + $this->vid['content_loc'] = $this->file_url; + } + } + + /** + * Set the video duration + * + * {@internal In some rare cases this may result in a video time * 1000. This is a GetID3 bug. + * The value will in that case be a string, which is why we use + * length_formatted in that case. + * + * {@link https://core.trac.wordpress.org/ticket/29176} + * Note: This was fixed in WP 4.1.} + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response['length'] ) && ! is_string( $this->decoded_response['length'] ) && $this->decoded_response['length'] > 0 ) { + $this->vid['duration'] = $this->decoded_response['length']; + } + elseif ( ! empty( $this->decoded_response['length_formatted'] ) && $this->decoded_response['length_formatted'] > 0 ) { + // The presumption is made that no videos longer than 24 hours will be posted. + $duration = 0; + $time = explode( ':', $this->decoded_response['length_formatted'] ); + if ( count( $time ) === 2 ) { + $duration += $time[1]; + $duration += ( $time[0] * MINUTE_IN_SECONDS ); + } + elseif ( count( $time ) === 3 ) { + $duration += $time[2]; + $duration += ( $time[1] * MINUTE_IN_SECONDS ); + $duration += ( $time[0] * HOUR_IN_SECONDS ); + } + + if ( $duration > 0 ) { + $this->vid['duration'] = $duration; + } + } + } + + /** + * Set the file path + */ + protected function set_file_path() { + if ( ! empty( $this->file_path ) ) { + $this->vid['file_path'] = $this->file_path; + } + } + + /** + * Set the file url + */ + protected function set_file_url() { + if ( ! empty( $this->file_url ) ) { + $this->vid['file_url'] = $this->file_url; + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response['height'] ) ) { + $this->vid['height'] = $this->decoded_response['height']; + } + } + + /** + * (Don't) Set the player location + */ + protected function set_player_loc() { + return; + } + + /** + * Set the thumbnail location - try and find a local image file for the video + */ + protected function set_thumbnail_loc() { + if ( ! empty( $this->file_path ) && ! empty( $this->file_url ) ) { + + // @todo transform from path to url. + $img_file = preg_replace( '`\.(' . WPSEO_Video_Sitemap::$video_ext_pattern . ')$`', '', $this->file_path ); + $img_url = preg_replace( '`\.(' . WPSEO_Video_Sitemap::$video_ext_pattern . ')$`', '', $this->file_url ); + + if ( file_exists( $img_file . '.jpg' ) ) { + $this->vid['thumbnail_loc'] = $img_url . '.jpg'; + } + elseif ( file_exists( $img_file . '.jpeg' ) ) { + $this->vid['thumbnail_loc'] = $img_url . '.jpeg'; + } + elseif ( file_exists( $img_file . '.png' ) ) { + $this->vid['thumbnail_loc'] = $img_url . '.png'; + } + elseif ( file_exists( $img_file . '.gif' ) ) { + $this->vid['thumbnail_loc'] = $img_url . '.gif'; + } + } + } + + /** + * (Don't) Set the video type - leave as is to prevent filters failing + */ + protected function set_type() { + return; + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response['width'] ) ) { + $this->vid['width'] = $this->decoded_response['width']; + } + } + } +} diff --git a/detail-retrieval/class-metacafe.php b/detail-retrieval/class-metacafe.php new file mode 100644 index 0000000..c805a9a --- /dev/null +++ b/detail-retrieval/class-metacafe.php @@ -0,0 +1,163 @@ + 'http://www.metacafe.com/api/item/%s/', + 'replace_key' => 'id', + 'response_type' => '', + ]; + + /** + * Check if the response is for a valid video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) && preg_match( '`[0-9]+`', $this->decoded_response ) ); + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( preg_match( '`decoded_response, $match ) ) { + $this->vid['content_loc'] = $match[1]; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( preg_match( '`duration="(\d+)"`', $this->decoded_response, $match ) ) { + $this->vid['duration'] = $match[1]; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.metacafe.com/fplayer/' . rawurlencode( $this->vid['id'] ) . '/metacafe.swf'; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( preg_match( '`decoded_response, $match ) ) { + $image = $this->make_image_local( $match[1] ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + } +} + +// phpcs:disable -- API response format documentation doesn't need to comply with CS. + +/* + * Remote response (XML) format [2014/7/22]: + * + + + Metacafe + + + http://www.metacafe.com/watch/7050424/ + + http://s.mcstatic.com/Images/MCLogo4RSS.jpg + http://www.metacafe.com + Metacafe + 65 + 229 + + + + 7050424 + SplashNews + Arnold Schwarzenegger Stretches Out + http://www.metacafe.com/watch/7050424/arnold_schwarzenegger_stretches_out/ + 3.43 + Entertainment + + Arnold Schwarzenegger Stretches Out +

+ Is Arnold Schwarzenegger getting in shape now that he's single?
Ranked 3.43 / 5 | 302 views | 0 comments
+

+

+ Click here to watch the video ()
+ Submitted By: SplashNews
+ Tags: + Arnold Schwarzenegger Maria Shriver Shape Working Out Biking Exercise Massachusetts 
+ Categories: Entertainment

+ ]]> + + http://www.metacafe.com/watch/7050424/arnold_schwarzenegger_stretches_out/ + Tue, 23 Aug 2011 17:25:02 +0000 + + + + Arnold Schwarzenegger Stretches Out + Arnold Schwarzenegger,Maria Shriver,Shape,Working Out,Biking,Exercise,Massachusetts + Is Arnold Schwarzenegger getting in shape now that he's single? + SplashNews + nonadult + + + */ diff --git a/detail-retrieval/class-muzutv.php b/detail-retrieval/class-muzutv.php new file mode 100644 index 0000000..bfd9b8e --- /dev/null +++ b/detail-retrieval/class-muzutv.php @@ -0,0 +1,188 @@ + 'http://www.muzu.tv/api/video/details/%s?muzuid=b00q0xGOTl', + 'replace_key' => 'id', + 'response_type' => 'simplexml', + ]; + + /** + * Check if the response is for a valid video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) && ( is_object( $this->xml ) && ( isset( $this->xml->channel->item->description ) && (string) $this->xml->channel->item->description !== 'Invalid video' ) ) ); + } + + /** + * Set the video duration + */ + protected function set_duration() { + $duration = $this->decoded_response->content->attributes()->duration; + if ( ! empty( $duration ) ) { + $this->vid['duration'] = (string) $duration; + } + } + + /** + * Set the video height + */ + protected function set_height() { + $height = $this->decoded_response->content->attributes()->height; + if ( ! empty( $height ) ) { + $this->vid['height'] = (string) $height; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'https://player.muzu.tv/player/getPlayer/i/293053/vidId=' . urlencode( $this->vid['id'] ) . '&autostart=y&dswf=y'; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( ! empty( $this->xml->channel->image->url ) ) { + $image = $this->make_image_local( (string) $this->xml->channel->image->url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + $views = $this->xml->channel->item->children( 'http://www.muzu.tv/schemas/muzu/1.0' )->video->info->attributes()->views; + if ( ! empty( $views ) ) { + $this->vid['view_count'] = (string) $views; + } + } + + /** + * Set the video width + */ + protected function set_width() { + $width = $this->decoded_response->content->attributes()->width; + if ( ! empty( $width ) ) { + // @todo Why cast to string ? Int would be more logical + $this->vid['width'] = (string) $width; + } + } + } +} + +// phpcs:disable -- API response format documentation doesn't need to comply with CS. + +/* + * Remote response (XML) format [2014/7/22]: + * + + + + Sean Paul, Beenie Man - Greatest Gallis + http://www.muzu.tv/sean-paul-beenie-man/greatest-gallis-music-video/1847016/ + + + http://static.muzu.tv/media/images/001/847/016/001/1847016-thb3.jpg + Sean Paul, Beenie Man - Greatest Gallis + http://www.muzu.tv/sean-paul-beenie-man/greatest-gallis-music-video/1847016/ + + en-gb + + + <![CDATA[Greatest Gallis]]> + + http://www.muzu.tv/sean-paul-beenie-man/greatest-gallis-music-video/1847016/ + MUZU:1847016 + Tue, 23 Apr 2013 00:00:00 0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nonadult + DE DK US ES GB FR FI AT AR AU PT BR CA BE SE CH CO MX NL NZ NO IE IT + + ]]> + + + + + */ diff --git a/detail-retrieval/class-revision3.php b/detail-retrieval/class-revision3.php new file mode 100644 index 0000000..ed8a02d --- /dev/null +++ b/detail-retrieval/class-revision3.php @@ -0,0 +1,84 @@ +" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Revision3' ) ) { + + /** + * Class WPSEO_Video_Details_Revision3 + */ + class WPSEO_Video_Details_Revision3 extends WPSEO_Video_Details_Oembed { + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://revision3.com/api/oembed/?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Set the video id + */ + protected function set_id() { + if ( ! empty( $this->decoded_response->html ) && preg_match( '`[&\?]videoId=([0-9]+)`', $this->decoded_response->html, $match ) ) { + $this->vid['id'] = $match[1]; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://embed.revision3.com/player/embed?videoId=' . urlencode( $this->vid['id'] ) . '&external=true'; + } + } + } +} diff --git a/detail-retrieval/class-screencast.php b/detail-retrieval/class-screencast.php new file mode 100644 index 0000000..49b7f31 --- /dev/null +++ b/detail-retrieval/class-screencast.php @@ -0,0 +1,154 @@ +vid['url']['url'] ) && is_string( $this->vid['url']['url'] ) && $this->vid['url']['url'] !== '' ) && $this->id_regex !== '' ) { + if ( preg_match( $this->id_regex, $this->vid['url']['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + } + } + + /** + * Retrieve information on a video via a remote API call. + * + * Currently implemented to use already existing information. + * + * @return void + */ + protected function get_remote_video_info() { + if ( is_array( $this->vid['url'] ) && isset( $this->vid['url']['embed'] ) ) { + $this->remote_response = $this->vid['url']['embed']; + } + } + + /** + * Decode a remote response as DOMXPath object + * + * @uses WPSEO_Video_Details::$remote_url + * + * @return void + */ + protected function decode_remote_video_info() { + if ( ! empty( $this->remote_response ) && ( extension_loaded( 'dom' ) && class_exists( 'DOMXPath' ) ) ) { + $dom = new DOMDocument(); + + // The loadHTML() method will throw an error on malformed HTML. + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @$dom->loadHTML( $this->remote_response ); + $xpath = new DOMXPath( $dom ); + + $item = $xpath->query( '//object/param[@name="flashVars"]' ); + if ( $item instanceof DOMNodeList && $item->length > 0 ) { + $item = $item->item( 0 )->getAttribute( 'value' ); + parse_str( $item, $response ); + if ( is_array( $response ) && $response !== [] ) { + $this->decoded_response = $response; + } + } + } + + if ( ! isset( $this->decoded_response ) ) { + $this->decoded_response = $this->remote_response; + } + } + + /** + * Check if the response is for a valid video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) ); + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response['containerheight'] ) ) { + $this->vid['height'] = $this->decoded_response['containerheight']; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response['content'] ) ) { + $this->vid['player_loc'] = $this->decoded_response['content']; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response['thumb'] ) && is_string( $this->decoded_response['thumb'] ) && $this->decoded_response['thumb'] !== '' ) { + $image = $this->make_image_local( $this->decoded_response['thumb'] ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response['containerwidth'] ) ) { + $this->vid['width'] = $this->decoded_response['containerwidth']; + } + } + } +} diff --git a/detail-retrieval/class-screenr.php b/detail-retrieval/class-screenr.php new file mode 100644 index 0000000..9d62174 --- /dev/null +++ b/detail-retrieval/class-screenr.php @@ -0,0 +1,78 @@ +", + * "thumbnail_url":"https://az21792.vo.msecnd.net/images/e9938535-f19e-4a7a-bcea-6dcf0bf33d70_thumb.jpg" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Screenr' ) ) { + + /** + * Class WPSEO_Video_Details_Screenr + */ + class WPSEO_Video_Details_Screenr extends WPSEO_Video_Details_Oembed { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`[/\.]screenr\.com/(?:embed/)?([a-z0-9-]+)(?:$|[#\?])`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'http://screenr.com/%s'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://www.screenr.com/api/oembed.json?url=http://screenr.com/%s', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.screenr.com/embed/' . rawurlencode( $this->vid['id'] ); + } + } + } +} diff --git a/detail-retrieval/class-snotr.php b/detail-retrieval/class-snotr.php new file mode 100644 index 0000000..8e56b29 --- /dev/null +++ b/detail-retrieval/class-snotr.php @@ -0,0 +1,81 @@ +", + * "provider_name": "Snotr.com", + * "thumbnail_url": "http://cdn.videos.snotr.com/13751-large.jpg", + * "type": "video", + * "thumbnail_height": 135 + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Snotr' ) ) { + + /** + * Class WPSEO_Video_Details_Snotr + * + * Retrieve details via the Embedly service. + */ + class WPSEO_Video_Details_Snotr extends WPSEO_Video_Details_Embedly { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`[/\.]snotr\.com/(?:video|embed)/([0-9]+)(?:$|[/#\?])`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'http://snotr.com/video/%s'; + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.snotr.com/embed/' . rawurlencode( $this->vid['id'] ); + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://cdn.videos.snotr.com/' . rawurlencode( $this->vid['id'] ) . '-large.jpg'; + } + } + } +} diff --git a/detail-retrieval/class-spike.php b/detail-retrieval/class-spike.php new file mode 100644 index 0000000..9b55f09 --- /dev/null +++ b/detail-retrieval/class-spike.php @@ -0,0 +1,70 @@ +", + * "version": "1.0", + * "provider_name": "Spike", + * "thumbnail_url": "http://2.images.spike.com/images/shows/cops/methbust600072114.jpg?quality=0.91", + * "type": "video", + * "thumbnail_height": 347 + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Spike' ) ) { + + /** + * Class WPSEO_Video_Details_Spike + * + * Retrieve details via the Embedly service. + */ + class WPSEO_Video_Details_Spike extends WPSEO_Video_Details_Embedly { + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'http://www.spike.com/video-clips/%s/'; + + /** + * Set the player location + * http://media.mtvnservices.com/embed/mgid:arc:video:spike.com:27d5de4f-35f8-4565-8267-255ca62e3534.swf + */ + protected function set_player_loc() { + if ( ! empty( $this->decoded_response->html ) ) { + $this->decoded_response->html = stripslashes( $this->decoded_response->html ); + if ( preg_match( '` src="[^\?]+\?(?:[^&]+&)*?src=([^&]+)&`i', $this->decoded_response->html, $match ) ) { + $this->vid['player_loc'] = rawurldecode( $match[1] ); + } + } + } + } +} diff --git a/detail-retrieval/class-ted.php b/detail-retrieval/class-ted.php new file mode 100644 index 0000000..648e983 --- /dev/null +++ b/detail-retrieval/class-ted.php @@ -0,0 +1,188 @@ +<\/iframe>", + * "width":560, + * "height":315, + * "title":"Jill Bolte Taylor: My stroke of insight", + * "url":"http:\/\/www.ted.com\/talks\/jill_bolte_taylor_s_powerful_stroke_of_insight", + * "author_name":"Jill Bolte Taylor", + * "author_url":"http:\/\/www.ted.com\/speakers\/jill_bolte_taylor", + * "provider_name":"TED", + * "provider_url":"http:\/\/ted.com", + * "thumbnail_url":"http:\/\/images.ted.com\/images\/ted\/e86e4fdeedbff174a70b8e80f6c3ebe12b9e9cfa_480x360.jpg?lang=en", + * "thumbnail_width":"480", + * "thumbnail_height":"360" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Ted' ) ) { + + /** + * Class WPSEO_Video_Details_Ted + */ + class WPSEO_Video_Details_Ted extends WPSEO_Video_Details_Oembed { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`[/\.]ted\.com/talks/([a-z0-9_-]+)(?:$|\.html|[/#\?])`i'; + + /** + * Regular expression to retrieve a numeric video ID from a known video short URL. + * + * @var string + */ + protected $short_id_regex = '`[/\.]ted\.com/talks/view/id/(.+?)(?:$|/)`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'http://www.ted.com/talks/%s.html'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $short_id_url_template = 'http://www.ted.com/talks/view/id/%s'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://www.ted.com/talks/oembed.json?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Deal with potentially wrong ids from short id url format and instantiate the class + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + // Check for wrongly set short id as id. + if ( ! empty( $vid['id'] ) && preg_match( '`^[0-9]+$`', $vid['id'] ) ) { + $vid['short_id'] = $vid['id']; + unset( $vid['id'] ); + } + parent::__construct( $vid, $old_vid ); + } + + /** + * Retrieve the video id or short id from a known video url based on a regex match + * + * @uses WPSEO_Video_Details_Ted::$id_regex + * @uses WPSEO_Video_Details_Ted::$short_id_regex + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + $this->determine_id_from_url( $this->vid['url'] ); + } + + /** + * Retrieve the video id or short id from a known video url based on a regex match + * + * @uses WPSEO_Video_Details_Ted::$id_regex + * @uses WPSEO_Video_Details_Ted::$short_id_regex + * + * @param string $url The video url. + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + private function determine_id_from_url( $url, $match_nr = 1 ) { + if ( is_string( $url ) && $url !== '' ) { + // Check for short id form first as the normal regex would match on the 'view' bit. + if ( preg_match( $this->short_id_regex, $url, $match ) ) { + $this->vid['short_id'] = $match[ $match_nr ]; + } + elseif ( preg_match( $this->id_regex, $url, $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + } + } + + /** + * Create a video url based on a known video id and url template + * + * @uses WPSEO_Video_Details_Ted::$url_template + * @uses WPSEO_Video_Details_Ted::$short_id_url_template + * + * @return void + */ + protected function determine_video_url_from_id() { + if ( ! empty( $this->vid['id'] ) && $this->url_template !== '' ) { + $this->vid['url'] = sprintf( $this->url_template, $this->vid['id'] ); + } + elseif ( ! empty( $this->vid['short_id'] ) && $this->short_id_url_template !== '' ) { + $this->vid['url'] = sprintf( $this->short_id_url_template, $this->vid['short_id'] ); + } + } + + /** + * Set the player location + * + * @todo - verify if this is the correct setting for content_loc + */ + protected function set_content_loc() { + if ( ! empty( $this->decoded_response->url ) ) { + $this->vid['content_loc'] = $this->decoded_response->url; + } + } + + /** + * Set the video id + */ + protected function set_id() { + if ( ! empty( $this->decoded_response->url ) ) { + $this->determine_id_from_url( $this->decoded_response->url ); + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://embed.ted.com/talks/' . rawurlencode( $this->vid['id'] ) . '.html'; + } + } + } +} diff --git a/detail-retrieval/class-ustudio.php b/detail-retrieval/class-ustudio.php new file mode 100644 index 0000000..f4738b8 --- /dev/null +++ b/detail-retrieval/class-ustudio.php @@ -0,0 +1,184 @@ + 'https://app.ustudio.com/embed/%s/config.json', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * First video in the response. + * + * @var array|false + */ + private $first_video; + + /** + * Information on the largest transcode. + * + * @var array|false + */ + private $transcode; + + /** + * Set video details to their new values + * + * The actual setting is done via methods in the concrete classes. + * + * @return void + */ + protected function put_video_details() { + $this->first_video = $this->get_decoded_video(); + + if ( is_array( $this->first_video ) ) { + $this->transcode = $this->get_transcode(); + parent::put_video_details(); + } + } + + /** + * Set the player location. + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'https://app.ustudio.com/embed/' . $this->vid['id']; + } + } + + /** + * Pull the first video from the details, if exists. + * + * @return array|false + */ + protected function get_decoded_video() { + if ( ! empty( $this->decoded_response->videos ) ) { + return $this->decoded_response->videos[0]; + } + return false; + } + + /** + * Pull the largest (widest) transcode, preferably mp4. + * + * @return array|false + */ + protected function get_transcode() { + $transcode = false; + if ( ! empty( $this->first_video->transcodes ) ) { + foreach ( $this->first_video->transcodes as $format => $items ) { + foreach ( $items as $item ) { + $item->format = $format; + if ( ! $transcode ) { + // For starters, use the first transcode we find. + $transcode = $item; + } + elseif ( $format === 'mp4' && $transcode->format !== 'mp4' ) { + // If item is mp4 and best transcode isn't, use the mp4. + $transcode = $item; + } + elseif ( $item->width > $transcode->width ) { + // If item is wider, use it. + $transcode = $item; + } + } + } + } + return $transcode; + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( ! empty( $this->first_video->images ) ) { + foreach ( $this->first_video->images as $image ) { + if ( $image->type === 'poster' ) { + $local_img = $this->make_image_local( $image->image_url ); + if ( is_string( $local_img ) && $local_img !== '' ) { + $this->vid['thumbnail_loc'] = $local_img; + return; + } + } + } + } + } + + /** + * Set the duration + */ + protected function set_duration() { + if ( ! empty( $this->first_video->duration ) && is_numeric( $this->first_video->duration ) ) { + $this->vid['duration'] = $this->first_video->duration; + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->transcode->height ) && is_numeric( $this->transcode->height ) ) { + $this->vid['height'] = $this->transcode->height; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->transcode->width ) && is_numeric( $this->transcode->width ) ) { + $this->vid['width'] = $this->transcode->width; + } + } + + /** + * Set the location of the content + */ + protected function set_content_loc() { + if ( ! empty( $this->transcode->url ) ) { + $this->vid['content_loc'] = $this->transcode->url; + } + } + } +} diff --git a/detail-retrieval/class-veoh.php b/detail-retrieval/class-veoh.php new file mode 100644 index 0000000..eb2a99a --- /dev/null +++ b/detail-retrieval/class-veoh.php @@ -0,0 +1,62 @@ +vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.veoh.com/veohplayer.swf?permalinkId=' . urlencode( $this->vid['id'] ); + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $url = $this->url_encode( 'http://ll-images.veoh.com/media/w300/thumb-' . $this->vid['id'] . '-1.jpg' ); + $image = $this->make_image_local( $url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + } +} diff --git a/detail-retrieval/class-viddler.php b/detail-retrieval/class-viddler.php new file mode 100644 index 0000000..f10a27f --- /dev/null +++ b/detail-retrieval/class-viddler.php @@ -0,0 +1,355 @@ + 'http://api.viddler.com/api/v2/viddler.videos.getDetails.php?key=0118093f713643444556524f452f&add_embed_code=1&video_id=%s', + 'replace_key' => 'id', + 'response_type' => 'serial', + ]; + + /** + * The file from the files array which contains the data we need. + * + * @var array + */ + private $video_file = []; + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * Match whether old data can be used on video id or on video url if id is not available + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data. + * Defaults to 'url' for this implementation. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'url' ) { + if ( ( isset( $this->old_vid['id'] ) && isset( $this->vid['id'] ) ) && $this->old_vid['id'] == $this->vid['id'] ) { + $match_on = 'id'; + } + return parent::maybe_use_old_video_data( $match_on ); + } + + /** + * Retrieve information on a video via a remote API call + * + * Change the $remote_url parameters if id is not available, before passing off to the parent + * + * @return void + */ + protected function get_remote_video_info() { + if ( empty( $this->vid['id'] ) && ! empty( $this->vid['url'] ) ) { + $this->remote_url['pattern'] = str_replace( '&video_id=%s', '&url=%s', $this->remote_url['pattern'] ); + $this->remote_url['replace_key'] = 'url'; + } + + parent::get_remote_video_info(); + } + + /** + * Check if the response is for a valid video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response['video'] ) && is_array( $this->decoded_response['video'] ) ); + } + + /** + * Set video details to their new values (mostly by passing off to the parent method) + */ + protected function put_video_details() { + $this->get_video_file_data(); + parent::put_video_details(); + } + + /** + * Retrieve the file we want to use from the remote response + */ + protected function get_video_file_data() { + if ( isset( $this->decoded_response['video']['files'] ) && is_array( $this->decoded_response['video']['files'] ) && $this->decoded_response['video']['files'] !== [] ) { + foreach ( $this->decoded_response['video']['files'] as $file ) { + if ( ( isset( $file['ext'] ) && $file['ext'] === 'mp4' ) && ( isset( $file['status'] ) && $file['status'] === 'ready' && isset( $file['url'] ) ) && ( is_string( $file['url'] ) && $file['url'] !== '' ) ) { + $this->video_file = $file; + break; + } + } + } + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( isset( $this->video_file['url'] ) && ( is_string( $this->video_file['url'] ) && $this->video_file['url'] !== '' ) ) { + $this->vid['content_loc'] = $this->video_file['url']; + } + // @todo needs checking if this gives a valid return value, but don't have enough sample data + elseif ( ! empty( $this->decoded_response['video']['html5_video_source'] ) ) { + $this->vid['content_loc'] = $this->decoded_response['video']['html5_video_source']; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response['video']['length'] ) ) { + $this->vid['duration'] = $this->decoded_response['video']['length']; + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->video_file['height'] ) ) { + $this->vid['height'] = $this->video_file['height']; + } + elseif ( ! empty( $this->decoded_response['video']['embed_code'] ) && preg_match( '`<(?:iframe|video).*? height="([0-9]+)"`', $this->decoded_response['video']['embed_code'], $match ) ) { + $this->vid['height'] = $match[1]; + } + } + + /** + * (Re-)Set the video id + */ + protected function set_id() { + if ( ! empty( $this->decoded_response['video']['id'] ) ) { + $this->vid['id'] = $this->decoded_response['video']['id']; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.viddler.com/player/' . rawurlencode( $this->vid['id'] ) . '/'; + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response['video']['thumbnail_url'] ) && is_string( $this->decoded_response['video']['thumbnail_url'] ) && $this->decoded_response['video']['thumbnail_url'] !== '' ) { + $image = $this->make_image_local( $this->decoded_response['video']['thumbnail_url'] ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response['video']['view_count'] ) ) { + $this->vid['view_count'] = $this->decoded_response['video']['view_count']; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->video_file['width'] ) ) { + $this->vid['width'] = $this->video_file['width']; + } + elseif ( ! empty( $this->decoded_response['video']['embed_code'] ) && preg_match( '`<(?:iframe|video).*? width="([0-9]+)"`', $this->decoded_response['video']['embed_code'], $match ) ) { + $this->vid['width'] = $match[1]; + } + } + } +} + +// phpcs:disable -- API response format documentation doesn't need to comply with CS. + +/* + * Remote response (unserialized version of serialized response) format [2014/7/22]: + * +Array +( + [video] => Array + ( + [id] => 1646c55 + [status] => ready + [author] => cdevroe + [title] => iPhone macro lens demonstration + [upload_time] => 1206114091 + [updated_at] => 1206103291 + [made_public_time] => 1206104030 + [length] => 245 + [description] => This video has a better description on my site. + [age_limit] => + [url] => http://www.viddler.com/v/1646c55 + [thumbnail_url] => http://thumbs.cdn-ec.viddler.com/thumbnail_2_1646c55_v1.jpg + [thumbnail_version] => v1 + [permalink] => http://www.viddler.com/v/1646c55 + [html5_video_source] => http://www.viddler.com/file/1646c55/html5 + [view_count] => 8482 + [impression_count] => 24042 + [favorite] => 0 + [comment_count] => 21 + [tags] => Array + ( + [0] => Array + ( + [type] => global + [text] => Colin Devroe + ) + + [1] => Array + ( + [type] => global + [text] => handmade + ) + + [2] => Array + ( + [type] => global + [text] => demo + ) + + [3] => Array + ( + [type] => global + [text] => apple + ) + + [4] => Array + ( + [type] => global + [text] => mobile + ) + + [5] => Array + ( + [type] => global + [text] => diy + ) + + [6] => Array + ( + [type] => global + [text] => iphone + ) + + [7] => Array + ( + [type] => global + [text] => timed + ) + + [8] => Array + ( + [type] => global + [text] => macro + ) + + [9] => Array + ( + [type] => global + [text] => lens + ) + + [10] => Array + ( + [type] => global + [text] => photography + ) + + [11] => Array + ( + [type] => timed + [text] => lens + [offset] => 11390 + [thumbnail_url] => http://thumbs.cdn-ec.viddler.com/tagthumbnail_2_718102594727e1ee.jpg + ) + + [12] => Array + ( + [type] => timed + [text] => 66 + [offset] => 1870 + [thumbnail_url] => http://thumbs.cdn-ec.viddler.com/tagthumbnail_2_7186005b4123e1ee.jpg + ) + + [13] => Array + ( + [type] => timed + [text] => iphone + [offset] => 11390 + [thumbnail_url] => http://thumbs.cdn-ec.viddler.com/tagthumbnail_2_718102594725e1ee.jpg + ) + + ) + + [embed_code] => + [player_type] => Array + ( + [player_type_id] => 1 + [player_type] => full + ) + + [display_aspect_ratio] => 16:9 + [closed_captioning_list] => Array + ( + ) + + ) + +) + */ diff --git a/detail-retrieval/class-videojug.php b/detail-retrieval/class-videojug.php new file mode 100644 index 0000000..4ffeb6b --- /dev/null +++ b/detail-retrieval/class-videojug.php @@ -0,0 +1,94 @@ + 'http://www.videojug.com/oembed.json?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * (Re-)Set the video id + */ + protected function set_id() { + if ( ! empty( $this->decoded_response->id ) ) { + $this->vid['id'] = $this->decoded_response->id; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'http://www.videojug.com/embed/' . rawurlencode( $this->vid['id'] ); + } + } + } +} diff --git a/detail-retrieval/class-videopress.php b/detail-retrieval/class-videopress.php new file mode 100644 index 0000000..f4a7d17 --- /dev/null +++ b/detail-retrieval/class-videopress.php @@ -0,0 +1,160 @@ + 'https://v.wordpress.com/data/wordpress.json?guid=%s&domain=', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Instantiate the class + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + // Pre-adjust the remote url. + $host = WPSEO_Video_Analyse_Post::wp_parse_url( home_url(), PHP_URL_HOST ); + $this->remote_url['pattern'] .= $host; + + parent::__construct( $vid, $old_vid ); + } + + /** + * Check if the response is for a valid video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response->mp4 ) ); + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->decoded_response->mp4->url ) ) { + $this->vid['content_loc'] = $this->decoded_response->mp4->url; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + $this->set_duration_from_json_object(); + } + + /** + * Set the video height + */ + protected function set_height() { + $this->set_height_from_json_object(); + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + // @todo: check - original had & encoded as & - is this necessary ? Shouldn't we only encode in output context ? + $this->vid['player_loc'] = $this->url_encode( 'https://v0.wordpress.com/player.swf?v=1.03&guid=' . $this->vid['id'] . '&isDynamicSeeking=true' ); + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response->posterframe ) && is_string( $this->decoded_response->posterframe ) && $this->decoded_response->posterframe !== '' ) { + $image = $this->make_image_local( $this->decoded_response->posterframe ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + + /** + * Set the video width + */ + protected function set_width() { + $this->set_width_from_json_object(); + } + } +} diff --git a/detail-retrieval/class-vidyard.php b/detail-retrieval/class-vidyard.php new file mode 100644 index 0000000..6464a22 --- /dev/null +++ b/detail-retrieval/class-vidyard.php @@ -0,0 +1,138 @@ + 'http://play.vidyard.com/%s', + 'replace_key' => 'id', + 'response_type' => '', + ]; + + /** + * Data array or false if data could not be found/decoded. + * + * @var array|false + */ + protected $chapter_data = []; + + /** + * Check if the response is for a valid video + * + * @return bool + */ + protected function is_video_response() { + $this->get_chapter_data(); + return ( $this->chapter_data !== [] ); + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->chapter_data['sd_unsecure_url'] ) ) { + $this->vid['content_loc'] = $this->chapter_data['sd_unsecure_url']; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->chapter_data['seconds'] ) ) { + $this->vid['duration'] = $this->chapter_data['seconds']; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ( is_string( $this->remote_url['pattern'] ) && $this->remote_url['pattern'] !== '' ) + && ( is_string( $this->vid[ $this->remote_url['replace_key'] ] ) + && $this->vid[ $this->remote_url['replace_key'] ] !== '' ) ) { + + $url = sprintf( $this->remote_url['pattern'], $this->vid[ $this->remote_url['replace_key'] ] ); + $this->vid['player_loc'] = $this->url_encode( $url ); + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( preg_match( '`vidyard_thumbnail_data = ({.*?});`s', $this->decoded_response, $match ) ) { + $thumbnail_data = str_replace( '\'', '"', trim( $match[1] ) ); + $thumbnail_data = json_decode( $thumbnail_data, true ); + + if ( ( is_array( $thumbnail_data ) && $thumbnail_data !== [] ) ) { + // Get the first element. + $thumbnail_data = reset( $thumbnail_data ); + if ( isset( $thumbnail_data['url'] ) && is_string( $thumbnail_data['url'] ) && $thumbnail_data['url'] !== '' ) { + $image = $this->make_image_local( $thumbnail_data['url'] ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + } + } + } + + /** + * Get decoded vidyard chapter data + */ + private function get_chapter_data() { + // Must use preg match because the data is in inline javascript. + if ( ! empty( $this->decoded_response ) && preg_match( '`vidyard_chapter_data = (\[\s*{[^\}]*\}\s*\]);`', $this->decoded_response, $match ) ) { + // Replace single quotes with double quotes so it can be json decoded. + $json = str_replace( '\'', '"', trim( $match[1] ) ); + $json = json_decode( $json, true ); + + if ( is_array( $json ) && $json !== [] ) { + // Get the first element. + $this->chapter_data = reset( $json ); + } + } + } + } +} diff --git a/detail-retrieval/class-vimeo.php b/detail-retrieval/class-vimeo.php new file mode 100644 index 0000000..deb2102 --- /dev/null +++ b/detail-retrieval/class-vimeo.php @@ -0,0 +1,232 @@ +<\/iframe>", + * "width":480, + * "height":270, + * "duration":332, + * "description":"Built off ...", + * "thumbnail_url":"https:\/\/i.vimeocdn.com\/video\/487915832_295x166.jpg", + * "thumbnail_width":295, + * "thumbnail_height":166, + * "thumbnail_url_with_play_button":"https:\/\/i.vimeocdn.com\/filter\/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F487915832_295x166.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png", + * "upload_date":"2014-07-22 10:32:07", + * "video_id":101410103, + * "uri":"\/videos\/101410103" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Vimeo' ) ) { + + /** + * Class WPSEO_Video_Details_Vimeo + */ + class WPSEO_Video_Details_Vimeo extends WPSEO_Video_Details { + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'https://vimeo.com/%s'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'https://vimeo.com/api/v2/video/%s.json', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Alternate remote url (oembed) to use for private videos where the + * API will refuse to provide information. + * + * @var string[] + */ + protected $alternate_remote = [ + 'pattern' => 'https://vimeo.com/api/oembed.json?url=http://vimeo.com/%s', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Retrieve the video id from a known video url based on a regex match. + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( isset( $this->vid['url'] ) && is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) { + if ( preg_match( '`vimeo\.com/(?:(?:m|video|channels|groups)/(?:[a-z0-9]+/)*)?([0-9]+)(?:$|[/#\?])`i', $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + elseif ( preg_match( '`vimeo\.com/moogaloop\.swf\?clip_id=([^&]+)`i', $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + elseif ( preg_match( '`player\.vimeo\.com/(?:video|external)/([0-9]+)`i', $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + } + } + + /** + * Retrieve information on a video via a remote API call. + * + * Deal with private videos separately. + * + * @since 3.9.0 + */ + protected function get_remote_video_info() { + parent::get_remote_video_info(); + + if ( ! isset( $this->remote_response ) ) { + // If no valid response was received, this may be a private video. + // Try again using oembed as that will still yield most of the needed information. + $this->remote_url = $this->alternate_remote; + parent::get_remote_video_info(); + } + } + + /** + * Decode a remote response as json + */ + protected function decode_as_json() { + $response = json_decode( $this->remote_response ); + // API response. + if ( is_array( $response ) && ! empty( $response[0] ) && is_object( $response[0] ) ) { + $this->decoded_response = $response[0]; + } + // Oembed response. + elseif ( is_object( $response ) ) { + $this->decoded_response = $response; + } + } + + /** + * Check to see if this is really a video. + * + * @return bool + */ + protected function is_video_response() { + // All valid video responses will have a duration. + return ( ! empty( $this->decoded_response->duration ) ); + } + + /** + * Set the video duration + */ + protected function set_duration() { + $this->set_duration_from_json_object(); + } + + /** + * Set the video height + */ + protected function set_height() { + $this->set_height_from_json_object(); + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'https://player.vimeo.com/video/' . rawurlencode( $this->vid['id'] ); + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + if ( isset( $this->decoded_response->thumbnail_large ) && is_string( $this->decoded_response->thumbnail_large ) && $this->decoded_response->thumbnail_large !== '' ) { + $image = $this->make_image_local( $this->decoded_response->thumbnail_large ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + } + } + else { + // Oembed fallback. + $this->set_thumbnail_loc_from_json_object(); + } + } + + /** + * Set the video width + */ + protected function set_width() { + $this->set_width_from_json_object(); + } + + /** + * Set the video view count + * + * Property only available via full API call. + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->stats_number_of_plays ) ) { + $this->vid['view_count'] = $this->decoded_response->stats_number_of_plays; + } + } + } +} diff --git a/detail-retrieval/class-vine.php b/detail-retrieval/class-vine.php new file mode 100644 index 0000000..97f7f30 --- /dev/null +++ b/detail-retrieval/class-vine.php @@ -0,0 +1,95 @@ +", + * "provider_name": "Vine", + * "thumbnail_url": "https://v.cdn.vine.co/r/thumbs/690FA3F8111106985425564004352_2.1.2.10930682652597924920.mp4.jpg?versionId=dmw3H1Tpl_E71k7ggMm9TEZZGFWPfsNh", + * "type": "video", + * "thumbnail_height": 480 + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Vine' ) ) { + + /** + * Class WPSEO_Video_Details_Vine + * + * Retrieve details via the Embedly service. + */ + class WPSEO_Video_Details_Vine extends WPSEO_Video_Details_Embedly { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * @var string + */ + protected $id_regex = '`[/\.]vine\.co/v/([a-z0-9]+)(?:$|[/#\?])`i'; + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'https://vine.co/v/%s'; + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( ! empty( $this->decoded_response->html ) ) { + $this->decoded_response->html = stripslashes( $this->decoded_response->html ); + if ( preg_match( '` src="[^\?]+\?(?:[^&]+&)*?src=([^&]+\.mp4)`i', $this->decoded_response->html, $match ) ) { + $this->vid['content_loc'] = rawurldecode( $match[1] ); + } + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( ! empty( $this->vid['id'] ) ) { + $this->vid['player_loc'] = 'https://vine.co/v/' . rawurlencode( $this->vid['id'] ) . '/embed/simple'; + } + } + } +} diff --git a/detail-retrieval/class-wistia.php b/detail-retrieval/class-wistia.php new file mode 100644 index 0000000..d0b94d4 --- /dev/null +++ b/detail-retrieval/class-wistia.php @@ -0,0 +1,254 @@ +", + * "width":960, + * "height":540, + * "provider_name":"Wistia, Inc.", + * "provider_url":"http://wistia.com", + * "title":"Puppies", + * "thumbnail_url":"https://embed-ssl.wistia.com/deliveries/d5f1c25578e8748cc5b6b946e02e654d9f9db47a.jpg?image_crop_resized=960x540", + * "thumbnail_width":960, + * "thumbnail_height":540, + * "duration":23.268 + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Wistia' ) ) { + + /** + * Class WPSEO_Video_Details_Wistia + */ + class WPSEO_Video_Details_Wistia extends WPSEO_Video_Details { + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = 'http://fast.wistia.net/medias/%s?embedType=iframe'; + + /** + * Information on the remote URL to use for retrieving the video details. + * + * @var string[] + */ + protected $remote_url = [ + 'pattern' => 'http://fast.wistia.com/oembed?url=%s', + 'replace_key' => 'url', + 'response_type' => 'json', + ]; + + /** + * Whether or not to use the fallback method to retrieve certain information on the video. + * + * @var bool + */ + protected $use_fallback = false; + + /** + * Frame source - used by fallback method. + * + * @var string + */ + protected $frame_source; + + /** + * Instantiate the class + * + * Adjust the video url before passing off to the parent constructor + * + * @param array $vid The video array with all the data. + * @param array $old_vid The video array with all the data of the previous "fetch", if available. + */ + public function __construct( $vid, $old_vid = [] ) { + if ( isset( $vid['id'] ) ) { + $vid['url'] = sprintf( $this->url_template, rawurlencode( $vid['id'] ) ); + } + parent::__construct( $vid, $old_vid ); + } + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * Match whether old data can be used on url rather than video id + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data + * Defaults to 'url' for this implementation. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'url' ) { + return parent::maybe_use_old_video_data( $match_on ); + } + + /** + * Retrieve information on a video via a remote API call + * + * @return void + */ + protected function get_remote_video_info() { + if ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) { + // Temporarily change the url. + $real_url = $this->vid['url']; + if ( strpos( $this->vid['url'], 'embedType=' ) !== false ) { + // Avoid adding embedType twice. + $this->vid['url'] = str_replace( [ 'embedType=api', 'embedType=iframe', 'embedType=popover', 'embedType=seo', 'embedType=async' ], 'embedType=seo', $this->vid['url'] ); + } + else { + $this->vid['url'] = add_query_arg( 'embedType', 'seo', $this->vid['url'] ); + } + + parent::get_remote_video_info(); + + // Reset the url. + $this->vid['url'] = $real_url; + } + } + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) && isset( $this->decoded_response->type ) && $this->decoded_response->type === 'video' ); + } + + /** + * Determine whether or not the use of the fallback method is needed and set video details + * to their new values by passing off to the parent method + */ + protected function put_video_details() { + if ( ! empty( $this->decoded_response->html ) ) { + $this->decoded_response->html = stripslashes( $this->decoded_response->html ); + + if ( strpos( $this->decoded_response->html, '
decoded_response->html, $match ) ) { + + $this->use_fallback = true; + + $response = $this->remote_get( $match[2] ); + if ( is_string( $response ) && $response !== '' && $response !== 'null' ) { + $this->frame_source = $response; + } + } + } + + parent::put_video_details(); + } + + /** + * Set the content location + */ + protected function set_content_loc() { + if ( $this->use_fallback === false ) { + if ( ! empty( $this->decoded_response->html ) && preg_match( '``', $this->decoded_response->html, $match ) ) { + $this->vid['content_loc'] = $match[1]; + } + } + elseif ( ! empty( $this->frame_source ) && preg_match( '`frame_source, $match ) ) { + $this->vid['content_loc'] = $match[2]; + } + } + + /** + * Set the video duration + */ + protected function set_duration() { + $this->set_duration_from_json_object(); + } + + /** + * Set the video height + */ + protected function set_height() { + $this->set_height_from_json_object(); + } + + /** + * Set the player location + */ + protected function set_player_loc() { + if ( $this->use_fallback === false ) { + if ( ! empty( $this->decoded_response->html ) && preg_match( '``', $this->decoded_response->html, $match ) ) { + $this->vid['player_loc'] = $match[1]; + } + } + elseif ( ! empty( $this->frame_source ) ) { + if ( preg_match( '`"type":"flv","url":"([^"]*)"`', $this->frame_source, $match ) ) { + $flv = $match[1]; + } + elseif ( preg_match( '`"type":"hdflv","url":"([^"]*)"`', $this->frame_source, $match ) ) { + $flv = $match[1]; + } + + if ( preg_match( '`"type":"still","url":"([^"]*)"`', $this->frame_source, $match ) ) { + $still = $match[1]; + } + + if ( preg_match( '`"accountKey":"([^"]*)"`', $this->frame_source, $match ) ) { + $account_key = $match[1]; + } + + if ( preg_match( '`"mediaKey":"([^"]*)"`', $this->frame_source, $match ) ) { + $media_key = $match[1]; + } + + if ( empty( $this->vid['duration'] ) ) { + $this->set_duration(); + } + + if ( isset( $flv, $still, $account_key, $media_key, $this->vid['duration'] ) ) { + $url = sprintf( + 'https://wistia.sslcs.cdngc.net/flash/embed_player_v2.0.swf?videoUrl=%1$s&stillUrl=%2$s&controlsVisibleOnLoad=false&unbufferedSeek=true&autoLoad=false&autoPlay=true&endVideoBehavior=default&embedServiceURL=http://distillery.wistia.com/x&accountKey=%3$s&mediaID=%4$s&mediaDuration=%5$s&fullscreenDisabled=false', + $flv, // #1. + $still, // #2. + $account_key, // #3. + $media_key, // #4. + $this->vid['duration'] // #5. + ); + + $this->vid['player_loc'] = $this->url_encode( $url ); + } + } + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + $this->set_thumbnail_loc_from_json_object(); + } + + /** + * Set the video width + */ + protected function set_width() { + $this->set_width_from_json_object(); + } + } +} diff --git a/detail-retrieval/class-wordpresstv.php b/detail-retrieval/class-wordpresstv.php new file mode 100644 index 0000000..c28481d --- /dev/null +++ b/detail-retrieval/class-wordpresstv.php @@ -0,0 +1,102 @@ +<\/embed>" + * } + */ +if ( ! class_exists( 'WPSEO_Video_Details_Wordpresstv' ) ) { + + /** + * Class WPSEO_Video_Details_Wordpresstv + * + * Retrieve video details from WordPress.tv (well grab the ID and then use the VideoPress API) + */ + class WPSEO_Video_Details_Wordpresstv extends WPSEO_Video_Details_Videopress { + + /** + * Regular expression to retrieve a video ID from a known video URL. + * + * {@internal Is used in a slightly different way than in the other classes - uses + * a remote call first to get the URL to use this against.} + * + * @var string + */ + protected $id_regex = '`v\.wordpress\.com/([^"]+)`i'; // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled -- URL regex. + + /** + * Sprintf template to create a URL from an ID. + * + * @var string + */ + protected $url_template = '//v.wordpress.com/%s'; + + /** + * Retrieve the video id from a known video url via a remote call, then match it based on a regex + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( ( is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) && $this->id_regex !== '' ) { + + $replace_key = $this->vid['url']; + // Fix protocol-less urls in parameters as the remote get call most often will not work with them. + if ( strpos( $this->vid['url'], '//' ) === 0 ) { + $replace_key = 'http:' . $this->vid['url']; + } + + $url = sprintf( 'http://wordpress.tv/oembed/?url=%s', $replace_key ); + $url = $this->url_encode( $url ); + + $response = $this->remote_get( $url ); + if ( is_string( $response ) && $response !== '' && $response !== 'null' ) { + + $response = json_decode( $response ); + if ( is_object( $response ) ) { + if ( preg_match( $this->id_regex, $response->html, $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + } + } + } + } + } + + /** + * Use the "new" post data with the old video data, to prevent the need for an external video + * API call when the video hasn't changed. + * + * Match whether old data can be used on url rather than video id + * + * @param string $match_on Array key to use in the $vid array to determine whether or not to use the old data. + * Defaults to 'url' for this implementation. + * + * @return bool Whether or not valid old data was found (and used) + */ + protected function maybe_use_old_video_data( $match_on = 'url' ) { + return parent::maybe_use_old_video_data( $match_on ); + } + } +} diff --git a/detail-retrieval/class-youtube.php b/detail-retrieval/class-youtube.php new file mode 100644 index 0000000..31a1c27 --- /dev/null +++ b/detail-retrieval/class-youtube.php @@ -0,0 +1,220 @@ + 'https://www.googleapis.com/youtube/v3/videos?part=snippet,status,statistics,contentDetails,player&id=%1$s&fields=items&key=%2$s', + 'replace_key' => 'id', + 'response_type' => 'json', + ]; + + /** + * Google API access key. + * + * @var string + */ + protected $api_key = 'AIzaSyD5OvaM_lplFbQjDr-tgK9SEkLxdkW79Lw'; + + /** + * Retrieve the video id from a known video url based on a regex match. + * Also change the url based on the new video id. + * + * @param int $match_nr The captured parenthesized sub-pattern to use from matches. Defaults to 1. + * + * @return void + */ + protected function determine_video_id_from_url( $match_nr = 1 ) { + if ( isset( $this->vid['url'] ) && is_string( $this->vid['url'] ) && $this->vid['url'] !== '' ) { + + $yt_id = WPSEO_Video_Sitemap::$youtube_id_pattern; + + $patterns = [ + '`youtube\.(?:com|[a-z]{2})/(?:v/|(?:watch)?(?:\?|#!)(?:.*&)?v=)(' . $yt_id . ')`i', + '`youtube(?:-nocookie)?\.com/(?:embed|v)/(?!videoseries|playlist)(' . $yt_id . ')`i', + '`https?://youtu\.be/(' . $yt_id . ')`i', + ]; + + foreach ( $patterns as $pattern ) { + if ( preg_match( $pattern, $this->vid['url'], $match ) ) { + $this->vid['id'] = $match[ $match_nr ]; + break; + } + } + + // @todo [JRF => Yoast] shouldn't this be checked against $youtube_id_pattern as well ? + if ( ( ! isset( $this->vid['id'] ) || empty( $this->vid['id'] ) ) && ! preg_match( '`^(?:http|//)`', $this->vid['url'] ) ) { + $this->vid['id'] = $this->vid['url']; + } + } + } + + /** + * Check if the response is for a video + * + * @return bool + */ + protected function is_video_response() { + return ( ! empty( $this->decoded_response ) ); + } + + /** + * Set the video last fetched. + */ + protected function set_last_fetched() { + $this->vid['last_fetched'] = time(); + } + + /** + * Set the video duration + */ + protected function set_duration() { + if ( ! empty( $this->decoded_response->contentDetails->duration ) ) { + $date = new DateTime( '@0' ); + $date->add( new DateInterval( $this->decoded_response->contentDetails->duration ) ); + + $this->vid['duration'] = (int) $date->format( 'U' ); + } + } + + /** + * Set the video height + */ + protected function set_height() { + if ( ! empty( $this->decoded_response->player->embedHtml ) + && preg_match( '` height="([^"]+)"`i', $this->decoded_response->player->embedHtml, $match ) + ) { + $this->vid['height'] = (int) $match[1]; + } + else { + // Fall back to hard-coded default. + $this->vid['height'] = 390; + } + } + + /** + * Set the player location + */ + protected function set_player_loc() { + // Bow out if video is explicitely not embeddable - falls through if embeddable status not available. + if ( isset( $this->decoded_response->status->embeddable ) && $this->decoded_response->status->embeddable !== true ) { + return; + } + + if ( ! empty( $this->decoded_response->player->embedHtml ) + && preg_match( '` src="([^"]+)"`i', $this->decoded_response->player->embedHtml, $match ) + ) { + $player_loc = $match[1]; + } + else { + // Fall back to hard-coded default. + $player_loc = '//www.youtube.com/embed/' . rawurlencode( $this->vid['id'] ); + } + + // Add protocol if the resulting player URL would be protocol-less. + if ( strpos( $player_loc, 'http' ) !== 0 ) { + $player_loc = 'https:' . $player_loc; + } + + $this->vid['player_loc'] = $player_loc; + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + $formats = [ 'maxres', 'standard', 'high', 'medium', 'default' ]; + + foreach ( $formats as $format ) { + if ( ! empty( $this->decoded_response->snippet->thumbnails->$format ) && is_object( $this->decoded_response->snippet->thumbnails->$format ) ) { + $thumbnail = $this->decoded_response->snippet->thumbnails->$format; + if ( ! empty( $thumbnail->url ) ) { + $image = $this->make_image_local( $thumbnail->url ); + if ( is_string( $image ) && $image !== '' ) { + $this->vid['thumbnail_loc'] = $image; + + return; + } + } + } + } + } + + /** + * Set the video view count + */ + protected function set_view_count() { + if ( ! empty( $this->decoded_response->statistics->viewCount ) ) { + $this->vid['view_count'] = $this->decoded_response->statistics->viewCount; + } + } + + /** + * Set the video width + */ + protected function set_width() { + if ( ! empty( $this->decoded_response->player->embedHtml ) + && preg_match( '` width="([^"]+)"`i', $this->decoded_response->player->embedHtml, $match ) + ) { + $this->vid['width'] = (int) $match[1]; + } + else { + // Fall back to hard-coded default. + $this->vid['width'] = 640; + } + } + + /** + * Extends the parent method. By letting the parent set the response and get the first item afterwards + */ + protected function decode_as_json() { + parent::decode_as_json(); + + if ( ! empty( $this->decoded_response->items[0] ) ) { + $this->decoded_response = $this->decoded_response->items[0]; + } + else { + // Reset if no valid data received. + $this->decoded_response = null; + } + } + } +} diff --git a/detail-retrieval/index.php b/detail-retrieval/index.php new file mode 100644 index 0000000..9070b02 --- /dev/null +++ b/detail-retrieval/index.php @@ -0,0 +1,4 @@ + '.../%s', + 'replace_key' => 'url|id', + 'response_type' => 'json|serial|simplexml', + ]; + + /** + * Set the player location + */ + protected function set_player_loc() { + } + + /** + * Set the thumbnail location + */ + protected function set_thumbnail_loc() { + } + + /* + protected function set_content_loc() {} + protected function set_duration() {} + protected function set_height() {} + protected function set_id() {} + protected function set_view_count() {} + protected function set_width() {} + + protected function set_type() {} -> normally not needed + */ + } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..9070b02 --- /dev/null +++ b/index.php @@ -0,0 +1,4 @@ +x

";d.appendChild(f.childNodes[1])}if(b){a.extend(e,b)}return this.each(function(){var g=["iframe[src*='player.vimeo.com']","iframe[src*='youtube.com']","iframe[src*='youtube-nocookie.com']","iframe[src*='kickstarter.com'][src*='video.html']","object","embed"];if(e.customSelector){g.push(e.customSelector)}var h=a(this).find(g.join(","));h=h.not("object object");h.each(function(){var m=a(this);if(this.tagName.toLowerCase()==="embed"&&m.parent("object").length||m.parent(".fluid-width-video-wrapper").length){return}var i=(this.tagName.toLowerCase()==="object"||(m.attr("height")&&!isNaN(parseInt(m.attr("height"),10))))?parseInt(m.attr("height"),10):m.height(),j=!isNaN(parseInt(m.attr("width"),10))?parseInt(m.attr("width"),10):m.width(),k=i/j;if(!m.attr("id")){var l="fitvid"+Math.floor(Math.random()*999999);m.attr("id",l)}m.wrap('
').parent(".fluid-width-video-wrapper").css("padding-top",(k*100)+"%");m.removeAttr("height").removeAttr("width")})})}})(window.jQuery||window.Zepto); \ No newline at end of file diff --git a/js/jquery.min.js b/js/jquery.min.js new file mode 100644 index 0000000..68b0893 --- /dev/null +++ b/js/jquery.min.js @@ -0,0 +1,9 @@ +/*! +* FitVids 1.1 +* +* Copyright 2013, Chris Coyier - http://css-tricks.com + Dave Rupert - http://daverupert.com +* Credit to Thierry Koblentz - http://www.alistapart.com/articles/creating-intrinsic-ratios-for-video/ +* Released under the WTFPL license - http://sam.zoy.org/wtfpl/ +* +*/ +!function(a){"use strict";a.fn.fitVids=function(b){var c={customSelector:null};if(!document.getElementById("fit-vids-style")){var d=document.head||document.getElementsByTagName("head")[0],e=".fluid-width-video-wrapper{width:100%;position:relative;padding:0;}.fluid-width-video-wrapper iframe,.fluid-width-video-wrapper object,.fluid-width-video-wrapper embed {position:absolute;top:0;left:0;width:100%;height:100%;}",f=document.createElement("div");f.innerHTML='

x

",d.appendChild(f.childNodes[1])}return b&&a.extend(c,b),this.each(function(){var b=["iframe[src*='player.vimeo.com']","iframe[src*='youtube.com']","iframe[src*='youtube-nocookie.com']","iframe[src*='kickstarter.com'][src*='video.html']","object","embed"];c.customSelector&&b.push(c.customSelector);var d=a(this).find(b.join(","));d=d.not("object object"),d.each(function(){var b=a(this);if(!("embed"===this.tagName.toLowerCase()&&b.parent("object").length||b.parent(".fluid-width-video-wrapper").length)){var c="object"===this.tagName.toLowerCase()||b.attr("height")&&!isNaN(parseInt(b.attr("height"),10))?parseInt(b.attr("height"),10):b.height(),d=isNaN(parseInt(b.attr("width"),10))?b.width():parseInt(b.attr("width"),10),e=c/d;if(!b.attr("id")){var f="fitvid"+Math.floor(999999*Math.random());b.attr("id",f)}b.wrap('
').parent(".fluid-width-video-wrapper").css("padding-top",100*e+"%"),b.removeAttr("height").removeAttr("width")}})})}}(window.jQuery||window.Zepto); \ No newline at end of file diff --git a/js/videoseo-admin-global.min.js b/js/videoseo-admin-global.min.js new file mode 100644 index 0000000..3ad3195 --- /dev/null +++ b/js/videoseo-admin-global.min.js @@ -0,0 +1 @@ +function videoseo_setIgnore(a,b,c){jQuery.post(ajaxurl,{action:"videoseo_set_ignore",option:a,_wpnonce:c},function(c){c&&(jQuery("#"+b).hide(),jQuery("#hidden_ignore_"+a).val("ignore"))})} \ No newline at end of file diff --git a/js/videoseo-admin-progressbar.min.js b/js/videoseo-admin-progressbar.min.js new file mode 100644 index 0000000..905a346 --- /dev/null +++ b/js/videoseo-admin-progressbar.min.js @@ -0,0 +1 @@ +function update_bar(a){a>100&&(a=99),a=Math.round(100*a)/100,jQuery(".bar","#video_seo_progressbar").css("width",a+"%"),jQuery("#video_seo_percentage_hidden").val(a),jQuery(".bar_status","#video_seo_progressbar").html(a>=5?a+"%":" ")}function video_fetch(a){var b=parseInt(jQuery("#video_seo_total_posts").html()),c=jQuery("#video_seo_force_reindex").length,d={action:"index_posts",type:"index",start:a,total:b,portion:5,nonce:jQuery("#videoseo-nonce-ajax").val()};c>0&&(d.force="on"),jQuery.post(ajaxurl,d,function(c){b>a?(setTimeout(function(){video_fetch(a+5)},200),calculate_to_go(a,b,c)):(update_bar(100),jQuery("#video_seo_total_time").html(" "),jQuery("#video_seo_done").show())});var e=b-a;0>e&&(e=0),jQuery("#video_seo_posts_to_go").html(e);var f=5/b*100,g=parseFloat(jQuery("#video_seo_percentage_hidden").val()),h=f+g;update_bar(h)}function calculate_to_go(a,b,c){var d=b-a,e=d/5,f=e*parseInt(c);jQuery("#video_seo_total_time").html(f==1/0?"Unknown":Math.round(f)+" seconds")}function get_total_posts(){update_bar(0);var a={action:"index_posts",type:"total_posts",nonce:jQuery("#videoseo-nonce-ajax").val()};jQuery.post(ajaxurl,a,function(a){var b=a;jQuery("#video_seo_total_posts").html(b),video_fetch(0)})}jQuery("#video_seo_done").hide(),setTimeout(function(){get_total_posts()},500); \ No newline at end of file diff --git a/js/yoast-video-seo-plugin-1480.min.js b/js/yoast-video-seo-plugin-1480.min.js new file mode 100644 index 0000000..5ae50f8 --- /dev/null +++ b/js/yoast-video-seo-plugin-1480.min.js @@ -0,0 +1 @@ +!function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=8)}([function(e,t){e.exports=window.wp.element},function(e,t){e.exports=window.wp.data},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.VIDEO_DISMISS_ALERT=t.VIDEO_SET_TAGS=t.VIDEO_CLEAR_THUMBNAIL=t.VIDEO_SET_THUMBNAIL=t.VIDEO_TOGGLE_NOT_FAMILY_FRIENDLY=t.VIDEO_SET_DURATION=t.VIDEO_LOAD_EDITOR_DATA=void 0,t.loadVideoEditorData=function(){return{type:i,duration:o.default.duration,notFamilyFriendly:o.default.notFamilyFriendly,thumbnail:o.default.thumbnail,videoTagsArray:o.default.tags,reactAlertIsDismissed:wpseoVideoL10n.react_alert_is_dismissed}},t.setDuration=function(e){return o.default.duration=e,{type:r,value:e}},t.toggleNotFamilyFriendly=function(){return o.default.notFamilyFriendly=!o.default.notFamilyFriendly,{type:a}},t.setVideoThumbnail=function(e){return o.default.thumbnail=e,{type:l,thumbnailUrl:e}},t.clearVideoThumbnail=function(){return o.default.thumbnail="",{type:u}},t.setVideoTags=function(e){return o.default.tags=e,{type:d,videoTagsArray:e}},t.dismissAlert=function(){return{type:s}};var o=function(e){return e&&e.__esModule?e:{default:e}}(n(12));var i=t.VIDEO_LOAD_EDITOR_DATA="VIDEO_LOAD_EDITOR_DATA",r=t.VIDEO_SET_DURATION="VIDEO_SET_DURATION",a=t.VIDEO_TOGGLE_NOT_FAMILY_FRIENDLY="VIDEO_SET_NOT_FAMILY_FRIENDLY",l=t.VIDEO_SET_THUMBNAIL="VIDEO_SET_THUMBNAIL",u=t.VIDEO_CLEAR_THUMBNAIL="VIDEO_CLEAR_THUMBNAIL",d=t.VIDEO_SET_TAGS="VIDEO_SET_TAGS",s=t.VIDEO_DISMISS_ALERT="VIDEO_DISMISS_ALERT"},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:r,t=arguments[1];switch(t.type){case i.VIDEO_LOAD_EDITOR_DATA:return o({},e,{duration:t.duration,notFamilyFriendly:t.notFamilyFriendly,thumbnail:t.thumbnail,tags:t.videoTagsArray,reactAlertIsDismissed:t.reactAlertIsDismissed,isLoaded:!0});case i.VIDEO_SET_DURATION:return o({},e,{duration:t.value});case i.VIDEO_TOGGLE_NOT_FAMILY_FRIENDLY:return o({},e,{notFamilyFriendly:!e.notFamilyFriendly});case i.VIDEO_SET_THUMBNAIL:return o({},e,{thumbnail:t.thumbnailUrl});case i.VIDEO_CLEAR_THUMBNAIL:return o({},e,{thumbnail:""});case i.VIDEO_SET_TAGS:return o({},e,{tags:t.videoTagsArray});case i.VIDEO_DISMISS_ALERT:return o({},e,{reactAlertIsDismissed:!0});default:return e}};var i=n(2),r={isLoaded:!1,notFamilyFriendly:!1,duration:0,thumbnail:"",reactAlertIsDismissed:!0}},function(e,t){e.exports=window.wp.components},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(18),i=n(1),r=function(e){return e&&e.__esModule?e:{default:e}}(n(19));var a=null,l=function(){return(a||(a=window.wp.media()).on("select",function(){var e=a.state().get("selection").first();(0,i.dispatch)("yoast-seo-video/editor").setVideoThumbnail(e.attributes.url)}),a).open()};t.default=(0,o.compose)([(0,i.withSelect)(function(e){var t=e("yoast-seo-video/editor"),n=t.getDuration,o=t.getNotFamilyFriendly,i=t.getVideoThumbnail,r=t.getVideoTags,a=t.getReactAlertIsDismissed,l=e("yoast-seo/editor").getEditorType;return{duration:n(),notFamilyFriendly:o(),thumbnail:i(),defaultThumbnail:wpseoVideoL10n.default_thumbnail||"",editorType:l(),tags:r(),reactAlertIsDismissed:a()}}),(0,i.withDispatch)(function(e){var t=e("yoast-seo-video/editor"),n=t.loadVideoEditorData,o=t.setDuration,i=t.toggleNotFamilyFriendly,r=t.clearVideoThumbnail,a=t.setVideoTags,u=t.dismissAlert;return{onLoad:n,onSelectImageClick:l,onRemoveImageClick:r,setVideoTags:a,setDuration:o,toggleNotFamilyFriendly:i,dismissAlert:u}})])(r.default)},function(e,t){e.exports=yoast.componentsNew},function(e,t){e.exports=window.wp.i18n},function(e,t,n){"use strict";var o=n(0),i=n(9),r=u(n(10)),a=u(n(17)),l=u(n(5));function u(e){return e&&e.__esModule?e:{default:e}}var d=window.yoast.editorModules.components.contexts.location.LocationProvider;function s(){return"undefined"!=typeof YoastSEO&&void 0!==YoastSEO.analysis&&void 0!==YoastSEO.analysis.worker}function c(){window.YoastSEO._registerReactComponent("yoast-seo-video",function(){return wp.element.createElement(a.default,{fillName:"YoastElementor"})})}function f(){s()&&YoastSEO.analysis.worker.loadScript(wpseoVideoL10n.script_url).then(function(){return YoastSEO.analysis.worker.sendMessage("initialize",wpseoVideoL10n,"YoastVideoSEO")})}"1"===wpseoVideoL10n.has_video&&(s()?f():jQuery(window).on("YoastSEO:ready",f)),jQuery(window).on("elementor:init",function(){window.elementor.on("panel:init",function(){(0,r.default)()}),(0,i.addAction)("yoast.elementor.loaded","yoast/yoast-video-seo/load-video-in-elementor",c)}),jQuery(window).on("YoastSEO:ready",function(){(0,r.default)(),wpseoScriptData.isBlockEditor&&(0,wp.plugins.registerPlugin)("yoast-seo-video",{render:a.default}),(0,o.render)(wp.element.createElement(d,{value:"metabox"},wp.element.createElement(l.default,null)),document.getElementById("wpseo-video-react-metabox-root"))})},function(e,t){e.exports=window.wp.hooks},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){return(0,o.registerStore)("yoast-seo-video/editor",{reducer:(0,o.combineReducers)(r.default),actions:i,selectors:a})};var o=n(1),i=l(n(11)),r=function(e){return e&&e.__esModule?e:{default:e}}(n(13)),a=l(n(14));function l(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(2);Object.keys(o).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return o[e]}})})},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;n","
"),{a:wp.element.createElement("a",{href:e,target:"_blank",rel:"noopener noreferrer"})})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t=(0,i.useCallback)(function(){"blockEditor"===e.editorType?((0,r.dispatch)("core/interface").enableComplementaryArea("core/edit-post","yoast-seo/seo-sidebar"),(0,r.dispatch)("yoast-seo/editor").openEditorModal("yoast-google-preview-modal")):"elementorEditor"===e.editorType?(0,r.dispatch)("yoast-seo/editor").openEditorModal("yoast-google-preview-modal"):function(){document.getElementById("wpseo-meta-tab-content").click();var e=document.querySelector("#yoast-snippet-editor-metabox .yoast-svg-icon-chevron-down");e&&e.parentElement.click()}()},[e.editorType]);return wp.element.createElement(o.FieldGroup,{label:(0,a.__)("Title and description","yoast-video-seo"),description:(0,a.__)("Your video will use the same SEO title and meta description you enter in our Google Preview editor.","yoast-video-seo")},wp.element.createElement(o.NewButton,{variant:"secondary",onClick:t},(0,a.__)("Open Google Preview editor","yoast-video-seo")))};var o=n(6),i=n(0),r=n(1),a=n(7)}]); \ No newline at end of file diff --git a/js/yoast-video-seo-worker-1480.min.js b/js/yoast-video-seo-worker-1480.min.js new file mode 100644 index 0000000..53cba31 --- /dev/null +++ b/js/yoast-video-seo-worker-1480.min.js @@ -0,0 +1 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t){e.exports=yoast.analysis},function(e,t,n){e.exports=function(e,t){var n,r,i=0;function o(){var o,s,a=n,c=arguments.length;e:for(;a;){if(a.args.length===arguments.length){for(s=0;s=0),a.type){case"b":n=parseInt(n,10).toString(2);break;case"c":n=String.fromCharCode(parseInt(n,10));break;case"d":case"i":n=parseInt(n,10);break;case"j":n=JSON.stringify(n,null,a.width?parseInt(a.width):0);break;case"e":n=a.precision?parseFloat(n).toExponential(a.precision):parseFloat(n).toExponential();break;case"f":n=a.precision?parseFloat(n).toFixed(a.precision):parseFloat(n);break;case"g":n=a.precision?String(Number(n.toPrecision(a.precision))):parseFloat(n);break;case"o":n=(parseInt(n,10)>>>0).toString(8);break;case"s":n=String(n),n=a.precision?n.substring(0,a.precision):n;break;case"t":n=String(!!n),n=a.precision?n.substring(0,a.precision):n;break;case"T":n=Object.prototype.toString.call(n).slice(8,-1).toLowerCase(),n=a.precision?n.substring(0,a.precision):n;break;case"u":n=parseInt(n,10)>>>0;break;case"v":n=n.valueOf(),n=a.precision?n.substring(0,a.precision):n;break;case"x":n=(parseInt(n,10)>>>0).toString(16);break;case"X":n=(parseInt(n,10)>>>0).toString(16).toUpperCase()}i.json.test(a.type)?g+=n:(!i.number.test(a.type)||d&&!a.sign?p="":(p=d?"+":"-",n=n.toString().replace(i.sign,"")),l=a.pad_char?"0"===a.pad_char?"0":a.pad_char.charAt(1):" ",u=a.width-(p+n).length,c=a.width&&u>0?l.repeat(u):"",g+=a.align?p+n+c:"0"===l?p+c+n:c+p+n)}return g}(function(e){if(a[e])return a[e];var t,n=e,r=[],o=0;for(;n;){if(null!==(t=i.text.exec(n)))r.push(t[0]);else if(null!==(t=i.modulo.exec(n)))r.push("%");else{if(null===(t=i.placeholder.exec(n)))throw new SyntaxError("[sprintf] unexpected placeholder");if(t[2]){o|=1;var s=[],c=t[2],l=[];if(null===(l=i.key.exec(c)))throw new SyntaxError("[sprintf] failed to parse named argument key");for(s.push(l[1]);""!==(c=c.substring(l[0].length));)if(null!==(l=i.key_access.exec(c)))s.push(l[1]);else{if(null===(l=i.index_access.exec(c)))throw new SyntaxError("[sprintf] failed to parse named argument key");s.push(l[1])}t[2]=s}else o|=2;if(3===o)throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");r.push({placeholder:t[0],param_no:t[1],keys:t[2],sign:t[3],pad_char:t[4],align:t[5],width:t[6],precision:t[7],type:t[8]})}n=n.substring(t[0].length)}return a[e]=r}(e),arguments)}function s(e,t){return o.apply(null,[e].concat(t||[]))}var a=Object.create(null);t.sprintf=o,t.vsprintf=s,"undefined"!=typeof window&&(window.sprintf=o,window.vsprintf=s,void 0===(r=function(){return{sprintf:o,vsprintf:s}}.call(t,n,t,e))||(e.exports=r))}()},function(e,t,n){"use strict";n.r(t);var r=n(0);class i extends r.Assessment{constructor(e){super(),this.settings=e}getResult(e){const t=new RegExp(this.settings.video,"ig"),n=e.getTitle().match(t)||0,i=new r.AssessmentResult,o=this.score(n);return i.setScore(o.score),i.setText(o.text),i}score(e){return e.length>0?{score:9,text:this.settings.video_title_good}:{score:6,text:this.settings.video_title_ok}}}var o=n(1),s=n.n(o),a=n(2),c=n.n(a);const l=s()(console.error);var u,d,p,f;u={"(":9,"!":8,"*":7,"/":7,"%":7,"+":6,"-":6,"<":5,"<=":5,">":5,">=":5,"==":4,"!=":4,"&&":3,"||":2,"?":1,"?:":1},d=["(","?"],p={")":["("],":":["?","?:"]},f=/<=|>=|==|!=|&&|\|\||\?:|\(|!|\*|\/|%|\+|-|<|>|\?|\)|:/;var h={"!":function(e){return!e},"*":function(e,t){return e*t},"/":function(e,t){return e/t},"%":function(e,t){return e%t},"+":function(e,t){return e+t},"-":function(e,t){return e-t},"<":function(e,t){return e":function(e,t){return e>t},">=":function(e,t){return e>=t},"==":function(e,t){return e===t},"!=":function(e,t){return e!==t},"&&":function(e,t){return e&&t},"||":function(e,t){return e||t},"?:":function(e,t,n){if(e)throw t;return n}};function g(e){var t=function(e){for(var t,n,r,i,o=[],s=[];t=e.match(f);){for(n=t[0],(r=e.substr(0,t.index).trim())&&o.push(r);i=s.pop();){if(p[n]){if(p[n][0]===i){n=p[n][1]||n;break}}else if(d.indexOf(i)>=0||u[i]1===e?0:1}},y=/^i18n\.(n?gettext|has_translation)(_|$)/;var x=function(e){return"string"!=typeof e||""===e?(console.error("The namespace must be a non-empty string."),!1):!!/^[a-zA-Z][a-zA-Z0-9_.\-\/]*$/.test(e)||(console.error("The namespace can only contain numbers, letters, dashes, periods, underscores and slashes."),!1)};var b=function(e){return"string"!=typeof e||""===e?(console.error("The hook name must be a non-empty string."),!1):/^__/.test(e)?(console.error("The hook name cannot begin with `__`."),!1):!!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(e)||(console.error("The hook name can only contain numbers, letters, dashes, periods and underscores."),!1)};var w=function(e,t){return function(n,r,i){let o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:10;const s=e[t];if(!b(n))return;if(!x(r))return;if("function"!=typeof i)return void console.error("The hook callback must be a function.");if("number"!=typeof o)return void console.error("If specified, the hook priority must be a number.");const a={callback:i,priority:o,namespace:r};if(s[n]){const e=s[n].handlers;let t;for(t=e.length;t>0&&!(o>=e[t-1].priority);t--);t===e.length?e[t]=a:e.splice(t,0,a),s.__current.forEach(e=>{e.name===n&&e.currentIndex>=t&&e.currentIndex++})}else s[n]={handlers:[a],runs:0};"hookAdded"!==n&&e.doAction("hookAdded",n,r,i,o)}};var k=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return function(r,i){const o=e[t];if(!b(r))return;if(!n&&!x(i))return;if(!o[r])return 0;let s=0;if(n)s=o[r].handlers.length,o[r]={runs:o[r].runs,handlers:[]};else{const e=o[r].handlers;for(let t=e.length-1;t>=0;t--)e[t].namespace===i&&(e.splice(t,1),s++,o.__current.forEach(e=>{e.name===r&&e.currentIndex>=t&&e.currentIndex--}))}return"hookRemoved"!==r&&e.doAction("hookRemoved",r,i),s}};var A=function(e,t){return function(n,r){const i=e[t];return void 0!==r?n in i&&i[n].handlers.some(e=>e.namespace===r):n in i}};var F=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return function(r){const i=e[t];i[r]||(i[r]={handlers:[],runs:0}),i[r].runs++;const o=i[r].handlers;for(var s=arguments.length,a=new Array(s>1?s-1:0),c=1;c{const r=new v({}),i=new Set,o=()=>{i.forEach(e=>e())},s=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"default";r.data[t]={..._,...r.data[t],...e},r.data[t][""]={..._[""],...r.data[t][""]}},a=(e,t)=>{s(e,t),o()},c=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"default",t=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,i=arguments.length>3?arguments[3]:void 0,o=arguments.length>4?arguments[4]:void 0;return r.data[e]||s(void 0,e),r.dcnpgettext(e,t,n,i,o)},l=function(){return arguments.length>0&&void 0!==arguments[0]?arguments[0]:"default"},u=(e,t,r)=>{let i=c(r,t,e);return n?(i=n.applyFilters("i18n.gettext_with_context",i,e,t,r),n.applyFilters("i18n.gettext_with_context_"+l(r),i,e,t,r)):i};if(e&&a(e,t),n){const e=e=>{y.test(e)&&o()};n.addAction("hookAdded","core/i18n",e),n.addAction("hookRemoved","core/i18n",e)}return{getLocaleData:function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"default";return r.data[e]},setLocaleData:a,resetLocaleData:(e,t)=>{r.data={},r.pluralForms={},a(e,t)},subscribe:e=>(i.add(e),()=>i.delete(e)),__:(e,t)=>{let r=c(t,void 0,e);return n?(r=n.applyFilters("i18n.gettext",r,e,t),n.applyFilters("i18n.gettext_"+l(t),r,e,t)):r},_x:u,_n:(e,t,r,i)=>{let o=c(i,void 0,e,t,r);return n?(o=n.applyFilters("i18n.ngettext",o,e,t,r,i),n.applyFilters("i18n.ngettext_"+l(i),o,e,t,r,i)):o},_nx:(e,t,r,i,o)=>{let s=c(o,i,e,t,r);return n?(s=n.applyFilters("i18n.ngettext_with_context",s,e,t,r,i,o),n.applyFilters("i18n.ngettext_with_context_"+l(o),s,e,t,r,i,o)):s},isRTL:()=>"rtl"===u("ltr","text direction"),hasTranslation:(e,t,i)=>{var o,s;const a=t?t+""+e:e;let c=!(null===(o=r.data)||void 0===o||null===(s=o[null!==i&&void 0!==i?i:"default"])||void 0===s||!s[a]);return n&&(c=n.applyFilters("i18n.has_translation",c,e,t,i),c=n.applyFilters("i18n.has_translation_"+l(i),c,e,t,i)),c}}})(void 0,void 0,I);U.getLocaleData.bind(U),U.setLocaleData.bind(U),U.resetLocaleData.bind(U),U.subscribe.bind(U),U.__.bind(U),U._x.bind(U),U._n.bind(U),U._nx.bind(U),U.isRTL.bind(U),U.hasTranslation.bind(U);class W extends r.Assessment{constructor(e){super(),this.settings=e}getConfig(e){return e.getConfig("countCharacters")?{parameters:{recommendedMinimum:300,recommendedMaximum:800}}:{parameters:{recommendedMinimum:150,recommendedMaximum:400}}}getResult(e,t){const n=t.getResearch("wordCountInText"),i=this.getConfig(t),o=new r.AssessmentResult,s=this.score(n.count,i);return o.setScore(s.score),o.setText(s.text),o}score(e,t){return e<=t.parameters.recommendedMinimum?{score:6,text:this.settings.video_body_short}:e>t.parameters.recommendedMinimum&&e=t.parameters.recommendedMaximum?{score:6,text:function(e){try{for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r")}:void 0}}(new class{constructor(){this._worker=analysisWorker}register(){this._worker.registerMessageHandler("initialize",this.initialize.bind(this),"YoastVideoSEO")}initialize(e){this.titleAssessment=new i(e),this.bodyAssessment=new W(e),this._worker.registerAssessment("videoTitle",this.titleAssessment,"YoastVideoSEO"),this._worker.registerAssessment("videoBody",this.bodyAssessment,"YoastVideoSEO")}}).register()}]); \ No newline at end of file diff --git a/languages/index.php b/languages/index.php new file mode 100644 index 0000000..9070b02 --- /dev/null +++ b/languages/index.php @@ -0,0 +1,4 @@ +\n" +"Language-Team: Yoast Translate \n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Poedit-Country: UNITED STATES\n" +"X-Poedit-SourceCharset: utf-8\n" +"X-Poedit-KeywordsList: __;_e;__ngettext:1,2;_n:1,2;__ngettext_noop:1,2;" +"_n_noop:1,2;_c,_nc:4c,1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;\n" +"X-Poedit-Basepath: .\n" +"X-Poedit-SearchPath-0: .\n" +"X-Poedit-Bookmarks: \n" +"X-Textdomain-Support: yes\n" +"X-Generator: CSL v1.x\n" +"X-Poedit-Language: English\n" + +#. translators: 1: link open tag; 2: link close tag. +#: classes/class-wpseo-video-admin-page.php:58 +msgid "" +"Please enable XML sitemaps in Yoast SEO > Settings > %1$sSite features%2$s." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:65 +msgid "General Settings" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:67 +msgid "Please find your video sitemap here:" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:67 +msgid "XML Video Sitemap" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:70 +msgid "Select at least one post type to enable the video sitemap." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:73 +msgid "Hide the sitemap from normal visitors?" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:74 +msgid "Disable Media RSS Enhancement" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:77 +msgid "Custom fields" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:78 +msgid "" +"Custom fields the plugin should check for video content (comma separated)" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:79 +msgid "(Optional) Embedly API Key" +msgstr "" + +#. translators: 1,3: link open tag; 2: link close tag. +#: classes/class-wpseo-video-admin-page.php:81 +msgid "" +"The video SEO plugin provides where possible enriched information about your " +"videos. A lot of %1$svideo services%2$s are supported by default. For those " +"services which aren't supported, we can try to retrieve enriched video " +"information using %3$sEmbedly%2$s. If you want to use this option, you'll " +"need to sign up for a (free) %3$sEmbedly%2$s account and provide the API key " +"you receive." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:84 +msgid "Embed Settings" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:86 +msgid "" +"Allow videos to be played directly on other websites, such as Facebook or " +"Twitter?" +msgstr "" + +#. translators: 1: link open tag, 2: link close tag. +#: classes/class-wpseo-video-admin-page.php:88 +msgid "Try to make videos responsive using %1$sFitVids.js%2$s?" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:90 +msgid "" +"YouTube embeds: make pages load faster by only loading the YouTube player " +"when the user clicks play." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:93 +msgid "Content width" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:94 +msgid "" +"This defaults to your themes content width, but if it's empty, setting a " +"value here will make sure videos are embedded in the right width." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:96 +msgid "Wistia domain" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:97 +msgid "" +"If you use Wistia in combination with a custom domain, set this to the " +"domain name you use for your Wistia videos, no http: or slashes needed." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:100 +msgid "Post Types for which to enable the Video SEO plugin" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:101 +msgid "Determine which post types on your site might contain video." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:112 +msgid "Taxonomies to include in XML Video Sitemap" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:113 +msgid "" +"You can also include your taxonomy archives, for instance, if you have " +"videos on a category page." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:133 +msgid "Indexation of videos in your content" +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:135 +msgid "" +"This process goes through all the post types specified by you, as well as " +"the terms of each taxonomy, to check for videos in the content. If the " +"plugin finds a video, it updates the metadata for that piece of content, so " +"it can add that metadata and content to the XML Video Sitemap." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:137 +msgid "" +"By default the plugin only checks content that hasn't been checked yet. " +"However, if you check 'Force Re-Index', it will re-check all content. This " +"is particularly interesting if you want to check for a video embed code that " +"wasn't supported before, or if you want to update thumbnail images en masse." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:143 +msgid "Force reindex of already indexed videos." +msgstr "" + +#: classes/class-wpseo-video-admin-page.php:146 +msgid "Re-Index Videos" +msgstr "" + +#: classes/class-wpseo-video-bootstrap.php:219 +msgid "" +"The PHP SPL extension seems to be unavailable. Please ask your web host to " +"enable it." +msgstr "" + +#: classes/class-wpseo-video-bootstrap.php:229 +msgid "" +"Please upgrade WordPress to the latest version to allow WordPress and the " +"Video SEO module to work properly." +msgstr "" + +#. translators: $1$s expands to Yoast SEO. +#: classes/class-wpseo-video-bootstrap.php:245 +msgid "" +"Please upgrade the %1$s plugin to the latest version to allow the Video SEO " +"module to work." +msgstr "" + +#: classes/class-wpseo-video-bootstrap.php:407 +msgid "Activation of Video SEO failed:" +msgstr "" + +#. translators: %1$s expands to Yoast SEO. +#: classes/class-wpseo-video-bootstrap.php:418 +msgid "" +"Please ask the (network) admin to install & activate %1$s and then enable " +"its XML sitemap functionality to allow the Video SEO module to work." +msgstr "" + +#. translators: %1$s and %3$s expand to anchor tags with a link to the download +#. page for Yoast SEO . %2$s expands to Yoast SEO. +#: classes/class-wpseo-video-bootstrap.php:444 +msgid "" +"Please %1$sinstall & activate %2$s%3$s and then enable its XML sitemap " +"functionality to allow the Video SEO module to work." +msgstr "" + +#: classes/class-wpseo-video-embed.php:46 +msgid "Load YouTube video" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:53 +msgid "Disable video" +msgstr "" + +#. translators: %s: post type name. +#: classes/class-wpseo-video-metabox.php:55 +msgid "Disables all Video SEO output for this %s" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:57 +msgid "Video Thumbnail" +msgstr "" + +#. translators: 1: link open tag; 2: link closing tag. +#: classes/class-wpseo-video-metabox.php:59 +msgid "Now set to %1$sthis image%2$s based on the embed code." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:60 +msgid "URL to thumbnail image (remember it'll be displayed as 16:9)" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:62 +msgid "Video Duration" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:63 +msgid "Overwrite the video duration, or enter one if it's empty." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:65 +msgid "Tags" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:66 +msgid "Add extra tags for this video" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:68 +msgid "Rating" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:69 +msgid "Set a rating between 0 and 5." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:71 +msgid "Not Family-friendly" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:72 +msgid "Mark this video as not Family-friendly" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:73 +msgid "" +"If this video should not be available for safe search users, check this box." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:108 +msgid "Video" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:239 +msgid "video" +msgstr "" + +#: classes/class-wpseo-video-metabox.php:240 +msgid "" +"You should consider adding the word \"video\" in your title, to optimize " +"your ability to be found by people searching for video." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:241 +msgid "" +"You're using the word \"video\" in your title, this optimizes your ability " +"to be found by people searching for video." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:242 +msgid "" +"Your body copy is too short for Search Engines to understand the topic of " +"your video, add some more content describing the contents of the video." +msgstr "" + +#: classes/class-wpseo-video-metabox.php:243 +msgid "" +"Your body copy is at optimal length for your video to be recognized by " +"Search Engines." +msgstr "" + +#. translators: 1: links to https:yoast.com/video-not-showing-search-results, +#. 2: closing link tag +#: classes/class-wpseo-video-metabox.php:245 +msgid "" +"Your body copy is quite long, make sure that the video is the most important " +"asset on the page, read %1$sthis post%2$s for more info." +msgstr "" + +#: classes/class-wpseo-video-schema-videoobject.php:198 +msgid "No description" +msgstr "" + +#: classes/class-wpseo-video-sitemap.php:380 +msgid "Video SEO" +msgstr "" + +#: classes/class-wpseo-video-sitemap.php:494 +msgid "Ignore." +msgstr "" + +#. translators: 1: link open tag, 2: link close tag. +#: classes/class-wpseo-video-sitemap.php:497 +msgid "" +"The VideoSEO upgrade which was just applied contains a lot of improvements. " +"It is strongly recommended that you %1$sre-index the video content on your " +"website%2$s with the 'force reindex' option checked." +msgstr "" + +#: views/reindex-page.php:8 +msgid "Re-indexation" +msgstr "" + +#: views/reindex-page.php:10 +msgid "" +"Your site is being indexed at the moment, so don't close this window. This " +"process may take a few minutes to complete." +msgstr "" + +#: views/reindex-page.php:28 +msgid "Estimated time to go:" +msgstr "" + +#: views/reindex-page.php:29 +msgid "Posts to go:" +msgstr "" + +#: views/reindex-page.php:30 +msgid "Total posts:" +msgstr "" + +#. translators: %1$s expands to a link start tag to the plugin settings page, +#. %2$s is the link closing tag. +#: views/reindex-page.php:36 +msgid "%1$sDone! Go back to the Video SEO settings%2$s" +msgstr "" + +#. Plugin Name of the plugin/theme +msgid "Yoast SEO: Video" +msgstr "" + +#. Plugin URI of the plugin/theme +msgid "https://yoa.st/4fh" +msgstr "" + +#. Description of the plugin/theme +msgid "" +"The Yoast Video SEO plugin makes sure your videos are recognized by search " +"engines and social platforms, so they look good when found on these social " +"platforms and in the search results." +msgstr "" + +#. Author of the plugin/theme +msgid "Team Yoast" +msgstr "" + +#. Author URI of the plugin/theme +msgid "https://yoa.st/team-yoast-video" +msgstr "" + +#: js/src/components/OpenGooglePreview.js:50 +msgid "Title and description" +msgstr "" + +#: js/src/components/OpenGooglePreview.js:51 +msgid "" +"Your video will use the same SEO title and meta description you enter in our " +"Google Preview editor." +msgstr "" + +#: js/src/components/OpenGooglePreview.js:60 +msgid "Open Google Preview editor" +msgstr "" + +#: js/src/components/VideoSettings.js:115 +msgid "Mark this video as not family friendly" +msgstr "" + +# %s expands to an opening anchor tag, %s expands to a closing anchor tag +#: js/src/components/VideoSettings.js:32 +msgid "To learn more, read the %sYoast SEO Video configuration guide.%s" +msgstr "" + +#: js/src/components/VideoSettings.js:58 +msgid "" +"It looks like your content does not yet contain a video. Please add a video " +"and save your draft in order for Video SEO to work." +msgstr "" + +#: js/src/components/VideoSettings.js:68 +msgid "We've made some changes to Yoast SEO Video." +msgstr "" + +#: js/src/components/VideoSettings.js:74 +msgid "Learn more about what has changed." +msgstr "" + +#: js/src/components/VideoSettings.js:78 +msgid "" +"Edit the Schema information below to optimize your video for search engines. " +msgstr "" + +#: js/src/components/VideoSettings.js:84 +msgid "Video thumbnail" +msgstr "" + +#: js/src/components/VideoSettings.js:95 +msgid "Duration" +msgstr "" diff --git a/languages/yoast-video-seojs-ar.json b/languages/yoast-video-seojs-ar.json new file mode 100644 index 0000000..b0f59d7 --- /dev/null +++ b/languages/yoast-video-seojs-ar.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;","lang":"ar"},"Duration":["المدة"],"Video thumbnail":["صورة الفيديو المصغرة"],"Edit the Schema information below to optimize your video for search engines. ":["قم بتحرير معلومات المخطط Schema أدناه لتحسين الفيديو الخاص بك لمحركات البحث."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["لمعرفة المزيد ، اقرأ دليل تكوين Video %s Yoast SEO.%s"],"Learn more about what has changed.":["تعرف على المزيد حول ما تغير."],"We've made some changes to Yoast SEO Video.":["لقد أجرينا بعض التغييرات على Yoast SEO Video."],"Open Google Preview editor":["افتح محرر معاينة Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["سيستخدم مقطع الفيديو الخاص بك نفس عنوان SEO ووصف التعريف الذي تدخله في محرر معاينة Google."],"Title and description":["العنوان والوصف"],"Mark this video as not family friendly":["حدد هذا الفيديو على أنه ليس مناسبًا للعائلة"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["يبدو أن محتواك لا يحتوي على فيديو حتى الآن. الرجاء إضافة مقطع فيديو وحفظ مسودتك لكي يعمل فيديو سيو"],"Tags":["وسوم"],"If this video should not be available for safe search users, check this box.":["إذا لم يكن هذا الفيديو متاحًا لمستخدمي البحث الآمن ، فحدّد هذا المربع."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-bg_BG.json b/languages/yoast-video-seojs-bg_BG.json new file mode 100644 index 0000000..9f900ad --- /dev/null +++ b/languages/yoast-video-seojs-bg_BG.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"bg"},"Duration":[],"Video thumbnail":[],"Edit the Schema information below to optimize your video for search engines. ":[],"To learn more, read the %sYoast SEO Video configuration guide.%s":[],"Learn more about what has changed.":[],"We've made some changes to Yoast SEO Video.":[],"Open Google Preview editor":[],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[],"Title and description":[],"Mark this video as not family friendly":[],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Изглежда съдържанието на публикацията все още не съдържа видео. Добавете видео и запишете чернова версия, за да сработи Video SEO."],"Tags":["Етикети"],"If this video should not be available for safe search users, check this box.":["Ако видеоклипът не трябва да се показва при safe search търсения, активирайте тази опция."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-cs_CZ.json b/languages/yoast-video-seojs-cs_CZ.json new file mode 100644 index 0000000..4528ab8 --- /dev/null +++ b/languages/yoast-video-seojs-cs_CZ.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;","lang":"cs_CZ"},"Duration":["Délka"],"Video thumbnail":["Miniatura videa"],"Edit the Schema information below to optimize your video for search engines. ":["Upravte níže uvedené informace o schématu a optimalizujte své video pro vyhledávače."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Chcete-li se dozvědět více, přečtěte si průvodce konfigurace %sYoast SEO video.%s"],"Learn more about what has changed.":["Další informace o tom, co se změnilo."],"We've made some changes to Yoast SEO Video.":["U Yoast SEO Video jsme provedli několik změn."],"Open Google Preview editor":["Otevřete editor Google náhledu"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Vaše video bude používat stejný SEO název a metapopis, který zadáte v našem editoru náhledu Google."],"Title and description":["Název a popis"],"Mark this video as not family friendly":["Označit toto video, že není pro rodiny s dětmi"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Vypadá to, že váš obsah ještě neobsahuje video. Prosím přidejte video a uložte rozpracovaný projekt aby Video SEO fungovalo."],"Tags":["Štítky"],"If this video should not be available for safe search users, check this box.":["Pokud by toto video nemělo být k dispozici uživatelům bezpečného vyhledávání, zaškrtněte toto políčko."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-da_DK.json b/languages/yoast-video-seojs-da_DK.json new file mode 100644 index 0000000..603aaf7 --- /dev/null +++ b/languages/yoast-video-seojs-da_DK.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"da_DK"},"Duration":["Varighed"],"Video thumbnail":["Video miniature billede"],"Edit the Schema information below to optimize your video for search engines. ":["Rediger skemaoplysningerne nedenfor for at optimere videoen til søgemaskiner. "],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Hvis du vil vide mere, kan du læse %sYoast SEO Video indstillingsvejledning.%s"],"Learn more about what has changed.":["Læs mere om, hvad der er ændret."],"We've made some changes to Yoast SEO Video.":["Vi har foretaget nogle ændringer i Yoast SEO Video."],"Open Google Preview editor":["Åbn Google forhåndsvisningseditor"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Din video vil bruge den samme SEO-titel og metabeskrivelse, som du indtaster i vores Google forhåndsvisningseditor."],"Title and description":["Titel og beskrivelse"],"Mark this video as not family friendly":["Marker denne video som ikke-familievenlig"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Det ser ud, som om dit indhold ikke indeholder nogen video. Tilføj venligst en video og gem din kladde, så Video SEO kan virke."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["Markér dette felt, hvis denne video ikke skal vises i beskyttede søgninger (dvs. for safe search-brugere)."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-de_DE.json b/languages/yoast-video-seojs-de_DE.json new file mode 100644 index 0000000..ca7ceb6 --- /dev/null +++ b/languages/yoast-video-seojs-de_DE.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"de"},"Duration":["Dauer"],"Video thumbnail":["Video-Vorschaubild"],"Edit the Schema information below to optimize your video for search engines. ":["Bearbeite die Schema-Information unten, um dein Video für Suchmaschinen zu optimieren."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Um mehr zu erfahren, lies die %sYoast-SEO-Video-Konfigurationsanleitung.%s"],"Learn more about what has changed.":["Mehr über die Änderungen erfahren."],"We've made some changes to Yoast SEO Video.":["Wir haben einige Änderungen zu Yoast SEO Video gemacht."],"Open Google Preview editor":["Google-Vorschau-Editor öffnen"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Dein Video wird denselben SEO-Titel und Meta-Beschreibung benutzen, wie du es in unserem Google-Vorschau-Editor eingegeben hast."],"Title and description":["Titel und Beschreibung"],"Mark this video as not family friendly":["Dieses Video als nicht familienfreundlich markieren"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Es sieht so aus, als enthält dein Inhalt kein Video. Bitte für ein Video hinzu und speichere den Entwurf damit Video SEO arbeiten kann. "],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["Wenn dieses Video nur Benutzern angezeigt werden soll, welche die sichere Suche (\"safe search\") verwenden, aktiviere diese Option."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-en_AU.json b/languages/yoast-video-seojs-en_AU.json new file mode 100644 index 0000000..cdfe62e --- /dev/null +++ b/languages/yoast-video-seojs-en_AU.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"en_AU"},"Duration":["Duration"],"Video thumbnail":["Video thumbnail"],"Edit the Schema information below to optimize your video for search engines. ":["Edit the Schema information below to optimise your video for search engines. "],"To learn more, read the %sYoast SEO Video configuration guide.%s":["To learn more, read the %sYoast SEO Video configuration guide.%s"],"Learn more about what has changed.":["Learn more about what has changed."],"We've made some changes to Yoast SEO Video.":["We've made some changes to Yoast SEO Video."],"Open Google Preview editor":["Open Google Preview editor"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Your video will use the same SEO title and meta description you enter in our Google Preview editor."],"Title and description":["Title and description"],"Mark this video as not family friendly":["Mark this video as not family friendly"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["If this video should not be available for safe search users, check this box."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-en_CA.json b/languages/yoast-video-seojs-en_CA.json new file mode 100644 index 0000000..7d4e87a --- /dev/null +++ b/languages/yoast-video-seojs-en_CA.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"en_CA"},"Duration":[],"Video thumbnail":[],"Edit the Schema information below to optimize your video for search engines. ":[],"To learn more, read the %sYoast SEO Video configuration guide.%s":[],"Learn more about what has changed.":[],"We've made some changes to Yoast SEO Video.":[],"Open Google Preview editor":[],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[],"Title and description":[],"Mark this video as not family friendly":[],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["If this video should not be available for safe search users, check this box."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-en_GB.json b/languages/yoast-video-seojs-en_GB.json new file mode 100644 index 0000000..f5c684f --- /dev/null +++ b/languages/yoast-video-seojs-en_GB.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"en_GB"},"Duration":["Duration"],"Video thumbnail":["Video thumbnail"],"Edit the Schema information below to optimize your video for search engines. ":["Edit the Schema information below to optimise your video for search engines. "],"To learn more, read the %sYoast SEO Video configuration guide.%s":["To learn more, read the %sYoast SEO Video configuration guide.%s"],"Learn more about what has changed.":["Learn more about what has changed."],"We've made some changes to Yoast SEO Video.":["We've made some changes to Yoast SEO Video."],"Open Google Preview editor":["Open Google Preview editor"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Your video will use the same SEO title and meta description you enter in our Google Preview editor."],"Title and description":["Title and description"],"Mark this video as not family friendly":["Mark this video as not family friendly"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["If this video should not be available for safe search users, check this box."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-en_NZ.json b/languages/yoast-video-seojs-en_NZ.json new file mode 100644 index 0000000..3adc032 --- /dev/null +++ b/languages/yoast-video-seojs-en_NZ.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"en_NZ"},"Duration":[],"Video thumbnail":[],"Edit the Schema information below to optimize your video for search engines. ":[],"To learn more, read the %sYoast SEO Video configuration guide.%s":[],"Learn more about what has changed.":[],"We've made some changes to Yoast SEO Video.":[],"Open Google Preview editor":[],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[],"Title and description":[],"Mark this video as not family friendly":[],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["If this video should not be available for safe search users, check this box."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-es_ES.json b/languages/yoast-video-seojs-es_ES.json new file mode 100644 index 0000000..0268577 --- /dev/null +++ b/languages/yoast-video-seojs-es_ES.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"es"},"Duration":["Duración"],"Video thumbnail":["Miniatura del vídeo"],"Edit the Schema information below to optimize your video for search engines. ":["Edita abajo la información de Schema para optimizar tu vídeo para los motores de búsqueda."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Para aprender más lee la %sguía de configuración de vídeo de Yoast SEO.%s"],"Learn more about what has changed.":["Aprende m ás sobre lo que ha cambiado."],"We've made some changes to Yoast SEO Video.":["Hemos hecho algunos cambios a Yoast SEO Video."],"Open Google Preview editor":["Abrir el editor de vista previa de Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Tu vídeo usará el mismo title y meta description SEO que introduzcas en nuestro editor de vista previa de Google."],"Title and description":["Título y descripción"],"Mark this video as not family friendly":["Marcar este vídeo como no recomendable para familias"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Parece que tu contenido no contiene aún ningún vídeo. Por favor, añade un vídeo y guarda tu borrador para que funcione Vídeo SEO."],"Tags":["Etiquetas"],"If this video should not be available for safe search users, check this box.":["Si este vídeo no debería estar disponible para usuarios con la búsqueda segura activa marca esta casilla."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-es_MX.json b/languages/yoast-video-seojs-es_MX.json new file mode 100644 index 0000000..5caef91 --- /dev/null +++ b/languages/yoast-video-seojs-es_MX.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"es_MX"},"Duration":["Duración"],"Video thumbnail":["Miniatura de video"],"Edit the Schema information below to optimize your video for search engines. ":["Edita la información de Schema de abajo para optimizar tu vídeo para los motores de búsqueda."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Para aprender más, lee la %sGuía de configuración de Yoast SEO Video.%s"],"Learn more about what has changed.":["Aprende más sobre lo que ha cambiado."],"We've made some changes to Yoast SEO Video.":["Hemos hecho algunos cambios a Yoast SEO Video."],"Open Google Preview editor":["Abrir el editor de vista previa de Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Tu video utilizará el título SEO y la meta descripción que has escrito en nuestro editor de vista previa de Google."],"Title and description":["Título y descripción"],"Mark this video as not family friendly":["Marca este video como no apto para toda la familia"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Parece que tu contenido no contiene aún ningún vídeo. Por favor, añade un vídeo y guarda tu borrador para que funcione Vídeo SEO."],"Tags":["Etiquetas"],"If this video should not be available for safe search users, check this box.":["Marca esta casilla si este vídeo no debería estar disponible para usuarios que usen la búsqueda segura."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-fa_IR.json b/languages/yoast-video-seojs-fa_IR.json new file mode 100644 index 0000000..60315c4 --- /dev/null +++ b/languages/yoast-video-seojs-fa_IR.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=1; plural=0;","lang":"fa"},"Duration":["مدت زمان"],"Video thumbnail":["تصویر بندانگشتی ویدئو"],"Edit the Schema information below to optimize your video for search engines. ":["برای بهینه سازی ویدیوی خود برای موتورهای جستجو ، اطلاعات Schema زیر را ویرایش کنید."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["بررای کسب اطلاعات بیشتر، %s راهنمای پیکربندی سئوی ویدئو%s را مطالعه کنید"],"Learn more about what has changed.":["اطلاعات بیشتر درباره تغییرات اخیر."],"We've made some changes to Yoast SEO Video.":["تغییراتی را در Yoast SEO Video اعمال کردیم."],"Open Google Preview editor":["ویرایشگر پیشنمایش گوگل را باز کنید"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["ویدیوی شما از همان عنوان سئو و توضیحات متای شما در ویرایشگر پیشنمایش گوگل ما استفاده می کند."],"Title and description":["عنوان و توضیحات"],"Mark this video as not family friendly":["این ویدیو را به عنوان دوستانه خانوادگی علامت گذاری نکنید"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["بنظر می رسد محتوای شما هنوز ویدئویی ندارد. لطفا ویدئویی افزوده و پیشنویستان را ذخیره کنید تا سئوی ویدئو عمل نماید."],"Tags":["برچسب‌ها"],"If this video should not be available for safe search users, check this box.":["اگر اینرا چک بزنید، این ویدیو فقط برای کاربران با جستجوی امن قابل دسترس می شود."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-fi.json b/languages/yoast-video-seojs-fi.json new file mode 100644 index 0000000..1b231fe --- /dev/null +++ b/languages/yoast-video-seojs-fi.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"fi"},"Duration":["Kesto"],"Video thumbnail":["Videon pienoiskuva"],"Edit the Schema information below to optimize your video for search engines. ":["Muokkaa alta löytyviä Schema-tietoja optimoidaksesi videosi hakukoneita varten."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Oppiaksesi lisää, lue %sYoast SEO Videon asetusopas%s."],"Learn more about what has changed.":["Lue lisää siitä mikä on muuttunut."],"We've made some changes to Yoast SEO Video.":["Olemme tehneet joitakin muutoksia Yoast SEO Videoon."],"Open Google Preview editor":["Avaa Google Preview -editori"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Videosi tulee käyttämään samaa SEO-otsikkoa ja metakuvausta, jonka kirjoitat Google Preview -editoriin."],"Title and description":["Otsikko ja kuvaus"],"Mark this video as not family friendly":["Merkitse tämä video lapsilta kielletyksi"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Vaikuttaa siltä, että sisällössäsi ei vielä ole yhtään videota. Lisää video ja tallenna luonnos, jotta Video SEO voi toimia."],"Tags":["Avainsanat"],"If this video should not be available for safe search users, check this box.":["Valitse tämä ruutu, mikäli tätä videota ei tule näyttää turvallisten hakujen käyttäjille."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-fr_FR.json b/languages/yoast-video-seojs-fr_FR.json new file mode 100644 index 0000000..3f4f79b --- /dev/null +++ b/languages/yoast-video-seojs-fr_FR.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n > 1;","lang":"fr"},"Duration":["Durée"],"Video thumbnail":["Miniature de la vidéo"],"Edit the Schema information below to optimize your video for search engines. ":["Modifiez les informations Schema ci-dessous pour optimiser votre vidéo pour les moteurs de recherche."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Pour en savoir plus, consultez le %sguide de configuration de Yoast SEO Video.%s"],"Learn more about what has changed.":["En savoir plus sur ce qui a changé."],"We've made some changes to Yoast SEO Video.":["Nous avons apporté quelques modifications à Yoast SEO Video."],"Open Google Preview editor":["Ouvrir l’éditeur de prévisualisation Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Votre vidéo utilisera le même titre SEO et la même méta-description que vous saisissez dans notre éditeur de prévisualisation Google"],"Title and description":["Titre et description"],"Mark this video as not family friendly":["Marquer cette vidéo comme non familiale"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Il semblerait que votre contenu ne contienne pas encore de vidéo. Veuillez ajouter une vidéo et enregistrer votre brouillon pour que le référencement de Vidéo Yoast fonctionne."],"Tags":["Mots-clés"],"If this video should not be available for safe search users, check this box.":["Si cette vidéo ne doit être disponible que pour les recherches protégées, cochez cette case."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-he_IL.json b/languages/yoast-video-seojs-he_IL.json new file mode 100644 index 0000000..3e64f9d --- /dev/null +++ b/languages/yoast-video-seojs-he_IL.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"he_IL"},"Duration":["משך"],"Video thumbnail":["תמונת וידאו"],"Edit the Schema information below to optimize your video for search engines. ":["ערוך את המידע על הסכימה שלהלן כדי לייעל את הסרטון שלך למנועי חיפוש."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["למידע נוסף, קרא את %sהמדריך לתצורת הווידאו של Yoast SEO%s."],"Learn more about what has changed.":["למידע נוסף על מה השתנה."],"We've made some changes to Yoast SEO Video.":["ביצענו כמה שינויים בסרטון של Yoast SEO."],"Open Google Preview editor":["פתח את עורך התצוגה המקדימה של גוגל"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["הסרטון ישתמש באותה כותרת SEO והתיאור שהזנת בעורך התצוגה המקדימה של גוגל."],"Title and description":["כותרת ותיאור"],"Mark this video as not family friendly":["סמן את הסרטון הזה כלא ידידותי למשפחה"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["נראה שהתוכן עדיין לא מכיל סרטונים. יש להוסיף סרטון ולשמור טיוטה כדי שהוידאו SEO יופעל."],"Tags":["תגיות"],"If this video should not be available for safe search users, check this box.":["אם סרטון זה לא אמור להיות זמין למשתמשים בחיפוש בטוח, יש לסמן תיבה זו."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-hi_IN.json b/languages/yoast-video-seojs-hi_IN.json new file mode 100644 index 0000000..fec91d0 --- /dev/null +++ b/languages/yoast-video-seojs-hi_IN.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"hi_IN"},"Duration":["अवधि"],"Video thumbnail":["वीडियो थंबनेल"],"Edit the Schema information below to optimize your video for search engines. ":["खोज इंजन के लिए अपने वीडियो का अनुकूलन करने के लिए नीचे स्कीमा जानकारी संपादित करें।"],"To learn more, read the %sYoast SEO Video configuration guide.%s":["अधिक जानने के लिए, %sयोस्ट एसईओ वीडियो कॉन्फ़िगरेशन गाइड%s पढ़ें।"],"Learn more about what has changed.":["जो बदल गया है, उसके बारे में और जानें।"],"We've made some changes to Yoast SEO Video.":["हमने योस्ट एसईओ वीडियो में कुछ बदलाव किए हैं।"],"Open Google Preview editor":["गूगल पूर्वावलोकन संपादक खोलें"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["आपका वीडियो उसी एसईओ शीर्षक और मेटा विवरण का उपयोग करेगा जो आप हमारे गूगल पूर्वावलोकन संपादक में दर्ज करते हैं।"],"Title and description":["शीर्षक और विवरण"],"Mark this video as not family friendly":["इस वीडियो को परिवार के अनुकूल नहीं चिह्नित करें"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["ऐसा लगता है कि आपकी सामग्री में अभी तक एक वीडियो नहीं है। कृपया वीडियो जोड़ें और कार्य करने के लिए वीडियो एसईओ के लिए अपने ड्राफ्ट को सहेजें।"],"Tags":["टैग्स"],"If this video should not be available for safe search users, check this box.":["यदि यह वीडियो सुरक्षित खोज उपयोगकर्ताओं के लिए उपलब्ध नहीं होना चाहिए, तो इस बॉक्स को देखें।"]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-hu_HU.json b/languages/yoast-video-seojs-hu_HU.json new file mode 100644 index 0000000..14e7a3a --- /dev/null +++ b/languages/yoast-video-seojs-hu_HU.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"hu"},"Duration":[],"Video thumbnail":[],"Edit the Schema information below to optimize your video for search engines. ":[],"To learn more, read the %sYoast SEO Video configuration guide.%s":[],"Learn more about what has changed.":[],"We've made some changes to Yoast SEO Video.":[],"Open Google Preview editor":[],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[],"Title and description":[],"Mark this video as not family friendly":[],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Úgy tűnik, hogy még nem került videó beillesztésre a tartalomba. Kérlek adj hozzá egy videót és mentsd el a piszkozatba, hogy a Videó SEO működjön."],"Tags":["Címkék"],"If this video should not be available for safe search users, check this box.":["Ha ezt a videót csak biztonságos kereséshez ajánlod, akkor jelöld be ezt az opciót."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-id_ID.json b/languages/yoast-video-seojs-id_ID.json new file mode 100644 index 0000000..3f4845b --- /dev/null +++ b/languages/yoast-video-seojs-id_ID.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n > 1;","lang":"id"},"Duration":["Durasi"],"Video thumbnail":["Thumbnail video"],"Edit the Schema information below to optimize your video for search engines. ":["Edit informasi Skema berikut untuk mengoptimalkan video Anda di mesin pencari."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Untuk mempelajari lebih lanjut, baca %spanduan pengaturan Yoast SEO Video.%s"],"Learn more about what has changed.":["Pelajari lebih lanjut tentang perubahan."],"We've made some changes to Yoast SEO Video.":["Kami membuat perubahan pada Yoast SEO Video."],"Open Google Preview editor":["Buka editor Google Preview"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Video Anda akan menggunakan judul SEO dan meta deskripsi yang Anda masukkan di editor Google Preview kami."],"Title and description":["Judul dan deskripsi"],"Mark this video as not family friendly":["Tandai video ini tidak ramah keluarga"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Tampaknya konten Anda belum memiliki video. Silakan tambahkan video dan simpan dalam draf Anda supaya SEO Video berfungsi."],"Tags":["Tag"],"If this video should not be available for safe search users, check this box.":["Jika video ini seharusnya tidak tersedia untuk pengguna pencarian yang aman, centang kotak ini."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-it_IT.json b/languages/yoast-video-seojs-it_IT.json new file mode 100644 index 0000000..1615ac9 --- /dev/null +++ b/languages/yoast-video-seojs-it_IT.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"it"},"Duration":["Durata"],"Video thumbnail":["Miniatura del video"],"Edit the Schema information below to optimize your video for search engines. ":["Modifica le informazioni Schema qui sotto per ottimizzare il tuo video per i motori di ricerca. "],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Per maggiori informazioni, leggi la %sguida di configurazione a Yoast SEO Video.%s"],"Learn more about what has changed.":["Scopri cosa è cambiato."],"We've made some changes to Yoast SEO Video.":["Abbiamo apportato qualche modifica a Yoast SEO Video."],"Open Google Preview editor":["Apri l'Anteprima di Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Il tuo video userà lo stesso titolo SEO e la stessa meta descrizione che inserisci nella metabox, nei campi dell'anteprima di Google."],"Title and description":["Titolo e descrizione"],"Mark this video as not family friendly":["Segnala questo video come non adatto alle famiglie"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Sembra che il tuo contenuto non abbia ancora alcun video. Aggiungi un video e salva la tua bozza per permettere a Video SEO di funzionare."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["Se questo video non dovrebbe essere disponibile per gli utenti che hanno attivato la ricerca sicura, selezionare questa casella."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-ja.json b/languages/yoast-video-seojs-ja.json new file mode 100644 index 0000000..4ea26c2 --- /dev/null +++ b/languages/yoast-video-seojs-ja.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=1; plural=0;","lang":"ja_JP"},"Duration":["期間"],"Video thumbnail":["サムネイル"],"Edit the Schema information below to optimize your video for search engines. ":["以下のスキーマ情報を編集して、検索エンジン用に動画を最適化します。"],"To learn more, read the %sYoast SEO Video configuration guide.%s":["詳細については、%sYoast SEO Video 設定ガイドをお読みください。%s"],"Learn more about what has changed.":["何が変わったかについてもっと学びましょう。"],"We've made some changes to Yoast SEO Video.":["Yoast SEO Video に変更を加えました。"],"Open Google Preview editor":["Googleプレビューエディターを開く"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["動画には、Google プレビューエディターに入力したものと同じ SEO タイトルとメタディスクリプションが使用されます。"],"Title and description":["タイトルと説明"],"Mark this video as not family friendly":["この動画を「家族向けではない」とマークする"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["コンテンツにまだ動画が含まれていないようです。Video SEOを機能させるには、動画を追加して下書きを保存してください。"],"Tags":["タグ"],"If this video should not be available for safe search users, check this box.":["この動画をセーフサーチユーザーが利用できないようにする場合は、このチェックボックスをオンにします。"]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-ms_MY.json b/languages/yoast-video-seojs-ms_MY.json new file mode 100644 index 0000000..de3c713 --- /dev/null +++ b/languages/yoast-video-seojs-ms_MY.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=1; plural=0;","lang":"ms"},"Duration":[],"Video thumbnail":[],"Edit the Schema information below to optimize your video for search engines. ":[],"To learn more, read the %sYoast SEO Video configuration guide.%s":[],"Learn more about what has changed.":[],"We've made some changes to Yoast SEO Video.":[],"Open Google Preview editor":[],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[],"Title and description":[],"Mark this video as not family friendly":[],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Nampaknya kandungan anda tidak mengandungi video. Sila masukkan video dan simpan draf anda untuk membolehkan Video SEO berfungsi."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":[]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-nl_BE.json b/languages/yoast-video-seojs-nl_BE.json new file mode 100644 index 0000000..aea2265 --- /dev/null +++ b/languages/yoast-video-seojs-nl_BE.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"nl_BE"},"Duration":["Duur"],"Video thumbnail":["Video thumbnail"],"Edit the Schema information below to optimize your video for search engines. ":["Bewerk de onderstaande Schema-informatie om je video voor zoekmachines te optimaliseren."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Lees voor meer informatie de %sYoast SEO Video configuratiegids%s."],"Learn more about what has changed.":["Lees meer over wat er is veranderd."],"We've made some changes to Yoast SEO Video.":["We hebben enkele wijzigingen aangebracht in Yoast SEO Video."],"Open Google Preview editor":["Open Google Preview editor"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Je video gebruikt dezelfde SEO-titel en metabeschrijving die je invoert in onze Google Preview-editor."],"Title and description":["Titel en beschrijving"],"Mark this video as not family friendly":["Markeer deze video als kindonvriendelijk"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Het ziet er naar uit dat jouw content nog geen video bevat. Voeg een video toe en sla je concept op om Video SEO te laten werken. "],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["Als deze video niet beschikbaar moet zijn voor de safe search gebruikers, zet dan een vinkje."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-nl_NL.json b/languages/yoast-video-seojs-nl_NL.json new file mode 100644 index 0000000..547a7f0 --- /dev/null +++ b/languages/yoast-video-seojs-nl_NL.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"nl"},"Duration":["Duur"],"Video thumbnail":["Video thumbnail"],"Edit the Schema information below to optimize your video for search engines. ":["Bewerk de onderstaande Schema-informatie om je video voor zoekmachines te optimaliseren."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Lees voor meer informatie de %sYoast SEO Video configuratiegids%s."],"Learn more about what has changed.":["Lees meer over wat er is veranderd."],"We've made some changes to Yoast SEO Video.":["We hebben enkele wijzigingen aangebracht in Yoast SEO Video."],"Open Google Preview editor":["Open Google Preview editor"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Je video gebruikt dezelfde SEO-titel en metabeschrijving die je invoert in onze Google Preview-editor."],"Title and description":["Titel en beschrijving"],"Mark this video as not family friendly":["Markeer deze video als kindonvriendelijk"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Het ziet er naar uit dat jouw content nog geen video bevat. Voeg een video toe en sla je concept op om Video SEO te laten werken. "],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["Als deze video niet beschikbaar moet zijn voor de safe search gebruikers, zet dan een vinkje."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-pl_PL.json b/languages/yoast-video-seojs-pl_PL.json new file mode 100644 index 0000000..2786c94 --- /dev/null +++ b/languages/yoast-video-seojs-pl_PL.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);","lang":"pl"},"Duration":["Czas trwania"],"Video thumbnail":["Miniaturka filmu"],"Edit the Schema information below to optimize your video for search engines. ":["Edytuj informacje Schema poniżej, aby zoptymalizować swój film dla wyszukiwarek. "],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Aby dowiedzieć się więcej, przeczytaj %sprzewodnik po konfiguracji wideo w Yoast SEO.%s"],"Learn more about what has changed.":["Dowiedz się więcej, co się zmieniło."],"We've made some changes to Yoast SEO Video.":["Wprowadziliśmy kilka zmian w Yoast SEO Video."],"Open Google Preview editor":["Otwórz edytor Google Preview"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Twoje wideo będzie używać tego samego tytułu SEO i meta opisu, który wprowadzisz w edytorze podglądu Google."],"Title and description":["Tytuł i opis"],"Mark this video as not family friendly":["Oznacz ten film jako nieprzyjazny rodzinie"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Wygląda na to, że w treści nie został osadzony film. Dodaj film i zapisz szkic, aby wtyczka Video SEO mogła działać."],"Tags":["Tagi"],"If this video should not be available for safe search users, check this box.":["Jeżeli ten film powinien być dostępny jedynie dla użytkowników korzystających z bezpiecznego wyszukiwania, zaznacz tę opcję."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-pt_BR.json b/languages/yoast-video-seojs-pt_BR.json new file mode 100644 index 0000000..9db6308 --- /dev/null +++ b/languages/yoast-video-seojs-pt_BR.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=(n > 1);","lang":"pt_BR"},"Duration":["Duração"],"Video thumbnail":["Miniatura de vídeo"],"Edit the Schema information below to optimize your video for search engines. ":["Edite as informações do esquema abaixo para otimizar seu vídeo para mecanismos de pesquisa."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Para saber mais, leia o guia de configuração de vídeo %sYoast SEO.%s"],"Learn more about what has changed.":["Saiba mais sobre o que mudou."],"We've made some changes to Yoast SEO Video.":["Fizemos algumas alterações no Yoast SEO Video."],"Open Google Preview editor":["Abra o editor de visualização do Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Seu vídeo usará o mesmo título de SEO e meta descrição que você inseriu em nosso editor de visualização do Google."],"Title and description":["Título e descrição"],"Mark this video as not family friendly":["Marque este vídeo como não adequado para famílias"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Parece que o seu conteúdo ainda não contém um vídeo. Por favor, adicionar um vídeo e salvar o seu projeto, a fim de trabalhar SEO no seu vídeo."],"Tags":["Tags"],"If this video should not be available for safe search users, check this box.":["Se este vídeo só deverá estar disponível para os usuários de pesquisa segura, marque esta caixa."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-pt_PT.json b/languages/yoast-video-seojs-pt_PT.json new file mode 100644 index 0000000..13e949e --- /dev/null +++ b/languages/yoast-video-seojs-pt_PT.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"pt"},"Duration":["Duração"],"Video thumbnail":["Miniatura do vídeo"],"Edit the Schema information below to optimize your video for search engines. ":["Edite a informação de Schema abaixo para optimizar o seu vídeo para motores de pesquisa."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Para saber mais, leia o %sguia de configuração do Yoast SEO Video%s."],"Learn more about what has changed.":["Saiba mais sobre o que foi alterado."],"We've made some changes to Yoast SEO Video.":["Fizemos algumas alterações ao Yoast SEO Video."],"Open Google Preview editor":["Abrir editor de pré-visualização do Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["O seu vídeo irá utilizar o mesmo título e descrição SEO que inseriu no editor da pré-visualização do Google."],"Title and description":["Título e descrição"],"Mark this video as not family friendly":["Marcar este vídeo como não apropriado para famílias"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Parece que o seu conteúdo ainda não contém um vídeo. Por favor, adicione um vídeo e guarde o seu rascunho de forma a que o Video SEO possa funcionar."],"Tags":["Etiquetas"],"If this video should not be available for safe search users, check this box.":["Se este vídeo não deve estar disponível para utilizadores com pesquisa segura, seleccione esta caixa."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-ro_RO.json b/languages/yoast-video-seojs-ro_RO.json new file mode 100644 index 0000000..85315eb --- /dev/null +++ b/languages/yoast-video-seojs-ro_RO.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);","lang":"ro"},"Duration":["Durată"],"Video thumbnail":["Miniatură video"],"Edit the Schema information below to optimize your video for search engines. ":["Editează informațiile despre Schema.org de mai jos pentru a-ți optimiza videoclipul pentru motoarele de căutare."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Pentru a afla mai multe, citește %sghidul de configurare Yoast SEO Video.%s"],"Learn more about what has changed.":["Află mai multe despre ce s-a schimbat."],"We've made some changes to Yoast SEO Video.":["Am făcut câteva modificări la Yoast SEO Video."],"Open Google Preview editor":["Deschide editorul de previzualizare Google"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Videoul tău va folosi același titlu SEO și descriere meta pe care le-ai introdus în editorul de previzualizare Google."],"Title and description":["Titlu și descriere"],"Mark this video as not family friendly":["Marchează acest video ca neprietenos pentru mediul familial"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Se pare că nu ai încă niciun video în conținutul tău. Te rog adaugă un video și salvează-l ca o ciornă pentru ca Video SEO să funcționeze."],"Tags":["Etichete"],"If this video should not be available for safe search users, check this box.":["Dacă acest video nu ar trebui să fie disponibil pentru utilizatorii care caută numai videouri sigure, bifează această casetă."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-ru_RU.json b/languages/yoast-video-seojs-ru_RU.json new file mode 100644 index 0000000..f2ea21f --- /dev/null +++ b/languages/yoast-video-seojs-ru_RU.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);","lang":"ru"},"Duration":["Продолжительность"],"Video thumbnail":["Миниатюра видео"],"Edit the Schema information below to optimize your video for search engines. ":["Отредактируйте данные схемы ниже для оптимизации видео для поисковых движков."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Чтобы узнать больше, прочитайте %sруководство по настройке Yoast SEO Video.%s"],"Learn more about what has changed.":["Узнайте больше об изменениях."],"We've made some changes to Yoast SEO Video.":["Мы внесли некотоые изменения в Yoast SEO Video."],"Open Google Preview editor":["Открыть редактор Google Preview"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["В вашем видео будет использовано то же название SEO и мета-описание, которое вы введете в нашем редакторе Google Preview."],"Title and description":["Название и описание"],"Mark this video as not family friendly":["Пометить видео, как не подходящее для семейного просмотра"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Похоже, что контент не содержит видео. Пожалуйста добавьте видео и сохраните черновик."],"Tags":["Тэги"],"If this video should not be available for safe search users, check this box.":["Если это видео должно быть доступно только для безопасного пользовательского поиска, установите этот флажок."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-sk_SK.json b/languages/yoast-video-seojs-sk_SK.json new file mode 100644 index 0000000..8d3a4be --- /dev/null +++ b/languages/yoast-video-seojs-sk_SK.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;","lang":"sk"},"Duration":[],"Video thumbnail":[],"Edit the Schema information below to optimize your video for search engines. ":[],"To learn more, read the %sYoast SEO Video configuration guide.%s":[],"Learn more about what has changed.":[],"We've made some changes to Yoast SEO Video.":[],"Open Google Preview editor":[],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[],"Title and description":[],"Mark this video as not family friendly":[],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Zdá sa, že váš obsah zatiaľ neobsahuje žiadne video. Prosí vložte video a uložte nábrhaby Video SEO fungovalo"],"Tags":["Tagy"],"If this video should not be available for safe search users, check this box.":["Ak by toto video nemalo byť prístupné pre užívateľv s bezpečným vyhľadávaním."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-sr_RS.json b/languages/yoast-video-seojs-sr_RS.json new file mode 100644 index 0000000..a3f3118 --- /dev/null +++ b/languages/yoast-video-seojs-sr_RS.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);","lang":"sr_RS"},"Duration":["Трајање"],"Video thumbnail":["Видео сличица"],"Edit the Schema information below to optimize your video for search engines. ":["Уредите доле наведене податке о шеми да бисте оптимизовали свој видео за претраживаче."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Да бисте сазнали више, прочитајте водич за конфигурацију %sYoast SEO Video.%s"],"Learn more about what has changed.":["Сазнајте више о томе шта се променило."],"We've made some changes to Yoast SEO Video.":["Унели смо неке промене у Yoast SEO Video."],"Open Google Preview editor":["Отвори уређивач Гугл прегледа"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Ваш видео ће користити исти SEO наслов и мета опис који сте унели у наш уређивач Гугл прегледа."],"Title and description":["Назив и опис"],"Mark this video as not family friendly":["Означите овај видео као неприкладан за породицу"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Изгледа да ваш садржај још увек не садржи видео. Додајте видео и сачувајте радну верзију како би видео SEO функционисао."],"Tags":["Tagovi"],"If this video should not be available for safe search users, check this box.":["Ukoliko je ovaj video dostupan za korisnike koji koriste safe search, označi ga."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-sv_SE.json b/languages/yoast-video-seojs-sv_SE.json new file mode 100644 index 0000000..aaf7bd3 --- /dev/null +++ b/languages/yoast-video-seojs-sv_SE.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=n != 1;","lang":"sv_SE"},"Duration":["Varaktighet"],"Video thumbnail":["Videominiatyr"],"Edit the Schema information below to optimize your video for search engines. ":["Redigera schemainformationen nedan för att optimera din video för sökmotorer. "],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Du hittar mer information i %skonfigurationsguiden för Yoast SEO Video.%s"],"Learn more about what has changed.":["Läs mer om förändringarna."],"We've made some changes to Yoast SEO Video.":["Vi har gjort några ändringar i Yoast SEO Video."],"Open Google Preview editor":["Öppna redigeraren för Google-förhandsgranskning"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Din video kommer att använda samma SEO-rubrik och metabeskrivning som du anger i vår redigerare för Google-förhandsvisning."],"Title and description":["Rubrik och beskrivning"],"Mark this video as not family friendly":["Markera att denna video inte är familjevänlig"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Det verkar som om ditt innehåll ännu inte innehåller en video. Lägg till en video och spara ditt utkast för att Video SEO ska fungera."],"Tags":["Etiketter"],"If this video should not be available for safe search users, check this box.":["Om denna video inte ska vara tillgänglig för användare med säker sökning, markera denna ruta."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-tr_TR.json b/languages/yoast-video-seojs-tr_TR.json new file mode 100644 index 0000000..d074caa --- /dev/null +++ b/languages/yoast-video-seojs-tr_TR.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=2; plural=(n > 1);","lang":"tr"},"Duration":["Süre"],"Video thumbnail":["Video küçük resmi"],"Edit the Schema information below to optimize your video for search engines. ":["Videonuzu arama motorları için optimize etmek üzere aşağıdaki şema bilgilerini düzenleyin."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Daha fazla bilgi edinmek için %sYoast SEO Video yapılandırma rehberini%s okuyun."],"Learn more about what has changed.":["Nelerin değiştiği hakkında daha fazla bilgi edinin."],"We've made some changes to Yoast SEO Video.":["Yoast SEO Video'da bazı değişiklikler yaptık."],"Open Google Preview editor":["Google önizleme düzenleyicisini aç"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Videonuz, Google önizleme düzenleyicimize girdiğiniz aynı SEO başlığını ve meta açıklamasını kullanacaktır."],"Title and description":["Başlık ve açıklama"],"Mark this video as not family friendly":["Bu videoyu ailelere uygun değil olarak işaretleyin"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Görünüşe bakılırsa içeriğinizde herhangi bir video yüklenmemiş görünüyor. Video SEO'nun çalışabilirmesi için lütfen bir video ekleyiniz ve taslak olarak kaydedin."],"Tags":["Etiketler"],"If this video should not be available for safe search users, check this box.":["Eğer bu video güvenli arama kullanıcıları tarafından görüntülenmemeli ise bu kutuyu işaretleyin."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs-uk.json b/languages/yoast-video-seojs-uk.json new file mode 100644 index 0000000..9289e2e --- /dev/null +++ b/languages/yoast-video-seojs-uk.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo","plural-forms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);","lang":"uk_UA"},"Duration":["Тривалість"],"Video thumbnail":["Мініатюра Відео"],"Edit the Schema information below to optimize your video for search engines. ":["Відредагуйте інформацію у схемі нижче для оптимізації відео для пошуковиків."],"To learn more, read the %sYoast SEO Video configuration guide.%s":["Прочитайте %sінструкцію з налаштування Yoast SEO Video%s, щоб дізнатися більше."],"Learn more about what has changed.":["Дізнайтеся більше про зміни."],"We've made some changes to Yoast SEO Video.":["Ми внесли деякі зміни до Yoast SEO Video."],"Open Google Preview editor":["Відкрити редактор Google Preview"],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":["Ваше відео використовуватиме ту саму назву та метаопис SEO, що ви ввели в редакторі Google Preview."],"Title and description":["Назва і опис"],"Mark this video as not family friendly":["Позначити це відео, як неприйнятне для родинного перегляду"],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":["Схоже, у вашому вмісті ще немає відео. Щоб відео SEO працювало, додайте його та збережіть його."],"Tags":["Теги"],"If this video should not be available for safe search users, check this box.":["Якщо це відео не повинно бути доступним для користувачів безпечного пошуку, поставте прапорець у цьому полі."]}}} \ No newline at end of file diff --git a/languages/yoast-video-seojs.json b/languages/yoast-video-seojs.json new file mode 100644 index 0000000..edbf0ec --- /dev/null +++ b/languages/yoast-video-seojs.json @@ -0,0 +1 @@ +{"domain":"yoast-video-seo","locale_data":{"yoast-video-seo":{"":{"domain":"yoast-video-seo"},"Title and description":[""],"Your video will use the same SEO title and meta description you enter in our Google Preview editor.":[""],"Open Google Preview editor":[""],"Tags":[""],"If this video should not be available for safe search users, check this box.":[""],"Mark this video as not family friendly":[""],"To learn more, read the %sYoast SEO Video configuration guide.%s":[""],"It looks like your content does not yet contain a video. Please add a video and save your draft in order for Video SEO to work.":[""],"We've made some changes to Yoast SEO Video.":[""],"Learn more about what has changed.":[""],"Edit the Schema information below to optimize your video for search engines. ":[""],"Video thumbnail":[""],"Duration":[""]}}} \ No newline at end of file diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..0ae0def --- /dev/null +++ b/license.txt @@ -0,0 +1,642 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 18. Additional terms + + In the light of Article 7 of the GPL license, the following additional +terms apply: + + a) You are prohibited to make misrepresentations of the origin of that +material, or to require that modified versions of such material be marked +in reasonable ways as different from the original version; + + b) You are limited in the use for publicity purposes of names of +licensors or authors of the material; + + c) You are declined any grant of rights under trademark law for use of +the trade names, trademarks, or service marks of YOAST B.V.; + + d) You are required to indemnify licensors and authors of that material +by anyone who conveys the material (or modified versions of it) with +contractual assumptions of liability to the recipient, for any liability +that these contractual assumptions directly impose on those licensors and +authors. + +END OF TERMS AND CONDITIONS diff --git a/post-analysis/abstract-class-supported-plugin.php b/post-analysis/abstract-class-supported-plugin.php new file mode 100644 index 0000000..0f0a87e --- /dev/null +++ b/post-analysis/abstract-class-supported-plugin.php @@ -0,0 +1,323 @@ +maybe_get_property( $this->shortcodes ); + } + + /** + * Retrieve the video post types added by a plugin + * + * @return array|bool Array with post types or false if the plugin does not add video post types + */ + public function get_post_types() { + return $this->maybe_get_property( $this->post_types ); + } + + /** + * Retrieve the meta keys which may contain video information as added by a plugin + * + * @return array|bool Array with meta keys or false if the plugin does not add relevant meta keys + */ + public function get_meta_keys() { + return $this->maybe_get_property( $this->meta_keys ); + } + + /** + * Retrieve the alternative url protocols added by a plugin + * + * @return array|bool Array with alternative url protocols or false if the plugin does not add protocols + */ + public function get_alt_protocols() { + return $this->maybe_get_property( $this->alt_protocols ); + } + + /** + * Retrieve the names for the video embeds added by a plugin + * + * @return array|bool Array with video embeds or false if the plugin does not add embeds + */ + public function get_video_autoembeds() { + return $this->maybe_get_property( $this->video_autoembeds ); + } + + /** + * Retrieve the info for the video oembeds added by a plugin + * + * @return array|bool Array with video oembeds or false if the plugin does not add oembeds + */ + public function get_video_oembeds() { + return $this->maybe_get_property( $this->video_oembeds ); + } + + /** + * Deal with non-named width/height attributes + * + * Examples: + * [collegehumor 1727961 200 100] + * [tube]http://www.youtube.com/watch?v=AFVlJAi3Cso, 500, 290[/tube] + * + * @param array $dimensions A pre-explode array of the attribute/content value. + * @param array $atts The current attributes. + * + * @return array + */ + protected function normalize_dimension_attributes( $dimensions, $atts ) { + if ( ! isset( $atts['width'] ) ) { + if ( ! empty( $dimensions[1] ) ) { + $atts['width'] = $dimensions[1]; + } + elseif ( ! empty( $atts[1] ) ) { + $atts['width'] = $atts[1]; + unset( $atts[1] ); + } + } + if ( ! isset( $atts['height'] ) ) { + if ( ! empty( $dimensions[2] ) ) { + $atts['height'] = $dimensions[2]; + } + elseif ( ! empty( $atts[2] ) ) { + $atts['height'] = $atts[2]; + unset( $atts[2] ); + } + } + return $atts; + } + + /** + * Distill video dimensions from shortcodes attributes + * + * @param array $vid Current video info array. + * @param array $atts The shortcode attributes. + * @param bool $try_alternative Whether to try and find only the 'normal' "width" and "height" attributes + * or also to try and find the alternative "w" and "h" attributes. + * + * @return array Potentially adjusted video info array + */ + protected function maybe_get_dimensions( $vid, $atts, $try_alternative = false ) { + if ( isset( $atts['width'] ) && ! empty( $atts['width'] ) && $atts['width'] > 0 ) { + $vid['width'] = (int) $atts['width']; + } + if ( isset( $atts['height'] ) && ! empty( $atts['height'] ) && $atts['height'] > 0 ) { + $vid['height'] = (int) $atts['height']; + } + + if ( $try_alternative === true ) { + if ( ! isset( $vid['width'] ) && ! empty( $atts['w'] ) && $atts['w'] > 0 ) { + $vid['width'] = (int) $atts['w']; + } + if ( ! isset( $vid['height'] ) && ! empty( $atts['h'] ) && $atts['h'] > 0 ) { + $vid['height'] = (int) $atts['h']; + } + } + + return $vid; + } + + /** + * Determine if a given id could be a youtube video id + * + * @param string $id ID string to evaluate. + * + * @return bool + */ + public function is_youtube_id( $id ) { + return ( ! empty( $id ) && preg_match( '`^(' . WPSEO_Video_Sitemap::$youtube_id_pattern . ')$`', $id ) ); + } + + /** + * Determine if a given id could be a google video id + * + * @param string $id ID string to evaluate. + * + * @return bool + */ + public function is_googlevideo_id( $id ) { + return ( ! empty( $id ) && preg_match( '`^[-]?[0-9]+$`', $id ) ); + } + + /** + * Determine if a given id could be a vimeo video id + * + * @param string $id ID string to evaluate. + * + * @return bool + */ + public function is_vimeo_id( $id ) { + return $this->is_numeric_id( $id ); + } + + /** + * Determine if a given id could be a flickr video id + * + * Allow both real (numeric) ids as well as short ids + * Keep this regex in line with the one in the flickr service class + * + * @param string $id ID string to evaluate. + * + * @return bool + */ + public function is_flickr_id( $id ) { + return ( ! empty( $id ) && preg_match( '`^[a-z0-9_-]+$`i', $id ) ); + } + + /** + * Determine if a given id is numeric + * + * @param string $id ID string to evaluate. + * + * @return bool + */ + public function is_numeric_id( $id ) { + return ( ! empty( $id ) && preg_match( '`^[0-9]+$`', $id ) ); + } + + /** + * Figure out whether the received input is a blip url, id or embedlookup or combination of those + * (url which contains the id or url which contains the embedlookup). + * + * Example data: + * [bliptv id="hdljgdbVBwI"] + * http://blip.tv/rss/view/3516963 + * http://blip.tv/day9tv/day-9-daily-101-kawaii-rice-tvp-style-3516963 + * http://blip.tv/play/hdljgdbVBwI + * + * @param array $vid The current video info array. + * @param string $check_this Primary value to check. + * @param string $full_shortcode The full Shortcode found. + * + * @return array Potentially adjusted video info array + */ + protected function what_the_blip( $vid, $check_this, $full_shortcode ) { + // Is it a url, an id or embedlookup ? + if ( $check_this !== '' && ( ( strpos( $check_this, 'http' ) === 0 || strpos( $check_this, '//' ) === 0 ) && strpos( $check_this, 'blip.tv' ) !== false ) ) { + $vid['url'] = $check_this; + } + + if ( preg_match( '`posts_id=["\']?([0-9]+)`i', $full_shortcode, $match ) ) { + $vid['id'] = $match[1]; + } + elseif ( isset( $vid['url'] ) && preg_match( '`(?:[/-])([0-9]+)$`i', $vid['url'], $match ) ) { + $vid['id'] = $match[1]; + } + elseif ( $check_this !== '' && preg_match( '`(?:^|[\?/=])([A-Za-z0-9-]{5,})(?:$|[&%\./])`', $check_this, $match ) ) { + $vid['embedlookup'] = $match[1]; + } + return $vid; + } + } +} diff --git a/post-analysis/class-analyse-post.php b/post-analysis/class-analyse-post.php new file mode 100644 index 0000000..ad6926a --- /dev/null +++ b/post-analysis/class-analyse-post.php @@ -0,0 +1,1702 @@ + Yoast] This currently stops at the first video (=old behaviour). Is this correct + * and as intended ? What about adding the potential secondary videos to the sitemap as well ? + * and what about wrapping them in schema.org info in the content ? + * This would impact the saving of metadata, how the metadata is presented to the user in the metabox + * as the user should be able to edit info on all videos (should they get a choice which is the main + * video to use for header info ? ), how the schema.org data is added and how the sitemap is generated. + * Adding the 'matched' string to the saved metadata would help with the schema.org data + * (str_replace on the correct video) + * + * @package WordPress\Plugins\Video-seo + * @subpackage Internals + * @since 1.8.0 + * @version 1.8.0 + */ + class WPSEO_Video_Analyse_Post { + + /** + * Whether or not the DOM extension is enabled. + * + * @var bool + */ + public static $dom_enabled = false; + + /** + * Array of supported plugins to take into account when analysing a post. + * + * Format: key = class name suffix, value = plugin basename. + * + * {@internal Changing the order of this array will change the priority with which the plugin + * is treated. + * The current order is based on the number of plugin downloads as stated in the + * WP repository per 2014-07-25.} + * + * {@internal To add (or remove) support for a plugin: + * - Create a class file in the supported-plugins folder (see other files and template for examples). + * - Add the plugin to the below list. + * - Add the class file to the autoload list in video-seo.php. + * - Add one of more unit test file(s) for the features supported by the plugin. + * - Add the plugin to travis.yml for download via git/svn.} + * + * @var array + */ + public static $supported_plugins = [ + 'Jetpack' => 'jetpack/jetpack.php', + 'Cincopa_Media' => 'video-playlist-and-gallery-plugin/wp-media-cincopa.php', + 'Youtube_Embed_Plus' => 'youtube-embed-plus/youtube.php', + 'Media_Element_Player' => 'media-element-html5-video-and-audio-player/mediaelement-js-wp.php', + 'Youtube_Embed' => 'youtube-embed/youtube-embed.php', + 'Featured_Video_Plus' => 'featured-video-plus/featured-video-plus.php', + 'FV_WordPress_Flowplayer' => 'fv-wordpress-flowplayer/flowplayer.php', + 'WP_Youtube_Lyte' => 'wp-youtube-lyte/wp-youtube-lyte.php', + 'WP_Video_Lightbox' => 'wp-video-lightbox/wp-video-lightbox.php', + 'Advanced_Responsive_Video_Embedder' => 'advanced-responsive-video-embedder/advanced-responsive-video-embedder.php', + 'Ustudio' => 'ustudio/plugin.php', + ]; + + /** + * Array of active plugins - subset of the supported plugins. + * + * Format: key = class name suffix, value = object instance. + * + * @var array + */ + protected static $active_plugins = []; + + /** + * Array of supported shortcodes with their handler methods. + * + * Format: key = shortcode, value = array of handler methods, i.e. if several plugins + * use the same shortcode, each handler method will be used in turn. + * + * @var array + */ + protected static $shortcodes = []; + + /** + * Array of additional supported post_types with their handler methods. + * + * Format: key = post_type, value = array of handler methods, i.e. if several plugins + * use the same post_type, each handler method will be used in turn. + * + * @var array + */ + protected static $post_types = []; + + /** + * Array of additional supported custom post meta fields with their handler methods. + * + * Format: key = meta_key, value = array of handler methods, i.e. if several plugins + * use the same meta_key, each handler method will be used in turn. + * + * @var array + */ + protected static $meta_keys = []; + + /** + * Array of alternative protocol schemes to take into account. + * + * @var array + */ + protected static $alt_protocols = []; + + /** + * Array of video embeds to take into account. + * + * @var array + */ + protected static $video_autoembeds = []; + + /** + * Array of video OEmbeds to take into account. + * + * @var array + */ + protected static $video_oembeds = []; + + /** + * The video info array. + * + * @var array + */ + protected $vid = [ + 'id' => null, + 'type' => null, + 'url' => null, + ]; + + /** + * The video array with all the data of the previous "fetch", if available. + * + * @var array + */ + protected $old_vid = []; + + /** + * The content of the post to analyse. + * + * @var string + */ + protected $content = ''; + + /** + * The post object for the post to analyse. + * + * @var object + */ + protected $post; + + /** + * Use embedly as a fall back method for video detail retrieval? + * + * @var bool + */ + protected static $use_embedly; + + /** + * Initialize the class + * + * @param string $content The content to parse for videos. + * @param array $vid The video array to update. + * @param array $old_vid The former video array. + * @param object|int|null $post The post object or the post id of the post to analyse. + */ + public function __construct( $content, $vid, $old_vid = [], $post = null ) { + $this->vid = array_merge( $this->vid, $vid ); + $this->old_vid = $old_vid; + // Set up post object if needed. + if ( ! empty( $post ) && ! is_object( $post ) ) { + $post = get_post( $post ); + } + if ( is_object( $post ) ) { + $this->post = $post; + } + elseif ( ! empty( $GLOBALS['post'] ) ) { + // Default to the current post - @todo probably wrong as it might be a term which is being analysed. + $this->post = $GLOBALS['post']; + } + + /** + * Filter: 'wpseo_video_index_content' - Allow changing the content of a post before indexation. + * + * @api string $content The content to analyze. + * + * @param array $vid Array with video info, usually empty. + * @param \WP_Post $post Post object. + */ + $content = apply_filters( 'wpseo_video_index_content', $content, $this->vid, $this->post ); + + // Deal with alternative protocols. + if ( in_array( 'youtube::', self::$alt_protocols, true ) ) { + // Very specific to the YouTube Embed plugin - maybe should be moved to plugin methods. + $content = str_replace( 'youtube::', 'http://www.youtube.com/watch?v=', $content ); + } + $this->content = str_replace( self::$alt_protocols, 'http://', $content ); + + // Can we use Embedly? + if ( ! isset( self::$use_embedly ) ) { + // Check if we have an Embedly api key. + if ( WPSEO_Options::get( 'video_embedly_api_key', '' ) !== '' ) { + self::$use_embedly = true; + } + else { + self::$use_embedly = false; + } + } + + $this->analyse(); + } + + /** + * Reset statics + */ + public static function reset_statics() { + self::$dom_enabled = false; + self::$active_plugins = []; + self::$shortcodes = []; + self::$post_types = []; + self::$meta_keys = []; + self::$alt_protocols = []; + self::$video_autoembeds = []; + self::$video_oembeds = []; + } + + /** + * Set statics + */ + public static function set_statics() { + // Reset just in case this method is called more than once (like when we're testing). + self::reset_statics(); + + if ( extension_loaded( 'dom' ) && class_exists( 'domxpath' ) ) { + self::$dom_enabled = true; + } + + /* + * Add WP core to 'active plugins' as the first (highest prio) item + * Add this plugin as the second + */ + self::$active_plugins['wp_core'] = new WPSEO_Video_Support_Core(); + self::$active_plugins['videoseo'] = new WPSEO_Video_Plugin_Yoast_Videoseo(); + + /* + * @todo It might be better if we can figure out if the plugin is installed rather than active + * Also - this will give issues with plugins in the mu-plugins directory as is_plugin_active() + * incorrectly returns false (should be fixed in WP4 ?) + */ + if ( ! function_exists( 'is_plugin_active' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + /** + * Filter: 'wpseo_video_supported_plugins' - Allow control of which plugins are supported. + * + * @api array $supported_plugins The existing list of supported plugins. + */ + $supported_plugins = apply_filters( 'wpseo_video_supported_plugins', self::$supported_plugins ); + + foreach ( $supported_plugins as $name => $plugin_basenames ) { + $plugin_basenames = (array) $plugin_basenames; + foreach ( $plugin_basenames as $plugin_basename ) { + if ( is_plugin_active( $plugin_basename ) ) { + $classname = 'WPSEO_Video_Plugin_' . $name; + self::$active_plugins[ $name ] = new $classname(); + break; + } + } + } + unset( $name, $plugin_basenames, $plugin_basename, $classname ); + + // Add the plugin features for all active plugins. + if ( is_array( self::$active_plugins ) && self::$active_plugins !== [] ) { + foreach ( self::$active_plugins as $name => $instance ) { + + // Add known shortcodes. + $shortcodes = $instance->get_shortcodes(); + if ( is_array( $shortcodes ) && $shortcodes !== [] ) { + foreach ( $shortcodes as $sc ) { + self::$shortcodes[ $sc ][] = [ $instance, 'get_info_from_shortcode' ]; + } + } + unset( $shortcodes, $sc ); + + // Add known additional post_types. + $post_types = $instance->get_post_types(); + if ( is_array( $post_types ) && $post_types !== [] ) { + foreach ( $post_types as $pt ) { + self::$post_types[ $pt ][] = [ $instance, 'get_info_for_post_type' ]; + } + } + unset( $post_types, $pt ); + + // Add known additional meta_keys. + $meta_keys = $instance->get_meta_keys(); + if ( is_array( $meta_keys ) && $meta_keys !== [] ) { + foreach ( $meta_keys as $key ) { + self::$meta_keys[ $key ][] = [ $instance, 'get_info_from_post_meta' ]; + } + } + unset( $meta_keys, $key ); + + // Add alternative protocols. + $alt_protocols = $instance->get_alt_protocols(); + if ( is_array( $alt_protocols ) && $alt_protocols !== [] ) { + self::$alt_protocols = array_unique( array_merge( self::$alt_protocols, $alt_protocols ) ); + } + unset( $alt_protocols ); + + + // Add autoembed information. + $video_autoembeds = $instance->get_video_autoembeds(); + if ( is_array( $video_autoembeds ) && $video_autoembeds !== [] ) { + /* + * {@internal Merge order reversed, if there is a handler name conflict + * between plugins, defer to the more popular plugin which will + * have been added first.} + */ + self::$video_autoembeds = array_unique( array_merge( $video_autoembeds, self::$video_autoembeds ) ); + } + unset( $video_autoembeds ); + + + // Add oembed information. + $video_oembeds = $instance->get_video_oembeds(); + if ( is_array( $video_oembeds ) && $video_oembeds !== [] ) { + /* + * {@internal Merge order reversed, if there is a handler name conflict + * between plugins, defer to the more popular plugin which will + * have been added first.} + */ + self::$video_oembeds = array_unique( array_merge( $video_oembeds, self::$video_oembeds ) ); + } + + unset( $video_oembeds ); + } + + unset( $name, $instance ); + } + } + + /** + * Get the video info + * + * @return array|string Return video array or 'none' + */ + public function get_vid_info() { + if ( isset( $this->vid['content_loc'] ) || isset( $this->vid['player_loc'] ) ) { + $this->normalize_values(); + + /** + * Filter: 'wpseo_video_{$type}_details' - Allow changing the details of a video + * + * @api array $video The video array. + * + * @param \WP_Post $post Post object. + */ + $vid = apply_filters( 'wpseo_video_' . $this->vid['type'] . '_details', $this->vid, $this->post ); + + return array_filter( $vid ); + } + else { + return 'none'; + } + } + + /** + * Make sure all duration, view_count, height and width values are integers + */ + protected function normalize_values() { + $keys = [ + 'duration', + 'height', + 'view_count', + 'width', + ]; + foreach ( $keys as $key ) { + if ( isset( $this->vid[ $key ] ) ) { + $this->vid[ $key ] = absint( round( $this->vid[ $key ] ) ); + } + } + } + + /** + * Analyse the post for video content + */ + protected function analyse() { + + $methods = [ + 'get_video_from_post_type', + 'get_video_from_post_meta', + 'get_video_from_attachment', + 'get_video_from_shortcode', + 'get_video_from_auto_embeds', + + 'get_video_through_old_methods', + ]; + + foreach ( $methods as $method ) { + if ( is_callable( [ $this, $method ] ) ) { + $vid = call_user_func( [ $this, $method ] ); + + // Check for video. + if ( $vid !== [] && ( isset( $vid['content_loc'] ) || isset( $vid['player_loc'] ) ) ) { + $this->vid = array_merge( $this->vid, $vid ); + break; + } + } + } + } + + /** + * Check if the current post type is a video post type and if we can find usable info through it + */ + protected function get_video_from_post_type() { + $vid = []; + + if ( ( is_array( self::$post_types ) && self::$post_types !== [] ) && ( is_object( $this->post ) && isset( self::$post_types[ $this->post->post_type ] ) ) ) { + foreach ( self::$post_types[ $this->post->post_type ] as $function ) { + if ( is_callable( $function ) ) { + $vid = call_user_func( $function, $this->post->ID, $this->post->post_type, $this->post ); + if ( is_array( $vid ) && $vid !== [] ) { + $vid = $this->get_video_details( $vid ); + if ( ! empty( $vid['player_loc'] ) || ! empty( $vid['content_loc'] ) ) { + // Stop on the first function which delivers results. + break; + } + else { + // Reset $vid if no usable info was found. + $vid = []; + } + } + } + } + } + + return $vid; + } + + /** + * Check if any custom fields are video fields and if they contain usable info + */ + protected function get_video_from_post_meta() { + $vid = []; + + if ( is_array( self::$meta_keys ) && self::$meta_keys !== [] && ! empty( $this->post->ID ) ) { + + foreach ( self::$meta_keys as $key => $callables ) { + $meta_values = $this->get_normalized_meta_values( $key, $this->post->ID ); + + if ( is_array( $meta_values ) && $meta_values !== [] ) { + foreach ( $meta_values as $single_meta_value ) { + + foreach ( $callables as $function ) { + if ( is_callable( $function ) ) { + + $vid = call_user_func( $function, $single_meta_value, $key, $this->post->ID ); + + if ( is_array( $vid ) && $vid !== [] ) { + $vid = $this->get_video_details( $vid ); + if ( ! empty( $vid['player_loc'] ) || ! empty( $vid['content_loc'] ) ) { + // Stop on the first function which deliveres results. + unset( $vid['__add_to_content'] ); + break 3; + } + elseif ( ! empty( $vid['__add_to_content'] ) ) { + $this->content = $vid['__add_to_content'] . "\n" . $this->content; + $vid = []; + } + else { + // Reset $vid if no usable info was found. + $vid = []; + } + } + } + } + } + } + } + } + + return $vid; + } + + /** + * Get post meta values to analyse for video content + * + * @param string $key Meta key to get the values for. + * @param int $post_id Post to get the values for. + * + * @return array Single dimensional array with already entity normalized potentially usable meta values. + */ + protected function get_normalized_meta_values( $key, $post_id ) { + + $real_values = []; + $meta_values = get_post_custom_values( $key, $post_id ); + + if ( is_array( $meta_values ) && $meta_values !== [] ) { + foreach ( $meta_values as $meta_value ) { + $meta_value = maybe_unserialize( $meta_value ); + + if ( is_scalar( $meta_value ) && ! empty( $meta_value ) ) { + $real_values[] = $meta_value; + } + elseif ( is_array( $meta_value ) && $meta_value !== [] ) { + foreach ( $meta_value as $value ) { + if ( is_scalar( $value ) && ! empty( $value ) ) { + $real_values[] = $value; + } + elseif ( is_array( $value ) && ! empty( $value[0] ) && is_scalar( $value[0] ) ) { + /* + * Ignore deeper meta values which are multi-dim arrays as we really + * don't know what we need from it them + */ + $real_values[] = $value[0]; + } + } + } + } + } + unset( $meta_values, $meta_value, $value ); + + /* + * Silly, silly themes _encode_ the value of the post meta field. Yeah it's ridiculous. + * But this fixes it. + * + * ^ Helpful comment, thanks! + */ + $real_values = array_map( [ $this, 'normalize_entities' ], $real_values ); + $real_values = array_map( 'trim', $real_values ); + + // Remove empties. + $real_values = array_filter( $real_values ); + + return $real_values; + } + + /** + * Get all video attachments and see if we can find one we can use + */ + protected function get_video_from_attachment() { + $vid = []; + + if ( ! empty( $this->post->ID ) ) { + $media = get_attached_media( 'video', $this->post->ID ); + if ( is_array( $media ) && $media !== [] ) { + foreach ( $media as $video ) { + $vid['type'] = 'localfile'; + $vid['maybe_local'] = true; + $vid['attachment_id'] = $video->ID; + $vid['url'] = $video->guid; + + $vid = $this->get_video_details( $vid ); + if ( ! empty( $vid['player_loc'] ) || ! empty( $vid['content_loc'] ) ) { + // Stop on the first video which delivers results (i.e. has a usable extension). + break; + } + else { + // Reset $vid if no usable info was found. + $vid = []; + } + } + } + } + + return $vid; + } + + /** + * Get all the shortcodes, check for any video shortcodes and see if we can parse them to useable info + */ + protected function get_video_from_shortcode() { + $vid = []; + + if ( strpos( $this->content, '[' ) !== false && is_array( self::$shortcodes ) && self::$shortcodes !== [] ) { + + $old_shortcode_tags = $GLOBALS['shortcode_tags']; + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reset at end of function. + $GLOBALS['shortcode_tags'] = self::$shortcodes; + + /* + * 1 - An extra [ to allow for escaping shortcodes with double [[]] + * 2 - The shortcode name + * 3 - The shortcode argument list + * 4 - The self closing / + * 5 - The content of a shortcode when it wraps some content. + * 6 - An extra ] to allow for escaping shortcodes with double [[]] + */ + if ( preg_match_all( '/' . get_shortcode_regex() . '/s', $this->content, $matches, PREG_SET_ORDER ) ) { + + foreach ( $matches as $match ) { + // No need to do anything is it's an escaped shortcode. + if ( $match[1] !== '[' && $match[6] !== ']' ) { + $full = $match[0]; + $tag = trim( $match[2] ); + $sc_content = $match[5]; + $atts = shortcode_parse_atts( $match[3] ); + + // Handle WordPress.com shortcode format. + if ( isset( $atts[0] ) && $sc_content === '' ) { + $atts = $this->fix_sc_attributes( $atts ); + $sc_content = trim( $atts[0] ); + unset( $atts[0] ); + } + + $sc_content = $this->normalize_entities( $sc_content ); + if ( is_array( $atts ) && $atts !== [] ) { + $atts = array_map( [ $this, 'normalize_entities' ], $atts ); + } + + $thumb = ''; + if ( isset( $atts['image'] ) && ( is_string( $atts['image'] ) && $atts['images'] !== '' ) ) { + $thumb = $atts['image']; + } + + foreach ( self::$shortcodes[ $tag ] as $function ) { + if ( is_callable( $function ) ) { + $vid = call_user_func( $function, $full, $tag, $atts, $sc_content ); + if ( is_array( $vid ) && $vid !== [] ) { + if ( ! isset( $vid['thumbnail_loc'] ) && $thumb !== '' ) { + $vid['thumbnail_loc'] = $thumb; + } + + $vid = $this->get_video_details( $vid ); + if ( ! empty( $vid['player_loc'] ) || ! empty( $vid['content_loc'] ) ) { + // Stop on the first function which delivers results. + break 2; + } + elseif ( ! empty( $vid['__add_to_content'] ) ) { + $this->content = $vid['__add_to_content'] . "\n" . $this->content; + $vid = []; + } + else { + // Reset $vid if no usable info was found. + $vid = []; + } + } + } + } + } + } + } + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Resetting the variable. + $GLOBALS['shortcode_tags'] = $old_shortcode_tags; + } + + return $vid; + } + + /** + * Grab all urls and check if any are registered video urls + * and if so, grab usable info + */ + protected function get_video_from_auto_embeds() { + $vid = []; + + $urls = []; + + // Check if we are in an Elementor ajax save request, because we need a different embed regex. + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing and thus no need to sanitize. + $post_action = isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ? wp_unslash( $_POST['action'] ) : ''; + $is_elementor_ajax_save = $post_action === 'elementor_ajax' || $post_action === 'wpseo_elementor_save'; + + if ( \wp_doing_ajax() && $is_elementor_ajax_save ) { + if ( preg_match_all( '`https?://[^\s<>"]+`im', $this->content, $matches, PREG_PATTERN_ORDER ) ) { + $urls = $matches[0]; + } + } + else { + if ( preg_match_all( '`^(?:\s*)(https?://[^\s<>"]+)(?:\s*)$`im', $this->content, $matches, PREG_PATTERN_ORDER ) ) { + // Only interested in the real url matches. + $urls = $matches[1]; + } + } + + foreach ( $urls as $url ) { + // Follow WP. + $url = str_replace( '&', '&', $url ); + + if ( ! empty( $GLOBALS['wp_embed']->handlers ) && is_array( $GLOBALS['wp_embed']->handlers ) ) { + // Go through the embed handlers. + foreach ( $GLOBALS['wp_embed']->handlers as $handler_array ) { + foreach ( $handler_array as $name => $details ) { + if ( isset( self::$video_autoembeds[ $name ] ) && preg_match( $details['regex'], $url ) ) { + $vid['url'] = $url; + if ( self::$video_autoembeds[ $name ] !== '' ) { + $vid['type'] = self::$video_autoembeds[ $name ]; + } + + $vid = $this->get_video_details( $vid ); + if ( ! empty( $vid['player_loc'] ) || ! empty( $vid['content_loc'] ) ) { + // Stop on the first function which delivers results. + break 3; + } + else { + // Reset $vid if no usable info was found. + $vid = []; + } + } + } + unset( $name, $details ); + } + unset( $handler_array ); + } + + /* + * Go through the Oembed handlers + */ + $oembed = _wp_oembed_get_object(); + $providerurl = $oembed->get_provider( $url, [ 'discover' => false ] ); + if ( ( is_string( $providerurl ) && $providerurl !== '' ) && ! empty( self::$video_oembeds ) ) { + foreach ( self::$video_oembeds as $partial_url => $service ) { + if ( stripos( $providerurl, $partial_url ) !== false ) { + $vid['url'] = $url; + if ( $service !== '' ) { + $vid['type'] = $service; + } + + $vid = $this->get_video_details( $vid ); + if ( ! empty( $vid['player_loc'] ) || ! empty( $vid['content_loc'] ) ) { + // Stop on the first function which delivers results. + break 2; + } + else { + // Reset $vid if no usable info was found. + $vid = []; + } + } + } + } + } + + return $vid; + } + + /** + * Parse the content of a post or term description. + * + * {@internal Stripped version of the old function.} + * + * @since 1.3 + * + * @return array + */ + protected function get_video_through_old_methods() { + $content = $this->content; + $vid = []; + + if ( preg_match( '`()`s', $content, $html5vid ) ) { + + if ( preg_match( '`src=([\'"])(.*?)\.(' . WPSEO_Video_Sitemap::$video_ext_pattern . ')\1`', $html5vid[1], $content_loc ) ) { + $vid['content_loc'] = $content_loc[2] . '.' . $content_loc[3]; + $vid['maybe_local'] = true; + + if ( preg_match( '`poster=([\'"])([^\'"\s]+)\1`', $html5vid[1], $thumbnail_loc ) ) { + $vid['thumbnail_loc'] = $thumbnail_loc[2]; + } + + $vid['type'] = 'html5vid'; + + return $this->get_video_details( $vid ); + } + } + + $vid = $this->get_wistia_video_through_old_methods( $content ); + + if ( isset( $vid['content_loc'] ) || isset( $vid['player_loc'] ) ) { + return $vid; + } + else { + // Reset vid. + $vid = []; + } + + + $oembed = $this->grab_embeddable_urls_xpath( $content ); + if ( is_array( $oembed ) && $oembed !== [] ) { + + foreach ( $oembed as $url ) { + $vid['url'] = $url; + $vid = $this->get_video_details( $vid ); + + if ( isset( $vid['content_loc'] ) || isset( $vid['player_loc'] ) ) { + return $vid; + } + else { + // Reset vid. + $vid = []; + } + } + } + unset( $oembed ); + + + $oembed = $this->grab_embeddable_urls( $content ); + if ( is_array( $oembed ) && $oembed !== [] ) { + + foreach ( $oembed as $url ) { + $vid['url'] = $url; + $vid = $this->get_video_details( $vid ); + + if ( isset( $vid['content_loc'] ) || isset( $vid['player_loc'] ) ) { + return $vid; + } + else { + // Reset vid. + $vid = []; + } + } + } + unset( $oembed ); + + + return $vid; + } + + /** + * Analyse post content for typical Wistia embed codes. + * + * @link https://wistia.com/doc/embedding + * + * @since 3.9 + * + * @param string $content Post content. + * + * @return array Video info array or empty array if no wistia video was matched. + */ + protected function get_wistia_video_through_old_methods( $content ) { + $vid = []; + + if ( preg_match( '`<(?:div|span)(?: [a-z]+=\S+)* class=(?:[\'"])wistia_embed wistia_async_([^\'"\s]+)`', $content, $matches ) ) { + $vid['id'] = $matches[1]; + $vid['type'] = 'wistia'; + $vid = $this->get_video_details( $vid ); + } + elseif ( preg_match( '`
get_video_details( $vid ); + } + elseif ( preg_match( '`get_video_details( $vid ); + } + + return $vid; + } + + /** + * Checks whether there are oembed URLs in the post that should be included in the video sitemap. + * + * {@internal Look at WP native function `get_media_embedded_in_content( $content, $types = null )`.} + * + * @since 0.1 + * + * @param string $content The content of the post. + * + * @return array|bool returns array $urls with type of video as array key and video URL as content, or false on negative + */ + protected function grab_embeddable_urls( $content ) { + $evs_location = get_option( 'evs_location' ); + + // Catch both the single line embeds as well as the embeds using the [embed] shortcode. + preg_match_all( '`\[embed(?:[^\]]+)?\](http[s]?://[^\s"]+)\s*\[/embed\]`im', $content, $matches ); + preg_match_all( '`^\s*(?:

)?(http[s]?://[^\s"]+)\s*$`im', $content, $matches2 ); + + $matched_urls = []; + if ( isset( $matches[1] ) && is_array( $matches[1] ) ) { + $matched_urls = array_merge( $matched_urls, $matches[1] ); + } + if ( isset( $matches2[1] ) && is_array( $matches2[1] ) ) { + $matched_urls = array_merge( $matched_urls, $matches2[1] ); + } + + if ( preg_match_all( '`()`s', $content, $iframes, PREG_SET_ORDER ) ) { + foreach ( $iframes as $iframe ) { + if ( preg_match( '`src=([\'"])([^\s\'"]+)\1`', $iframe[1], $iframesrc ) ) { + $matched_urls[] = $iframesrc[2]; + } + } + } + + if ( preg_match_all( '`()`s', $content, $objects, PREG_SET_ORDER ) ) { + foreach ( $objects as $object ) { + if ( preg_match( '``', $content, $matches ) ) { + $matched_urls[] = $matches[3]; + } + + $wistia_info = [ 'domain' => 'wistia.com' ]; + $wistia_domain = WPSEO_Options::get( 'video_wistia_domain', '' ); + if ( $wistia_domain !== '' ) { + $wistia_info = $this->parse_url( $wistia_domain ); + } + + $evs_info = $this->parse_url( $evs_location ); + if ( ! isset( $evs_info ) || ! isset( $evs_info['domain'] ) ) { + $evs_info = [ 'domain' => 'easyvideosuite.com' ]; + } + + + if ( count( $matched_urls ) > 0 ) { + $urls = []; + + foreach ( $matched_urls as $match ) { + $url_info = $this->parse_url( $match ); + if ( ! isset( $url_info['domain'] ) ) { + continue; + } + + switch ( $url_info['domain'] ) { + case 'brightcove.com': + if ( preg_match( '`length > 0 ) { + foreach ( $objects as $object ) { + $value = $object->getAttribute( 'value' ); + $matched_urls[] = $value; + } + } + unset( $objects, $object, $value ); + + // For iframe embeds (i.e. vidyard.com). + $iframes = $xpath->query( '//iframe' ); + if ( is_object( $iframes ) && $iframes->length > 0 ) { + foreach ( $iframes as $iframe ) { + $src = $iframe->getAttribute( 'src' ); + $matched_urls[] = $src; + } + } + unset( $iframes, $iframe, $src ); + + // Specific check for vidyard embed with javascript and lightbox. + $script = $xpath->query( '//script[contains(@src,"play.vidyard.com")]' ); + if ( is_object( $script ) && $script->length > 0 ) { + foreach ( $script as $element ) { + $src = $element->getAttribute( 'src' ); + $matched_urls[] = $src; + } + } + unset( $script, $element, $src ); + + // Specific check for cincopa embed via javascript. + $script = $xpath->query( '//script/text()[contains(.,"cp_load_widget")]' ); + if ( is_object( $script ) && $script->length > 0 ) { + foreach ( $script as $element ) { + // Remove CDATA. + $src = preg_replace( '`//\s*?`', '', $element->wholeText ); + $src = 'http://cincopa.com?' . $src; + $matched_urls[] = $src; + } + } + unset( $script, $element, $src ); + + // Specific check for brightcove. + $script = $xpath->query( '//object/param[contains(@value,"brightcove.com")]/following-sibling::param[@name="flashVars"]' ); + if ( is_object( $script ) && $script->length > 0 ) { + foreach ( $script as $element ) { + $src = $element->getAttribute( 'value' ); + $src = 'http://brightcove.com?' . $src; + $matched_urls[] = $src; + } + } + unset( $script, $element, $src ); + + // Specific check for screenr. + $script = $xpath->query( '//object/param[contains(@value,"screenr.com")]/following-sibling::param[@name="flashvars"]' ); + if ( is_object( $script ) && $script->length > 0 ) { + foreach ( $script as $element ) { + $flashvars = $element->getAttribute( 'value' ); + if ( preg_match( '`