Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 165
0.00% covered (danger)
0.00%
0 / 7
CRAP
n/a
0 / 0
Automattic\Jetpack\Extensions\Map\register_block
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Map\wpcom_load_event
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
Automattic\Jetpack\Extensions\Map\get_map_provider
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
Automattic\Jetpack\Extensions\Map\load_assets
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Map\rebuild_map_block_div
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
56
Automattic\Jetpack\Extensions\Map\render_single_block_page
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
Automattic\Jetpack\Extensions\Map\map_block_from_geo_points
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Map block.
4 *
5 * @since 6.8.0
6 *
7 * @package automattic/jetpack
8 */
9
10namespace Automattic\Jetpack\Extensions\Map;
11
12use Automattic\Jetpack\Blocks;
13use Automattic\Jetpack\Status\Host;
14use Automattic\Jetpack\Tracking;
15use Jetpack;
16use Jetpack_Gutenberg;
17use Jetpack_Mapbox_Helper;
18
19if ( ! defined( 'ABSPATH' ) ) {
20    exit( 0 );
21}
22
23if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) {
24    require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-mapbox-helper.php';
25}
26
27/**
28 * Registers the block for use in Gutenberg
29 * This is done via an action so that we can disable
30 * registration if we need to.
31 */
32function register_block() {
33    Blocks::jetpack_register_block(
34        __DIR__,
35        array(
36            'render_callback' => __NAMESPACE__ . '\load_assets',
37        )
38    );
39}
40add_action( 'init', __NAMESPACE__ . '\register_block' );
41
42/**
43 * Record a Tracks event every time the Map block is loaded on WordPress.com and Atomic.
44 *
45 * @param string $access_token_source The Mapbox API access token source.
46 */
47function wpcom_load_event( $access_token_source ) {
48    if ( 'wpcom' !== $access_token_source ) {
49        return;
50    }
51
52    $event_name = 'map_block_mapbox_wpcom_key_load';
53    if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
54        require_lib( 'tracks/client' );
55        // Tracking::record_user_event() prefixes the source itself; tracks_record_event() does not.
56        tracks_record_event( wp_get_current_user(), 'jetpack_' . $event_name );
57    } elseif ( ( new Host() )->is_woa_site() && Jetpack::is_connection_ready() ) {
58        $tracking = new Tracking();
59        $tracking->record_user_event( $event_name );
60    }
61}
62
63/**
64 * Function to determine which map provider to choose
65 *
66 * @param string $html The block's HTML - needed for the class name.
67 *
68 * @return string The name of the map provider.
69 */
70function get_map_provider( $html ) {
71    $mapbox_styles = array( 'is-style-terrain' );
72    // return mapbox if html contains one of the mapbox styles
73    foreach ( $mapbox_styles as $style ) {
74        if ( str_contains( $html, $style ) ) {
75            return 'mapbox';
76        }
77    }
78
79    // you can override the map provider with a cookie
80    if ( isset( $_COOKIE['map_provider'] ) ) {
81        return sanitize_text_field( wp_unslash( $_COOKIE['map_provider'] ) );
82    }
83
84    // if we don't apply the filters & default to mapbox
85    return apply_filters( 'wpcom_map_block_map_provider', 'mapbox' );
86}
87
88/**
89 * Map block registration/dependency declaration.
90 *
91 * @param array  $attr    Array containing the map block attributes.
92 * @param string $content String containing the map block content.
93 *
94 * @return string
95 */
96function load_assets( $attr, $content ) {
97    $access_token = Jetpack_Mapbox_Helper::get_access_token();
98    wpcom_load_event( $access_token['source'] );
99
100    if ( Blocks::is_amp_request() ) {
101        static $map_block_counter = array();
102
103        $id = get_the_ID();
104        if ( ! isset( $map_block_counter[ $id ] ) ) {
105            $map_block_counter[ $id ] = 0;
106        }
107        ++$map_block_counter[ $id ];
108
109        $iframe_url = add_query_arg(
110            array(
111                'map-block-counter' => absint( $map_block_counter[ $id ] ),
112                'map-block-post-id' => $id,
113            ),
114            get_permalink()
115        );
116
117        $placeholder = preg_replace( '/(?<=<div\s)/', 'placeholder ', $content );
118
119        return sprintf(
120            '<amp-iframe src="%s" width="%d" height="%d" layout="responsive" allowfullscreen sandbox="allow-scripts">%s</amp-iframe>',
121            esc_url( $iframe_url ),
122            4,
123            3,
124            $placeholder
125        );
126    }
127
128    Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
129
130    $map_provider = get_map_provider( $content );
131
132    return rebuild_map_block_div( $attr, $content, $map_provider, $access_token );
133}
134
135/**
136 * Rebuild the map block div to move large JSON data from HTML attributes
137 * into an inline JS payload, keeping the DOM lightweight.
138 *
139 * @param array  $attr         Array containing the map block attributes.
140 * @param string $content      String containing the map block content.
141 * @param string $map_provider The map provider (mapbox or mapkit).
142 * @param array  $access_token The Mapbox/MapKit access token data.
143 *
144 * @return string
145 */
146function rebuild_map_block_div( $attr, $content, $map_provider, $access_token ) {
147    static $did_inline = false;
148
149    $block_id = wp_unique_id( 'jp-map-' );
150
151    // Extract map-style from the saved HTML since it's no longer a block attribute
152    // (was deprecated and converted to CSS class names like is-style-terrain).
153    $map_style = 'default';
154    if ( preg_match( '/data-map-style="([^"]*)"/', $content, $matches ) ) {
155        $map_style = $matches[1];
156    }
157
158    $classes = array( 'wp-block-jetpack-map' );
159    if ( ! empty( $attr['align'] ) ) {
160        $classes[] = 'align' . $attr['align'];
161    }
162    if ( ! empty( $attr['className'] ) ) {
163        $classes[] = $attr['className'];
164    }
165
166    // Keep the tag small: no giant JSON in attributes.
167    $data_attrs = array(
168        'class'             => implode( ' ', $classes ),
169        'data-map-id'       => $block_id,
170        'data-map-provider' => $map_provider,
171        'data-api-key'      => $access_token['key'],
172        'data-map-style'    => $map_style,
173    );
174
175    if ( 'mapkit' === $map_provider ) {
176        $data_attrs['data-blog-id'] = \Jetpack_Options::get_option( 'id' );
177    }
178
179    // Put everything else in the JS payload (including points).
180    $payload = array(
181        'points'               => $attr['points'] ?? null,
182        'zoom'                 => $attr['zoom'] ?? null,
183        'mapCenter'            => $attr['mapCenter'] ?? null,
184        'markerColor'          => $attr['markerColor'] ?? null,
185        'scrollToZoom'         => $attr['scrollToZoom'] ?? null,
186        'mapDetails'           => $attr['mapDetails'] ?? null,
187        'mapHeight'            => $attr['mapHeight'] ?? null,
188        'showFullscreenButton' => $attr['showFullscreenButton'] ?? null,
189        'mapStyle'             => $map_style,
190    );
191
192    $handle = 'jetpack-block-map';
193
194    // Seed global once.
195    if ( ! $did_inline ) {
196        wp_add_inline_script( $handle, 'window.JetpackMapBlockData = window.JetpackMapBlockData || {};', 'before' );
197        $did_inline = true;
198    }
199
200    // Add this block's payload keyed by ID.
201    wp_add_inline_script(
202        $handle,
203        'window.JetpackMapBlockData[' . wp_json_encode( $block_id, JSON_HEX_TAG | JSON_HEX_AMP ) . '] = ' . wp_json_encode( $payload, JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
204        'before'
205    );
206
207    $div_open = '<div';
208    foreach ( $data_attrs as $key => $value ) {
209        $div_open .= ' ' . $key . '="' . esc_attr( $value ) . '"';
210    }
211    $div_open .= '>';
212
213    $result = preg_replace( '/<div[^>]*>/', $div_open, $content, 1 );
214    return $result ?? $content;
215}
216
217/**
218 * Render a page containing only a single Map block.
219 */
220function render_single_block_page() {
221    // phpcs:ignore WordPress.Security.NonceVerification
222    $map_block_counter = isset( $_GET['map-block-counter'] ) ? absint( $_GET['map-block-counter'] ) : null;
223    // phpcs:ignore WordPress.Security.NonceVerification
224    $map_block_post_id = isset( $_GET['map-block-post-id'] ) ? absint( $_GET['map-block-post-id'] ) : null;
225
226    if ( ! $map_block_counter || ! $map_block_post_id ) {
227        return;
228    }
229
230    /* Create an array of all root-level DIVs that are Map Blocks */
231    $post = get_post( $map_block_post_id );
232
233    if ( ! class_exists( 'DOMDocument' ) ) {
234        return;
235    }
236
237    $post_html = new \DOMDocument();
238    /** This filter is already documented in core/wp-includes/post-template.php */
239    $content = apply_filters( 'the_content', $post->post_content );
240
241    // Return early if empty to prevent DOMDocument::loadHTML fatal.
242    if ( empty( $content ) ) {
243        return;
244    }
245
246    /* Suppress warnings */
247    libxml_use_internal_errors( true );
248    @$post_html->loadHTML( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
249    libxml_use_internal_errors( false );
250
251    $xpath     = new \DOMXPath( $post_html );
252    $container = $xpath->query( '//div[ contains( @class, "wp-block-jetpack-map" ) ]' )->item( $map_block_counter - 1 );
253
254    /* Check that we have a block matching the counter position */
255    if ( ! $container ) {
256        return;
257    }
258
259    /* Compile scripts and styles */
260    ob_start();
261
262    add_filter( 'jetpack_is_amp_request', '__return_false' );
263
264    Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
265    wp_scripts()->do_items();
266    wp_styles()->do_items();
267
268    add_filter( 'jetpack_is_amp_request', '__return_true' );
269
270    $head_content = ob_get_clean();
271
272    /* Put together a new complete document containing only the requested block markup and the scripts/styles needed to render it */
273    $block_markup = $post_html->saveHTML( $container );
274    $access_token = Jetpack_Mapbox_Helper::get_access_token();
275    $page_html    = sprintf(
276        '<!DOCTYPE html><head><style>html, body { margin: 0; padding: 0; }</style>%s</head><body>%s</body>',
277        $head_content,
278        preg_replace( '/(?<=<div\s)/', 'data-api-key="' . esc_attr( $access_token['key'] ) . '" ', $block_markup, 1 )
279    );
280    echo $page_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
281    exit( 0 );
282}
283add_action( 'wp', __NAMESPACE__ . '\render_single_block_page' );
284
285/**
286 * Helper function to generate the markup of the block in PHP.
287 *
288 * @param Array $points - Array containing geo location points.
289 *
290 * @return string Markup for the jetpack/map block.
291 */
292function map_block_from_geo_points( $points ) {
293    $map_block_data = array(
294        'points'    => $points,
295        'zoom'      => 8,
296        'mapCenter' => array(
297            'lng' => $points[0]['coordinates']['longitude'],
298            'lat' => $points[0]['coordinates']['latitude'],
299        ),
300    );
301
302    $list_items = array_map(
303        function ( $point ) {
304            $link = add_query_arg(
305                array(
306                    'api'   => 1,
307                    'query' => $point['coordinates']['latitude'] . ',' . $point['coordinates']['longitude'],
308                ),
309                'https://www.google.com/maps/search/'
310            );
311            return sprintf( '<li><a href="%s">%s</a></li>', esc_url( $link ), $point['title'] );
312        },
313        $points
314    );
315
316    $map_block  = '<!-- wp:jetpack/map ' . wp_json_encode( $map_block_data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ' -->' . PHP_EOL;
317    $map_block .= sprintf(
318        '<div class="wp-block-jetpack-map" data-map-style="default" data-map-details="true" data-points="%1$s" data-zoom="%2$d" data-map-center="%3$s" data-marker-color="red" data-show-fullscreen-button="true">',
319        esc_attr( wp_json_encode( $map_block_data['points'], JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ),
320        $map_block_data['zoom'],
321        esc_attr( wp_json_encode( $map_block_data['mapCenter'], JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) )
322    );
323    $map_block .= '<ul>' . implode( "\n", $list_items ) . '</ul>';
324    $map_block .= '</div>' . PHP_EOL;
325    $map_block .= '<!-- /wp:jetpack/map -->';
326
327    return $map_block;
328}