mirror of
https://github.com/WenPai-org/wptag.git
synced 2025-08-04 13:00:53 +08:00
Refactor plugin structure and remove legacy admin code
Removed old admin controllers, interfaces, AJAX handlers, and partials. Introduced new classes in the includes directory for admin, config, frontend, loader, output manager, and validator. Updated asset files and main plugin entry point to use the new structure. Cleaned up obsolete files and improved overall maintainability.
This commit is contained in:
parent
840a779a84
commit
6d56c60b26
28 changed files with 3276 additions and 4899 deletions
672
includes/class-admin.php
Normal file
672
includes/class-admin.php
Normal file
|
@ -0,0 +1,672 @@
|
|||
<?php
|
||||
|
||||
namespace WPTag;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Admin {
|
||||
private $config;
|
||||
private $validator;
|
||||
private $output_manager;
|
||||
|
||||
public function __construct($config) {
|
||||
$this->config = $config;
|
||||
$this->validator = new Validator($config);
|
||||
$this->output_manager = new Output_Manager($config);
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
private function current_user_can_manage_codes() {
|
||||
return current_user_can('manage_options') || current_user_can('wptag_manage_codes');
|
||||
}
|
||||
|
||||
private function current_user_can_manage_services() {
|
||||
return current_user_can('manage_options') || current_user_can('wptag_manage_services');
|
||||
}
|
||||
|
||||
private function init_hooks() {
|
||||
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||
add_action('admin_init', array($this, 'admin_init'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
|
||||
add_action('wp_ajax_wptag_validate_code', array($this, 'ajax_validate_code'));
|
||||
add_action('wp_ajax_wptag_preview_code', array($this, 'ajax_preview_code'));
|
||||
add_action('wp_ajax_wptag_export_settings', array($this, 'ajax_export_settings'));
|
||||
add_action('wp_ajax_wptag_import_settings', array($this, 'ajax_import_settings'));
|
||||
add_action('wp_ajax_wptag_reset_settings', array($this, 'ajax_reset_settings'));
|
||||
add_filter('plugin_action_links_' . plugin_basename(WPTAG_PLUGIN_FILE), array($this, 'add_action_links'));
|
||||
add_filter('plugin_row_meta', array($this, 'add_row_meta'), 10, 2);
|
||||
add_action('init', array($this, 'add_custom_capabilities'));
|
||||
}
|
||||
|
||||
public function add_custom_capabilities() {
|
||||
$role = get_role('administrator');
|
||||
if ($role) {
|
||||
$role->add_cap('wptag_manage_codes');
|
||||
$role->add_cap('wptag_manage_services');
|
||||
}
|
||||
|
||||
$role = get_role('editor');
|
||||
if ($role) {
|
||||
$role->add_cap('wptag_manage_codes');
|
||||
}
|
||||
}
|
||||
|
||||
public function add_admin_menu() {
|
||||
$main_hook = add_options_page(
|
||||
'WPTag Settings',
|
||||
'WPTag',
|
||||
'manage_options',
|
||||
'wptag-settings',
|
||||
array($this, 'display_admin_page')
|
||||
);
|
||||
|
||||
add_action('load-' . $main_hook, array($this, 'handle_form_submission'));
|
||||
}
|
||||
|
||||
public function admin_init() {
|
||||
register_setting(
|
||||
'wptag_settings_group',
|
||||
'wptag_settings',
|
||||
array(
|
||||
'sanitize_callback' => array($this, 'sanitize_settings'),
|
||||
'default' => array()
|
||||
)
|
||||
);
|
||||
|
||||
register_setting(
|
||||
'wptag_services_group',
|
||||
'wptag_services',
|
||||
array(
|
||||
'sanitize_callback' => array($this, 'sanitize_services'),
|
||||
'default' => array()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_admin_assets($hook) {
|
||||
if ($hook !== 'settings_page_wptag-settings') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'wptag-admin',
|
||||
WPTAG_PLUGIN_URL . 'assets/admin.css',
|
||||
array(),
|
||||
WPTAG_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wptag-admin',
|
||||
WPTAG_PLUGIN_URL . 'assets/admin.js',
|
||||
array('jquery', 'wp-util'),
|
||||
WPTAG_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script('wptag-admin', 'wptagAdmin', array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wptag_admin_nonce'),
|
||||
'strings' => array(
|
||||
'validating' => 'Validating...',
|
||||
'valid' => 'Valid',
|
||||
'invalid' => 'Invalid',
|
||||
'preview' => 'Preview',
|
||||
'close' => 'Close',
|
||||
'export_success' => 'Settings exported successfully',
|
||||
'import_success' => 'Settings imported successfully',
|
||||
'reset_success' => 'Settings reset successfully',
|
||||
'confirm_reset' => 'Are you sure you want to reset all settings? This cannot be undone.',
|
||||
'confirm_import' => 'This will overwrite your current settings. Continue?',
|
||||
'loading' => 'Loading...'
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
public function display_admin_page() {
|
||||
if (!$this->current_user_can_manage_codes() && !$this->current_user_can_manage_services()) {
|
||||
wp_die(__('You do not have sufficient permissions to access this page.'));
|
||||
}
|
||||
|
||||
$this->handle_form_submission();
|
||||
|
||||
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'analytics';
|
||||
$categories = $this->get_categories_with_services();
|
||||
|
||||
?>
|
||||
<div class="wrap wptag-admin">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
|
||||
<?php $this->display_admin_notices(); ?>
|
||||
|
||||
<div class="wptag-header">
|
||||
<div class="wptag-header-info">
|
||||
<p>Manage your tracking codes and analytics services with ease.</p>
|
||||
</div>
|
||||
<div class="wptag-header-actions">
|
||||
<?php if ($this->current_user_can_manage_codes()): ?>
|
||||
<button type="button" class="button" id="wptag-export-btn">Export Settings</button>
|
||||
<button type="button" class="button" id="wptag-import-btn">Import Settings</button>
|
||||
<button type="button" class="button button-secondary" id="wptag-reset-btn">Reset All</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-tab-wrapper">
|
||||
<?php foreach ($categories as $category_key => $category_data): ?>
|
||||
<a href="?page=wptag-settings&tab=<?php echo esc_attr($category_key); ?>"
|
||||
class="nav-tab <?php echo $active_tab === $category_key ? 'nav-tab-active' : ''; ?>">
|
||||
<?php echo esc_html(ucfirst($category_key)); ?>
|
||||
<span class="count">(<?php echo count($category_data['services']); ?>)</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($this->current_user_can_manage_services()): ?>
|
||||
<a href="?page=wptag-settings&tab=services"
|
||||
class="nav-tab <?php echo $active_tab === 'services' ? 'nav-tab-active' : ''; ?>">
|
||||
Services Management
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<?php if ($active_tab === 'services' && $this->current_user_can_manage_services()): ?>
|
||||
<?php $this->display_services_tab(); ?>
|
||||
<?php else: ?>
|
||||
<?php $this->display_category_tab($active_tab, $categories[$active_tab] ?? array()); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" id="wptag-import-file" accept=".json" style="display: none;">
|
||||
|
||||
<div id="wptag-preview-modal" style="display: none;">
|
||||
<div class="wptag-modal-content">
|
||||
<div class="wptag-modal-header">
|
||||
<h3>Code Preview</h3>
|
||||
<button type="button" class="wptag-modal-close">×</button>
|
||||
</div>
|
||||
<div class="wptag-modal-body">
|
||||
<pre id="wptag-preview-code"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function display_services_tab() {
|
||||
if (!$this->current_user_can_manage_services()) {
|
||||
echo '<div class="wptag-no-services">';
|
||||
echo '<p>You do not have permission to manage services.</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$all_services = $this->config->get_all_services();
|
||||
$enabled_services = $this->config->get_enabled_services();
|
||||
$categories = array();
|
||||
|
||||
foreach ($all_services as $service_key => $service_config) {
|
||||
$categories[$service_config['category']][$service_key] = $service_config;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="wptag-services-management">
|
||||
<div class="wptag-services-header">
|
||||
<p>Enable or disable tracking services. Only enabled services will appear in the category tabs.</p>
|
||||
<div class="wptag-services-actions">
|
||||
<button type="button" class="button" id="wptag-enable-all">Enable All</button>
|
||||
<button type="button" class="button" id="wptag-disable-all">Disable All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('wptag_save_services', 'wptag_services_nonce'); ?>
|
||||
|
||||
<?php foreach ($categories as $category_name => $category_services): ?>
|
||||
<div class="wptag-service-category">
|
||||
<h2><?php echo esc_html(ucfirst($category_name)); ?></h2>
|
||||
<div class="wptag-services-grid">
|
||||
<?php foreach ($category_services as $service_key => $service_config): ?>
|
||||
<div class="wptag-service-item service-<?php echo esc_attr($service_key); ?>">
|
||||
<div class="wptag-service-info">
|
||||
<div class="wptag-service-icon">
|
||||
<span class="dashicons <?php echo esc_attr($service_config['icon']); ?>"></span>
|
||||
</div>
|
||||
<div class="wptag-service-details">
|
||||
<h3><?php echo esc_html($service_config['name']); ?></h3>
|
||||
<p><?php echo esc_html($service_config['description']); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wptag-service-toggle">
|
||||
<label class="wptag-switch">
|
||||
<input type="checkbox"
|
||||
name="enabled_services[]"
|
||||
value="<?php echo esc_attr($service_key); ?>"
|
||||
<?php checked(in_array($service_key, $enabled_services)); ?>>
|
||||
<span class="wptag-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="wptag-form-actions">
|
||||
<?php submit_button('Save Services', 'primary', 'save_services', false); ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function get_categories_with_services() {
|
||||
$categories = array();
|
||||
$services_config = $this->config->get_services_config();
|
||||
|
||||
foreach ($services_config as $service_key => $service_config) {
|
||||
$category = $service_config['category'];
|
||||
if (!isset($categories[$category])) {
|
||||
$categories[$category] = array(
|
||||
'title' => ucfirst($category),
|
||||
'services' => array()
|
||||
);
|
||||
}
|
||||
$categories[$category]['services'][$service_key] = $service_config;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
private function display_category_tab($active_tab, $category_data) {
|
||||
if (empty($category_data['services'])) {
|
||||
echo '<div class="wptag-no-services">';
|
||||
echo '<p>No services enabled for this category. <a href="?page=wptag-settings&tab=services">Enable some services</a> to get started.</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<form method="post" action="" class="wptag-settings-form">
|
||||
<?php wp_nonce_field('wptag_save_settings', 'wptag_nonce'); ?>
|
||||
|
||||
<div class="wptag-services-grid">
|
||||
<?php foreach ($category_data['services'] as $service_key => $service_config): ?>
|
||||
<?php $this->display_service_card($service_key, $service_config); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="wptag-form-actions">
|
||||
<?php submit_button('Save Settings', 'primary', 'save_settings', false); ?>
|
||||
</div>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function display_service_card($service_key, $service_config) {
|
||||
$service_settings = $this->config->get_service_settings($service_key);
|
||||
$field_key = $service_config['field'];
|
||||
$can_edit = $this->current_user_can_manage_codes();
|
||||
?>
|
||||
<div class="wptag-service-card" data-service="<?php echo esc_attr($service_key); ?>">
|
||||
<div class="wptag-service-header">
|
||||
<div class="wptag-service-icon">
|
||||
<span class="dashicons <?php echo esc_attr($service_config['icon']); ?>"></span>
|
||||
</div>
|
||||
<div class="wptag-service-title">
|
||||
<h3><?php echo esc_html($service_config['name']); ?></h3>
|
||||
<div class="wptag-service-toggle">
|
||||
<label class="wptag-switch">
|
||||
<input type="checkbox"
|
||||
name="wptag_settings[<?php echo esc_attr($service_key); ?>][enabled]"
|
||||
value="1"
|
||||
<?php checked($service_settings['enabled']); ?>
|
||||
<?php disabled(!$can_edit); ?>>
|
||||
<span class="wptag-slider <?php echo !$can_edit ? 'disabled' : ''; ?>"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wptag-service-content">
|
||||
<div class="wptag-form-row">
|
||||
<div class="wptag-radio-group">
|
||||
<label>
|
||||
<input type="radio"
|
||||
name="wptag_settings[<?php echo esc_attr($service_key); ?>][use_template]"
|
||||
value="1"
|
||||
<?php checked($service_settings['use_template']); ?>
|
||||
<?php disabled(!$can_edit); ?>>
|
||||
Template
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio"
|
||||
name="wptag_settings[<?php echo esc_attr($service_key); ?>][use_template]"
|
||||
value="0"
|
||||
<?php checked($service_settings['use_template'], false); ?>
|
||||
<?php disabled(!$can_edit); ?>>
|
||||
Custom
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wptag-template-fields" <?php echo !$service_settings['use_template'] ? 'style="display: none;"' : ''; ?>>
|
||||
<div class="wptag-form-row">
|
||||
<label class="wptag-form-label" for="<?php echo esc_attr($service_key . '_' . $field_key); ?>">
|
||||
<?php echo esc_html(ucfirst(str_replace('_', ' ', $field_key))); ?>
|
||||
</label>
|
||||
<div class="wptag-input-group">
|
||||
<input type="text"
|
||||
id="<?php echo esc_attr($service_key . '_' . $field_key); ?>"
|
||||
name="wptag_settings[<?php echo esc_attr($service_key); ?>][<?php echo esc_attr($field_key); ?>]"
|
||||
value="<?php echo esc_attr($service_settings[$field_key]); ?>"
|
||||
placeholder="<?php echo esc_attr($service_config['placeholder']); ?>"
|
||||
class="wptag-input"
|
||||
<?php disabled(!$can_edit); ?>>
|
||||
<?php if ($can_edit): ?>
|
||||
<button type="button" class="button wptag-validate-btn" data-service="<?php echo esc_attr($service_key); ?>">
|
||||
Validate
|
||||
</button>
|
||||
<button type="button" class="button wptag-preview-btn" data-service="<?php echo esc_attr($service_key); ?>">
|
||||
Preview
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wptag-validation-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wptag-custom-fields" <?php echo $service_settings['use_template'] ? 'style="display: none;"' : ''; ?>>
|
||||
<div class="wptag-form-row">
|
||||
<label class="wptag-form-label" for="<?php echo esc_attr($service_key . '_custom_code'); ?>">
|
||||
Custom Code
|
||||
</label>
|
||||
<div class="wptag-code-editor-wrapper">
|
||||
<textarea id="<?php echo esc_attr($service_key . '_custom_code'); ?>"
|
||||
name="wptag_settings[<?php echo esc_attr($service_key); ?>][custom_code]"
|
||||
rows="12"
|
||||
placeholder="Paste your complete tracking code here..."
|
||||
class="wptag-code-editor"
|
||||
<?php disabled(!$can_edit); ?>><?php echo esc_textarea($service_settings['custom_code']); ?></textarea>
|
||||
<div class="wptag-code-editor-toolbar">
|
||||
<button type="button" class="button wptag-format-code" title="Format Code">
|
||||
<span class="dashicons dashicons-editor-code"></span>
|
||||
</button>
|
||||
<button type="button" class="button wptag-clear-code" title="Clear Code">
|
||||
<span class="dashicons dashicons-trash"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($can_edit): ?>
|
||||
<div class="wptag-input-group">
|
||||
<button type="button" class="button wptag-validate-btn" data-service="<?php echo esc_attr($service_key); ?>">
|
||||
Validate
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="wptag-validation-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wptag-advanced-settings" style="display: none;">
|
||||
<div class="wptag-form-row">
|
||||
<label class="wptag-form-label">Position</label>
|
||||
<select name="wptag_settings[<?php echo esc_attr($service_key); ?>][position]"
|
||||
class="wptag-select" <?php disabled(!$can_edit); ?>>
|
||||
<option value="head" <?php selected($service_settings['position'], 'head'); ?>>Head</option>
|
||||
<option value="body" <?php selected($service_settings['position'], 'body'); ?>>Body</option>
|
||||
<option value="footer" <?php selected($service_settings['position'], 'footer'); ?>>Footer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="wptag-form-row">
|
||||
<label class="wptag-form-label">Priority</label>
|
||||
<input type="number"
|
||||
name="wptag_settings[<?php echo esc_attr($service_key); ?>][priority]"
|
||||
value="<?php echo esc_attr($service_settings['priority']); ?>"
|
||||
min="1"
|
||||
max="100"
|
||||
class="wptag-input wptag-input-small"
|
||||
<?php disabled(!$can_edit); ?>>
|
||||
</div>
|
||||
|
||||
<div class="wptag-form-row">
|
||||
<label class="wptag-form-label">Device</label>
|
||||
<select name="wptag_settings[<?php echo esc_attr($service_key); ?>][device]"
|
||||
class="wptag-select" <?php disabled(!$can_edit); ?>>
|
||||
<option value="all" <?php selected($service_settings['device'], 'all'); ?>>All Devices</option>
|
||||
<option value="desktop" <?php selected($service_settings['device'], 'desktop'); ?>>Desktop Only</option>
|
||||
<option value="mobile" <?php selected($service_settings['device'], 'mobile'); ?>>Mobile Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wptag-advanced-toggle">
|
||||
<button type="button" class="button button-link wptag-toggle-advanced">
|
||||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||||
Advanced Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function display_admin_notices() {
|
||||
$notices = get_transient('wptag_admin_notices');
|
||||
if (!$notices) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($notices as $notice) {
|
||||
printf(
|
||||
'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
|
||||
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 = '<a href="' . admin_url('options-general.php?page=wptag-settings') . '">Settings</a>';
|
||||
array_unshift($links, $settings_link);
|
||||
return $links;
|
||||
}
|
||||
|
||||
public function add_row_meta($links, $file) {
|
||||
if ($file === plugin_basename(WPTAG_PLUGIN_FILE)) {
|
||||
$links[] = '<a href="https://wptag.com/docs/" target="_blank">Documentation</a>';
|
||||
$links[] = '<a href="https://wptag.com/support/" target="_blank">Support</a>';
|
||||
}
|
||||
return $links;
|
||||
}
|
||||
}
|
447
includes/class-config.php
Normal file
447
includes/class-config.php
Normal file
|
@ -0,0 +1,447 @@
|
|||
<?php
|
||||
|
||||
namespace WPTag;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Config {
|
||||
private $option_name = 'wptag_settings';
|
||||
private $services_option = 'wptag_services';
|
||||
private $services_config = array();
|
||||
private $cached_settings = null;
|
||||
private $cached_services = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->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');
|
||||
}
|
||||
}
|
69
includes/class-frontend.php
Normal file
69
includes/class-frontend.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace WPTag;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Frontend {
|
||||
private $config;
|
||||
private $output_manager;
|
||||
|
||||
public function __construct($config) {
|
||||
$this->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');
|
||||
}
|
||||
}
|
51
includes/class-loader.php
Normal file
51
includes/class-loader.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace WPTag;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Loader {
|
||||
private $actions = array();
|
||||
private $filters = array();
|
||||
private $shortcodes = array();
|
||||
|
||||
public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
|
||||
$this->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']));
|
||||
}
|
||||
}
|
||||
}
|
479
includes/class-output-manager.php
Normal file
479
includes/class-output-manager.php
Normal file
|
@ -0,0 +1,479 @@
|
|||
<?php
|
||||
|
||||
namespace WPTag;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Output_Manager {
|
||||
private $config;
|
||||
private $templates = array();
|
||||
private $cached_codes = array();
|
||||
|
||||
public function __construct($config) {
|
||||
$this->config = $config;
|
||||
$this->init_templates();
|
||||
}
|
||||
|
||||
private function init_templates() {
|
||||
$this->templates = array(
|
||||
'google_analytics' => array(
|
||||
'G-' => '<!-- Google Analytics 4 -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={ID}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "{ID}");
|
||||
</script>',
|
||||
'UA-' => '<!-- Universal Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={ID}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "{ID}");
|
||||
</script>'
|
||||
),
|
||||
'google_tag_manager' => array(
|
||||
'head' => '<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({"gtm.start":
|
||||
new Date().getTime(),event:"gtm.js"});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!="dataLayer"?"&l="+l:"";j.async=true;j.src=
|
||||
"https://www.googletagmanager.com/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,"script","dataLayer","{ID}");</script>',
|
||||
'body' => '<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={ID}"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>'
|
||||
),
|
||||
'facebook_pixel' => '<!-- Facebook Pixel -->
|
||||
<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version="2.0";
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,"script",
|
||||
"https://connect.facebook.net/en_US/fbevents.js");
|
||||
fbq("init", "{ID}");
|
||||
fbq("track", "PageView");
|
||||
</script>
|
||||
<noscript><img height="1" width="1" style="display:none"
|
||||
src="https://www.facebook.com/tr?id={ID}&ev=PageView&noscript=1"
|
||||
/></noscript>',
|
||||
'google_ads' => '<!-- Google Ads -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={ID}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "{ID}");
|
||||
</script>',
|
||||
'microsoft_clarity' => '<!-- Microsoft Clarity -->
|
||||
<script type="text/javascript">
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "{ID}");
|
||||
</script>',
|
||||
'hotjar' => '<!-- Hotjar -->
|
||||
<script>
|
||||
(function(h,o,t,j,a,r){
|
||||
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
|
||||
h._hjSettings={hjid:{ID},hjsv:6};
|
||||
a=o.getElementsByTagName("head")[0];
|
||||
r=o.createElement("script");r.async=1;
|
||||
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
|
||||
a.appendChild(r);
|
||||
})(window,document,"https://static.hotjar.com/c/hotjar-",".js?sv=");
|
||||
</script>',
|
||||
'tiktok_pixel' => '<!-- TikTok Pixel -->
|
||||
<script>
|
||||
!function (w, d, t) {
|
||||
w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var i="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=i,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=document.createElement("script");o.type="text/javascript",o.async=!0,o.src=i+"?sdkid="+e+"&lib="+t;var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)};
|
||||
ttq.load("{ID}");
|
||||
ttq.page();
|
||||
}(window, document, "ttq");
|
||||
</script>',
|
||||
'linkedin_insight' => '<!-- LinkedIn Insight -->
|
||||
<script type="text/javascript">
|
||||
_linkedin_partner_id = "{ID}";
|
||||
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
|
||||
window._linkedin_data_partner_ids.push(_linkedin_partner_id);
|
||||
</script><script type="text/javascript">
|
||||
(function(){var s = document.getElementsByTagName("script")[0];
|
||||
var b = document.createElement("script");
|
||||
b.type = "text/javascript";b.async = true;
|
||||
b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
|
||||
s.parentNode.insertBefore(b, s);})();
|
||||
</script>
|
||||
<noscript>
|
||||
<img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid={ID}&fmt=gif" />
|
||||
</noscript>',
|
||||
'twitter_pixel' => '<!-- Twitter Pixel -->
|
||||
<script>!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
|
||||
},s.version="1.1",s.queue=[],u=t.createElement(n),u.async=!0,u.src="//static.ads-twitter.com/uwt.js",
|
||||
a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,"script");
|
||||
twq("init","{ID}");
|
||||
twq("track","PageView");
|
||||
</script>',
|
||||
'pinterest_pixel' => '<!-- Pinterest Pixel -->
|
||||
<script>
|
||||
!function(e){if(!window.pintrk){window.pintrk = function () {
|
||||
window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var
|
||||
n=window.pintrk;n.queue=[],n.version="3.0";var
|
||||
t=document.createElement("script");t.async=!0,t.src=e;var
|
||||
r=document.getElementsByTagName("script")[0];
|
||||
r.parentNode.insertBefore(t,r)}}("https://s.pinimg.com/ct/core.js");
|
||||
pintrk("load", "{ID}");
|
||||
pintrk("page");
|
||||
</script>
|
||||
<noscript>
|
||||
<img height="1" width="1" style="display:none;" alt=""
|
||||
src="https://ct.pinterest.com/v3/?tid={ID}&event=init&noscript=1" />
|
||||
</noscript>',
|
||||
'snapchat_pixel' => '<!-- Snapchat Pixel -->
|
||||
<script type="text/javascript">
|
||||
(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function()
|
||||
{a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};
|
||||
a.queue=[];var s="script";r=t.createElement(s);r.async=!0;
|
||||
r.src=n;var u=t.getElementsByTagName(s)[0];
|
||||
u.parentNode.insertBefore(r,u);})(window,document,
|
||||
"https://sc-static.net/scevent.min.js");
|
||||
snaptr("init", "{ID}");
|
||||
snaptr("track", "PAGE_VIEW");
|
||||
</script>',
|
||||
'google_optimize' => '<!-- Google Optimize -->
|
||||
<script src="https://www.googleoptimize.com/optimize.js?id={ID}"></script>',
|
||||
'crazyegg' => '<!-- Crazy Egg -->
|
||||
<script type="text/javascript">
|
||||
setTimeout(function(){var a=document.createElement("script");
|
||||
var b=document.getElementsByTagName("script")[0];
|
||||
a.src=document.location.protocol+"//script.crazyegg.com/pages/scripts/{ID}.js?"+Math.floor(new Date().getTime()/3600000);
|
||||
a.async=true;a.type="text/javascript";b.parentNode.insertBefore(a,b)}, 1);
|
||||
</script>',
|
||||
'mixpanel' => '<!-- Mixpanel -->
|
||||
<script type="text/javascript">(function(c,a){if(!a.__SV){var b=window;try{var d,m,j,k=b.location,f=k.hash;d=function(a,b){return(m=a.match(RegExp(b+"=([^&]*)")))?m[1]:null};f&&d(f,"state")&&(j=JSON.parse(decodeURIComponent(d(f,"state"))),"mpeditor"===j.action&&(b.sessionStorage.setItem("_mpcehash",f),history.replaceState(j.desiredHash||"",c.title,k.pathname+k.search)))}catch(n){}var l,h;window.mixpanel=a;a._i=[];a.init=function(b,d,g){function c(b,i){var a=i.split(".");2==a.length&&(b=b[a[0]],i=a[1]);b[i]=function(){b.push([i].concat(Array.prototype.slice.call(arguments,0)))}}var e=a;"undefined"!==typeof g?e=a[g]=[]:g="mixpanel";e.people=e.people||[];e.toString=function(b){var a="mixpanel";"mixpanel"!==g&&(a+="."+g);b||(a+=" (stub)");return a};e.people.toString=function(){return e.toString(1)+".people (stub)"};l="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");for(h=0;h<l.length;h++)c(e,l[h]);a._i.push([b,d,g])};a.__SV=1.2;b=c.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===c.location.protocol&&"//cdn4.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\\/\\//)?"https://cdn4.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn4.mxpnl.com/libs/mixpanel-2-latest.min.js";d=c.getElementsByTagName("script")[0];d.parentNode.insertBefore(b,d)}})(document,window.mixpanel||[]);
|
||||
mixpanel.init("{ID}");</script>',
|
||||
'amplitude' => '<!-- Amplitude -->
|
||||
<script type="text/javascript">
|
||||
(function(e,t){var n=e.amplitude||{_q:[],_iq:{}};var r=t.createElement("script")
|
||||
;r.type="text/javascript"
|
||||
;r.integrity="sha384-vYYnQ3LPdp/RkQjoKBTGSq0X5F73gXU3G2QopHaVDVgTmCFocxCNFh3e8bD5gOo9"
|
||||
;r.crossOrigin="anonymous";r.async=true
|
||||
;r.src="https://cdn.amplitude.com/libs/amplitude-8.17.0-min.gz.js"
|
||||
;r.onload=function(){if(!e.amplitude.runQueuedFunctions){console.log("[Amplitude] Error: could not load SDK")}}
|
||||
;var i=t.getElementsByTagName("script")[0];i.parentNode.insertBefore(r,i)
|
||||
;function s(e,t){e.prototype[t]=function(){this._q.push([t].concat(Array.prototype.slice.call(arguments,0)));return this}}
|
||||
var o=function(){this._q=[];return this}
|
||||
;var a=["add","append","clearAll","prepend","set","setOnce","unset","preInsert","postInsert","remove"]
|
||||
;for(var c=0;c<a.length;c++){s(o,a[c])} n.Identify=o;var u=function(){this._q=[]
|
||||
;return this}
|
||||
;var l=["setProductId","setQuantity","setPrice","setRevenueType","setEventProperties"]
|
||||
;for(var p=0;p<l.length;p++){s(u,l[p])} n.Revenue=u
|
||||
;var d=["init","logEvent","logRevenue","setUserId","setUserProperties","setOptOut","setVersionName","setDomain","setDeviceId","enableTracking","setGlobalUserProperties","identify","clearUserProperties","setGroup","logRevenueV2","regenerateDeviceId","groupIdentify","onInit","logEventWithTimestamp","logEventWithGroups","setSessionId","resetSessionId"]
|
||||
;function v(e){function t(t){e[t]=function(){e._q.push([t].concat(Array.prototype.slice.call(arguments,0)))}}for(var n=0;n<d.length;n++){t(d[n])}}v(n);n.getInstance=function(e){e=(!e||e.length===0?"$default_instance":e).toLowerCase()
|
||||
;if(!Object.prototype.hasOwnProperty.call(n._iq,e)){n._iq[e]={_q:[]};v(n._iq[e])} return n._iq[e]};e.amplitude=n})(window,document);
|
||||
amplitude.getInstance().init("{ID}");
|
||||
</script>',
|
||||
'matomo' => '<!-- Matomo -->
|
||||
<script type="text/javascript">
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push([\'trackPageView\']);
|
||||
_paq.push([\'enableLinkTracking\']);
|
||||
(function() {
|
||||
var u="//your-matomo-domain.com/";
|
||||
_paq.push([\'setTrackerUrl\', u+"matomo.php"]);
|
||||
_paq.push([\'setSiteId\', "{ID}"]);
|
||||
var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0];
|
||||
g.type="text/javascript"; g.async=true; g.src=u+"matomo.js"; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>',
|
||||
);
|
||||
}
|
||||
|
||||
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<!-- WPTag Codes - Position: {$position} -->\n";
|
||||
|
||||
foreach ($codes as $code) {
|
||||
$output .= $code . "\n";
|
||||
}
|
||||
|
||||
$output .= "<!-- End WPTag Codes -->\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);
|
||||
}
|
||||
}
|
393
includes/class-validator.php
Normal file
393
includes/class-validator.php
Normal file
|
@ -0,0 +1,393 @@
|
|||
<?php
|
||||
|
||||
namespace WPTag;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Validator {
|
||||
private $config;
|
||||
private $errors = array();
|
||||
|
||||
public function __construct($config = null) {
|
||||
$this->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, '<script') !== false;
|
||||
$has_noscript_tag = strpos($custom_code, '<noscript') !== false;
|
||||
|
||||
if ($has_script_tag) {
|
||||
$script_open_count = substr_count($custom_code, '<script');
|
||||
$script_close_count = substr_count($custom_code, '</script>');
|
||||
|
||||
if ($script_open_count !== $script_close_count) {
|
||||
$this->add_error('mismatched_script_tags', 'Mismatched script tags');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_noscript_tag) {
|
||||
$noscript_open_count = substr_count($custom_code, '<noscript');
|
||||
$noscript_close_count = substr_count($custom_code, '</noscript>');
|
||||
|
||||
if ($noscript_open_count !== $noscript_close_count) {
|
||||
$this->add_error('mismatched_noscript_tags', 'Mismatched noscript tags');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validate_code_security($custom_code) {
|
||||
$dangerous_patterns = array(
|
||||
'/\beval\s*\(/i' => 'eval() function is not allowed',
|
||||
'/\bFunction\s*\(/i' => 'Function() constructor is not allowed',
|
||||
'/\bsetTimeout\s*\(\s*["\']/' => 'setTimeout with string argument is not allowed',
|
||||
'/\bsetInterval\s*\(\s*["\']/' => 'setInterval with string argument is not allowed',
|
||||
'/\bdocument\.write\s*\(/i' => 'document.write() is discouraged',
|
||||
'/\bwindow\.location\s*=/' => 'Redirecting window.location is not allowed',
|
||||
'/\bwindow\.open\s*\(/i' => 'window.open() is not allowed',
|
||||
'/\balert\s*\(/i' => 'alert() is not allowed',
|
||||
'/\bconfirm\s*\(/i' => 'confirm() is not allowed',
|
||||
'/\bprompt\s*\(/i' => 'prompt() is not allowed',
|
||||
'/javascript\s*:/i' => 'javascript: protocol is not allowed',
|
||||
'/\<\s*iframe[^>]*src\s*=\s*["\']?javascript:/i' => 'javascript: in iframe src is not allowed',
|
||||
'/\<\s*object[^>]*data\s*=\s*["\']?javascript:/i' => 'javascript: in object data is not allowed'
|
||||
);
|
||||
|
||||
foreach ($dangerous_patterns as $pattern => $message) {
|
||||
if (preg_match($pattern, $custom_code)) {
|
||||
$this->add_error('security_violation', $message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match_all('/https?:\/\/([^\/\s"\']+)/i', $custom_code, $matches)) {
|
||||
$domains = $matches[1];
|
||||
$suspicious_domains = array(
|
||||
'bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'ow.ly',
|
||||
'malware.com', 'virus.com', 'phishing.com'
|
||||
);
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
if (in_array(strtolower($domain), $suspicious_domains)) {
|
||||
$this->add_error('suspicious_domain', 'Suspicious domain detected: ' . $domain);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validate_settings($settings) {
|
||||
$this->errors = array();
|
||||
|
||||
if (!is_array($settings)) {
|
||||
$this->add_error('invalid_settings_format', 'Settings must be an array');
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid = true;
|
||||
|
||||
foreach ($settings as $service_key => $service_settings) {
|
||||
if (!$this->validate_service_settings($service_key, $service_settings)) {
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
private function validate_service_settings($service_key, $service_settings) {
|
||||
$service_config = $this->config->get_service_config($service_key);
|
||||
if (!$service_config) {
|
||||
$this->add_error($service_key . '_not_found', 'Service configuration not found for ' . $service_key);
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid = true;
|
||||
|
||||
if (!$this->validate_boolean($service_settings['enabled'] ?? false)) {
|
||||
$this->add_error($service_key . '_enabled_invalid', 'Enabled setting must be boolean');
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
if (!$this->validate_boolean($service_settings['use_template'] ?? true)) {
|
||||
$this->add_error($service_key . '_use_template_invalid', 'Use template setting must be boolean');
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
if (!$this->validate_position($service_settings['position'] ?? 'head')) {
|
||||
$this->add_error($service_key . '_position_invalid', 'Invalid position setting');
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
if (!$this->validate_priority($service_settings['priority'] ?? 10)) {
|
||||
$this->add_error($service_key . '_priority_invalid', 'Priority must be between 1 and 100');
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
if (!$this->validate_device($service_settings['device'] ?? 'all')) {
|
||||
$this->add_error($service_key . '_device_invalid', 'Invalid device setting');
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
private function validate_boolean($value) {
|
||||
return is_bool($value) || $value === '1' || $value === '0' || $value === 1 || $value === 0;
|
||||
}
|
||||
|
||||
private function validate_position($position) {
|
||||
$valid_positions = array('head', 'body', 'footer');
|
||||
return in_array($position, $valid_positions);
|
||||
}
|
||||
|
||||
private function validate_priority($priority) {
|
||||
$priority = intval($priority);
|
||||
return $priority >= 1 && $priority <= 100;
|
||||
}
|
||||
|
||||
private function validate_device($device) {
|
||||
$valid_devices = array('all', 'desktop', 'mobile');
|
||||
return in_array($device, $valid_devices);
|
||||
}
|
||||
|
||||
public function validate_import_data($json_data) {
|
||||
$this->errors = array();
|
||||
|
||||
$data = json_decode($json_data, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->add_error('invalid_json', 'Invalid JSON format: ' . json_last_error_msg());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
$this->add_error('invalid_data_type', 'Import data must be an array');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($data['settings'])) {
|
||||
$this->add_error('missing_settings', 'Import data missing settings');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($data['version'])) {
|
||||
if (!$this->validate_version_compatibility($data['version'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['services'])) {
|
||||
if (!$this->validate_services_list($data['services'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->validate_settings($data['settings']);
|
||||
}
|
||||
|
||||
private function validate_version_compatibility($version) {
|
||||
$current_version = WPTAG_VERSION;
|
||||
|
||||
$import_version_parts = explode('.', $version);
|
||||
$current_version_parts = explode('.', $current_version);
|
||||
|
||||
if (count($import_version_parts) !== 3 || count($current_version_parts) !== 3) {
|
||||
$this->add_error('invalid_version_format', 'Invalid version format');
|
||||
return false;
|
||||
}
|
||||
|
||||
$import_major = intval($import_version_parts[0]);
|
||||
$current_major = intval($current_version_parts[0]);
|
||||
|
||||
if ($import_major > $current_major) {
|
||||
$this->add_error('version_incompatible', 'Import data is from a newer version and may not be compatible');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validate_services_list($services) {
|
||||
if (!is_array($services)) {
|
||||
$this->add_error('invalid_services_format', 'Services list must be an array');
|
||||
return false;
|
||||
}
|
||||
|
||||
$all_services = $this->config->get_all_services();
|
||||
|
||||
foreach ($services as $service_key) {
|
||||
if (!isset($all_services[$service_key])) {
|
||||
$this->add_error('unknown_service', 'Unknown service: ' . $service_key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function add_error($code, $message) {
|
||||
$this->errors[] = array(
|
||||
'code' => $code,
|
||||
'message' => $message
|
||||
);
|
||||
}
|
||||
|
||||
public function get_errors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function get_error_messages() {
|
||||
return array_column($this->errors, 'message');
|
||||
}
|
||||
|
||||
public function has_errors() {
|
||||
return !empty($this->errors);
|
||||
}
|
||||
|
||||
public function get_last_error() {
|
||||
if (empty($this->errors)) {
|
||||
return null;
|
||||
}
|
||||
return end($this->errors);
|
||||
}
|
||||
|
||||
public function clear_errors() {
|
||||
$this->errors = array();
|
||||
}
|
||||
|
||||
public function get_error_count() {
|
||||
return count($this->errors);
|
||||
}
|
||||
|
||||
public function validate_tracking_id($service_key, $id_value) {
|
||||
$service_config = $this->config->get_service_config($service_key);
|
||||
if (!$service_config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = $service_config['validation_pattern'] ?? null;
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return preg_match($pattern, $id_value);
|
||||
}
|
||||
|
||||
public function sanitize_tracking_id($service_key, $id_value) {
|
||||
$id_value = trim($id_value);
|
||||
$id_value = sanitize_text_field($id_value);
|
||||
|
||||
$id_value = wp_kses($id_value, array());
|
||||
|
||||
return $id_value;
|
||||
}
|
||||
|
||||
public function get_validation_pattern($service_key) {
|
||||
$service_config = $this->config->get_service_config($service_key);
|
||||
return $service_config['validation_pattern'] ?? null;
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPTag_Cache_Manager {
|
||||
private $cache_group = 'wptag';
|
||||
private $cache_enabled;
|
||||
private $ttl;
|
||||
|
||||
public function __construct() {
|
||||
$this->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();
|
||||
}
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPTag_Condition_Engine {
|
||||
private $context_cache = [];
|
||||
|
||||
public function evaluate_conditions($conditions, $context = []) {
|
||||
if (empty($conditions) || !is_array($conditions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cache_key = md5(json_encode($conditions) . json_encode($context));
|
||||
if (isset($this->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')
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,267 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPTag_Core {
|
||||
private static $instance = null;
|
||||
private $snippet_manager;
|
||||
private $condition_engine;
|
||||
private $output_controller;
|
||||
private $template_manager;
|
||||
private $cache_manager;
|
||||
|
||||
public static function get_instance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->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' => '<script async src="https://www.googletagmanager.com/gtag/js?id={{measurement_id}}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "{{measurement_id}}");
|
||||
</script>',
|
||||
'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' => '<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version="2.0";
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,"script",
|
||||
"https://connect.facebook.net/en_US/fbevents.js");
|
||||
fbq("init", "{{pixel_id}}");
|
||||
fbq("track", "PageView");
|
||||
</script>
|
||||
<noscript><img height="1" width="1" style="display:none"
|
||||
src="https://www.facebook.com/tr?id={{pixel_id}}&ev=PageView&noscript=1"
|
||||
/></noscript>',
|
||||
'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' => '<script async src="https://www.googletagmanager.com/gtag/js?id=AW-{{conversion_id}}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "AW-{{conversion_id}}");
|
||||
gtag("event", "conversion", {"send_to": "AW-{{conversion_id}}/{{conversion_label}}"});
|
||||
</script>',
|
||||
'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' => '<meta name="google-site-verification" content="{{verification_code}}" />',
|
||||
'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' => '<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?{{site_id}}";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>',
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPTag_Output_Controller {
|
||||
private $snippet_manager;
|
||||
private $condition_engine;
|
||||
private $cache_manager;
|
||||
private $rendered_snippets = [];
|
||||
|
||||
public function __construct($snippet_manager, $condition_engine, $cache_manager) {
|
||||
$this->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<!-- WPTAG Start -->\n" . $output . "<!-- WPTAG End -->\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, '<script') === false) {
|
||||
$code = '<script>' . $code . '</script>';
|
||||
}
|
||||
|
||||
return str_replace('<script', '<script async', $code);
|
||||
}
|
||||
|
||||
private function wrap_defer_script($code) {
|
||||
if (strpos($code, '<script') === false) {
|
||||
$code = '<script>' . $code . '</script>';
|
||||
}
|
||||
|
||||
return str_replace('<script', '<script defer', $code);
|
||||
}
|
||||
|
||||
private function wrap_preview_mode($code, $snippet) {
|
||||
$name = esc_html($snippet['name']);
|
||||
$id = esc_attr($snippet['id']);
|
||||
|
||||
return "<!-- WPTAG Preview: {$name} (ID: {$id}) -->\n{$code}\n<!-- /WPTAG Preview -->\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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,264 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPTag_Snippet_Manager {
|
||||
private $table_name;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->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')
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,271 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPTag_Template_Manager {
|
||||
private $table_name;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue