| 1 |
<?php |
| 2 |
/** |
| 3 |
* VideoPress caption track post type and helpers. |
| 4 |
* |
| 5 |
* @package automattic/jetpack-videopress |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace Automattic\Jetpack\VideoPress; |
| 9 |
|
| 10 |
/** |
| 11 |
* Stores editable caption track documents as private WordPress posts. |
| 12 |
* |
| 13 |
* Track content is stored as serialized `videopress/caption-cue` block markup, |
| 14 |
* matching the block-based editor that produces it. This is a deliberate |
| 15 |
* trade-off: it keeps the draft round-trippable through the editor without a |
| 16 |
* bespoke format, at the cost of coupling stored drafts to the block markup |
| 17 |
* (sanitization re-parses blocks, and replacing the editor later means |
| 18 |
* migrating stored content). |
| 19 |
* |
| 20 |
* The REST routes serving this store live in |
| 21 |
* {@see WPCOM_REST_API_V2_Endpoint_VideoPress_Caption_Tracks}. |
| 22 |
*/ |
| 23 |
class Caption_Tracks { |
| 24 |
|
| 25 |
const POST_TYPE = 'vp_caption_track'; |
| 26 |
|
| 27 |
const CUE_BLOCK_NAME = 'videopress/caption-cue'; |
| 28 |
|
| 29 |
const META_GUID = '_videopress_guid'; |
| 30 |
const META_KIND = '_videopress_caption_kind'; |
| 31 |
const META_SRC_LANG = '_videopress_caption_src_lang'; |
| 32 |
const META_LABEL = '_videopress_caption_label'; |
| 33 |
const META_SOURCE_TRACK_KIND = '_videopress_source_track_kind'; |
| 34 |
const META_SOURCE_TRACK_SRC_LANG = '_videopress_source_track_src_lang'; |
| 35 |
const META_SOURCE_TRACK_SRC = '_videopress_source_track_src'; |
| 36 |
|
| 37 |
/** |
| 38 |
* Initialize the caption track post type. |
| 39 |
* |
| 40 |
* @return void |
| 41 |
*/ |
| 42 |
public static function init() { |
| 43 |
add_action( 'init', array( __CLASS__, 'register_post_type' ) ); |
| 44 |
add_action( 'init', array( __CLASS__, 'register_meta' ) ); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Register the private caption track post type. |
| 49 |
* |
| 50 |
* @return void |
| 51 |
*/ |
| 52 |
public static function register_post_type() { |
| 53 |
register_post_type( |
| 54 |
self::POST_TYPE, |
| 55 |
array( |
| 56 |
'labels' => array( |
| 57 |
'name' => __( 'VideoPress caption tracks', 'jetpack-videopress-pkg' ), |
| 58 |
'singular_name' => __( 'VideoPress caption track', 'jetpack-videopress-pkg' ), |
| 59 |
), |
| 60 |
'public' => false, |
| 61 |
'exclude_from_search' => true, |
| 62 |
'publicly_queryable' => false, |
| 63 |
'show_ui' => false, |
| 64 |
'show_in_menu' => false, |
| 65 |
'show_in_rest' => false, |
| 66 |
// No revisions: there is no restore UI, and each one would copy up to 1MB of content. |
| 67 |
'supports' => array( 'title', 'editor' ), |
| 68 |
|
| 69 |
/* |
| 70 |
* Caption tracks are an internal store reached only through the |
| 71 |
* /wpcom/v2/videopress/caption-tracks routes, which authorize |
| 72 |
* each request against the video it targets. The post type itself |
| 73 |
* is not exposed (no REST, no admin UI), so standard post |
| 74 |
* capabilities are sufficient. |
| 75 |
*/ |
| 76 |
'capability_type' => 'post', |
| 77 |
'map_meta_cap' => true, |
| 78 |
) |
| 79 |
); |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Register caption track metadata so values are sanitized when saved. |
| 84 |
* |
| 85 |
* @return void |
| 86 |
*/ |
| 87 |
public static function register_meta() { |
| 88 |
$string_meta = array( |
| 89 |
self::META_GUID => array( __CLASS__, 'sanitize_guid' ), |
| 90 |
self::META_KIND => array( __CLASS__, 'sanitize_kind' ), |
| 91 |
self::META_SRC_LANG => array( __CLASS__, 'sanitize_manual_language' ), |
| 92 |
self::META_LABEL => 'sanitize_text_field', |
| 93 |
self::META_SOURCE_TRACK_KIND => array( __CLASS__, 'sanitize_kind' ), |
| 94 |
self::META_SOURCE_TRACK_SRC_LANG => 'sanitize_text_field', |
| 95 |
self::META_SOURCE_TRACK_SRC => 'esc_url_raw', |
| 96 |
); |
| 97 |
|
| 98 |
foreach ( $string_meta as $key => $sanitize_callback ) { |
| 99 |
register_post_meta( |
| 100 |
self::POST_TYPE, |
| 101 |
$key, |
| 102 |
array( |
| 103 |
'type' => 'string', |
| 104 |
'single' => true, |
| 105 |
'sanitize_callback' => $sanitize_callback, |
| 106 |
) |
| 107 |
); |
| 108 |
} |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Sanitize a VideoPress GUID. |
| 113 |
* |
| 114 |
* @param string $value Raw value. |
| 115 |
* @return string |
| 116 |
*/ |
| 117 |
public static function sanitize_guid( $value ) { |
| 118 |
$value = sanitize_text_field( $value ); |
| 119 |
return preg_match( '/^[a-z0-9]{8}$/i', $value ) ? $value : ''; |
| 120 |
} |
| 121 |
|
| 122 |
/** |
| 123 |
* Sanitize a text track kind. |
| 124 |
* |
| 125 |
* @param string $value Raw value. |
| 126 |
* @return string |
| 127 |
*/ |
| 128 |
public static function sanitize_kind( $value ) { |
| 129 |
$value = sanitize_key( $value ); |
| 130 |
$valid = array( 'subtitles', 'captions', 'descriptions', 'chapters', 'metadata' ); |
| 131 |
return in_array( $value, $valid, true ) ? $value : ''; |
| 132 |
} |
| 133 |
|
| 134 |
/** |
| 135 |
* Validate a caption track's BCP-47 language tag. |
| 136 |
* |
| 137 |
* The client canonicalizes tags with `Intl.getCanonicalLocales` before |
| 138 |
* saving, and the "one track per language" lookup matches stored tags by |
| 139 |
* exact string, so this is a validation gate only: it rejects generated |
| 140 |
* source keys such as `auto_en` and malformed tags, then keeps the client's |
| 141 |
* canonical value verbatim. Re-casing here would risk diverging from the |
| 142 |
* client's canonical form and silently create duplicate tracks. |
| 143 |
* |
| 144 |
* @param string $value Raw value. |
| 145 |
* @return string Validated tag, or empty string when invalid. |
| 146 |
*/ |
| 147 |
public static function sanitize_manual_language( $value ) { |
| 148 |
$value = trim( sanitize_text_field( $value ) ); |
| 149 |
|
| 150 |
if ( preg_match( '/^auto[_-]/i', $value ) ) { |
| 151 |
return ''; |
| 152 |
} |
| 153 |
|
| 154 |
if ( ! preg_match( '/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/i', $value ) ) { |
| 155 |
return ''; |
| 156 |
} |
| 157 |
|
| 158 |
return $value; |
| 159 |
} |
| 160 |
} |
| 161 |
|