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));
?>
repo->values((int) $facet['id']) as $value): ?>
-
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;
}
}