PluginProbe
Parse.ly / 2.3
Parse.ly v2.3
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / wp-parsely.php

wp-parsely.php in Parse.ly 2.3, at wp-parsely.php

1,723 lines 56.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Parse.ly
4 * Plugin URI: http://www.parsely.com/
5 * Description: This plugin makes it a snap to add Parse.ly tracking code to your WordPress blog.
6 * Author: Mike Sukmanowsky ( mike@parsely.com )
7 * Version: 2.3
8 * Requires at least: 4.0.0
9 * Author: Parse.ly
10 * Author URI: http://www.parsely.com/
11 * License: GPL2
12 * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
13
14 @package WordPress
15
16 Copyright 2012 Parsely Incorporated
17
18 This program is free software; you can redistribute it and/or modify
19 it under the terms of the GNU General Public License, version 2, as
20 published by the Free Software Foundation.
21
22 This program is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 GNU General Public License for more details.
26
27 You should have received a copy of the GNU General Public License
28 along with this program; if not, write to the Free Software
29 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
30 or visit https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
31
32 Authors: Mike Sukmanowsky ( mike@parsely.com), Xand Lourenco ( xand@parsely.com ), James O'Toole (james.otoole@parsely.com )
33 */
34
35 /*
36 TODO List:
37 * WordPress Network support - going to hold off on any specific support here as content id prefix should work ok for now
38 * Allow the user to map get_post_types() to Parse.ly post types
39 * Support: is_search(), is_404()
40 */
41
42 /**
43 * This is the main class for Parsely
44 *
45 * @category Class
46 * @package Parsely
47 */
48 class Parsely {
49 /**
50 * Declare our constants
51 *
52 * @codeCoverageIgnoreStart
53 */
54 const VERSION = '2.3';
55 const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php.
56 const MENU_TITLE = 'Parse.ly'; // Text to be used for the menu as seen in Settings sub-menu.
57 const MENU_PAGE_TITLE = 'Parse.ly > Settings'; // Text shown in <title></title> when the settings screen is viewed.
58 const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database.
59 const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings.
60
61 /**
62 * Declare some class propeties
63 *
64 * @var array $option_defaults The defaults we need for the class.
65 */
66 private $option_defaults = array(
67 'apikey' => '',
68 'content_id_prefix' => '',
69 'api_secret' => '',
70 'use_top_level_cats' => false,
71 'custom_taxonomy_section' => 'category',
72 'cats_as_tags' => false,
73 'track_authenticated_users' => true,
74 'lowercase_tags' => true,
75 'force_https_canonicals' => false,
76 'track_post_types' => array( 'post' ),
77 'track_page_types' => array( 'page' ),
78 'disable_javascript' => false,
79 'disable_amp' => false,
80 'meta_type' => 'json_ld',
81 'logo' => '',
82 'metadata_secret' => '',
83 'parsely_wipe_metadata_cache' => false,
84 );
85
86 /**
87 * The constructor
88 *
89 * @category Function
90 * @package Parsely
91 */
92 public function __construct() {
93 // Run upgrade options if they exist for the version currently defined.
94 $options = $this->get_options();
95 if ( empty( $options['plugin_version'] ) || self::VERSION !== $options['plugin_version'] ) {
96 $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
97 if ( method_exists( $this, $method ) ) {
98 call_user_func_array( array( $this, $method ), array( $options ) );
99 }
100 // Update our version info.
101 $options['plugin_version'] = self::VERSION;
102 update_option( self::OPTIONS_KEY, $options );
103 }
104
105 // admin_menu and a settings link.
106 add_action( 'admin_head', array( $this, 'add_admin_header' ) );
107 add_action( 'admin_menu', array( $this, 'add_settings_sub_menu' ) );
108 add_action( 'admin_init', array( $this, 'initialize_settings' ) );
109 // display warning when plugin hasn't been configured.
110 add_action( 'admin_footer', array( $this, 'display_admin_warning' ) );
111
112 $basename = plugin_basename( __FILE__ );
113 add_filter(
114 'plugin_action_links_' . $basename,
115 array( $this, 'add_plugin_meta_links' )
116 );
117
118 add_filter( 'cron_schedules', [ $this, 'wpparsely_add_cron_interval' ] );
119 add_action( 'parsely_bulk_metas_update', array( $this, 'bulk_update_posts' ) );
120 // inserting parsely code.
121 add_action( 'wp_head', array( $this, 'insert_parsely_page' ) );
122 add_action( 'wp_footer', array( $this, 'insert_parsely_javascript' ) );
123 add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
124 add_action( 'instant_articles_compat_registry_analytics', array( $this, 'insert_parsely_tracking_fbia' ) );
125 add_action( 'template_redirect', array( $this, 'parsely_add_amp_actions' ) );
126 if ( ! defined( 'WP_PARSELY_TESTING' ) ) {
127 add_action( 'wp_enqueue_scripts', [ $this, 'wp_parsely_style_init' ] );
128 add_action( 'wp_enqueue_scripts', [ $this, 'ensure_jquery_exists' ] );
129 }
130 }
131
132 /**
133 * Adds 10 minute cron interval
134 *
135 * @param array $schedules WP schedules array.
136 */
137 public function wpparsely_add_cron_interval( $schedules ) {
138 $schedules['everytenminutes'] = array(
139 'interval' => 600, // time in seconds.
140 'display' => 'Every 10 Minutes',
141 );
142 return $schedules;
143 }
144
145 /**
146 * Initialize parsely WordPress style
147 */
148 public function wp_parsely_style_init() {
149 wp_enqueue_style( 'wp-parsely-style', plugins_url( 'wp-parsely.css', __FILE__ ), array(), filemtime( get_stylesheet_directory() ) );
150 }
151
152 /**
153 * Make sure that jquery exists
154 */
155 public function ensure_jquery_exists() {
156 wp_enqueue_script( 'jquery' );
157 }
158
159 /**
160 * Include the parsely admin header
161 *
162 * @category Function
163 * @package Parsely
164 */
165 public function add_admin_header() {
166 include 'parsely-admin-header.php';
167 }
168
169 /**
170 * Parsely settings page in WordPress settings menu.
171 *
172 * @category Function
173 * @package Parsely
174 */
175 public function add_settings_sub_menu() {
176 add_options_page(
177 self::MENU_PAGE_TITLE,
178 self::MENU_TITLE,
179 self::CAPABILITY,
180 self::MENU_SLUG,
181 array( $this, 'display_settings' )
182 );
183 }
184
185 /**
186 * Parse.ly settings screen ( options-general.php?page=[MENU_SLUG] )
187 *
188 * @category Function
189 * @package Parsely
190 */
191 public function display_settings() {
192 if ( ! current_user_can( self::CAPABILITY ) ) {
193 wp_die( esc_attr( 'You do not have sufficient permissions to access this page.' ) );
194 }
195
196 include 'parsely-settings.php';
197 }
198
199 /**
200 * Initialize the settings for Parsely
201 *
202 * @category Function
203 * @package Parsely
204 */
205 public function initialize_settings() {
206 // All our options are actually stored in one single array to reduce
207 // DB queries.
208 register_setting(
209 self::OPTIONS_KEY,
210 self::OPTIONS_KEY,
211 array( $this, 'validate_options' )
212 );
213
214 // These are the Required Settings.
215 add_settings_section(
216 'required_settings',
217 'Required Settings',
218 array( $this, 'print_required_settings' ),
219 self::MENU_SLUG
220 );
221
222 // Get the API Key.
223 $h = 'Your Site ID is your own site domain ( e.g. `mydomain.com` )';
224
225 $field_args = array(
226 'option_key' => 'apikey',
227 'help_text' => $h,
228 );
229 add_settings_field(
230 'apikey',
231 'Parse.ly Site ID <div class="help-icons"></div>',
232 array( $this, 'print_text_tag' ),
233 self::MENU_SLUG,
234 'required_settings',
235 $field_args
236 );
237
238 // These are the Optional Settings.
239 add_settings_section(
240 'optional_settings',
241 'Optional Settings',
242 array( $this, 'print_optional_settings' ),
243 self::MENU_SLUG
244 );
245
246 $h = 'Your API secret is your secret code to %s%s%saccess our API.%s
247 It can be found at dash.parsely.com/yoursitedomain/settings/api
248 ( replace yoursitedown with your domain name, e.g. `mydomain.com` ) If you haven\'t purchased access to the API, and would
249 like to do so, email your account manager or support@parsely.com!';
250 $h_link = 'https://www.parse.ly/help/api/analytics/';
251
252 $field_args = array(
253 'option_key' => 'api_secret',
254 'help_text' => $h,
255 'help_link' => $h_link,
256 );
257 add_settings_field(
258 'api_secret',
259 'Parse.ly API Secret <div class="help-icons"></div>',
260 array( $this, 'print_text_tag' ),
261 self::MENU_SLUG,
262 'optional_settings',
263 $field_args
264 );
265
266 $h = 'Your metadata secret is given to you by Parse.ly support. DO NOT enter anything here unless given to you by Parse.ly support!';
267 $h_link = 'https://www.parse.ly/help/api/analytics/';
268
269 $field_args = array(
270 'option_key' => 'metadata_secret',
271 'help_text' => $h,
272 'help_link' => $h_link,
273 );
274 add_settings_field(
275 'metadata_secret',
276 'Parse.ly Metadata Secret <div class="help-icons"></div>',
277 array( $this, 'print_text_tag' ),
278 self::MENU_SLUG,
279 'optional_settings',
280 $field_args
281 );
282
283 // Clear metadata.
284 $h = 'Check this radio button and hit "Save Changes" to clear all metadata information for Parsely posts and re-send all metadata
285 to Parsely. WARNING: do not do this unless explicitly instructed by Parse.ly Staff!';
286 add_settings_field(
287 'parsely_wipe_metadata_cache',
288 'Wipe Parsely Metadata Info <div class="help-icons"></div>',
289 array( $this, 'print_checkbox_tag' ),
290 self::MENU_SLUG,
291 'optional_settings',
292 array(
293 'option_key' => 'parsely_wipe_metadata_cache',
294 'help_text' => $h,
295 'requires_recrawl' => false,
296 )
297 );
298
299 $h = 'Choose the metadata format for our crawlers to access. ' .
300 'Most publishers are fine with JSON-LD ( %s%s%shttps://www.parse.ly/help/integration/jsonld/%s ), ' .
301 'but if you prefer to use our proprietary metadata format then you can do so here.';
302 $h_link = 'https://www.parse.ly/help/integration/jsonld/';
303
304 add_settings_field(
305 'meta_type',
306 'Metadata Format <div class="help-icons"></div>',
307 array( $this, 'print_select_tag' ),
308 self::MENU_SLUG,
309 'optional_settings',
310 array(
311 'option_key' => 'meta_type',
312 'help_text' => $h,
313 'help_link' => $h_link,
314 // filter WordPress taxonomies under the hood that should not appear in dropdown.
315 'select_options' => array(
316 'json_ld' => 'json_ld',
317 'repeated_metas' => 'repeated_metas',
318 ),
319 'requires_recrawl' => true,
320 'multiple' => false,
321 )
322 );
323
324 $h = 'If you want to specify the url for your logo, you can do so here.';
325
326 $option_defaults['logo'] = $this->get_logo_default();
327
328 $field_args = array(
329 'option_key' => 'logo',
330 'help_text' => $h,
331 );
332
333 add_settings_field(
334 'logo',
335 'Logo <div class="help-icons"></div>',
336 array( $this, 'print_text_tag' ),
337 self::MENU_SLUG,
338 'optional_settings',
339 $field_args
340 );
341
342 // Content ID Prefix.
343 $h = 'If you use more than one content management system (e.g. ' .
344 'WordPress and Drupal), you may end up with duplicate content ' .
345 'IDs. Adding a Content ID Prefix will ensure the content IDs ' .
346 'from WordPress will not conflict with other content management ' .
347 'systems. We recommend using "WP-" for your prefix.';
348
349 $field_args = array(
350 'option_key' => 'content_id_prefix',
351 'optional_args' => array(
352 'placeholder' => 'WP-',
353 ),
354 'help_text' => $h,
355 'requires_recrawl' => true,
356 );
357 add_settings_field(
358 'content_id_prefix',
359 'Content ID Prefix <div class="help-icons"></div>',
360 array( $this, 'print_text_tag' ),
361 self::MENU_SLUG,
362 'optional_settings',
363 $field_args
364 );
365
366 // Disable javascript.
367 $h = 'If you use a separate system for Javascript tracking ( Tealium / Segment / Google Tag Manager / other tag manager solution ) ' .
368 'you may want to use that instead of having the plugin load the tracker. WARNING: disabling this option ' .
369 'will also disable the "Personalize Results" section of the recommended widget! We highly recommend leaving ' .
370 'this option set to "No"!';
371 add_settings_field(
372 'disable_javascript',
373 'Disable Javascript <div class="help-icons"></div>',
374 array( $this, 'print_binary_radio_tag' ),
375 self::MENU_SLUG,
376 'optional_settings',
377 array(
378 'option_key' => 'disable_javascript',
379 'help_text' => $h,
380 'requires_recrawl' => false,
381 )
382 );
383
384 // Disable amp tracking.
385 $h = 'If you use a separate system for Javascript tracking on AMP pages ( Tealium / Segment / Google Tag Manager / other tag manager solution ) ' .
386 'you may want to use that instead of having the plugin load the tracker.';
387 add_settings_field(
388 'disable_amp',
389 'Disable Amp Tracking <div class="help-icons"></div>',
390 array( $this, 'print_binary_radio_tag' ),
391 self::MENU_SLUG,
392 'optional_settings',
393 array(
394 'option_key' => 'disable_amp',
395 'help_text' => $h,
396 'requires_recrawl' => false,
397 )
398 );
399
400 // Use top-level categories.
401 $h = 'wp-parsely will use the first category assigned to a post. ' .
402 'With this option selected, if you post a story to News > ' .
403 'National > Florida, wp-parsely will use the "News" for the ' .
404 'section name in your dashboard instead of "Florida".';
405 add_settings_field(
406 'use_top_level_cats',
407 'Use Top-Level Categories for Section <div class="help-icons"></div>',
408 array( $this, 'print_binary_radio_tag' ),
409 self::MENU_SLUG,
410 'optional_settings',
411 array(
412 'option_key' => 'use_top_level_cats',
413 'help_text' => $h,
414 'requires_recrawl' => true,
415 )
416 );
417
418 // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
419 $h = 'By default, the section value in your Parse.ly dashboard maps to a post\'s category. ' .
420 'You can optionally choose a custom taxonomy, if you\'ve created one, to ' .
421 'populate the section value instead. ';
422 add_settings_field(
423 'custom_taxonomy_section',
424 'Use Custom Taxonomy for Section <div class="help-icons"></div>',
425 array( $this, 'print_select_tag' ),
426 self::MENU_SLUG,
427 'optional_settings',
428 array(
429 'option_key' => 'custom_taxonomy_section',
430 'help_text' => $h,
431 // filter WordPress taxonomies under the hood that should not appear in dropdown.
432 'select_options' => array_diff( get_taxonomies(), array( 'post_tag', 'nav_menu', 'author', 'link_category', 'post_format' ) ),
433 'requires_recrawl' => true,
434 )
435 );
436
437 // Use categories and custom taxonomies as tags.
438 $h = 'You can use this option to add all assigned categories and taxonomies to ' .
439 'your tags. For example, if you had a post assigned to ' .
440 'the categories: "Business/Tech", "Business/Social", your tags would include ' .
441 '"Business/Tech" and "Business/Social" in addition to your other tags.';
442 add_settings_field(
443 'cats_as_tags',
444 'Add Categories to Tags <div class="help-icons"></div>',
445 array( $this, 'print_binary_radio_tag' ),
446 self::MENU_SLUG,
447 'optional_settings',
448 array(
449 'option_key' => 'cats_as_tags',
450 'help_text' => $h,
451 'requires_recrawl' => true,
452 )
453 );
454
455 // Track logged-in users.
456 $h = 'By default, wp-parsely will track the activity of users that ' .
457 'are logged into this site. You can change this setting to only ' .
458 'track the activity of anonymous visitors. Note: You will no ' .
459 'longer see the Parse.ly tracking code on your site if you ' .
460 'browse while logged in.';
461 add_settings_field(
462 'track_authenticated_users',
463 'Track Logged-in Users <div class="help-icons"></div>',
464 array( $this, 'print_binary_radio_tag' ),
465 self::MENU_SLUG,
466 'optional_settings',
467 array(
468 'option_key' => 'track_authenticated_users',
469 'help_text' => $h,
470 'requires_recrawl' => true,
471 )
472 );
473
474 // Lowercase all tags.
475 $h = 'By default, wp-parsely will use lowercase versions of your ' .
476 'tags to correct for potential misspellings. You can change this ' .
477 'setting to ensure that tag names are used verbatim.';
478 add_settings_field(
479 'lowercase_tags',
480 'Lowercase All Tags <div class="help-icons"></div>',
481 array( $this, 'print_binary_radio_tag' ),
482 self::MENU_SLUG,
483 'optional_settings',
484 array(
485 'option_key' => 'lowercase_tags',
486 'help_text' => $h,
487 'requires_recrawl' => true,
488 )
489 );
490
491 $h = 'wp-parsely uses http canonical URLs by default. If this needs to be forced to use https, set this option ' .
492 ' to true. Note: the default is fine for almost all publishers, it\'s unlikely you\'ll have to change this unless' .
493 ' directed to do so by a Parsely support rep.';
494 add_settings_field(
495 'force_https_canonicals',
496 'Force HTTPS canonicals <div class="help-icons"></div>',
497 array( $this, 'print_binary_radio_tag' ),
498 self::MENU_SLUG,
499 'optional_settings',
500 array(
501 'option_key' => 'force_https_canonicals',
502 'help_text' => $h,
503 'requires_recrawl' => true,
504 )
505 );
506
507 // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
508 $h = 'By default, Parsely only tracks the default post type as a post page. ' .
509 'If you want to track custom post types, select them here!';
510 add_settings_field(
511 'track_post_types',
512 'Post Types To Track <div class="help-icons"></div>',
513 array( $this, 'print_select_tag' ),
514 self::MENU_SLUG,
515 'optional_settings',
516 array(
517 'option_key' => 'track_post_types',
518 'help_text' => $h,
519 // filter WordPress taxonomies under the hood that should not appear in dropdown.
520 'select_options' => get_post_types(),
521 'requires_recrawl' => true,
522 'multiple' => true,
523 )
524 );
525
526 // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
527 $h = 'By default, Parsely only tracks the default page type as a non-post page. ' .
528 'If you want to track custom post types as non-post pages, select them here!';
529 add_settings_field(
530 'track_page_types',
531 'Page Types To Track <div class="help-icons"></div>',
532 array( $this, 'print_select_tag' ),
533 self::MENU_SLUG,
534 'optional_settings',
535 array(
536 'option_key' => 'track_page_types',
537 'help_text' => $h,
538 // filter WordPress taxonomies under the hood that should not appear in dropdown.
539 'select_options' => get_post_types(),
540 'requires_recrawl' => true,
541 'multiple' => true,
542 )
543 );
544
545 // Dynamic tracking note.
546 add_settings_field(
547 'dynamic_tracking_note',
548 'Note: ',
549 array( $this, 'print_dynamic_tracking_note' ),
550 self::MENU_SLUG,
551 'optional_settings'
552 );
553 }
554
555 /**
556 * Validate options from an array
557 *
558 * @category Function
559 * @package Parsely
560 * @param array $array Array of options to be sanitized.
561 * @param string $name Unused?.
562 */
563 public function validate_option_array( $array, $name ) {
564 $new_array = $array;
565 foreach ( $array as $key => $val ) {
566 $new_array[ $key ] = sanitize_text_field( $val );
567 }
568 return $new_array;
569 }
570
571 /**
572 * Validate the options provided by the user
573 *
574 * @category Function
575 * @package Parsely
576 * @param array $input Options from the settings page.
577 * @return array $input list of validated input settings.
578 */
579 public function validate_options( $input ) {
580 if ( empty( $input['apikey'] ) ) {
581 add_settings_error(
582 self::OPTIONS_KEY,
583 'apikey',
584 'Please specify the Site ID'
585 );
586 } else {
587 $input['apikey'] = strtolower( $input['apikey'] );
588 $input['apikey'] = sanitize_text_field( $input['apikey'] );
589 if ( strpos( $input['apikey'], '.' ) === false || strpos( $input['apikey'], ' ' ) !== false ) {
590 add_settings_error(
591 self::OPTIONS_KEY,
592 'apikey',
593 'Your Parse.ly Site ID looks incorrect, it should look like "example.com".'
594 );
595 }
596 }
597 // these can't be null, if somebody accidentally deselected them just reset to default.
598 if ( ! isset( $input['track_post_types'] ) ) {
599 $input['track_post_types'] = array( 'post' );
600
601 }
602 if ( ! isset( $input['track_page_types'] ) ) {
603 $input['track_page_types'] = array( 'page' );
604 }
605
606 if ( empty( $input['logo'] ) ) {
607 $input['logo'] = $this->get_logo_default();
608 }
609
610 $input['track_post_types'] = $this->validate_option_array( $input['track_post_types'], 'track_post_types' );
611 $input['track_page_types'] = $this->validate_option_array( $input['track_page_types'], 'track_page_types' );
612
613 $input['api_secret'] = sanitize_text_field( $input['api_secret'] );
614 // Content ID prefix.
615 $input['content_id_prefix'] = sanitize_text_field( $input['content_id_prefix'] );
616 $input['custom_taxonomy_section'] = sanitize_text_field( $input['custom_taxonomy_section'] );
617
618 // Custom taxonomy as section.
619 // Top-level categories.
620 if ( 'true' !== $input['use_top_level_cats'] && 'false' !== $input['use_top_level_cats'] ) {
621 add_settings_error(
622 self::OPTIONS_KEY,
623 'use_top_level_cats',
624 'Value passed for use_top_level_cats must be either "true" or "false".'
625 );
626 } else {
627 $input['use_top_level_cats'] = 'true' === $input['use_top_level_cats'];
628 }
629
630 // Child categories as tags.
631 if ( 'true' !== $input['cats_as_tags'] && 'false' !== $input['cats_as_tags'] ) {
632 add_settings_error(
633 self::OPTIONS_KEY,
634 'cats_as_tags',
635 'Value passed for cats_as_tags must be either "true" or "false".'
636 );
637 } else {
638 $input['cats_as_tags'] = 'true' === $input['cats_as_tags'];
639 }
640
641 // Track authenticated users.
642 if ( 'true' !== $input['track_authenticated_users'] && 'false' !== $input['track_authenticated_users'] ) {
643 add_settings_error(
644 self::OPTIONS_KEY,
645 'track_authenticated_users',
646 'Value passed for track_authenticated_users must be either "true" or "false".'
647 );
648 } else {
649 $input['track_authenticated_users'] = 'true' === $input['track_authenticated_users'];
650 }
651
652 // Lowercase tags.
653 if ( 'true' !== $input['lowercase_tags'] && 'false' !== $input['lowercase_tags'] ) {
654 add_settings_error(
655 self::OPTIONS_KEY,
656 'lowercase_tags',
657 'Value passed for lowercase_tags must be either "true" or "false".'
658 );
659 } else {
660 $input['lowercase_tags'] = 'true' === $input['lowercase_tags'];
661 }
662
663 if ( 'true' !== $input['force_https_canonicals'] && 'false' !== $input['force_https_canonicals'] ) {
664 add_settings_error(
665 self::OPTIONS_KEY,
666 'force_https_canonicals',
667 'Value passed for force_https_canonicals must be either "true" or "false".'
668 );
669 } else {
670 $input['force_https_canonicals'] = 'true' === $input['force_https_canonicals'];
671 }
672
673 if ( 'true' !== $input['disable_javascript'] && 'false' !== $input['disable_javascript'] ) {
674 add_settings_error(
675 self::OPTIONS_KEY,
676 'disable_javascript',
677 'Value passed for disable_javascript must be either "true" or "false".'
678 );
679 } else {
680 $input['disable_javascript'] = 'true' === $input['disable_javascript'];
681 }
682
683 if ( 'true' !== $input['disable_amp'] && 'false' !== $input['disable_amp'] ) {
684 add_settings_error(
685 self::OPTIONS_KEY,
686 'disable_amp',
687 'Value passed for disable_amp must be either "true" or "false".'
688 );
689 } else {
690 $input['disable_amp'] = 'true' === $input['disable_amp'];
691 }
692
693 if ( ! empty( $input['metadata_secret'] ) ) {
694 if ( strlen( $input['metadata_secret'] ) !== 10 ) {
695 add_settings_error(
696 self::OPTIONS_KEY,
697 'metadata_secret',
698 'Metadata secret is incorrect. Please contact Parse.ly support!'
699 );
700 } elseif ( 'true' === $input['parsely_wipe_metadata_cache'] ) {
701 delete_post_meta_by_key( 'parsely_metadata_last_updated' );
702
703 wp_schedule_event( time() + 100, 'everytenminutes', 'parsely_bulk_metas_update' );
704 $input['parsely_wipe_metadata_cache'] = false;
705 }
706 }
707
708 return $input;
709 }
710
711 /**
712 * Not doing anything here
713 *
714 * @category Function
715 * @package Parsely
716 */
717 public function print_required_settings() {
718 // We can optionally print some text here in the future, but we don't
719 // need to now.
720 }
721
722 /**
723 * Not doing anything here
724 *
725 * @category Function
726 * @package Parsely
727 */
728 public function print_optional_settings() {
729 // We can optionally print some text here in the future, but we don't
730 // need to now.
731 }
732
733 /**
734 * Adds a 'Settings' link to the Plugins screen in WP admin
735 *
736 * @category Function
737 * @package Parsely
738 * @param array $links The links to add.
739 */
740 public function add_plugin_meta_links( $links ) {
741 array_unshift( $links, '<a href="' . esc_url( $this->get_settings_url() ) . '">' . __( 'Settings' ) . '</a>' );
742 return $links;
743 }
744
745 /**
746 * Display the admin warning if needed
747 *
748 * @category Function
749 * @package Parsely
750 */
751 public function display_admin_warning() {
752 $options = $this->get_options();
753 if ( ! isset( $options['apikey'] ) || empty( $options['apikey'] ) ) {
754 ?>
755 <div id='message' class='error'>
756 <p>
757 <strong>Parse.ly - Dash plugin is not active.</strong>
758 You need to
759 <a href='<?php echo esc_url( $this->get_settings_url() ); ?>'>
760 provide your Parse.ly Dash Site ID
761 </a>
762 before things get cooking.
763 </p>
764 </div>
765 <?php
766 }
767 }
768
769 /**
770 * Show our note about dynamic tracking
771 *
772 * @category Function
773 * @package Parsely
774 */
775 public function print_dynamic_tracking_note() {
776 printf(
777 'This plugin does not currently support dynamic tracking ( the tracking of multiple pageviews on a single page). Some common use-cases for dynamic tracking are slideshows or articles loaded via AJAX calls in single-page applications -- situations in which new content is loaded without a full page refresh. Tracking these events requires manually implementing additional JavaScript above <a href="%s">the standard Parse.ly include</a> that the plugin injects into your page source. Please consult <a href="%s">the Parse.ly documentation on dynamic tracking</a> for instructions on implementing dynamic tracking, or contact Parse.ly support (<a href="%s">support@parsely.com</a> ) for additional assistance.',
778 esc_url( 'http://www.parsely.com/help/integration/basic/' ),
779 esc_url( 'https://www.parsely.com/help/integration/dynamic/' ),
780 esc_url( 'mailto:support@parsely.com' )
781 );
782 }
783
784 /**
785 * End the code coverage ignore
786 *
787 * @codeCoverageIgnoreEnd
788 */
789
790 /**
791 * Actually inserts the code for the <meta name='parsely-page'> parameter within the <head></head> tag.
792 */
793 public function insert_parsely_page() {
794 $parsely_options = $this->get_options();
795
796 // If we don't have an API key or if we aren't supposed to show to logged in users, there's no need to proceed.
797 if ( empty( $parsely_options['apikey'] ) || ( ! $parsely_options['track_authenticated_users'] && $this->parsely_is_user_logged_in() ) || is_404() ) {
798 return '';
799 }
800
801 global $post;
802 // Assign default values for LD+JSON
803 // TODO: Maping of an install's post types to Parse.ly post types (namely page/post).
804 $parsely_page = $this->construct_parsely_metadata( $parsely_options, $post );
805 include 'parsely-parsely-page.php';
806 return $parsely_page;
807 }
808
809
810 /**
811 * Creates parsely metadata object from post metadata.
812 *
813 * @param array $parsely_options parsely_options array.
814 * @param WP_Post $post object.
815 * @return mixed|void
816 */
817 public function construct_parsely_metadata( array $parsely_options, $post ) {
818 $parsely_page = array(
819 '@context' => 'http://schema.org',
820 '@type' => 'WebPage',
821 );
822 $current_url = $this->get_current_url();
823 if ( in_array( get_post_type( $post ), $parsely_options['track_post_types'], true ) && 'publish' === $post->post_status ) {
824 $authors = $this->get_author_names( $post );
825 $category = $this->get_category_name( $post, $parsely_options );
826 $post_id = $parsely_options['content_id_prefix'] . get_the_ID();
827
828 if ( has_post_thumbnail( $post ) ) {
829 $image_id = get_post_thumbnail_id( $post );
830 $image_url = wp_get_attachment_image_src( $image_id );
831 $image_url = $image_url[0];
832 } else {
833 $image_url = $this->get_first_image( $post );
834 }
835
836 $tags = $this->get_tags( $post->ID );
837 if ( $parsely_options['cats_as_tags'] ) {
838 $tags = array_merge( $tags, $this->get_categories( $post->ID ) );
839 // add custom taxonomy values.
840 $tags = array_merge( $tags, $this->get_custom_taxonomy_values( $post, $parsely_options ) );
841 }
842 // the function 'mb_strtolower' is not enabled by default in php, so this check
843 // falls back to the native php function 'strtolower' if necessary.
844 if ( function_exists( 'mb_strtolower' ) ) {
845 $lowercase_callback = 'mb_strtolower';
846 } else {
847 $lowercase_callback = 'strtolower';
848 }
849 if ( $parsely_options['lowercase_tags'] ) {
850 $tags = array_map( $lowercase_callback, $tags );
851 }
852 $tags = apply_filters( 'wp_parsely_post_tags', $tags, $post->ID );
853 $tags = array_map( array( $this, 'get_clean_parsely_page_value' ), $tags );
854 $tags = array_values( array_unique( $tags ) );
855
856 $parsely_page['@type'] = 'NewsArticle';
857 $parsely_page['mainEntityOfPage'] = array(
858 '@type' => 'WebPage',
859 '@id' => $this->get_current_url( 'post' ),
860 );
861 $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_the_title( $post ) );
862 $parsely_page['url'] = $this->get_current_url( 'post', $post->ID );
863 $parsely_page['thumbnailUrl'] = $image_url;
864 $parsely_page['image'] = array(
865 '@type' => 'ImageObject',
866 'url' => $image_url,
867 );
868 $parsely_page['dateCreated'] = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
869 $parsely_page['datePublished'] = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
870 if ( get_the_modified_date( 'U', true ) >= get_post_time( 'U', true, $post ) ) {
871 $parsely_page['dateModified'] = gmdate( 'Y-m-d\TH:i:s\Z', get_the_modified_date( 'U', true ) );
872 } else {
873 // Use the post time as the earliest possible modification date.
874 $parsely_page['dateModified'] = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
875 }
876 $parsely_page['articleSection'] = $category;
877 $author_objects = array();
878 foreach ( $authors as $author ) {
879 $author_tag = array(
880 '@type' => 'Person',
881 'name' => $author,
882 );
883 array_push( $author_objects, $author_tag );
884 }
885 $parsely_page['author'] = $author_objects;
886 $parsely_page['creator'] = $authors;
887 $parsely_page['publisher'] = array(
888 '@type' => 'Organization',
889 'name' => get_bloginfo( 'name' ),
890 'logo' => $parsely_options['logo'],
891 );
892 $parsely_page['keywords'] = $tags;
893 } elseif ( in_array( get_post_type(), $parsely_options['track_page_types'], true ) && 'publish' === $post->post_status ) {
894 $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_the_title( $post ) );
895 $parsely_page['url'] = $this->get_current_url( 'post' );
896 } elseif ( is_author() ) {
897 // TODO: why can't we have something like a WP_User object for all the other cases? Much nicer to deal with than functions.
898 $author = ( get_query_var( 'author_name' ) ) ? get_user_by( 'slug', get_query_var( 'author_name' ) ) : get_userdata( get_query_var( 'author' ) );
899 $parsely_page['headline'] = $this->get_clean_parsely_page_value( 'Author - ' . $author->data->display_name );
900 $parsely_page['url'] = $current_url;
901 } elseif ( is_category() ) {
902 $category = get_the_category();
903 $category = $category[0];
904 $parsely_page['headline'] = $this->get_clean_parsely_page_value( $category->name );
905 $parsely_page['url'] = $current_url;
906 } elseif ( is_date() ) {
907 if ( is_year() ) {
908 $parsely_page['headline'] = 'Yearly Archive - ' . get_the_time( 'Y' );
909 } elseif ( is_month() ) {
910 $parsely_page['headline'] = 'Monthly Archive - ' . get_the_time( 'F, Y' );
911 } elseif ( is_day() ) {
912 $parsely_page['headline'] = 'Daily Archive - ' . get_the_time( 'F jS, Y' );
913 } elseif ( is_time() ) {
914 $parsely_page['headline'] = 'Hourly, Minutely, or Secondly Archive - ' . get_the_time( 'F jS g:i:s A' );
915 }
916 $parsely_page['url'] = $current_url;
917 } elseif ( is_tag() ) {
918 $tag = single_tag_title( '', false );
919 if ( empty( $tag ) ) {
920 $tag = single_term_title( '', false );
921 }
922 $parsely_page['headline'] = $this->get_clean_parsely_page_value( 'Tagged - ' . $tag );
923 $parsely_page['url'] = $current_url;
924 } elseif ( is_front_page() ) {
925 $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_bloginfo( 'name', 'raw' ) );
926 $parsely_page['url'] = home_url();
927 }
928 $parsely_page = apply_filters( 'after_set_parsely_page', $parsely_page, $post, $parsely_options );
929 return $parsely_page;
930 }
931
932
933 /**
934 * Updates the Parsely metadata endpoint with the new metadata of the post.
935 *
936 * @param int $post_id id of the post to update.
937 * @return string
938 */
939 public function update_metadata_endpoint( $post_id ) {
940 $parsely_options = $this->get_options();
941
942 if ( empty( $parsely_options['apikey'] ) || empty( $parsely_options['metadata_secret'] ) ) {
943 return '';
944 }
945
946 $post = get_post( $post_id );
947 $metadata = $this->construct_parsely_metadata( $parsely_options, $post );
948 $page_type_mapping = array(
949 'NewsArticle' => 'post',
950 'WebPage' => 'index',
951 );
952
953 $endpoint_metadata = array(
954 'canonical_url' => $metadata['url'],
955 'page_type' => $page_type_mapping[ $metadata['@type'] ],
956 'title' => $metadata['headline'],
957 'image_url' => $metadata['thumbnailUrl'],
958 'pub_date_tmsp' => $metadata['datePublished'],
959 'section' => $metadata['articleSection'],
960 'authors' => $metadata['creator'],
961 'tags' => $metadata['keywords'],
962 );
963
964 $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts';
965 $parsely_metadata_secret = $parsely_options['metadata_secret'];
966 $headers = array(
967 'Content-Type' => 'application/json',
968 );
969 $body = wp_json_encode(
970 array(
971 'secret' => $parsely_metadata_secret,
972 'apikey' => $parsely_options['apikey'],
973 'metadata' => $endpoint_metadata,
974 )
975 );
976 $response = wp_remote_post(
977 $parsely_api_endpoint,
978 array(
979 'method' => 'POST',
980 'headers' => $headers,
981 'blocking' => false,
982 'body' => $body,
983 'data_format' => 'body',
984 )
985 );
986 $current_timestamp = time();
987 $meta_update = update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
988
989 }
990
991
992 /**
993 * Updates posts with Parsely metadata api in bulk.
994 */
995 public function bulk_update_posts() {
996 global $wpdb;
997 $parsely_options = $this->get_options();
998 $allowed_types = array_merge( $parsely_options['track_post_types'], $parsely_options['track_page_types'] );
999 $allowed_types_string = implode(
1000 ', ',
1001 array_map(
1002 function( $v ) {
1003 return "'" . esc_sql( $v ) . "'";
1004 },
1005 $allowed_types
1006 )
1007 );
1008 $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
1009 if ( false === $ids ) {
1010 $ids = array();
1011 $results = $wpdb->get_results(
1012 $wpdb->prepare( "SELECT DISTINCT(id) FROM {$wpdb->posts} WHERE post_type IN (\" . %s . \") AND id NOT IN (SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'parsely_metadata_last_updated');", $allowed_types_string ),
1013 ARRAY_N
1014 );
1015 foreach ( $results as $result ) {
1016 array_push( $ids, $result[0] );
1017 }
1018 wp_cache_set( 'parsely_post_ids_need_meta_updating', $ids, '', 86400 );
1019 }
1020
1021 for ( $i = 0; $i < 100; $i++ ) {
1022 $post_id = array_pop( $ids );
1023 if ( null === $post_id ) {
1024 wp_clear_scheduled_hook( 'parsely_bulk_metas_update' );
1025 break;
1026 }
1027 $this->update_metadata_endpoint( $post_id );
1028 }
1029 }
1030
1031 /**
1032 * Inserts the JavaScript code required to send off beacon requests
1033 */
1034 public function insert_parsely_javascript() {
1035 $parsely_options = $this->get_options();
1036 // If we don't have an API key, there's no need to proceed.
1037 if ( empty( $parsely_options['apikey'] ) || $parsely_options['disable_javascript'] ) {
1038 return '';
1039 }
1040
1041 global $post;
1042 $display = true;
1043 if ( in_array( get_post_type(), $parsely_options['track_post_types'], true ) && 'publish' !== $post->post_status ) {
1044 $display = false;
1045 }
1046 if ( ! $parsely_options['track_authenticated_users'] && $this->parsely_is_user_logged_in() ) {
1047 $display = false;
1048 }
1049 if ( ! in_array( get_post_type(), $parsely_options['track_post_types'], true ) && ! in_array( get_post_type(), $parsely_options['track_page_types'], true ) ) {
1050 $display = false;
1051 }
1052 if ( apply_filters( 'parsely_filter_insert_javascript', $display ) ) {
1053 include 'parsely-javascript.php';
1054 }
1055 }
1056
1057 /**
1058 * Print out the select tags
1059 *
1060 * @param array $args The arguments for the select drop downs.
1061 */
1062 public function print_select_tag( $args ) {
1063 $options = $this->get_options();
1064 $name = $args['option_key'];
1065 $select_options = $args['select_options'];
1066 if ( isset( $args['multiple'] ) ) {
1067 $multiple = $args['multiple'];
1068 } else {
1069 $multiple = false;
1070 }
1071 $selected = isset( $options[ $name ] ) ? $options[ $name ] : null;
1072 $id = esc_attr( $name );
1073 $name = self::OPTIONS_KEY . "[$id]";
1074
1075 if ( isset( $args['help_text'] ) ) {
1076 echo '<div class="parsely-form-controls" data-has-help-text="true">';
1077 }
1078 if ( isset( $args['requires_recrawl'] ) ) {
1079 echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1080 }
1081
1082 if ( $multiple ) {
1083 echo sprintf( "<select multiple='multiple' name='%s[]'id='%s'", esc_attr( $name ), esc_attr( $name ) );
1084 } else {
1085 echo sprintf( "<select name='%s' id='%s'", esc_attr( $name ), esc_attr( $name ) );
1086 }
1087
1088 echo '>';
1089
1090 foreach ( $select_options as $key => $val ) {
1091 echo '<option value="' . esc_attr( $key ) . '" ';
1092
1093 if ( $multiple ) {
1094 $selected = in_array( $val, $options[ $args['option_key'] ], true );
1095 echo selected( $selected, true, false ) . '>';
1096 } else {
1097 echo selected( $selected, $key, false ) . '>';
1098 }
1099 echo esc_html( $val );
1100 echo '</option>';
1101 }
1102 echo '</select>';
1103
1104 if ( isset( $args['help_text'] ) ) {
1105 if ( isset( $args['help_link'] ) ) {
1106 echo '<div class="help-text"> <p class="description">' .
1107 sprintf( esc_html( $args['help_text'] ), '<a href="', esc_url( $args['help_link'] ), '">', '</a>' ) .
1108 '</p></div>';
1109 } else {
1110 echo '<div class="help-text"> <p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
1111 }
1112 }
1113 echo '</div>';
1114 }
1115
1116 /**
1117 * Print out the radio buttons
1118 *
1119 * @param array $args The arguments for the radio buttons.
1120 */
1121 public function print_binary_radio_tag( $args ) {
1122 $options = $this->get_options();
1123 $name = $args['option_key'];
1124 $value = $options[ $name ];
1125 $id = esc_attr( $name );
1126 $name = self::OPTIONS_KEY . "[$id]";
1127
1128 if ( isset( $args['help_text'] ) ) {
1129 echo '<div class="parsely-form-controls" data-has-help-text="true">';
1130 }
1131 if ( isset( $args['requires_recrawl'] ) ) {
1132 echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1133 }
1134
1135 echo sprintf( "<input type='radio' name='%s' id='%s_true' value='true' ", esc_attr( $name ), esc_attr( $id ) );
1136 echo checked( true === $value, true, false );
1137 echo sprintf( " /> <label for='%s_true'>Yes</label> <input type='radio' name='%s' id='%s_false' value='false' ", esc_attr( $id ), esc_attr( $name ), esc_attr( $id ) );
1138 echo checked( true !== $value, true, false );
1139 echo sprintf( " /> <label for='%s_false'>No</label>", esc_attr( $id ) );
1140
1141 if ( isset( $args['help_text'] ) ) {
1142 echo '<div class="help-text"><p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
1143 }
1144 echo '</div>';
1145
1146 }
1147
1148 /**
1149 * Prints a checkbox tag in the settings page.
1150 *
1151 * @param array $args Arguments to print to checkbox tag.
1152 */
1153 public function print_checkbox_tag( $args ) {
1154 $options = $this->get_options();
1155 $name = $args['option_key'];
1156 $value = $options[ $name ];
1157 $id = esc_attr( $name );
1158 $name = self::OPTIONS_KEY . "[$id]";
1159
1160 if ( isset( $args['help_text'] ) ) {
1161 echo '<div class="parsely-form-controls" data-has-help-text="true">';
1162 }
1163 if ( isset( $args['requires_recrawl'] ) ) {
1164 echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1165 }
1166
1167 echo sprintf( "<input type='checkbox' name='%s' id='%s_true' value='true' ", esc_attr( $name ), esc_attr( $id ) );
1168 echo checked( true === $value, true, false );
1169 echo sprintf( " /> <label for='%s_true'>Yes</label>", esc_attr( $id ) );
1170
1171 if ( isset( $args['help_text'] ) ) {
1172 echo '<div class="help-text"><p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
1173 }
1174 echo '</div>';
1175
1176 }
1177
1178 /**
1179 * Print out the radio buttons
1180 *
1181 * @param array $args The arguments for text tags.
1182 */
1183 public function print_text_tag( $args ) {
1184 $options = $this->get_options();
1185 $name = $args['option_key'];
1186 $value = isset( $options[ $name ] ) ? $options[ $name ] : '';
1187 $optional_args = isset( $args['optional_args'] ) ? $args['optional_args'] : array();
1188 $id = esc_attr( $name );
1189 $name = self::OPTIONS_KEY . "[$id]";
1190 $value = esc_attr( $value );
1191 $accepted_args = array( 'placeholder' );
1192
1193 if ( isset( $args['help_text'] ) ) {
1194 echo '<div class="parsely-form-controls" data-has-help-text="true">';
1195 }
1196 if ( isset( $args['requires_recrawl'] ) ) {
1197 echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1198 }
1199
1200 echo sprintf( "<input type='text' name='%s' id='%s' value='%s'", esc_attr( $name ), esc_attr( $id ), esc_attr( $value ) );
1201 foreach ( $optional_args as $key => $val ) {
1202 if ( in_array( $key, $accepted_args, true ) ) {
1203 echo ' ' . esc_attr( $key ) . '="' . esc_attr( $val ) . '"';
1204 }
1205 }
1206 if ( isset( $args['requires_recrawl'] ) ) {
1207 echo ' data-requires-recrawl="true"';
1208 }
1209 echo ' />';
1210
1211 if ( isset( $args['help_text'] ) ) {
1212 if ( isset( $args['help_link'] ) ) {
1213 echo ' <div class="help-text" id="' .
1214 esc_attr( $args['option_key'] ) .
1215 '_help_text"><p class="description">' .
1216 sprintf( esc_html( $args['help_text'] ), '<a href="', esc_url( $args['help_link'] ), '">', '</a>' ) .
1217 '</p>' .
1218 '</div>';
1219 } else {
1220 echo ' <div class="help-text" id="' .
1221 esc_attr( $args['option_key'] ) .
1222 '_help_text"><p class="description">' .
1223 esc_html( $args['help_text'] ) . '</p>' .
1224 '</div>';
1225 }
1226 }
1227 }
1228
1229 /**
1230 * Returns default logo if one can be found
1231 */
1232 private function get_logo_default() {
1233 $custom_logo_id = get_theme_mod( 'custom_logo' );
1234 if ( $custom_logo_id ) {
1235 $logo_attrs = wp_get_attachment_image_src( $custom_logo_id, 'full' );
1236 if ( $logo_attrs ) {
1237 return $logo_attrs[0];
1238 }
1239 }
1240
1241 // get_site_icon_url returns an empty string if one isn't found,
1242 // which is what we want to use as the default anyway.
1243 return get_site_icon_url();
1244 }
1245
1246 /**
1247 * Extracts a host ( not TLD ) from a URL
1248 *
1249 * @param string $url The url of the host.
1250 * @return string $url The host of the url…
1251 */
1252 private function get_host_from_url( $url ) {
1253 if ( preg_match( '/^https?:\/\/( [^\/]+ )\/.*$/', $url, $matches ) ) {
1254 return $matches[1];
1255 }
1256
1257 return $url;
1258 }
1259
1260 /**
1261 * Returns the tags associated with this page or post
1262 *
1263 * @param string $post_id The id of the post you're trying to get tags for.
1264 * @return array $tags The tags of the post represented by the post id.
1265 */
1266 private function get_tags( $post_id ) {
1267 $tags = array();
1268 $wp_tags = wp_get_post_tags( $post_id );
1269 foreach ( $wp_tags as $wp_tag ) {
1270 array_push( $tags, $wp_tag->name );
1271 }
1272
1273 return $tags;
1274 }
1275
1276 /**
1277 * Returns an array of all the child categories for the current post
1278 *
1279 * @param string $post_id The id of the post you're trying to get categories for.
1280 * @param string $delimiter What character will delimit the categories.
1281 * @return array $tags all the child categories of the current post.
1282 */
1283 private function get_categories( $post_id, $delimiter = '/' ) {
1284 $tags = array();
1285 $categories = get_the_category( $post_id );
1286 foreach ( $categories as $category ) {
1287 $hierarchy = get_category_parents( $category, false, $delimiter );
1288 $hierarchy = rtrim( $hierarchy, '/' );
1289 array_push( $tags, $hierarchy );
1290 }
1291 // take last element in the hierarchy, a string representing the full parent->child tree,
1292 // and split it into individual category names.
1293 $tags = explode( '/', end( $tags ) );
1294 // remove uncategorized value from tags.
1295 $tags = array_diff( $tags, array( 'Uncategorized' ) );
1296 return $tags;
1297 }
1298
1299 /**
1300 * Safely returns options for the plugin by assigning defaults contained in optionDefaults. As soon as actual
1301 * options are saved, they override the defaults. This prevents us from having to do a lot of isset() checking
1302 * on variables.
1303 */
1304 private function get_options() {
1305 $options = get_option( self::OPTIONS_KEY );
1306 if ( false === $options ) {
1307 $options = $this->option_defaults;
1308 } else {
1309 $options = array_merge( $this->option_defaults, $options );
1310 }
1311 return $options;
1312 }
1313
1314 /**
1315 * Returns a properly cleaned category/taxonomy value and will optionally use the top-level category/taxonomy value
1316 * if so instructed via the `use_top_level_cats` option.
1317 *
1318 * @param WP_Post $post_obj The object for the post.
1319 * @param array $parsely_options The parsely options.
1320 * @return string $category Cleaned category name for for post in question.
1321 */
1322 private function get_category_name( $post_obj, $parsely_options ) {
1323 $taxonomy_dropdown_choice = get_the_terms( $post_obj->ID, $parsely_options['custom_taxonomy_section'] );
1324 // Get top-level taxonomy name for chosen taxonomy and assign to $parent_name; it will be used
1325 // as the category value if 'use_top_level_cats' option is checked.
1326 // Assign as "Uncategorized" if no value is checked for the chosen taxonomy.
1327 $category = 'Uncategorized';
1328 if ( ! empty( $taxonomy_dropdown_choice ) ) {
1329 if ( $parsely_options['use_top_level_cats'] ) {
1330 $first_term = array_shift( $taxonomy_dropdown_choice );
1331 $term_name = $this->get_top_level_term( $first_term->term_id, $first_term->taxonomy );
1332 } else {
1333 $term_name = $this->get_bottom_level_term( $post_obj->ID, $parsely_options['custom_taxonomy_section'] );
1334 }
1335
1336 if ( $term_name ) {
1337 $category = $term_name;
1338 }
1339 }
1340 $category = apply_filters( 'wp_parsely_post_category', $category, $post_obj, $parsely_options );
1341 $category = $this->get_clean_parsely_page_value( $category );
1342 return $category;
1343 }
1344
1345 /**
1346 * Return the top-most category/taxonomy value in a hierarcy given a taxonomy value's ID
1347 * ( WordPress calls taxonomy values 'terms' ).
1348 *
1349 * @param string $term_id The id of the top level term.
1350 * @param string $taxonomy_name The name of the taxonomy.
1351 * @return string $parent The top level name of the category / taxonomy.
1352 */
1353 private function get_top_level_term( $term_id, $taxonomy_name ) {
1354 $parent = get_term_by( 'id', $term_id, $taxonomy_name );
1355 while ( false !== $parent && 0 !== $parent->parent ) {
1356 $parent = get_term_by( 'id', $parent->parent, $taxonomy_name );
1357 }
1358 return $parent ? $parent->name : false;
1359 }
1360
1361 /**
1362 * Return the bottom-most category/taxonomy value in a hierarcy given a post ID
1363 * ( WordPress calls taxonomy values 'terms' ).
1364 *
1365 * @param string $post_id The post id you're interested in.
1366 * @param string $taxonomy_name The name of the taxonomy.
1367 * @return string name of the custom taxonomy.
1368 */
1369 private function get_bottom_level_term( $post_id, $taxonomy_name ) {
1370 $terms = get_the_terms( $post_id, $taxonomy_name );
1371 $term_ids = is_array( $terms ) ? wp_list_pluck( $terms, 'term_id' ) : null;
1372 $parents = is_array( $terms ) ? array_filter( wp_list_pluck( $terms, 'parent' ) ) : null;
1373
1374 // Get array of IDs of terms which are not parents.
1375 $term_ids_not_parents = array_diff( $term_ids, $parents );
1376 // Get corresponding term objects, which are mapped to array index keys.
1377 $terms_not_parents = array_intersect_key( $terms, $term_ids_not_parents );
1378 // remove array index keys.
1379 $terms_not_parents_cleaned = array();
1380 foreach ( $terms_not_parents as $index => $value ) {
1381 array_push( $terms_not_parents_cleaned, $value );
1382 }
1383 // if you assign multiple child terms in a custom taxonomy, will only return the first.
1384 return $terms_not_parents_cleaned[0]->name;
1385 }
1386
1387 /**
1388 * Get all term values from custom taxonomies.
1389 *
1390 * @param WP_Post $post_obj The post object.
1391 * @param array $parsely_options The pparsely options.
1392 */
1393 private function get_custom_taxonomy_values( $post_obj, $parsely_options ) {
1394 // filter out default WordPress taxonomies.
1395 $all_taxonomies = array_diff( get_taxonomies(), array( 'post_tag', 'nav_menu', 'author', 'link_category', 'post_format' ) );
1396 $all_values = array();
1397
1398 if ( is_array( $all_taxonomies ) ) {
1399 foreach ( $all_taxonomies as $taxonomy ) {
1400 $custom_taxonomy_objects = get_the_terms( $post_obj->ID, $taxonomy );
1401 if ( is_array( $custom_taxonomy_objects ) ) {
1402 foreach ( $custom_taxonomy_objects as $custom_taxonomy_object ) {
1403 array_push( $all_values, $custom_taxonomy_object->name );
1404 }
1405 }
1406 }
1407 }
1408 return $all_values;
1409 }
1410
1411 /**
1412 * Returns a list of coauthors for a post assuming the coauthors plugin is
1413 * installed. Borrowed from
1414 * https://github.com/Automattic/Co-Authors-Plus/blob/master/template-tags.php#L3-35
1415 *
1416 * @param string $post_id The id of the post.
1417 */
1418 private function get_coauthor_names( $post_id ) {
1419 $coauthors = array();
1420 if ( class_exists( 'coauthors_plus' ) ) {
1421 global $post, $post_ID, $coauthors_plus, $wpdb;
1422
1423 $post_id = (int) $post_id;
1424 if ( ! $post_id && $post_ID ) {
1425 $post_id = $post_ID;
1426 }
1427
1428 if ( ! $post_id && $post ) {
1429 $post_id = $post->ID;
1430 }
1431
1432 if ( $post_id ) {
1433 $coauthor_terms = get_the_terms( $post_id, $coauthors_plus->coauthor_taxonomy );
1434
1435 if ( is_array( $coauthor_terms ) && ! empty( $coauthor_terms ) ) {
1436 foreach ( $coauthor_terms as $coauthor ) {
1437 $coauthor_slug = preg_replace( '#^cap\-#', '', $coauthor->slug );
1438 $post_author = $coauthors_plus->get_coauthor_by( 'user_nicename', $coauthor_slug );
1439 // In case the user has been deleted while plugin was deactivated.
1440 if ( ! empty( $post_author ) ) {
1441 $coauthors[] = $post_author;
1442 }
1443 }
1444 } elseif ( ! $coauthors_plus->force_guest_authors ) {
1445 if ( $post && $post_id === $post->ID ) {
1446 $post_author = get_userdata( $post->post_author );
1447 }
1448 if ( ! empty( $post_author ) ) {
1449 $coauthors[] = $post_author;
1450 }
1451 } // the empty else case is because if we force guest authors, we don't ever care what value wp_posts.post_author has.
1452 }
1453 }
1454 return $coauthors;
1455 }
1456
1457 /**
1458 * Determine author name from display name, falling back to firstname
1459 * lastname, then nickname and finally the nicename.
1460 *
1461 * @param WP_User $author The author of the post.
1462 */
1463 private function get_author_name( $author ) {
1464 // gracefully handle situation where no author is available.
1465 if ( empty( $author ) || ! is_object( $author ) ) {
1466 return '';
1467 }
1468 $author_name = $author->display_name;
1469 if ( ! empty( $author_name ) ) {
1470 return $author_name;
1471 }
1472
1473 $author_name = $author->user_firstname . ' ' . $author->user_lastname;
1474 if ( ' ' !== $author_name ) {
1475 return $author_name;
1476 }
1477
1478 $author_name = $author->nickname;
1479 if ( ! empty( $author_name ) ) {
1480 return $author_name;
1481 }
1482
1483 return $author->user_nicename;
1484 }
1485
1486 /**
1487 * Retrieve all the authors for a post as an array. Can include multiple
1488 * authors if coauthors plugin is in use.
1489 *
1490 * @param WP_Post $post The post object.
1491 * @return array
1492 */
1493 private function get_author_names( $post ) {
1494 $authors = $this->get_coauthor_names( $post->ID );
1495 if ( empty( $authors ) ) {
1496 $authors = array( get_user_by( 'id', $post->post_author ) );
1497 }
1498 $authors = apply_filters( 'wp_parsely_pre_authors', $authors, $post );
1499 $authors = array_map( array( $this, 'get_author_name' ), $authors );
1500 $authors = apply_filters( 'wp_parsely_post_authors', $authors, $post );
1501 $authors = array_map( array( $this, 'get_clean_parsely_page_value' ), $authors );
1502 return $authors;
1503 }
1504
1505 /**
1506 * Sanitize content
1507 *
1508 * @param string $val The content you'd like sanitized.
1509 * @return string
1510 */
1511 private function get_clean_parsely_page_value( $val ) {
1512 if ( is_string( $val ) ) {
1513 $val = str_replace( "\n", '', $val );
1514 $val = str_replace( "\r", '', $val );
1515 $val = wp_strip_all_tags( $val );
1516 $val = trim( $val );
1517 return $val;
1518 }
1519
1520 return $val;
1521 }
1522
1523
1524 /**
1525 * Get the URL of the plugin settings page
1526 */
1527 private function get_settings_url() {
1528 return admin_url( 'options-general.php?page=' . self::MENU_SLUG );
1529 }
1530
1531
1532 /**
1533 * Get the URL of the current PHP script.
1534 * A fall-back implementation to determine permalink
1535 *
1536 * @param string $post The post object you're interested in.
1537 * @param int $post_id id of the post you want to get the url for. Optional.
1538 * @return string|void
1539 */
1540 private function get_current_url( $post = 'nonpost', $post_id = 0 ) {
1541 $options = $this->get_options();
1542 $scheme = ( $options['force_https_canonicals'] ? 'https://' : 'http://' );
1543
1544 if ( 'post' === $post ) {
1545 $permalink = get_permalink( $post_id );
1546 $permalink = apply_filters( 'wp_parsely_permalink', $permalink, $post );
1547 $parsed_canonical = wp_parse_url( $permalink );
1548 // handle issue if wp_parse_url doesn't return good host & path data, fallback to page url as a last resort.
1549 if ( isset( $parsed_canonical['host'], $parsed_canonical['path'] ) ) {
1550 $canonical = $scheme . $parsed_canonical['host'] . $parsed_canonical['path'];
1551 } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { // Input var okay.
1552 $canonical = $scheme . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // Input var okay.
1553 }
1554
1555 return $canonical;
1556 }
1557 $page_url = site_url( null, $scheme );
1558
1559 if ( isset( $_SERVER['SERVER_PORT'] ) ) { // Input var okay.
1560 $port_number = intval( $_SERVER['SERVER_PORT'] ); // Input var okay.
1561 }
1562 if ( 80 !== $port_number && 443 !== $port_number ) {
1563 $page_url .= ':' . $port_number;
1564 }
1565 if ( isset( $_SERVER['REQUEST_URI'] ) ) { // Input var okay.
1566 $page_url .= sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // Input var okay.
1567 }
1568 return $page_url;
1569 }
1570
1571 /**
1572 * Get the first image from a post
1573 * https://css-tricks.com/snippets/wordpress/get-the-first-image-from-a-post/
1574 *
1575 * @param WP_Post $post The post object you're interested in.
1576 * @return mixed|string
1577 */
1578 public function get_first_image( $post ) {
1579 ob_start();
1580 ob_end_clean();
1581 if ( preg_match_all( '/<img.+src=[\'"]( [^\'"]+ )[\'"].*>/i', $post->post_content, $matches ) ) {
1582 return $matches[1][0];
1583 }
1584 return '';
1585 }
1586
1587 /**
1588 * Add parsely tracking to facebook instant articles
1589 *
1590 * @param type $registry The registry info for fbia.
1591 * @return string
1592 */
1593 public function insert_parsely_tracking_fbia( &$registry ) {
1594 $options = $this->get_options();
1595 $display_name = 'Parsely Analytics';
1596 $identifier = 'parsely-analytics-for-wordpress';
1597
1598 $embed_code = '<script>
1599 PARSELY = {
1600 autotrack: false,
1601 onload: function() {
1602 PARSELY.beacon.trackPageView({
1603 urlref: \'http://facebook.com/instantarticles\'
1604 });
1605 return true;
1606 }
1607 }
1608 </script>
1609 <script data-cfasync="false" id="parsely-cfg" data-parsely-site="' . esc_attr( $options['apikey'] ) . '" src="//cdn.parsely.com/keys/' . esc_attr( $options['apikey'] ) . '/p.js"></script>
1610 <!-- END Parse.ly Include: Standard -->';
1611
1612 $registry[ $identifier ] = array(
1613 'name' => $display_name,
1614 'payload' => $embed_code,
1615 );
1616
1617 return $embed_code;
1618 }
1619
1620 /**
1621 * Add amp actions.
1622 */
1623 public function parsely_add_amp_actions() {
1624 if ( ! function_exists( 'is_amp_endpoint' ) || ! is_amp_endpoint() ) {
1625 return '';
1626 }
1627
1628 $options = $this->get_options();
1629
1630 if ( $options['disable_amp'] ) {
1631 return '';
1632 }
1633
1634 add_filter( 'amp_post_template_analytics', array( $this, 'parsely_add_amp_analytics' ) );
1635 add_filter( 'amp_analytics_entries', array( $this, 'parsely_add_amp_native_analytics' ) );
1636 }
1637
1638 /**
1639 * Add amp analytics.
1640 *
1641 * @param type $analytics The analytics object you want to add.
1642 * @return type
1643 */
1644 public function parsely_add_amp_analytics( $analytics ) {
1645 $options = $this->get_options();
1646
1647 if ( empty( $options['apikey'] ) ) {
1648 return $analytics;
1649 }
1650
1651 $analytics['parsely'] = array(
1652 'type' => 'parsely',
1653 'attributes' => array(),
1654 'config_data' => array(
1655 'vars' => array(
1656 'apikey' => $options['apikey'],
1657 ),
1658 ),
1659 );
1660
1661 return $analytics;
1662 }
1663
1664 /**
1665 * Add amp native analytics.
1666 *
1667 * @param type $analytics The analytics object you want to add.
1668 * @return string|type
1669 */
1670 public function parsely_add_amp_native_analytics( $analytics ) {
1671 $options = $this->get_options();
1672
1673 if ( ! empty( $options['disable_amp'] ) && true === $options['disable_amp'] ) {
1674 return '';
1675 }
1676
1677 if ( empty( $options['apikey'] ) ) {
1678 return $analytics;
1679 }
1680
1681 $analytics['parsely'] = array(
1682 'type' => 'parsely',
1683 'attributes' => array(),
1684 'config' => wp_json_encode(
1685 array(
1686 'vars' => array(
1687 'apikey' => $options['apikey'],
1688 ),
1689 )
1690 ),
1691 );
1692
1693 return $analytics;
1694 }
1695
1696 /**
1697 * Check to see if parsely user is logged in
1698 */
1699 public function parsely_is_user_logged_in() {
1700 // can't use $blog_id here because it futzes with the global $blog_id.
1701 $current_blog_id = get_current_blog_id();
1702 $current_user_id = get_current_user_id();
1703 return is_user_member_of_blog( $current_user_id, $current_blog_id );
1704 }
1705
1706 /**
1707 * Why is this here?
1708 */
1709 public function return_personalized_json() {
1710
1711 }
1712 }
1713
1714
1715
1716
1717 if ( class_exists( 'Parsely' ) ) {
1718 define( 'PARSELY_VERSION', Parsely::VERSION );
1719 $parsely = new Parsely();
1720 }
1721
1722 require 'class-parsely-recommended-widget.php';
1723