From 840a779a84f7653b4cd4cb9dc933d2f7287c6ae9 Mon Sep 17 00:00:00 2001 From: feibisi Date: Wed, 16 Jul 2025 03:44:42 +0800 Subject: [PATCH] Add initial WPTag admin and core plugin files Introduce the main admin controllers, AJAX handlers, UI partials, and core classes for the WPTag plugin. This includes admin interface components, dashboard, settings, snippet management, asset files, and localization support. These files establish the foundation for managing code snippets, templates, and plugin settings within the WordPress admin. --- admin/class-wptag-admin-controller.php | 301 +++++++++++++++++ admin/class-wptag-admin-interface.php | 195 +++++++++++ admin/class-wptag-ajax-handler.php | 322 ++++++++++++++++++ admin/partials/dashboard.php | 177 ++++++++++ admin/partials/settings.php | 245 ++++++++++++++ admin/partials/snippet-form.php | 207 ++++++++++++ admin/partials/snippets.php | 149 ++++++++ admin/partials/templates.php | 119 +++++++ assets/admin.css | 374 +++++++++++++++++++++ assets/admin.js | 322 ++++++++++++++++++ includes/class-wptag-cache-manager.php | 160 +++++++++ includes/class-wptag-condition-engine.php | 311 +++++++++++++++++ includes/class-wptag-core.php | 267 +++++++++++++++ includes/class-wptag-output-controller.php | 178 ++++++++++ includes/class-wptag-snippet-manager.php | 264 +++++++++++++++ includes/class-wptag-template-manager.php | 271 +++++++++++++++ uninstall.php | 41 +++ wptag-readme.md | 155 +++++++++ wptag.php | 33 ++ wptag.pot | 298 ++++++++++++++++ 20 files changed, 4389 insertions(+) create mode 100644 admin/class-wptag-admin-controller.php create mode 100644 admin/class-wptag-admin-interface.php create mode 100644 admin/class-wptag-ajax-handler.php create mode 100644 admin/partials/dashboard.php create mode 100644 admin/partials/settings.php create mode 100644 admin/partials/snippet-form.php create mode 100644 admin/partials/snippets.php create mode 100644 admin/partials/templates.php create mode 100644 assets/admin.css create mode 100644 assets/admin.js create mode 100644 includes/class-wptag-cache-manager.php create mode 100644 includes/class-wptag-condition-engine.php create mode 100644 includes/class-wptag-core.php create mode 100644 includes/class-wptag-output-controller.php create mode 100644 includes/class-wptag-snippet-manager.php create mode 100644 includes/class-wptag-template-manager.php create mode 100644 uninstall.php create mode 100644 wptag-readme.md create mode 100644 wptag.php create mode 100644 wptag.pot diff --git a/admin/class-wptag-admin-controller.php b/admin/class-wptag-admin-controller.php new file mode 100644 index 0000000..db89a6f --- /dev/null +++ b/admin/class-wptag-admin-controller.php @@ -0,0 +1,301 @@ +snippet_manager = $snippet_manager; + $this->template_manager = $template_manager; + + add_action('admin_menu', [$this, 'add_admin_menu']); + add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); + add_action('admin_init', [$this, 'handle_form_submissions']); + add_action('admin_notices', [$this, 'display_admin_notices']); + } + + public function add_admin_menu() { + $capability = 'manage_options'; + + $this->page_hook_suffix['main'] = add_menu_page( + __('WPTAG', 'wptag'), + __('WPTAG', 'wptag'), + $capability, + 'wptag', + [$this, 'render_dashboard_page'], + 'dashicons-code-standards', + 85 + ); + + $this->page_hook_suffix['dashboard'] = add_submenu_page( + 'wptag', + __('Dashboard', 'wptag'), + __('Dashboard', 'wptag'), + $capability, + 'wptag', + [$this, 'render_dashboard_page'] + ); + + $this->page_hook_suffix['snippets'] = add_submenu_page( + 'wptag', + __('Code Snippets', 'wptag'), + __('Code Snippets', 'wptag'), + $capability, + 'wptag-snippets', + [$this, 'render_snippets_page'] + ); + + $this->page_hook_suffix['templates'] = add_submenu_page( + 'wptag', + __('Service Templates', 'wptag'), + __('Service Templates', 'wptag'), + $capability, + 'wptag-templates', + [$this, 'render_templates_page'] + ); + + $this->page_hook_suffix['settings'] = add_submenu_page( + 'wptag', + __('Settings', 'wptag'), + __('Settings', 'wptag'), + $capability, + 'wptag-settings', + [$this, 'render_settings_page'] + ); + } + + public function enqueue_admin_assets($hook) { + if (!in_array($hook, $this->page_hook_suffix)) { + return; + } + + wp_enqueue_style( + 'wptag-admin', + WPTAG_PLUGIN_URL . 'assets/css/admin.css', + ['wp-components'], + WPTAG_VERSION + ); + + wp_enqueue_script( + 'wptag-admin', + WPTAG_PLUGIN_URL . 'assets/js/admin.js', + ['jquery', 'wp-api', 'wp-i18n', 'wp-components', 'wp-element'], + WPTAG_VERSION, + true + ); + + wp_localize_script('wptag-admin', 'wptagAdmin', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wptag_admin'), + 'strings' => [ + 'confirmDelete' => __('Are you sure you want to delete this snippet?', 'wptag'), + 'saved' => __('Settings saved successfully.', 'wptag'), + 'error' => __('An error occurred. Please try again.', 'wptag') + ] + ]); + + if (isset($_GET['action']) && ($_GET['action'] === 'new' || $_GET['action'] === 'edit')) { + wp_enqueue_code_editor(['type' => 'text/html']); + } + } + + public function render_dashboard_page() { + require_once WPTAG_PLUGIN_DIR . 'admin/partials/dashboard.php'; + } + + public function render_snippets_page() { + require_once WPTAG_PLUGIN_DIR . 'admin/partials/snippets.php'; + } + + public function render_templates_page() { + require_once WPTAG_PLUGIN_DIR . 'admin/partials/templates.php'; + } + + public function render_settings_page() { + require_once WPTAG_PLUGIN_DIR . 'admin/partials/settings.php'; + } + + public function handle_form_submissions() { + if (!isset($_POST['wptag_action'])) { + return; + } + + $action = sanitize_key($_POST['wptag_action']); + + if (!wp_verify_nonce($_POST['_wpnonce'], 'wptag_' . $action)) { + wp_die(__('Security check failed', 'wptag')); + } + + if (!current_user_can('manage_options')) { + wp_die(__('Permission denied', 'wptag')); + } + + switch ($action) { + case 'create_snippet': + $this->handle_create_snippet(); + break; + + case 'update_snippet': + $this->handle_update_snippet(); + break; + + case 'delete_snippet': + $this->handle_delete_snippet(); + break; + + case 'toggle_snippet': + $this->handle_toggle_snippet(); + break; + + case 'save_settings': + $this->handle_save_settings(); + break; + } + } + + private function handle_create_snippet() { + $data = [ + 'name' => $_POST['name'] ?? '', + 'description' => $_POST['description'] ?? '', + 'code' => $_POST['code'] ?? '', + 'code_type' => $_POST['code_type'] ?? 'html', + 'position' => $_POST['position'] ?? 'head', + 'category' => $_POST['category'] ?? 'custom', + 'priority' => $_POST['priority'] ?? 10, + 'status' => isset($_POST['status']) ? 1 : 0, + 'device_type' => $_POST['device_type'] ?? 'all', + 'load_method' => $_POST['load_method'] ?? 'normal', + 'conditions' => $this->parse_conditions($_POST['conditions'] ?? []) + ]; + + $result = $this->snippet_manager->create_snippet($data); + + if (is_wp_error($result)) { + $this->add_admin_notice($result->get_error_message(), 'error'); + } else { + $this->add_admin_notice(__('Snippet created successfully', 'wptag'), 'success'); + wp_redirect(admin_url('admin.php?page=wptag-snippets')); + exit; + } + } + + private function handle_update_snippet() { + $id = intval($_POST['snippet_id'] ?? 0); + + if (!$id) { + $this->add_admin_notice(__('Invalid snippet ID', 'wptag'), 'error'); + return; + } + + $data = [ + 'name' => $_POST['name'] ?? '', + 'description' => $_POST['description'] ?? '', + 'code' => $_POST['code'] ?? '', + 'code_type' => $_POST['code_type'] ?? 'html', + 'position' => $_POST['position'] ?? 'head', + 'category' => $_POST['category'] ?? 'custom', + 'priority' => $_POST['priority'] ?? 10, + 'status' => isset($_POST['status']) ? 1 : 0, + 'device_type' => $_POST['device_type'] ?? 'all', + 'load_method' => $_POST['load_method'] ?? 'normal', + 'conditions' => $this->parse_conditions($_POST['conditions'] ?? []) + ]; + + $result = $this->snippet_manager->update_snippet($id, $data); + + if (is_wp_error($result)) { + $this->add_admin_notice($result->get_error_message(), 'error'); + } else { + $this->add_admin_notice(__('Snippet updated successfully', 'wptag'), 'success'); + } + } + + private function handle_delete_snippet() { + $id = intval($_GET['snippet_id'] ?? 0); + + if (!$id) { + $this->add_admin_notice(__('Invalid snippet ID', 'wptag'), 'error'); + return; + } + + $result = $this->snippet_manager->delete_snippet($id); + + if (is_wp_error($result)) { + $this->add_admin_notice($result->get_error_message(), 'error'); + } else { + $this->add_admin_notice(__('Snippet deleted successfully', 'wptag'), 'success'); + } + + wp_redirect(admin_url('admin.php?page=wptag-snippets')); + exit; + } + + private function handle_toggle_snippet() { + $id = intval($_GET['snippet_id'] ?? 0); + + if (!$id) { + wp_die(json_encode(['success' => false, 'message' => 'Invalid ID'])); + } + + $result = $this->snippet_manager->toggle_status($id); + + if (is_wp_error($result)) { + wp_die(json_encode(['success' => false, 'message' => $result->get_error_message()])); + } + + wp_die(json_encode(['success' => true, 'status' => $result])); + } + + private function handle_save_settings() { + $settings = [ + 'enable_cache' => isset($_POST['enable_cache']) ? 1 : 0, + 'cache_ttl' => intval($_POST['cache_ttl'] ?? 3600), + 'enable_debug' => isset($_POST['enable_debug']) ? 1 : 0, + 'cleanup_on_uninstall' => isset($_POST['cleanup_on_uninstall']) ? 1 : 0 + ]; + + update_option('wptag_settings', $settings); + + $this->add_admin_notice(__('Settings saved successfully', 'wptag'), 'success'); + + wp_redirect(admin_url('admin.php?page=wptag-settings')); + exit; + } + + private function parse_conditions($conditions_data) { + if (empty($conditions_data) || !is_array($conditions_data)) { + return null; + } + + return $conditions_data; + } + + private function add_admin_notice($message, $type = 'info') { + set_transient('wptag_admin_notice', [ + 'message' => $message, + 'type' => $type + ], 30); + } + + public function display_admin_notices() { + $notice = get_transient('wptag_admin_notice'); + + if (!$notice) { + return; + } + + delete_transient('wptag_admin_notice'); + + $class = 'notice notice-' . esc_attr($notice['type']); + printf( + '

%2$s

', + $class, + esc_html($notice['message']) + ); + } +} diff --git a/admin/class-wptag-admin-interface.php b/admin/class-wptag-admin-interface.php new file mode 100644 index 0000000..e447fa2 --- /dev/null +++ b/admin/class-wptag-admin-interface.php @@ -0,0 +1,195 @@ +' . esc_html($label); + if ($required) { + echo ' *'; + } + echo ''; + } + + switch ($type) { + case 'text': + case 'email': + case 'url': + case 'number': + self::render_input_field($type, $name, $value, $required); + break; + + case 'textarea': + self::render_textarea_field($name, $value, $required); + break; + + case 'select': + self::render_select_field($name, $value, $options, $required); + break; + + case 'checkbox': + self::render_checkbox_field($name, $value, $label); + break; + + case 'radio': + self::render_radio_field($name, $value, $options); + break; + } + + if ($description) { + echo '

' . esc_html($description) . '

'; + } + } + + private static function render_input_field($type, $name, $value, $required) { + printf( + '', + esc_attr($type), + esc_attr($name), + esc_attr($name), + esc_attr($value), + $required ? 'required' : '' + ); + } + + private static function render_textarea_field($name, $value, $required) { + printf( + '', + esc_attr($name), + esc_attr($name), + $required ? 'required' : '', + esc_textarea($value) + ); + } + + private static function render_select_field($name, $value, $options, $required) { + printf( + ''; + } + + private static function render_checkbox_field($name, $value, $label) { + printf( + '', + esc_attr($name), + esc_attr($name), + checked($value, 1, false), + esc_html($label) + ); + } + + private static function render_radio_field($name, $value, $options) { + foreach ($options as $option_value => $option_label) { + printf( + '
', + esc_attr($name), + esc_attr($option_value), + checked($value, $option_value, false), + esc_html($option_label) + ); + } + } + + public static function render_modal($id, $title, $content, $footer = '') { + ?> + + 'notice-info', + 'success' => 'notice-success', + 'warning' => 'notice-warning', + 'error' => 'notice-error' + ]; + + $class = $classes[$type] ?? 'notice-info'; + + printf( + '

%s

', + esc_attr($class), + esc_html($message) + ); + } + + public static function render_tabs($tabs, $current_tab) { + echo ''; + } + + public static function render_action_buttons($actions) { + echo '
'; + + foreach ($actions as $action) { + $class = $action['primary'] ?? false ? 'button-primary' : 'button-secondary'; + $url = $action['url'] ?? '#'; + $onclick = $action['onclick'] ?? ''; + + printf( + '%s ', + esc_url($url), + esc_attr($class), + $onclick ? 'onclick="' . esc_attr($onclick) . '"' : '', + esc_html($action['label']) + ); + } + + echo '
'; + } +} diff --git a/admin/class-wptag-ajax-handler.php b/admin/class-wptag-ajax-handler.php new file mode 100644 index 0000000..8d3ddfe --- /dev/null +++ b/admin/class-wptag-ajax-handler.php @@ -0,0 +1,322 @@ +snippet_manager = $snippet_manager; + $this->template_manager = $template_manager; + + $this->register_ajax_handlers(); + } + + private function register_ajax_handlers() { + $actions = [ + 'wptag_toggle_snippet', + 'wptag_delete_snippet', + 'wptag_search_snippets', + 'wptag_validate_code', + 'wptag_preview_snippet', + 'wptag_get_template', + 'wptag_process_template', + 'wptag_export_snippets', + 'wptag_import_snippets', + 'wptag_clear_cache' + ]; + + foreach ($actions as $action) { + add_action('wp_ajax_' . $action, [$this, 'handle_' . str_replace('wptag_', '', $action)]); + } + } + + public function handle_toggle_snippet() { + $this->verify_ajax_request(); + + $snippet_id = intval($_POST['snippet_id'] ?? 0); + + if (!$snippet_id) { + wp_send_json_error(['message' => __('Invalid snippet ID', 'wptag')]); + } + + $result = $this->snippet_manager->toggle_status($snippet_id); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + wp_send_json_success([ + 'status' => $result, + 'message' => $result ? __('Snippet enabled', 'wptag') : __('Snippet disabled', 'wptag') + ]); + } + + public function handle_delete_snippet() { + $this->verify_ajax_request(); + + $snippet_id = intval($_POST['snippet_id'] ?? 0); + + if (!$snippet_id) { + wp_send_json_error(['message' => __('Invalid snippet ID', 'wptag')]); + } + + $result = $this->snippet_manager->delete_snippet($snippet_id); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + wp_send_json_success(['message' => __('Snippet deleted successfully', 'wptag')]); + } + + public function handle_search_snippets() { + $this->verify_ajax_request(); + + $args = [ + 'search' => sanitize_text_field($_POST['search'] ?? ''), + 'category' => sanitize_key($_POST['category'] ?? ''), + 'position' => sanitize_key($_POST['position'] ?? ''), + 'status' => isset($_POST['status']) ? intval($_POST['status']) : null, + 'per_page' => intval($_POST['per_page'] ?? 20), + 'page' => intval($_POST['page'] ?? 1) + ]; + + $snippets = $this->snippet_manager->get_snippets($args); + + wp_send_json_success(['snippets' => $snippets]); + } + + public function handle_validate_code() { + $this->verify_ajax_request(); + + $code = $_POST['code'] ?? ''; + $code_type = sanitize_key($_POST['code_type'] ?? 'html'); + + $errors = []; + + switch ($code_type) { + case 'javascript': + $errors = $this->validate_javascript($code); + break; + + case 'css': + $errors = $this->validate_css($code); + break; + + case 'html': + $errors = $this->validate_html($code); + break; + } + + if (empty($errors)) { + wp_send_json_success(['message' => __('Code is valid', 'wptag')]); + } else { + wp_send_json_error(['errors' => $errors]); + } + } + + public function handle_preview_snippet() { + $this->verify_ajax_request(); + + $snippet_id = intval($_POST['snippet_id'] ?? 0); + + if (!$snippet_id) { + wp_send_json_error(['message' => __('Invalid snippet ID', 'wptag')]); + } + + $snippet = $this->snippet_manager->get_snippet($snippet_id); + + if (!$snippet) { + wp_send_json_error(['message' => __('Snippet not found', 'wptag')]); + } + + $preview_url = add_query_arg([ + 'wptag_preview' => 1, + 'snippet_id' => $snippet_id + ], home_url()); + + wp_send_json_success(['preview_url' => $preview_url]); + } + + public function handle_get_template() { + $this->verify_ajax_request(); + + $service_type = sanitize_key($_POST['service_type'] ?? ''); + + if (!$service_type) { + wp_send_json_error(['message' => __('Invalid service type', 'wptag')]); + } + + $template = $this->template_manager->get_template($service_type); + + if (!$template) { + wp_send_json_error(['message' => __('Template not found', 'wptag')]); + } + + wp_send_json_success(['template' => $template]); + } + + public function handle_process_template() { + $this->verify_ajax_request(); + + $service_type = sanitize_key($_POST['service_type'] ?? ''); + $config_data = $_POST['config'] ?? []; + + if (!$service_type) { + wp_send_json_error(['message' => __('Invalid service type', 'wptag')]); + } + + $result = $this->template_manager->process_template_config($service_type, $config_data); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + $snippet_data = array_merge($result, [ + 'description' => sprintf(__('Generated from %s template', 'wptag'), $result['name']), + 'status' => 1 + ]); + + $snippet_id = $this->snippet_manager->create_snippet($snippet_data); + + if (is_wp_error($snippet_id)) { + wp_send_json_error(['message' => $snippet_id->get_error_message()]); + } + + wp_send_json_success([ + 'snippet_id' => $snippet_id, + 'message' => __('Snippet created successfully from template', 'wptag') + ]); + } + + public function handle_export_snippets() { + $this->verify_ajax_request(); + + $snippet_ids = array_map('intval', $_POST['snippet_ids'] ?? []); + + $export_data = [ + 'version' => WPTAG_VERSION, + 'exported_at' => current_time('mysql'), + 'snippets' => [] + ]; + + foreach ($snippet_ids as $id) { + $snippet = $this->snippet_manager->get_snippet($id); + if ($snippet) { + unset($snippet['id'], $snippet['created_by'], $snippet['last_modified_by']); + $export_data['snippets'][] = $snippet; + } + } + + wp_send_json_success([ + 'filename' => 'wptag-export-' . date('Y-m-d') . '.json', + 'data' => json_encode($export_data, JSON_PRETTY_PRINT) + ]); + } + + public function handle_import_snippets() { + $this->verify_ajax_request(); + + $import_data = json_decode(stripslashes($_POST['import_data'] ?? ''), true); + + if (!$import_data || !isset($import_data['snippets'])) { + wp_send_json_error(['message' => __('Invalid import data', 'wptag')]); + } + + $imported = 0; + $errors = []; + + foreach ($import_data['snippets'] as $snippet) { + $result = $this->snippet_manager->create_snippet($snippet); + + if (is_wp_error($result)) { + $errors[] = sprintf( + __('Failed to import "%s": %s', 'wptag'), + $snippet['name'], + $result->get_error_message() + ); + } else { + $imported++; + } + } + + if ($imported > 0) { + $message = sprintf( + _n('%d snippet imported successfully', '%d snippets imported successfully', $imported, 'wptag'), + $imported + ); + + if (!empty($errors)) { + $message .= ' ' . __('Some snippets failed to import.', 'wptag'); + } + + wp_send_json_success([ + 'message' => $message, + 'imported' => $imported, + 'errors' => $errors + ]); + } else { + wp_send_json_error([ + 'message' => __('No snippets were imported', 'wptag'), + 'errors' => $errors + ]); + } + } + + public function handle_clear_cache() { + $this->verify_ajax_request(); + + $cache_manager = new WPTag_Cache_Manager(); + $cache_manager->flush(); + + wp_send_json_success(['message' => __('Cache cleared successfully', 'wptag')]); + } + + private function verify_ajax_request() { + if (!check_ajax_referer('wptag_admin', 'nonce', false)) { + wp_send_json_error(['message' => __('Security check failed', 'wptag')]); + } + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => __('Permission denied', 'wptag')]); + } + } + + private function validate_javascript($code) { + $errors = []; + + if (preg_match('/\bdocument\.write\b/i', $code)) { + $errors[] = __('document.write is not recommended', 'wptag'); + } + + if (preg_match('/\beval\s*\(/i', $code)) { + $errors[] = __('eval() is potentially dangerous', 'wptag'); + } + + return $errors; + } + + private function validate_css($code) { + $errors = []; + + if (preg_match('/@import\s+url/i', $code)) { + $errors[] = __('@import may affect performance', 'wptag'); + } + + return $errors; + } + + private function validate_html($code) { + $errors = []; + + if (preg_match('/]*src=["\'](?!https?:\/\/)/i', $code)) { + $errors[] = __('Use absolute URLs for external scripts', 'wptag'); + } + + return $errors; + } +} diff --git a/admin/partials/dashboard.php b/admin/partials/dashboard.php new file mode 100644 index 0000000..41b9123 --- /dev/null +++ b/admin/partials/dashboard.php @@ -0,0 +1,177 @@ +prefix . 'wptag_snippets'; +$logs_table = $wpdb->prefix . 'wptag_logs'; + +$total_snippets = $wpdb->get_var("SELECT COUNT(*) FROM $snippets_table"); +$active_snippets = $wpdb->get_var("SELECT COUNT(*) FROM $snippets_table WHERE status = 1"); +$categories_count = $wpdb->get_results("SELECT category, COUNT(*) as count FROM $snippets_table GROUP BY category", ARRAY_A); + +$recent_activity = $wpdb->get_results( + "SELECT l.*, u.display_name + FROM $logs_table l + LEFT JOIN {$wpdb->users} u ON l.user_id = u.ID + ORDER BY l.created_at DESC + LIMIT 10", + ARRAY_A +); + +$categories = $this->snippet_manager->get_categories(); +?> + +
+
+

+
+ +
+
+
+

+
+
+ +
+

+
+
+ +
+

+
+
+ +
+

+
+ 0 ? round(($active_snippets / $total_snippets) * 100) : 0; + echo $success_rate . '%'; + ?> +
+
+
+ +
+
+

+ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

+ + +

+
    +
  1. +
  2. +
  3. +
  4. +
+
+
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + diff --git a/admin/partials/settings.php b/admin/partials/settings.php new file mode 100644 index 0000000..cc2f324 --- /dev/null +++ b/admin/partials/settings.php @@ -0,0 +1,245 @@ + 1, + 'cache_ttl' => 3600, + 'enable_debug' => 0, + 'cleanup_on_uninstall' => 0 +]); + +$cache_manager = new WPTag_Cache_Manager(); +$cache_stats = $cache_manager->get_cache_stats(); +?> + +
+
+

+
+ +
+
+ + + +

+ + + + + + + + + + + + + + + +
+ +

+ +

+
+ + + + +

+ +

+
+
+

+ + +

+

+ + +

+
+

+ +

+
+ +

+ + + + + +
+ +

+ +

+
+ +

+ + + + + +
+ +

+ +

+
+ +

+ + + + + + + + + + +
+

+ +
+

+ + +
+ +

+ +

+
+
+
+ + + + diff --git a/admin/partials/snippet-form.php b/admin/partials/snippet-form.php new file mode 100644 index 0000000..9990564 --- /dev/null +++ b/admin/partials/snippet-form.php @@ -0,0 +1,207 @@ +snippet_manager->get_snippet($snippet_id); + if (!$snippet) { + wp_die(__('Snippet not found', 'wptag')); + } + $is_edit = true; +} + +$categories = $this->snippet_manager->get_categories(); +$positions = $this->snippet_manager->get_positions(); +?> + +
+
+

+ + + + +

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+
+ + + +

+
+ + + +
+ + + +

+
+ + + +

+
+ + + +
+ + + +

+
+ + + +
+ + + +

+
+ + + +

+
+ + +
+

+
+
+ + +
+ + + + +
+ + +
+ +
+
+
+ +

+ +

+
+
+
diff --git a/admin/partials/snippets.php b/admin/partials/snippets.php new file mode 100644 index 0000000..5c5ec9f --- /dev/null +++ b/admin/partials/snippets.php @@ -0,0 +1,149 @@ + sanitize_text_field($_GET['search'] ?? ''), + 'category' => sanitize_key($_GET['filter_category'] ?? ''), + 'position' => sanitize_key($_GET['filter_position'] ?? ''), + 'status' => isset($_GET['filter_status']) ? intval($_GET['filter_status']) : null, + 'per_page' => $per_page, + 'page' => $current_page +]; + +$snippets = $this->snippet_manager->get_snippets($args); +$categories = $this->snippet_manager->get_categories(); +$positions = $this->snippet_manager->get_positions(); +?> + +
+
+

+ + + + +

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ + + + + + + +

+

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ +
+ + + + + +
+ +
+
diff --git a/admin/partials/templates.php b/admin/partials/templates.php new file mode 100644 index 0000000..a6861b8 --- /dev/null +++ b/admin/partials/templates.php @@ -0,0 +1,119 @@ +template_manager->get_templates($selected_category); +$categories = $this->template_manager->get_categories(); + +$templates_by_category = []; +foreach ($templates as $template) { + $cat = $template['service_category']; + if (!isset($templates_by_category[$cat])) { + $templates_by_category[$cat] = []; + } + $templates_by_category[$cat][] = $template; +} +?> + +
+
+

+
+ +
+
+ +

+
+ +
+ + + + $label) : ?> + + + + +
+ + +
+ + + + + + +

+

+
+ + $cat_templates) : ?> + + +

+ +
+ +
+

+

+ +
+ v + +
+ + +
+ +
+ + +
+
+ + + + __('Track website traffic and user behavior with Google Analytics 4', 'wptag'), + 'google_analytics_universal' => __('Track website traffic with Universal Analytics (legacy)', 'wptag'), + 'facebook_pixel' => __('Track conversions and build audiences for Facebook ads', 'wptag'), + 'google_ads' => __('Track conversions for Google Ads campaigns', 'wptag'), + 'google_search_console' => __('Verify site ownership for Google Search Console', 'wptag'), + 'baidu_tongji' => __('Track website traffic with Baidu Analytics', 'wptag'), + 'cnzz' => __('Track website traffic with CNZZ Analytics', 'wptag'), + '51la' => __('Track website traffic with 51.la Analytics', 'wptag'), + 'baidu_push' => __('Submit URLs to Baidu for faster indexing', 'wptag'), + 'toutiao_pixel' => __('Track conversions for Toutiao/TikTok ads', 'wptag'), + ]; + + return $descriptions[$service_type] ?? __('Configure and add this service to your site', 'wptag'); +} +?> diff --git a/assets/admin.css b/assets/admin.css new file mode 100644 index 0000000..2b5d52e --- /dev/null +++ b/assets/admin.css @@ -0,0 +1,374 @@ +.wptag-admin-wrap { + margin-top: 20px; +} + +.wptag-header { + background: #fff; + border: 1px solid #ccd0d4; + border-bottom: 0; + padding: 20px; + margin-bottom: 0; +} + +.wptag-header h1 { + margin: 0; + font-size: 24px; + font-weight: 400; + line-height: 1.3; +} + +.wptag-header .page-title-action { + margin-left: 10px; +} + +.wptag-content { + background: #fff; + border: 1px solid #ccd0d4; + padding: 20px; +} + +.wptag-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.wptag-stat-card { + background: #f8f9fa; + border: 1px solid #e2e4e7; + border-radius: 4px; + padding: 20px; + text-align: center; +} + +.wptag-stat-card h3 { + margin: 0 0 10px; + font-size: 14px; + color: #666; + font-weight: 400; +} + +.wptag-stat-card .stat-value { + font-size: 32px; + font-weight: 600; + color: #2271b1; +} + +.wptag-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +.wptag-table th, +.wptag-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #e2e4e7; +} + +.wptag-table th { + background: #f8f9fa; + font-weight: 600; + color: #2c3338; +} + +.wptag-table tbody tr:hover { + background: #f6f7f7; +} + +.wptag-status-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + font-size: 12px; + font-weight: 500; +} + +.wptag-status-badge.active { + background: #d4f4dd; + color: #00a32a; +} + +.wptag-status-badge.inactive { + background: #f5e6e6; + color: #d63638; +} + +.wptag-actions { + display: flex; + gap: 10px; +} + +.wptag-action-link { + color: #2271b1; + text-decoration: none; + font-size: 13px; +} + +.wptag-action-link:hover { + color: #135e96; + text-decoration: underline; +} + +.wptag-action-link.delete { + color: #d63638; +} + +.wptag-action-link.delete:hover { + color: #a02222; +} + +.wptag-form-table { + width: 100%; + max-width: 800px; +} + +.wptag-form-table th { + width: 200px; + padding: 20px 10px 20px 0; + vertical-align: top; + text-align: left; + font-weight: 600; +} + +.wptag-form-table td { + padding: 15px 10px; +} + +.wptag-form-table input[type="text"], +.wptag-form-table input[type="number"], +.wptag-form-table select, +.wptag-form-table textarea { + width: 100%; + max-width: 400px; +} + +.wptag-code-editor { + width: 100%; + min-height: 300px; + font-family: Consolas, Monaco, monospace; + font-size: 13px; +} + +.wptag-conditions-builder { + background: #f8f9fa; + border: 1px solid #e2e4e7; + border-radius: 4px; + padding: 20px; + margin-top: 10px; +} + +.wptag-condition-group { + background: #fff; + border: 1px solid #e2e4e7; + border-radius: 4px; + padding: 15px; + margin-bottom: 15px; +} + +.wptag-condition-row { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 10px; +} + +.wptag-condition-row select, +.wptag-condition-row input { + flex: 1; + min-width: 0; +} + +.wptag-templates-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.wptag-template-card { + background: #fff; + border: 1px solid #e2e4e7; + border-radius: 4px; + padding: 20px; + transition: box-shadow 0.2s; +} + +.wptag-template-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.wptag-template-card h3 { + margin: 0 0 10px; + font-size: 16px; + color: #2c3338; +} + +.wptag-template-card p { + color: #666; + margin: 0 0 15px; + font-size: 14px; +} + +.wptag-template-card .button { + width: 100%; + text-align: center; +} + +.wptag-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 100000; +} + +.wptag-modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #fff; + border-radius: 4px; + max-width: 600px; + width: 90%; + max-height: 90vh; + overflow: auto; +} + +.wptag-modal-header { + padding: 20px; + border-bottom: 1px solid #e2e4e7; +} + +.wptag-modal-header h2 { + margin: 0; + font-size: 20px; +} + +.wptag-modal-body { + padding: 20px; +} + +.wptag-modal-footer { + padding: 20px; + border-top: 1px solid #e2e4e7; + text-align: right; +} + +.wptag-filters { + display: flex; + gap: 15px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.wptag-filter-item { + display: flex; + flex-direction: column; + gap: 5px; +} + +.wptag-filter-item label { + font-size: 13px; + font-weight: 600; + color: #2c3338; +} + +.wptag-empty-state { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.wptag-empty-state svg { + width: 64px; + height: 64px; + margin-bottom: 20px; + opacity: 0.3; +} + +.wptag-empty-state h3 { + font-size: 18px; + font-weight: 400; + margin: 0 0 10px; +} + +.wptag-empty-state p { + margin: 0 0 20px; +} + +.wptag-tabs { + display: flex; + border-bottom: 1px solid #e2e4e7; + margin: -20px -20px 20px; + padding: 0 20px; +} + +.wptag-tab { + padding: 15px 20px; + border-bottom: 2px solid transparent; + text-decoration: none; + color: #2c3338; + font-weight: 500; + transition: all 0.2s; +} + +.wptag-tab:hover { + color: #2271b1; +} + +.wptag-tab.active { + color: #2271b1; + border-bottom-color: #2271b1; +} + +.wptag-notice { + padding: 12px 20px; + margin: 20px 0; + border-radius: 4px; + display: flex; + align-items: center; + gap: 10px; +} + +.wptag-notice.info { + background: #e5f5fa; + color: #0073aa; +} + +.wptag-notice.success { + background: #d4f4dd; + color: #00a32a; +} + +.wptag-notice.warning { + background: #fcf9e8; + color: #996800; +} + +.wptag-notice.error { + background: #f5e6e6; + color: #d63638; +} + +@media screen and (max-width: 782px) { + .wptag-form-table th { + width: auto; + display: block; + padding-bottom: 5px; + } + + .wptag-filters { + flex-direction: column; + } + + .wptag-condition-row { + flex-direction: column; + } +} diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..d3ff23e --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,322 @@ +(function($) { + 'use strict'; + + const WPTagAdmin = { + init: function() { + this.bindEvents(); + this.initCodeEditor(); + this.initConditionsBuilder(); + }, + + bindEvents: function() { + $(document).on('click', '.wptag-toggle-status', this.toggleStatus); + $(document).on('click', '.wptag-delete-snippet', this.deleteSnippet); + $(document).on('change', '.wptag-filter', this.filterSnippets); + $(document).on('click', '.wptag-template-use', this.useTemplate); + $(document).on('submit', '.wptag-snippet-form', this.validateForm); + $(document).on('click', '.wptag-add-condition', this.addCondition); + $(document).on('click', '.wptag-remove-condition', this.removeCondition); + $(document).on('change', '#code_type', this.updateCodeEditor); + }, + + toggleStatus: function(e) { + e.preventDefault(); + + const $link = $(this); + const snippetId = $link.data('snippet-id'); + + $.ajax({ + url: wptagAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wptag_toggle_snippet', + snippet_id: snippetId, + nonce: wptagAdmin.nonce + }, + success: function(response) { + if (response.success) { + location.reload(); + } else { + alert(response.data.message || wptagAdmin.strings.error); + } + }, + error: function() { + alert(wptagAdmin.strings.error); + } + }); + }, + + deleteSnippet: function(e) { + e.preventDefault(); + + if (!confirm(wptagAdmin.strings.confirmDelete)) { + return; + } + + const $link = $(this); + const snippetId = $link.data('snippet-id'); + + $.ajax({ + url: wptagAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wptag_delete_snippet', + snippet_id: snippetId, + nonce: wptagAdmin.nonce + }, + success: function(response) { + if (response.success) { + $link.closest('tr').fadeOut(400, function() { + $(this).remove(); + }); + } else { + alert(response.data.message || wptagAdmin.strings.error); + } + }, + error: function() { + alert(wptagAdmin.strings.error); + } + }); + }, + + filterSnippets: function() { + const filters = { + search: $('#filter-search').val(), + category: $('#filter-category').val(), + position: $('#filter-position').val(), + status: $('#filter-status').val() + }; + + const params = new URLSearchParams(filters); + params.delete('page'); + + Object.keys(filters).forEach(key => { + if (!filters[key]) { + params.delete(key); + } + }); + + window.location.href = window.location.pathname + '?page=wptag-snippets&' + params.toString(); + }, + + initCodeEditor: function() { + const $codeTextarea = $('#snippet-code'); + + if ($codeTextarea.length && typeof wp !== 'undefined' && wp.codeEditor) { + const editorSettings = wp.codeEditor.defaultSettings || {}; + const codeType = $('#code_type').val(); + + editorSettings.codemirror = { + ...editorSettings.codemirror, + mode: this.getCodeMirrorMode(codeType), + lineNumbers: true, + lineWrapping: true, + indentUnit: 2, + tabSize: 2 + }; + + this.codeEditor = wp.codeEditor.initialize($codeTextarea, editorSettings); + } + }, + + getCodeMirrorMode: function(codeType) { + const modes = { + 'html': 'htmlmixed', + 'javascript': 'javascript', + 'css': 'css' + }; + + return modes[codeType] || 'htmlmixed'; + }, + + updateCodeEditor: function() { + const codeType = $(this).val(); + + if (WPTagAdmin.codeEditor && WPTagAdmin.codeEditor.codemirror) { + WPTagAdmin.codeEditor.codemirror.setOption('mode', WPTagAdmin.getCodeMirrorMode(codeType)); + } + }, + + initConditionsBuilder: function() { + const $builder = $('.wptag-conditions-builder'); + + if (!$builder.length) { + return; + } + + this.loadConditionTypes(); + }, + + loadConditionTypes: function() { + $.ajax({ + url: wptagAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wptag_get_condition_types', + nonce: wptagAdmin.nonce + }, + success: function(response) { + if (response.success) { + WPTagAdmin.conditionTypes = response.data.types; + } + } + }); + }, + + addCondition: function(e) { + e.preventDefault(); + + const $group = $(this).closest('.wptag-condition-group'); + const $newRow = WPTagAdmin.createConditionRow(); + + $group.find('.wptag-conditions-list').append($newRow); + }, + + removeCondition: function(e) { + e.preventDefault(); + + $(this).closest('.wptag-condition-row').fadeOut(300, function() { + $(this).remove(); + }); + }, + + createConditionRow: function() { + const html = ` +
+ + + + +
+ `; + + return $(html); + }, + + useTemplate: function(e) { + e.preventDefault(); + + const $button = $(this); + const serviceType = $button.data('service-type'); + + $.ajax({ + url: wptagAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wptag_get_template', + service_type: serviceType, + nonce: wptagAdmin.nonce + }, + success: function(response) { + if (response.success) { + WPTagAdmin.showTemplateModal(response.data.template); + } else { + alert(response.data.message || wptagAdmin.strings.error); + } + } + }); + }, + + showTemplateModal: function(template) { + const fields = template.config_fields.map(field => { + return ` +
+ + +
+ `; + }).join(''); + + const modalHtml = ` +
+
+
+

${template.service_name} Configuration

+
+
+
+ ${fields} +
+ +
+
+
+ `; + + $('body').append(modalHtml); + $('#template-config-modal').fadeIn(); + + $('#template-config-form').on('submit', function(e) { + e.preventDefault(); + WPTagAdmin.processTemplate(template.service_type, $(this).serialize()); + }); + }, + + processTemplate: function(serviceType, formData) { + $.ajax({ + url: wptagAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wptag_process_template', + service_type: serviceType, + config: formData, + nonce: wptagAdmin.nonce + }, + success: function(response) { + if (response.success) { + window.location.href = `admin.php?page=wptag-snippets&action=edit&snippet_id=${response.data.snippet_id}`; + } else { + alert(response.data.message || wptagAdmin.strings.error); + } + } + }); + }, + + closeModal: function() { + $('.wptag-modal').fadeOut(300, function() { + $(this).remove(); + }); + }, + + validateForm: function(e) { + const $form = $(this); + const name = $form.find('#snippet-name').val(); + const code = WPTagAdmin.codeEditor ? + WPTagAdmin.codeEditor.codemirror.getValue() : + $form.find('#snippet-code').val(); + + if (!name.trim()) { + e.preventDefault(); + alert('Please enter a snippet name'); + return false; + } + + if (!code.trim()) { + e.preventDefault(); + alert('Please enter some code'); + return false; + } + + return true; + } + }; + + $(document).ready(function() { + WPTagAdmin.init(); + }); + +})(jQuery); diff --git a/includes/class-wptag-cache-manager.php b/includes/class-wptag-cache-manager.php new file mode 100644 index 0000000..c90d5ff --- /dev/null +++ b/includes/class-wptag-cache-manager.php @@ -0,0 +1,160 @@ +cache_enabled = !defined('WPTAG_DISABLE_CACHE') || !WPTAG_DISABLE_CACHE; + $this->ttl = defined('WPTAG_CACHE_TTL') ? WPTAG_CACHE_TTL : 3600; + } + + public function get($key) { + if (!$this->cache_enabled) { + return false; + } + + return wp_cache_get($key, $this->cache_group); + } + + public function set($key, $value, $ttl = null) { + if (!$this->cache_enabled) { + return false; + } + + $ttl = $ttl ?? $this->ttl; + return wp_cache_set($key, $value, $this->cache_group, $ttl); + } + + public function delete($key) { + return wp_cache_delete($key, $this->cache_group); + } + + public function flush() { + wp_cache_flush(); + } + + public function clear_snippet_cache($snippet_id = null) { + if ($snippet_id) { + $this->delete('snippet_' . $snippet_id); + } + + $this->delete('active_snippets'); + $this->clear_output_cache(); + } + + public function clear_output_cache() { + $positions = ['head', 'footer', 'before_content', 'after_content']; + + foreach ($positions as $position) { + $this->delete_by_prefix('output_' . $position); + } + } + + public function clear_condition_cache() { + $this->delete_by_prefix('condition_'); + } + + private function delete_by_prefix($prefix) { + global $wp_object_cache; + + if (method_exists($wp_object_cache, 'delete_by_group')) { + $wp_object_cache->delete_by_group($this->cache_group); + } else { + $this->delete($prefix); + } + } + + public function warm_cache() { + if (!$this->cache_enabled) { + return; + } + + global $wpdb; + $table = $wpdb->prefix . 'wptag_snippets'; + + $active_snippets = $wpdb->get_results( + "SELECT * FROM $table WHERE status = 1 ORDER BY priority ASC", + ARRAY_A + ); + + $by_position = []; + foreach ($active_snippets as $snippet) { + $position = $snippet['position']; + if (!isset($by_position[$position])) { + $by_position[$position] = []; + } + $by_position[$position][] = $snippet; + } + + foreach ($by_position as $position => $snippets) { + $this->set('snippets_' . $position, $snippets); + } + + $this->set('active_snippets', $active_snippets); + } + + public function get_cache_stats() { + global $wp_object_cache; + + $stats = [ + 'enabled' => $this->cache_enabled, + 'ttl' => $this->ttl, + 'hits' => 0, + 'misses' => 0, + 'size' => 0 + ]; + + if (method_exists($wp_object_cache, 'stats')) { + $cache_stats = $wp_object_cache->stats(); + $stats['hits'] = $cache_stats['hits'] ?? 0; + $stats['misses'] = $cache_stats['misses'] ?? 0; + } + + return $stats; + } + + public function schedule_cleanup() { + if (!wp_next_scheduled('wptag_cache_cleanup')) { + wp_schedule_event(time(), 'daily', 'wptag_cache_cleanup'); + } + + add_action('wptag_cache_cleanup', [$this, 'cleanup_expired']); + } + + public function cleanup_expired() { + global $wpdb; + $logs_table = $wpdb->prefix . 'wptag_logs'; + + $thirty_days_ago = date('Y-m-d H:i:s', strtotime('-30 days')); + + $wpdb->query($wpdb->prepare( + "DELETE FROM $logs_table WHERE created_at < %s", + $thirty_days_ago + )); + + $this->clear_output_cache(); + } + + public function invalidate_on_save($post_id) { + if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { + return; + } + + $this->clear_output_cache(); + } + + public function init_hooks() { + add_action('save_post', [$this, 'invalidate_on_save']); + add_action('switch_theme', [$this, 'flush']); + add_action('activated_plugin', [$this, 'flush']); + add_action('deactivated_plugin', [$this, 'flush']); + + $this->schedule_cleanup(); + } +} diff --git a/includes/class-wptag-condition-engine.php b/includes/class-wptag-condition-engine.php new file mode 100644 index 0000000..1d91720 --- /dev/null +++ b/includes/class-wptag-condition-engine.php @@ -0,0 +1,311 @@ +context_cache[$cache_key])) { + return $this->context_cache[$cache_key]; + } + + $result = $this->process_condition_group($conditions, $context); + $this->context_cache[$cache_key] = $result; + + return $result; + } + + private function process_condition_group($conditions, $context) { + $logic = $conditions['logic'] ?? 'AND'; + $rules = $conditions['rules'] ?? []; + + if (empty($rules)) { + return true; + } + + $results = []; + foreach ($rules as $rule) { + if (isset($rule['rules'])) { + $results[] = $this->process_condition_group($rule, $context); + } else { + $results[] = $this->evaluate_single_condition($rule, $context); + } + } + + if ($logic === 'OR') { + return in_array(true, $results, true); + } else { + return !in_array(false, $results, true); + } + } + + private function evaluate_single_condition($rule, $context) { + $type = $rule['type'] ?? ''; + $operator = $rule['operator'] ?? 'equals'; + $value = $rule['value'] ?? ''; + + switch ($type) { + case 'page_type': + return $this->check_page_type($value, $operator); + + case 'user_status': + return $this->check_user_status($value, $operator); + + case 'user_role': + return $this->check_user_role($value, $operator); + + case 'device_type': + return $this->check_device_type($value, $operator); + + case 'post_id': + return $this->check_post_id($value, $operator); + + case 'category': + return $this->check_category($value, $operator); + + case 'tag': + return $this->check_tag($value, $operator); + + case 'url': + return $this->check_url($value, $operator); + + case 'date_range': + return $this->check_date_range($value, $operator); + + case 'time': + return $this->check_time($value, $operator); + + case 'day_of_week': + return $this->check_day_of_week($value, $operator); + + default: + return apply_filters('wptag_custom_condition', true, $type, $value, $operator, $context); + } + } + + private function check_page_type($value, $operator) { + $page_types = [ + 'home' => is_home() || is_front_page(), + 'single' => is_single(), + 'page' => is_page(), + 'archive' => is_archive(), + 'category' => is_category(), + 'tag' => is_tag(), + 'search' => is_search(), + '404' => is_404(), + 'author' => is_author(), + 'date' => is_date() + ]; + + $is_type = $page_types[$value] ?? false; + + return $operator === 'not_equals' ? !$is_type : $is_type; + } + + private function check_user_status($value, $operator) { + $is_logged_in = is_user_logged_in(); + + if ($value === 'logged_in') { + return $operator === 'not_equals' ? !$is_logged_in : $is_logged_in; + } else { + return $operator === 'not_equals' ? $is_logged_in : !$is_logged_in; + } + } + + private function check_user_role($value, $operator) { + if (!is_user_logged_in()) { + return $operator === 'not_equals'; + } + + $user = wp_get_current_user(); + $has_role = in_array($value, $user->roles); + + return $operator === 'not_equals' ? !$has_role : $has_role; + } + + private function check_device_type($value, $operator) { + $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $is_mobile = wp_is_mobile(); + + $device_checks = [ + 'mobile' => $is_mobile && !$this->is_tablet($user_agent), + 'tablet' => $this->is_tablet($user_agent), + 'desktop' => !$is_mobile + ]; + + $is_device = $device_checks[$value] ?? false; + + return $operator === 'not_equals' ? !$is_device : $is_device; + } + + private function is_tablet($user_agent) { + $tablet_patterns = '/iPad|Android.*Tablet|Tablet.*Android|Kindle|Silk|Galaxy Tab/i'; + return preg_match($tablet_patterns, $user_agent); + } + + private function check_post_id($value, $operator) { + $current_id = get_the_ID(); + $ids = array_map('intval', explode(',', $value)); + + switch ($operator) { + case 'equals': + return in_array($current_id, $ids); + case 'not_equals': + return !in_array($current_id, $ids); + case 'greater_than': + return $current_id > $ids[0]; + case 'less_than': + return $current_id < $ids[0]; + default: + return false; + } + } + + private function check_category($value, $operator) { + if (!is_single() && !is_category()) { + return $operator === 'not_equals'; + } + + $categories = explode(',', $value); + $has_category = false; + + if (is_single()) { + foreach ($categories as $cat) { + if (has_category($cat)) { + $has_category = true; + break; + } + } + } elseif (is_category()) { + $current_cat = get_queried_object(); + $has_category = in_array($current_cat->slug, $categories) || + in_array($current_cat->term_id, $categories); + } + + return $operator === 'not_equals' ? !$has_category : $has_category; + } + + private function check_tag($value, $operator) { + if (!is_single() && !is_tag()) { + return $operator === 'not_equals'; + } + + $tags = explode(',', $value); + $has_tag = false; + + if (is_single()) { + foreach ($tags as $tag) { + if (has_tag($tag)) { + $has_tag = true; + break; + } + } + } elseif (is_tag()) { + $current_tag = get_queried_object(); + $has_tag = in_array($current_tag->slug, $tags) || + in_array($current_tag->term_id, $tags); + } + + return $operator === 'not_equals' ? !$has_tag : $has_tag; + } + + private function check_url($value, $operator) { + $current_url = $_SERVER['REQUEST_URI'] ?? ''; + + switch ($operator) { + case 'contains': + return strpos($current_url, $value) !== false; + case 'not_contains': + return strpos($current_url, $value) === false; + case 'equals': + return $current_url === $value; + case 'not_equals': + return $current_url !== $value; + case 'starts_with': + return strpos($current_url, $value) === 0; + case 'ends_with': + return substr($current_url, -strlen($value)) === $value; + default: + return false; + } + } + + private function check_date_range($value, $operator) { + $current_time = current_time('timestamp'); + $dates = explode('|', $value); + + if (count($dates) !== 2) { + return false; + } + + $start_date = strtotime($dates[0]); + $end_date = strtotime($dates[1] . ' 23:59:59'); + + $in_range = $current_time >= $start_date && $current_time <= $end_date; + + return $operator === 'not_in' ? !$in_range : $in_range; + } + + private function check_time($value, $operator) { + $current_time = current_time('H:i'); + $times = explode('|', $value); + + if (count($times) !== 2) { + return false; + } + + $in_range = $current_time >= $times[0] && $current_time <= $times[1]; + + return $operator === 'not_in' ? !$in_range : $in_range; + } + + private function check_day_of_week($value, $operator) { + $current_day = strtolower(current_time('l')); + $days = array_map('strtolower', explode(',', $value)); + + $is_day = in_array($current_day, $days); + + return $operator === 'not_in' ? !$is_day : $is_day; + } + + public function get_condition_types() { + return [ + 'page_type' => [ + 'label' => __('Page Type', 'wptag'), + 'values' => [ + 'home' => __('Home Page', 'wptag'), + 'single' => __('Single Post', 'wptag'), + 'page' => __('Page', 'wptag'), + 'archive' => __('Archive', 'wptag'), + 'category' => __('Category', 'wptag'), + 'tag' => __('Tag', 'wptag'), + 'search' => __('Search', 'wptag'), + '404' => __('404 Page', 'wptag') + ] + ], + 'user_status' => [ + 'label' => __('User Status', 'wptag'), + 'values' => [ + 'logged_in' => __('Logged In', 'wptag'), + 'logged_out' => __('Logged Out', 'wptag') + ] + ], + 'device_type' => [ + 'label' => __('Device Type', 'wptag'), + 'values' => [ + 'mobile' => __('Mobile', 'wptag'), + 'tablet' => __('Tablet', 'wptag'), + 'desktop' => __('Desktop', 'wptag') + ] + ] + ]; + } +} diff --git a/includes/class-wptag-core.php b/includes/class-wptag-core.php new file mode 100644 index 0000000..eac4dbb --- /dev/null +++ b/includes/class-wptag-core.php @@ -0,0 +1,267 @@ +load_dependencies(); + $this->init(); + } + + private function load_dependencies() { + require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-snippet-manager.php'; + require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-condition-engine.php'; + require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-output-controller.php'; + require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-template-manager.php'; + require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-cache-manager.php'; + + if (is_admin()) { + require_once WPTAG_PLUGIN_DIR . 'admin/class-wptag-admin-controller.php'; + require_once WPTAG_PLUGIN_DIR . 'admin/class-wptag-ajax-handler.php'; + require_once WPTAG_PLUGIN_DIR . 'admin/class-wptag-admin-interface.php'; + } + } + + private function init() { + $this->snippet_manager = new WPTag_Snippet_Manager(); + $this->condition_engine = new WPTag_Condition_Engine(); + $this->template_manager = new WPTag_Template_Manager(); + $this->cache_manager = new WPTag_Cache_Manager(); + $this->output_controller = new WPTag_Output_Controller( + $this->snippet_manager, + $this->condition_engine, + $this->cache_manager + ); + + if (is_admin()) { + new WPTag_Admin_Controller($this->snippet_manager, $this->template_manager); + new WPTag_Ajax_Handler($this->snippet_manager, $this->template_manager); + } else { + $this->register_output_hooks(); + } + + add_action('init', [$this, 'check_version']); + } + + private function register_output_hooks() { + add_action('wp_head', [$this->output_controller, 'render_head'], 1); + add_action('wp_footer', [$this->output_controller, 'render_footer'], 999); + add_filter('the_content', [$this->output_controller, 'filter_content'], 10); + } + + public function check_version() { + $installed_version = get_option('wptag_db_version'); + if ($installed_version !== WPTAG_DB_VERSION) { + self::create_tables(); + update_option('wptag_db_version', WPTAG_DB_VERSION); + } + } + + public static function activate() { + if (version_compare(PHP_VERSION, '8.0', '<')) { + deactivate_plugins(plugin_basename(WPTAG_PLUGIN_FILE)); + wp_die('WPTAG requires PHP 8.0 or higher.'); + } + + if (version_compare(get_bloginfo('version'), '6.8', '<')) { + deactivate_plugins(plugin_basename(WPTAG_PLUGIN_FILE)); + wp_die('WPTAG requires WordPress 6.8 or higher.'); + } + + self::create_tables(); + self::create_default_templates(); + flush_rewrite_rules(); + } + + public static function deactivate() { + wp_clear_scheduled_hook('wptag_cleanup_logs'); + flush_rewrite_rules(); + } + + private static function create_tables() { + global $wpdb; + $charset_collate = $wpdb->get_charset_collate(); + + $sql_snippets = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wptag_snippets ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + description text, + code longtext NOT NULL, + code_type varchar(50) DEFAULT 'html', + position varchar(100) NOT NULL, + category varchar(100) DEFAULT 'custom', + priority int(11) DEFAULT 10, + status tinyint(1) DEFAULT 1, + conditions longtext, + device_type varchar(50) DEFAULT 'all', + load_method varchar(50) DEFAULT 'normal', + created_by bigint(20) unsigned, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + last_modified_by bigint(20) unsigned, + PRIMARY KEY (id), + KEY idx_status_position (status, position), + KEY idx_category (category), + KEY idx_priority (priority) + ) $charset_collate;"; + + $sql_templates = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wptag_templates ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + service_type varchar(100) NOT NULL, + service_name varchar(255) NOT NULL, + service_category varchar(100) NOT NULL, + config_fields longtext, + code_template longtext NOT NULL, + default_position varchar(100) NOT NULL, + is_active tinyint(1) DEFAULT 1, + version varchar(20) DEFAULT '1.0', + created_at datetime DEFAULT CURRENT_TIMESTAMP, + updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY idx_service_type (service_type), + KEY idx_category (service_category) + ) $charset_collate;"; + + $sql_logs = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wptag_logs ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + user_id bigint(20) unsigned NOT NULL, + action varchar(100) NOT NULL, + object_type varchar(50) NOT NULL, + object_id bigint(20) unsigned, + old_value longtext, + new_value longtext, + ip_address varchar(45), + user_agent text, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_user_action (user_id, action), + KEY idx_created_at (created_at) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql_snippets); + dbDelta($sql_templates); + dbDelta($sql_logs); + } + + private static function create_default_templates() { + global $wpdb; + $table = $wpdb->prefix . 'wptag_templates'; + + $templates = [ + [ + 'service_type' => 'google_analytics_4', + 'service_name' => 'Google Analytics 4', + 'service_category' => 'analytics', + 'config_fields' => json_encode([ + ['name' => 'measurement_id', 'label' => 'Measurement ID', 'type' => 'text', 'required' => true] + ]), + 'code_template' => ' +', + 'default_position' => 'head' + ], + [ + 'service_type' => 'facebook_pixel', + 'service_name' => 'Facebook Pixel', + 'service_category' => 'marketing', + 'config_fields' => json_encode([ + ['name' => 'pixel_id', 'label' => 'Pixel ID', 'type' => 'text', 'required' => true] + ]), + 'code_template' => ' +', + 'default_position' => 'head' + ], + [ + 'service_type' => 'google_ads', + 'service_name' => 'Google Ads Conversion', + 'service_category' => 'marketing', + 'config_fields' => json_encode([ + ['name' => 'conversion_id', 'label' => 'Conversion ID', 'type' => 'text', 'required' => true], + ['name' => 'conversion_label', 'label' => 'Conversion Label', 'type' => 'text', 'required' => true] + ]), + 'code_template' => ' +', + 'default_position' => 'head' + ], + [ + 'service_type' => 'google_search_console', + 'service_name' => 'Google Search Console', + 'service_category' => 'seo', + 'config_fields' => json_encode([ + ['name' => 'verification_code', 'label' => 'Verification Code', 'type' => 'text', 'required' => true] + ]), + 'code_template' => '', + 'default_position' => 'head' + ], + [ + 'service_type' => 'baidu_tongji', + 'service_name' => 'Baidu Tongji', + 'service_category' => 'analytics', + 'config_fields' => json_encode([ + ['name' => 'site_id', 'label' => 'Site ID', 'type' => 'text', 'required' => true] + ]), + 'code_template' => '', + 'default_position' => 'head' + ] + ]; + + foreach ($templates as $template) { + $exists = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $table WHERE service_type = %s", + $template['service_type'] + )); + + if (!$exists) { + $wpdb->insert($table, $template); + } + } + } +} diff --git a/includes/class-wptag-output-controller.php b/includes/class-wptag-output-controller.php new file mode 100644 index 0000000..4cdbbb9 --- /dev/null +++ b/includes/class-wptag-output-controller.php @@ -0,0 +1,178 @@ +snippet_manager = $snippet_manager; + $this->condition_engine = $condition_engine; + $this->cache_manager = $cache_manager; + } + + public function render_head() { + $this->render_snippets('head'); + } + + public function render_footer() { + $this->render_snippets('footer'); + } + + public function filter_content($content) { + if (!in_the_loop() || !is_main_query()) { + return $content; + } + + $before = $this->get_rendered_snippets('before_content'); + $after = $this->get_rendered_snippets('after_content'); + + return $before . $content . $after; + } + + private function render_snippets($position) { + echo $this->get_rendered_snippets($position); + } + + private function get_rendered_snippets($position) { + $cache_key = 'wptag_output_' . $position . '_' . $this->get_cache_context(); + $cached = $this->cache_manager->get($cache_key); + + if ($cached !== false && !$this->is_preview_mode()) { + return $cached; + } + + $snippets = $this->snippet_manager->get_active_snippets_by_position($position); + $output = ''; + + foreach ($snippets as $snippet) { + if ($this->should_render_snippet($snippet)) { + $output .= $this->render_single_snippet($snippet); + $this->rendered_snippets[] = $snippet['id']; + } + } + + if (!empty($output)) { + $output = "\n\n" . $output . "\n"; + } + + $this->cache_manager->set($cache_key, $output, 3600); + + return $output; + } + + private function should_render_snippet($snippet) { + if (in_array($snippet['id'], $this->rendered_snippets)) { + return false; + } + + if ($this->is_preview_mode() && !current_user_can('manage_options')) { + return false; + } + + if (!empty($snippet['device_type']) && $snippet['device_type'] !== 'all') { + $device_check = $this->condition_engine->evaluate_conditions([ + 'rules' => [[ + 'type' => 'device_type', + 'operator' => 'equals', + 'value' => $snippet['device_type'] + ]] + ]); + + if (!$device_check) { + return false; + } + } + + if (!empty($snippet['conditions'])) { + return $this->condition_engine->evaluate_conditions($snippet['conditions']); + } + + return true; + } + + private function render_single_snippet($snippet) { + $code = $snippet['code']; + + if ($snippet['load_method'] === 'async' && $snippet['code_type'] === 'javascript') { + $code = $this->wrap_async_script($code); + } elseif ($snippet['load_method'] === 'defer' && $snippet['code_type'] === 'javascript') { + $code = $this->wrap_defer_script($code); + } + + $code = apply_filters('wptag_snippet_output', $code, $snippet); + + if ($this->is_preview_mode() && current_user_can('manage_options')) { + $code = $this->wrap_preview_mode($code, $snippet); + } + + return $code . "\n"; + } + + private function wrap_async_script($code) { + if (strpos($code, ''; + } + + return str_replace(''; + } + + return str_replace('\n{$code}\n\n"; + } + + private function get_cache_context() { + $context = [ + 'type' => $this->get_page_type(), + 'id' => get_the_ID(), + 'user' => is_user_logged_in() ? 'logged_in' : 'logged_out' + ]; + + if (is_user_logged_in()) { + $user = wp_get_current_user(); + $context['roles'] = $user->roles; + } + + return md5(json_encode($context)); + } + + private function get_page_type() { + if (is_home() || is_front_page()) return 'home'; + if (is_single()) return 'single'; + if (is_page()) return 'page'; + if (is_category()) return 'category'; + if (is_tag()) return 'tag'; + if (is_archive()) return 'archive'; + if (is_search()) return 'search'; + if (is_404()) return '404'; + return 'other'; + } + + private function is_preview_mode() { + return isset($_GET['wptag_preview']) && $_GET['wptag_preview'] === '1'; + } + + public function clear_output_cache() { + $positions = ['head', 'footer', 'before_content', 'after_content']; + + foreach ($positions as $position) { + wp_cache_delete('wptag_output_' . $position, 'wptag'); + } + } +} diff --git a/includes/class-wptag-snippet-manager.php b/includes/class-wptag-snippet-manager.php new file mode 100644 index 0000000..13399a2 --- /dev/null +++ b/includes/class-wptag-snippet-manager.php @@ -0,0 +1,264 @@ +table_name = $wpdb->prefix . 'wptag_snippets'; + } + + public function get_snippet($id) { + global $wpdb; + $snippet = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$this->table_name} WHERE id = %d", + $id + ), ARRAY_A); + + if ($snippet && !empty($snippet['conditions'])) { + $snippet['conditions'] = json_decode($snippet['conditions'], true); + } + + return $snippet; + } + + public function get_snippets($args = []) { + global $wpdb; + + $defaults = [ + 'status' => null, + 'position' => null, + 'category' => null, + 'search' => '', + 'orderby' => 'priority', + 'order' => 'ASC', + 'per_page' => 20, + 'page' => 1 + ]; + + $args = wp_parse_args($args, $defaults); + + $where = ['1=1']; + $where_values = []; + + if ($args['status'] !== null) { + $where[] = 'status = %d'; + $where_values[] = $args['status']; + } + + if (!empty($args['position'])) { + $where[] = 'position = %s'; + $where_values[] = $args['position']; + } + + if (!empty($args['category'])) { + $where[] = 'category = %s'; + $where_values[] = $args['category']; + } + + if (!empty($args['search'])) { + $where[] = '(name LIKE %s OR description LIKE %s)'; + $search_term = '%' . $wpdb->esc_like($args['search']) . '%'; + $where_values[] = $search_term; + $where_values[] = $search_term; + } + + $where_clause = implode(' AND ', $where); + $orderby = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']); + $offset = ($args['page'] - 1) * $args['per_page']; + + $query = "SELECT * FROM {$this->table_name} WHERE {$where_clause} ORDER BY {$orderby} LIMIT %d OFFSET %d"; + $where_values[] = $args['per_page']; + $where_values[] = $offset; + + $results = $wpdb->get_results($wpdb->prepare($query, $where_values), ARRAY_A); + + foreach ($results as &$result) { + if (!empty($result['conditions'])) { + $result['conditions'] = json_decode($result['conditions'], true); + } + } + + return $results; + } + + public function get_active_snippets_by_position($position) { + global $wpdb; + + $snippets = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$this->table_name} + WHERE status = 1 AND position = %s + ORDER BY priority ASC, id ASC", + $position + ), ARRAY_A); + + foreach ($snippets as &$snippet) { + if (!empty($snippet['conditions'])) { + $snippet['conditions'] = json_decode($snippet['conditions'], true); + } + } + + return $snippets; + } + + public function create_snippet($data) { + global $wpdb; + + $snippet_data = $this->prepare_snippet_data($data); + $snippet_data['created_by'] = get_current_user_id(); + $snippet_data['created_at'] = current_time('mysql'); + + $result = $wpdb->insert($this->table_name, $snippet_data); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to create snippet'); + } + + $snippet_id = $wpdb->insert_id; + $this->log_action('create', $snippet_id, null, $snippet_data); + + return $snippet_id; + } + + public function update_snippet($id, $data) { + global $wpdb; + + $old_snippet = $this->get_snippet($id); + if (!$old_snippet) { + return new WP_Error('not_found', 'Snippet not found'); + } + + $snippet_data = $this->prepare_snippet_data($data); + $snippet_data['last_modified_by'] = get_current_user_id(); + $snippet_data['updated_at'] = current_time('mysql'); + + $result = $wpdb->update( + $this->table_name, + $snippet_data, + ['id' => $id] + ); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to update snippet'); + } + + $this->log_action('update', $id, $old_snippet, $snippet_data); + $this->clear_cache(); + + return true; + } + + public function delete_snippet($id) { + global $wpdb; + + $old_snippet = $this->get_snippet($id); + if (!$old_snippet) { + return new WP_Error('not_found', 'Snippet not found'); + } + + $result = $wpdb->delete($this->table_name, ['id' => $id]); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to delete snippet'); + } + + $this->log_action('delete', $id, $old_snippet, null); + $this->clear_cache(); + + return true; + } + + public function toggle_status($id) { + global $wpdb; + + $snippet = $this->get_snippet($id); + if (!$snippet) { + return new WP_Error('not_found', 'Snippet not found'); + } + + $new_status = $snippet['status'] ? 0 : 1; + + $result = $wpdb->update( + $this->table_name, + ['status' => $new_status], + ['id' => $id] + ); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to update status'); + } + + $this->clear_cache(); + + return $new_status; + } + + private function prepare_snippet_data($data) { + $prepared = [ + 'name' => sanitize_text_field($data['name'] ?? ''), + 'description' => sanitize_textarea_field($data['description'] ?? ''), + 'code' => $data['code'] ?? '', + 'code_type' => sanitize_key($data['code_type'] ?? 'html'), + 'position' => sanitize_key($data['position'] ?? 'head'), + 'category' => sanitize_key($data['category'] ?? 'custom'), + 'priority' => intval($data['priority'] ?? 10), + 'status' => isset($data['status']) ? intval($data['status']) : 1, + 'device_type' => sanitize_key($data['device_type'] ?? 'all'), + 'load_method' => sanitize_key($data['load_method'] ?? 'normal') + ]; + + if (!empty($data['conditions']) && is_array($data['conditions'])) { + $prepared['conditions'] = json_encode($data['conditions']); + } else { + $prepared['conditions'] = null; + } + + return $prepared; + } + + private function log_action($action, $object_id, $old_value = null, $new_value = null) { + global $wpdb; + + $log_data = [ + 'user_id' => get_current_user_id(), + 'action' => $action, + 'object_type' => 'snippet', + 'object_id' => $object_id, + 'old_value' => $old_value ? json_encode($old_value) : null, + 'new_value' => $new_value ? json_encode($new_value) : null, + 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + 'created_at' => current_time('mysql') + ]; + + $wpdb->insert($wpdb->prefix . 'wptag_logs', $log_data); + } + + private function clear_cache() { + wp_cache_delete('wptag_active_snippets', 'wptag'); + wp_cache_delete('wptag_snippet_conditions', 'wptag'); + } + + public function get_categories() { + return [ + 'statistics' => __('Statistics', 'wptag'), + 'marketing' => __('Marketing', 'wptag'), + 'advertising' => __('Advertising', 'wptag'), + 'seo' => __('SEO', 'wptag'), + 'custom' => __('Custom', 'wptag') + ]; + } + + public function get_positions() { + return [ + 'head' => __('Site Header', 'wptag'), + 'footer' => __('Site Footer', 'wptag'), + 'before_content' => __('Before Post Content', 'wptag'), + 'after_content' => __('After Post Content', 'wptag') + ]; + } +} diff --git a/includes/class-wptag-template-manager.php b/includes/class-wptag-template-manager.php new file mode 100644 index 0000000..2936647 --- /dev/null +++ b/includes/class-wptag-template-manager.php @@ -0,0 +1,271 @@ +table_name = $wpdb->prefix . 'wptag_templates'; + } + + public function get_template($service_type) { + global $wpdb; + + $template = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$this->table_name} WHERE service_type = %s AND is_active = 1", + $service_type + ), ARRAY_A); + + if ($template && !empty($template['config_fields'])) { + $template['config_fields'] = json_decode($template['config_fields'], true); + } + + return $template; + } + + public function get_templates($category = null) { + global $wpdb; + + $query = "SELECT * FROM {$this->table_name} WHERE is_active = 1"; + $params = []; + + if ($category) { + $query .= " AND service_category = %s"; + $params[] = $category; + } + + $query .= " ORDER BY service_category, service_name"; + + if (!empty($params)) { + $templates = $wpdb->get_results($wpdb->prepare($query, $params), ARRAY_A); + } else { + $templates = $wpdb->get_results($query, ARRAY_A); + } + + foreach ($templates as &$template) { + if (!empty($template['config_fields'])) { + $template['config_fields'] = json_decode($template['config_fields'], true); + } + } + + return $templates; + } + + public function get_categories() { + return [ + 'analytics' => __('Analytics & Statistics', 'wptag'), + 'marketing' => __('Marketing & Tracking', 'wptag'), + 'seo' => __('SEO Tools', 'wptag'), + 'support' => __('Customer Support', 'wptag'), + 'other' => __('Other Services', 'wptag') + ]; + } + + public function process_template_config($service_type, $config_data) { + $template = $this->get_template($service_type); + + if (!$template) { + return new WP_Error('template_not_found', 'Service template not found'); + } + + $validated_config = $this->validate_config($template['config_fields'], $config_data); + + if (is_wp_error($validated_config)) { + return $validated_config; + } + + $code = $this->render_template($template['code_template'], $validated_config); + + return [ + 'code' => $code, + 'position' => $template['default_position'], + 'name' => $template['service_name'] . ' - ' . ($validated_config['measurement_id'] ?? $validated_config['pixel_id'] ?? 'Config'), + 'category' => $template['service_category'] + ]; + } + + private function validate_config($fields, $data) { + $validated = []; + + foreach ($fields as $field) { + $field_name = $field['name']; + $field_value = $data[$field_name] ?? ''; + + if (!empty($field['required']) && empty($field_value)) { + return new WP_Error('missing_field', sprintf('Field %s is required', $field['label'])); + } + + if (!empty($field_value)) { + switch ($field['type']) { + case 'text': + $validated[$field_name] = sanitize_text_field($field_value); + break; + + case 'textarea': + $validated[$field_name] = sanitize_textarea_field($field_value); + break; + + case 'url': + $validated[$field_name] = esc_url_raw($field_value); + break; + + case 'number': + $validated[$field_name] = intval($field_value); + break; + + case 'select': + if (isset($field['options'][$field_value])) { + $validated[$field_name] = $field_value; + } + break; + + default: + $validated[$field_name] = sanitize_text_field($field_value); + } + } + } + + return $validated; + } + + private function render_template($template, $variables) { + $code = $template; + + foreach ($variables as $key => $value) { + $placeholder = '{{' . $key . '}}'; + $code = str_replace($placeholder, $value, $code); + } + + $code = preg_replace('/\{\{[^}]+\}\}/', '', $code); + + return trim($code); + } + + public function create_template($data) { + global $wpdb; + + $template_data = [ + 'service_type' => sanitize_key($data['service_type']), + 'service_name' => sanitize_text_field($data['service_name']), + 'service_category' => sanitize_key($data['service_category']), + 'config_fields' => json_encode($data['config_fields']), + 'code_template' => $data['code_template'], + 'default_position' => sanitize_key($data['default_position']), + 'is_active' => 1, + 'version' => sanitize_text_field($data['version'] ?? '1.0') + ]; + + $result = $wpdb->insert($this->table_name, $template_data); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to create template'); + } + + return $wpdb->insert_id; + } + + public function update_template($service_type, $data) { + global $wpdb; + + $template_data = []; + + if (isset($data['service_name'])) { + $template_data['service_name'] = sanitize_text_field($data['service_name']); + } + + if (isset($data['service_category'])) { + $template_data['service_category'] = sanitize_key($data['service_category']); + } + + if (isset($data['config_fields'])) { + $template_data['config_fields'] = json_encode($data['config_fields']); + } + + if (isset($data['code_template'])) { + $template_data['code_template'] = $data['code_template']; + } + + if (isset($data['default_position'])) { + $template_data['default_position'] = sanitize_key($data['default_position']); + } + + if (isset($data['version'])) { + $template_data['version'] = sanitize_text_field($data['version']); + } + + $template_data['updated_at'] = current_time('mysql'); + + $result = $wpdb->update( + $this->table_name, + $template_data, + ['service_type' => $service_type] + ); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to update template'); + } + + return true; + } + + public function delete_template($service_type) { + global $wpdb; + + $result = $wpdb->delete($this->table_name, ['service_type' => $service_type]); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to delete template'); + } + + return true; + } + + public function export_templates($service_types = []) { + global $wpdb; + + if (empty($service_types)) { + $templates = $wpdb->get_results("SELECT * FROM {$this->table_name}", ARRAY_A); + } else { + $placeholders = array_fill(0, count($service_types), '%s'); + $query = $wpdb->prepare( + "SELECT * FROM {$this->table_name} WHERE service_type IN (" . implode(',', $placeholders) . ")", + $service_types + ); + $templates = $wpdb->get_results($query, ARRAY_A); + } + + return json_encode($templates, JSON_PRETTY_PRINT); + } + + public function import_templates($json_data) { + $templates = json_decode($json_data, true); + + if (!is_array($templates)) { + return new WP_Error('invalid_format', 'Invalid template format'); + } + + $imported = 0; + + foreach ($templates as $template) { + if (!isset($template['service_type']) || !isset($template['code_template'])) { + continue; + } + + $existing = $this->get_template($template['service_type']); + + if ($existing) { + $this->update_template($template['service_type'], $template); + } else { + $this->create_template($template); + } + + $imported++; + } + + return $imported; + } +} diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..ee4027f --- /dev/null +++ b/uninstall.php @@ -0,0 +1,41 @@ +prefix . 'wptag_snippets', + $wpdb->prefix . 'wptag_templates', + $wpdb->prefix . 'wptag_logs' +]; + +foreach ($tables as $table) { + $wpdb->query("DROP TABLE IF EXISTS {$table}"); +} + +$options = [ + 'wptag_db_version', + 'wptag_settings', + 'wptag_activated', + 'wptag_cache_cleared' +]; + +foreach ($options as $option) { + delete_option($option); +} + +delete_transient('wptag_admin_notice'); + +wp_clear_scheduled_hook('wptag_cleanup_logs'); +wp_clear_scheduled_hook('wptag_cache_cleanup'); + +wp_cache_flush(); diff --git a/wptag-readme.md b/wptag-readme.md new file mode 100644 index 0000000..9c75226 --- /dev/null +++ b/wptag-readme.md @@ -0,0 +1,155 @@ +# WPTAG - WordPress Code Tag Manager + +Professional WordPress plugin for managing tracking codes, analytics scripts, and third-party integrations with advanced conditional loading. + +## Features + +### Core Functionality +- **Code Snippet Management**: Add, edit, and organize code snippets with categories +- **Multiple Insert Positions**: Head, footer, before/after content +- **Smart Conditional Loading**: Control when and where snippets appear +- **Service Templates**: Quick setup for popular services like Google Analytics, Facebook Pixel +- **Performance Optimization**: Built-in caching and code optimization +- **Import/Export**: Backup and migrate your snippets easily + +### Conditional Loading Options +- Page types (home, posts, pages, archives, etc.) +- User status (logged in/out, user roles) +- Device types (desktop, mobile, tablet) +- Specific posts/pages +- Categories and tags +- URL patterns +- Date/time ranges +- Custom conditions via filters + +### Supported Services Templates +- Google Analytics 4 +- Facebook Pixel +- Google Ads Conversion +- Google Search Console +- Baidu Analytics +- And more... + +## Installation + +1. Upload the `wptag` folder to `/wp-content/plugins/` +2. Activate the plugin through the 'Plugins' menu in WordPress +3. Navigate to WPTAG in your WordPress admin + +## Usage + +### Creating a Snippet +1. Go to WPTAG > Code Snippets +2. Click "Add New" +3. Enter snippet details: + - Name and description + - Code content + - Position (head/footer/content) + - Category and priority + - Conditions (optional) +4. Save and activate + +### Using Templates +1. Go to WPTAG > Service Templates +2. Select a service (e.g., Google Analytics) +3. Enter your configuration (e.g., Tracking ID) +4. Click "Create Snippet" + +### Setting Conditions +- Add multiple conditions to control snippet visibility +- Combine conditions with AND/OR logic +- Test conditions in preview mode + +## Requirements + +- WordPress 6.8+ +- PHP 8.0+ +- MySQL 5.7+ or MariaDB 10.3+ + +## Database Tables + +The plugin creates three tables: +- `wp_wptag_snippets` - Stores code snippets +- `wp_wptag_templates` - Service templates +- `wp_wptag_logs` - Activity logs + +## Hooks and Filters + +### Actions +- `wptag_before_render_snippet` - Before snippet output +- `wptag_after_render_snippet` - After snippet output + +### Filters +- `wptag_snippet_output` - Modify snippet output +- `wptag_custom_condition` - Add custom conditions +- `wptag_cache_ttl` - Modify cache duration + +## Performance + +- Intelligent caching reduces database queries +- Conditional pre-processing for faster page loads +- Code minification option +- Compatible with popular caching plugins + +## Security + +- Input validation and sanitization +- XSS protection +- SQL injection prevention +- User capability checks +- Nonce verification for all actions + +## Troubleshooting + +### Snippets not appearing +1. Check if snippet is active +2. Verify conditions are met +3. Clear cache (WPTAG Settings > Clear Cache) +4. Enable debug mode for detailed output + +### Performance issues +1. Reduce number of active snippets +2. Enable caching +3. Optimize conditions +4. Use priority settings wisely + +## Developer Documentation + +### Adding Custom Conditions + +```php +add_filter('wptag_custom_condition', function($result, $type, $value, $operator, $context) { + if ($type === 'my_custom_condition') { + // Your condition logic here + return $result; + } + return $result; +}, 10, 5); +``` + +### Modifying Snippet Output + +```php +add_filter('wptag_snippet_output', function($code, $snippet) { + // Modify code before output + return $code; +}, 10, 2); +``` + +## Support + +For support and documentation, visit [wptag.com](https://wptag.com) + +## License + +GPL v2 or later + +## Changelog + +### 1.0.0 +- Initial release +- Core snippet management +- Conditional loading engine +- Service templates +- Caching system +- Import/export functionality diff --git a/wptag.php b/wptag.php new file mode 100644 index 0000000..5e26197 --- /dev/null +++ b/wptag.php @@ -0,0 +1,33 @@ +\n" +"Language-Team: LANGUAGE \n" + +#: admin/partials/dashboard.php:15 +msgid "WPTAG Dashboard" +msgstr "" + +#: admin/partials/dashboard.php:22 +msgid "Total Snippets" +msgstr "" + +#: admin/partials/dashboard.php:27 +msgid "Active Snippets" +msgstr "" + +#: admin/partials/dashboard.php:32 +msgid "Inactive Snippets" +msgstr "" + +#: admin/partials/dashboard.php:37 +msgid "Success Rate" +msgstr "" + +#: admin/partials/snippets.php:15 +msgid "Code Snippets" +msgstr "" + +#: admin/partials/snippets.php:17 +msgid "Add New" +msgstr "" + +#: admin/partials/snippets.php:25 +msgid "Search" +msgstr "" + +#: admin/partials/snippets.php:26 +msgid "Search snippets..." +msgstr "" + +#: admin/partials/snippets.php:31 +msgid "Category" +msgstr "" + +#: admin/partials/snippets.php:33 +msgid "All Categories" +msgstr "" + +#: admin/partials/snippets.php:43 +msgid "Position" +msgstr "" + +#: admin/partials/snippets.php:45 +msgid "All Positions" +msgstr "" + +#: admin/partials/snippets.php:55 +msgid "Status" +msgstr "" + +#: admin/partials/snippets.php:57 +msgid "All Status" +msgstr "" + +#: admin/partials/snippets.php:58 +msgid "Active" +msgstr "" + +#: admin/partials/snippets.php:59 +msgid "Inactive" +msgstr "" + +#: admin/partials/snippets.php:71 +msgid "No snippets found" +msgstr "" + +#: admin/partials/snippets.php:72 +msgid "Get started by creating your first code snippet." +msgstr "" + +#: admin/partials/snippets.php:74 +msgid "Create Snippet" +msgstr "" + +#: admin/partials/snippets.php:81 +msgid "Name" +msgstr "" + +#: admin/partials/snippets.php:82 +msgid "Priority" +msgstr "" + +#: admin/partials/snippets.php:83 +msgid "Actions" +msgstr "" + +#: admin/partials/snippets.php:115 +msgid "Edit" +msgstr "" + +#: admin/partials/snippets.php:119 +msgid "Enable" +msgstr "" + +#: admin/partials/snippets.php:119 +msgid "Disable" +msgstr "" + +#: admin/partials/snippets.php:122 +msgid "Delete" +msgstr "" + +#: admin/partials/templates.php:11 +msgid "Service Templates" +msgstr "" + +#: admin/partials/templates.php:17 +msgid "Service templates help you quickly add popular services to your site. Simply select a template, enter your configuration details, and a snippet will be created automatically." +msgstr "" + +#: admin/partials/templates.php:22 +msgid "All Templates" +msgstr "" + +#: admin/partials/templates.php:48 +msgid "No templates found" +msgstr "" + +#: admin/partials/templates.php:49 +msgid "No templates available in this category." +msgstr "" + +#: admin/partials/templates.php:68 +msgid "Use This Template" +msgstr "" + +#: admin/partials/settings.php:11 +msgid "WPTAG Settings" +msgstr "" + +#: admin/partials/settings.php:20 +msgid "Cache Settings" +msgstr "" + +#: admin/partials/settings.php:23 +msgid "Enable Cache" +msgstr "" + +#: admin/partials/settings.php:27 +msgid "Enable caching for better performance" +msgstr "" + +#: admin/partials/settings.php:30 +msgid "Caching stores processed snippets and conditions to improve page load times." +msgstr "" + +#: admin/partials/settings.php:37 +msgid "Cache Duration" +msgstr "" + +#: admin/partials/settings.php:42 +msgid "seconds" +msgstr "" + +#: admin/partials/settings.php:44 +msgid "How long to keep cached data. Default is 3600 seconds (1 hour)." +msgstr "" + +#: admin/partials/settings.php:50 +msgid "Cache Status" +msgstr "" + +#: admin/partials/settings.php:54 +msgid "Cache Enabled:" +msgstr "" + +#: admin/partials/settings.php:55 +msgid "Yes" +msgstr "" + +#: admin/partials/settings.php:55 +msgid "No" +msgstr "" + +#: admin/partials/settings.php:64 +msgid "Clear Cache Now" +msgstr "" + +#: admin/partials/settings.php:71 +msgid "Debug Settings" +msgstr "" + +#: admin/partials/settings.php:74 +msgid "Debug Mode" +msgstr "" + +#: admin/partials/settings.php:78 +msgid "Enable debug mode" +msgstr "" + +#: admin/partials/settings.php:81 +msgid "Show additional information in HTML comments for troubleshooting." +msgstr "" + +#: admin/partials/settings.php:87 +msgid "Uninstall Settings" +msgstr "" + +#: admin/partials/settings.php:90 +msgid "Data Cleanup" +msgstr "" + +#: admin/partials/settings.php:94 +msgid "Remove all data when uninstalling the plugin" +msgstr "" + +#: admin/partials/settings.php:97 +msgid "Warning: This will permanently delete all snippets, templates, and settings when the plugin is uninstalled." +msgstr "" + +#: admin/partials/settings.php:103 +msgid "Import/Export" +msgstr "" + +#: admin/partials/settings.php:106 +msgid "Export Snippets" +msgstr "" + +#: admin/partials/settings.php:108 +msgid "Export all your snippets to a JSON file for backup or migration." +msgstr "" + +#: admin/partials/settings.php:110 +msgid "Export All Snippets" +msgstr "" + +#: admin/partials/settings.php:116 +msgid "Import Snippets" +msgstr "" + +#: admin/partials/settings.php:118 +msgid "Import snippets from a JSON file." +msgstr "" + +#: admin/partials/settings.php:121 +msgid "Import Snippets" +msgstr "" + +#: admin/partials/settings.php:129 +msgid "Save Settings" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:355 +msgid "Statistics" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:356 +msgid "Marketing" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:357 +msgid "Advertising" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:358 +msgid "SEO" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:359 +msgid "Custom" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:365 +msgid "Site Header" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:366 +msgid "Site Footer" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:367 +msgid "Before Post Content" +msgstr "" + +#: includes/class-wptag-snippet-manager.php:368 +msgid "After Post Content" +msgstr ""