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 +1670 -740 3.18.12.5.1 View file →
@@ -2,158 +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 - * track_authenticated_users: bool,
36 - * lowercase_tags: bool,
37 - * force_https_canonicals: bool,
38 - * track_post_types: string[],
39 - * track_page_types: string[],
40 - * track_post_types_as?: array<string, string>,
41 - * full_metadata_in_non_posts: bool,
42 - * disable_javascript: bool,
43 - * disable_amp: bool,
44 - * meta_type: string,
45 - * logo: string,
46 - * metadata_secret: string,
47 - * disable_autotrack: bool,
48 - * plugin_version: string,
49 - * }
50 - *
51 - * @phpstan-type Parsely_Options_Content_Helper array{
52 - * ai_features_enabled: bool,
53 - * smart_linking: Parsely_Options_Content_Helper_Feature,
54 - * title_suggestions: Parsely_Options_Content_Helper_Feature,
55 - * excerpt_suggestions: Parsely_Options_Content_Helper_Feature,
56 - * }
57 - *
58 - * @phpstan-type Parsely_Options_Content_Helper_Feature array{
59 - * enabled: bool,
60 - * allowed_user_roles: string[],
61 - * }
62 - *
63 - * @phpstan-type WP_HTTP_Request_Args array{
64 - * method?: string,
65 - * timeout?: float,
66 - * blocking?: bool,
67 - * headers?: array<string, string>,
68 - * body?: string,
69 - * data_format?: string,
70 - * }
71 - *
72 - * @phpstan-import-type Metadata_Attributes from Metadata
73 16 */
74 17 class Parsely {
75 18 /**
76 19 * Declare our constants
77 - */
78 - public const VERSION = PARSELY_VERSION;
79 - public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php.
80 - public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
81 - public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
82 - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
83 -
84 - /**
85 - * The Content API service.
86 20 *
87 - * @var ?Content_API_Service $content_api_service
21 + * @codeCoverageIgnoreStart
88 22 */
89 - 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.
90 27
91 28 /**
92 - * The Suggestions API service.
29 + * Declare some class propeties
93 30 *
94 - * @var ?Suggestions_API_Service $suggestions_api_service
31 + * @var array $option_defaults The defaults we need for the class.
95 32 */
96 - private $suggestions_api_service;
97 -
98 - /**
99 - * The Parse.ly internal REST API controller.
100 - *
101 - * @var REST_API_Controller|null $rest_api_controller
102 - */
103 - private $rest_api_controller;
104 -
105 - /**
106 - * Declare some class properties
107 - *
108 - * @var Parsely_Options $option_defaults The defaults we need for the class.
109 - */
110 33 private $option_defaults = array(
111 - 'apikey' => '',
112 - 'content_id_prefix' => '',
113 - 'api_secret' => '',
114 - 'use_top_level_cats' => false,
115 - 'custom_taxonomy_section' => 'category',
116 - 'cats_as_tags' => false,
117 - 'content_helper' => array(
118 - 'ai_features_enabled' => true,
119 - 'smart_linking' => array(
120 - 'enabled' => true,
121 - 'allowed_user_roles' => array( 'administrator' ),
122 - ),
123 - 'title_suggestions' => array(
124 - 'enabled' => true,
125 - 'allowed_user_roles' => array( 'administrator' ),
126 - ),
127 - 'excerpt_suggestions' => array(
128 - 'enabled' => true,
129 - 'allowed_user_roles' => array( 'administrator' ),
130 - ),
131 - ),
132 - 'track_authenticated_users' => false,
133 - 'lowercase_tags' => true,
134 - 'force_https_canonicals' => false,
135 - 'track_post_types' => array(),
136 - 'track_page_types' => array(),
137 - 'full_metadata_in_non_posts' => true,
138 - 'disable_javascript' => false,
139 - 'disable_amp' => false,
140 - 'meta_type' => 'json_ld',
141 - 'logo' => '',
142 - 'metadata_secret' => '',
143 - 'disable_autotrack' => false,
144 - '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,
145 51 );
146 52
147 53 /**
148 54 * Declare post types that Parse.ly will process as "posts".
149 55 *
56 + * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
57 + *
150 58 * @since 2.5.0
151 59 * @var string[]
152 - *
153 - * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
154 60 */
155 - public const SUPPORTED_JSONLD_POST_TYPES = array(
61 + private $supported_jsonld_post_types = array(
156 62 'NewsArticle',
157 63 'Article',
158 64 'TechArticle',
159 65 'BlogPosting',
@@ -160,28 +66,19 @@
160 66 'LiveBlogPosting',
161 67 'Report',
162 68 'Review',
163 69 'CreativeWork',
164 - 'OpinionNewsArticle',
165 - 'AnalysisNewsArticle',
166 - 'BackgroundNewsArticle',
167 - 'ReviewNewsArticle',
168 - 'ReportageNewsArticle',
169 - 'Recipe',
170 - 'AdvertiserContentArticle',
171 - 'MedicalWebPage',
172 - 'PodcastEpisode',
173 70 );
174 71
175 72 /**
176 73 * Declare post types that Parse.ly will process as "non-posts".
177 74 *
75 + * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
76 + *
178 77 * @since 2.5.0
179 78 * @var string[]
180 - *
181 - * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
182 79 */
183 - public const SUPPORTED_JSONLD_NON_POST_TYPES = array(
80 + private $supported_jsonld_non_post_types = array(
184 81 'WebPage',
185 82 'Event',
186 83 'Hotel',
187 84 'Restaurant',
@@ -188,887 +85,1920 @@
188 85 'Movie',
189 86 );
190 87
191 88 /**
192 - * Declare all supported types (both post and non-post types).
89 + * The constructor
193 90 *
194 - * @since 3.7.0
195 - * @var string[]
91 + * @category Function
92 + * @package Parsely
196 93 */
197 - 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 + }
198 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 +
199 135 /**
200 - * Returns whether credentials are being managed at the platform level.
136 + * Adds 10 minute cron interval
201 137 *
202 - * This allows hosting providers to provide a more customized experience for
203 - * 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
204 157 *
205 - * @since 3.9.0
206 - * @access private
207 - * @var bool
158 + * @category Function
159 + * @package Parsely
208 160 */
209 - 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 +';
210 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 +
211 179 /**
212 - * Holds the managed options and their values.
180 + * Parsely settings page in WordPress settings menu.
213 181 *
214 - * This allows hosting providers to provide a more customized experience for
215 - * 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] )
216 197 *
217 - * @since 3.9.0
218 - * @access private
219 - * @var array<empty>|array<string, bool|string|null>
198 + * @category Function
199 + * @package Parsely
220 200 */
221 - 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 + }
222 205
206 + include PARSELY_PLUGIN_DIR . 'src/parsely-settings.php';
207 + }
208 +
223 209 /**
224 - * Constructor.
210 + * Initialize the settings for Parsely
211 + *
212 + * @category Function
213 + * @package Parsely
225 214 */
226 - public function __construct() {
227 - 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 + );
228 223
229 - $this->are_credentials_managed = $this->are_credentials_managed();
230 - $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 + );
231 231
232 - $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 + );
233 540 }
234 541
235 542 /**
236 - * Gets the allowed post statuses for tracking.
543 + * Validate options from an array
237 544 *
238 - * Uses the `wp_parsely_trackable_statuses` filter to determine which post statuses are allowed to be tracked.
239 - *
240 - * @since 3.17.0
241 - *
242 - * @param WP_Post|int|null $post The post object.
243 - * @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?.
244 549 */
245 - public static function get_trackable_statuses( $post = null ): array {
246 - /**
247 - * Filters the statuses that are permitted to be tracked.
248 - *
249 - * By default, the only status tracked is 'publish'. Use this filter if
250 - * you have other published content that has a different (custom) status.
251 - *
252 - * @since 2.5.0
253 - * @since 3.17.0 Filter extracted to a separate method.
254 - *
255 - * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
256 - * @param WP_Post|int|null $post Which post object or ID is being checked.
257 - */
258 - 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;
259 556 }
260 557
261 558 /**
262 - * Registers action and filter hook callbacks, and immediately upgrades
263 - * 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.
264 565 */
265 - public function run(): void {
266 - // Run upgrade options if they exist for the version currently defined.
267 - $options = $this->get_options();
268 - if ( self::VERSION !== $options['plugin_version'] ) {
269 - $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
270 - if ( method_exists( $this, $method ) ) {
271 - /**
272 - * Variable.
273 - *
274 - * @var callable
275 - */
276 - $callable = array( $this, $method );
277 - 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 + );
278 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' );
279 587
280 - // Update our version info.
281 - $options['plugin_version'] = self::VERSION;
282 - update_option( self::OPTIONS_KEY, $options );
283 588 }
589 + if ( ! isset( $input['track_page_types'] ) ) {
590 + $input['track_page_types'] = array( 'page' );
591 + }
284 592
285 - // @phpstan-ignore return.void
286 - 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;
287 696 }
288 697
289 698 /**
290 - * Returns the Content API service.
699 + * Not doing anything here
291 700 *
292 - * This method returns the Content API service, which is used to interact with the Parse.ly Content API.
701 + * @category Function
702 + * @package Parsely
703 + */
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.
707 + }
708 +
709 + /**
710 + * Not doing anything here
293 711 *
294 - * @since 3.17.0
295 - *
296 - * @return Content_API_Service
712 + * @category Function
713 + * @package Parsely
297 714 */
298 - public function get_content_api(): Content_API_Service {
299 - if ( ! isset( $this->content_api_service ) ) {
300 - $this->content_api_service = new Content_API_Service( $this );
301 - }
302 -
303 - return $this->content_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.
304 718 }
305 719
306 720 /**
307 - * Returns the Suggestions API service.
721 + * Adds a 'Settings' link to the Plugins screen in WP admin
308 722 *
309 - * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
310 - *
311 - * @since 3.17.0
312 - *
313 - * @return Suggestions_API_Service
723 + * @category Function
724 + * @package Parsely
725 + * @param array $links The links to add.
314 726 */
315 - public function get_suggestions_api(): Suggestions_API_Service {
316 - if ( ! isset( $this->suggestions_api_service ) ) {
317 - $this->suggestions_api_service = new Suggestions_API_Service( $this );
318 - }
319 -
320 - return $this->suggestions_api_service;
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;
321 730 }
322 731
323 732 /**
324 - * Gets the REST API controller.
733 + * Display the admin warning if needed
325 734 *
326 - * If the controller is not set, a new instance is created.
327 - *
328 - * @since 3.17.0
329 - *
330 - * @return REST_API_Controller
735 + * @category Function
736 + * @package Parsely
331 737 */
332 - public function get_rest_api_controller(): REST_API_Controller {
333 - if ( ! isset( $this->rest_api_controller ) ) {
334 - $this->rest_api_controller = new REST_API_Controller( $this );
738 + public function display_admin_warning() {
739 + $options = $this->get_options();
740 +
741 + if ( isset( $options['apikey'] ) && ! empty( $options['apikey'] ) ) {
742 + return;
335 743 }
336 744
337 - return $this->rest_api_controller;
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
338 753 }
339 754
340 755 /**
341 - * Gets the full URL of the JavaScript tracker file for the site. If an API
342 - * key is not set, return an empty string.
756 + * Show our note about dynamic tracking
343 757 *
344 - * @since 3.2.0
345 - *
346 - * @return string
758 + * @category Function
759 + * @package Parsely
347 760 */
348 - public function get_tracker_url(): string {
349 - if ( $this->site_id_is_set() ) {
350 - $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
351 - return esc_url( $tracker_url );
352 - }
353 - return '';
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 + );
354 769 }
355 770
356 771 /**
357 - * Deprecated.
358 - * Inserts the code for the <meta name='parsely-page'> parameter within the
359 - * head tag.
772 + * End the code coverage ignore
360 773 *
361 - * @since 3.2.0
362 - * @deprecated 3.3.0
363 - * @see Metadata_Renderer::render_metadata
364 - *
365 - * @param string $meta_type `json_ld` or `repeated_metas`.
774 + * @codeCoverageIgnoreEnd
366 775 */
367 - public function render_metadata( string $meta_type ): void {
368 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
369 - $metadata_renderer = new Metadata_Renderer( $this );
370 - $metadata_renderer->render_metadata( $meta_type );
371 - }
372 776
373 777 /**
374 - * Deprecated.
375 - * Insert the code for the <meta name='parsely-page'> parameter within the
376 - * head tag.
377 - *
378 - * @since 3.0.0
379 - * @deprecated 3.3.0
380 - * @see Metadata_Renderer::render_metadata
778 + * Actually inserts the code for the <meta name='parsely-page'> parameter within the <head></head> tag.
381 779 */
382 - public function insert_page_header_metadata(): void {
383 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
384 - $parsely_options = $this->get_options();
385 - $metadata_renderer = new Metadata_Renderer( $this );
386 - $metadata_renderer->render_metadata( $parsely_options['meta_type'] );
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;
387 831 }
388 832
389 833 /**
390 - * 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).
391 835 *
392 - * By default, only 'publish'ed content includes tracking data.
393 - *
394 836 * @since 2.5.0
395 837 *
396 838 * @param int|WP_Post $post Which post object or ID to check.
397 - * @return bool Should the post status be tracked for the provided post's post_type.
398 - * 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.
399 840 */
400 - 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 +
401 848 /**
402 - * Filters whether the post password check should be skipped when getting
403 - * the post trackable status.
849 + * Filters the statuses that are permitted to be tracked.
404 850 *
405 - * @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.
406 852 *
407 - * @param bool $skip True if the password check should be skipped.
408 - * @param int|WP_Post $post Which post object or ID is being checked.
853 + * @since 2.5.0
409 854 *
410 - * @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.
411 857 */
412 - $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
413 - if ( ! $skip_password_check && post_password_required( $post ) ) {
414 - return false;
415 - }
416 -
417 - $statuses = self::get_trackable_statuses( $post );
418 - 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 ];
419 861 }
420 862
421 863 /**
422 - * Deprecated. Please use the `Metadata` class instead.
423 - *
424 864 * Creates parsely metadata object from post metadata.
425 865 *
426 - * @deprecated 3.3.0
427 - * @see \Parsely\Metadata::construct_metadata
428 - *
429 - * @param array<string, mixed> $parsely_options parsely_options array.
430 - * @param WP_Post $post object.
431 - * @return Metadata_Attributes
866 + * @param array $parsely_options parsely_options array.
867 + * @param WP_Post $post object.
868 + * @return mixed|void
432 869 */
433 - public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
434 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
435 - $metadata = new Metadata( $this );
436 - return $metadata->construct_metadata( $post );
437 - }
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();
438 876
439 - /**
440 - * Calls Parse.ly's update metadata endpoint, sending the post's updated
441 - * metadata.
442 - *
443 - * @param int $post_id The ID of the post to update.
444 - * @return bool True if the metadata endpoint was called, false otherwise.
445 - */
446 - public function call_update_metadata_endpoint( int $post_id ): bool {
447 - $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();
448 923
449 - if ( $this->site_id_is_missing() || '' === $options['metadata_secret'] ) {
450 - return false;
451 - }
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 + }
452 931
453 - $current_post_type = get_post_type( $post_id );
454 - if ( false === $current_post_type ) {
455 - 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' );
456 1024 }
457 1025
458 - $tracked_post_types = array_merge(
459 - $options['track_post_types'],
460 - $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'
461 1041 );
462 1042
463 - // Check that the post's type is trackable.
464 - if ( ! in_array( $current_post_type, $tracked_post_types, true ) ) {
465 - return false;
466 - }
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 );
467 1053
468 - // Check that the post's status is trackable.
469 - if ( ! self::post_has_trackable_status( $post_id ) ) {
470 - return false;
471 - }
1054 + return $parsely_page;
1055 + }
472 1056
473 - $post = get_post( $post_id );
474 - if ( null === $post ) {
475 - return false;
476 - }
477 1057
478 - // Don't call the endpoint when integration tests are running, but
479 - // signal that the above checks have passed.
480 - if ( defined( 'INTEGRATION_TESTS_RUNNING' ) ) {
481 - 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 '';
482 1069 }
483 1070
484 - $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
1071 + $post = get_post( $post_id );
1072 + $metadata = $this->construct_parsely_metadata( $parsely_options, $post );
485 1073
486 1074 $endpoint_metadata = array(
487 - 'canonical_url' => $metadata['url'] ?? '',
488 - 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
489 - 'title' => $metadata['headline'] ?? '',
490 - 'image_url' => $metadata['image']['url'] ?? '',
491 - 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
492 - 'section' => $metadata['articleSection'] ?? '',
493 - 'authors' => $metadata['creator'] ?? '',
494 - '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'],
495 1083 );
496 1084
497 - $parsely_api_base_url = Content_API_Service::get_base_url();
498 - $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
499 - $parsely_metadata_secret = $options['metadata_secret'];
500 -
501 - $headers = array( 'Content-Type' => 'application/json' );
502 - $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(
503 1091 array(
504 1092 'secret' => $parsely_metadata_secret,
505 - 'apikey' => $this->get_site_id(),
1093 + 'apikey' => $parsely_options['apikey'],
506 1094 'metadata' => $endpoint_metadata,
507 1095 )
508 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 );
509 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 +
510 1160 /**
511 - * POST request options.
1161 + * Filters the cache buster value for linked scripts and styles.
512 1162 *
513 - * @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.
514 1166 */
515 - $request_options = array(
516 - 'method' => 'POST',
517 - 'headers' => $headers,
518 - 'blocking' => false,
519 - 'body' => $body,
520 - 'data_format' => 'body',
521 - );
1167 + return apply_filters( 'wp_parsely_cache_buster', $cache_buster );
1168 + }
522 1169
523 - $response = wp_remote_post( $parsely_api_endpoint, $request_options );
1170 + public function register_js() {
1171 + $parsely_options = $this->get_options();
524 1172
525 - if ( is_wp_error( $response ) ) {
526 - 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 '';
527 1176 }
528 1177
529 - 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 + );
530 1185
531 - 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 + );
532 1202 }
533 1203
534 1204 /**
535 - * Safely returns options for the plugin by assigning defaults contained in
536 - * optionDefaults.
537 - *
538 - * As soon as actual options are saved, they override the defaults. This
539 - * prevents us from having to do a lot of isset() checking on variables.
540 - *
541 - * @return Parsely_Options
1205 + * Enqueues the JavaScript code required to send off beacon requests
542 1206 */
543 - 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 +
544 1226 /**
545 - * Variable.
1227 + * Filters whether to include the Parsely JavaScript file.
546 1228 *
547 - * @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.
548 1235 */
549 - $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 + }
550 1244
551 - // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
552 - if ( is_array( $options ) && ! isset( $options['full_metadata_in_non_posts'] ) ) {
553 - // Existing plugin installation without full metadata option.
554 - $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;
555 1256 }
556 1257
557 - // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
558 - if ( is_array( $options ) && ! isset( $options['content_helper'] ) ) {
559 - // Existing plugin installation without Content Helper options.
560 - $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 );
561 1260 }
562 1261
563 - // New plugin installation that hasn't saved its options yet.
564 - if ( ! is_array( $options ) ) {
565 - $this->set_default_track_as_values();
566 - $this->set_default_full_metadata_in_non_posts();
567 - $options = $this->option_defaults;
1262 + wp_enqueue_script( 'wp-parsely-tracker' );
1263 + }
1264 +
1265 + public function load_js_api() {
1266 + $parsely_options = $this->get_options();
1267 +
1268 + // If we don't have an API secret, there's no need to proceed.
1269 + if ( empty( $parsely_options['api_secret'] ) ) {
1270 + return;
568 1271 }
569 1272
570 - /**
571 - * Final options including managed credentials and options.
572 - *
573 - * @var Parsely_Options
574 - */
575 - return array_merge(
576 - $this->option_defaults,
577 - $options,
578 - $this->get_managed_credentials(),
579 - $this->managed_options
580 - );
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 );
1275 + }
1276 +
1277 + wp_enqueue_script( 'wp-parsely-api' );
581 1278 }
582 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 );
1291 + }
1292 +
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;
1302 + }
1303 +
583 1304 /**
584 - * Returns the value of a nested option.
1305 + * Print out the select tags
585 1306 *
586 - * @since 3.16.0
587 - *
588 - * @param string $option The option to get.
589 - * @param Parsely_Options $options The options to get the value from.
590 - * @return mixed The value of the nested option.
1307 + * @param array $args The arguments for the select drop downs.
591 1308 */
592 - public static function get_nested_option_value( $option, $options ) {
593 - $keys = explode( '[', str_replace( ']', '', $option ) );
594 - $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]";
595 1321
596 - foreach ( $keys as $key ) {
597 - if ( isset( $value[ $key ] ) ) {
598 - $value = $value[ $key ];
599 - }
1322 + if ( isset( $args['help_text'] ) ) {
1323 + echo '<div class="parsely-form-controls" data-has-help-text="true">';
600 1324 }
1325 + if ( isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ) {
1326 + echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1327 + }
601 1328
602 - return $value;
603 - }
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 + }
604 1334
605 - /**
606 - * Sets the default values for the track_post_types and track_page_types
607 - * options.
608 - *
609 - * @since 3.9.0
610 - */
611 - public function set_default_track_as_values(): void {
612 - $this->option_defaults['track_page_types'] = array();
613 - $this->option_defaults['track_post_types'] = array();
1335 + echo '>';
614 1336
615 - $post_types = get_post_types( array( 'public' => true ) );
1337 + foreach ( $select_options as $key => $val ) {
1338 + echo '<option value="' . esc_attr( $key ) . '" ';
616 1339
617 - foreach ( $post_types as $post_type ) {
618 - if ( ! post_type_supports( $post_type, 'editor' ) ) {
619 - 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 ) . '>';
620 1345 }
1346 + echo esc_html( $val );
1347 + echo '</option>';
1348 + }
1349 + echo '</select>';
621 1350
622 - if ( is_post_type_hierarchical( $post_type ) ) {
623 - $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>';
624 1356 } else {
625 - $this->option_defaults['track_post_types'][] = $post_type;
1357 + echo '<div class="help-text"> <p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
626 1358 }
627 1359 }
1360 + echo '</div>';
628 1361 }
629 1362
630 1363 /**
631 - * Sets the default value for the full_metadata_in_non_posts option.
1364 + * Print out the radio buttons
632 1365 *
633 - * @since 3.14.0
1366 + * @param array $args The arguments for the radio buttons.
634 1367 */
635 - public function set_default_full_metadata_in_non_posts(): void {
636 - $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]";
637 1374
638 - // Usage of any of these filters will result in the setting being set
639 - // to false.
640 - $filter_tags = array(
641 - 'wp_parsely_metadata',
642 - 'wp_parsely_post_tags',
643 - 'wp_parsely_permalink',
644 - 'wp_parsely_post_category',
645 - 'wp_parsely_pre_authors',
646 - 'wp_parsely_post_authors',
647 - 'wp_parsely_custom_taxonomies',
648 - 'wp_parsely_post_type',
649 - );
650 -
651 - foreach ( $filter_tags as $filter_tag ) {
652 - if ( has_filter( $filter_tag ) ) {
653 - $this->option_defaults['full_metadata_in_non_posts'] = false;
654 - break;
655 - }
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
656 1392 }
1393 + ?>
1394 + </fieldset>
1395 + <?php
657 1396 }
658 1397
659 1398 /**
660 - * Sets the default values for Content Helper options.
1399 + * Prints a checkbox tag in the settings page.
661 1400 *
662 - * Gives PCH access to all users having the edit_posts capability, to keep
663 - * consistent behavior with plugin versions prior to 3.16.0.
664 - *
665 - * @since 3.16.0
1401 + * @param array $args Arguments to print to checkbox tag.
666 1402 */
667 - public function set_default_content_helper_settings_values(): void {
668 - $this->option_defaults['content_helper'] =
669 - Permissions::build_pch_permissions_settings_array(
670 - true,
671 - array_keys( Permissions::get_user_roles_with_edit_posts_cap() )
672 - );
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]";
1409 +
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 + }
1416 +
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 ) );
1420 +
1421 + if ( isset( $args['help_text'] ) ) {
1422 + echo '<div class="help-text"><p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
1423 + }
1424 + echo '</div>';
1425 +
673 1426 }
674 1427
675 1428 /**
676 - * Gets the URL of the plugin's settings page.
1429 + * Print out the radio buttons
677 1430 *
678 - * @param int|null $_blog_id The Blog ID for the multisite subsite to use
679 - * for context (Default null for current).
680 - * @return string
1431 + * @param array $args The arguments for text tags.
681 1432 */
682 - public static function get_settings_url( ?int $_blog_id = null ): string {
683 - return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
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">';
1445 + }
1446 + if ( isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ) {
1447 + echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1448 + }
1449 +
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 ' />';
1457 +
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 + }
684 1474 }
685 1475
686 1476 /**
687 - * Returns the URL of the Parse.ly dashboard for a specific page. If a page
688 - * is not specified, the home dashboard URL for the specified Site ID is
689 - * returned.
690 - *
691 - * @since 3.7.0
692 - *
693 - * @param string $site_id The Site ID for which to get the URL.
694 - * @param string $page_url Optional. The page for which to get the URL.
695 - * @return string The complete dashboard URL.
1477 + * Returns default logo if one can be found
696 1478 */
697 - public static function get_dash_url( string $site_id, string $page_url = '' ): string {
698 - $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
699 -
700 - if ( '' !== $page_url ) {
701 - $page_url = self::get_url_with_itm_source( $page_url, null );
702 - $result .= '?url=' . rawurlencode( $page_url );
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 + }
703 1486 }
704 1487
705 - return $result;
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();
706 1491 }
707 1492
708 1493 /**
709 - * Adds or replaces the itm_source parameter in the URL. Removes the
710 - * parameter if the passed value is null or an empty string.
1494 + * Extracts a host ( not TLD ) from a URL
711 1495 *
712 - * @since 3.9.0
713 - *
714 - * @param string $url The URL to modify.
715 - * @param string|null $itm_source The value of the itm_source parameter.
716 - * @return string The resulting URL.
1496 + * @param string $url The url of the host.
1497 + * @return string $url The host of the url…
717 1498 */
718 - public static function get_url_with_itm_source( string $url, $itm_source ): string {
719 - if ( null === $itm_source || '' === $itm_source ) {
720 - return remove_query_arg( 'itm_source', $url );
1499 + private function get_host_from_url( $url ) {
1500 + if ( preg_match( '/^https?:\/\/( [^\/]+ )\/.*$/', $url, $matches ) ) {
1501 + return $matches[1];
721 1502 }
722 1503
723 - $itm_source = rawurlencode( $itm_source );
724 -
725 - return add_query_arg( 'itm_source', $itm_source, $url );
1504 + return $url;
726 1505 }
727 1506
728 1507 /**
729 - * Checks to see if the current user is a member of the current blog.
1508 + * Returns the tags associated with this page or post
730 1509 *
731 - * @return bool
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.
732 1512 */
733 - public function is_blog_member_logged_in(): bool {
734 - // Can't use $blog_id here because it futzes with the global $blog_id.
735 - $current_blog_id = get_current_blog_id();
736 - $current_user_id = get_current_user_id();
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 + }
737 1519
738 - return is_user_member_of_blog( $current_user_id, $current_blog_id );
1520 + return $tags;
739 1521 }
740 1522
741 1523 /**
742 - * Converts JSON-LD type to respective Parse.ly page type.
1524 + * Returns an array of all the child categories for the current post
743 1525 *
744 - * If the JSON-LD type is one of the types Parse.ly supports as a "post",
745 - * then "post" will be returned. Otherwise, for "non-posts" and unknown
746 - * types, "index" is returned.
747 - *
748 - * @since 2.5.0
749 - *
750 - * @see https://docs.parse.ly/metatags/#h-field-description
751 - *
752 - * @param string $type JSON-LD type.
753 - * @return string "post" or "index".
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.
754 1529 */
755 - public function convert_jsonld_to_parsely_type( string $type ): string {
756 - return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
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;
757 1544 }
758 1545
759 1546 /**
760 - * Determines if a Site ID is saved in the options.
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.
1550 + */
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;
1559 + }
1560 +
1561 + /**
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.
761 1564 *
762 - * @since 2.6.0
763 - * @since 3.7.0 renamed from api_key_is_set.
764 - *
765 - * @return bool True is Site ID is set, false if it is missing.
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.
766 1568 */
767 - public function site_id_is_set(): bool {
768 - $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 + }
769 1582
770 - return '' !== $options['apikey'];
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;
771 1600 }
772 1601
773 1602 /**
774 - * Determines if a Site ID is not saved in the options.
1603 + * Return the top-most category/taxonomy value in a hierarcy given a taxonomy value's ID
1604 + * ( WordPress calls taxonomy values 'terms' ).
775 1605 *
776 - * @since 2.6.0
777 - * @since 3.7.0 renamed from api_key_is_missing.
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.
1609 + */
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;
1616 + }
1617 +
1618 + /**
1619 + * Return the bottom-most category/taxonomy value in a hierarcy given a post ID
1620 + * ( WordPress calls taxonomy values 'terms' ).
778 1621 *
779 - * @return bool True if Site ID is missing, false if it is set.
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.
780 1625 */
781 - public function site_id_is_missing(): bool {
782 - return ! $this->site_id_is_set();
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;
783 1642 }
784 1643
785 1644 /**
786 - * Gets the Site ID if set.
1645 + * Get all term values from custom taxonomies.
787 1646 *
788 - * @since 2.6.0
789 - * @since 3.7.0 renamed from get_site_id.
790 - *
791 - * @return string Site ID if set, or empty string if not.
1647 + * @param WP_Post $post_obj The post object.
1648 + * @param array $parsely_options The pparsely options.
792 1649 */
793 - public function get_site_id(): string {
794 - $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();
795 1654
796 - return $this->site_id_is_set() ? $options['apikey'] : '';
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;
797 1666 }
798 1667
799 1668 /**
800 - * Returns whether the API Secret is set in the plugin's 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
801 1672 *
802 - * @since 3.4.0
803 - *
804 - * @return bool True if the API Secret is set, false if not set.
1673 + * @param string $post_id The id of the post.
805 1674 */
806 - public function api_secret_is_set(): bool {
807 - $options = $this->get_options();
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;
808 1679
809 - return '' !== $options['api_secret'];
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;
810 1712 }
811 1713
812 1714 /**
813 - * Returns the API Secret stored in the plugin's options.
1715 + * Determine author name from display name, falling back to firstname
1716 + * lastname, then nickname and finally the nicename.
814 1717 *
815 - * @since 3.4.0
816 - *
817 - * @return string The API Secret, empty string if the API secret is not set.
1718 + * @param WP_User $author The author of the post.
818 1719 */
819 - public function get_api_secret(): string {
820 - $options = $this->get_options();
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 '';
1724 + }
1725 + $author_name = $author->display_name;
1726 + if ( ! empty( $author_name ) ) {
1727 + return $author_name;
1728 + }
821 1729
822 - return $this->api_secret_is_set() ? $options['api_secret'] : '';
1730 + $author_name = $author->user_firstname . ' ' . $author->user_lastname;
1731 + if ( ' ' !== $author_name ) {
1732 + return $author_name;
1733 + }
1734 +
1735 + $author_name = $author->nickname;
1736 + if ( ! empty( $author_name ) ) {
1737 + return $author_name;
1738 + }
1739 +
1740 + return $author->user_nicename;
823 1741 }
824 1742
825 1743 /**
826 - * Returns all supported post and non-post types.
1744 + * Retrieve all the authors for a post as an array. Can include multiple
1745 + * authors if coauthors plugin is in use.
827 1746 *
828 - * @since 3.7.0
829 - *
830 - * @return string[] all supported types
1747 + * @param WP_Post $post The post object.
1748 + * @return array
831 1749 */
832 - public function get_all_supported_types(): array {
833 - return self::$all_supported_types;
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 + }
1755 +
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 );
1765 +
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;
834 1779 }
835 1780
836 1781 /**
837 - * Gets all tracked post types.
1782 + * Sanitize content
838 1783 *
839 - * @since 3.7.0
840 - *
841 - * @return array<string>
1784 + * @param string $val The content you'd like sanitized.
1785 + * @return string
842 1786 */
843 - public function get_all_track_types(): array {
844 - $options = $this->get_options();
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;
1794 + }
845 1795
846 - return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
1796 + return $val;
847 1797 }
848 1798
1799 +
849 1800 /**
850 - * Gets default options.
851 - *
852 - * @since 3.8.0
853 - *
854 - * @return Parsely_Options
1801 + * Get the URL of the plugin settings page
855 1802 */
856 - public function get_default_options() {
857 - return $this->option_defaults;
1803 + private function get_settings_url() {
1804 + return admin_url( 'options-general.php?page=' . self::MENU_SLUG );
858 1805 }
859 1806
1807 +
860 1808 /**
861 - * Returns the credentials that are being managed at the platform level.
1809 + * Get the URL of the current PHP script.
1810 + * A fall-back implementation to determine permalink
862 1811 *
863 - * @since 3.9.0
864 - * @access private
865 - *
866 - * @return Parsely_Options|array<empty> The managed credentials.
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
867 1815 */
868 - private function get_managed_credentials() {
869 - if ( true !== $this->are_credentials_managed ) {
870 - return array();
871 - }
1816 + public function get_current_url( $parsely_type = 'nonpost', $post_id = 0 ) {
1817 + if ( 'post' === $parsely_type ) {
1818 + $permalink = get_permalink( $post_id );
872 1819
873 - $credentials = apply_filters( 'wp_parsely_credentials', array() );
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 + : '';
874 1836
875 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
876 - return array();
1837 + $url = home_url( $request_uri );
877 1838 }
878 1839
879 - $result = array();
1840 + $options = $this->get_options();
1841 + return $options['force_https_canonicals']
1842 + ? str_replace( 'http://', 'https://', $url )
1843 + : str_replace( 'https://', 'http://', $url );
1844 + }
880 1845
881 - if ( isset( $credentials['site_id'] ) ) {
882 - $result['apikey'] = $credentials['site_id'];
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];
883 1858 }
1859 + return '';
1860 + }
884 1861
885 - if ( isset( $credentials['api_secret'] ) ) {
886 - $result['api_secret'] = $credentials['api_secret'];
887 - }
1862 + /**
1863 + * Add parsely tracking to facebook instant articles
1864 + *
1865 + * @param type $registry The registry info for fbia.
1866 + * @return string
1867 + */
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';
888 1872
889 - if ( isset( $credentials['metadata_secret'] ) ) {
890 - $result['metadata_secret'] = $credentials['metadata_secret'];
891 - }
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 -->';
892 1886
893 - return $result;
1887 + $registry[ $identifier ] = array(
1888 + 'name' => $display_name,
1889 + 'payload' => $embed_code,
1890 + );
1891 +
1892 + return $embed_code;
894 1893 }
895 1894
896 1895 /**
897 - * Returns whether credentials are being managed at the platform level.
898 - *
899 - * @since 3.9.0
900 - * @access private
901 - *
902 - * @return bool Whether credentials are being managed at the platform level.
1896 + * Add amp actions.
903 1897 */
904 - private function are_credentials_managed(): bool {
905 - $credentials = apply_filters( 'wp_parsely_credentials', array() );
1898 + public function parsely_add_amp_actions() {
1899 + if ( ! function_exists( 'is_amp_endpoint' ) || ! is_amp_endpoint() ) {
1900 + return '';
1901 + }
906 1902
907 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
908 - return false;
1903 + $options = $this->get_options();
1904 +
1905 + if ( $options['disable_amp'] ) {
1906 + return '';
909 1907 }
910 1908
911 - return $credentials['is_managed'] ?? false;
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' ) );
912 1911 }
913 1912
914 1913 /**
915 - * Sets the values of managed options.
1914 + * Add amp analytics.
916 1915 *
917 - * This function won't accept managing credentials or certain plugin options
918 - * that are being managed through other means. For managing credentials,
919 - * please use the `wp_parsely_credentials` filter.
920 - *
921 - * @since 3.9.0
922 - * @access private
1916 + * @param type $analytics The analytics object you want to add.
1917 + * @return type
923 1918 */
924 - private function set_managed_options(): void {
925 - $managed_options = apply_filters( 'wp_parsely_managed_options', false );
1919 + public function parsely_add_amp_analytics( $analytics ) {
1920 + $options = $this->get_options();
926 1921
927 - if ( ! is_array( $managed_options ) ) {
928 - return;
1922 + if ( empty( $options['apikey'] ) ) {
1923 + return $analytics;
929 1924 }
930 1925
931 - // Don't allow certain options to be set as managed.
932 - unset(
933 - $managed_options['apikey'],
934 - $managed_options['api_secret'],
935 - $managed_options['metadata_secret'],
936 - $managed_options['track_post_types'],
937 - $managed_options['track_page_types'],
938 - $managed_options['plugin_version']
1926 + $analytics['parsely'] = array(
1927 + 'type' => 'parsely',
1928 + 'attributes' => array(),
1929 + 'config_data' => array(
1930 + 'vars' => array(
1931 + 'apikey' => $options['apikey'],
1932 + ),
1933 + ),
939 1934 );
940 1935
941 - if ( 0 === count( $managed_options ) ) {
942 - return;
943 - }
944 -
945 - /**
946 - * Current options.
947 - *
948 - * @var Parsely_Options $current_options
949 - */
950 - $current_options = get_option( self::OPTIONS_KEY, array() );
951 -
952 - // Set managed options values.
953 - foreach ( $managed_options as $key => $value ) {
954 - $is_option_valid = isset( $this->option_defaults[ $key ] );
955 -
956 - if ( $is_option_valid ) {
957 - if ( null === $value ) {
958 - // When null, the option gets its value from the database.
959 - $this->managed_options[ $key ] =
960 - $current_options[ $key ] ?? $this->option_defaults[ $key ];
961 - } else {
962 - $this->managed_options[ $key ] =
963 - $this->sanitize_managed_option( $key, $value );
964 - }
965 - }
966 - }
1936 + return $analytics;
967 1937 }
968 1938
969 1939 /**
970 - * Sanitizes the value of the passed managed option.
1940 + * Add amp native analytics.
971 1941 *
972 - * @since 3.9.0
973 - * @access private
974 - *
975 - * @param string $option_id The option's ID.
976 - * @param bool|string $value The option's value.
977 - * @return bool|string The sanitized option value.
1942 + * @param type $analytics The analytics object you want to add.
1943 + * @return string|type
978 1944 */
979 - private function sanitize_managed_option( string $option_id, $value ) {
980 - $option_value_type = gettype( $this->option_defaults[ $option_id ] );
1945 + public function parsely_add_amp_native_analytics( $analytics ) {
1946 + $options = $this->get_options();
981 1947
982 - if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
983 - _doing_it_wrong(
984 - __FUNCTION__,
985 - esc_html(
986 - sprintf( /* translators: 1: Option ID */
987 - __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
988 - $option_id
989 - )
990 - ),
991 - ''
992 - );
1948 + if ( ! empty( $options['disable_amp'] ) && true === $options['disable_amp'] ) {
1949 + return '';
1950 + }
993 1951
994 - return false;
1952 + if ( empty( $options['apikey'] ) ) {
1953 + return $analytics;
995 1954 }
996 1955
997 - if ( 'string' === $option_value_type ) {
998 - if ( ! is_string( $value ) ) {
999 - _doing_it_wrong(
1000 - __FUNCTION__,
1001 - esc_html(
1002 - sprintf( /* translators: 1: Option ID */
1003 - __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
1004 - $option_id
1005 - )
1956 + $analytics['parsely'] = array(
1957 + 'type' => 'parsely',
1958 + 'attributes' => array(),
1959 + 'config' => wp_json_encode(
1960 + array(
1961 + 'vars' => array(
1962 + 'apikey' => $options['apikey'],
1006 1963 ),
1007 - ''
1008 - );
1964 + )
1965 + ),
1966 + );
1009 1967
1010 - $value = strval( $value );
1011 - }
1968 + return $analytics;
1969 + }
1012 1970
1013 - // String options that are restricted to specific values.
1014 - $restricted_value_options = array(
1015 - 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
1016 - 'meta_type' => array( 'json_ld', 'repeated_metas' ),
1017 - );
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 + }
1018 1980
1019 - // Verify that the above values are respected.
1020 - foreach ( $restricted_value_options as $option_key => $valid_values ) {
1021 - if ( $option_id === $option_key ) {
1022 - if ( ! in_array( $value, $valid_values, true ) ) {
1023 - _doing_it_wrong(
1024 - __FUNCTION__,
1025 - esc_html(
1026 - sprintf( /* translators: 1: Option value 2: Option ID */
1027 - __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
1028 - $value,
1029 - $option_id
1030 - )
1031 - ),
1032 - ''
1033 - );
1981 + /**
1982 + * Why is this here?
1983 + */
1984 + public function return_personalized_json() {
1034 1985
1035 - $value = $this->option_defaults[ $option_id ];
1036 - }
1037 - }
1038 - }
1039 - }
1040 -
1041 - return $value;
1042 1986 }
1043 1987
1044 1988 /**
1045 - * Allows remote requests to Parse.ly.
1989 + * Convert JSON-LD type to respective Parse.ly page type.
1046 1990 *
1047 - * 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.
1048 1993 *
1049 - * @since 3.13.0
1050 - * @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".
1051 2000 */
1052 - private function allow_parsely_remote_requests(): void {
1053 - $allowed_urls = array(
1054 - self::DASHBOARD_BASE_URL,
1055 - Content_API_Service::get_base_url(),
1056 - Suggestions_API_Service::get_base_url(),
1057 - );
1058 -
1059 - add_filter(
1060 - 'http_request_host_is_external',
1061 - function ( $external, $host, $url ) use ( $allowed_urls ) {
1062 - // Check if the URL matches any URLs on the allowed list.
1063 - foreach ( $allowed_urls as $allowed_url ) {
1064 - if ( Utils::str_starts_with( $url, $allowed_url ) ) {
1065 - return true;
1066 - }
1067 - }
1068 - return $external;
1069 - },
1070 - 10,
1071 - 3
1072 - );
2001 + public function convert_jsonld_to_parsely_type( $type ) {
2002 + return in_array( $type, $this->supported_jsonld_post_types ) ? 'post' : 'index';
1073 2003 }
1074 2004 }