| @@ -178,9 +178,9 @@ | ||
| 178 | 178 | if ($value === '') { |
| 179 | 179 | return false; |
| 180 | 180 | } |
| 181 | 181 | # 'default' text domain, matching the __() call core itself uses. |
| 182 | - if ($value === 'Auto Draft' || $value === trim(__('Auto Draft'))) { | |
| 182 | + if ($value === 'Auto Draft' || $value === trim(__('Auto Draft', 'default'))) { // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- resolves core's own 'Auto Draft' placeholder, which only exists in core's 'default' domain; the plugin text domain cannot reproduce it | |
| 183 | 183 | return true; |
| 184 | 184 | } |
| 185 | 185 | # Registry arm: a variant captured in another locale's admin request. |
| 186 | 186 | # Without it the guard above is English-only on every non-admin request, |
| @@ -241,9 +241,9 @@ | ||
| 241 | 241 | * |
| 242 | 242 | * @return void |
| 243 | 243 | */ |
| 244 | 244 | public static function seed_auto_draft_placeholder() { |
| 245 | - self::remember_auto_draft_placeholder(__('Auto Draft')); | |
| 245 | + self::remember_auto_draft_placeholder(__('Auto Draft', 'default')); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- resolves core's own 'Auto Draft' placeholder, which only exists in core's 'default' domain; the plugin text domain cannot reproduce it | |
| 246 | 246 | } |
| 247 | 247 | |
| 248 | 248 | /** |
| 249 | 249 | * Read a persisted social meta value, collapsing the "Auto Draft" placeholder |
| @@ -730,9 +730,9 @@ | ||
| 730 | 730 | |
| 731 | 731 | # LPS / custom-HTML pages bake their own OG/social tags into their HTML bundle, |
| 732 | 732 | # served before wp_head — so this box does nothing on them. Hide it; the SEO |
| 733 | 733 | # read-only notice covers the messaging. |
| 734 | - $lps_post_id = isset($_GET['post']) ? intval($_GET['post']) : (isset($_POST['post_ID']) ? intval($_POST['post_ID']) : 0); | |
| 734 | + $lps_post_id = isset($_GET['post']) ? intval($_GET['post']) : (isset($_POST['post_ID']) ? intval($_POST['post_ID']) : 0); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended -- read-only post ID to pick meta boxes | |
| 735 | 735 | if (function_exists('metasync_is_custom_or_lps_page') && $lps_post_id > 0 && metasync_is_custom_or_lps_page($lps_post_id)) { |
| 736 | 736 | return; |
| 737 | 737 | } |
| 738 | 738 | |
| @@ -742,8 +742,9 @@ | ||
| 742 | 742 | |
| 743 | 743 | foreach ($post_types as $post_type) { |
| 744 | 744 | add_meta_box( |
| 745 | 745 | self::META_BOX_ID, |
| 746 | + /* translators: %s: effective plugin name (whitelabel-aware). */ | |
| 746 | 747 | sprintf(esc_html__('Social Media & Open Graph by %s', 'metasync'), $plugin_name), |
| 747 | 748 | [$this, 'render_meta_box'], |
| 748 | 749 | $post_type, |
| 749 | 750 | 'normal', |
| @@ -1044,9 +1045,9 @@ | ||
| 1044 | 1045 | # remember_auto_draft_placeholder() already skips. Verified against |
| 1045 | 1046 | # __( 'Auto Draft' ), never trusted as a bare string, so a builder's |
| 1046 | 1047 | # custom-seeded auto-draft title is never captured. |
| 1047 | 1048 | if ($post->post_status === 'auto-draft' |
| 1048 | - && $title !== '' && $title === trim(__('Auto Draft'))) { | |
| 1049 | + && $title !== '' && $title === trim(__('Auto Draft', 'default'))) { // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- resolves core's own 'Auto Draft' placeholder, which only exists in core's 'default' domain; the plugin text domain cannot reproduce it | |
| 1049 | 1050 | self::remember_auto_draft_placeholder($title); |
| 1050 | 1051 | } |
| 1051 | 1052 | |
| 1052 | 1053 | # Flag maintenance: recognition is context-independent thanks to the |
| @@ -1238,12 +1239,19 @@ | ||
| 1238 | 1239 | $og_image = Metasync_Seo_Precedence::value($post->ID, Metasync_Seo_Precedence::FIELD_OG_IMAGE) |
| 1239 | 1240 | ?: $this->get_featured_image_url($post->ID); |
| 1240 | 1241 | $canonical_override = ''; |
| 1241 | 1242 | if (class_exists('Metasync_Headless_Config') && Metasync_Headless_Config::is_active()) { |
| 1242 | - # The same two-key chain the canonical tag itself resolves, so the | |
| 1243 | - # two cannot disagree. meta_canonical is not a legacy alias: the | |
| 1244 | - # REST sync and the classic-editor Canonical box both still write | |
| 1245 | - # it, and the GraphQL surface reads both keys as well. | |
| 1243 | + # The two manual keys of the canonical chain, in the same order the | |
| 1244 | + # canonical tag resolves them. OTTO's volatile staging tier is | |
| 1245 | + # deliberately left out: headless rehosts derived WordPress URLs, | |
| 1246 | + # and an OTTO suggestion is not the author's answer about where | |
| 1247 | + # content lives — a staged cross-domain canonical must not flow | |
| 1248 | + # into headless og:url. The two can therefore disagree with the | |
| 1249 | + # WordPress-render canonical tag only when OTTO is staging a | |
| 1250 | + # canonical and no manual value exists; that is the documented | |
| 1251 | + # scope split. meta_canonical is not a legacy alias: the REST sync | |
| 1252 | + # and the classic-editor Canonical box both still write it, and | |
| 1253 | + # the GraphQL surface reads both keys as well. | |
| 1246 | 1254 | foreach (array('_metasync_canonical_url', 'meta_canonical') as $canonical_key) { |
| 1247 | 1255 | $canonical_override = Metasync_Canonical_Sanitizer::sanitize( |
| 1248 | 1256 | get_post_meta($post->ID, $canonical_key, true) |
| 1249 | 1257 | ); |
| @@ -1768,9 +1776,8 @@ | ||
| 1768 | 1776 | $preview_html = $this->generate_preview_html($title, $description, $image, $url, $twitter_title, $twitter_description, $twitter_image); |
| 1769 | 1777 | |
| 1770 | 1778 | if (empty($preview_html)) { |
| 1771 | 1779 | wp_send_json_error(['message' => 'Failed to generate preview HTML']); |
| 1772 | - return; | |
| 1773 | 1780 | } |
| 1774 | 1781 | |
| 1775 | 1782 | wp_send_json_success(['preview' => $preview_html]); |
| 1776 | 1783 | |
| @@ -1785,9 +1792,9 @@ | ||
| 1785 | 1792 | private function generate_preview_html($title, $description, $image, $url, $twitter_title = '', $twitter_description = '', $twitter_image = '') { |
| 1786 | 1793 | # Parse domain from URL |
| 1787 | 1794 | $domain = ''; |
| 1788 | 1795 | if (!empty($url)) { |
| 1789 | - $parsed = parse_url($url); | |
| 1796 | + $parsed = wp_parse_url($url); | |
| 1790 | 1797 | $domain = $parsed['host'] ?? ''; |
| 1791 | 1798 | } |
| 1792 | 1799 | |
| 1793 | 1800 | # Fallback to site URL if no domain found |
| @@ -1792,9 +1799,9 @@ | ||
| 1792 | 1799 | |
| 1793 | 1800 | # Fallback to site URL if no domain found |
| 1794 | 1801 | if (empty($domain)) { |
| 1795 | 1802 | $site_url = get_site_url(); |
| 1796 | - $parsed = parse_url($site_url); | |
| 1803 | + $parsed = wp_parse_url($site_url); | |
| 1797 | 1804 | $domain = $parsed['host'] ?? 'your-site.com'; |
| 1798 | 1805 | } |
| 1799 | 1806 | |
| 1800 | 1807 | # Provide fallbacks for empty values |
| @@ -2371,11 +2378,20 @@ | ||
| 2371 | 2378 | /** |
| 2372 | 2379 | * Check if post slug changed via edit slug functionality |
| 2373 | 2380 | */ |
| 2374 | 2381 | public function check_slug_change() { |
| 2382 | + # Fires on the same admin-ajax request as core's sample-permalink | |
| 2383 | + # handler, but at priority 5 — before core validates the nonce and | |
| 2384 | + # capability at its own priority-10 callback. Verify them ourselves | |
| 2385 | + # before any post-meta write. Return (not die) so core still serves | |
| 2386 | + # the preview request normally. | |
| 2387 | + if (!check_ajax_referer('samplepermalink', 'samplepermalinknonce', false)) { | |
| 2388 | + return; | |
| 2389 | + } | |
| 2390 | + | |
| 2375 | 2391 | # Get the post ID from the request |
| 2376 | 2392 | $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; |
| 2377 | - if (!$post_id) { | |
| 2393 | + if (!$post_id || !current_user_can('edit_post', $post_id)) { | |
| 2378 | 2394 | return; |
| 2379 | 2395 | } |
| 2380 | 2396 | |
| 2381 | 2397 | # Only check for supported post types |
| @@ -2408,9 +2424,9 @@ | ||
| 2408 | 2424 | # This means the og:url was set to the post permalink (not a custom URL) |
| 2409 | 2425 | if ($current_og_url !== $current_permalink && strpos($current_og_url, '?p=') === false) { |
| 2410 | 2426 | # Check if the og:url was the old permalink by comparing with a generated old permalink |
| 2411 | 2427 | $old_post = clone $post; |
| 2412 | - $old_slug = isset($_POST['new_slug']) ? sanitize_title($_POST['new_slug']) : $post->post_name; | |
| 2428 | + $old_slug = isset($_POST['new_slug']) ? sanitize_title(wp_unslash($_POST['new_slug'])) : $post->post_name; | |
| 2413 | 2429 | |
| 2414 | 2430 | # If the og:url doesn't match the current permalink, it might be the old one |
| 2415 | 2431 | # We'll update it to the new permalink |
| 2416 | 2432 | if ($current_og_url !== $current_permalink) { |