Completed
Push — try/gutenberg-separate-jetpack... ( e8dd3e...f0efb9 )
by Bernhard
39:26 queued 23:22
created

Jetpack_Geo_Location::wp_head()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 0
dl 0
loc 34
rs 9.376
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Adds support for geo-location features.
5
 *
6
 * All Jetpack sites can support geo-location features.  Users can tag posts with geo-location data
7
 * using the UI provided by Calypso.  That information will be included in RSS feeds, meta tags during
8
 * wp_head, and in the Geo microformat following post content.
9
 *
10
 * If your theme declares support for "geo-location", you'll also get a small icon and location label
11
 * visible to users at the bottom of single posts and pages.
12
 *
13
 * To declare support in your theme, call `add_theme_support( 'jetpack-geo-location' )`.
14
 *
15
 * Once you've added theme support, you can rely on the standard HTML output generated in the
16
 * the_content_location_display() method of this class.  Or, you can use the "geo_location_display"
17
 * filter to generate custom HTML for your particular theme.  Your filter function will receive an
18
 * the default HTML as its first argument and an array containing the geo-location information as
19
 * its second argument in the following format:
20
 *
21
 * array(
22
 *     'is_public'    => boolean,
23
 *     'latitude'     => float,
24
 *     'longitude'    => float,
25
 *     'label'        => string,
26
 *     'is_populated' => boolean
27
 * )
28
 *
29
 * Add your filter with:
30
 *
31
 * add_filter( 'jetpack_geo_location_display', 'your_filter_function_name', 10, 2);
32
 */
33
class Jetpack_Geo_Location {
34
	private static $instance;
35
36
	public static function init() {
37
		if ( is_null( self::$instance ) ) {
38
			self::$instance = new Jetpack_Geo_Location();
39
		}
40
41
		return self::$instance;
42
	}
43
44
	/**
45
	 * This is mostly just used for testing purposes.
46
	 */
47
	public static function reset_instance() {
48
		self::$instance = null;
49
	}
50
51
	public function __construct() {
52
		add_action( 'init', array( $this, 'wordpress_init' ) );
53
		add_action( 'wp_head', array( $this, 'wp_head' ) );
54
		add_filter( 'the_content', array( $this, 'the_content_microformat' ) );
55
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
56
57
		$this->register_rss_hooks();
58
	}
59
60
	/**
61
	 * Register support for the geo-location feature on pages and posts.  Register the meta
62
	 * fields managed by this plugin so that they are properly sanitized during save.
63
	 */
64
	public function wordpress_init() {
65
		// Only render location label after post content, if the theme claims to support "geo-location".
66
		if ( current_theme_supports( 'jetpack-geo-location' ) ) {
67
			add_filter( 'the_content', array( $this, 'the_content_location_display' ), 15, 1 );
68
		}
69
70
		add_post_type_support( 'post', 'geo-location' );
71
		add_post_type_support( 'page', 'geo-location' );
72
73
		register_meta(
74
			'post',
75
			'geo_public',
76
			array(
77
				'sanitize_callback' => array( $this, 'sanitize_public' ),
78
				'type'              => 'boolean',
79
				'single'            => true,
80
			)
81
		);
82
83
		register_meta(
84
			'post',
85
			'geo_latitude',
86
			array(
87
				'sanitize_callback' => array( $this, 'sanitize_coordinate' ),
88
				'type'              => 'float',
89
				'single'            => true,
90
			)
91
		);
92
93
		register_meta(
94
			'post',
95
			'geo_longitude',
96
			array(
97
				'sanitize_callback' => array( $this, 'sanitize_coordinate' ),
98
				'type'              => 'float',
99
				'single'            => true,
100
			)
101
		);
102
103
		register_meta(
104
			'post',
105
			'geo_address',
106
			array(
107
				'sanitize_callback' => 'sanitize_text_field',
108
				'type'              => 'string',
109
				'single'            => true,
110
			)
111
		);
112
	}
113
114
	/**
115
	 * Filter "public" input to always be either 1 or 0.
116
	 *
117
	 * @param mixed $public
118
	 *
119
	 * @return int
120
	 */
121
	public function sanitize_public( $public ) {
122
		return absint( $public ) ? 1 : 0;
123
	}
124
125
	/**
126
	 * Filter geo coordinates and normalize them to floats with 7 digits of precision.
127
	 *
128
	 * @param mixed $coordinate
129
	 *
130
	 * @return float|null
131
	 */
132
	public function sanitize_coordinate( $coordinate ) {
133
		if ( ! $coordinate ) {
134
			return null;
135
		}
136
137
		return round( (float) $coordinate, 7 );
138
	}
139
140
	/**
141
	 * Render geo.position and ICBM meta tags with public geo meta values when rendering
142
	 * a single post.
143
	 */
144
	public function wp_head() {
145
		if ( ! is_single() ) {
146
			return;
147
		}
148
149
		$meta_values = $this->get_meta_values( $this->get_post_id() );
150
151
		if ( ! $meta_values['is_public'] ) {
152
			return;
153
		}
154
155
		echo "\n<!-- Jetpack Geo-location Tags -->\n";
156
157
		if ( $meta_values['label'] ) {
158
			printf(
159
				'<meta name="geo.placename" content="%s" />',
160
				esc_attr( $meta_values['label'] )
161
			);
162
		}
163
164
		printf(
165
			'<meta name="geo.position" content="%s;%s" />' . PHP_EOL,
166
			esc_attr( $meta_values['latitude'] ),
167
			esc_attr( $meta_values['longitude'] )
168
		);
169
170
		printf(
171
			'<meta name="ICBM" content="%s, %s" />' . PHP_EOL,
172
			esc_attr( $meta_values['latitude'] ),
173
			esc_attr( $meta_values['longitude'] )
174
		);
175
176
		echo "\n<!-- End Jetpack Geo-location Tags -->\n";
177
	}
178
179
	/**
180
	 * Append public meta values in the Geo microformat (https://en.wikipedia.org/wiki/Geo_(microformat)
181
	 * to the supplied content.
182
	 *
183
	 * Note that we cannot render the microformat in the context of an excerpt because tags are stripped
184
	 * in that context, making our microformat data visible.
185
	 *
186
	 * @param string $content
187
	 *
188
	 * @return string
189
	 */
190
	public function the_content_microformat( $content ) {
191
		if ( is_feed() || $this->is_currently_excerpt_filter() ) {
192
			return $content;
193
		}
194
195
		$meta_values = $this->get_meta_values( $this->get_post_id() );
196
197
		if ( ! $meta_values['is_public'] ) {
198
			return $content;
199
		}
200
201
		$microformat = sprintf(
202
			'<div id="geo-post-%d" class="geo geo-post" style="display: none">',
203
			esc_attr( $this->get_post_id() )
204
		);
205
206
		$microformat .= sprintf(
207
			'<span class="latitude">%s</span>',
208
			esc_html( $meta_values['latitude'] )
209
		);
210
211
		$microformat .= sprintf(
212
			'<span class="longitude">%s</span>',
213
			esc_html( $meta_values['longitude'] )
214
		);
215
216
		$microformat .= '</div>';
217
218
		return $content . $microformat;
219
	}
220
221
	/**
222
	 * Register a range of hooks for integrating geo data with various feeds.
223
	 */
224
	public function register_rss_hooks() {
225
		add_action( 'rss2_ns', array( $this, 'rss_namespace' ) );
226
		add_action( 'atom_ns', array( $this, 'rss_namespace' ) );
227
		add_action( 'rdf_ns', array( $this, 'rss_namespace' ) );
228
		add_action( 'rss_item', array( $this, 'rss_item' ) );
229
		add_action( 'rss2_item', array( $this, 'rss_item' ) );
230
		add_action( 'atom_entry', array( $this, 'rss_item' ) );
231
		add_action( 'rdf_item', array( $this, 'rss_item' ) );
232
	}
233
234
	/**
235
	 * Add the georss namespace during RSS generation.
236
	 */
237
	public function rss_namespace() {
238
		echo 'xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" ';
239
	}
240
241
	/**
242
	 * Output georss data for RSS items, assuming we have data for the currently rendered post and
243
	 * that data as marked as public.
244
	 */
245
	public function rss_item() {
246
		$meta_values = $this->get_meta_values( $this->get_post_id() );
247
248
		if ( ! $meta_values['is_public'] ) {
249
			return;
250
		}
251
252
		printf(
253
			"\t<georss:point>%s %s</georss:point>\n",
254
			ent2ncr( esc_html( $meta_values['latitude'] ) ),
255
			ent2ncr( esc_html( $meta_values['longitude'] ) )
256
		);
257
258
		printf( "\t\t<geo:lat>%s</geo:lat>\n", ent2ncr( esc_html( $meta_values['latitude'] ) ) );
259
		printf( "\t\t<geo:long>%s</geo:long>\n", ent2ncr( esc_html( $meta_values['longitude'] ) ) );
260
	}
261
262
	/**
263
	 * Enqueue CSS for rendering post flair with geo-location.
264
	 */
265
	public function enqueue_scripts() {
266
		wp_enqueue_style( 'dashicons' );
267
	}
268
269
	/**
270
	 * If we're rendering a single post and public geo-location data is available for it,
271
	 * include the human-friendly location label in the output.
272
	 *
273
	 * @param string $content
274
	 *
275
	 * @return string
276
	 */
277
	public function the_content_location_display( $content ) {
278
		if ( ! is_single() ) {
279
			return $content;
280
		}
281
282
		return $content . $this->get_location_label();
283
	}
284
285
	/**
286
	 * Get the HTML for displaying a label representing the location associated with the
287
	 * supplied post ID.  If no post ID is given, we'll use the global $post variable, if
288
	 * it is available.
289
	 *
290
	 * @param integer|null $post_id
291
	 *
292
	 * @return string
293
	 */
294
	public function get_location_label( $post_id = null ) {
295
		$meta_values = $this->get_meta_values( $post_id ? $post_id : $this->get_post_id() );
296
297
		if ( ! $meta_values['is_public'] ) {
298
			return '';
299
		}
300
301
		// If the location has not been labeled, do not show the location.
302
		if ( ! $meta_values['label'] ) {
303
			return '';
304
		}
305
306
		$html  = '<div class="post-geo-location-label geo-chip">';
307
		$html .= '<span class="dashicons dashicons-location" style="vertical-align: text-top;"></span> ';
308
		$html .= esc_html( $meta_values['label'] );
309
		$html .= '</div>';
310
311
		/**
312
		 * Allow modification or replacement of the default geo-location display HTML.
313
		 *
314
		 * @module geo-location
315
		 *
316
		 * @param array $html The default HTML for displaying a geo-location label.
317
		 * @param array $geo_data An array containing "latitude", "longitude" and "label".
318
		 */
319
		$html = apply_filters( 'jetpack_geo_location_display', $html, $meta_values );
320
321
		return $html;
322
	}
323
324
	/**
325
	 * Get the ID of the current global post object, if available.  Otherwise, return null.
326
	 *
327
	 * This isolates the access of the global scope to this single method, making it easier to
328
	 * safeguard against unexpected missing $post objects in other hook functions.
329
	 *
330
	 * @return int|null
331
	 */
332
	public function get_post_id() {
333
		global $post;
334
335
		if ( ! isset( $post ) || ! $post || ! is_object( $post ) || ! isset( $post->ID ) ) {
336
			return null;
337
		}
338
339
		return $post->ID;
340
	}
341
342
	/**
343
	 * This method always returns an array with the following structure:
344
	 *
345
	 * array(is_public => bool, latitude => float, longitude => float, label => string, is_populated => bool)
346
	 *
347
	 * So, regardless of whether your post actually has values in postmeta for the geo-location fields,
348
	 * you can be sure that you can reference those array keys in calling code without having to juggle
349
	 * isset(), array_key_exists(), etc.
350
	 *
351
	 * Mocking this method during testing can also be useful for testing output and logic in various
352
	 * hook functions.
353
	 *
354
	 * @param integer $post_id
355
	 *
356
	 * @return array A predictably structured array representing the meta values for the supplied post ID.
357
	 */
358
	public function get_meta_values( $post_id ) {
359
		$meta_values = array(
360
			'is_public'    => (bool) $this->sanitize_public( $this->get_meta_value( $post_id, 'public' ) ),
361
			'latitude'     => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'latitude' ) ),
362
			'longitude'    => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'longitude' ) ),
363
			'label'        => trim( $this->get_meta_value( $post_id, 'address' ) ),
364
			'is_populated' => false,
365
		);
366
367
		if ( $meta_values['latitude'] && $meta_values['longitude'] && $meta_values['label'] ) {
368
			$meta_values['is_populated'] = true;
369
		}
370
371
		return $meta_values;
372
	}
373
374
	/**
375
	 * This function wraps get_post_meta() to enable us to keep the "geo_" prefix isolated to a single
376
	 * location in the code and to assist in mocking during testing.
377
	 *
378
	 * @param integer $post_id
379
	 * @param string  $meta_field_name
380
	 *
381
	 * @return mixed
382
	 */
383
	public function get_meta_value( $post_id, $meta_field_name ) {
384
		if ( ! $post_id ) {
385
			return null;
386
		}
387
388
		return get_post_meta( $post_id, 'geo_' . $meta_field_name, true );
389
	}
390
391
	/**
392
	 * Check to see if the current filter is the get_the_excerpt filter.
393
	 *
394
	 * Just checking current_filter() here is not adequate because current_filter() only looks
395
	 * at the last element in the $wp_current_filter array.  In the context of rendering an
396
	 * excerpt, however, both get_the_excerpt and the_content are present in that array.
397
	 *
398
	 * @return bool
399
	 */
400
	public function is_currently_excerpt_filter() {
401
		if ( ! isset( $GLOBALS['wp_current_filter'] ) ) {
402
			return false;
403
		}
404
405
		$current_filters = (array) $GLOBALS['wp_current_filter'];
406
407
		return in_array( 'get_the_excerpt', $current_filters, true );
408
	}
409
}
410
411
Jetpack_Geo_Location::init();
412