Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.01% covered (warning)
57.01%
61 / 107
25.00% covered (danger)
25.00%
1 / 4
CRAP
n/a
0 / 0
github_gist_embed_handler
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
jetpack_gist_get_shortcode_id
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
182
github_gist_shortcode
93.10% covered (success)
93.10%
54 / 58
0.00% covered (danger)
0.00%
0 / 1
21.14
github_gist_simple_embed
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * GitHub's Gist site supports oEmbed but their oembed provider only
4 * returns raw HTML (no styling) and the first little bit of the code.
5 *
6 * Their JavaScript-based embed method is a lot better, so that's what we're using.
7 *
8 * Supported formats:
9 * Full URL: https://gist.github.com/57cc50246aab776e110060926a2face2
10 * Full URL with username: https://gist.github.com/jeherve/57cc50246aab776e110060926a2face2
11 * Full URL linking to specific file: https://gist.github.com/jeherve/57cc50246aab776e110060926a2face2#file-wp-config-php
12 * Full URL, no username, linking to specific file: https://gist.github.com/57cc50246aab776e110060926a2face2#file-wp-config-php
13 * Gist ID: [gist]57cc50246aab776e110060926a2face2[/gist]
14 * Gist ID within tag: [gist 57cc50246aab776e110060926a2face2]
15 * Gist ID with username: [gist jeherve/57cc50246aab776e110060926a2face2]
16 * Gist private ID with username: [gist xknown/fc5891af153e2cf365c9]
17 *
18 * @package automattic/jetpack
19 */
20
21if ( ! defined( 'ABSPATH' ) ) {
22    exit( 0 );
23}
24
25wp_embed_register_handler( 'github-gist', '#https?://gist\.github\.com/([a-zA-Z0-9/]+)(\#file\-[a-zA-Z0-9\_\-]+)?#', 'github_gist_embed_handler' );
26add_shortcode( 'gist', 'github_gist_shortcode' );
27
28/**
29 * Handle gist embeds.
30 *
31 * @since 2.8.0
32 *
33 * @global WP_Embed $wp_embed
34 *
35 * @param array  $matches Results after parsing the URL using the regex in wp_embed_register_handler().
36 * @param array  $attr    Embed attributes.
37 * @param string $url     The original URL that was matched by the regex.
38 * @param array  $rawattr The original unmodified attributes.
39 * @return string The embed HTML, or a shortcode to render after content formatting.
40 */
41function github_gist_embed_handler( $matches, $attr, $url, $rawattr ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
42    $shortcode_priority = has_filter( 'the_content', 'do_shortcode' );
43    if (
44        doing_filter( 'the_content' )
45        && shortcode_exists( 'gist' )
46        && false !== $shortcode_priority
47        // current_priority() reports the outermost the_content pass, including recursive calls.
48        && $shortcode_priority > $GLOBALS['wp_filter']['the_content']->current_priority()
49    ) {
50        // Render after wpautop to preserve the gist markup. Self-closing, so a later [gist]ID[/gist] isn't read as this one's content.
51        return sprintf( '[gist %s /]', esc_url_raw( $url ) );
52    }
53
54    // Let the shortcode callback do all the work.
55    return github_gist_shortcode( $matches, $url );
56}
57
58/**
59 * Extract an ID from a Gist shortcode or a full Gist URL.
60 *
61 * @since 7.3.0
62 *
63 * @param string $gist Gist shortcode or full Gist URL.
64 *
65 * @return array $gist_info {
66 * Array of information about our gist.
67 *     @type string $id   Unique identifier for the gist.
68 *     @type string $file File name if the gist links to a specific file.
69 * }
70 */
71function jetpack_gist_get_shortcode_id( $gist = '' ) {
72    $gist_info = array(
73        'id'   => '',
74        'file' => '',
75        'ts'   => 8,
76    );
77    // Simple shortcode, with just an ID.
78    if ( ctype_alnum( $gist ) ) {
79        $gist_info['id'] = $gist;
80    }
81
82    // Full URL? Only keep the relevant parts.
83    $parsed_url = wp_parse_url( $gist );
84    if (
85        ! empty( $parsed_url )
86        && is_array( $parsed_url )
87        && isset( $parsed_url['scheme'] ) && isset( $parsed_url['host'] ) && isset( $parsed_url['path'] )
88    ) {
89        // Not a Gist URL? Bail.
90        if ( 'gist.github.com' !== $parsed_url['host'] ) {
91            return array(
92                'id'   => '',
93                'file' => '',
94                'ts'   => 8,
95            );
96        }
97
98        // Keep the file name if there was one.
99        if ( ! empty( $parsed_url['fragment'] ) ) {
100            $gist_info['file'] = preg_replace( '/(?:file-)(.+)/', '$1', $parsed_url['fragment'] );
101        }
102
103        // Validate the path structure - should be either username/gistid or just gistid
104        $path = trim( $parsed_url['path'], '/' );
105        if ( ! preg_match( '#^([a-zA-Z0-9_-]+/)?([a-f0-9]+)$#', $path ) ) {
106            return array(
107                'id'   => '',
108                'file' => '',
109                'ts'   => 8,
110            );
111        }
112
113        $gist_info['id'] = $path;
114        // Reassign $gist with the identifier to clean it up below.
115        $gist = $path;
116
117        // Parse the query args to obtain the tab spacing.
118        if ( ! empty( $parsed_url['query'] ) ) {
119            $query_args = array();
120            wp_parse_str( $parsed_url['query'], $query_args );
121            if ( ! empty( $query_args['ts'] ) ) {
122                $gist_info['ts'] = absint( $query_args['ts'] );
123            }
124        }
125    }
126
127    // Not a URL nor an ID? Look for "username/id", "/username/id", or "id", and only keep the ID.
128    if ( preg_match( '#^/?(([a-z0-9_-]+/)?([a-z0-9]+))$#i', $gist, $matches ) ) {
129        $gist_info['id'] = $matches[3];
130    }
131
132    return $gist_info;
133}
134
135/**
136 * Callback for gist shortcode.
137 *
138 * @since 2.8.0
139 *
140 * @param array  $atts Attributes found in the shortcode.
141 * @param string $content Content enclosed by the shortcode.
142 *
143 * @return string The gist HTML.
144 */
145function github_gist_shortcode( $atts, $content = '' ) {
146
147    if ( empty( $atts[0] ) && empty( $content ) ) {
148        if ( current_user_can( 'edit_posts' ) ) {
149            return esc_html__( 'Please specify a Gist URL or ID.', 'jetpack' );
150        } else {
151            return '<!-- Missing Gist ID -->';
152        }
153    }
154
155    $id = ( ! empty( $content ) ) ? $content : $atts[0];
156
157    // Parse a URL to get an ID we can use.
158    $gist_info = jetpack_gist_get_shortcode_id( $id );
159    if ( empty( $gist_info['id'] ) ) {
160        if ( current_user_can( 'edit_posts' ) ) {
161            return esc_html__( 'The Gist ID you provided is not valid. Please try a different one.', 'jetpack' );
162        } else {
163            return '<!-- Invalid Gist ID -->';
164        }
165    } else {
166        // Add trailing .json to all unique gist identifiers.
167        $id = $gist_info['id'] . '.json';
168    }
169
170    // The file name can come from the URL passed, or from a shortcode attribute.
171    if ( ! empty( $gist_info['file'] ) ) {
172        $file = $gist_info['file'];
173    } elseif ( ! empty( $atts['file'] ) ) {
174        $file = $atts['file'];
175    } else {
176        $file = '';
177    }
178
179    // Replace - by . to get a real file name from slug.
180    if ( ! empty( $file ) ) {
181        // Find the last -.
182        $dash_position = strrpos( $file, '-' );
183        if ( false !== $dash_position ) {
184            // Replace the - by a period.
185            $file = substr_replace( $file, '.', $dash_position, 1 );
186        }
187
188        $file = rawurlencode( $file );
189    }
190
191    // Set the tab size, allowing attributes to override the query string.
192    $tab_size = $gist_info['ts'];
193    if ( ! empty( $atts['ts'] ) ) {
194        $tab_size = absint( $atts['ts'] );
195    }
196
197    if (
198        class_exists( 'Jetpack_AMP_Support' )
199        && Jetpack_AMP_Support::is_amp_request()
200    ) {
201        /*
202         * According to <https://www.ampproject.org/docs/reference/components/amp-gist#height-(required)>:
203         *
204         * > Note: You must find the height of the gist by inspecting it with your browser (e.g., Chrome Developer Tools).
205         *
206         * However, this does not seem to be the case any longer. The actual height of the content does get set in the
207         * page after loading. So this is just the initial height.
208         * See <https://github.com/ampproject/amphtml/pull/17738>.
209         */
210        $height = 240;
211
212        $amp_tag = sprintf(
213            '<amp-gist layout="fixed-height" data-gistid="%s" height="%s"',
214            esc_attr( basename( $id, '.json' ) ),
215            esc_attr( $height )
216        );
217        if ( ! empty( $file ) ) {
218            $amp_tag .= sprintf( ' data-file="%s"', esc_attr( $file ) );
219        }
220        $amp_tag .= '></amp-gist>';
221        return $amp_tag;
222    }
223
224    // URL points to the entire gist, including the file name if there was one.
225    $id     = ( ! empty( $file ) ? $id . '?file=' . $file : $id );
226    $return = false;
227
228    $request      = wp_remote_get( esc_url_raw( 'https://gist.github.com/' . esc_attr( $id ) ) );
229    $request_code = wp_remote_retrieve_response_code( $request );
230
231    if ( 200 === $request_code ) {
232        $request_body = wp_remote_retrieve_body( $request );
233        $request_data = json_decode( $request_body );
234
235        wp_enqueue_style( 'jetpack-gist-styling', esc_url( $request_data->stylesheet ), array(), JETPACK__VERSION );
236
237        $gist = substr_replace( $request_data->div, sprintf( 'style="tab-size: %1$s" ', absint( $tab_size ) ), 5, 0 );
238
239        // Add inline styles for the tab style in the opening div of the gist.
240        $gist = preg_replace(
241            '#(\<div\s)+(id=\"gist[0-9]+\")+(\sclass=\"gist\"\>)?#',
242            sprintf( '$1style="tab-size: %1$s" $2$3', absint( $tab_size ) ),
243            $request_data->div,
244            1
245        );
246
247        // Add inline style to prevent the bottom margin to the embed that themes like TwentyTen, et al., add to tables.
248        $return = sprintf( '<style>.gist table { margin-bottom: 0; }</style>%1$s', $gist );
249    }
250
251    if (
252        // No need to check for a nonce here, that's already handled by Core further up.
253        // phpcs:disable WordPress.Security.NonceVerification.Missing
254        isset( $_POST['type'] )
255        && 'embed' === $_POST['type']
256        && isset( $_POST['action'] )
257        && 'parse-embed' === $_POST['action']
258        // phpcs:enable WordPress.Security.NonceVerification.Missing
259    ) {
260        return github_gist_simple_embed( $id, $tab_size );
261    }
262
263    return $return;
264}
265
266/**
267 * Use script tag to load shortcode in editor.
268 * Can't use wp_enqueue_script here.
269 *
270 * @since 3.9.0
271 *
272 * @param string $id       The ID of the gist.
273 * @param int    $tab_size The tab size of the gist.
274 * @return string          The script tag of the gist.
275 */
276function github_gist_simple_embed( $id, $tab_size = 8 ) {
277    $id = str_replace( 'json', 'js', $id );
278    return '<script src="' . esc_url( "https://gist.github.com/$id?ts=$tab_size" ) . '"></script>'; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
279}