Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Caption_Tracks
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
6 / 6
11
100.00% covered (success)
100.00%
1 / 1
 init
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 register_post_type
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 register_meta
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_guid
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_kind
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_manual_language
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * VideoPress caption track post type and helpers.
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8namespace 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 */
23class 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}