Bez popisu

class.jetpack-geo-location.php 12KB

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