diff options
Diffstat (limited to 'plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php')
-rw-r--r-- | plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php b/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php new file mode 100644 index 00000000..e4941537 --- /dev/null +++ b/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php @@ -0,0 +1,425 @@ +<?php + +/** + * Adds support for geo-location features. + * + * All Jetpack sites can support geo-location features. Users can tag posts with geo-location data + * using the UI provided by Calypso. That information will be included in RSS feeds, meta tags during + * wp_head, and in the Geo microformat following post content. + * + * If your theme declares support for "geo-location", you'll also get a small icon and location label + * visible to users at the bottom of single posts and pages. + * + * To declare support in your theme, call `add_theme_support( 'jetpack-geo-location' )`. + * + * Once you've added theme support, you can rely on the standard HTML output generated in the + * the_content_location_display() method of this class. Or, you can use the "geo_location_display" + * filter to generate custom HTML for your particular theme. Your filter function will receive an + * the default HTML as its first argument and an array containing the geo-location information as + * its second argument in the following format: + * + * array( + * 'is_public' => boolean, + * 'latitude' => float, + * 'longitude' => float, + * 'label' => string, + * 'is_populated' => boolean + * ) + * + * Add your filter with: + * + * add_filter( 'jetpack_geo_location_display', 'your_filter_function_name', 10, 2); + */ +class Jetpack_Geo_Location { + private static $instance; + + /** + * Whether dashicons are enqueued. + * + * @since 6.6.0 + * + * @var bool + */ + private static $style_enqueued = false; + + public static function init() { + if ( is_null( self::$instance ) ) { + self::$instance = new Jetpack_Geo_Location(); + } + + return self::$instance; + } + + /** + * This is mostly just used for testing purposes. + */ + public static function reset_instance() { + self::$instance = null; + } + + public function __construct() { + add_action( 'init', array( $this, 'wordpress_init' ) ); + add_action( 'wp_head', array( $this, 'wp_head' ) ); + add_filter( 'the_content', array( $this, 'the_content_microformat' ) ); + + $this->register_rss_hooks(); + } + + /** + * Register support for the geo-location feature on pages and posts. Register the meta + * fields managed by this plugin so that they are properly sanitized during save. + */ + public function wordpress_init() { + // Only render location label after post content, if the theme claims to support "geo-location". + if ( current_theme_supports( 'jetpack-geo-location' ) ) { + add_filter( 'the_content', array( $this, 'the_content_location_display' ), 15, 1 ); + } + + add_post_type_support( 'post', 'geo-location' ); + add_post_type_support( 'page', 'geo-location' ); + + register_meta( + 'post', + 'geo_public', + array( + 'sanitize_callback' => array( $this, 'sanitize_public' ), + 'type' => 'boolean', + 'single' => true, + ) + ); + + register_meta( + 'post', + 'geo_latitude', + array( + 'sanitize_callback' => array( $this, 'sanitize_coordinate' ), + 'type' => 'float', + 'single' => true, + ) + ); + + register_meta( + 'post', + 'geo_longitude', + array( + 'sanitize_callback' => array( $this, 'sanitize_coordinate' ), + 'type' => 'float', + 'single' => true, + ) + ); + + register_meta( + 'post', + 'geo_address', + array( + 'sanitize_callback' => 'sanitize_text_field', + 'type' => 'string', + 'single' => true, + ) + ); + } + + /** + * Filter "public" input to always be either 1 or 0. + * + * @param mixed $public + * + * @return int + */ + public function sanitize_public( $public ) { + return absint( $public ) ? 1 : 0; + } + + /** + * Filter geo coordinates and normalize them to floats with 7 digits of precision. + * + * @param mixed $coordinate + * + * @return float|null + */ + public function sanitize_coordinate( $coordinate ) { + if ( ! $coordinate ) { + return null; + } + + return round( (float) $coordinate, 7 ); + } + + /** + * Render geo.position and ICBM meta tags with public geo meta values when rendering + * a single post. + */ + public function wp_head() { + if ( ! is_single() ) { + return; + } + + $meta_values = $this->get_meta_values( $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return; + } + + if ( ! self::$style_enqueued ) { + // only enqueue scripts and styles when needed. + self::enqueue_scripts(); + self::$style_enqueued = true; + } + + echo "\n<!-- Jetpack Geo-location Tags -->\n"; + + if ( $meta_values['label'] ) { + printf( + '<meta name="geo.placename" content="%s" />', + esc_attr( $meta_values['label'] ) + ); + } + + printf( + '<meta name="geo.position" content="%s;%s" />' . PHP_EOL, + esc_attr( $meta_values['latitude'] ), + esc_attr( $meta_values['longitude'] ) + ); + + printf( + '<meta name="ICBM" content="%s, %s" />' . PHP_EOL, + esc_attr( $meta_values['latitude'] ), + esc_attr( $meta_values['longitude'] ) + ); + + echo "\n<!-- End Jetpack Geo-location Tags -->\n"; + } + + /** + * Append public meta values in the Geo microformat (https://en.wikipedia.org/wiki/Geo_(microformat) + * to the supplied content. + * + * Note that we cannot render the microformat in the context of an excerpt because tags are stripped + * in that context, making our microformat data visible. + * + * @param string $content + * + * @return string + */ + public function the_content_microformat( $content ) { + if ( is_feed() || $this->is_currently_excerpt_filter() ) { + return $content; + } + + $meta_values = $this->get_meta_values( $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return $content; + } + + $microformat = sprintf( + '<div id="geo-post-%d" class="geo geo-post" style="display: none">', + esc_attr( $this->get_post_id() ) + ); + + $microformat .= sprintf( + '<span class="latitude">%s</span>', + esc_html( $meta_values['latitude'] ) + ); + + $microformat .= sprintf( + '<span class="longitude">%s</span>', + esc_html( $meta_values['longitude'] ) + ); + + $microformat .= '</div>'; + + return $content . $microformat; + } + + /** + * Register a range of hooks for integrating geo data with various feeds. + */ + public function register_rss_hooks() { + add_action( 'rss2_ns', array( $this, 'rss_namespace' ) ); + add_action( 'atom_ns', array( $this, 'rss_namespace' ) ); + add_action( 'rdf_ns', array( $this, 'rss_namespace' ) ); + add_action( 'rss_item', array( $this, 'rss_item' ) ); + add_action( 'rss2_item', array( $this, 'rss_item' ) ); + add_action( 'atom_entry', array( $this, 'rss_item' ) ); + add_action( 'rdf_item', array( $this, 'rss_item' ) ); + } + + /** + * Add the georss namespace during RSS generation. + */ + public function rss_namespace() { + echo PHP_EOL . 'xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"' . PHP_EOL; + } + + /** + * Output georss data for RSS items, assuming we have data for the currently rendered post and + * that data as marked as public. + */ + public function rss_item() { + $meta_values = $this->get_meta_values( $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return; + } + + printf( + "\t<georss:point>%s %s</georss:point>\n", + ent2ncr( esc_html( $meta_values['latitude'] ) ), + ent2ncr( esc_html( $meta_values['longitude'] ) ) + ); + + printf( "\t\t<geo:lat>%s</geo:lat>\n", ent2ncr( esc_html( $meta_values['latitude'] ) ) ); + printf( "\t\t<geo:long>%s</geo:long>\n", ent2ncr( esc_html( $meta_values['longitude'] ) ) ); + } + + /** + * Enqueue CSS for rendering post flair with geo-location. + */ + private static function enqueue_scripts() { + wp_enqueue_style( 'dashicons' ); + } + + /** + * If we're rendering a single post and public geo-location data is available for it, + * include the human-friendly location label in the output. + * + * @param string $content + * + * @return string + */ + public function the_content_location_display( $content ) { + if ( ! is_single() ) { + return $content; + } + + return $content . $this->get_location_label(); + } + + /** + * Get the HTML for displaying a label representing the location associated with the + * supplied post ID. If no post ID is given, we'll use the global $post variable, if + * it is available. + * + * @param integer|null $post_id + * + * @return string + */ + public function get_location_label( $post_id = null ) { + $meta_values = $this->get_meta_values( $post_id ? $post_id : $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return ''; + } + + // If the location has not been labeled, do not show the location. + if ( ! $meta_values['label'] ) { + return ''; + } + + $html = '<div class="post-geo-location-label geo-chip">'; + $html .= '<span class="dashicons dashicons-location" style="vertical-align: text-top;"></span> '; + $html .= esc_html( $meta_values['label'] ); + $html .= '</div>'; + + /** + * Allow modification or replacement of the default geo-location display HTML. + * + * @module geo-location + * + * @param array $html The default HTML for displaying a geo-location label. + * @param array $geo_data An array containing "latitude", "longitude" and "label". + */ + $html = apply_filters( 'jetpack_geo_location_display', $html, $meta_values ); + + return $html; + } + + /** + * Get the ID of the current global post object, if available. Otherwise, return null. + * + * This isolates the access of the global scope to this single method, making it easier to + * safeguard against unexpected missing $post objects in other hook functions. + * + * @return int|null + */ + public function get_post_id() { + global $post; + + if ( ! isset( $post ) || ! $post || ! is_object( $post ) || ! isset( $post->ID ) ) { + return null; + } + + return $post->ID; + } + + /** + * This method always returns an array with the following structure: + * + * array(is_public => bool, latitude => float, longitude => float, label => string, is_populated => bool) + * + * So, regardless of whether your post actually has values in postmeta for the geo-location fields, + * you can be sure that you can reference those array keys in calling code without having to juggle + * isset(), array_key_exists(), etc. + * + * Mocking this method during testing can also be useful for testing output and logic in various + * hook functions. + * + * @param integer $post_id + * + * @return array A predictably structured array representing the meta values for the supplied post ID. + */ + public function get_meta_values( $post_id ) { + $meta_values = array( + 'is_public' => (bool) $this->sanitize_public( $this->get_meta_value( $post_id, 'public' ) ), + 'latitude' => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'latitude' ) ), + 'longitude' => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'longitude' ) ), + 'label' => trim( $this->get_meta_value( $post_id, 'address' ) ), + 'is_populated' => false, + ); + + if ( $meta_values['latitude'] && $meta_values['longitude'] && $meta_values['label'] ) { + $meta_values['is_populated'] = true; + } + + return $meta_values; + } + + /** + * This function wraps get_post_meta() to enable us to keep the "geo_" prefix isolated to a single + * location in the code and to assist in mocking during testing. + * + * @param integer $post_id + * @param string $meta_field_name + * + * @return mixed + */ + public function get_meta_value( $post_id, $meta_field_name ) { + if ( ! $post_id ) { + return null; + } + + return get_post_meta( $post_id, 'geo_' . $meta_field_name, true ); + } + + /** + * Check to see if the current filter is the get_the_excerpt filter. + * + * Just checking current_filter() here is not adequate because current_filter() only looks + * at the last element in the $wp_current_filter array. In the context of rendering an + * excerpt, however, both get_the_excerpt and the_content are present in that array. + * + * @return bool + */ + public function is_currently_excerpt_filter() { + if ( ! isset( $GLOBALS['wp_current_filter'] ) ) { + return false; + } + + $current_filters = (array) $GLOBALS['wp_current_filter']; + + return in_array( 'get_the_excerpt', $current_filters, true ); + } +} + +Jetpack_Geo_Location::init(); |