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