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 +1665 -710 3.17.02.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,862 +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 - add_action( 'save_post', array( $this, '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;
286 696 }
287 697
288 698 /**
289 - * Returns the Content API service.
699 + * Not doing anything here
290 700 *
291 - * 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
292 711 *
293 - * @since 3.17.0
294 - *
295 - * @return Content_API_Service
712 + * @category Function
713 + * @package Parsely
296 714 */
297 - public function get_content_api(): Content_API_Service {
298 - if ( ! isset( $this->content_api_service ) ) {
299 - $this->content_api_service = new Content_API_Service( $this );
300 - }
301 -
302 - 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.
303 718 }
304 719
305 720 /**
306 - * Returns the Suggestions API service.
721 + * Adds a 'Settings' link to the Plugins screen in WP admin
307 722 *
308 - * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
309 - *
310 - * @since 3.17.0
311 - *
312 - * @return Suggestions_API_Service
723 + * @category Function
724 + * @package Parsely
725 + * @param array $links The links to add.
313 726 */
314 - public function get_suggestions_api(): Suggestions_API_Service {
315 - if ( ! isset( $this->suggestions_api_service ) ) {
316 - $this->suggestions_api_service = new Suggestions_API_Service( $this );
317 - }
318 -
319 - 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;
320 730 }
321 731
322 732 /**
323 - * Gets the REST API controller.
733 + * Display the admin warning if needed
324 734 *
325 - * If the controller is not set, a new instance is created.
326 - *
327 - * @since 3.17.0
328 - *
329 - * @return REST_API_Controller
735 + * @category Function
736 + * @package Parsely
330 737 */
331 - public function get_rest_api_controller(): REST_API_Controller {
332 - if ( ! isset( $this->rest_api_controller ) ) {
333 - $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;
334 743 }
335 744
336 - 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
337 753 }
338 754
339 755 /**
340 - * Gets the full URL of the JavaScript tracker file for the site. If an API
341 - * key is not set, return an empty string.
756 + * Show our note about dynamic tracking
342 757 *
343 - * @since 3.2.0
344 - *
345 - * @return string
758 + * @category Function
759 + * @package Parsely
346 760 */
347 - public function get_tracker_url(): string {
348 - if ( $this->site_id_is_set() ) {
349 - $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
350 - return esc_url( $tracker_url );
351 - }
352 - 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 + );
353 769 }
354 770
355 771 /**
356 - * Deprecated.
357 - * Inserts the code for the <meta name='parsely-page'> parameter within the
358 - * head tag.
772 + * End the code coverage ignore
359 773 *
360 - * @since 3.2.0
361 - * @deprecated 3.3.0
362 - * @see Metadata_Renderer::render_metadata
363 - *
364 - * @param string $meta_type `json_ld` or `repeated_metas`.
774 + * @codeCoverageIgnoreEnd
365 775 */
366 - public function render_metadata( string $meta_type ): void {
367 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
368 - $metadata_renderer = new Metadata_Renderer( $this );
369 - $metadata_renderer->render_metadata( $meta_type );
370 - }
371 776
372 777 /**
373 - * Deprecated.
374 - * Insert the code for the <meta name='parsely-page'> parameter within the
375 - * head tag.
376 - *
377 - * @since 3.0.0
378 - * @deprecated 3.3.0
379 - * @see Metadata_Renderer::render_metadata
778 + * Actually inserts the code for the <meta name='parsely-page'> parameter within the <head></head> tag.
380 779 */
381 - public function insert_page_header_metadata(): void {
382 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
383 - $parsely_options = $this->get_options();
384 - $metadata_renderer = new Metadata_Renderer( $this );
385 - $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;
386 831 }
387 832
388 833 /**
389 - * 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).
390 835 *
391 - * By default, only 'publish'ed content includes tracking data.
392 - *
393 836 * @since 2.5.0
394 837 *
395 838 * @param int|WP_Post $post Which post object or ID to check.
396 - * @return bool Should the post status be tracked for the provided post's post_type.
397 - * 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.
398 840 */
399 - public static function post_has_trackable_status( $post ): bool {
841 + public static function post_has_trackable_status( $post ) {
400 842 static $cache = array();
401 - $post_id = is_int( $post ) ? $post : $post->ID;
843 + $post_id = is_int( $post ) ? $post : $post->ID;
402 844 if ( isset( $cache[ $post_id ] ) ) {
403 845 return $cache[ $post_id ];
404 846 }
405 847
406 848 /**
407 - * Filters whether the post password check should be skipped when getting
408 - * the post trackable status.
849 + * Filters the statuses that are permitted to be tracked.
409 850 *
410 - * @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.
411 852 *
412 - * @param bool $skip True if the password check should be skipped.
413 - * @param int|WP_Post $post Which post object or ID is being checked.
853 + * @since 2.5.0
414 854 *
415 - * @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.
416 857 */
417 - $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
418 - if ( ! $skip_password_check && post_password_required( $post ) ) {
419 - $cache[ $post_id ] = false;
420 - return false;
421 - }
422 -
423 - $statuses = self::get_trackable_statuses( $post );
858 + $statuses = apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
424 859 $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true );
425 860 return $cache[ $post_id ];
426 861 }
427 862
428 863 /**
429 - * Deprecated. Please use the `Metadata` class instead.
430 - *
431 864 * Creates parsely metadata object from post metadata.
432 865 *
433 - * @deprecated 3.3.0
434 - * @see \Parsely\Metadata::construct_metadata
435 - *
436 - * @param array<string, mixed> $parsely_options parsely_options array.
437 - * @param WP_Post $post object.
438 - * @return Metadata_Attributes
866 + * @param array $parsely_options parsely_options array.
867 + * @param WP_Post $post object.
868 + * @return mixed|void
439 869 */
440 - public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
441 - _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
442 - $metadata = new Metadata( $this );
443 - return $metadata->construct_metadata( $post );
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();
876 +
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();
923 +
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 + }
931 +
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' );
1024 + }
1025 +
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'
1041 + );
1042 +
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 );
1053 +
1054 + return $parsely_page;
444 1055 }
445 1056
1057 +
446 1058 /**
447 1059 * Updates the Parsely metadata endpoint with the new metadata of the post.
448 1060 *
449 1061 * @param int $post_id id of the post to update.
1062 + * @return string
450 1063 */
451 - public function update_metadata_endpoint( int $post_id ): void {
1064 + public function update_metadata_endpoint( $post_id ) {
452 1065 $parsely_options = $this->get_options();
453 - if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) {
454 - return;
455 - }
456 1066
457 - $post = get_post( $post_id );
458 - if ( null === $post ) {
459 - return;
1067 + if ( empty( $parsely_options['apikey'] ) || empty( $parsely_options['metadata_secret'] ) ) {
1068 + return '';
460 1069 }
461 1070
462 - $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
1071 + $post = get_post( $post_id );
1072 + $metadata = $this->construct_parsely_metadata( $parsely_options, $post );
463 1073
464 1074 $endpoint_metadata = array(
465 - 'canonical_url' => $metadata['url'] ?? '',
466 - 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
467 - 'title' => $metadata['headline'] ?? '',
468 - 'image_url' => $metadata['image']['url'] ?? '',
469 - 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
470 - 'section' => $metadata['articleSection'] ?? '',
471 - 'authors' => $metadata['creator'] ?? '',
472 - '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'],
473 1083 );
474 1084
475 - $parsely_api_base_url = Content_API_Service::get_base_url();
476 - $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
1085 + $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts';
477 1086 $parsely_metadata_secret = $parsely_options['metadata_secret'];
478 -
479 - $headers = array( 'Content-Type' => 'application/json' );
480 - $body = wp_json_encode(
1087 + $headers = array(
1088 + 'Content-Type' => 'application/json',
1089 + );
1090 + $body = wp_json_encode(
481 1091 array(
482 1092 'secret' => $parsely_metadata_secret,
483 - 'apikey' => $this->get_site_id(),
1093 + 'apikey' => $parsely_options['apikey'],
484 1094 'metadata' => $endpoint_metadata,
485 1095 )
486 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 );
487 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 +
488 1160 /**
489 - * POST request options.
1161 + * Filters the cache buster value for linked scripts and styles.
490 1162 *
491 - * @var WP_HTTP_Request_Args $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.
492 1166 */
493 - $options = array(
494 - 'method' => 'POST',
495 - 'headers' => $headers,
496 - 'blocking' => false,
497 - 'body' => $body,
498 - 'data_format' => 'body',
1167 + return apply_filters( 'wp_parsely_cache_buster', $cache_buster );
1168 + }
1169 +
1170 + public function register_js() {
1171 + $parsely_options = $this->get_options();
1172 +
1173 + // If we don't have an API key, there's no need to proceed.
1174 + if ( empty( $parsely_options['apikey'] ) ) {
1175 + return '';
1176 + }
1177 +
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
499 1184 );
500 1185
501 - $response = wp_remote_post( $parsely_api_endpoint, $options );
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 + );
502 1194
503 - if ( ! is_wp_error( $response ) ) {
504 - $current_timestamp = time();
505 - update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
506 - }
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 + );
507 1202 }
508 1203
509 1204 /**
510 - * Safely returns options for the plugin by assigning defaults contained in
511 - * optionDefaults.
512 - *
513 - * As soon as actual options are saved, they override the defaults. This
514 - * prevents us from having to do a lot of isset() checking on variables.
515 - *
516 - * @return Parsely_Options
1205 + * Enqueues the JavaScript code required to send off beacon requests
517 1206 */
518 - 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 +
519 1226 /**
520 - * Variable.
1227 + * Filters whether to include the Parsely JavaScript file.
521 1228 *
522 - * @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.
523 1235 */
524 - $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 + }
525 1244
526 - // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
527 - if ( is_array( $options ) && ! isset( $options['full_metadata_in_non_posts'] ) ) {
528 - // Existing plugin installation without full metadata option.
529 - $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;
530 1256 }
531 1257
532 - // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
533 - if ( is_array( $options ) && ! isset( $options['content_helper'] ) ) {
534 - // Existing plugin installation without Content Helper options.
535 - $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 );
536 1260 }
537 1261
538 - // New plugin installation that hasn't saved its options yet.
539 - if ( ! is_array( $options ) ) {
540 - $this->set_default_track_as_values();
541 - $this->set_default_full_metadata_in_non_posts();
542 - $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;
543 1271 }
544 1272
545 - /**
546 - * Final options including managed credentials and options.
547 - *
548 - * @var Parsely_Options
549 - */
550 - return array_merge(
551 - $this->option_defaults,
552 - $options,
553 - $this->get_managed_credentials(),
554 - $this->managed_options
555 - );
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' );
556 1278 }
557 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 +
558 1304 /**
559 - * Returns the value of a nested option.
1305 + * Print out the select tags
560 1306 *
561 - * @since 3.16.0
562 - *
563 - * @param string $option The option to get.
564 - * @param Parsely_Options $options The options to get the value from.
565 - * @return mixed The value of the nested option.
1307 + * @param array $args The arguments for the select drop downs.
566 1308 */
567 - public static function get_nested_option_value( $option, $options ) {
568 - $keys = explode( '[', str_replace( ']', '', $option ) );
569 - $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]";
570 1321
571 - foreach ( $keys as $key ) {
572 - if ( isset( $value[ $key ] ) ) {
573 - $value = $value[ $key ];
574 - }
1322 + if ( isset( $args['help_text'] ) ) {
1323 + echo '<div class="parsely-form-controls" data-has-help-text="true">';
575 1324 }
1325 + if ( isset( $args['requires_recrawl'] ) && true === $args['requires_recrawl'] ) {
1326 + echo '<div class="parsely-form-controls" data-requires-recrawl="true">';
1327 + }
576 1328
577 - return $value;
578 - }
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 + }
579 1334
580 - /**
581 - * Sets the default values for the track_post_types and track_page_types
582 - * options.
583 - *
584 - * @since 3.9.0
585 - */
586 - public function set_default_track_as_values(): void {
587 - $this->option_defaults['track_page_types'] = array();
588 - $this->option_defaults['track_post_types'] = array();
1335 + echo '>';
589 1336
590 - $post_types = get_post_types( array( 'public' => true ) );
1337 + foreach ( $select_options as $key => $val ) {
1338 + echo '<option value="' . esc_attr( $key ) . '" ';
591 1339
592 - foreach ( $post_types as $post_type ) {
593 - if ( ! post_type_supports( $post_type, 'editor' ) ) {
594 - 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 ) . '>';
595 1345 }
1346 + echo esc_html( $val );
1347 + echo '</option>';
1348 + }
1349 + echo '</select>';
596 1350
597 - if ( is_post_type_hierarchical( $post_type ) ) {
598 - $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>';
599 1356 } else {
600 - $this->option_defaults['track_post_types'][] = $post_type;
1357 + echo '<div class="help-text"> <p class="description">' . esc_html( $args['help_text'] ) . '</p></div>';
601 1358 }
602 1359 }
1360 + echo '</div>';
603 1361 }
604 1362
605 1363 /**
606 - * Sets the default value for the full_metadata_in_non_posts option.
1364 + * Print out the radio buttons
607 1365 *
608 - * @since 3.14.0
1366 + * @param array $args The arguments for the radio buttons.
609 1367 */
610 - public function set_default_full_metadata_in_non_posts(): void {
611 - $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]";
612 1374
613 - // Usage of any of these filters will result in the setting being set
614 - // to false.
615 - $filter_tags = array(
616 - 'wp_parsely_metadata',
617 - 'wp_parsely_post_tags',
618 - 'wp_parsely_permalink',
619 - 'wp_parsely_post_category',
620 - 'wp_parsely_pre_authors',
621 - 'wp_parsely_post_authors',
622 - 'wp_parsely_custom_taxonomies',
623 - 'wp_parsely_post_type',
624 - );
625 -
626 - foreach ( $filter_tags as $filter_tag ) {
627 - if ( has_filter( $filter_tag ) ) {
628 - $this->option_defaults['full_metadata_in_non_posts'] = false;
629 - break;
630 - }
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
631 1392 }
1393 + ?>
1394 + </fieldset>
1395 + <?php
632 1396 }
633 1397
634 1398 /**
635 - * Sets the default values for Content Helper options.
1399 + * Prints a checkbox tag in the settings page.
636 1400 *
637 - * Gives PCH access to all users having the edit_posts capability, to keep
638 - * consistent behavior with plugin versions prior to 3.16.0.
639 - *
640 - * @since 3.16.0
1401 + * @param array $args Arguments to print to checkbox tag.
641 1402 */
642 - public function set_default_content_helper_settings_values(): void {
643 - $this->option_defaults['content_helper'] =
644 - Permissions::build_pch_permissions_settings_array(
645 - true,
646 - array_keys( Permissions::get_user_roles_with_edit_posts_cap() )
647 - );
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 +
648 1426 }
649 1427
650 1428 /**
651 - * Gets the URL of the plugin's settings page.
1429 + * Print out the radio buttons
652 1430 *
653 - * @param int|null $_blog_id The Blog ID for the multisite subsite to use
654 - * for context (Default null for current).
655 - * @return string
1431 + * @param array $args The arguments for text tags.
656 1432 */
657 - public static function get_settings_url( ?int $_blog_id = null ): string {
658 - 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 + }
659 1474 }
660 1475
661 1476 /**
662 - * Returns the URL of the Parse.ly dashboard for a specific page. If a page
663 - * is not specified, the home dashboard URL for the specified Site ID is
664 - * returned.
665 - *
666 - * @since 3.7.0
667 - *
668 - * @param string $site_id The Site ID for which to get the URL.
669 - * @param string $page_url Optional. The page for which to get the URL.
670 - * @return string The complete dashboard URL.
1477 + * Returns default logo if one can be found
671 1478 */
672 - public static function get_dash_url( string $site_id, string $page_url = '' ): string {
673 - $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
674 -
675 - if ( '' !== $page_url ) {
676 - $page_url = self::get_url_with_itm_source( $page_url, null );
677 - $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 + }
678 1486 }
679 1487
680 - 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();
681 1491 }
682 1492
683 1493 /**
684 - * Adds or replaces the itm_source parameter in the URL. Removes the
685 - * parameter if the passed value is null or an empty string.
1494 + * Extracts a host ( not TLD ) from a URL
686 1495 *
687 - * @since 3.9.0
688 - *
689 - * @param string $url The URL to modify.
690 - * @param string|null $itm_source The value of the itm_source parameter.
691 - * @return string The resulting URL.
1496 + * @param string $url The url of the host.
1497 + * @return string $url The host of the url…
692 1498 */
693 - public static function get_url_with_itm_source( string $url, $itm_source ): string {
694 - if ( null === $itm_source || '' === $itm_source ) {
695 - 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];
696 1502 }
697 1503
698 - $itm_source = rawurlencode( $itm_source );
699 -
700 - return add_query_arg( 'itm_source', $itm_source, $url );
1504 + return $url;
701 1505 }
702 1506
703 1507 /**
704 - * Checks to see if the current user is a member of the current blog.
1508 + * Returns the tags associated with this page or post
705 1509 *
706 - * @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.
707 1512 */
708 - public function is_blog_member_logged_in(): bool {
709 - // Can't use $blog_id here because it futzes with the global $blog_id.
710 - $current_blog_id = get_current_blog_id();
711 - $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 + }
712 1519
713 - return is_user_member_of_blog( $current_user_id, $current_blog_id );
1520 + return $tags;
714 1521 }
715 1522
716 1523 /**
717 - * Converts JSON-LD type to respective Parse.ly page type.
1524 + * Returns an array of all the child categories for the current post
718 1525 *
719 - * If the JSON-LD type is one of the types Parse.ly supports as a "post",
720 - * then "post" will be returned. Otherwise, for "non-posts" and unknown
721 - * types, "index" is returned.
722 - *
723 - * @since 2.5.0
724 - *
725 - * @see https://docs.parse.ly/metatags/#h-field-description
726 - *
727 - * @param string $type JSON-LD type.
728 - * @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.
729 1529 */
730 - public function convert_jsonld_to_parsely_type( string $type ): string {
731 - 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;
732 1544 }
733 1545
734 1546 /**
735 - * 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.
736 1564 *
737 - * @since 2.6.0
738 - * @since 3.7.0 renamed from api_key_is_set.
739 - *
740 - * @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.
741 1568 */
742 - public function site_id_is_set(): bool {
743 - $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 + }
744 1582
745 - 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;
746 1600 }
747 1601
748 1602 /**
749 - * 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' ).
750 1605 *
751 - * @since 2.6.0
752 - * @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' ).
753 1621 *
754 - * @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.
755 1625 */
756 - public function site_id_is_missing(): bool {
757 - 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;
758 1642 }
759 1643
760 1644 /**
761 - * Gets the Site ID if set.
1645 + * Get all term values from custom taxonomies.
762 1646 *
763 - * @since 2.6.0
764 - * @since 3.7.0 renamed from get_site_id.
765 - *
766 - * @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.
767 1649 */
768 - public function get_site_id(): string {
769 - $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();
770 1654
771 - 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;
772 1666 }
773 1667
774 1668 /**
775 - * 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
776 1672 *
777 - * @since 3.4.0
778 - *
779 - * @return bool True if the API Secret is set, false if not set.
1673 + * @param string $post_id The id of the post.
780 1674 */
781 - public function api_secret_is_set(): bool {
782 - $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;
783 1679
784 - 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;
785 1712 }
786 1713
787 1714 /**
788 - * 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.
789 1717 *
790 - * @since 3.4.0
791 - *
792 - * @return string The API Secret, empty string if the API secret is not set.
1718 + * @param WP_User $author The author of the post.
793 1719 */
794 - public function get_api_secret(): string {
795 - $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 + }
796 1729
797 - 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;
798 1741 }
799 1742
800 1743 /**
801 - * 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.
802 1746 *
803 - * @since 3.7.0
804 - *
805 - * @return string[] all supported types
1747 + * @param WP_Post $post The post object.
1748 + * @return array
806 1749 */
807 - public function get_all_supported_types(): array {
808 - 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;
809 1779 }
810 1780
811 1781 /**
812 - * Gets all tracked post types.
1782 + * Sanitize content
813 1783 *
814 - * @since 3.7.0
815 - *
816 - * @return array<string>
1784 + * @param string $val The content you'd like sanitized.
1785 + * @return string
817 1786 */
818 - public function get_all_track_types(): array {
819 - $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 + }
820 1795
821 - return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
1796 + return $val;
822 1797 }
823 1798
1799 +
824 1800 /**
825 - * Gets default options.
826 - *
827 - * @since 3.8.0
828 - *
829 - * @return Parsely_Options
1801 + * Get the URL of the plugin settings page
830 1802 */
831 - public function get_default_options() {
832 - return $this->option_defaults;
1803 + private function get_settings_url() {
1804 + return admin_url( 'options-general.php?page=' . self::MENU_SLUG );
833 1805 }
834 1806
1807 +
835 1808 /**
836 - * 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
837 1811 *
838 - * @since 3.9.0
839 - * @access private
840 - *
841 - * @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
842 1815 */
843 - private function get_managed_credentials() {
844 - if ( true !== $this->are_credentials_managed ) {
845 - return array();
846 - }
1816 + public function get_current_url( $parsely_type = 'nonpost', $post_id = 0 ) {
1817 + if ( 'post' === $parsely_type ) {
1818 + $permalink = get_permalink( $post_id );
847 1819
848 - $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 + : '';
849 1836
850 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
851 - return array();
1837 + $url = home_url( $request_uri );
852 1838 }
853 1839
854 - $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 + }
855 1845
856 - if ( isset( $credentials['site_id'] ) ) {
857 - $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];
858 1858 }
1859 + return '';
1860 + }
859 1861
860 - if ( isset( $credentials['api_secret'] ) ) {
861 - $result['api_secret'] = $credentials['api_secret'];
862 - }
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';
863 1872
864 - if ( isset( $credentials['metadata_secret'] ) ) {
865 - $result['metadata_secret'] = $credentials['metadata_secret'];
866 - }
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 -->';
867 1886
868 - return $result;
1887 + $registry[ $identifier ] = array(
1888 + 'name' => $display_name,
1889 + 'payload' => $embed_code,
1890 + );
1891 +
1892 + return $embed_code;
869 1893 }
870 1894
871 1895 /**
872 - * Returns whether credentials are being managed at the platform level.
873 - *
874 - * @since 3.9.0
875 - * @access private
876 - *
877 - * @return bool Whether credentials are being managed at the platform level.
1896 + * Add amp actions.
878 1897 */
879 - private function are_credentials_managed(): bool {
880 - $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 + }
881 1902
882 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
883 - return false;
1903 + $options = $this->get_options();
1904 +
1905 + if ( $options['disable_amp'] ) {
1906 + return '';
884 1907 }
885 1908
886 - 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' ) );
887 1911 }
888 1912
889 1913 /**
890 - * Sets the values of managed options.
1914 + * Add amp analytics.
891 1915 *
892 - * This function won't accept managing credentials or certain plugin options
893 - * that are being managed through other means. For managing credentials,
894 - * please use the `wp_parsely_credentials` filter.
895 - *
896 - * @since 3.9.0
897 - * @access private
1916 + * @param type $analytics The analytics object you want to add.
1917 + * @return type
898 1918 */
899 - private function set_managed_options(): void {
900 - $managed_options = apply_filters( 'wp_parsely_managed_options', false );
1919 + public function parsely_add_amp_analytics( $analytics ) {
1920 + $options = $this->get_options();
901 1921
902 - if ( ! is_array( $managed_options ) ) {
903 - return;
1922 + if ( empty( $options['apikey'] ) ) {
1923 + return $analytics;
904 1924 }
905 1925
906 - // Don't allow certain options to be set as managed.
907 - unset(
908 - $managed_options['apikey'],
909 - $managed_options['api_secret'],
910 - $managed_options['metadata_secret'],
911 - $managed_options['track_post_types'],
912 - $managed_options['track_page_types'],
913 - $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 + ),
914 1934 );
915 1935
916 - if ( 0 === count( $managed_options ) ) {
917 - return;
918 - }
919 -
920 - /**
921 - * Current options.
922 - *
923 - * @var Parsely_Options $current_options
924 - */
925 - $current_options = get_option( self::OPTIONS_KEY, array() );
926 -
927 - // Set managed options values.
928 - foreach ( $managed_options as $key => $value ) {
929 - $is_option_valid = isset( $this->option_defaults[ $key ] );
930 -
931 - if ( $is_option_valid ) {
932 - if ( null === $value ) {
933 - // When null, the option gets its value from the database.
934 - $this->managed_options[ $key ] =
935 - $current_options[ $key ] ?? $this->option_defaults[ $key ];
936 - } else {
937 - $this->managed_options[ $key ] =
938 - $this->sanitize_managed_option( $key, $value );
939 - }
940 - }
941 - }
1936 + return $analytics;
942 1937 }
943 1938
944 1939 /**
945 - * Sanitizes the value of the passed managed option.
1940 + * Add amp native analytics.
946 1941 *
947 - * @since 3.9.0
948 - * @access private
949 - *
950 - * @param string $option_id The option's ID.
951 - * @param bool|string $value The option's value.
952 - * @return bool|string The sanitized option value.
1942 + * @param type $analytics The analytics object you want to add.
1943 + * @return string|type
953 1944 */
954 - private function sanitize_managed_option( string $option_id, $value ) {
955 - $option_value_type = gettype( $this->option_defaults[ $option_id ] );
1945 + public function parsely_add_amp_native_analytics( $analytics ) {
1946 + $options = $this->get_options();
956 1947
957 - if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
958 - _doing_it_wrong(
959 - __FUNCTION__,
960 - esc_html(
961 - sprintf( /* translators: 1: Option ID */
962 - __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
963 - $option_id
964 - )
965 - ),
966 - ''
967 - );
1948 + if ( ! empty( $options['disable_amp'] ) && true === $options['disable_amp'] ) {
1949 + return '';
1950 + }
968 1951
969 - return false;
1952 + if ( empty( $options['apikey'] ) ) {
1953 + return $analytics;
970 1954 }
971 1955
972 - if ( 'string' === $option_value_type ) {
973 - if ( ! is_string( $value ) ) {
974 - _doing_it_wrong(
975 - __FUNCTION__,
976 - esc_html(
977 - sprintf( /* translators: 1: Option ID */
978 - __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
979 - $option_id
980 - )
1956 + $analytics['parsely'] = array(
1957 + 'type' => 'parsely',
1958 + 'attributes' => array(),
1959 + 'config' => wp_json_encode(
1960 + array(
1961 + 'vars' => array(
1962 + 'apikey' => $options['apikey'],
981 1963 ),
982 - ''
983 - );
1964 + )
1965 + ),
1966 + );
984 1967
985 - $value = strval( $value );
986 - }
1968 + return $analytics;
1969 + }
987 1970
988 - // String options that are restricted to specific values.
989 - $restricted_value_options = array(
990 - 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
991 - 'meta_type' => array( 'json_ld', 'repeated_metas' ),
992 - );
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 + }
993 1980
994 - // Verify that the above values are respected.
995 - foreach ( $restricted_value_options as $option_key => $valid_values ) {
996 - if ( $option_id === $option_key ) {
997 - if ( ! in_array( $value, $valid_values, true ) ) {
998 - _doing_it_wrong(
999 - __FUNCTION__,
1000 - esc_html(
1001 - sprintf( /* translators: 1: Option value 2: Option ID */
1002 - __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
1003 - $value,
1004 - $option_id
1005 - )
1006 - ),
1007 - ''
1008 - );
1981 + /**
1982 + * Why is this here?
1983 + */
1984 + public function return_personalized_json() {
1009 1985
1010 - $value = $this->option_defaults[ $option_id ];
1011 - }
1012 - }
1013 - }
1014 - }
1015 -
1016 - return $value;
1017 1986 }
1018 1987
1019 1988 /**
1020 - * Allows remote requests to Parse.ly.
1989 + * Convert JSON-LD type to respective Parse.ly page type.
1021 1990 *
1022 - * 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.
1023 1993 *
1024 - * @since 3.13.0
1025 - * @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".
1026 2000 */
1027 - private function allow_parsely_remote_requests(): void {
1028 - $allowed_urls = array(
1029 - self::DASHBOARD_BASE_URL,
1030 - Content_API_Service::get_base_url(),
1031 - Suggestions_API_Service::get_base_url(),
1032 - );
1033 -
1034 - add_filter(
1035 - 'http_request_host_is_external',
1036 - function ( $external, $host, $url ) use ( $allowed_urls ) {
1037 - // Check if the URL matches any URLs on the allowed list.
1038 - foreach ( $allowed_urls as $allowed_url ) {
1039 - if ( Utils::str_starts_with( $url, $allowed_url ) ) {
1040 - return true;
1041 - }
1042 - }
1043 - return $external;
1044 - },
1045 - 10,
1046 - 3
1047 - );
2001 + public function convert_jsonld_to_parsely_type( $type ) {
2002 + return in_array( $type, $this->supported_jsonld_post_types ) ? 'post' : 'index';
1048 2003 }
1049 2004 }