PluginProbe
Parse.ly / 2.5.1
Parse.ly v2.5.1
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
← All changes | src/class-parsely.php +1645 -862 3.23.62.5.1 View file →
@@ -2,183 +2,64 @@
2 2 /**
3 3 * Parsely class
4 4 *
5 5 * @package Parsely
6 - * @since 2.5.0
6 + * @since 2.5.0
7 7 */
8 8
9 -declare(strict_types=1);
10 -
11 -namespace Parsely;
12 -
13 -use Parsely\REST_API\REST_API_Controller;
14 -use Parsely\Services\Content_API\Content_API_Service;
15 -use Parsely\Services\Suggestions_API\Suggestions_API_Service;
16 -use Parsely\UI\Metadata_Renderer;
17 -use Parsely\UI\Settings_Page;
18 -use Parsely\Utils\Utils;
19 -use WP_Post;
20 -
21 9 /**
22 10 * Holds most of the logic for the plugin.
23 11 *
12 + * @internal This needs splitting up in the future.
13 + *
24 14 * @since 1.0.0
25 15 * @since 2.5.0 Moved from plugin root file to this file.
26 - *
27 - * @phpstan-type Parsely_Options array{
28 - * apikey: string,
29 - * content_id_prefix: string,
30 - * api_secret: string,
31 - * use_top_level_cats: bool,
32 - * custom_taxonomy_section: string,
33 - * cats_as_tags: bool,
34 - * content_helper: Parsely_Options_Content_Helper,
35 - * headline_testing: Parsely_Options_Headline_Testing,
36 - * track_authenticated_users: bool,
37 - * lowercase_tags: bool,
38 - * force_https_canonicals: bool,
39 - * track_post_types: string[],
40 - * track_page_types: string[],
41 - * track_post_types_as?: array<string, string>,
42 - * full_metadata_in_non_posts: bool,
43 - * disable_javascript: bool,
44 - * disable_amp: bool,
45 - * meta_type: string,
46 - * logo: string,
47 - * metadata_secret: string,
48 - * disable_autotrack: bool,
49 - * plugin_version: string,
50 - * }
51 - *
52 - * @phpstan-type Parsely_Options_Content_Helper array{
53 - * ai_features_enabled: bool,
54 - * smart_linking: Parsely_Options_Content_Helper_Feature,
55 - * title_suggestions: Parsely_Options_Content_Helper_Feature,
56 - * excerpt_suggestions: Parsely_Options_Content_Helper_Feature,
57 - * traffic_boost: Parsely_Options_Content_Helper_Feature,
58 - * }
59 - *
60 - * @phpstan-type Parsely_Options_Content_Helper_Feature array{
61 - * enabled: bool,
62 - * allowed_user_roles: string[],
63 - * }
64 - *
65 - * @phpstan-type Parsely_Options_Headline_Testing array{
66 - * enabled: bool,
67 - * installation_method: string,
68 - * enable_flicker_control: bool,
69 - * enable_live_updates: bool,
70 - * live_update_timeout: int,
71 - * allow_after_content_load: bool,
72 - * }
73 - *
74 - * @phpstan-type WP_HTTP_Request_Args array{
75 - * method?: string,
76 - * timeout?: float,
77 - * blocking?: bool,
78 - * headers?: array<string, string>,
79 - * body?: string,
80 - * data_format?: string,
81 - * }
82 - *
83 - * @phpstan-import-type Metadata_Attributes from Metadata
84 16 */
85 17 class Parsely {
86 18 /**
87 19 * Declare our constants
88 - */
89 - public const VERSION = PARSELY_VERSION;
90 - public const MENU_SLUG = 'parsely-settings'; // The page param passed to admin.php.
91 - public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
92 - public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
93 - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
94 -
95 - private const PARSELY_CANONICAL_URL_META_KEY = '_parsely_canonical_url';
96 -
97 - /**
98 - * The Content API service.
99 20 *
100 - * @var ?Content_API_Service $content_api_service
21 + * @codeCoverageIgnoreStart
101 22 */
102 - private $content_api_service;
23 + const VERSION = PARSELY_VERSION;
24 + const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php.
25 + const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database.
26 + const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings.
103 27
104 28 /**
105 - * The Suggestions API service.
29 + * Declare some class propeties
106 30 *
107 - * @var ?Suggestions_API_Service $suggestions_api_service
31 + * @var array $option_defaults The defaults we need for the class.
108 32 */
109 - private $suggestions_api_service;
110 -
111 - /**
112 - * The Parse.ly internal REST API controller.
113 - *
114 - * @var REST_API_Controller|null $rest_api_controller
115 - */
116 - private $rest_api_controller;
117 -
118 - /**
119 - * Declare some class properties
120 - *
121 - * @var Parsely_Options $option_defaults The defaults we need for the class.
122 - */
123 33 private $option_defaults = array(
124 - 'apikey' => '',
125 - 'content_id_prefix' => '',
126 - 'api_secret' => '',
127 - 'use_top_level_cats' => false,
128 - 'custom_taxonomy_section' => 'category',
129 - 'cats_as_tags' => false,
130 - 'content_helper' => array(
131 - 'ai_features_enabled' => true,
132 - 'smart_linking' => array(
133 - 'enabled' => true,
134 - 'allowed_user_roles' => array( 'administrator' ),
135 - ),
136 - 'title_suggestions' => array(
137 - 'enabled' => true,
138 - 'allowed_user_roles' => array( 'administrator' ),
139 - ),
140 - 'excerpt_suggestions' => array(
141 - 'enabled' => true,
142 - 'allowed_user_roles' => array( 'administrator' ),
143 - ),
144 - 'traffic_boost' => array(
145 - 'enabled' => true,
146 - 'allowed_user_roles' => array( 'administrator' ),
147 - ),
148 - ),
149 - 'headline_testing' => array(
150 - 'enabled' => false,
151 - 'installation_method' => 'one_line',
152 - 'enable_flicker_control' => false,
153 - 'enable_live_updates' => false,
154 - 'live_update_timeout' => 30000,
155 - 'allow_after_content_load' => false,
156 - ),
157 - 'track_authenticated_users' => false,
158 - 'lowercase_tags' => true,
159 - 'force_https_canonicals' => false,
160 - 'track_post_types' => array(),
161 - 'track_page_types' => array(),
162 - 'full_metadata_in_non_posts' => true,
163 - 'disable_javascript' => false,
164 - 'disable_amp' => false,
165 - 'meta_type' => 'json_ld',
166 - 'logo' => '',
167 - 'metadata_secret' => '',
168 - 'disable_autotrack' => false,
169 - 'plugin_version' => self::VERSION,
34 + 'apikey' => '',
35 + 'content_id_prefix' => '',
36 + 'api_secret' => '',
37 + 'use_top_level_cats' => false,
38 + 'custom_taxonomy_section' => 'category',
39 + 'cats_as_tags' => false,
40 + 'track_authenticated_users' => true,
41 + 'lowercase_tags' => true,
42 + 'force_https_canonicals' => false,
43 + 'track_post_types' => array( 'post' ),
44 + 'track_page_types' => array( 'page' ),
45 + 'disable_javascript' => false,
46 + 'disable_amp' => false,
47 + 'meta_type' => 'json_ld',
48 + 'logo' => '',
49 + 'metadata_secret' => '',
50 + 'parsely_wipe_metadata_cache' => false,
170 51 );
171 52
172 53 /**
173 54 * Declare post types that Parse.ly will process as "posts".
174 55 *
56 + * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
57 + *
175 58 * @since 2.5.0
176 59 * @var string[]
177 - *
178 - * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
179 60 */
180 - public const SUPPORTED_JSONLD_POST_TYPES = array(
61 + private $supported_jsonld_post_types = array(
181 62 'NewsArticle',
182 63 'Article',
183 64 'TechArticle',
184 65 'BlogPosting',
@@ -185,28 +66,19 @@
185 66 'LiveBlogPosting',
186 67 'Report',
187 68 'Review',
188 69 'CreativeWork',
189 - 'OpinionNewsArticle',
190 - 'AnalysisNewsArticle',
191 - 'BackgroundNewsArticle',
192 - 'ReviewNewsArticle',
193 - 'ReportageNewsArticle',
194 - 'Recipe',
195 - 'AdvertiserContentArticle',
196 - 'MedicalWebPage',
197 - 'PodcastEpisode',
198 70 );
199 71
200 72 /**
201 73 * Declare post types that Parse.ly will process as "non-posts".
202 74 *
75 + * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
76 + *
203 77 * @since 2.5.0
204 78 * @var string[]
205 - *
206 - * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
207 79 */
208 - public const SUPPORTED_JSONLD_NON_POST_TYPES = array(
80 + private $supported_jsonld_non_post_types = array(
209 81 'WebPage',
210 82 'Event',
211 83 'Hotel',
212 84 'Restaurant',
@@ -213,1009 +85,1920 @@
213 85 'Movie',
214 86 );
215 87
216 88 /**
217 - * Declare all supported types (both post and non-post types).
89 + * The constructor
218 90 *
219 - * @since 3.7.0
220 - * @var string[]
91 + * @category Function
92 + * @package Parsely
221 93 */
222 - private static $all_supported_types;
94 + public function __construct() {
95 + // Run upgrade options if they exist for the version currently defined.
96 + $options = $this->get_options();
97 + if ( empty( $options['plugin_version'] ) || self::VERSION !== $options['plugin_version'] ) {
98 + $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
99 + if ( method_exists( $this, $method ) ) {
100 + call_user_func_array( array( $this, $method ), array( $options ) );
101 + }
102 + // Update our version info.
103 + $options['plugin_version'] = self::VERSION;
104 + update_option( self::OPTIONS_KEY, $options );
105 + }
223 106
107 + // admin_menu and a settings link.
108 + add_action( 'admin_head-settings_page_parsely', array( $this, 'add_admin_header' ) );
109 + add_action( 'admin_menu', array( $this, 'add_settings_sub_menu' ) );
110 + add_action( 'admin_init', array( $this, 'initialize_settings' ) );
111 + // display warning when plugin hasn't been configured.
112 + add_action( 'admin_footer', array( $this, 'display_admin_warning' ) );
113 +
114 + add_filter(
115 + 'plugin_action_links_' . PARSELY_PLUGIN_BASENAME,
116 + array( $this, 'add_plugin_meta_links' )
117 + );
118 +
119 + add_filter( 'cron_schedules', [ $this, 'wpparsely_add_cron_interval' ] );
120 + add_action( 'parsely_bulk_metas_update', array( $this, 'bulk_update_posts' ) );
121 + // inserting parsely code.
122 + add_action( 'wp_head', array( $this, 'insert_parsely_page' ) );
123 + add_action( 'init', array( $this, 'register_js' ) );
124 +
125 + // load_js_api should be called prior to load_js_tracker so the relevant scripts are enqueued in order.
126 + add_action( 'wp_footer', array( $this, 'load_js_api' ) );
127 + add_action( 'wp_footer', array( $this, 'load_js_tracker' ) );
128 +
129 + add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
130 + add_action( 'instant_articles_compat_registry_analytics', array( $this, 'insert_parsely_tracking_fbia' ) );
131 + add_action( 'template_redirect', array( $this, 'parsely_add_amp_actions' ) );
132 + add_action( 'wp_enqueue_scripts', [ $this, 'wp_parsely_style_init' ] );
133 + }
134 +
224 135 /**
225 - * Returns whether credentials are being managed at the platform level.
136 + * Adds 10 minute cron interval
226 137 *
227 - * This allows hosting providers to provide a more customized experience for
228 - * the plugin by handling credentials automatically.
138 + * @param array $schedules WP schedules array.
139 + */
140 + public function wpparsely_add_cron_interval( $schedules ) {
141 + $schedules['everytenminutes'] = array(
142 + 'interval' => 600, // time in seconds.
143 + 'display' => __( 'Every 10 Minutes', 'wp-parsely' ),
144 + );
145 + return $schedules;
146 + }
147 +
148 + /**
149 + * Initialize parsely WordPress style
150 + */
151 + public function wp_parsely_style_init() {
152 + wp_register_style( 'wp-parsely-style', PARSELY_PLUGIN_URL . 'wp-parsely.css', array(), Parsely::VERSION );
153 + }
154 +
155 + /**
156 + * Include the parsely admin header
229 157 *
230 - * @since 3.9.0
231 - * @access private
232 - * @var bool
158 + * @category Function
159 + * @package Parsely
233 160 */
234 - public $are_credentials_managed;
161 + public function add_admin_header() {
162 + echo '
163 +<style>
164 +#wp-parsely_version { color: #777; font-size: 12px; margin-left: 1em; }
165 +.help-text { width: 75%; }
166 +</style>
167 +';
235 168
169 + $admin_script_asset = require PARSELY_PLUGIN_DIR . 'build/admin-page.asset.php';
170 + wp_enqueue_script(
171 + 'wp-parsely-admin',
172 + PARSELY_PLUGIN_URL . 'build/admin-page.js',
173 + $admin_script_asset[ 'dependencies' ],
174 + self::get_asset_cache_buster(),
175 + true
176 + );
177 + }
178 +
236 179 /**
237 - * Holds the managed options and their values.
180 + * Parsely settings page in WordPress settings menu.
238 181 *
239 - * This allows hosting providers to provide a more customized experience for
240 - * the plugin by handling options automatically.
182 + * @category Function
183 + * @package Parsely
184 + */
185 + public function add_settings_sub_menu() {
186 + add_options_page(
187 + __( 'Parse.ly Settings', 'wp-parsely' ),
188 + __( 'Parse.ly', 'wp-parsely' ),
189 + self::CAPABILITY,
190 + self::MENU_SLUG,
191 + array( $this, 'display_settings' )
192 + );
193 + }
194 +
195 + /**
196 + * Parse.ly settings screen ( options-general.php?page=[MENU_SLUG] )
241 197 *
242 - * @since 3.9.0
243 - * @access private
244 - * @var array<empty>|array<string, bool|string|null>
198 + * @category Function
199 + * @package Parsely
245 200 */
246 - public $managed_options = array();
201 + public function display_settings() {
202 + if ( ! current_user_can( self::CAPABILITY ) ) {
203 + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-parsely' ) );
204 + }
247 205
206 + include PARSELY_PLUGIN_DIR . 'src/parsely-settings.php';
207 + }
208 +
248 209 /**
249 - * Constructor.
210 + * Initialize the settings for Parsely
211 + *
212 + * @category Function
213 + * @package Parsely
250 214 */
251 - public function __construct() {
252 - self::$all_supported_types = array_merge( self::SUPPORTED_JSONLD_POST_TYPES, self::SUPPORTED_JSONLD_NON_POST_TYPES );
215 + public function initialize_settings() {
216 + // All our options are actually stored in one single array to reduce
217 + // DB queries.
218 + register_setting(
219 + self::OPTIONS_KEY,
220 + self::OPTIONS_KEY,
221 + array( $this, 'validate_options' )
222 + );
253 223
254 - $this->are_credentials_managed = $this->are_credentials_managed();
255 - $this->set_managed_options();
224 + // These are the Required Settings.
225 + add_settings_section(
226 + 'required_settings',
227 + __( 'Required Settings', 'wp-parsely' ),
228 + array( $this, 'print_required_settings' ),
229 + self::MENU_SLUG
230 + );
256 231
257 - $this->allow_parsely_remote_requests();
232 + // Get the API Key.
233 + $h = __( 'Your Site ID is your own site domain ( e.g. `mydomain.com` )', 'wp-parsely' );
234 +
235 + $field_args = array(
236 + 'option_key' => 'apikey',
237 + 'help_text' => $h,
238 + );
239 + add_settings_field(
240 + 'apikey',
241 + __( 'Parse.ly Site ID', 'wp-parsely' ),
242 + array( $this, 'print_text_tag' ),
243 + self::MENU_SLUG,
244 + 'required_settings',
245 + $field_args
246 + );
247 +
248 + // These are the Optional Settings.
249 + add_settings_section(
250 + 'optional_settings',
251 + __( 'Optional Settings', 'wp-parsely' ),
252 + array( $this, 'print_optional_settings' ),
253 + self::MENU_SLUG
254 + );
255 +
256 + /* translators: 1: Opening anchor tag markup, 2: Documentation URL, 3: Opening anchor tag markup continued, 4: Closing anchor tag */
257 + $h = __( 'Your API secret is your secret code to %1$s%2$s%3$saccess our API.%4$s It can be found at dash.parsely.com/yoursitedomain/settings/api ( replace yoursitedomain with your domain name, e.g. `mydomain.com` ) If you haven\'t purchased access to the API, and would like to do so, email your account manager or support@parsely.com!', 'wp-parsely' );
258 + $h_link = 'https://www.parse.ly/help/api/analytics/';
259 +
260 + $field_args = array(
261 + 'option_key' => 'api_secret',
262 + 'help_text' => $h,
263 + 'help_link' => $h_link,
264 + );
265 + add_settings_field(
266 + 'api_secret',
267 + __( 'Parse.ly API Secret', 'wp-parsely' ),
268 + array( $this, 'print_text_tag' ),
269 + self::MENU_SLUG,
270 + 'optional_settings',
271 + $field_args
272 + );
273 +
274 + $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!', 'wp-parsely' );
275 + $h_link = 'https://www.parse.ly/help/api/analytics/';
276 +
277 + $field_args = array(
278 + 'option_key' => 'metadata_secret',
279 + 'help_text' => $h,
280 + 'help_link' => $h_link,
281 + );
282 + add_settings_field(
283 + 'metadata_secret',
284 + __( 'Parse.ly Metadata Secret', 'wp-parsely' ),
285 + array( $this, 'print_text_tag' ),
286 + self::MENU_SLUG,
287 + 'optional_settings',
288 + $field_args
289 + );
290 +
291 + // Clear metadata.
292 + $h = __( 'Check this radio button and hit "Save Changes" to clear all metadata information for Parse.ly posts and re-send all metadata to Parse.ly. WARNING: do not do this unless explicitly instructed by Parse.ly Staff!', 'wp-parsely' );
293 + add_settings_field(
294 + 'parsely_wipe_metadata_cache',
295 + __( 'Wipe Parse.ly Metadata Info', 'wp-parsely' ),
296 + array( $this, 'print_checkbox_tag' ),
297 + self::MENU_SLUG,
298 + 'optional_settings',
299 + array(
300 + 'option_key' => 'parsely_wipe_metadata_cache',
301 + 'help_text' => $h,
302 + 'requires_recrawl' => false,
303 + )
304 + );
305 +
306 + /* translators: 1: Opening anchor tag markup, 2: Documentation URL, 3: Opening anchor tag markup continued, 4: Closing anchor tag */
307 + $h = __( 'Choose the metadata format for our crawlers to access. Most publishers are fine with JSON-LD ( %1$s%2$s%3$shttps://www.parse.ly/help/integration/jsonld/%4$s ), but if you prefer to use our proprietary metadata format then you can do so here.', 'wp-parsely' );
308 + $h_link = 'https://www.parse.ly/help/integration/jsonld/';
309 +
310 + add_settings_field(
311 + 'meta_type',
312 + __( 'Metadata Format', 'wp-parsely' ),
313 + array( $this, 'print_select_tag' ),
314 + self::MENU_SLUG,
315 + 'optional_settings',
316 + array(
317 + 'option_key' => 'meta_type',
318 + 'help_text' => $h,
319 + 'help_link' => $h_link,
320 + // filter WordPress taxonomies under the hood that should not appear in dropdown.
321 + 'select_options' => array(
322 + 'json_ld' => 'json_ld',
323 + 'repeated_metas' => 'repeated_metas',
324 + ),
325 + 'requires_recrawl' => true,
326 + 'multiple' => false,
327 + )
328 + );
329 +
330 + $h = __( 'If you want to specify the url for your logo, you can do so here.', 'wp-parsely' );
331 +
332 + $option_defaults['logo'] = $this->get_logo_default();
333 +
334 + $field_args = array(
335 + 'option_key' => 'logo',
336 + 'help_text' => $h,
337 + );
338 +
339 + add_settings_field(
340 + 'logo',
341 + __( 'Logo', 'wp-parsely' ),
342 + array( $this, 'print_text_tag' ),
343 + self::MENU_SLUG,
344 + 'optional_settings',
345 + $field_args
346 + );
347 +
348 + // Content ID Prefix.
349 + $h = __( 'If you use more than one content management system (e.g. WordPress and Drupal), you may end up with duplicate content IDs. Adding a Content ID Prefix will ensure the content IDs from WordPress will not conflict with other content management systems. We recommend using "WP-" for your prefix.', 'wp-parsely' );
350 +
351 + $field_args = array(
352 + 'option_key' => 'content_id_prefix',
353 + 'optional_args' => array(
354 + 'placeholder' => 'WP-',
355 + ),
356 + 'help_text' => $h,
357 + 'requires_recrawl' => true,
358 + );
359 + add_settings_field(
360 + 'content_id_prefix',
361 + __( 'Content ID Prefix', 'wp-parsely' ),
362 + array( $this, 'print_text_tag' ),
363 + self::MENU_SLUG,
364 + 'optional_settings',
365 + $field_args
366 + );
367 +
368 + // Disable JavaScript.
369 + $h = __( 'If you use a separate system for JavaScript tracking ( Tealium / Segment / Google Tag Manager / other tag manager solution ) you may want to use that instead of having the plugin load the tracker. WARNING: disabling this option will also disable the "Personalize Results" section of the recommended widget! We highly recommend leaving this option set to "No"!', 'wp-parsely' );
370 + add_settings_field(
371 + 'disable_javascript',
372 + __( 'Disable JavaScript', 'wp-parsely' ),
373 + array( $this, 'print_binary_radio_tag' ),
374 + self::MENU_SLUG,
375 + 'optional_settings',
376 + array(
377 + 'title' => __( 'Disable JavaScript', 'wp-parsely' ), // Passed for legend element.
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 ) you may want to use that instead of having the plugin load the tracker.', 'wp-parsely' );
386 + add_settings_field(
387 + 'disable_amp',
388 + __( 'Disable AMP Tracking', 'wp-parsely' ),
389 + array( $this, 'print_binary_radio_tag' ),
390 + self::MENU_SLUG,
391 + 'optional_settings',
392 + array(
393 + 'title' => __( 'Disable AMP Tracking', 'wp-parsely' ), // Passed for legend element.
394 + 'option_key' => 'disable_amp',
395 + 'help_text' => $h,
396 + 'requires_recrawl' => false,
397 + )
398 + );
399 +
400 + // Use top-level categories.
401 + $h = __( 'The plugin will use the first category assigned to a post. With this option selected, if you post a story to News > National > Florida, the plugin will use the "News" for the section name in your dashboard instead of "Florida".', 'wp-parsely' );
402 + add_settings_field(
403 + 'use_top_level_cats',
404 + __( 'Use Top-Level Categories for Section', 'wp-parsely' ),
405 + array( $this, 'print_binary_radio_tag' ),
406 + self::MENU_SLUG,
407 + 'optional_settings',
408 + array(
409 + 'title' => __( 'Use Top-Level Categories for Section', 'wp-parsely' ), // Passed for legend element.
410 + 'option_key' => 'use_top_level_cats',
411 + 'help_text' => $h,
412 + 'requires_recrawl' => true,
413 + )
414 + );
415 +
416 + // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
417 + $h = __( 'By default, the section value in your Parse.ly dashboard maps to a post\'s category. You can optionally choose a custom taxonomy, if you\'ve created one, to populate the section value instead.', 'wp-parsely' );
418 + add_settings_field(
419 + 'custom_taxonomy_section',
420 + __( 'Use Custom Taxonomy for Section', 'wp-parsely' ),
421 + array( $this, 'print_select_tag' ),
422 + self::MENU_SLUG,
423 + 'optional_settings',
424 + array(
425 + 'option_key' => 'custom_taxonomy_section',
426 + 'help_text' => $h,
427 + // filter WordPress taxonomies under the hood that should not appear in dropdown.
428 + 'select_options' => array_diff( get_taxonomies(), array( 'post_tag', 'nav_menu', 'author', 'link_category', 'post_format' ) ),
429 + 'requires_recrawl' => true,
430 + )
431 + );
432 +
433 + // Use categories and custom taxonomies as tags.
434 + $h = __( 'You can use this option to add all assigned categories and taxonomies to your tags. For example, if you had a post assigned to the categories: "Business/Tech", "Business/Social", your tags would include "Business/Tech" and "Business/Social" in addition to your other tags.', 'wp-parsely' );
435 + add_settings_field(
436 + 'cats_as_tags',
437 + __( 'Add Categories to Tags', 'wp-parsely' ),
438 + array( $this, 'print_binary_radio_tag' ),
439 + self::MENU_SLUG,
440 + 'optional_settings',
441 + array(
442 + 'title' => __( 'Add Categories to Tags', 'wp-parsely' ), // Passed for legend element.
443 + 'option_key' => 'cats_as_tags',
444 + 'help_text' => $h,
445 + 'requires_recrawl' => true,
446 + )
447 + );
448 +
449 + // Track logged-in users.
450 + $h = __( 'By default, the plugin will track the activity of users that are logged into this site. You can change this setting to only track the activity of anonymous visitors. Note: You will no longer see the Parse.ly tracking code on your site if you browse while logged in.', 'wp-parsely' );
451 + add_settings_field(
452 + 'track_authenticated_users',
453 + __( 'Track Logged-in Users', 'wp-parsely' ),
454 + array( $this, 'print_binary_radio_tag' ),
455 + self::MENU_SLUG,
456 + 'optional_settings',
457 + array(
458 + 'title' => __( 'Track Logged-in Users', 'wp-parsely' ), // Passed for legend element.
459 + 'option_key' => 'track_authenticated_users',
460 + 'help_text' => $h,
461 + 'requires_recrawl' => false,
462 + )
463 + );
464 +
465 + // Lowercase all tags.
466 + $h = __( 'By default, the plugin will use lowercase versions of your tags to correct for potential misspellings. You can change this setting to ensure that tag names are used verbatim.', 'wp-parsely' );
467 + add_settings_field(
468 + 'lowercase_tags',
469 + __( 'Lowercase All Tags', 'wp-parsely' ),
470 + array( $this, 'print_binary_radio_tag' ),
471 + self::MENU_SLUG,
472 + 'optional_settings',
473 + array(
474 + 'title' => __( 'Lowercase All Tags', 'wp-parsely' ), // Passed for legend element.
475 + 'option_key' => 'lowercase_tags',
476 + 'help_text' => $h,
477 + 'requires_recrawl' => true,
478 + )
479 + );
480 +
481 + $h = __( 'The plugin uses http canonical URLs by default. If this needs to be forced to use https, set this option to true. Note: the default is fine for almost all publishers, it\'s unlikely you\'ll have to change this unless directed to do so by a Parse.ly support rep.', 'wp-parsely' );
482 + add_settings_field(
483 + 'force_https_canonicals',
484 + __( 'Force HTTPS canonicals', 'wp-parsely' ),
485 + array( $this, 'print_binary_radio_tag' ),
486 + self::MENU_SLUG,
487 + 'optional_settings',
488 + array(
489 + 'title' => __( 'Force HTTPS canonicals', 'wp-parsely' ), // Passed for legend element.
490 + 'option_key' => 'force_https_canonicals',
491 + 'help_text' => $h,
492 + 'requires_recrawl' => true,
493 + )
494 + );
495 +
496 + // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
497 + $h = __( 'By default, Parse.ly only tracks the default post type as a post page. If you want to track custom post types, select them here!', 'wp-parsely' );
498 + add_settings_field(
499 + 'track_post_types',
500 + __( 'Post Types To Track', 'wp-parsely' ),
501 + array( $this, 'print_select_tag' ),
502 + self::MENU_SLUG,
503 + 'optional_settings',
504 + array(
505 + 'option_key' => 'track_post_types',
506 + 'help_text' => $h,
507 + // filter WordPress taxonomies under the hood that should not appear in dropdown.
508 + 'select_options' => get_post_types(),
509 + 'requires_recrawl' => true,
510 + 'multiple' => true,
511 + )
512 + );
513 +
514 + // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
515 + $h = __( 'By default, Parse.ly only tracks the default page type as a non-post page. If you want to track custom post types as non-post pages, select them here!', 'wp-parsely' );
516 + add_settings_field(
517 + 'track_page_types',
518 + __( 'Page Types To Track', 'wp-parsely' ),
519 + array( $this, 'print_select_tag' ),
520 + self::MENU_SLUG,
521 + 'optional_settings',
522 + array(
523 + 'option_key' => 'track_page_types',
524 + 'help_text' => $h,
525 + // filter WordPress taxonomies under the hood that should not appear in dropdown.
526 + 'select_options' => get_post_types(),
527 + 'requires_recrawl' => true,
528 + 'multiple' => true,
529 + )
530 + );
531 +
532 + // Dynamic tracking note.
533 + add_settings_field(
534 + 'dynamic_tracking_note',
535 + __( 'Note: ', 'wp-parsely' ),
536 + array( $this, 'print_dynamic_tracking_note' ),
537 + self::MENU_SLUG,
538 + 'optional_settings'
539 + );
258 540 }
259 541
260 542 /**
261 - * Gets the allowed post statuses for tracking.
543 + * Validate options from an array
262 544 *
263 - * Uses the `wp_parsely_trackable_statuses` filter to determine which post statuses are allowed to be tracked.
264 - *
265 - * @since 3.17.0
266 - *
267 - * @param WP_Post|int|null $post The post object.
268 - * @return array<string> The allowed post statuses.
545 + * @category Function
546 + * @package Parsely
547 + * @param array $array Array of options to be sanitized.
548 + * @param string $name Unused?.
269 549 */
270 - public static function get_trackable_statuses( $post = null ): array {
271 - /**
272 - * Filters the statuses that are permitted to be tracked.
273 - *
274 - * By default, the only status tracked is 'publish'. Use this filter if
275 - * you have other published content that has a different (custom) status.
276 - *
277 - * @since 2.5.0
278 - * @since 3.17.0 Filter extracted to a separate method.
279 - *
280 - * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
281 - * @param WP_Post|int|null $post Which post object or ID is being checked.
282 - */
283 - return apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
550 + public function validate_option_array( $array, $name ) {
551 + $new_array = $array;
552 + foreach ( $array as $key => $val ) {
553 + $new_array[ $key ] = sanitize_text_field( $val );
554 + }
555 + return $new_array;
284 556 }
285 557
286 558 /**
287 - * Registers action and filter hook callbacks, and immediately upgrades
288 - * options if needed.
559 + * Validate the options provided by the user
560 + *
561 + * @category Function
562 + * @package Parsely
563 + * @param array $input Options from the settings page.
564 + * @return array $input list of validated input settings.
289 565 */
290 - public function run(): void {
291 - // Run upgrade options if they exist for the version currently defined.
292 - $options = $this->get_options();
293 - if ( self::VERSION !== $options['plugin_version'] ) {
294 - $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
295 - if ( method_exists( $this, $method ) ) {
296 - /**
297 - * Variable.
298 - *
299 - * @var callable
300 - */
301 - $callable = array( $this, $method );
302 - call_user_func_array( $callable, array( $options ) );
566 + public function validate_options( $input ) {
567 + if ( empty( $input['apikey'] ) ) {
568 + add_settings_error(
569 + self::OPTIONS_KEY,
570 + 'apikey',
571 + __( 'Please specify the Site ID', 'wp-parsely' )
572 + );
573 + } else {
574 + $input['apikey'] = strtolower( $input['apikey'] );
575 + $input['apikey'] = sanitize_text_field( $input['apikey'] );
576 + if ( strpos( $input['apikey'], '.' ) === false || strpos( $input['apikey'], ' ' ) !== false ) {
577 + add_settings_error(
578 + self::OPTIONS_KEY,
579 + 'apikey',
580 + __( 'Your Parse.ly Site ID looks incorrect, it should look like "example.com".', 'wp-parsely' )
581 + );
303 582 }
583 + }
584 + // these can't be null, if somebody accidentally deselected them just reset to default.
585 + if ( ! isset( $input['track_post_types'] ) ) {
586 + $input['track_post_types'] = array( 'post' );
304 587
305 - // Update our version info.
306 - $options['plugin_version'] = self::VERSION;
307 - update_option( self::OPTIONS_KEY, $options );
308 588 }
589 + if ( ! isset( $input['track_page_types'] ) ) {
590 + $input['track_page_types'] = array( 'page' );
591 + }
309 592
310 - // @phpstan-ignore return.void
311 - add_action( 'save_post', array( $this, 'call_update_metadata_endpoint' ) );
593 + if ( empty( $input['logo'] ) ) {
594 + $input['logo'] = $this->get_logo_default();
595 + }
596 +
597 + $input['track_post_types'] = $this->validate_option_array( $input['track_post_types'], 'track_post_types' );
598 + $input['track_page_types'] = $this->validate_option_array( $input['track_page_types'], 'track_page_types' );
599 +
600 + $input['api_secret'] = sanitize_text_field( $input['api_secret'] );
601 + // Content ID prefix.
602 + $input['content_id_prefix'] = sanitize_text_field( $input['content_id_prefix'] );
603 + $input['custom_taxonomy_section'] = sanitize_text_field( $input['custom_taxonomy_section'] );
604 +
605 + // Custom taxonomy as section.
606 + // Top-level categories.
607 + if ( 'true' !== $input['use_top_level_cats'] && 'false' !== $input['use_top_level_cats'] ) {
608 + add_settings_error(
609 + self::OPTIONS_KEY,
610 + 'use_top_level_cats',
611 + __( 'Value passed for use_top_level_cats must be either "true" or "false".', 'wp-parsely' )
612 + );
613 + } else {
614 + $input['use_top_level_cats'] = 'true' === $input['use_top_level_cats'];
615 + }
616 +
617 + // Child categories as tags.
618 + if ( 'true' !== $input['cats_as_tags'] && 'false' !== $input['cats_as_tags'] ) {
619 + add_settings_error(
620 + self::OPTIONS_KEY,
621 + 'cats_as_tags',
622 + __( 'Value passed for cats_as_tags must be either "true" or "false".', 'wp-parsely' )
623 + );
624 + } else {
625 + $input['cats_as_tags'] = 'true' === $input['cats_as_tags'];
626 + }
627 +
628 + // Track authenticated users.
629 + if ( 'true' !== $input['track_authenticated_users'] && 'false' !== $input['track_authenticated_users'] ) {
630 + add_settings_error(
631 + self::OPTIONS_KEY,
632 + 'track_authenticated_users',
633 + __( 'Value passed for track_authenticated_users must be either "true" or "false".', 'wp-parsely' )
634 + );
635 + } else {
636 + $input['track_authenticated_users'] = 'true' === $input['track_authenticated_users'];
637 + }
638 +
639 + // Lowercase tags.
640 + if ( 'true' !== $input['lowercase_tags'] && 'false' !== $input['lowercase_tags'] ) {
641 + add_settings_error(
642 + self::OPTIONS_KEY,
643 + 'lowercase_tags',
644 + __( 'Value passed for lowercase_tags must be either "true" or "false".', 'wp-parsely' )
645 + );
646 + } else {
647 + $input['lowercase_tags'] = 'true' === $input['lowercase_tags'];
648 + }
649 +
650 + if ( 'true' !== $input['force_https_canonicals'] && 'false' !== $input['force_https_canonicals'] ) {
651 + add_settings_error(
652 + self::OPTIONS_KEY,
653 + 'force_https_canonicals',
654 + __( 'Value passed for force_https_canonicals must be either "true" or "false".', 'wp-parsely' )
655 + );
656 + } else {
657 + $input['force_https_canonicals'] = 'true' === $input['force_https_canonicals'];
658 + }
659 +
660 + if ( 'true' !== $input['disable_javascript'] && 'false' !== $input['disable_javascript'] ) {
661 + add_settings_error(
662 + self::OPTIONS_KEY,
663 + 'disable_javascript',
664 + __( 'Value passed for disable_javascript must be either "true" or "false".', 'wp-parsely' )
665 + );
666 + } else {
667 + $input['disable_javascript'] = 'true' === $input['disable_javascript'];
668 + }
669 +
670 + if ( 'true' !== $input['disable_amp'] && 'false' !== $input['disable_amp'] ) {
671 + add_settings_error(
672 + self::OPTIONS_KEY,
673 + 'disable_amp',
674 + __( 'Value passed for disable_amp must be either "true" or "false".', 'wp-parsely' )
675 + );
676 + } else {
677 + $input['disable_amp'] = 'true' === $input['disable_amp'];
678 + }
679 +
680 + if ( ! empty( $input['metadata_secret'] ) ) {
681 + if ( strlen( $input['metadata_secret'] ) !== 10 ) {
682 + add_settings_error(
683 + self::OPTIONS_KEY,
684 + 'metadata_secret',
685 + __( 'Metadata secret is incorrect. Please contact Parse.ly support!', 'wp-parsely' )
686 + );
687 + } elseif ( 'true' === $input['parsely_wipe_metadata_cache'] ) {
688 + delete_post_meta_by_key( 'parsely_metadata_last_updated' );
689 +
690 + wp_schedule_event( time() + 100, 'everytenminutes', 'parsely_bulk_metas_update' );
691 + $input['parsely_wipe_metadata_cache'] = false;
692 + }
693 + }
694 +
695 + return $input;
312 696 }
313 697
314 698 /**
315 - * Returns the Content API service.
699 + * Not doing anything here
316 700 *
317 - * This method returns the Content API service, which is used to interact with the Parse.ly Content API.
318 - *
319 - * @since 3.17.0
320 - *
321 - * @return Content_API_Service
701 + * @category Function
702 + * @package Parsely
322 703 */
323 - public function get_content_api(): Content_API_Service {
324 - if ( ! isset( $this->content_api_service ) ) {
325 - $this->content_api_service = new Content_API_Service( $this );
326 - }
327 -
328 - return $this->content_api_service;
704 + public function print_required_settings() {
705 + // We can optionally print some text here in the future, but we don't
706 + // need to now.
329 707 }
330 708
331 709 /**
332 - * Returns the Suggestions API service.
710 + * Not doing anything here
333 711 *
334 - * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
335 - *
336 - * @since 3.17.0
337 - *
338 - * @return Suggestions_API_Service
712 + * @category Function
713 + * @package Parsely
339 714 */
340 - public function get_suggestions_api(): Suggestions_API_Service {
341 - if ( ! isset( $this->suggestions_api_service ) ) {
342 - $this->suggestions_api_service = new Suggestions_API_Service( $this );
343 - }
344 -
345 - return $this->suggestions_api_service;
715 + public function print_optional_settings() {
716 + // We can optionally print some text here in the future, but we don't
717 + // need to now.
346 718 }
347 719
348 720 /**
349 - * Gets the REST API controller.
721 + * Adds a 'Settings' link to the Plugins screen in WP admin
350 722 *
351 - * If the controller is not set, a new instance is created.
352 - *
353 - * @since 3.17.0
354 - *
355 - * @return REST_API_Controller
723 + * @category Function
724 + * @package Parsely
725 + * @param array $links The links to add.
356 726 */
357 - public function get_rest_api_controller(): REST_API_Controller {
358 - if ( ! isset( $this->rest_api_controller ) ) {
359 - $this->rest_api_controller = new REST_API_Controller( $this );
360 - }
361 -
362 - return $this->rest_api_controller;
727 + public function add_plugin_meta_links( $links ) {
728 + array_unshift( $links, '<a href="' . esc_url( $this->get_settings_url() ) . '">' . __( 'Settings', 'wp-parsely' ) . '</a>' );
729 + return $links;
363 730 }
364 731
365 732 /**
366 - * Gets the full URL of the JavaScript tracker file for the site. If an API
367 - * key is not set, return an empty string.
733 + * Display the admin warning if needed
368 734 *
369 - * @since 3.2.0
370 - *
371 - * @return string
735 + * @category Function
736 + * @package Parsely
372 737 */
373 - public function get_tracker_url(): string {
374 - if ( $this->site_id_is_set() ) {
375 - $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
738 + public function display_admin_warning() {
739 + $options = $this->get_options();
376 740
377 - /**
378 - * Filters the URL of the Parse.ly tracker script.
379 - *
380 - * The filtered value gets sanitized with {@see esc_url_raw()}.
381 - *
382 - * @since 3.23.0
383 - *
384 - * @param string $tracker_url The URL of the tracker script.
385 - */
386 - return esc_url_raw( apply_filters( 'wp_parsely_tracker_url', $tracker_url ) );
741 + if ( isset( $options['apikey'] ) && ! empty( $options['apikey'] ) ) {
742 + return;
387 743 }
388 744
389 - return '';
745 + $message = sprintf(
746 + /* translators: %s: Plugin settings page URL */
747 + __( '<strong>The Parse.ly plugin is not active.</strong> You need to <a href="%s">provide your Parse.ly Dash Site ID</a> before things get cooking.', 'wp-parsely' ),
748 + esc_url( $this->get_settings_url() )
749 + );
750 + ?>
751 + <div id="message" class="error"><p><?php echo wp_kses_post( $message ); ?></p></div>
752 + <?php
390 753 }
391 754
392 755 /**
393 - * Deprecated.
394 - * Inserts the code for the <meta name='parsely-page'> parameter within the
395 - * head tag.
756 + * Show our note about dynamic tracking
396 757 *
397 - * @since 3.2.0
398 - * @deprecated 3.3.0
399 - * @see Metadata_Renderer::render_metadata
400 - *
401 - * @param string $meta_type `json_ld` or `repeated_metas`.
758 + * @category Function
759 + * @package Parsely
402 760 */
403 - public function render_metadata( string $meta_type ): void {
404 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
405 - $metadata_renderer = new Metadata_Renderer( $this );
406 - $metadata_renderer->render_metadata( $meta_type );
761 + public function print_dynamic_tracking_note() {
762 + printf(
763 + /* translators: 1: Documentation URL 2: Documentation URL */
764 + wp_kses_post( __( '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="%1$s">the standard Parse.ly include</a> that the plugin injects into your page source. Please consult <a href="%2$s">the Parse.ly documentation on dynamic tracking</a> for instructions on implementing dynamic tracking, or contact Parse.ly support (<a href="%3$s">support@parsely.com</a> ) for additional assistance.', 'wp-parsely' ) ),
765 + esc_url( 'http://www.parsely.com/help/integration/basic/' ),
766 + esc_url( 'https://www.parsely.com/help/integration/dynamic/' ),
767 + esc_url( 'mailto:support@parsely.com' )
768 + );
407 769 }
408 770
409 771 /**
410 - * Deprecated.
411 - * Insert the code for the <meta name='parsely-page'> parameter within the
412 - * head tag.
772 + * End the code coverage ignore
413 773 *
414 - * @since 3.0.0
415 - * @deprecated 3.3.0
416 - * @see Metadata_Renderer::render_metadata
774 + * @codeCoverageIgnoreEnd
417 775 */
418 - public function insert_page_header_metadata(): void {
419 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
420 - $parsely_options = $this->get_options();
421 - $metadata_renderer = new Metadata_Renderer( $this );
422 - $metadata_renderer->render_metadata( $parsely_options['meta_type'] );
776 +
777 + /**
778 + * Actually inserts the code for the <meta name='parsely-page'> parameter within the <head></head> tag.
779 + */
780 + public function insert_parsely_page() {
781 + $parsely_options = $this->get_options();
782 +
783 + if (
784 + // No API key.
785 + empty( $parsely_options['apikey'] ) ||
786 +
787 + // Chosen not to track logged in users.
788 + ( ! $parsely_options['track_authenticated_users'] && $this->parsely_is_user_logged_in() ) ||
789 +
790 + // 404 pages are not tracked.
791 + is_404() ||
792 +
793 + // Search pages are not tracked.
794 + is_search()
795 + ) {
796 + return '';
797 + }
798 +
799 + global $post;
800 + // Assign default values for LD+JSON
801 + // TODO: Maping of an install's post types to Parse.ly post types (namely page/post).
802 + $parsely_page = $this->construct_parsely_metadata( $parsely_options, $post );
803 +
804 + // Something went wrong - abort.
805 + if ( empty( $parsely_page ) || ! isset( $parsely_page['headline'] ) ) {
806 + return;
807 + }
808 +
809 + echo "\n" . '<!-- BEGIN Parse.ly ' . esc_html( Parsely::VERSION ) . ' -->' . "\n";
810 +
811 + // Insert JSON-LD or repeated metas.
812 + if ( 'json_ld' === $parsely_options['meta_type'] ) {
813 + include PARSELY_PLUGIN_DIR . 'views/json-ld.php';
814 + } else {
815 + $parsely_post_type = $this->convert_jsonld_to_parsely_type( $parsely_page['@type'] );
816 + if ( is_array( $parsely_page['keywords'] ) ) {
817 + $parsely_page['keywords'] = implode( ',', $parsely_page['keywords'] );
818 + }
819 +
820 + include PARSELY_PLUGIN_DIR . 'views/repeated-metas.php';
821 + }
822 +
823 + // Add any custom metadata.
824 + if ( isset( $parsely_page['custom_metadata'] ) ) {
825 + include PARSELY_PLUGIN_DIR . 'views/custom-metadata.php';
826 + }
827 +
828 + echo '<!-- END Parse.ly -->' . "\n\n";
829 +
830 + return $parsely_page;
423 831 }
424 832
425 833 /**
426 - * Compares the post_status key against an allowed list.
834 + * Compare the post_status key against an allowed list (by default, only 'publish'ed content includes tracking data).
427 835 *
428 - * By default, only 'publish'ed content includes tracking data.
429 - *
430 836 * @since 2.5.0
431 837 *
432 838 * @param int|WP_Post $post Which post object or ID to check.
433 - * @return bool Should the post status be tracked for the provided post's post_type.
434 - * By default,only 'publish' is allowed.
839 + * @return bool Should the post status be tracked for the provided post's post_type. By default, only 'publish' is allowed.
435 840 */
436 - public static function post_has_trackable_status( $post ): bool {
841 + public static function post_has_trackable_status( $post ) {
842 + static $cache = array();
843 + $post_id = is_int( $post ) ? $post : $post->ID;
844 + if ( isset( $cache[ $post_id ] ) ) {
845 + return $cache[ $post_id ];
846 + }
847 +
437 848 /**
438 - * Filters whether the post password check should be skipped when getting
439 - * the post trackable status.
849 + * Filters the statuses that are permitted to be tracked.
440 850 *
441 - * @since 3.0.1
851 + * By default, the only status tracked is 'publish'. Use this filter if you have other published content that has a different (custom) status.
442 852 *
443 - * @param bool $skip True if the password check should be skipped.
444 - * @param int|WP_Post $post Which post object or ID is being checked.
853 + * @since 2.5.0
445 854 *
446 - * @return bool
855 + * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
856 + * @param int|WP_Post $post Which post object or ID is being checked.
447 857 */
448 - $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
449 - if ( ! $skip_password_check && post_password_required( $post ) ) {
450 - return false;
451 - }
452 -
453 - $statuses = self::get_trackable_statuses( $post );
454 - return in_array( get_post_status( $post ), $statuses, true );
858 + $statuses = apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
859 + $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true );
860 + return $cache[ $post_id ];
455 861 }
456 862
457 863 /**
458 - * Deprecated. Please use the `Metadata` class instead.
459 - *
460 864 * Creates parsely metadata object from post metadata.
461 865 *
462 - * @deprecated 3.3.0
463 - * @see \Parsely\Metadata::construct_metadata
464 - *
465 - * @param array<string, mixed> $parsely_options parsely_options array.
466 - * @param WP_Post $post object.
467 - * @return Metadata_Attributes
866 + * @param array $parsely_options parsely_options array.
867 + * @param WP_Post $post object.
868 + * @return mixed|void
468 869 */
469 - public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
470 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
471 - $metadata = new Metadata( $this );
472 - return $metadata->construct_metadata( $post );
473 - }
870 + public function construct_parsely_metadata( array $parsely_options, $post ) {
871 + $parsely_page = array(
872 + '@context' => 'http://schema.org',
873 + '@type' => 'WebPage',
874 + );
875 + $current_url = $this->get_current_url();
474 876
475 - /**
476 - * Calls Parse.ly's update metadata endpoint, sending the post's updated
477 - * metadata.
478 - *
479 - * @param int $post_id The ID of the post to update.
480 - * @return bool True if the metadata endpoint was called, false otherwise.
481 - */
482 - public function call_update_metadata_endpoint( int $post_id ): bool {
483 - $options = $this->get_options();
877 + if ( is_front_page() && ! is_paged() || ( 'page' === get_option( 'show_on_front' ) && ! get_option( 'page_on_front' ) ) ) {
878 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_bloginfo( 'name', 'raw' ) );
879 + $parsely_page['url'] = home_url();
880 + } elseif ( is_front_page() && is_paged() ) {
881 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_bloginfo( 'name', 'raw' ) );
882 + $parsely_page['url'] = $current_url;
883 + } elseif ( is_home() ) {
884 + $parsely_page['headline'] = get_the_title( get_option('page_for_posts', true) );
885 + $parsely_page['url'] = $current_url;
886 + } elseif ( is_author() ) {
887 + // TODO: why can't we have something like a WP_User object for all the other cases? Much nicer to deal with than functions.
888 + $author = ( get_query_var( 'author_name' ) ) ? get_user_by( 'slug', get_query_var( 'author_name' ) ) : get_userdata( get_query_var( 'author' ) );
889 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( 'Author - ' . $author->data->display_name );
890 + $parsely_page['url'] = $current_url;
891 + } elseif ( is_category() ) {
892 + $category = get_the_category();
893 + $category = $category[0];
894 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( $category->name );
895 + $parsely_page['url'] = $current_url;
896 + } elseif ( is_date() ) {
897 + if ( is_year() ) {
898 + /* translators: %s: Archive year */
899 + $parsely_page['headline'] = sprintf( __( 'Yearly Archive - %s', 'wp-parsely' ), get_the_time( 'Y' ) );
900 + } elseif ( is_month() ) {
901 + /* translators: %s: Archive month, formatted as F, Y */
902 + $parsely_page['headline'] = sprintf( __( 'Monthly Archive - %s', 'wp-parsely' ), get_the_time( 'F, Y' ) );
903 + } elseif ( is_day() ) {
904 + /* translators: %s: Archive day, formatted as F jS, Y */
905 + $parsely_page['headline'] = sprintf( __( 'Daily Archive - %s', 'wp-parsely' ), get_the_time( 'F jS, Y' ) );
906 + } elseif ( is_time() ) {
907 + /* translators: %s: Archive time, formatted as F jS g:i:s A */
908 + $parsely_page['headline'] = sprintf( __( 'Hourly, Minutely, or Secondly Archive - %s', 'wp-parsely' ), get_the_time( 'F jS g:i:s A' ) );
909 + }
910 + $parsely_page['url'] = $current_url;
911 + } elseif ( is_tag() ) {
912 + $tag = single_tag_title( '', false );
913 + if ( empty( $tag ) ) {
914 + $tag = single_term_title( '', false );
915 + }
916 + /* translators: %s: Tag name */
917 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( sprintf( __( 'Tagged - %s', 'wp-parsely' ), $tag ) );
918 + $parsely_page['url'] = $current_url;
919 + } elseif ( in_array( get_post_type( $post ), $parsely_options['track_post_types'], true ) && self::post_has_trackable_status( $post ) ) {
920 + $authors = $this->get_author_names( $post );
921 + $category = $this->get_category_name( $post, $parsely_options );
922 + $post_id = $parsely_options['content_id_prefix'] . get_the_ID();
484 923
485 - if ( $this->site_id_is_missing() || '' === $options['metadata_secret'] ) {
486 - return false;
487 - }
924 + if ( has_post_thumbnail( $post ) ) {
925 + $image_id = get_post_thumbnail_id( $post );
926 + $image_url = wp_get_attachment_image_src( $image_id );
927 + $image_url = $image_url[0];
928 + } else {
929 + $image_url = $this->get_first_image( $post );
930 + }
488 931
489 - $current_post_type = get_post_type( $post_id );
490 - if ( false === $current_post_type ) {
491 - return false;
932 + $tags = $this->get_tags( $post->ID );
933 + if ( $parsely_options['cats_as_tags'] ) {
934 + $tags = array_merge( $tags, $this->get_categories( $post->ID ) );
935 + // add custom taxonomy values.
936 + $tags = array_merge( $tags, $this->get_custom_taxonomy_values( $post, $parsely_options ) );
937 + }
938 + // the function 'mb_strtolower' is not enabled by default in php, so this check
939 + // falls back to the native php function 'strtolower' if necessary.
940 + if ( function_exists( 'mb_strtolower' ) ) {
941 + $lowercase_callback = 'mb_strtolower';
942 + } else {
943 + $lowercase_callback = 'strtolower';
944 + }
945 + if ( $parsely_options['lowercase_tags'] ) {
946 + $tags = array_map( $lowercase_callback, $tags );
947 + }
948 +
949 + /**
950 + * Filters the post tags that are used as metadata keywords.
951 + *
952 + * @since 1.8.0
953 + *
954 + * @param string[] $tags Post tags.
955 + * @param int $ID Post ID.
956 + */
957 + $tags = apply_filters( 'wp_parsely_post_tags', $tags, $post->ID );
958 + $tags = array_map( array( $this, 'get_clean_parsely_page_value' ), $tags );
959 + $tags = array_values( array_unique( $tags ) );
960 +
961 + /**
962 + * Filters the JSON-LD @type.
963 + *
964 + * @since 2.5.0
965 + *
966 + * @param array $jsonld_type JSON-LD @type value, default is NewsArticle.
967 + * @param integer $id Post ID.
968 + * @param string $post_type Post type in WordPress.
969 + */
970 + $type = (string) apply_filters( 'wp_parsely_post_type', 'NewsArticle', $post->ID, $post->post_type );
971 + $supported_types = array_merge( $this->supported_jsonld_post_types, $this->supported_jsonld_non_post_types );
972 +
973 + // Validate type before passing it further as an invalid type will not be recognized by Parse.ly.
974 + if ( ! in_array( $type, $supported_types ) ) {
975 + $error = sprintf(
976 + __( '@type %1$s is not supported by Parse.ly. Please use a type mentioned in %2$s', 'wp-parsely' ),
977 + $type,
978 + 'https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages'
979 + );
980 + trigger_error( $error, E_USER_WARNING );
981 + $type = 'NewsArticle';
982 + }
983 +
984 + $parsely_page['@type'] = $type;
985 + $parsely_page['mainEntityOfPage'] = array(
986 + '@type' => 'WebPage',
987 + '@id' => $this->get_current_url( 'post' ),
988 + );
989 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_the_title( $post ) );
990 + $parsely_page['url'] = $this->get_current_url( 'post', $post->ID );
991 + $parsely_page['thumbnailUrl'] = $image_url;
992 + $parsely_page['image'] = array(
993 + '@type' => 'ImageObject',
994 + 'url' => $image_url,
995 + );
996 + $parsely_page['dateCreated'] = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
997 + $parsely_page['datePublished'] = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
998 + if ( get_the_modified_date( 'U', true ) >= get_post_time( 'U', true, $post ) ) {
999 + $parsely_page['dateModified'] = gmdate( 'Y-m-d\TH:i:s\Z', get_the_modified_date( 'U', true ) );
1000 + } else {
1001 + // Use the post time as the earliest possible modification date.
1002 + $parsely_page['dateModified'] = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
1003 + }
1004 + $parsely_page['articleSection'] = $category;
1005 + $author_objects = array();
1006 + foreach ( $authors as $author ) {
1007 + $author_tag = array(
1008 + '@type' => 'Person',
1009 + 'name' => $author,
1010 + );
1011 + array_push( $author_objects, $author_tag );
1012 + }
1013 + $parsely_page['author'] = $author_objects;
1014 + $parsely_page['creator'] = $authors;
1015 + $parsely_page['publisher'] = array(
1016 + '@type' => 'Organization',
1017 + 'name' => get_bloginfo( 'name' ),
1018 + 'logo' => $parsely_options['logo'],
1019 + );
1020 + $parsely_page['keywords'] = $tags;
1021 + } elseif ( in_array( get_post_type(), $parsely_options['track_page_types'], true ) && self::post_has_trackable_status( $post ) ) {
1022 + $parsely_page['headline'] = $this->get_clean_parsely_page_value( get_the_title( $post ) );
1023 + $parsely_page['url'] = $this->get_current_url( 'post' );
492 1024 }
493 1025
494 - $tracked_post_types = array_merge(
495 - $options['track_post_types'],
496 - $options['track_page_types']
1026 + /**
1027 + * Filters the structured metadata.
1028 + *
1029 + * @deprecated 2.5.0 Use `wp_parsely_metadata` filter instead.
1030 + * @since 1.10.0
1031 + *
1032 + * @param array $parsely_page Existing structured metadata for a page.
1033 + * @param WP_Post $post Post object.
1034 + * @param array $parsely_options The Parsely options.
1035 + */
1036 + $parsely_page = apply_filters_deprecated(
1037 + 'after_set_parsely_page',
1038 + array( $parsely_page, $post, $parsely_options ),
1039 + '2.5.0',
1040 + 'wp_parsely_metadata'
497 1041 );
498 1042
499 - // Check that the post's type is trackable.
500 - if ( ! in_array( $current_post_type, $tracked_post_types, true ) ) {
501 - return false;
502 - }
1043 + /**
1044 + * Filters the structured metadata.
1045 + *
1046 + * @since 2.5.0
1047 + *
1048 + * @param array $parsely_page Existing structured metadata for a page.
1049 + * @param WP_Post $post Post object.
1050 + * @param array $parsely_options The Parsely options.
1051 + */
1052 + $parsely_page = apply_filters( 'wp_parsely_metadata', $parsely_page, $post, $parsely_options );
503 1053
504 - // Check that the post's status is trackable.
505 - if ( ! self::post_has_trackable_status( $post_id ) ) {
506 - return false;
507 - }
1054 + return $parsely_page;
1055 + }
508 1056
509 - $post = get_post( $post_id );
510 - if ( null === $post ) {
511 - return false;
512 - }
513 1057
514 - // Don't call the endpoint when integration tests are running, but
515 - // signal that the above checks have passed.
516 - if ( defined( 'INTEGRATION_TESTS_RUNNING' ) ) {
517 - return true;
1058 + /**
1059 + * Updates the Parsely metadata endpoint with the new metadata of the post.
1060 + *
1061 + * @param int $post_id id of the post to update.
1062 + * @return string
1063 + */
1064 + public function update_metadata_endpoint( $post_id ) {
1065 + $parsely_options = $this->get_options();
1066 +
1067 + if ( empty( $parsely_options['apikey'] ) || empty( $parsely_options['metadata_secret'] ) ) {
1068 + return '';
518 1069 }
519 1070
520 - $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
1071 + $post = get_post( $post_id );
1072 + $metadata = $this->construct_parsely_metadata( $parsely_options, $post );
521 1073
522 1074 $endpoint_metadata = array(
523 - 'canonical_url' => $metadata['url'] ?? '',
524 - 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
525 - 'title' => $metadata['headline'] ?? '',
526 - 'image_url' => $metadata['image']['url'] ?? '',
527 - 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
528 - 'section' => $metadata['articleSection'] ?? '',
529 - 'authors' => $metadata['creator'] ?? '',
530 - 'tags' => $metadata['keywords'] ?? '',
1075 + 'canonical_url' => $metadata['url'],
1076 + 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ),
1077 + 'title' => $metadata['headline'],
1078 + 'image_url' => $metadata['thumbnailUrl'],
1079 + 'pub_date_tmsp' => $metadata['datePublished'],
1080 + 'section' => $metadata['articleSection'],
1081 + 'authors' => $metadata['creator'],
1082 + 'tags' => $metadata['keywords'],
531 1083 );
532 1084
533 - $parsely_api_base_url = Content_API_Service::get_base_url();
534 - $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
535 - $parsely_metadata_secret = $options['metadata_secret'];
536 -
537 - $headers = array( 'Content-Type' => 'application/json' );
538 - $body = wp_json_encode(
1085 + $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts';
1086 + $parsely_metadata_secret = $parsely_options['metadata_secret'];
1087 + $headers = array(
1088 + 'Content-Type' => 'application/json',
1089 + );
1090 + $body = wp_json_encode(
539 1091 array(
540 1092 'secret' => $parsely_metadata_secret,
541 - 'apikey' => $this->get_site_id(),
1093 + 'apikey' => $parsely_options['apikey'],
542 1094 'metadata' => $endpoint_metadata,
543 1095 )
544 1096 );
1097 + $response = wp_remote_post(
1098 + $parsely_api_endpoint,
1099 + array(
1100 + 'method' => 'POST',
1101 + 'headers' => $headers,
1102 + 'blocking' => false,
1103 + 'body' => $body,
1104 + 'data_format' => 'body',
1105 + )
1106 + );
1107 + $current_timestamp = time();
1108 + $meta_update = update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
545 1109
1110 + }
1111 +
1112 +
1113 + /**
1114 + * Updates posts with Parsely metadata api in bulk.
1115 + */
1116 + public function bulk_update_posts() {
1117 + global $wpdb;
1118 + $parsely_options = $this->get_options();
1119 + $allowed_types = array_merge( $parsely_options['track_post_types'], $parsely_options['track_page_types'] );
1120 + $allowed_types_string = implode(
1121 + ', ',
1122 + array_map(
1123 + function( $v ) {
1124 + return "'" . esc_sql( $v ) . "'";
1125 + },
1126 + $allowed_types
1127 + )
1128 + );
1129 + $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
1130 + if ( false === $ids ) {
1131 + $ids = array();
1132 + $results = $wpdb->get_results(
1133 + $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 ),
1134 + ARRAY_N
1135 + );
1136 + foreach ( $results as $result ) {
1137 + array_push( $ids, $result[0] );
1138 + }
1139 + wp_cache_set( 'parsely_post_ids_need_meta_updating', $ids, '', 86400 );
1140 + }
1141 +
1142 + for ( $i = 0; $i < 100; $i++ ) {
1143 + $post_id = array_pop( $ids );
1144 + if ( null === $post_id ) {
1145 + wp_clear_scheduled_hook( 'parsely_bulk_metas_update' );
1146 + break;
1147 + }
1148 + $this->update_metadata_endpoint( $post_id );
1149 + }
1150 + }
1151 +
1152 + public static function get_asset_cache_buster() {
1153 + static $cache_buster;
1154 + if ( isset( $cache_buster ) ) {
1155 + return $cache_buster;
1156 + }
1157 +
1158 + $cache_buster = defined( 'WP_DEBUG' ) && WP_DEBUG && empty( 'WP_TESTS_DOMAIN' ) ? wp_rand() : PARSELY_VERSION;
1159 +
546 1160 /**
547 - * POST request options.
1161 + * Filters the cache buster value for linked scripts and styles.
548 1162 *
549 - * @var WP_HTTP_Request_Args $request_options
1163 + * @since 2.5.0
1164 + *
1165 + * @param string $cache_buster Plugin version, unless WP_DEBUG is defined and truthy, and tests are not running.
550 1166 */
551 - $request_options = array(
552 - 'method' => 'POST',
553 - 'headers' => $headers,
554 - 'blocking' => false,
555 - 'body' => $body,
556 - 'data_format' => 'body',
557 - );
1167 + return apply_filters( 'wp_parsely_cache_buster', $cache_buster );
1168 + }
558 1169
559 - $response = wp_remote_post( $parsely_api_endpoint, $request_options );
1170 + public function register_js() {
1171 + $parsely_options = $this->get_options();
560 1172
561 - if ( is_wp_error( $response ) ) {
562 - return false;
1173 + // If we don't have an API key, there's no need to proceed.
1174 + if ( empty( $parsely_options['apikey'] ) ) {
1175 + return '';
563 1176 }
564 1177
565 - update_post_meta( $post_id, 'parsely_metadata_last_updated', time() );
1178 + wp_register_script(
1179 + 'wp-parsely-tracker',
1180 + 'https://cdn.parsely.com/keys/' . $parsely_options['apikey'] . '/p.js',
1181 + array(),
1182 + self::get_asset_cache_buster(),
1183 + true
1184 + );
566 1185
567 - return true;
1186 + $api_script_asset = require PARSELY_PLUGIN_DIR . 'build/init-api.asset.php' ;
1187 + wp_register_script(
1188 + 'wp-parsely-api',
1189 + PARSELY_PLUGIN_URL . 'build/init-api.js',
1190 + $api_script_asset[ 'dependencies' ],
1191 + self::get_asset_cache_buster(),
1192 + true
1193 + );
1194 +
1195 + wp_localize_script(
1196 + 'wp-parsely-api',
1197 + 'wpParsely', // This globally-scoped object holds variables used to interact with the API
1198 + array(
1199 + 'apikey' => $parsely_options['apikey'],
1200 + )
1201 + );
568 1202 }
569 1203
570 1204 /**
571 - * Safely returns options for the plugin by assigning defaults contained in
572 - * optionDefaults.
573 - *
574 - * As soon as actual options are saved, they override the defaults. This
575 - * prevents us from having to do a lot of isset() checking on variables.
576 - *
577 - * @return Parsely_Options
1205 + * Enqueues the JavaScript code required to send off beacon requests
578 1206 */
579 - public function get_options() {
1207 + public function load_js_tracker() {
1208 + $parsely_options = $this->get_options();
1209 + // If we don't have an API key, there's no need to proceed.
1210 + if ( empty( $parsely_options['apikey'] ) || $parsely_options['disable_javascript'] ) {
1211 + return;
1212 + }
1213 +
1214 + global $post;
1215 + $display = true;
1216 + if ( in_array( get_post_type(), $parsely_options['track_post_types'], true ) && ! self::post_has_trackable_status( $post ) ) {
1217 + $display = false;
1218 + }
1219 + if ( ! $parsely_options['track_authenticated_users'] && $this->parsely_is_user_logged_in() ) {
1220 + $display = false;
1221 + }
1222 + if ( ! in_array( get_post_type(), $parsely_options['track_post_types'], true ) && ! in_array( get_post_type(), $parsely_options['track_page_types'], true ) ) {
1223 + $display = false;
1224 + }
1225 +
580 1226 /**
581 - * Variable.
1227 + * Filters whether to include the Parsely JavaScript file.
582 1228 *
583 - * @var Parsely_Options|null
1229 + * If true, the JavaScript files are sourced.
1230 + *
1231 + * @since 2.2.0
1232 + * @deprecated 2.5.0 Use `wp_parsely_load_js_tracker` filter instead.
1233 + *
1234 + * @param bool $display True if the JavaScript file should be included. False if not.
584 1235 */
585 - $options = get_option( self::OPTIONS_KEY, null );
1236 + if ( ! apply_filters_deprecated(
1237 + 'parsely_filter_insert_javascript',
1238 + array( $display ),
1239 + '2.5.0',
1240 + 'wp_parsely_load_js_tracker'
1241 + ) ) {
1242 + return;
1243 + }
586 1244
587 - // Existing plugin installation without full metadata option.
588 - /* @phpstan-ignore isset.offset, booleanAnd.alwaysFalse */
589 - if ( is_array( $options ) && ! isset( $options['full_metadata_in_non_posts'] ) ) {
590 - $this->set_default_full_metadata_in_non_posts();
1245 + /**
1246 + * Filters whether to enqueue the Parsely JavaScript tracking script from the CDN.
1247 + *
1248 + * If true, the script is enqueued.
1249 + *
1250 + * @since 2.5.0
1251 + *
1252 + * @param bool $display True if the JavaScript file should be included. False if not.
1253 + */
1254 + if ( ! apply_filters( 'wp_parsely_load_js_tracker', $display ) ) {
1255 + return;
591 1256 }
592 1257
593 - // Existing plugin installation without Content Intelligence options.
594 - /* @phpstan-ignore isset.offset, booleanAnd.alwaysFalse */
595 - if ( is_array( $options ) && ! isset( $options['content_helper'] ) ) {
596 - $this->set_default_content_helper_settings_values();
1258 + if ( ! has_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ) ) ) {
1259 + add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 3 );
597 1260 }
598 1261
599 - // Existing plugin installation that's missing a Content Intelligence
600 - // feature option.
601 - /* @phpstan-ignore isset.offset */
602 - if ( is_array( $options ) && isset( $options['content_helper'] ) ) {
603 - /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options */
604 - $pch_options = $options['content_helper'];
1262 + wp_enqueue_script( 'wp-parsely-tracker' );
1263 + }
605 1264
606 - /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options_defaults */
607 - $pch_options_defaults = $this->option_defaults['content_helper'];
1265 + public function load_js_api() {
1266 + $parsely_options = $this->get_options();
608 1267
609 - if ( count( $pch_options ) !== count( $pch_options_defaults ) ) {
610 - $new_keys = array_diff(
611 - array_keys( $pch_options_defaults ),
612 - array_keys( $pch_options )
613 - );
1268 + // If we don't have an API secret, there's no need to proceed.
1269 + if ( empty( $parsely_options['api_secret'] ) ) {
1270 + return;
1271 + }
614 1272
615 - foreach ( $new_keys as $key ) {
616 - $options['content_helper'][ $key ] = $pch_options_defaults[ $key ];
617 - }
618 - }
1273 + if ( ! has_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ) ) ) {
1274 + add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 3 );
619 1275 }
620 1276
621 - // New plugin installation that hasn't saved its options yet.
622 - if ( ! is_array( $options ) ) {
623 - $this->set_default_track_as_values();
624 - $this->set_default_full_metadata_in_non_posts();
625 - $options = $this->option_defaults;
1277 + wp_enqueue_script( 'wp-parsely-api' );
1278 + }
1279 +
1280 + public function script_loader_tag( $tag, $handle, $src ) {
1281 + $parsely_options = $this->get_options();
1282 + if ( in_array( $handle, array(
1283 + 'wp-parsely',
1284 + 'wp-parsely-api',
1285 + 'wp-parsely-tracker',
1286 + 'wp-parsely-recommended-widget',
1287 + ) ) ) {
1288 + // Have ClouldFlare Rocket Loader ignore these scripts:
1289 + // https://support.cloudflare.com/hc/en-us/articles/200169436-How-can-I-have-Rocket-Loader-ignore-specific-JavaScripts-
1290 + $tag = preg_replace( '/^<script /', '<script data-cfasync="false" ', $tag );
626 1291 }
627 1292
628 - /**
629 - * Final options including managed credentials and options.
630 - *
631 - * @var Parsely_Options
632 - */
633 - return array_merge(
634 - $this->option_defaults,
635 - $options,
636 - $this->get_managed_credentials(),
637 - $this->managed_options
638 - );
1293 + if ( $handle === 'wp-parsely-tracker' ) {
1294 + $tag = preg_replace( '/ id=(\"|\')wp-parsely-tracker-js\1/', ' id="parsely-cfg"', $tag );
1295 + $tag = preg_replace(
1296 + '/ src=/',
1297 + ' data-parsely-site="' . esc_attr( $parsely_options['apikey'] ) . '" src=',
1298 + $tag
1299 + );
1300 + }
1301 + return $tag;
639 1302 }
640 1303
641 1304 /**
642 - * Returns the value of a nested option.
1305 + * Print out the select tags
643 1306 *
644 - * @since 3.16.0
645 - *
646 - * @param string $option The option to get.
647 - * @param Parsely_Options $options The options to get the value from.
648 - * @return mixed The value of the nested option.
1307 + * @param array $args The arguments for the select drop downs.
649 1308 */
650 - public static function get_nested_option_value( $option, $options ) {
651 - $keys = explode( '[', str_replace( ']', '', $option ) );
652 - $value = $options;
1309 + public function print_select_tag( $args ) {
1310 + $options = $this->get_options();
1311 + $name = $args['option_key'];
1312 + $select_options = $args['select_options'];
1313 + if ( isset( $args['multiple'] ) ) {
1314 + $multiple = $args['multiple'];
1315 + } else {
1316 + $multiple = false;
1317 + }
1318 + $selected = isset( $options[ $name ] ) ? $options[ $name ] : null;
1319 + $id = esc_attr( $name );
1320 + $name = self::OPTIONS_KEY . "[$id]";
653 1321
654 - foreach ( $keys as $key ) {
655 - if ( isset( $value[ $key ] ) ) {
656 - $value = $value[ $key ];
657 - }
1322 + if ( isset( $args['help_text'] ) ) {
1323 + echo '<div class="parsely-form-controls" data-has-help-text="true">';
658 1324 }
1325 + if ( isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ) {
1326 + echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1327 + }
659 1328
660 - return $value;
661 - }
1329 + if ( $multiple ) {
1330 + echo sprintf( "<select multiple='multiple' name='%s[]'id='%s'", esc_attr( $name ), esc_attr( $name ) );
1331 + } else {
1332 + echo sprintf( "<select name='%s' id='%s'", esc_attr( $name ), esc_attr( $name ) );
1333 + }
662 1334
663 - /**
664 - * Sets the default values for the track_post_types and track_page_types
665 - * options.
666 - *
667 - * @since 3.9.0
668 - */
669 - public function set_default_track_as_values(): void {
670 - $this->option_defaults['track_page_types'] = array();
671 - $this->option_defaults['track_post_types'] = array();
1335 + echo '>';
672 1336
673 - $post_types = get_post_types( array( 'public' => true ) );
1337 + foreach ( $select_options as $key => $val ) {
1338 + echo '<option value="' . esc_attr( $key ) . '" ';
674 1339
675 - foreach ( $post_types as $post_type ) {
676 - if ( ! post_type_supports( $post_type, 'editor' ) ) {
677 - continue;
1340 + if ( $multiple ) {
1341 + $selected = in_array( $val, $options[ $args['option_key'] ], true );
1342 + echo selected( $selected, true, false ) . '>';
1343 + } else {
1344 + echo selected( $selected, $key, false ) . '>';
678 1345 }
1346 + echo esc_html( $val );
1347 + echo '</option>';
1348 + }
1349 + echo '</select>';
679 1350
680 - if ( is_post_type_hierarchical( $post_type ) ) {
681 - $this->option_defaults['track_page_types'][] = $post_type;
1351 + if ( isset( $args['help_text'] ) ) {
1352 + if ( isset( $args['help_link'] ) ) {
1353 + echo '<div class="help-text"> <p class="description">' .
1354 + sprintf( esc_html( $args['help_text'] ), '<a href="', esc_url( $args['help_link'] ), '">', '</a>' ) .
1355 + '</p></div>';
682 1356 } else {
683 - $this->option_defaults['track_post_types'][] = $post_type;
1357 + echo '<div class="help-text"> <p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
684 1358 }
685 1359 }
1360 + echo '</div>';
686 1361 }
687 1362
688 1363 /**
689 - * Sets the default value for the full_metadata_in_non_posts option.
1364 + * Print out the radio buttons
690 1365 *
691 - * @since 3.14.0
1366 + * @param array $args The arguments for the radio buttons.
692 1367 */
693 - public function set_default_full_metadata_in_non_posts(): void {
694 - $this->option_defaults['full_metadata_in_non_posts'] = true;
1368 + public function print_binary_radio_tag( $args ) {
1369 + $options = $this->get_options();
1370 + $name = $args['option_key'];
1371 + $value = $options[ $name ];
1372 + $id = esc_attr( $name );
1373 + $name = self::OPTIONS_KEY . "[$id]";
695 1374
696 - // Usage of any of these filters will result in the setting being set
697 - // to false.
698 - $filter_tags = array(
699 - 'wp_parsely_metadata',
700 - 'wp_parsely_post_tags',
701 - 'wp_parsely_permalink',
702 - 'wp_parsely_post_category',
703 - 'wp_parsely_pre_authors',
704 - 'wp_parsely_post_authors',
705 - 'wp_parsely_custom_taxonomies',
706 - 'wp_parsely_post_type',
707 - );
708 -
709 - foreach ( $filter_tags as $filter_tag ) {
710 - if ( has_filter( $filter_tag ) ) {
711 - $this->option_defaults['full_metadata_in_non_posts'] = false;
712 - break;
713 - }
1375 + $has_help_text = isset( $args['help_text'] ) ? ' data-has-help-text="true"' : '';
1376 + $requires_recrawl = isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ? ' data-requires-recrawl="true"' : '';
1377 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static text attribute key-value. ?>
1378 + <fieldset class="parsely-form-controls" <?php echo $has_help_text . $requires_recrawl; ?>>
1379 + <legend class="screen-reader-text"><span><?php echo esc_html( $args['title'] ); ?></span></legend>
1380 + <p>
1381 + <label for="<?php echo esc_attr( "{$id}_true" ); ?>">
1382 + <input type="radio" name="<?php echo esc_attr( $name ); ?>" id="<?php echo esc_attr( "{$id}_true" ); ?>" value="true"<?php checked( $value ); ?> />Yes
1383 + </label>
1384 + <br />
1385 + <label for="<?php echo esc_attr( "{$id}_false" ); ?>">
1386 + <input type="radio" name="<?php echo esc_attr( $name ); ?>" id="<?php echo esc_attr( "{$id}_false" ); ?>" value="false"<?php checked( $value, false ); ?> />No
1387 + </label>
1388 + </p>
1389 + <?php
1390 + if ( isset( $args['help_text'] ) ) {
1391 + ?><div class="help-text"><p class="description .help-text"><?php echo esc_html( $args['help_text'] ); ?></p></div><?php
714 1392 }
1393 + ?>
1394 + </fieldset>
1395 + <?php
715 1396 }
716 1397
717 1398 /**
718 - * Sets the default values for Content Intelligence options.
1399 + * Prints a checkbox tag in the settings page.
719 1400 *
720 - * Gives PCH access to all users having the edit_posts capability, to keep
721 - * consistent behavior with plugin versions prior to 3.16.0.
722 - *
723 - * @since 3.16.0
1401 + * @param array $args Arguments to print to checkbox tag.
724 1402 */
725 - public function set_default_content_helper_settings_values(): void {
726 - $this->option_defaults['content_helper'] =
727 - Permissions::build_pch_permissions_settings_array(
728 - true,
729 - array_keys( Permissions::get_user_roles_with_edit_posts_cap() )
730 - );
731 - }
1403 + public function print_checkbox_tag( $args ) {
1404 + $options = $this->get_options();
1405 + $name = $args['option_key'];
1406 + $value = $options[ $name ];
1407 + $id = esc_attr( $name );
1408 + $name = self::OPTIONS_KEY . "[$id]";
732 1409
733 - /**
734 - * Gets the URL of the plugin's settings page.
735 - *
736 - * @param int|null $_blog_id The Blog ID for the multisite subsite to use
737 - * for context (Default null for current).
738 - * @return string
739 - */
740 - public static function get_settings_url( ?int $_blog_id = null ): string {
741 - return get_admin_url( $_blog_id, 'admin.php?page=' . self::MENU_SLUG );
742 - }
1410 + if ( isset( $args['help_text'] ) ) {
1411 + echo '<div class="parsely-form-controls" data-has-help-text="true">';
1412 + }
1413 + if ( isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ) {
1414 + echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1415 + }
743 1416
744 - /**
745 - * Returns the URL of the Parse.ly dashboard for a specific page. If a page
746 - * is not specified, the home dashboard URL for the specified Site ID is
747 - * returned.
748 - *
749 - * @since 3.7.0
750 - *
751 - * @param string $site_id The Site ID for which to get the URL.
752 - * @param string $page_url Optional. The page for which to get the URL.
753 - * @return string The complete dashboard URL.
754 - */
755 - public static function get_dash_url( string $site_id, string $page_url = '' ): string {
756 - $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
1417 + echo sprintf( "<input type='checkbox' name='%s' id='%s_true' value='true' ", esc_attr( $name ), esc_attr( $id ) );
1418 + echo checked( true === $value, true, false );
1419 + echo sprintf( " /> <label for='%s_true'>Yes</label>", esc_attr( $id ) );
757 1420
758 - if ( '' !== $page_url ) {
759 - $page_url = self::get_url_with_itm_source( $page_url, null );
760 - $result .= '?url=' . rawurlencode( $page_url );
1421 + if ( isset( $args['help_text'] ) ) {
1422 + echo '<div class="help-text"><p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
761 1423 }
1424 + echo '</div>';
762 1425
763 - return $result;
764 1426 }
765 1427
766 1428 /**
767 - * Adds or replaces the itm_source parameter in the URL. Removes the
768 - * parameter if the passed value is null or an empty string.
1429 + * Print out the radio buttons
769 1430 *
770 - * @since 3.9.0
771 - *
772 - * @param string $url The URL to modify.
773 - * @param string|null $itm_source The value of the itm_source parameter.
774 - * @return string The resulting URL.
1431 + * @param array $args The arguments for text tags.
775 1432 */
776 - public static function get_url_with_itm_source( string $url, $itm_source ): string {
777 - if ( null === $itm_source || '' === $itm_source ) {
778 - return remove_query_arg( 'itm_source', $url );
1433 + public function print_text_tag( $args ) {
1434 + $options = $this->get_options();
1435 + $name = $args['option_key'];
1436 + $value = isset( $options[ $name ] ) ? $options[ $name ] : '';
1437 + $optional_args = isset( $args['optional_args'] ) ? $args['optional_args'] : array();
1438 + $id = esc_attr( $name );
1439 + $name = self::OPTIONS_KEY . "[$id]";
1440 + $value = esc_attr( $value );
1441 + $accepted_args = array( 'placeholder' );
1442 +
1443 + if ( isset( $args['help_text'] ) ) {
1444 + echo '<div class="parsely-form-controls" data-has-help-text="true">';
779 1445 }
1446 + if ( isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ) {
1447 + echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1448 + }
780 1449
781 - $itm_source = rawurlencode( $itm_source );
1450 + echo sprintf( "<input type='text' name='%s' id='%s' value='%s'", esc_attr( $name ), esc_attr( $id ), esc_attr( $value ) );
1451 + foreach ( $optional_args as $key => $val ) {
1452 + if ( in_array( $key, $accepted_args, true ) ) {
1453 + echo ' ' . esc_attr( $key ) . '="' . esc_attr( $val ) . '"';
1454 + }
1455 + }
1456 + echo ' />';
782 1457
783 - return add_query_arg( 'itm_source', $itm_source, $url );
1458 + if ( isset( $args['help_text'] ) ) {
1459 + if ( isset( $args['help_link'] ) ) {
1460 + echo ' <div class="help-text" id="' .
1461 + esc_attr( $args['option_key'] ) .
1462 + '_help_text"><p class="description">' .
1463 + sprintf( esc_html( $args['help_text'] ), '<a href="', esc_url( $args['help_link'] ), '">', '</a>' ) .
1464 + '</p>' .
1465 + '</div>';
1466 + } else {
1467 + echo ' <div class="help-text" id="' .
1468 + esc_attr( $args['option_key'] ) .
1469 + '_help_text"><p class="description">' .
1470 + esc_html( $args['help_text'] ) . '</p>' .
1471 + '</div>';
1472 + }
1473 + }
784 1474 }
785 1475
786 1476 /**
787 - * Checks to see if the current user is a member of the current blog.
788 - *
789 - * @return bool
1477 + * Returns default logo if one can be found
790 1478 */
791 - public function is_blog_member_logged_in(): bool {
792 - // Can't use $blog_id here because it futzes with the global $blog_id.
793 - $current_blog_id = get_current_blog_id();
794 - $current_user_id = get_current_user_id();
1479 + private function get_logo_default() {
1480 + $custom_logo_id = get_theme_mod( 'custom_logo' );
1481 + if ( $custom_logo_id ) {
1482 + $logo_attrs = wp_get_attachment_image_src( $custom_logo_id, 'full' );
1483 + if ( $logo_attrs ) {
1484 + return $logo_attrs[0];
1485 + }
1486 + }
795 1487
796 - return is_user_member_of_blog( $current_user_id, $current_blog_id );
1488 + // get_site_icon_url returns an empty string if one isn't found,
1489 + // which is what we want to use as the default anyway.
1490 + return get_site_icon_url();
797 1491 }
798 1492
799 1493 /**
800 - * Converts JSON-LD type to respective Parse.ly page type.
1494 + * Extracts a host ( not TLD ) from a URL
801 1495 *
802 - * If the JSON-LD type is one of the types Parse.ly supports as a "post",
803 - * then "post" will be returned. Otherwise, for "non-posts" and unknown
804 - * types, "index" is returned.
805 - *
806 - * @since 2.5.0
807 - *
808 - * @see https://docs.parse.ly/metatags/#h-field-description
809 - *
810 - * @param string $type JSON-LD type.
811 - * @return string "post" or "index".
1496 + * @param string $url The url of the host.
1497 + * @return string $url The host of the url…
812 1498 */
813 - public function convert_jsonld_to_parsely_type( string $type ): string {
814 - return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
1499 + private function get_host_from_url( $url ) {
1500 + if ( preg_match( '/^https?:\/\/( [^\/]+ )\/.*$/', $url, $matches ) ) {
1501 + return $matches[1];
1502 + }
1503 +
1504 + return $url;
815 1505 }
816 1506
817 1507 /**
818 - * Determines if a Site ID is saved in the options.
1508 + * Returns the tags associated with this page or post
819 1509 *
820 - * @since 2.6.0
821 - * @since 3.7.0 renamed from api_key_is_set.
822 - *
823 - * @return bool True is Site ID is set, false if it is missing.
1510 + * @param string $post_id The id of the post you're trying to get tags for.
1511 + * @return array $tags The tags of the post represented by the post id.
824 1512 */
825 - public function site_id_is_set(): bool {
826 - $options = $this->get_options();
1513 + private function get_tags( $post_id ) {
1514 + $tags = array();
1515 + $wp_tags = wp_get_post_tags( $post_id );
1516 + foreach ( $wp_tags as $wp_tag ) {
1517 + array_push( $tags, $wp_tag->name );
1518 + }
827 1519
828 - return '' !== $options['apikey'];
1520 + return $tags;
829 1521 }
830 1522
831 1523 /**
832 - * Determines if a Site ID is not saved in the options.
1524 + * Returns an array of all the child categories for the current post
833 1525 *
834 - * @since 2.6.0
835 - * @since 3.7.0 renamed from api_key_is_missing.
836 - *
837 - * @return bool True if Site ID is missing, false if it is set.
1526 + * @param string $post_id The id of the post you're trying to get categories for.
1527 + * @param string $delimiter What character will delimit the categories.
1528 + * @return array $tags all the child categories of the current post.
838 1529 */
839 - public function site_id_is_missing(): bool {
840 - return ! $this->site_id_is_set();
1530 + private function get_categories( $post_id, $delimiter = '/' ) {
1531 + $tags = array();
1532 + $categories = get_the_category( $post_id );
1533 + foreach ( $categories as $category ) {
1534 + $hierarchy = get_category_parents( $category, false, $delimiter );
1535 + $hierarchy = rtrim( $hierarchy, '/' );
1536 + array_push( $tags, $hierarchy );
1537 + }
1538 + // take last element in the hierarchy, a string representing the full parent->child tree,
1539 + // and split it into individual category names.
1540 + $tags = explode( '/', end( $tags ) );
1541 + // remove uncategorized value from tags.
1542 + $tags = array_diff( $tags, array( 'Uncategorized' ) );
1543 + return $tags;
841 1544 }
842 1545
843 1546 /**
844 - * Gets the Site ID if set.
845 - *
846 - * @since 2.6.0
847 - * @since 3.7.0 renamed from get_site_id.
848 - *
849 - * @return string Site ID if set, or empty string if not.
1547 + * Safely returns options for the plugin by assigning defaults contained in optionDefaults. As soon as actual
1548 + * options are saved, they override the defaults. This prevents us from having to do a lot of isset() checking
1549 + * on variables.
850 1550 */
851 - public function get_site_id(): string {
852 - $options = $this->get_options();
853 -
854 - return $this->site_id_is_set() ? $options['apikey'] : '';
1551 + private function get_options() {
1552 + $options = get_option( self::OPTIONS_KEY );
1553 + if ( false === $options ) {
1554 + $options = $this->option_defaults;
1555 + } else {
1556 + $options = array_merge( $this->option_defaults, $options );
1557 + }
1558 + return $options;
855 1559 }
856 1560
857 1561 /**
858 - * Returns whether the API Secret is set in the plugin's options.
1562 + * Returns a properly cleaned category/taxonomy value and will optionally use the top-level category/taxonomy value
1563 + * if so instructed via the `use_top_level_cats` option.
859 1564 *
860 - * @since 3.4.0
861 - *
862 - * @return bool True if the API Secret is set, false if not set.
1565 + * @param WP_Post $post_obj The object for the post.
1566 + * @param array $parsely_options The parsely options.
1567 + * @return string $category Cleaned category name for for post in question.
863 1568 */
864 - public function api_secret_is_set(): bool {
865 - $options = $this->get_options();
1569 + private function get_category_name( $post_obj, $parsely_options ) {
1570 + $taxonomy_dropdown_choice = get_the_terms( $post_obj->ID, $parsely_options['custom_taxonomy_section'] );
1571 + // Get top-level taxonomy name for chosen taxonomy and assign to $parent_name; it will be used
1572 + // as the category value if 'use_top_level_cats' option is checked.
1573 + // Assign as "Uncategorized" if no value is checked for the chosen taxonomy.
1574 + $category = 'Uncategorized';
1575 + if ( ! empty( $taxonomy_dropdown_choice ) ) {
1576 + if ( $parsely_options['use_top_level_cats'] ) {
1577 + $first_term = array_shift( $taxonomy_dropdown_choice );
1578 + $term_name = $this->get_top_level_term( $first_term->term_id, $first_term->taxonomy );
1579 + } else {
1580 + $term_name = $this->get_bottom_level_term( $post_obj->ID, $parsely_options['custom_taxonomy_section'] );
1581 + }
866 1582
867 - return '' !== $options['api_secret'];
1583 + if ( $term_name ) {
1584 + $category = $term_name;
1585 + }
1586 + }
1587 +
1588 + /**
1589 + * Filters the constructed category name that are used as metadata keywords.
1590 + *
1591 + * @since 1.8.0
1592 + *
1593 + * @param string $category Category name.
1594 + * @param WP_Post $post_obj Post object.
1595 + * @param array $parsely_options The Parsely options.
1596 + */
1597 + $category = apply_filters( 'wp_parsely_post_category', $category, $post_obj, $parsely_options );
1598 + $category = $this->get_clean_parsely_page_value( $category );
1599 + return $category;
868 1600 }
869 1601
870 1602 /**
871 - * Returns the API Secret stored in the plugin's options.
1603 + * Return the top-most category/taxonomy value in a hierarcy given a taxonomy value's ID
1604 + * ( WordPress calls taxonomy values 'terms' ).
872 1605 *
873 - * @since 3.4.0
874 - *
875 - * @return string The API Secret, empty string if the API secret is not set.
1606 + * @param string $term_id The id of the top level term.
1607 + * @param string $taxonomy_name The name of the taxonomy.
1608 + * @return string $parent The top level name of the category / taxonomy.
876 1609 */
877 - public function get_api_secret(): string {
878 - $options = $this->get_options();
879 -
880 - return $this->api_secret_is_set() ? $options['api_secret'] : '';
1610 + private function get_top_level_term( $term_id, $taxonomy_name ) {
1611 + $parent = get_term_by( 'id', $term_id, $taxonomy_name );
1612 + while ( false !== $parent && 0 !== $parent->parent ) {
1613 + $parent = get_term_by( 'id', $parent->parent, $taxonomy_name );
1614 + }
1615 + return $parent ? $parent->name : false;
881 1616 }
882 1617
883 1618 /**
884 - * Returns all supported post and non-post types.
1619 + * Return the bottom-most category/taxonomy value in a hierarcy given a post ID
1620 + * ( WordPress calls taxonomy values 'terms' ).
885 1621 *
886 - * @since 3.7.0
887 - *
888 - * @return string[] all supported types
1622 + * @param string $post_id The post id you're interested in.
1623 + * @param string $taxonomy_name The name of the taxonomy.
1624 + * @return string name of the custom taxonomy.
889 1625 */
890 - public function get_all_supported_types(): array {
891 - return self::$all_supported_types;
1626 + private function get_bottom_level_term( $post_id, $taxonomy_name ) {
1627 + $terms = get_the_terms( $post_id, $taxonomy_name );
1628 + $term_ids = is_array( $terms ) ? wp_list_pluck( $terms, 'term_id' ) : null;
1629 + $parents = is_array( $terms ) ? array_filter( wp_list_pluck( $terms, 'parent' ) ) : null;
1630 +
1631 + // Get array of IDs of terms which are not parents.
1632 + $term_ids_not_parents = array_diff( $term_ids, $parents );
1633 + // Get corresponding term objects, which are mapped to array index keys.
1634 + $terms_not_parents = array_intersect_key( $terms, $term_ids_not_parents );
1635 + // remove array index keys.
1636 + $terms_not_parents_cleaned = array();
1637 + foreach ( $terms_not_parents as $index => $value ) {
1638 + array_push( $terms_not_parents_cleaned, $value );
1639 + }
1640 + // if you assign multiple child terms in a custom taxonomy, will only return the first.
1641 + return $terms_not_parents_cleaned[0]->name;
892 1642 }
893 1643
894 1644 /**
895 - * Gets all tracked post types.
1645 + * Get all term values from custom taxonomies.
896 1646 *
897 - * @since 3.7.0
898 - *
899 - * @return array<string>
1647 + * @param WP_Post $post_obj The post object.
1648 + * @param array $parsely_options The pparsely options.
900 1649 */
901 - public function get_all_track_types(): array {
902 - $options = $this->get_options();
1650 + private function get_custom_taxonomy_values( $post_obj, $parsely_options ) {
1651 + // filter out default WordPress taxonomies.
1652 + $all_taxonomies = array_diff( get_taxonomies(), array( 'post_tag', 'nav_menu', 'author', 'link_category', 'post_format' ) );
1653 + $all_values = array();
903 1654
904 - return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
1655 + if ( is_array( $all_taxonomies ) ) {
1656 + foreach ( $all_taxonomies as $taxonomy ) {
1657 + $custom_taxonomy_objects = get_the_terms( $post_obj->ID, $taxonomy );
1658 + if ( is_array( $custom_taxonomy_objects ) ) {
1659 + foreach ( $custom_taxonomy_objects as $custom_taxonomy_object ) {
1660 + array_push( $all_values, $custom_taxonomy_object->name );
1661 + }
1662 + }
1663 + }
1664 + }
1665 + return $all_values;
905 1666 }
906 1667
907 1668 /**
908 - * Gets default options.
1669 + * Returns a list of coauthors for a post assuming the coauthors plugin is
1670 + * installed. Borrowed from
1671 + * https://github.com/Automattic/Co-Authors-Plus/blob/master/template-tags.php#L3-35
909 1672 *
910 - * @since 3.8.0
911 - *
912 - * @return Parsely_Options
1673 + * @param string $post_id The id of the post.
913 1674 */
914 - public function get_default_options() {
915 - return $this->option_defaults;
1675 + private function get_coauthor_names( $post_id ) {
1676 + $coauthors = array();
1677 + if ( class_exists( 'coauthors_plus' ) ) {
1678 + global $post, $post_ID, $coauthors_plus, $wpdb;
1679 +
1680 + $post_id = (int) $post_id;
1681 + if ( ! $post_id && $post_ID ) {
1682 + $post_id = $post_ID;
1683 + }
1684 +
1685 + if ( ! $post_id && $post ) {
1686 + $post_id = $post->ID;
1687 + }
1688 +
1689 + if ( $post_id ) {
1690 + $coauthor_terms = get_the_terms( $post_id, $coauthors_plus->coauthor_taxonomy );
1691 +
1692 + if ( is_array( $coauthor_terms ) && ! empty( $coauthor_terms ) ) {
1693 + foreach ( $coauthor_terms as $coauthor ) {
1694 + $coauthor_slug = preg_replace( '#^cap\-#', '', $coauthor->slug );
1695 + $post_author = $coauthors_plus->get_coauthor_by( 'user_nicename', $coauthor_slug );
1696 + // In case the user has been deleted while plugin was deactivated.
1697 + if ( ! empty( $post_author ) ) {
1698 + $coauthors[] = $post_author;
1699 + }
1700 + }
1701 + } elseif ( ! $coauthors_plus->force_guest_authors ) {
1702 + if ( $post && $post_id === $post->ID ) {
1703 + $post_author = get_userdata( $post->post_author );
1704 + }
1705 + if ( ! empty( $post_author ) ) {
1706 + $coauthors[] = $post_author;
1707 + }
1708 + } // the empty else case is because if we force guest authors, we don't ever care what value wp_posts.post_author has.
1709 + }
1710 + }
1711 + return $coauthors;
916 1712 }
917 1713
918 1714 /**
919 - * Returns the credentials that are being managed at the platform level.
1715 + * Determine author name from display name, falling back to firstname
1716 + * lastname, then nickname and finally the nicename.
920 1717 *
921 - * @since 3.9.0
922 - * @access private
923 - *
924 - * @return Parsely_Options|array<empty> The managed credentials.
1718 + * @param WP_User $author The author of the post.
925 1719 */
926 - private function get_managed_credentials() {
927 - if ( true !== $this->are_credentials_managed ) {
928 - return array();
1720 + private function get_author_name( $author ) {
1721 + // gracefully handle situation where no author is available.
1722 + if ( empty( $author ) || ! is_object( $author ) ) {
1723 + return '';
929 1724 }
930 -
931 - $credentials = apply_filters( 'wp_parsely_credentials', array() );
932 -
933 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
934 - return array();
1725 + $author_name = $author->display_name;
1726 + if ( ! empty( $author_name ) ) {
1727 + return $author_name;
935 1728 }
936 1729
937 - $result = array();
938 -
939 - if ( isset( $credentials['site_id'] ) ) {
940 - $result['apikey'] = $credentials['site_id'];
1730 + $author_name = $author->user_firstname . ' ' . $author->user_lastname;
1731 + if ( ' ' !== $author_name ) {
1732 + return $author_name;
941 1733 }
942 1734
943 - if ( isset( $credentials['api_secret'] ) ) {
944 - $result['api_secret'] = $credentials['api_secret'];
1735 + $author_name = $author->nickname;
1736 + if ( ! empty( $author_name ) ) {
1737 + return $author_name;
945 1738 }
946 1739
947 - if ( isset( $credentials['metadata_secret'] ) ) {
948 - $result['metadata_secret'] = $credentials['metadata_secret'];
949 - }
950 -
951 - return $result;
1740 + return $author->user_nicename;
952 1741 }
953 1742
954 1743 /**
955 - * Returns whether credentials are being managed at the platform level.
1744 + * Retrieve all the authors for a post as an array. Can include multiple
1745 + * authors if coauthors plugin is in use.
956 1746 *
957 - * @since 3.9.0
958 - * @access private
959 - *
960 - * @return bool Whether credentials are being managed at the platform level.
1747 + * @param WP_Post $post The post object.
1748 + * @return array
961 1749 */
962 - private function are_credentials_managed(): bool {
963 - $credentials = apply_filters( 'wp_parsely_credentials', array() );
1750 + private function get_author_names( $post ) {
1751 + $authors = $this->get_coauthor_names( $post->ID );
1752 + if ( empty( $authors ) ) {
1753 + $authors = array( get_user_by( 'id', $post->post_author ) );
1754 + }
964 1755
965 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
966 - return false;
967 - }
1756 + /**
1757 + * Filters the list of author WP_User objects for a post.
1758 + *
1759 + * @since 1.14.0
1760 + *
1761 + * @param array $authors One or more authors as WP_User objects (may also be `false`).
1762 + * @param WP_Post $post Post object.
1763 + */
1764 + $authors = apply_filters( 'wp_parsely_pre_authors', $authors, $post );
968 1765
969 - return $credentials['is_managed'] ?? false;
1766 + $authors = array_map( array( $this, 'get_author_name' ), $authors );
1767 +
1768 + /**
1769 + * Filters the list of author names for a post.
1770 + *
1771 + * @since 1.14.0
1772 + *
1773 + * @param string[] $authors One or more author names.
1774 + * @param WP_Post $post Post object.
1775 + */
1776 + $authors = apply_filters( 'wp_parsely_post_authors', $authors, $post );
1777 + $authors = array_map( array( $this, 'get_clean_parsely_page_value' ), $authors );
1778 + return $authors;
970 1779 }
971 1780
972 1781 /**
973 - * Sets the values of managed options.
1782 + * Sanitize content
974 1783 *
975 - * This function won't accept managing credentials or certain plugin options
976 - * that are being managed through other means. For managing credentials,
977 - * please use the `wp_parsely_credentials` filter.
978 - *
979 - * @since 3.9.0
980 - * @access private
1784 + * @param string $val The content you'd like sanitized.
1785 + * @return string
981 1786 */
982 - private function set_managed_options(): void {
983 - $managed_options = apply_filters( 'wp_parsely_managed_options', false );
984 -
985 - if ( ! is_array( $managed_options ) ) {
986 - return;
1787 + private function get_clean_parsely_page_value( $val ) {
1788 + if ( is_string( $val ) ) {
1789 + $val = str_replace( "\n", '', $val );
1790 + $val = str_replace( "\r", '', $val );
1791 + $val = wp_strip_all_tags( $val );
1792 + $val = trim( $val );
1793 + return $val;
987 1794 }
988 1795
989 - // Don't allow certain options to be set as managed.
990 - unset(
991 - $managed_options['apikey'],
992 - $managed_options['api_secret'],
993 - $managed_options['metadata_secret'],
994 - $managed_options['track_post_types'],
995 - $managed_options['track_page_types'],
996 - $managed_options['plugin_version']
997 - );
1796 + return $val;
1797 + }
998 1798
999 - if ( 0 === count( $managed_options ) ) {
1000 - return;
1001 - }
1002 1799
1003 - /**
1004 - * Current options.
1005 - *
1006 - * @var Parsely_Options $current_options
1007 - */
1008 - $current_options = get_option( self::OPTIONS_KEY, array() );
1800 + /**
1801 + * Get the URL of the plugin settings page
1802 + */
1803 + private function get_settings_url() {
1804 + return admin_url( 'options-general.php?page=' . self::MENU_SLUG );
1805 + }
1009 1806
1010 - // Set managed options values.
1011 - foreach ( $managed_options as $key => $value ) {
1012 - $is_option_valid = isset( $this->option_defaults[ $key ] );
1013 1807
1014 - if ( $is_option_valid ) {
1015 - if ( null === $value ) {
1016 - // When null, the option gets its value from the database.
1017 - $this->managed_options[ $key ] =
1018 - $current_options[ $key ] ?? $this->option_defaults[ $key ];
1019 - } else {
1020 - $this->managed_options[ $key ] =
1021 - $this->sanitize_managed_option( $key, $value );
1022 - }
1023 - }
1024 - }
1025 - }
1026 -
1027 1808 /**
1028 - * Gets the Parse.ly canonical URL for a given post.
1809 + * Get the URL of the current PHP script.
1810 + * A fall-back implementation to determine permalink
1029 1811 *
1030 - * @since 3.19.0
1031 - *
1032 - * @param WP_Post|int $post The post ID or post object.
1033 - * @return string The Parse.ly canonical URL.
1812 + * @param string $parsely_type Optional. Parse.ly post type you're interested in, either 'post' or 'nonpost'. Default is 'nonpost'.
1813 + * @param int $post_id Optional. ID of the post you want to get the URL for. Default is 0, which means the global `$post` is used.
1814 + * @return string|void
1034 1815 */
1035 - public static function get_canonical_url_from_post( $post ): string {
1036 - $post_id = is_int( $post ) ? $post : $post->ID;
1037 - $canonical_url = get_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, true );
1816 + public function get_current_url( $parsely_type = 'nonpost', $post_id = 0 ) {
1817 + if ( 'post' === $parsely_type ) {
1818 + $permalink = get_permalink( $post_id );
1038 1819
1039 - if ( null !== $canonical_url && is_string( $canonical_url ) && '' !== $canonical_url ) {
1040 - return self::get_canonical_url( $canonical_url );
1820 + /**
1821 + * Filters the permalink for a post.
1822 + *
1823 + * @since 1.14.0
1824 + * @since 2.5.0 Added $post_id.
1825 + *
1826 + * @param string $permalink The permalink URL or false if post does not exist.
1827 + * @param string $parsely_type Parse.ly type ("post" or "nonpost").
1828 + * @param int $post_id ID of the post you want to get the URL for. May be 0, so $permalink will be
1829 + * for the global $post.
1830 + */
1831 + $url = apply_filters( 'wp_parsely_permalink', $permalink, $parsely_type, $post_id );
1832 + } else {
1833 + $request_uri = isset( $_SERVER['REQUEST_URI'] )
1834 + ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
1835 + : '';
1836 +
1837 + $url = home_url( $request_uri );
1041 1838 }
1042 1839
1043 - $permalink = get_permalink( $post );
1840 + $options = $this->get_options();
1841 + return $options['force_https_canonicals']
1842 + ? str_replace( 'http://', 'https://', $url )
1843 + : str_replace( 'https://', 'http://', $url );
1844 + }
1044 1845
1045 - if ( false === $permalink ) {
1046 - return __( 'no permalink', 'wp-parsely' );
1846 + /**
1847 + * Get the first image from a post
1848 + * https://css-tricks.com/snippets/wordpress/get-the-first-image-from-a-post/
1849 + *
1850 + * @param WP_Post $post The post object you're interested in.
1851 + * @return mixed|string
1852 + */
1853 + public function get_first_image( $post ) {
1854 + ob_start();
1855 + ob_end_clean();
1856 + if ( preg_match_all( '/<img.+src=[\'"]( [^\'"]+ )[\'"].*>/i', $post->post_content, $matches ) ) {
1857 + return $matches[1][0];
1047 1858 }
1048 -
1049 - return self::get_canonical_url( $permalink );
1859 + return '';
1050 1860 }
1051 1861
1052 1862 /**
1053 - * Returns the canonical version of the passed URL.
1863 + * Add parsely tracking to facebook instant articles
1054 1864 *
1055 - * In this context, the canonical URL is the URL containing the Site ID as
1056 - * its domain. If the Site ID differs from the real domain, the
1057 - * `wp_parsely_canonical_url_domain` filter can be used to set it.
1058 - *
1059 - * @since 3.19.0
1060 - * @since 3.20.4 Made the domain overridable.
1061 - *
1062 - * @param string $url The URL to get the canonical URL for.
1063 - * @return string The canonical URL.
1865 + * @param type $registry The registry info for fbia.
1866 + * @return string
1064 1867 */
1065 - public static function get_canonical_url( string $url ): string {
1066 - $canonical_url_domain = apply_filters(
1067 - 'wp_parsely_canonical_url_domain',
1068 - null
1868 + public function insert_parsely_tracking_fbia( &$registry ) {
1869 + $options = $this->get_options();
1870 + $display_name = 'Parsely Analytics'; // Do not translate at this time.
1871 + $identifier = 'parsely-analytics-for-wordpress';
1872 +
1873 + $embed_code = '<script>
1874 + PARSELY = {
1875 + autotrack: false,
1876 + onload: function() {
1877 + PARSELY.beacon.trackPageView({
1878 + urlref: \'http://facebook.com/instantarticles\'
1879 + });
1880 + return true;
1881 + }
1882 + }
1883 + </script>
1884 + <script data-cfasync="false" id="parsely-cfg" data-parsely-site="' . esc_attr( $options['apikey'] ) . '" src="https://cdn.parsely.com/keys/' . esc_attr( $options['apikey'] ) . '/p.js"></script>
1885 + <!-- END Parse.ly Include: Standard -->';
1886 +
1887 + $registry[ $identifier ] = array(
1888 + 'name' => $display_name,
1889 + 'payload' => $embed_code,
1069 1890 );
1070 1891
1071 - // Handle domain override.
1072 - if ( is_string( $canonical_url_domain ) ) {
1073 - // Get the canonical URL domain without protocol, trailing slashes
1074 - // or accidental whitespace.
1075 - $canonical_url_domain = rtrim( trim( $canonical_url_domain ), '/' );
1076 - $canonical_url_domain = preg_replace( '#^https?://#', '', $canonical_url_domain );
1892 + return $embed_code;
1893 + }
1077 1894
1078 - if ( is_string( $canonical_url_domain ) && '' !== $canonical_url_domain ) {
1079 - $url_domain = (string) wp_parse_url( $url, PHP_URL_HOST );
1080 - return str_replace( $url_domain, $canonical_url_domain, $url );
1081 - }
1895 + /**
1896 + * Add amp actions.
1897 + */
1898 + public function parsely_add_amp_actions() {
1899 + if ( ! function_exists( 'is_amp_endpoint' ) || ! is_amp_endpoint() ) {
1900 + return '';
1082 1901 }
1083 1902
1084 - $site_id = \Parsely\get_parsely()->get_site_id();
1085 - $home_url_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
1903 + $options = $this->get_options();
1086 1904
1087 - if ( $home_url_host === $site_id ) {
1088 - // URL does not need to be modified.
1089 - return $url;
1905 + if ( $options['disable_amp'] ) {
1906 + return '';
1090 1907 }
1091 1908
1092 - // Return the URL with the Site ID as the domain.
1093 - return str_replace( $home_url_host, $site_id, $url );
1909 + add_filter( 'amp_post_template_analytics', array( $this, 'parsely_add_amp_analytics' ) );
1910 + add_filter( 'amp_analytics_entries', array( $this, 'parsely_add_amp_native_analytics' ) );
1094 1911 }
1095 1912
1096 1913 /**
1097 - * Sets the Parse.ly canonical URL for a post.
1914 + * Add amp analytics.
1098 1915 *
1099 - * @since 3.19.0
1100 - *
1101 - * @param WP_Post|int $post The post object or post ID.
1102 - * @param string $url The canonical URL.
1103 - * @return bool True if the canonical URL was set, false otherwise.
1916 + * @param type $analytics The analytics object you want to add.
1917 + * @return type
1104 1918 */
1105 - public static function set_canonical_url( $post, string $url ): bool {
1106 - $post_id = is_int( $post ) ? $post : $post->ID;
1107 - $canonical_url = self::get_canonical_url( $url );
1919 + public function parsely_add_amp_analytics( $analytics ) {
1920 + $options = $this->get_options();
1108 1921
1109 - return false !== update_post_meta(
1110 - $post_id,
1111 - self::PARSELY_CANONICAL_URL_META_KEY,
1112 - sanitize_url( $canonical_url, array( 'http', 'https' ) )
1922 + if ( empty( $options['apikey'] ) ) {
1923 + return $analytics;
1924 + }
1925 +
1926 + $analytics['parsely'] = array(
1927 + 'type' => 'parsely',
1928 + 'attributes' => array(),
1929 + 'config_data' => array(
1930 + 'vars' => array(
1931 + 'apikey' => $options['apikey'],
1932 + ),
1933 + ),
1113 1934 );
1935 +
1936 + return $analytics;
1114 1937 }
1115 1938
1116 1939 /**
1117 - * Sanitizes the value of the passed managed option.
1940 + * Add amp native analytics.
1118 1941 *
1119 - * @since 3.9.0
1120 - * @access private
1121 - *
1122 - * @param string $option_id The option's ID.
1123 - * @param bool|string $value The option's value.
1124 - * @return bool|string The sanitized option value.
1942 + * @param type $analytics The analytics object you want to add.
1943 + * @return string|type
1125 1944 */
1126 - private function sanitize_managed_option( string $option_id, $value ) {
1127 - $option_value_type = gettype( $this->option_defaults[ $option_id ] );
1945 + public function parsely_add_amp_native_analytics( $analytics ) {
1946 + $options = $this->get_options();
1128 1947
1129 - if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
1130 - _doing_it_wrong(
1131 - __FUNCTION__,
1132 - esc_html(
1133 - sprintf( /* translators: 1: Option ID */
1134 - __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
1135 - $option_id
1136 - )
1137 - ),
1138 - ''
1139 - );
1948 + if ( ! empty( $options['disable_amp'] ) && true === $options['disable_amp'] ) {
1949 + return '';
1950 + }
1140 1951
1141 - return false;
1952 + if ( empty( $options['apikey'] ) ) {
1953 + return $analytics;
1142 1954 }
1143 1955
1144 - if ( 'string' === $option_value_type ) {
1145 - if ( ! is_string( $value ) ) {
1146 - _doing_it_wrong(
1147 - __FUNCTION__,
1148 - esc_html(
1149 - sprintf( /* translators: 1: Option ID */
1150 - __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
1151 - $option_id
1152 - )
1956 + $analytics['parsely'] = array(
1957 + 'type' => 'parsely',
1958 + 'attributes' => array(),
1959 + 'config' => wp_json_encode(
1960 + array(
1961 + 'vars' => array(
1962 + 'apikey' => $options['apikey'],
1153 1963 ),
1154 - ''
1155 - );
1964 + )
1965 + ),
1966 + );
1156 1967
1157 - $value = strval( $value );
1158 - }
1968 + return $analytics;
1969 + }
1159 1970
1160 - // String options that are restricted to specific values.
1161 - $restricted_value_options = array(
1162 - 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
1163 - 'meta_type' => array( 'json_ld', 'repeated_metas' ),
1164 - );
1971 + /**
1972 + * Check to see if parsely user is logged in
1973 + */
1974 + public function parsely_is_user_logged_in() {
1975 + // can't use $blog_id here because it futzes with the global $blog_id.
1976 + $current_blog_id = get_current_blog_id();
1977 + $current_user_id = get_current_user_id();
1978 + return is_user_member_of_blog( $current_user_id, $current_blog_id );
1979 + }
1165 1980
1166 - // Verify that the above values are respected.
1167 - foreach ( $restricted_value_options as $option_key => $valid_values ) {
1168 - if ( $option_id === $option_key ) {
1169 - if ( ! in_array( $value, $valid_values, true ) ) {
1170 - _doing_it_wrong(
1171 - __FUNCTION__,
1172 - esc_html(
1173 - sprintf( /* translators: 1: Option value 2: Option ID */
1174 - __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
1175 - $value,
1176 - $option_id
1177 - )
1178 - ),
1179 - ''
1180 - );
1981 + /**
1982 + * Why is this here?
1983 + */
1984 + public function return_personalized_json() {
1181 1985
1182 - $value = $this->option_defaults[ $option_id ];
1183 - }
1184 - }
1185 - }
1186 - }
1187 -
1188 - return $value;
1189 1986 }
1190 1987
1191 1988 /**
1192 - * Allows remote requests to Parse.ly.
1989 + * Convert JSON-LD type to respective Parse.ly page type.
1193 1990 *
1194 - * This is needed for environments, such as wp-now, that block remote requests.
1991 + * If the JSON-LD type is one of the types Parse.ly supports as a "post", then "post" will be returned.
1992 + * Otherwise, for "non-posts" and unknown types, "index" is returned.
1195 1993 *
1196 - * @since 3.13.0
1197 - * @access private
1994 + * @since 2.5.0
1995 + *
1996 + * @see https://www.parse.ly/help/integration/metatags#field-description
1997 + *
1998 + * @param $type string JSON-LD type.
1999 + * @return string "post" or "index".
1198 2000 */
1199 - private function allow_parsely_remote_requests(): void {
1200 - $allowed_urls = array(
1201 - self::DASHBOARD_BASE_URL,
1202 - Content_API_Service::get_base_url(),
1203 - Suggestions_API_Service::get_base_url(),
1204 - );
1205 -
1206 - add_filter(
1207 - 'http_request_host_is_external',
1208 - function ( bool $external, string $host, string $url ) use ( $allowed_urls ) {
1209 - // Check if the URL matches any URLs on the allowed list.
1210 - foreach ( $allowed_urls as $allowed_url ) {
1211 - if ( Utils::str_starts_with( $url, $allowed_url ) ) {
1212 - return true;
1213 - }
1214 - }
1215 - return $external;
1216 - },
1217 - 10,
1218 - 3
1219 - );
2001 + public function convert_jsonld_to_parsely_type( $type ) {
2002 + return in_array( $type, $this->supported_jsonld_post_types ) ? 'post' : 'index';
1220 2003 }
1221 2004 }