Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.33% covered (success)
98.33%
59 / 60
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Shortcodes
98.33% covered (success)
98.33%
59 / 60
90.00% covered (success)
90.00%
9 / 10
33
0.00% covered (danger)
0.00%
0 / 1
 get_attribute_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_youtube_url
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 get_youtube_id
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
12.03
 get_vimeo_id
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 get_ted_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_wpvideo_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_videopress_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_hulu_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_archiveorg_id
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 get_archiveorg_book_id
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Shortcode ID extraction methods.
4 *
5 * Provides static methods to extract identifiers from shortcode attributes
6 * for various media types (YouTube, Vimeo, TED, VideoPress, etc.).
7 *
8 * @package automattic/jetpack-post-media
9 */
10
11namespace Automattic\Jetpack;
12
13/**
14 * Extracts media identifiers from shortcode attributes.
15 *
16 * @since 0.1.0
17 */
18class Shortcodes {
19
20    /**
21     * Get an ID from a shortcode attribute by key.
22     *
23     * Used for shortcodes that store their identifier in a single attribute,
24     * e.g. ted and hulu use a named 'id' attribute, while wpvideo and
25     * videopress use the first positional attribute.
26     *
27     * @since 0.1.0
28     *
29     * @param array      $atts Shortcode attributes.
30     * @param string|int $key  The attribute key to look up. Defaults to 0 (first positional attribute).
31     * @return string|int The attribute value, or 0 if not found.
32     */
33    public static function get_attribute_id( $atts, $key = 0 ) {
34        return ! empty( $atts[ $key ] ) ? $atts[ $key ] : 0;
35    }
36
37    /**
38     * Normalizes a YouTube URL to include a v= parameter and a query string free of encoded ampersands.
39     *
40     * Handles youtu.be short links, /v/ paths, /shorts/ paths, playlist URLs, and encoded ampersands.
41     *
42     * @since 0.1.0
43     *
44     * @param string|array $url YouTube URL, or an array with a 'url' key.
45     * @return string|false The normalized URL, or false if input is invalid.
46     */
47    public static function sanitize_youtube_url( $url ) {
48        if ( is_array( $url ) && isset( $url['url'] ) ) {
49            $url = $url['url'];
50        }
51        if ( ! is_string( $url ) ) {
52            return false;
53        }
54
55        $url = trim( $url, ' "' );
56        $url = trim( $url );
57        $url = str_replace(
58            array( 'youtu.be/', '/v/', '/shorts/', '#!v=', '&amp;', '&#038;', 'playlist' ),
59            array( 'youtu.be/?v=', '/?v=', '/watch?v=', '?v=', '&', '&', 'videoseries' ),
60            $url
61        );
62
63        // Replace any extra question marks with ampersands - the result of a URL like
64        // "https://www.youtube.com/v/dQw4w9WgXcQ?fs=1&hl=en_US" being passed in.
65        $query_string_start = strpos( $url, '?' );
66
67        if ( false !== $query_string_start ) {
68            $url = substr( $url, 0, $query_string_start + 1 ) . str_replace( '?', '&', substr( $url, $query_string_start + 1 ) );
69        }
70
71        return $url;
72    }
73
74    /**
75     * Extract a YouTube video or playlist ID from shortcode attributes.
76     *
77     * @since 0.1.0
78     *
79     * @param string|array $atts Shortcode attributes. Can be just the URL string or the full $atts array.
80     * @return string|false The YouTube video or playlist ID, or false on failure.
81     */
82    public static function get_youtube_id( $atts ) {
83        // Do we have an $atts array? Get first att.
84        if ( is_array( $atts ) ) {
85            $atts = reset( $atts );
86        }
87
88        $url = self::sanitize_youtube_url( $atts );
89
90        if ( ! is_string( $url ) || '' === $url ) {
91            return false;
92        }
93
94        $parsed = parse_url( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
95
96        if ( ! is_array( $parsed ) || ! isset( $parsed['query'] ) ) {
97            return false;
98        }
99
100        parse_str( $parsed['query'], $qargs );
101
102        if ( ! isset( $qargs['v'] ) && ! isset( $qargs['list'] ) ) {
103            return false;
104        }
105
106        $id = '';
107
108        if ( isset( $qargs['list'] ) ) {
109            $id = preg_replace( '|[^_a-z0-9-]|i', '', $qargs['list'] );
110        }
111
112        if ( empty( $id ) && isset( $qargs['v'] ) ) {
113            $id = preg_replace( '|[^_a-z0-9-]|i', '', $qargs['v'] );
114        }
115
116        return empty( $id ) ? false : $id;
117    }
118
119    /**
120     * Extract a Vimeo video ID from shortcode attributes.
121     *
122     * Supports numeric IDs and various Vimeo URL formats including
123     * groups, albums, channels, and player URLs.
124     *
125     * @since 0.1.0
126     *
127     * @param array $atts Shortcode attributes.
128     * @return int The Vimeo video ID, or 0 if not found.
129     */
130    public static function get_vimeo_id( $atts ) {
131        if ( isset( $atts[0] ) ) {
132            $atts[0] = trim( $atts[0], '=' );
133            if ( is_numeric( $atts[0] ) ) {
134                return (int) $atts[0];
135            }
136
137            /**
138             * Extract Vimeo ID from the URL. For examples:
139             * https://vimeo.com/12345
140             * https://vimeo.com/289091934/cd1f466bcc
141             * https://vimeo.com/album/2838732/video/6342264
142             * https://vimeo.com/groups/758728/videos/897094040
143             * https://vimeo.com/channels/staffpicks/videos/123456789
144             * https://vimeo.com/album/1234567/video/7654321
145             * https://player.vimeo.com/video/18427511
146             */
147            $pattern = '/(?:https?:\/\/)?vimeo\.com\/(?:groups\/[^\/]+\/videos\/|album\/\d+\/video\/|video\/|channels\/[^\/]+\/videos\/|[^\/]+\/)?([0-9]+)(?:[^\'\"0-9<]|$)/i';
148            $match   = array();
149            if ( preg_match( $pattern, $atts[0], $match ) ) {
150                return (int) $match[1];
151            }
152        }
153
154        return 0;
155    }
156
157    /**
158     * Get the unique ID of a TED video from shortcode attributes.
159     *
160     * @since 0.1.0
161     *
162     * @param array $atts Shortcode attributes.
163     * @return string|int The TED video ID, or 0 if not found.
164     */
165    public static function get_ted_id( $atts ) {
166        return self::get_attribute_id( $atts, 'id' );
167    }
168
169    /**
170     * Get a VideoPress ID from wpvideo shortcode attributes.
171     *
172     * @since 0.1.0
173     *
174     * @param array $atts Shortcode attributes.
175     * @return string|int The VideoPress ID, or 0 if not found.
176     */
177    public static function get_wpvideo_id( $atts ) {
178        return self::get_attribute_id( $atts );
179    }
180
181    /**
182     * Get a VideoPress ID from videopress shortcode attributes.
183     *
184     * @since 0.1.0
185     *
186     * @param array $atts Shortcode attributes.
187     * @return string|int The VideoPress ID, or 0 if not found.
188     */
189    public static function get_videopress_id( $atts ) {
190        return self::get_attribute_id( $atts );
191    }
192
193    /**
194     * Get a Hulu video ID from shortcode attributes.
195     *
196     * @since 0.1.0
197     *
198     * @param array $atts Shortcode attributes.
199     * @return string|int The Hulu video ID, or 0 if not found.
200     */
201    public static function get_hulu_id( $atts ) {
202        return self::get_attribute_id( $atts, 'id' );
203    }
204
205    /**
206     * Get an Archive.org embed ID from shortcode attributes.
207     *
208     * Extracts the identifier from an Archive.org details or embed URL,
209     * or uses the raw attribute value if it is not a URL.
210     *
211     * @since 0.1.0
212     *
213     * @param array $atts Shortcode attributes.
214     * @return string|int The Archive.org identifier, the raw attribute value
215     *                     (which may be empty), or 0 if the attribute is not set.
216     */
217    public static function get_archiveorg_id( $atts ) {
218        if ( isset( $atts[0] ) ) {
219            $atts[0] = trim( $atts[0], '=' );
220            if ( preg_match( '~archive\.org/(details|embed)/([^/?#]+)~i', $atts[0], $match ) ) {
221                $id = $match[2];
222            } else {
223                $id = $atts[0];
224            }
225            return $id;
226        }
227        return 0;
228    }
229
230    /**
231     * Get an Archive.org book embed ID from shortcode attributes.
232     *
233     * Extracts the identifier from an Archive.org stream URL,
234     * or uses the raw attribute value if it is not a URL.
235     *
236     * @since 0.1.0
237     *
238     * @param array $atts Shortcode attributes.
239     * @return string|int The Archive.org book identifier, the raw attribute value
240     *                     (which may be empty), or 0 if the attribute is not set.
241     */
242    public static function get_archiveorg_book_id( $atts ) {
243        if ( isset( $atts[0] ) ) {
244            $atts[0] = trim( $atts[0], '=' );
245            if ( preg_match( '~archive\.org/stream/([^/?#]+)~i', $atts[0], $match ) ) {
246                $id = $match[1];
247            } else {
248                $id = $atts[0];
249            }
250            return $id;
251        }
252        return 0;
253    }
254}