repo = $repo ?: new Repository(); } public function hooks(): void { // Legacy parameter admin menu and product meta box are hidden from customer-facing UI for 1.0. // Handlers remain available internally for backward compatibility with existing data. add_action('save_post_product', [$this, 'saveProduct'], 20, 2); add_action('admin_post_facetcore_create_facet', [$this, 'createFacet']); add_action('admin_post_facetcore_create_value', [$this, 'createValue']); add_action('admin_post_facetcore_delete_facet', [$this, 'deleteFacet']); add_action('admin_post_facetcore_delete_value', [$this, 'deleteValue']); } public function menu(): void { add_submenu_page( 'woocommerce', __('FacetCore Legacy Parameters', 'facetcore'), __('Legacy parameters', 'facetcore'), 'manage_woocommerce', 'facetcore', [$this, 'page'] ); } public function page(): void { if (!current_user_can('manage_woocommerce')) { wp_die(esc_html__('You do not have permission to manage FacetCore.', 'facetcore')); } $facets = array_values(array_filter($this->repo->facets(), static fn (array $facet): bool => strpos($facet['slug'], 'wc-attribute-') !== 0)); ?>


'return confirm(' . wp_json_encode(__('Delete this parameter and all assignments?', 'facetcore')) . ')']); ?>
ID, 'facetcore_product_nonce'); $assigned = $this->repo->productValues((int) $post->ID); $facets = $this->repo->facets(true); if (!$facets) { echo '

' . esc_html__('No FacetCore parameters yet. Create them under WooCommerce → FacetCore.', 'facetcore') . '

'; return; } foreach ($facets as $facet) { $id = (int) $facet['id']; $selected = $assigned[$id] ?? []; echo '


'; echo '

'; } } public function saveProduct(int $postId, \WP_Post $post): void { if ($post->post_type !== 'product' || wp_is_post_revision($postId) || wp_is_post_autosave($postId)) { return; } if (!current_user_can('edit_post', $postId)) { return; } $nonce = isset($_POST['facetcore_product_nonce']) ? sanitize_text_field(wp_unslash($_POST['facetcore_product_nonce'])) : ''; if (!$nonce || !wp_verify_nonce($nonce, 'facetcore_product_' . $postId)) { return; } $raw = isset($_POST['facetcore_values']) && is_array($_POST['facetcore_values']) ? map_deep(wp_unslash($_POST['facetcore_values']), 'intval') : []; $assignments = []; foreach ($raw as $facetId => $valueIds) { $assignments[(int) $facetId] = array_map('intval', (array) $valueIds); } $this->repo->replaceProductValues($postId, $assignments); (new AttributeIndex())->syncProduct($postId); } public function createFacet(): void { $this->guard('facetcore_create_facet'); // phpcs:disable WordPress.Security.NonceVerification.Missing -- guard() above verified the nonce with check_admin_referer(). $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : ''; $slug = sanitize_title(isset($_POST['slug']) ? sanitize_text_field(wp_unslash($_POST['slug'])) : $name); $priority = isset($_POST['priority']) ? (int) $_POST['priority'] : 0; // phpcs:enable WordPress.Security.NonceVerification.Missing if ($name && $slug) { $this->repo->createFacet($name, $slug, $priority); } $this->redirect(); } public function createValue(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- The ID only selects the per-item nonce action; guard() verifies it before any state change. $facetId = isset($_POST['facet_id']) ? (int) $_POST['facet_id'] : 0; $this->guard('facetcore_create_value_' . $facetId); $label = isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : ''; $sortOrder = isset($_POST['sort_order']) ? (int) $_POST['sort_order'] : 0; // phpcs:enable WordPress.Security.NonceVerification.Missing if ($facetId > 0 && $label !== '') { $this->repo->createValue($facetId, $label, $sortOrder); } $this->redirect(); } public function deleteFacet(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The ID only selects the per-item nonce action; guard() verifies it before any state change. $facetId = isset($_POST['facet_id']) ? (int) $_POST['facet_id'] : 0; $this->guard('facetcore_delete_facet_' . $facetId); if ($facetId > 0) { $this->repo->deleteFacet($facetId); } $this->redirect(); } public function deleteValue(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The ID only selects the per-item nonce action; guard() verifies it before any state change. $valueId = isset($_POST['value_id']) ? (int) $_POST['value_id'] : 0; $this->guard('facetcore_delete_value_' . $valueId); if ($valueId > 0) { $this->repo->deleteValue($valueId); } $this->redirect(); } private function guard(string $action): void { if (!current_user_can('manage_woocommerce')) { wp_die(esc_html__('Forbidden', 'facetcore'), '', ['response' => 403]); } check_admin_referer($action); } private function redirect(): void { wp_safe_redirect(admin_url('admin.php?page=facetcore')); exit; } }