%2$s
' . 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( - '%s
' + message + '
Manage your tracking codes and analytics services with ease.
+You do not have permission to manage services.
'; + echo 'Enable or disable tracking services. Only enabled services will appear in the category tabs.
+No services enabled for this category. Enable some services to get started.
'; + echo '%s
', + esc_attr($notice['type']), + esc_html($notice['message']) + ); + } + + delete_transient('wptag_admin_notices'); + } + + private function add_admin_notice($type, $message) { + $notices = get_transient('wptag_admin_notices') ?: array(); + $notices[] = array('type' => $type, 'message' => $message); + set_transient('wptag_admin_notices', $notices, 30); + } + + public function handle_form_submission() { + if (isset($_POST['wptag_services_nonce']) && wp_verify_nonce($_POST['wptag_services_nonce'], 'wptag_save_services')) { + $this->handle_services_form_submission(); + return; + } + + if (!isset($_POST['wptag_nonce']) || !wp_verify_nonce($_POST['wptag_nonce'], 'wptag_save_settings')) { + return; + } + + if (!$this->current_user_can_manage_codes()) { + $this->add_admin_notice('error', 'You do not have permission to manage tracking codes.'); + return; + } + + $settings = $_POST['wptag_settings'] ?? array(); + + if ($this->validator->validate_settings($settings)) { + $result = $this->config->update_settings($settings); + + if ($result) { + $this->add_admin_notice('success', 'Settings saved successfully.'); + } else { + $this->add_admin_notice('error', 'Failed to save settings.'); + } + } else { + $errors = $this->validator->get_error_messages(); + $this->add_admin_notice('error', 'Validation failed: ' . implode(', ', $errors)); + } + + $redirect_url = add_query_arg( + array( + 'page' => 'wptag-settings', + 'tab' => isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'analytics' + ), + admin_url('options-general.php') + ); + + wp_redirect($redirect_url); + exit; + } + + private function handle_services_form_submission() { + if (!$this->current_user_can_manage_services()) { + $this->add_admin_notice('error', 'You do not have permission to manage services.'); + return; + } + + $enabled_services = isset($_POST['enabled_services']) ? array_map('sanitize_text_field', $_POST['enabled_services']) : array(); + + $result = $this->config->update_enabled_services($enabled_services); + + if ($result) { + $this->add_admin_notice('success', 'Services updated successfully.'); + } else { + $this->add_admin_notice('error', 'Failed to update services.'); + } + + wp_redirect(add_query_arg(array('page' => 'wptag-settings', 'tab' => 'services'), admin_url('options-general.php'))); + exit; + } + + public function sanitize_settings($settings) { + return $this->config->sanitize_settings($settings); + } + + public function sanitize_services($services) { + return is_array($services) ? array_map('sanitize_text_field', $services) : array(); + } + + public function ajax_validate_code() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to validate codes.')); + return; + } + + $service_key = sanitize_text_field($_POST['service']); + $use_template = $_POST['use_template'] === '1'; + + $settings = array( + 'enabled' => true, + 'use_template' => $use_template + ); + + if ($use_template) { + $service_config = $this->config->get_service_config($service_key); + if ($service_config) { + $field_key = $service_config['field']; + $settings[$field_key] = sanitize_text_field($_POST['id_value']); + } + } else { + $settings['custom_code'] = wp_kses_post($_POST['custom_code']); + } + + $is_valid = $this->validator->validate_service_code($service_key, $settings); + + if ($is_valid) { + wp_send_json_success(array('message' => 'Code is valid')); + } else { + $errors = $this->validator->get_error_messages(); + wp_send_json_error(array('message' => implode(', ', $errors))); + } + } + + public function ajax_preview_code() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to preview codes.')); + return; + } + + $service_key = sanitize_text_field($_POST['service']); + $id_value = sanitize_text_field($_POST['id_value']); + + $preview = $this->output_manager->get_template_preview($service_key, $id_value); + + if (!empty($preview)) { + wp_send_json_success(array('preview' => $preview)); + } else { + wp_send_json_error(array('message' => 'Unable to generate preview')); + } + } + + public function ajax_export_settings() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to export settings.')); + return; + } + + $export_data = $this->config->export_settings(); + + wp_send_json_success(array( + 'data' => $export_data, + 'filename' => 'wptag-settings-' . date('Y-m-d-H-i-s') . '.json' + )); + } + + public function ajax_import_settings() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to import settings.')); + return; + } + + $import_data = stripslashes($_POST['import_data']); + + if ($this->validator->validate_import_data($import_data)) { + $result = $this->config->import_settings($import_data); + + if (!is_wp_error($result)) { + wp_send_json_success(array('message' => 'Settings imported successfully')); + } else { + wp_send_json_error(array('message' => $result->get_error_message())); + } + } else { + $errors = $this->validator->get_error_messages(); + wp_send_json_error(array('message' => implode(', ', $errors))); + } + } + + public function ajax_reset_settings() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to reset settings.')); + return; + } + + $this->config->reset_to_defaults(); + + wp_send_json_success(array('message' => 'Settings reset successfully')); + } + + public function add_action_links($links) { + $settings_link = 'Settings'; + array_unshift($links, $settings_link); + return $links; + } + + public function add_row_meta($links, $file) { + if ($file === plugin_basename(WPTAG_PLUGIN_FILE)) { + $links[] = 'Documentation'; + $links[] = 'Support'; + } + return $links; + } +} \ No newline at end of file diff --git a/includes/class-config.php b/includes/class-config.php new file mode 100644 index 0000000..e1297aa --- /dev/null +++ b/includes/class-config.php @@ -0,0 +1,447 @@ +init_services_config(); + } + + private function init_services_config() { + $this->services_config = array( + 'google_analytics' => array( + 'name' => 'Google Analytics', + 'category' => 'analytics', + 'field' => 'tracking_id', + 'placeholder' => 'G-XXXXXXXXXX or UA-XXXXXXXXX-X', + 'validation_pattern' => '/^(G-[A-Z0-9]{10}|UA-[0-9]+-[0-9]+)$/', + 'default_position' => 'head', + 'template' => 'google_analytics', + 'icon' => 'dashicons-chart-area', + 'description' => 'Track website traffic and user behavior with Google Analytics' + ), + 'google_tag_manager' => array( + 'name' => 'Google Tag Manager', + 'category' => 'analytics', + 'field' => 'container_id', + 'placeholder' => 'GTM-XXXXXXX', + 'validation_pattern' => '/^GTM-[A-Z0-9]{7}$/', + 'default_position' => 'head', + 'template' => 'google_tag_manager', + 'icon' => 'dashicons-tag', + 'description' => 'Manage all your website tags through Google Tag Manager' + ), + 'facebook_pixel' => array( + 'name' => 'Facebook Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => '123456789012345', + 'validation_pattern' => '/^[0-9]{15}$/', + 'default_position' => 'head', + 'template' => 'facebook_pixel', + 'icon' => 'dashicons-facebook', + 'description' => 'Track conversions and build audiences for Facebook ads' + ), + 'google_ads' => array( + 'name' => 'Google Ads', + 'category' => 'advertising', + 'field' => 'conversion_id', + 'placeholder' => 'AW-123456789', + 'validation_pattern' => '/^AW-[0-9]{10}$/', + 'default_position' => 'head', + 'template' => 'google_ads', + 'icon' => 'dashicons-googleplus', + 'description' => 'Track conversions for Google Ads campaigns' + ), + 'microsoft_clarity' => array( + 'name' => 'Microsoft Clarity', + 'category' => 'analytics', + 'field' => 'project_id', + 'placeholder' => 'abcdefghij', + 'validation_pattern' => '/^[a-z0-9]{10}$/', + 'default_position' => 'head', + 'template' => 'microsoft_clarity', + 'icon' => 'dashicons-visibility', + 'description' => 'Free user behavior analytics with heatmaps and session recordings' + ), + 'hotjar' => array( + 'name' => 'Hotjar', + 'category' => 'analytics', + 'field' => 'site_id', + 'placeholder' => '1234567', + 'validation_pattern' => '/^[0-9]{7}$/', + 'default_position' => 'head', + 'template' => 'hotjar', + 'icon' => 'dashicons-video-alt3', + 'description' => 'Understand user behavior with Hotjar heatmaps and recordings' + ), + 'tiktok_pixel' => array( + 'name' => 'TikTok Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', + 'validation_pattern' => '/^[A-Z0-9]{26}$/', + 'default_position' => 'head', + 'template' => 'tiktok_pixel', + 'icon' => 'dashicons-smartphone', + 'description' => 'Track conversions for TikTok advertising campaigns' + ), + 'linkedin_insight' => array( + 'name' => 'LinkedIn Insight Tag', + 'category' => 'advertising', + 'field' => 'partner_id', + 'placeholder' => '1234567', + 'validation_pattern' => '/^[0-9]{7}$/', + 'default_position' => 'footer', + 'template' => 'linkedin_insight', + 'icon' => 'dashicons-linkedin', + 'description' => 'Track conversions and retarget visitors for LinkedIn ads' + ), + 'twitter_pixel' => array( + 'name' => 'Twitter Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => 'o1234', + 'validation_pattern' => '/^o[0-9]{4}$/', + 'default_position' => 'head', + 'template' => 'twitter_pixel', + 'icon' => 'dashicons-twitter', + 'description' => 'Track conversions for Twitter advertising campaigns' + ), + 'pinterest_pixel' => array( + 'name' => 'Pinterest Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => '1234567890123456', + 'validation_pattern' => '/^[0-9]{16}$/', + 'default_position' => 'head', + 'template' => 'pinterest_pixel', + 'icon' => 'dashicons-format-image', + 'description' => 'Track conversions for Pinterest advertising campaigns' + ), + 'snapchat_pixel' => array( + 'name' => 'Snapchat Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => 'abcdefgh-1234-5678-9012-abcdefghijkl', + 'validation_pattern' => '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/', + 'default_position' => 'head', + 'template' => 'snapchat_pixel', + 'icon' => 'dashicons-camera', + 'description' => 'Track conversions for Snapchat advertising campaigns' + ), + 'google_optimize' => array( + 'name' => 'Google Optimize', + 'category' => 'analytics', + 'field' => 'container_id', + 'placeholder' => 'GTM-XXXXXXX', + 'validation_pattern' => '/^GTM-[A-Z0-9]{7}$/', + 'default_position' => 'head', + 'template' => 'google_optimize', + 'icon' => 'dashicons-admin-settings', + 'description' => 'A/B testing and website optimization tool' + ), + 'crazyegg' => array( + 'name' => 'Crazy Egg', + 'category' => 'analytics', + 'field' => 'account_id', + 'placeholder' => '12345678', + 'validation_pattern' => '/^[0-9]{8}$/', + 'default_position' => 'head', + 'template' => 'crazyegg', + 'icon' => 'dashicons-admin-tools', + 'description' => 'Heatmap and user session recording tool' + ), + 'mixpanel' => array( + 'name' => 'Mixpanel', + 'category' => 'analytics', + 'field' => 'project_token', + 'placeholder' => 'abcdefghijklmnopqrstuvwxyz123456', + 'validation_pattern' => '/^[a-z0-9]{32}$/', + 'default_position' => 'head', + 'template' => 'mixpanel', + 'icon' => 'dashicons-chart-pie', + 'description' => 'Advanced analytics to understand user behavior' + ), + 'amplitude' => array( + 'name' => 'Amplitude', + 'category' => 'analytics', + 'field' => 'api_key', + 'placeholder' => 'abcdefghijklmnopqrstuvwxyz123456', + 'validation_pattern' => '/^[a-z0-9]{32}$/', + 'default_position' => 'head', + 'template' => 'amplitude', + 'icon' => 'dashicons-chart-bar', + 'description' => 'Product analytics for mobile and web' + ), + 'matomo' => array( + 'name' => 'Matomo', + 'category' => 'analytics', + 'field' => 'site_id', + 'placeholder' => '1', + 'validation_pattern' => '/^[0-9]+$/', + 'default_position' => 'head', + 'template' => 'matomo', + 'icon' => 'dashicons-chart-line', + 'description' => 'Privacy-focused web analytics platform' + ) + ); + } + + public function get_all_services() { + return $this->services_config; + } + + public function get_enabled_services() { + if (null === $this->cached_services) { + $this->cached_services = get_option($this->services_option, array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads')); + } + return $this->cached_services; + } + + public function update_enabled_services($services) { + $this->cached_services = is_array($services) ? $services : array(); + $result = update_option($this->services_option, $this->cached_services); + + if ($result) { + $this->cached_settings = null; + } + + return $result; + } + + public function get_available_services() { + $enabled_services = $this->get_enabled_services(); + $available = array(); + + foreach ($enabled_services as $service_key) { + if (isset($this->services_config[$service_key])) { + $available[$service_key] = $this->services_config[$service_key]; + } + } + + return $available; + } + + public function get_services_config() { + return $this->get_available_services(); + } + + public function get_service_config($service_key) { + return isset($this->services_config[$service_key]) ? $this->services_config[$service_key] : null; + } + + public function get_services_by_category($category) { + $available_services = $this->get_available_services(); + return array_filter($available_services, function($service) use ($category) { + return $service['category'] === $category; + }); + } + + public function get_categories() { + $available_services = $this->get_available_services(); + $categories = array(); + + foreach ($available_services as $service) { + if (!in_array($service['category'], $categories)) { + $categories[] = $service['category']; + } + } + + return $categories; + } + + public function get_settings() { + if (null === $this->cached_settings) { + $this->cached_settings = get_option($this->option_name, $this->get_default_settings()); + } + return $this->cached_settings; + } + + public function get_service_settings($service_key) { + $settings = $this->get_settings(); + return isset($settings[$service_key]) ? $settings[$service_key] : $this->get_default_service_settings($service_key); + } + + public function update_settings($new_settings) { + $sanitized_settings = $this->sanitize_settings($new_settings); + $result = update_option($this->option_name, $sanitized_settings); + + if ($result) { + $this->cached_settings = $sanitized_settings; + do_action('wptag_settings_updated', $sanitized_settings); + } + + return $result; + } + + public function update_service_settings($service_key, $service_settings) { + $all_settings = $this->get_settings(); + $all_settings[$service_key] = $this->sanitize_service_settings($service_settings); + + return $this->update_settings($all_settings); + } + + private function get_default_settings() { + $defaults = array(); + $available_services = $this->get_available_services(); + + foreach ($available_services as $service_key => $service_config) { + $defaults[$service_key] = $this->get_default_service_settings($service_key); + } + + return $defaults; + } + + private function get_default_service_settings($service_key) { + $service_config = $this->get_service_config($service_key); + if (!$service_config) { + return array(); + } + + $defaults = array( + 'enabled' => false, + 'use_template' => true, + 'custom_code' => '', + 'position' => $service_config['default_position'], + 'priority' => 10, + 'device' => 'all', + 'conditions' => array(), + 'created_at' => current_time('mysql'), + 'updated_at' => current_time('mysql') + ); + + $defaults[$service_config['field']] = ''; + + return $defaults; + } + + public function sanitize_settings($settings) { + $sanitized = array(); + + if (!is_array($settings)) { + return $this->get_default_settings(); + } + + foreach ($settings as $service_key => $service_settings) { + if (isset($this->services_config[$service_key])) { + $sanitized[$service_key] = $this->sanitize_service_settings($service_settings); + } + } + + return $sanitized; + } + + private function sanitize_service_settings($settings) { + $sanitized = array( + 'enabled' => !empty($settings['enabled']), + 'use_template' => isset($settings['use_template']) ? (bool)$settings['use_template'] : true, + 'custom_code' => wp_kses($settings['custom_code'] ?? '', array( + 'script' => array( + 'type' => array(), + 'src' => array(), + 'async' => array(), + 'defer' => array(), + 'id' => array(), + 'class' => array() + ), + 'noscript' => array(), + 'img' => array( + 'src' => array(), + 'alt' => array(), + 'width' => array(), + 'height' => array(), + 'style' => array() + ), + 'iframe' => array( + 'src' => array(), + 'width' => array(), + 'height' => array(), + 'style' => array() + ) + )), + 'position' => sanitize_text_field($settings['position'] ?? 'head'), + 'priority' => intval($settings['priority'] ?? 10), + 'device' => sanitize_text_field($settings['device'] ?? 'all'), + 'conditions' => is_array($settings['conditions'] ?? array()) ? $settings['conditions'] : array(), + 'updated_at' => current_time('mysql') + ); + + foreach ($this->services_config as $service_key => $service_config) { + $field_key = $service_config['field']; + if (isset($settings[$field_key])) { + $sanitized[$field_key] = sanitize_text_field($settings[$field_key]); + } + } + + return $sanitized; + } + + public function install_default_settings() { + $existing_settings = get_option($this->option_name, array()); + $existing_services = get_option($this->services_option, array()); + + if (empty($existing_settings)) { + add_option($this->option_name, $this->get_default_settings()); + } + + if (empty($existing_services)) { + add_option($this->services_option, array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads')); + } + } + + public function reset_to_defaults() { + delete_option($this->option_name); + delete_option($this->services_option); + $this->cached_settings = null; + $this->cached_services = null; + $this->install_default_settings(); + + do_action('wptag_settings_reset'); + } + + public function export_settings() { + $settings = $this->get_settings(); + $services = $this->get_enabled_services(); + + $export_data = array( + 'version' => WPTAG_VERSION, + 'exported_at' => current_time('mysql'), + 'services' => $services, + 'settings' => $settings + ); + + return wp_json_encode($export_data, JSON_PRETTY_PRINT); + } + + public function import_settings($json_data) { + $data = json_decode($json_data, true); + + if (!is_array($data) || !isset($data['settings'])) { + return new \WP_Error('invalid_data', 'Invalid import data format'); + } + + $settings_result = $this->update_settings($data['settings']); + + if (isset($data['services'])) { + $services_result = $this->update_enabled_services($data['services']); + } + + if ($settings_result) { + do_action('wptag_settings_imported', $data['settings']); + return true; + } + + return new \WP_Error('import_failed', 'Failed to import settings'); + } +} \ No newline at end of file diff --git a/includes/class-frontend.php b/includes/class-frontend.php new file mode 100644 index 0000000..bb33e28 --- /dev/null +++ b/includes/class-frontend.php @@ -0,0 +1,69 @@ +config = $config; + $this->output_manager = new Output_Manager($config); + + $this->init_hooks(); + } + + private function init_hooks() { + add_action('wp_head', array($this, 'output_head_codes'), 1); + add_action('wp_body_open', array($this, 'output_body_codes'), 1); + add_action('wp_footer', array($this, 'output_footer_codes'), 1); + add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); + } + + public function output_head_codes() { + if (!$this->should_output_codes()) { + return; + } + + $this->output_manager->output_codes('head'); + } + + public function output_body_codes() { + if (!$this->should_output_codes()) { + return; + } + + $this->output_manager->output_codes('body'); + } + + public function output_footer_codes() { + if (!$this->should_output_codes()) { + return; + } + + $this->output_manager->output_codes('footer'); + } + + private function should_output_codes() { + if (is_admin()) { + return false; + } + + if (is_user_logged_in() && current_user_can('manage_options')) { + $show_for_admin = apply_filters('wptag_show_for_admin', false); + if (!$show_for_admin) { + return false; + } + } + + return apply_filters('wptag_should_output_codes', true); + } + + public function enqueue_frontend_assets() { + do_action('wptag_enqueue_frontend_assets'); + } +} \ No newline at end of file diff --git a/includes/class-loader.php b/includes/class-loader.php new file mode 100644 index 0000000..5ad0620 --- /dev/null +++ b/includes/class-loader.php @@ -0,0 +1,51 @@ +actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args); + } + + public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1) { + $this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args); + } + + public function add_shortcode($tag, $component, $callback) { + $this->shortcodes = $this->add($this->shortcodes, $tag, $component, $callback); + } + + private function add($hooks, $hook, $component, $callback, $priority = 10, $accepted_args = 1) { + $hooks[] = array( + 'hook' => $hook, + 'component' => $component, + 'callback' => $callback, + 'priority' => $priority, + 'accepted_args' => $accepted_args + ); + + return $hooks; + } + + public function run() { + foreach ($this->filters as $hook) { + add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']); + } + + foreach ($this->actions as $hook) { + add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']); + } + + foreach ($this->shortcodes as $hook) { + add_shortcode($hook['hook'], array($hook['component'], $hook['callback'])); + } + } +} \ No newline at end of file diff --git a/includes/class-output-manager.php b/includes/class-output-manager.php new file mode 100644 index 0000000..8ef15bc --- /dev/null +++ b/includes/class-output-manager.php @@ -0,0 +1,479 @@ +config = $config; + $this->init_templates(); + } + + private function init_templates() { + $this->templates = array( + 'google_analytics' => array( + 'G-' => ' + +', + 'UA-' => ' + +' + ), + 'google_tag_manager' => array( + 'head' => ' +', + 'body' => ' +' + ), + 'facebook_pixel' => ' + +', + 'google_ads' => ' + +', + 'microsoft_clarity' => ' +', + 'hotjar' => ' +', + 'tiktok_pixel' => ' +', + 'linkedin_insight' => ' + +', + 'twitter_pixel' => ' +', + 'pinterest_pixel' => ' + +', + 'snapchat_pixel' => ' +', + 'google_optimize' => ' +', + 'crazyegg' => ' +', + 'mixpanel' => ' +', + 'amplitude' => ' +', + 'matomo' => ' +', + ); + } + + public function get_codes_for_position($position) { + if (isset($this->cached_codes[$position])) { + return $this->cached_codes[$position]; + } + + $codes = array(); + $all_settings = $this->config->get_settings(); + + foreach ($all_settings as $service_key => $service_settings) { + if (!$this->should_output_service($service_key, $service_settings, $position)) { + continue; + } + + $code = $this->get_service_code($service_key, $service_settings, $position); + if (!empty($code)) { + $priority = intval($service_settings['priority']); + $codes[$priority][] = array( + 'service' => $service_key, + 'code' => $code, + 'priority' => $priority + ); + } + } + + ksort($codes); + + $output_codes = array(); + foreach ($codes as $priority_codes) { + foreach ($priority_codes as $code_data) { + $output_codes[] = $code_data['code']; + } + } + + $this->cached_codes[$position] = $output_codes; + return $output_codes; + } + + private function should_output_service($service_key, $service_settings, $position) { + if (!$service_settings['enabled']) { + return false; + } + + if ($service_settings['position'] !== $position) { + return false; + } + + if (!$this->check_device_condition($service_settings['device'])) { + return false; + } + + if (!$this->check_page_conditions($service_settings['conditions'] ?? array())) { + return false; + } + + return apply_filters('wptag_should_output_service', true, $service_key, $service_settings, $position); + } + + private function check_device_condition($device_setting) { + if ($device_setting === 'all') { + return true; + } + + $is_mobile = wp_is_mobile(); + + if ($device_setting === 'mobile' && $is_mobile) { + return true; + } + + if ($device_setting === 'desktop' && !$is_mobile) { + return true; + } + + return false; + } + + private function check_page_conditions($conditions) { + if (empty($conditions)) { + return true; + } + + foreach ($conditions as $condition) { + if (!$this->check_single_condition($condition)) { + return false; + } + } + + return true; + } + + private function check_single_condition($condition) { + $type = $condition['type'] ?? ''; + $value = $condition['value'] ?? ''; + $operator = $condition['operator'] ?? 'is'; + + switch ($type) { + case 'page_type': + return $this->check_page_type_condition($value, $operator); + case 'post_type': + return $this->check_post_type_condition($value, $operator); + case 'category': + return $this->check_category_condition($value, $operator); + case 'tag': + return $this->check_tag_condition($value, $operator); + case 'user_role': + return $this->check_user_role_condition($value, $operator); + default: + return true; + } + } + + private function check_page_type_condition($value, $operator) { + $current_page_type = $this->get_current_page_type(); + + if ($operator === 'is') { + return $current_page_type === $value; + } elseif ($operator === 'is_not') { + return $current_page_type !== $value; + } + + return true; + } + + private function get_current_page_type() { + if (is_home()) return 'home'; + if (is_front_page()) return 'front_page'; + 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 'unknown'; + } + + private function check_post_type_condition($value, $operator) { + $post_type = get_post_type(); + + if ($operator === 'is') { + return $post_type === $value; + } elseif ($operator === 'is_not') { + return $post_type !== $value; + } + + return true; + } + + private function check_category_condition($value, $operator) { + if (is_category($value)) { + return $operator === 'is'; + } elseif (is_single()) { + $has_category = has_category($value); + return $operator === 'is' ? $has_category : !$has_category; + } + + return $operator === 'is_not'; + } + + private function check_tag_condition($value, $operator) { + if (is_tag($value)) { + return $operator === 'is'; + } elseif (is_single()) { + $has_tag = has_tag($value); + return $operator === 'is' ? $has_tag : !$has_tag; + } + + return $operator === 'is_not'; + } + + private function check_user_role_condition($value, $operator) { + $user = wp_get_current_user(); + $has_role = in_array($value, $user->roles); + + return $operator === 'is' ? $has_role : !$has_role; + } + + private function get_service_code($service_key, $service_settings, $position) { + if ($service_settings['use_template']) { + return $this->get_template_code($service_key, $service_settings, $position); + } else { + return $service_settings['custom_code']; + } + } + + private function get_template_code($service_key, $service_settings, $position) { + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + return ''; + } + + $template = $this->get_template_for_service($service_key, $service_settings, $position); + if (empty($template)) { + return ''; + } + + $field_key = $service_config['field']; + $id_value = $service_settings[$field_key] ?? ''; + + if (empty($id_value)) { + return ''; + } + + $code = str_replace('{ID}', esc_attr($id_value), $template); + + return apply_filters('wptag_template_code', $code, $service_key, $service_settings, $position); + } + + private function get_template_for_service($service_key, $service_settings, $position) { + if (!isset($this->templates[$service_key])) { + return ''; + } + + $template_data = $this->templates[$service_key]; + + if ($service_key === 'google_analytics') { + $service_config = $this->config->get_service_config($service_key); + $field_key = $service_config['field']; + $id_value = $service_settings[$field_key] ?? ''; + + if (strpos($id_value, 'G-') === 0) { + return $template_data['G-']; + } elseif (strpos($id_value, 'UA-') === 0) { + return $template_data['UA-']; + } + + return ''; + } + + if ($service_key === 'google_tag_manager') { + if ($position === 'head') { + return $template_data['head']; + } elseif ($position === 'body') { + return $template_data['body']; + } + + return ''; + } + + if (is_array($template_data)) { + return $template_data['default'] ?? ''; + } + + return $template_data; + } + + public function output_codes($position) { + $codes = $this->get_codes_for_position($position); + + if (empty($codes)) { + return; + } + + $output = "\n\n"; + + foreach ($codes as $code) { + $output .= $code . "\n"; + } + + $output .= "\n"; + + echo apply_filters('wptag_output_codes', $output, $position); + } + + public function clear_cache() { + $this->cached_codes = array(); + } + + public function get_template_preview($service_key, $id_value) { + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + return ''; + } + + $template = $this->get_template_for_service($service_key, array($service_config['field'] => $id_value), 'head'); + if (empty($template)) { + return ''; + } + + return str_replace('{ID}', esc_attr($id_value), $template); + } +} \ No newline at end of file diff --git a/includes/class-validator.php b/includes/class-validator.php new file mode 100644 index 0000000..ac9da90 --- /dev/null +++ b/includes/class-validator.php @@ -0,0 +1,393 @@ +config = $config ?: new Config(); + } + + public function validate_service_code($service_key, $settings) { + $this->errors = array(); + + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + $this->add_error('service_not_found', 'Service configuration not found'); + return false; + } + + if (!$settings['enabled']) { + return true; + } + + if ($settings['use_template']) { + return $this->validate_template_code($service_key, $settings, $service_config); + } else { + return $this->validate_custom_code($settings['custom_code']); + } + } + + private function validate_template_code($service_key, $settings, $service_config) { + $field_key = $service_config['field']; + $id_value = $settings[$field_key] ?? ''; + + if (empty($id_value)) { + $this->add_error('empty_id', 'ID field cannot be empty'); + return false; + } + + if (!$this->validate_id_format($service_key, $id_value, $service_config)) { + return false; + } + + return true; + } + + private function validate_id_format($service_key, $id_value, $service_config) { + $pattern = $service_config['validation_pattern'] ?? null; + + if (!$pattern) { + return true; + } + + if (!preg_match($pattern, $id_value)) { + $this->add_error('invalid_id_format', sprintf('Invalid ID format for %s', $service_config['name'])); + return false; + } + + if ($service_key === 'google_analytics') { + return $this->validate_google_analytics_id($id_value); + } + + return true; + } + + private function validate_google_analytics_id($id_value) { + if (strpos($id_value, 'G-') === 0) { + if (!preg_match('/^G-[A-Z0-9]{10}$/', $id_value)) { + $this->add_error('invalid_ga4_format', 'Invalid Google Analytics 4 ID format'); + return false; + } + } elseif (strpos($id_value, 'UA-') === 0) { + if (!preg_match('/^UA-[0-9]+-[0-9]+$/', $id_value)) { + $this->add_error('invalid_ua_format', 'Invalid Universal Analytics ID format'); + return false; + } + } else { + $this->add_error('invalid_ga_format', 'Google Analytics ID must start with G- or UA-'); + return false; + } + + return true; + } + + private function validate_custom_code($custom_code) { + if (empty($custom_code)) { + $this->add_error('empty_custom_code', 'Custom code cannot be empty'); + return false; + } + + if (strlen($custom_code) > 50000) { + $this->add_error('code_too_long', 'Custom code is too long (max 50,000 characters)'); + return false; + } + + if (!$this->validate_script_structure($custom_code)) { + return false; + } + + if (!$this->validate_code_security($custom_code)) { + return false; + } + + return true; + } + + private function validate_script_structure($custom_code) { + $has_script_tag = strpos($custom_code, ' -', - '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 deleted file mode 100644 index 4cdbbb9..0000000 --- a/includes/class-wptag-output-controller.php +++ /dev/null @@ -1,178 +0,0 @@ -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('