true], 'names') as $postType) { if ('attachment' === $postType) { continue; } add_meta_box('rankifySeo', __('Crownify SEO', 'crownify'), [$this, 'render'], $postType, 'normal', 'high'); } } /** * The metabox lives outside the plugin's own page shell, so the prefixed * Tailwind bundle does not apply to it. Everything here is styled inline — * the same constraint the dashboard widget and admin notices work under. */ public function loadAssets($hook) { if (!in_array($hook, ['post.php', 'post-new.php'], true)) { return; } $screen = get_current_screen(); if (!$screen || !in_array($screen->post_type, get_post_types(['public' => true], 'names'), true)) { return; } wp_enqueue_script('rankify-editor', plugins_url('assets/js/editor.min.js', RANKIFY_FILE), ['wp-i18n', 'jquery', 'wp-api-request'], $this->assetVersion(), true); wp_set_script_translations('rankify-editor', 'crownify'); wp_localize_script('rankify-editor', 'rankify_editor_js_vars', [ 'metaVariables' => Variables::catalog(), 'metaSamples' => Variables::sampleValues(), 'homeUrl' => Utils::getHomeUrl('/', false), ]); } protected function assetVersion() { $path = RANKIFY_DIR_PATH . 'assets/js/editor.min.js'; $modified = file_exists($path) ? filemtime($path) : false; return $modified ? RANKIFY_VERSION . '.' . $modified : RANKIFY_VERSION; } public function render(\WP_Post $post) { wp_nonce_field(self::NONCE_ACTION, self::NONCE_NAME); $values = PostMeta::all($post->ID); // What this post will actually be served if the fields below stay empty. // // This used to render the site-wide template, which is wrong on any site // migrating from another SEO plugin: the value being output is the one // Yoast or Rank Math left behind, and showing the template instead made it // look as though nothing had been picked up. Resolver runs the whole chain, // legacy fallback included, so the placeholder is the truth. $context = $this->contextFor($post); Resolver::flush(); $inheritedTitle = Resolver::title($context); Resolver::flush(); $inheritedDescription = Resolver::description($context); Resolver::flush(); // Named so the editor can say where a value came from rather than leaving // the user to guess why a field is empty but the output is not. $legacySource = LegacyMeta::sourceForPost($post->ID); // Search Console is a premium class. class_exists is what survives the // free build, where the folder is removed; hasActivePro is the licence check. $rankifySearchConsole = ['state' => 'disconnected']; if (Utils::hasActivePro() && class_exists('\DaftPlug\Rankify\Features\SearchConsole')) { $rankifySearchConsole = \DaftPlug\Rankify\Features\SearchConsole::forPost($post); } include RANKIFY_DIR_PATH . 'includes/features/editor/views/metabox.php'; } /** * Build a Context for an arbitrary post without touching the global query — * this runs in wp-admin, where the main query is about the edit screen. */ protected function contextFor(\WP_Post $post) { $context = new Context(); $context->type = Context::TYPE_SINGULAR; $context->object = $post; $context->objectId = (int) $post->ID; $context->postType = $post->post_type; $context->settingsKey = $post->post_type; return $context; } public function save($postId, $post) { // save_post fires for autosaves, revisions and bulk edits too. Writing on // any of those would persist an empty payload over the real values. if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (wp_is_post_revision($postId) || wp_is_post_autosave($postId)) { return; } // Absent nonce means this save did not come from the editor screen — a // REST or WP-CLI update, for instance. Those are legitimate; they simply // are not this form, so there is nothing here to save. if (!isset($_POST[self::NONCE_NAME])) { return; } if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::NONCE_NAME])), self::NONCE_ACTION)) { return; } // The role manager can only narrow this, never widen it: `edit_post` is // still required, and the filter is absent entirely in the free build. if (!apply_filters('rankify_user_can_edit_meta', current_user_can('edit_post', $postId), $postId)) { return; } $payload = []; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above. $submitted = isset($_POST['rankify']) && is_array($_POST['rankify']) ? wp_unslash($_POST['rankify']) : []; foreach (array_keys(PostMeta::fields()) as $field) { if (array_key_exists($field, $submitted)) { $payload[$field] = $submitted[$field]; } } // Toggles are absent from $_POST when unchecked, and PostMeta::save() // relies on that distinction, so they are passed through as-is. PostMeta::save($postId, $payload); // Cornerstone is cached as a set, so the set has to be dropped when one // post joins or leaves it. \DaftPlug\Rankify\Features\Meta\SchemaTypes::flushCornerstone(); /** * Lets features owning their own metabox fields save alongside these. * * The whole $_POST is passed rather than a filtered slice: a feature that * adds fields knows their names, and this class should not have to. * * @param int $postId * @param array $request Raw, unslashed. Nonce and capability already checked. */ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above. do_action('rankify_save_post_meta', $postId, wp_unslash($_POST)); } }