Completed
Push — add/crowdsignal-shortcode ( 65c42e...1b4a63 )
by Kuba
14:46 queued 06:22
created

Jetpack_Geo_Location::wp_head()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

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