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\n"; if ( $meta_values['label'] ) { printf( '', esc_attr( $meta_values['label'] ) ); } printf( '' . PHP_EOL, esc_attr( $meta_values['latitude'] ), esc_attr( $meta_values['longitude'] ) ); printf( '' . PHP_EOL, esc_attr( $meta_values['latitude'] ), esc_attr( $meta_values['longitude'] ) ); echo "\n\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( ''; 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%s %s\n", ent2ncr( esc_html( $meta_values['latitude'] ) ), ent2ncr( esc_html( $meta_values['longitude'] ) ) ); printf( "\t\t%s\n", ent2ncr( esc_html( $meta_values['latitude'] ) ) ); printf( "\t\t%s\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 = '
'; $html .= ' '; $html .= esc_html( $meta_values['label'] ); $html .= '
'; /** * 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();