true], 'objects'); unset($types['attachment']); uasort($types, static function ($left, $right): int { return strcasecmp((string) $left->label, (string) $right->label); }); return $types; } function samtyk_cmp_search_public_content(string $post_type, string $search): array { $types = samtyk_cmp_get_public_content_types(); if (!isset($types[$post_type])) { return []; } $posts = get_posts([ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => 20, 's' => trim($search), 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, ]); return array_map(static function ($post) use ($types): array { return [ 'id' => (int) $post->ID, 'title' => (string) $post->post_title, 'post_type' => (string) $post->post_type, 'type_label' => (string) $types[$post->post_type]->label, ]; }, array_slice($posts, 0, 20)); } function samtyk_cmp_get_snippet_request_input(array $request): array { $source = isset($request['samtyk_cmp_snippet_source']) && is_string($request['samtyk_cmp_snippet_source']) ? wp_unslash($request['samtyk_cmp_snippet_source']) : ''; $source_error = ''; if (array_key_exists('samtyk_cmp_snippet_source_base64', $request)) { $encoded = $request['samtyk_cmp_snippet_source_base64']; $source = ''; if (!is_string($encoded)) { $source_error = __('The code could not be decoded. Reload the page and try again.', 'samtyk-cmp'); } elseif (strlen($encoded) > 4 * (int) ceil(SAMTYK_CMP_SNIPPET_MAX_BYTES / 3)) { $source_error = __('The code is larger than the allowed 100 KB.', 'samtyk-cmp'); } else { $encoded = wp_unslash($encoded); $decoded = base64_decode($encoded, true); if ($decoded === false || base64_encode($decoded) !== $encoded) { $source_error = __('The code could not be decoded. Reload the page and try again.', 'samtyk-cmp'); } elseif (strlen($decoded) > SAMTYK_CMP_SNIPPET_MAX_BYTES) { $source_error = __('The code is larger than the allowed 100 KB.', 'samtyk-cmp'); } else { // Decoded source is already unslashed; keep JavaScript backslashes intact. $source = $decoded; } } } $categories = $request['samtyk_cmp_categories'] ?? []; // Preserve an invalid marker instead of silently converting malformed consent to none. $categories = samtyk_cmp_valid_snippet_categories($categories) ? $categories : ['invalid']; foreach ($request as $key => $value) { if (!in_array($key, ['samtyk_cmp_categories', 'samtyk_cmp_content_ids'], true) && !is_string($value)) { $request[$key] = ''; } } $content_ids = $request['samtyk_cmp_content_ids'] ?? []; return [ 'name' => isset($request['samtyk_cmp_snippet_name']) ? sanitize_text_field(wp_unslash($request['samtyk_cmp_snippet_name'])) : '', 'source' => $source, 'source_error' => $source_error, 'categories' => $categories, 'location' => isset($request['samtyk_cmp_location']) ? sanitize_key(wp_unslash($request['samtyk_cmp_location'])) : '', 'shortcode_id' => isset($request['samtyk_cmp_shortcode_id']) ? sanitize_key(wp_unslash($request['samtyk_cmp_shortcode_id'])) : '', 'placeholder_enabled' => !empty($request['samtyk_cmp_placeholder_enabled']), 'placeholder_default' => !empty($request['samtyk_cmp_placeholder_default']), 'placeholder_text' => isset($request['samtyk_cmp_placeholder_text']) && is_string($request['samtyk_cmp_placeholder_text']) ? sanitize_textarea_field(wp_unslash($request['samtyk_cmp_placeholder_text'])) : '', 'placeholder_icon' => isset($request['samtyk_cmp_placeholder_icon']) ? sanitize_key(wp_unslash($request['samtyk_cmp_placeholder_icon'])) : '', 'active' => !empty($request['samtyk_cmp_active']), 'visibility' => isset($request['samtyk_cmp_visibility']) ? sanitize_key(wp_unslash($request['samtyk_cmp_visibility'])) : 'all', 'content_ids' => is_array($content_ids) ? array_map('intval', wp_unslash($content_ids)) : [], ]; } function samtyk_cmp_snippets_admin_url(array $args = []): string { return add_query_arg(array_merge([ 'page' => SAMTYK_CMP_MENU_SLUG, 'view' => 'scripts', ], $args), admin_url('admin.php')); } function samtyk_cmp_toggle_snippet(int $post_id): bool { if (!current_user_can('manage_options')) { return false; } $snippet = samtyk_cmp_get_snippet($post_id); if (!$snippet) { return false; } if (!$snippet['active'] && (!samtyk_cmp_can_manage_snippet_code() || !$snippet['approved'] || $snippet['generated_code'] === '')) { return false; } $result = wp_update_post([ 'ID' => $post_id, 'post_status' => $snippet['active'] ? 'draft' : 'publish', ], true); return !is_wp_error($result) && (int) $result > 0; } function samtyk_cmp_delete_snippet(int $post_id): bool { if (!current_user_can('manage_options')) { return false; } if (!samtyk_cmp_get_snippet($post_id)) { return false; } return (bool) wp_delete_post($post_id, true); } function samtyk_cmp_handle_snippet_admin_action(): void { global $samtyk_cmp_snippet_form_state; if (wp_doing_ajax()) { return; } // phpcs:disable WordPress.Security.NonceVerification.Missing -- This request is ignored unless it belongs to this handler, then the nonce is verified by samtyk_cmp_require_snippet_admin_access() before any state change. if (empty($_POST['samtyk_cmp_snippet_action'])) { return; } $action = is_string($_POST['samtyk_cmp_snippet_action']) ? sanitize_key(wp_unslash($_POST['samtyk_cmp_snippet_action'])) : ''; if (!in_array($action, ['save', 'toggle', 'delete'], true)) { return; } $nonce = isset($_POST['samtyk_cmp_snippet_nonce']) && is_string($_POST['samtyk_cmp_snippet_nonce']) ? sanitize_text_field(wp_unslash($_POST['samtyk_cmp_snippet_nonce'])) : ''; samtyk_cmp_require_snippet_admin_access($nonce, 'samtyk_cmp_manage_snippets'); $post_id = isset($_POST['samtyk_cmp_snippet_id']) ? (int) $_POST['samtyk_cmp_snippet_id'] : 0; if ($action === 'save') { $input = samtyk_cmp_get_snippet_request_input($_POST); $result = samtyk_cmp_save_snippet($input, $post_id); if (!$result['success']) { $samtyk_cmp_snippet_form_state = [ 'input' => $input, 'post_id' => $post_id, 'error' => $result['error'], ]; return; } wp_safe_redirect(samtyk_cmp_snippets_admin_url(['updated' => '1'])); exit; } $snippet = samtyk_cmp_get_snippet($post_id); if (!$snippet) { wp_safe_redirect(samtyk_cmp_snippets_admin_url(['snippet_error' => 'not_found'])); exit; } if ($action === 'delete') { samtyk_cmp_delete_snippet($post_id); wp_safe_redirect(samtyk_cmp_snippets_admin_url(['deleted' => '1'])); exit; } if (!samtyk_cmp_toggle_snippet($post_id)) { $samtyk_cmp_snippet_form_state = [ 'input' => [], 'post_id' => $post_id, 'error' => __('Review and save this snippet with the unfiltered_html capability before activating it.', 'samtyk-cmp'), ]; return; } wp_safe_redirect(samtyk_cmp_snippets_admin_url(['toggled' => '1'])); // phpcs:enable WordPress.Security.NonceVerification.Missing exit; } function samtyk_cmp_ajax_preview_snippet(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- The nonce is verified by samtyk_cmp_require_snippet_admin_access() before request data is processed. $nonce = isset($_POST['nonce']) && is_string($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; samtyk_cmp_require_snippet_admin_access($nonce, 'samtyk_cmp_preview_snippet'); $input = samtyk_cmp_get_snippet_request_input($_POST); // phpcs:enable WordPress.Security.NonceVerification.Missing if ($input['source_error'] !== '') { wp_send_json_error(['message' => $input['source_error']], 422); } $result = samtyk_cmp_convert_embed_snippet($input['source'], $input['categories'], ''); if (!$result['success']) { wp_send_json_error(['message' => $result['error']], 422); } wp_send_json_success(['html' => $result['html']]); } function samtyk_cmp_ajax_search_public_content(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- The nonce is verified by samtyk_cmp_require_snippet_admin_access() before request data is processed. $nonce = isset($_GET['nonce']) ? sanitize_text_field(wp_unslash($_GET['nonce'])) : ''; samtyk_cmp_require_snippet_admin_access($nonce, 'samtyk_cmp_search_content'); $post_type = isset($_GET['post_type']) ? sanitize_key(wp_unslash($_GET['post_type'])) : ''; $search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended wp_send_json_success(['items' => samtyk_cmp_search_public_content($post_type, $search)]); } function samtyk_cmp_get_snippet_form_value(?array $snippet, array $input, string $key, $default = null) { if (array_key_exists($key, $input)) { return $input[$key]; } if ($snippet !== null && array_key_exists($key, $snippet)) { return $snippet[$key]; } return $default; } function samtyk_cmp_get_snippet_list_page(int $page = 1, int $per_page = 20): array { $page = max(1, $page); $per_page = max(1, min(100, $per_page)); $counts = wp_count_posts(SAMTYK_CMP_SNIPPET_POST_TYPE); $total = (int) ($counts->publish ?? 0) + (int) ($counts->draft ?? 0); $pages = max(1, (int) ceil($total / $per_page)); $page = min($page, $pages); $posts = get_posts([ 'post_type' => SAMTYK_CMP_SNIPPET_POST_TYPE, 'post_status' => ['publish', 'draft'], 'posts_per_page' => $per_page, 'offset' => ($page - 1) * $per_page, // Keep rows stable when editing or toggling status, including across paginated results. 'orderby' => 'ID', 'order' => 'DESC', ]); return [ 'posts' => $posts, 'page' => $page, 'pages' => $pages, 'total' => $total, ]; } function samtyk_cmp_get_action_icon(string $icon): string { $paths = [ 'edit' => '', 'activate' => '', 'deactivate' => '', 'delete' => '', ]; if (!isset($paths[$icon])) { return ''; } return ''; } function samtyk_cmp_get_action_icon_allowed_html(): array { return [ 'img' => array_fill_keys(['class', 'src', 'alt', 'aria-hidden'], true), 'svg' => [ 'class' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'aria-hidden' => true, 'focusable' => true, ], 'path' => [ 'd' => true, ], 'rect' => [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, ], ]; } function samtyk_cmp_render_snippet_editor(?array $snippet = null, array $input = [], string $error = ''): void { $post_id = $snippet ? (int) $snippet['id'] : 0; $name = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'name', ''); $source = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'source', ''); $categories = samtyk_cmp_normalize_snippet_categories((array) samtyk_cmp_get_snippet_form_value($snippet, $input, 'categories', [])); $location = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'location', 'head'); $active = (bool) samtyk_cmp_get_snippet_form_value($snippet, $input, 'active', false); $visibility = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'visibility', 'all'); $content_ids = (array) samtyk_cmp_get_snippet_form_value($snippet, $input, 'content_ids', []); $shortcode_id = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'shortcode_id', ''); $placeholder_enabled = $categories !== [] && (bool) samtyk_cmp_get_snippet_form_value($snippet, $input, 'placeholder_enabled', false); $placeholder_text = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'placeholder_text', ''); $placeholder_icon = (string) samtyk_cmp_get_snippet_form_value($snippet, $input, 'placeholder_icon', ''); $placeholder_default = !empty($input['placeholder_default']) || samtyk_cmp_is_default_placeholder_text($placeholder_text); if ($placeholder_default) { $placeholder_text = samtyk_cmp_get_placeholder_default_text($categories); } if (!preg_match('/^[a-f0-9]{32}$/D', $shortcode_id)) { $shortcode_id = bin2hex(random_bytes(16)); } $generated = ''; if ($source !== '' && samtyk_cmp_can_manage_snippet_code()) { $preview = samtyk_cmp_convert_embed_snippet($source, $categories, ''); if ($preview['success']) { $generated = $preview['html']; } } $types = samtyk_cmp_get_public_content_types(); echo '
'; echo '

' . esc_html($post_id > 0 ? __('Edit script or embed', 'samtyk-cmp') : __('Add script or embed', 'samtyk-cmp')) . '

' . esc_html(__('Convert code, review it, and optionally insert it on the website.', 'samtyk-cmp')) . '

'; if ($error !== '') { echo ''; } if ($snippet && (!$snippet['approved'] || $snippet['generated_code'] === '')) { echo '

' . esc_html__('This snippet is paused. Review the code and save it again to approve frontend output.', 'samtyk-cmp') . '

'; } if (!samtyk_cmp_can_manage_snippet_code()) { echo '

' . esc_html__('Managing executable code requires the unfiltered_html capability.', 'samtyk-cmp') . '

'; } echo '
'; echo ''; echo ''; echo '
'; echo '

' . esc_html(__('1. Insert code and select consent', 'samtyk-cmp')) . '

'; echo ''; echo '
' . esc_html(__('Consent categories', 'samtyk-cmp')) . '
'; foreach (['functional' => __('Functional', 'samtyk-cmp'), 'statistics' => __('Statistics', 'samtyk-cmp'), 'marketing' => __('Marketing', 'samtyk-cmp')] as $value => $label) { echo ''; } echo '
'; echo '

' . esc_html(__('2. Review the converted code', 'samtyk-cmp')) . '

'; echo '

' . esc_html(__('3. Insert on the website', 'samtyk-cmp')) . '

'; echo ''; echo '
'; samtyk_cmp_render_dropdown('samtyk-cmp-location', 'samtyk_cmp_location', ['head' => 'Head', 'body_open' => __('After opening body', 'samtyk-cmp'), 'footer' => 'Footer', 'shortcode' => __('Shortcode', 'samtyk-cmp')], $location); echo '
'; echo '

' . esc_html__('Insert this shortcode in a Shortcode block, widget, or page builder where the content should appear. Save the script and enable insertion for the shortcode to work. The selected consent categories still apply.', 'samtyk-cmp') . '

'; echo '
'; echo ''; echo '
'; samtyk_cmp_render_dropdown('samtyk-cmp-placeholder-icon', 'samtyk_cmp_placeholder_icon', ['' => __('No icon', 'samtyk-cmp'), 'samtyk' => 'Samtyk', 'lock' => __('Lock', 'samtyk-cmp'), 'shield' => __('Shield', 'samtyk-cmp'), 'video' => __('Video', 'samtyk-cmp')], $placeholder_icon, [], true); echo '

' . esc_html__('Shown with a Manage consent button until all selected consent categories are accepted. Without consent categories, the content is displayed directly.', 'samtyk-cmp') . '

'; echo '
'; echo '
' . esc_html(__('Show on', 'samtyk-cmp')) . '
'; echo '

' . esc_html(__('First select a content type, then enter at least 2 characters.', 'samtyk-cmp')) . '

'; $content_types = ['' => __('Select type', 'samtyk-cmp')]; foreach ($types as $type) { $content_types[$type->name] = $type->label; } samtyk_cmp_render_dropdown('samtyk-cmp-content-type', '', $content_types, '', ['data-samtyk-content-type' => '']); echo '

' . esc_html(__('Select a content type to begin.', 'samtyk-cmp')) . '

'; foreach ($content_ids as $content_id) { $post_type = get_post_type((int) $content_id); if (!isset($types[$post_type])) { continue; } echo '' . esc_html(get_the_title((int) $content_id) . ' · ' . $types[$post_type]->label) . ''; } echo '
' . esc_html(__('Cancel', 'samtyk-cmp')) . '
'; } function samtyk_cmp_render_snippet_page(callable $t): void { global $samtyk_cmp_snippet_form_state; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect state after nonce-protected actions. $status = isset($_GET['updated']) && sanitize_key(wp_unslash($_GET['updated'])) === '1' ? __('The script or embed has been saved.', 'samtyk-cmp') : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect state after nonce-protected actions. if (isset($_GET['deleted']) && sanitize_key(wp_unslash($_GET['deleted'])) === '1') { $status = __('The script or embed has been deleted.', 'samtyk-cmp'); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect state after nonce-protected actions. if (isset($_GET['toggled']) && sanitize_key(wp_unslash($_GET['toggled'])) === '1') { $status = __('The status has been updated.', 'samtyk-cmp'); } if ($status !== '') { echo '
'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation state. $action = isset($_GET['script_action']) ? sanitize_key(wp_unslash($_GET['script_action'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation state. $post_id = isset($_GET['script_id']) ? (int) $_GET['script_id'] : 0; if ($action === 'new' || $action === 'edit' || !empty($samtyk_cmp_snippet_form_state)) { $state = is_array($samtyk_cmp_snippet_form_state ?? null) ? $samtyk_cmp_snippet_form_state : []; $post_id = isset($state['post_id']) ? (int) $state['post_id'] : $post_id; $snippet = $post_id > 0 ? samtyk_cmp_get_snippet($post_id) : null; samtyk_cmp_render_snippet_editor($snippet, $state['input'] ?? [], $state['error'] ?? ''); return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination state. $requested_page = isset($_GET['script_page']) ? max(1, (int) $_GET['script_page']) : 1; $list_page = samtyk_cmp_get_snippet_list_page($requested_page, 20); $posts = $list_page['posts']; echo '

' . esc_html($t('snippets_nav')) . '

' . esc_html(__('Convert, insert, and manage consent-controlled code.', 'samtyk-cmp')) . '

' . esc_html(__('Add new', 'samtyk-cmp')) . '
'; if ($posts === []) { echo '

' . esc_html(__('No scripts or embeds have been added yet.', 'samtyk-cmp')) . '

'; return; } echo '
'; foreach ($posts as $post) { $snippet = samtyk_cmp_get_snippet((int) $post->ID); if (!$snippet) { continue; } $category_labels = array_map(static function (string $category): string { return ['functional' => __('Functional', 'samtyk-cmp'), 'statistics' => __('Statistics', 'samtyk-cmp'), 'marketing' => __('Marketing', 'samtyk-cmp')][$category]; }, $snippet['categories']); if ($category_labels === []) { $category_labels[] = __('None', 'samtyk-cmp'); } $location_label = ['head' => 'Head', 'body_open' => __('After opening body', 'samtyk-cmp'), 'footer' => 'Footer', 'shortcode' => __('Shortcode', 'samtyk-cmp')][$snippet['location']]; if ($snippet['location'] === 'shortcode') { $visibility_label = '[samtyk_embed id="' . $snippet['shortcode_id'] . '"]'; } elseif ($snippet['visibility'] === 'all') { $visibility_label = __('All pages', 'samtyk-cmp'); } else { /* translators: %d: Number of selected pages or posts. */ $visibility_label = sprintf(__('%d selected', 'samtyk-cmp'), count($snippet['content_ids'])); } $edit_label = __('Edit', 'samtyk-cmp'); echo ''; $status_label = !$snippet['approved'] || $snippet['generated_code'] === '' ? __('Pending approval', 'samtyk-cmp') : ''; echo ''; } echo '
' . esc_html(__('Name', 'samtyk-cmp')) . '' . esc_html(__('Categories', 'samtyk-cmp')) . '' . esc_html(__('Placement', 'samtyk-cmp')) . '' . esc_html(__('Show on', 'samtyk-cmp')) . '' . esc_html(__('Modified', 'samtyk-cmp')) . '' . esc_html(__('Actions', 'samtyk-cmp')) . '
' . esc_html($snippet['name']) . ''; if ($status_label !== '') { echo '' . esc_html($status_label) . ''; } echo '' . esc_html(implode(', ', $category_labels)) . '' . esc_html($location_label) . '' . esc_html($visibility_label) . '' . esc_html($snippet['modified']) . '
' . wp_kses(samtyk_cmp_get_action_icon('edit'), samtyk_cmp_get_action_icon_allowed_html()) . '' . esc_html($edit_label) . ''; foreach (['toggle' => ($snippet['active'] ? __('Deactivate', 'samtyk-cmp') : __('Activate', 'samtyk-cmp')), 'delete' => __('Delete', 'samtyk-cmp')] as $action => $label) { $icon = $action === 'delete' ? 'delete' : ($snippet['active'] ? 'deactivate' : 'activate'); echo '
'; } echo '
'; if ($list_page['pages'] > 1) { echo ''; } echo ''; } add_action('admin_init', 'samtyk_cmp_handle_snippet_admin_action'); add_action('wp_ajax_samtyk_cmp_preview_snippet', 'samtyk_cmp_ajax_preview_snippet'); add_action('wp_ajax_samtyk_cmp_search_content', 'samtyk_cmp_ajax_search_public_content');