array($this, 'fiftyonedegrees_sanitize_resource_key')]); // Suspicious activity detection settings. add_option(Options::SUSPICIOUS_ENABLE, 'off'); add_option(Options::SUSPICIOUS_REDIRECT_URL, ''); add_option(Options::SUSPICIOUS_REQUESTS, 5); add_option(Options::SUSPICIOUS_WINDOW, 30); register_setting(Options::SUSPICIOUS_GROUP_KEY, Options::SUSPICIOUS_ENABLE, [ 'type' => 'string', 'sanitize_callback' => function ($v) { return $v === 'on' ? 'on' : 'off'; }, 'default' => 'off', ]); register_setting(Options::SUSPICIOUS_GROUP_KEY, Options::SUSPICIOUS_REDIRECT_URL, [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'default' => '', ]); register_setting(Options::SUSPICIOUS_GROUP_KEY, Options::SUSPICIOUS_REQUESTS, [ 'type' => 'integer', 'sanitize_callback' => function ($v) { return max(1, (int) $v); }, 'default' => 5, ]); register_setting(Options::SUSPICIOUS_GROUP_KEY, Options::SUSPICIOUS_WINDOW, [ 'type' => 'integer', 'sanitize_callback' => function ($v) { return max(1, min(3600, (int) $v)); }, 'default' => 30, ]); register_setting(Options::ROBOTS_GROUP_KEY, Options::ROBOTS_ENABLE); register_setting(Options::ROBOTS_GROUP_KEY, Options::ROBOTS_ENFORCE); register_setting( Options::ROBOTS_GROUP_KEY, Options::ROBOTS_REDIRECT_URL, ['sanitize_callback' => ['FiftyoneService', 'sanitize_robots_redirect_url']]); register_setting( Options::ROBOTS_GROUP_KEY, Options::ROBOTS_CUSTOM_TOP, ['sanitize_callback' => ['FiftyoneService', 'sanitize_robots_textarea']]); register_setting( Options::ROBOTS_GROUP_KEY, Options::ROBOTS_CUSTOM_BOTTOM, ['sanitize_callback' => ['FiftyoneService', 'sanitize_robots_textarea']]); register_setting( Options::ROBOTS_GROUP_KEY, Options::ROBOTS_ALLOWED_CATEGORIES, ['sanitize_callback' => ['FiftyoneService', 'sanitize_categories']]); register_setting( Options::ROBOTS_GROUP_KEY, Options::ROBOTS_STANDARD_TDL_SELECTED, ['sanitize_callback' => ['FiftyoneService', 'sanitize_standard_tdl_selected']]); register_setting( Options::ROBOTS_GROUP_KEY, Options::ROBOTS_CUSTOM_TDL, ['sanitize_callback' => ['FiftyoneService', 'sanitize_tdl']]); register_setting(Options::GROUP_KEY, Options::PIPELINE_ENABLE); // PMP (Preference Management Platform) settings. add_option(Options::PMP_ENABLE, 'off'); add_option(Options::PMP_TCF_VENDOR_STRING, ''); add_option(Options::PMP_ALT_LABEL, ''); add_option(Options::PMP_ALT_URL, ''); add_option(Options::PMP_BRAND_NAME, ''); add_option(Options::PMP_BRAND_LOGO_URL, ''); add_option(Options::PMP_BRAND_TERMS_URL, ''); add_option(Options::PMP_SHOW_STANDARD, 'off'); register_setting(Options::PMP_GROUP_KEY, Options::PMP_ENABLE, [ 'type' => 'string', 'sanitize_callback' => ['FiftyoneService', 'sanitize_pmp_enable'], 'default' => 'off', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_TCF_VENDOR_STRING, [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_ALT_LABEL, [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_ALT_URL, [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_BRAND_NAME, [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_BRAND_LOGO_URL, [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_BRAND_TERMS_URL, [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', ]); register_setting(Options::PMP_GROUP_KEY, Options::PMP_SHOW_STANDARD, [ 'type' => 'string', 'sanitize_callback' => function ($v) { return $v === 'on' ? 'on' : 'off'; }, 'default' => 'off', ]); } static function sanitize_robots_textarea($value) { return sanitize_textarea_field($value); } static function sanitize_robots_redirect_url($value) { return esc_url_raw($value); } static function sanitize_standard_tdl_selected($value) { if (!is_array($value)) { return []; } $valid_ids = array_column(FiftyOneDegreesStandardTdls::load(), 'id'); return array_values(array_unique(array_intersect($value, $valid_ids))); } static function sanitize_categories($value) { if (!is_array($value)) { return []; } $available = FiftyOneDegreesCloudMetadata::fetch_crawler_usage_values(); $valid = array_keys($available); if (empty($valid)) { $valid = ['Index', 'Train', 'Input', 'Search', 'Monitor', 'Archiving', 'Preview', 'Security', 'Analytics', 'Feed', 'Discovery']; } return array_values(array_intersect($value, $valid)); } static function sanitize_tdl($value) { if (is_string($value)) { $lines = explode("\n", $value); } elseif (is_array($value)) { $lines = $value; } else { return []; } $clean = []; foreach ($lines as $url) { $url = esc_url_raw(trim($url)); if (!empty($url)) { $clean[] = $url; } } return array_values(array_unique($clean)); } /** * Sanitizer for Options::PMP_ENABLE. When the caller asks for 'on', * verifies that fields without a sensible default are filled in. * Missing values force the option back to 'off' and register a * settings error listing the missing field labels. * * @param string $value the submitted PMP_ENABLE value * @return string 'on' if validation passes, 'off' otherwise */ static function sanitize_pmp_enable($value) { if ($value !== 'on') { return 'off'; } // Terms / Privacy URL plus the two alternative-button fields // are required server-side. PMP widget rejects an empty alt // button outright; Terms URL has no runtime fallback. The // remaining fields (TCF Vendor String, Brand Name) have // sensible runtime defaults so they stay optional. $required = [ Options::PMP_BRAND_TERMS_URL => 'Terms / Privacy URL', Options::PMP_ALT_LABEL => 'Alternative Button Label', Options::PMP_ALT_URL => 'Alternative Button URL', ]; $missing = []; foreach ($required as $key => $label) { $raw = isset($_POST[$key]) ? wp_unslash($_POST[$key]) : ''; if (trim((string) $raw) === '') { $missing[] = $label; } } if (!empty($missing)) { add_settings_error( Options::PMP_GROUP_KEY, 'pmp_required_missing', sprintf( /* translators: %s is a comma-separated list of field labels. */ __('PMP cannot be enabled: missing required field(s): %s.', 'fiftyonedegrees'), implode(', ', $missing) ), 'error' ); return 'off'; } return 'on'; } /** * Register the JSON endpoint for the pipeline. This is where the * JavaScript will callback to instead of the 51Degrees domain. * * @return void */ function fiftyonedegrees_rest_api_init() { register_rest_route('fiftyonedegrees/v4', "json", array( 'methods' => 'POST', 'args' => array(), 'callback' => array('Pipeline','getJSON'), 'permission_callback' => '__return_true' )); } /** * Checks if the Resource Key has been changed, and stores the new one * if it has. When the new option has been updated, the pipeline will be * rebuilt. * * @return void */ function submit_rk_submit_action() { if (isset($_POST[Options::RESOURCE_KEY]) && isset($_POST["action"]) && $_POST[Options::RESOURCE_KEY] !== get_option(Options::RESOURCE_KEY)) { $resource_key = sanitize_text_field(wp_unslash( $_POST[Options::RESOURCE_KEY])); update_option(Options::RESOURCE_KEY, $resource_key); // update_option above triggers a synchronous pipeline rebuild // that sets PIPELINE_VALIDATION_ERROR on failure. if (!get_option(Options::PIPELINE_VALIDATION_ERROR)) { if (get_option(Options::ENABLE_GA) && get_option(Options::RESOURCE_KEY_UPDATED)) { wp_redirect(get_admin_url() . 'options-general.php?page=51Degrees&tab=google-analytics'); exit(); } } else { wp_redirect(get_admin_url() . 'options-general.php?page=51Degrees&tab=setup' ); exit(); } } } /** * Add stylesheet for admin pages. * * @return void */ function fiftyonedegrees_admin_enqueue_scripts() { wp_enqueue_style( "fiftyonedegrees_admin_styles", plugin_dir_url(__FILE__) . "../assets/css/fod.css"); wp_enqueue_style( "fiftyonedegrees_admin_styles_icons", "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"); wp_enqueue_script( "fiftyonedegrees_jQuery", plugin_dir_url(__FILE__) . '../assets/js/51D.js', array('jquery') #dependencies ); } /** * After any option is updated, check if the option was something that * needs to be taken care of. For Resource Key, the flow data needs to * be removed from the session cache, and a new pipeline created. * * @return void */ function fiftyonedegrees_update_option($option, $old_value, $new_value) { if ($option === Options::RESOURCE_KEY) { // Remove the cached flowdata from the session. if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION["fiftyonedegrees_data"])) { unset($_SESSION["fiftyonedegrees_data"]); update_option( Options::SESSION_INVALIDATED, time()); } // Invalidate cloud metadata transient FiftyOneDegreesCloudMetadata::invalidate_all(); // Stale against the previous key — clear. delete_option(Options::ROBOTS_LAST_REFRESH); self::build_and_save_pipeline($new_value); if ($old_value !== $new_value) { update_option(Options::RESOURCE_KEY_UPDATED, true); delete_option(Options::GA_DIMENSIONS); } else { delete_option(Options::RESOURCE_KEY_UPDATED); } } // GA4 property selection invalidates any saved // property->parameter dimension mapping — the new property // may not share Custom Dimensions with the old one. if ($option === Options::GA_PROPERTY_ID && $old_value !== $new_value) { update_option(Options::GA_ID_UPDATED, true); delete_option(Options::GA_DIMENSIONS); } if ($option === Options::GA_SEND_PAGE_VIEW_VAL && $old_value !== $new_value) { update_option(Options::GA_SEND_PAGE_VIEW_UPDATED, true); } if ($option === Options::ROBOTS_ENFORCE && $new_value === 'on' && get_option(Options::PIPELINE_ENABLE, 'on') !== 'on') { update_option(Options::PIPELINE_ENABLE, 'on'); set_transient( 'fiftyonedegrees_pipeline_auto_enabled', 'Robots Enforce', 30); } if ($option === Options::PIPELINE_ENABLE && $new_value === 'off' && get_option(Options::ROBOTS_ENFORCE, 'off') === 'on') { update_option(Options::ROBOTS_ENFORCE, 'off'); } } /** * Builds the pipeline when the resource key option is first created, * e.g. via WP-CLI where update_option hook does not fire. * * @param string $option the option name * @param mixed $value the option value * @return void */ function fiftyonedegrees_add_option($option, $value) { if ($option === Options::RESOURCE_KEY && $value) { self::build_and_save_pipeline($value); } } /** * register_setting sanitize_callback for the Resource Key. Runs on every * settings save, even when the value is unchanged — which is the only * place we can re-validate an unchanged key. WordPress fires * updated_option (and the rebuild wired to it in fiftyonedegrees_update_option) * only when the value actually changes, so without this an unchanged * re-save never re-validates and a stale "rejected" error persists even * after its cause is fixed. Changed keys are left to the updated_option * path to avoid a duplicate cloud call. * * @param mixed $value the submitted resource key * @return mixed the value to persist (unchanged) */ static function fiftyonedegrees_sanitize_resource_key($value) { if ($value === get_option(Options::RESOURCE_KEY)) { self::build_and_save_pipeline($value); } return $value; } // On error: leave PIPELINE alone, record error string for setup.php. private static function build_and_save_pipeline($resource_key) { // Empty key: nothing to validate. Drop all cached state so the setup // tab renders clean (no stale red box from a previous failed save). if (empty($resource_key)) { delete_option(Options::PIPELINE); delete_option(Options::PIPELINE_VALIDATION_ERROR); return; } $pipeline = Pipeline::make_pipeline($resource_key); if ($pipeline && !isset($pipeline['error'])) { update_option(Options::PIPELINE, $pipeline); delete_option(Options::PIPELINE_VALIDATION_ERROR); return; } if (is_array($pipeline) && isset($pipeline['error']) && $pipeline['error'] !== '') { update_option(Options::PIPELINE_VALIDATION_ERROR, (string) $pipeline['error']); } } /** * Called on admin_init. When returning to the robots settings page after a * settings save (WordPress adds settings-updated=true to the URL), triggers * an immediate Cloud API call to refresh the cached robots.txt content. * * @return void */ function fiftyonedegrees_re_generate_robots() { if (!isset($_GET['settings-updated'])) { return; } $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; $tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : ''; if ($page !== '51Degrees' || $tab !== 'robots') { return; } $success = FiftyOneDegreesRobotsTxt::fetch_from_cloud(); if ($success) { set_transient('fiftyonedegrees_robots_generate_success', true, 30); } } /** * Displays an admin notice when device detection was automatically enabled * because a dependent feature (e.g. Robots Enforce) requires it. * * @return void */ function fiftyonedegrees_pipeline_autoenable_notice() { $feature = get_transient('fiftyonedegrees_pipeline_auto_enabled'); if ($feature === false) { return; } delete_transient('fiftyonedegrees_pipeline_auto_enabled'); echo '

' . '51Degrees: Device detection was automatically ' . 'enabled because ' . esc_html($feature) . ' requires it.' . '

'; } function fiftyonedegrees_suspicious_toggle_failed_notice() { $message = get_transient('fiftyonedegrees_suspicious_toggle_failed'); if ($message === false) { return; } delete_transient('fiftyonedegrees_suspicious_toggle_failed'); echo '

' . '51Degrees: ' . esc_html($message) . '

'; } /** * Rebuilds the pipeline when the permalink structure changes. * Hooked to 'updated_option' (fires after the DB write) so that * rest_url() returns the URL for the new permalink structure. * * @return void */ function fiftyonedegrees_updated_option($option, $old_value, $new_value) { if ($option === 'permalink_structure') { $resource_key = get_option(Options::RESOURCE_KEY); if ($resource_key) { if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION["fiftyonedegrees_data"])) { unset($_SESSION["fiftyonedegrees_data"]); update_option(Options::SESSION_INVALIDATED, time()); } self::build_and_save_pipeline($resource_key); } } } /** * Synchronously rebuild the cached pipeline whenever * SUSPICIOUS_ENABLE is toggled. The cached pipeline's engine list is * the single source of truth for both engine selection and the * query.id.usage evidence decision; keep it in sync on every toggle. */ public function fiftyonedegrees_suspicious_enable_updated($option, $old_value, $value) { if ($option !== Options::SUSPICIOUS_ENABLE) { return; } // $rebuilding = false reset via finally because the revert's update_option re-fires this hook; WP dispatcher guarantees the re-entry path. static $rebuilding = false; if ($rebuilding) { return; } $rebuilding = true; try { update_option(Options::SESSION_INVALIDATED, time()); $resource_key = get_option(Options::RESOURCE_KEY); if (empty($resource_key)) { return; } delete_option(Options::PIPELINE_VALIDATION_ERROR); self::build_and_save_pipeline($resource_key); $had_error = (bool) get_option(Options::PIPELINE_VALIDATION_ERROR); if ($had_error) { update_option(Options::SUSPICIOUS_ENABLE, $old_value); delete_option(Options::PIPELINE_VALIDATION_ERROR); set_transient( 'fiftyonedegrees_suspicious_toggle_failed', 'Suspicious activity detection setting could not be applied — the ' . '51Degrees cloud was unreachable. Your previous setting was ' . 'preserved.', MINUTE_IN_SECONDS * 10 ); } } finally { $rebuilding = false; } } /** * Register the options page for the plugin. * * @return void */ function fiftyonedegrees_register_options_page() { add_options_page( '51Degrees', '51Degrees', 'manage_options', '51Degrees', array($this, 'fiftyonedegrees_admin_page')); } /** * Set the link to settings for this plugin. * * @param string[] $links array of links to add to. * @return string[] updated array of links. */ function fiftyonedegrees_add_plugin_page_settings_link($links) { $links[] = '' . __('Settings') . ''; return $links; } /** * Inlude the admin page for this plugin. * * @return void */ function fiftyonedegrees_admin_page() { include plugin_dir_path(__FILE__) . "../admin.php"; } /** * Add the 51Degrees JavaScript to the page. * * @return void */ function fiftyonedegrees_javascript() { wp_enqueue_script( "fiftyonedegrees", plugin_dir_url(__FILE__) . "../assets/js/fod.js"); wp_add_inline_script( "fiftyonedegrees", Pipeline::getJavaScript(), "before"); if (get_option(Options::PMP_ENABLE, 'off') !== 'on') { return; } if (empty(get_option(Options::RESOURCE_KEY))) { return; } $url = self::pmp_resolve_script_url(); if (empty($url)) { return; } // Issue #61: skip enqueue on the configured alt-button destination and // terms/privacy pages — those are special-case for TCF/PMP (the visitor // either landed there AS an alternative to PMP, or clicked the popup's // own terms link to read them; wrapping such pages with PMP again is a // UX/legal contradiction). if (self::current_page_is_pmp_exempt()) { return; } // Footer load: PMP attaches its popup container as a sibling of this // script tag (see view.ts getRoot()). In that container would // be unrenderable (head has display:none). wp_register_script('fiftyonedegrees-pmp', $url, [], null, true); wp_enqueue_script('fiftyonedegrees-pmp'); // Publisher-overridable continuation hook. Site owners can use // it to bootstrap anything that depends on the visitor's // preference -- analytics, Prebid.js initialisation, lazy ad // stacks, etc. -- by defining their own window.onPMPCompletion. // The default is a no-op. PMP itself acts as the CMP via TCF // API, so no separate consent manager is needed. wp_add_inline_script( 'fiftyonedegrees-pmp', '(function(){window.onPMPCompletion=window.onPMPCompletion||function(preference){};})();', 'before' ); add_filter('script_loader_tag', [$this, 'pmp_add_data_attributes'], 10, 3); } /** * Composes the PMP loader address. Returns an empty string when the * resource key is missing, and the enqueue path uses that to * short-circuit registration. * * The Resource Key goes in the path and the address ends in '.js', * because the loader served from that address works out where to * fetch the rest of PMP from by reading its own 'src' attribute, * dropping any query string and any '.js' ending, then appending the * locale it picked. A query-string form leaves nothing in the path * for the loader to carry the key forward with, so the key has to be * a path segment. rawurlencode is still the right escape, because * the key is now a path segment rather than a query value. * * The base URL comes from FiftyOneDegreesCloudMetadata, which honours * the FOD_CLOUD_API_URL env var used across the plugin (robots, * suspicious, cloud metadata) and falls back to * https://cloud.51degrees.com when unset. * * The locale is chosen in the browser by the loader, which matches * navigator.languages against the locales it was built with and * falls back to en-us, so no allowlist and no locale handling lives * in the plugin. * * @return string */ private static function pmp_resolve_script_url() { $key = get_option(Options::RESOURCE_KEY); if (empty($key)) { return ''; } return sprintf( '%s/api/v4/pmp/%s.js', FiftyOneDegreesCloudMetadata::get_cloud_host_url(), rawurlencode($key)); } /** * Returns true when the current request matches the page configured as * PMP_ALT_URL (alternative-button destination) or PMP_BRAND_TERMS_URL * (terms/privacy linked from the PMP popup). On those pages the PMP * script must not be enqueued — see issue #61. * * Match strategy mirrors SuspiciousActivity::is_on_redirect_target(): * post-ID match first (handles internal pages with pretty permalinks), * canonical path match as fallback (handles trailing slash, query string, * full-URL option storage). * * Returns false when both options are empty or normalize to '/' — a * root-path target would otherwise exempt every page on the site. * * The result is filterable via 'fiftyonedegrees_pmp_exempt' so sites * with i18n plugins (WPML / TranslatePress) can broaden the match * across translated slugs. */ private static function current_page_is_pmp_exempt() { $targets = array_filter([ get_option(Options::PMP_ALT_URL), get_option(Options::PMP_BRAND_TERMS_URL), ]); // Drop targets that normalize to root '/' — treating those as // configured would exempt every page on the site (foot-gun guard). $targets = array_filter($targets, function ($t) { $p = trailingslashit(wp_parse_url($t, PHP_URL_PATH) ?: '/'); return strtolower($p) !== '/'; }); if (empty($targets)) { return false; } $current_uri = wp_unslash($_SERVER['REQUEST_URI'] ?? '/'); $current_path = strtolower(rawurldecode(trailingslashit(strtok($current_uri, '?')))); $current_id = function_exists('url_to_postid') && function_exists('home_url') ? (int) url_to_postid(home_url($current_uri)) : 0; // Subdir multisite / WP-in-subdir: REQUEST_URI starts with the site's // path prefix (e.g. '/blog/terms/'), while an option stored as a path- // only value ('/terms') would not. Also expose a stripped variant so // both shapes match. Single-site $home_path is '/' and stripping is // a no-op. $home_path = function_exists('home_url') ? strtolower(trailingslashit(wp_parse_url(home_url('/'), PHP_URL_PATH) ?: '/')) : '/'; $current_path_stripped = ($home_path !== '/' && str_starts_with($current_path, $home_path)) ? '/' . substr($current_path, strlen($home_path)) : $current_path; $match = false; foreach ($targets as $target) { if ($current_id > 0) { $target_id = (int) url_to_postid($target); if ($target_id > 0 && $current_id === $target_id) { $match = true; break; } } $target_path = strtolower(rawurldecode(trailingslashit(wp_parse_url($target, PHP_URL_PATH) ?: '/'))); if ($current_path === $target_path || $current_path_stripped === $target_path) { $match = true; break; } } /** * Filter the PMP-exempt decision. * * @param bool $match Whether the current request matches an exempt URL. * @param string $current_path Canonical lowercased trailing-slashed path of the current request. * @param array $targets Non-empty exempt URLs (after the foot-gun filter). */ return (bool) apply_filters('fiftyonedegrees_pmp_exempt', $match, $current_path, $targets); } /** * Default TCF Vendor String -- core segment with consent granted * to every vendor, purpose and special feature from IAB GVL * version 158 (cmpId=51, publisherCountryCode=AA). Regenerate * via the @iabtcf/core script when the GVL bumps a major * version; admins can override per-site via the PMP settings. */ public const PMP_DEFAULT_TCF_VENDOR_STRING = 'CQkMqUAQkMqUAAfABAENCfFsAP_wAEPgAAAAMetR_G__bWlr-bb3abtkeYxP9_hr7sQxBgbIkm4FzLvW7JwHx2EZNAzatiIKmRIAu3TBIQNlHJDURUCgKIgFryDMaE2U4TNKJ6BkiFMZI2tQCFxvm4tjeQCY4ur_9kc1mB-t7dr82dzyy6hHn3a5fmS1UJCdIYetDfv8ZBOT-9IEd-x8v4v4_EbpEm8eS1n_pGtp4jc6Yns_dBmxt-Tyff7Pn__7l_e7X_ve_n3zv8oXn7rr____f_-7___2b_-___b-__7Z_zI_4MegAmGh0QRlkQKBAoCEECABQVhABQIAgAASAogIATBgQ5AwAXWESAEAKAAYIAQAAgwABAAAJAAhEAEABAIAQIBAoAAwAIAgIAGBgADABQiAQAAgOgYpgQQCBYAJEZUBpgQgAJBAS2VCCUDAgrhCkWOAQQIiYKAAAEAAoAAEB8LAQklBKxIIAuILoAEAAAAKIEWBFIWYAgqDNFoKwJOAyNMASPMEiSnQRAEwQkZBkQmqCQeKYohQQ5AbFLMAdPEFADLtZIQ_1As3AIAA.IMetX_H__bX9v-f736ft0eY1f9_j77uQxBhfJs-4FzLvW_JwX32E7NF36tqYKmRIEu3bBIQNtHJnUTVihaogVrzHsak2c4TtKJ-BkiHMZe29YCF5vm4tj-QKZ5_r_93d92T_9_dv-3dzy3_1nv3f9_-f1eLide5_tH_v_bROb-_I_9_7-_4v8_t_rk2_eT1v_9evv7__-________9_____________-____f________________________f__________9____4AA'; /** * Returns the configured TCF Vendor String or the built-in * all-vendors-enabled default when none is set. */ public static function pmp_tcf_vendor_string() { $value = get_option(Options::PMP_TCF_VENDOR_STRING); return empty($value) ? self::PMP_DEFAULT_TCF_VENDOR_STRING : $value; } /** * Returns the configured Brand Name or '51Degrees' when none is set. */ public static function pmp_brand_name() { $value = get_option(Options::PMP_BRAND_NAME); return empty($value) ? '51Degrees' : $value; } /** * Returns the configured Brand Logo URL or the bundled 51Degrees logo * when none is set. */ public static function pmp_brand_logo_url() { $value = get_option(Options::PMP_BRAND_LOGO_URL); return empty($value) ? FIFTYONEDEGREES_PLUGIN_URL . 'assets/images/logo.png' : $value; } /** * Returns the configured Alternative Button Label or 'Remove ads' when * none is set. */ public static function pmp_alt_label() { $value = get_option(Options::PMP_ALT_LABEL); return empty($value) ? 'Remove ads' : $value; } /** * Returns the configured Alternative Button URL or * 'https://example.com' when none is set. */ public static function pmp_alt_url() { $value = get_option(Options::PMP_ALT_URL); return empty($value) ? 'https://example.com' : $value; } /** * Adds PMP data-* attributes to the