Enhance admin UI and UX for WPTag plugin

Improves the admin interface with refined CSS for better layout, accessibility, and responsiveness. Adds robust JS logic for disabling tracking scripts, change tracking, validation, preview, import/export, reset, tooltips, and modal handling. These changes provide a more user-friendly experience, prevent accidental data loss, and ensure tracking scripts do not run in the admin area.
This commit is contained in:
feibisi 2025-07-26 05:47:24 +08:00
parent 3c4c1f8e2e
commit 30e040134d
10 changed files with 3290 additions and 685 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,41 @@
jQuery(document).ready(function($) {
if (typeof window.gtag !== 'undefined') {
delete window.gtag;
}
if (typeof window.fbq !== 'undefined') {
delete window.fbq;
}
if (typeof window.dataLayer !== 'undefined') {
delete window.dataLayer;
}
window.gtag = function() {
console.log('WPTag: gtag() calls are disabled in admin area');
};
window.fbq = function() {
console.log('WPTag: fbq() calls are disabled in admin area');
};
WPTagAdmin.init();
$(window).on('load', function() {
setTimeout(function() {
WPTagAdmin.initializing = false;
WPTagAdmin.changeTrackingEnabled = true;
}, 500);
});
});
var WPTagAdmin = {
formChanged: false,
changeTrackingEnabled: false,
initializing: true,
init: function() {
var $ = jQuery;
var self = this;
this.handleServiceToggle();
this.handleCodeTypeToggle();
this.handleValidation();
@ -18,39 +48,72 @@ var WPTagAdmin = {
this.handleTabSwitching();
this.handleAdvancedToggle();
this.handleCodeEditor();
this.initTooltips();
setTimeout(function() {
self.initializing = false;
self.handleFormChangeTracking();
}, 1000);
},
handleServiceToggle: function() {
var $ = jQuery;
var self = this;
$('.wptag-service-card input[name*="[enabled]"]').on('change', function() {
var $card = $(this).closest('.wptag-service-card');
var isEnabled = $(this).is(':checked');
self.changeTrackingEnabled = false;
if (isEnabled) {
$card.removeClass('disabled');
$card.removeClass('disabled').addClass('enabled');
$card.find('.wptag-service-content input, .wptag-service-content select, .wptag-service-content textarea, .wptag-service-content button').prop('disabled', false);
} else {
$card.addClass('disabled');
$card.removeClass('enabled').addClass('disabled');
$card.find('.wptag-service-content input, .wptag-service-content select, .wptag-service-content textarea, .wptag-service-content button').prop('disabled', true);
$card.find('.wptag-validation-result').hide();
}
setTimeout(function() {
self.changeTrackingEnabled = true;
if (!self.initializing) {
self.formChanged = true;
}
}, 100);
});
$('.wptag-service-card input[name*="[enabled]"]').trigger('change');
$('.wptag-service-card input[name*="[enabled]"]').each(function() {
$(this).trigger('change');
});
},
handleCodeTypeToggle: function() {
var $ = jQuery;
var self = this;
$('input[name*="[use_template]"]').on('change', function() {
var $card = $(this).closest('.wptag-service-card');
var useTemplate = $(this).val() === '1' && $(this).is(':checked');
self.changeTrackingEnabled = false;
if (useTemplate) {
$card.find('.wptag-template-fields').show();
$card.find('.wptag-custom-fields').hide();
$card.find('.wptag-template-fields').slideDown(300);
$card.find('.wptag-custom-fields').slideUp(300);
} else {
$card.find('.wptag-template-fields').hide();
$card.find('.wptag-custom-fields').show();
$card.find('.wptag-template-fields').slideUp(300);
$card.find('.wptag-custom-fields').slideDown(300);
}
$card.find('.wptag-validation-result').hide();
setTimeout(function() {
self.changeTrackingEnabled = true;
if (!self.initializing) {
self.formChanged = true;
}
}, 100);
});
$('input[name*="[use_template]"]:checked').trigger('change');
@ -58,18 +121,31 @@ var WPTagAdmin = {
handleValidation: function() {
var $ = jQuery;
$('.wptag-validate-btn').on('click', function() {
$('.wptag-validate-btn').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var $card = $btn.closest('.wptag-service-card');
var service = $card.data('service');
var $result = $card.find('.wptag-validation-result');
var useTemplate = $card.find('input[name*="[use_template]"]:checked').val() === '1';
var idValue = $card.find('input[type="text"]').first().val();
var customCode = $card.find('textarea').val();
var idValue = $card.find('input[type="text"]').first().val().trim();
var customCode = $card.find('textarea').val().trim();
$btn.prop('disabled', true).text(wptagAdmin.strings.validating);
$result.removeClass('valid invalid').addClass('loading').text(wptagAdmin.strings.validating);
if (useTemplate && !idValue) {
WPTagAdmin.showValidationResult($result, 'invalid', 'Please enter an ID value first');
return;
}
if (!useTemplate && !customCode) {
WPTagAdmin.showValidationResult($result, 'invalid', 'Please enter custom code first');
return;
}
$btn.addClass('wptag-button-loading').prop('disabled', true);
WPTagAdmin.showValidationResult($result, 'loading', wptagAdmin.strings.validating);
$.ajax({
url: wptagAdmin.ajax_url,
@ -84,42 +160,54 @@ var WPTagAdmin = {
},
success: function(response) {
if (response.success) {
$result.removeClass('loading invalid').addClass('valid')
.text('✓ ' + response.data.message);
WPTagAdmin.showValidationResult($result, 'valid', '✓ ' + response.data.message);
} else {
$result.removeClass('loading valid').addClass('invalid')
.text('✗ ' + response.data.message);
WPTagAdmin.showValidationResult($result, 'invalid', '✗ ' + response.data.message);
}
},
error: function() {
$result.removeClass('loading valid').addClass('invalid')
.text('✗ Validation failed');
error: function(xhr, status, error) {
console.error('Validation error:', error);
WPTagAdmin.showValidationResult($result, 'invalid', '✗ Validation failed. Please try again.');
},
complete: function() {
$btn.prop('disabled', false).text('Validate');
$btn.removeClass('wptag-button-loading').prop('disabled', false);
}
});
});
$('input[type="text"], textarea').on('input', function() {
$(this).closest('.wptag-service-card').find('.wptag-validation-result').text('');
$(this).closest('.wptag-service-card').find('.wptag-validation-result').hide();
if (WPTagAdmin.changeTrackingEnabled && !WPTagAdmin.initializing) {
WPTagAdmin.formChanged = true;
}
});
},
showValidationResult: function($result, type, message) {
$result.removeClass('valid invalid loading')
.addClass(type)
.text(message)
.show();
},
handlePreview: function() {
var $ = jQuery;
$('.wptag-preview-btn').on('click', function() {
$('.wptag-preview-btn').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var $card = $btn.closest('.wptag-service-card');
var service = $card.data('service');
var idValue = $card.find('input[type="text"]').first().val();
var idValue = $card.find('input[type="text"]').first().val().trim();
if (!idValue.trim()) {
if (!idValue) {
alert('Please enter an ID value to preview');
$card.find('input[type="text"]').first().focus();
return;
}
$btn.prop('disabled', true).text(wptagAdmin.strings.loading);
$btn.addClass('wptag-button-loading').prop('disabled', true);
$.ajax({
url: wptagAdmin.ajax_url,
@ -132,17 +220,20 @@ var WPTagAdmin = {
},
success: function(response) {
if (response.success) {
$('#wptag-preview-code').text(response.data.preview);
$('#wptag-preview-modal').show();
var previewCode = response.data.preview;
$('#wptag-preview-code').text(previewCode);
$('#wptag-preview-modal').fadeIn(300);
$('body').addClass('modal-open');
} else {
alert('Error: ' + response.data.message);
WPTagAdmin.showNotice('error', 'Preview Error: ' + response.data.message);
}
},
error: function() {
alert('Failed to generate preview');
error: function(xhr, status, error) {
console.error('Preview error:', error);
WPTagAdmin.showNotice('error', 'Failed to generate preview. Please try again.');
},
complete: function() {
$btn.prop('disabled', false).text(wptagAdmin.strings.preview);
$btn.removeClass('wptag-button-loading').prop('disabled', false);
}
});
});
@ -150,10 +241,12 @@ var WPTagAdmin = {
handleExportImport: function() {
var $ = jQuery;
$('#wptag-export-btn').on('click', function() {
$('#wptag-export-btn').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var originalText = $btn.text();
$btn.prop('disabled', true).text(wptagAdmin.strings.loading);
$btn.addClass('wptag-button-loading').prop('disabled', true);
$.ajax({
url: wptagAdmin.ajax_url,
@ -167,19 +260,21 @@ var WPTagAdmin = {
WPTagAdmin.downloadFile(response.data.data, response.data.filename);
WPTagAdmin.showNotice('success', wptagAdmin.strings.export_success);
} else {
alert('Export failed: ' + response.data.message);
WPTagAdmin.showNotice('error', 'Export failed: ' + response.data.message);
}
},
error: function() {
alert('Export failed');
error: function(xhr, status, error) {
console.error('Export error:', error);
WPTagAdmin.showNotice('error', 'Export failed. Please try again.');
},
complete: function() {
$btn.prop('disabled', false).text(originalText);
$btn.removeClass('wptag-button-loading').prop('disabled', false);
}
});
});
$('#wptag-import-btn').on('click', function() {
$('#wptag-import-btn').on('click', function(e) {
e.preventDefault();
$('#wptag-import-file').click();
});
@ -187,7 +282,14 @@ var WPTagAdmin = {
var file = this.files[0];
if (!file) return;
if (!file.name.endsWith('.json')) {
WPTagAdmin.showNotice('error', 'Please select a valid JSON file.');
this.value = '';
return;
}
if (!confirm(wptagAdmin.strings.confirm_import)) {
this.value = '';
return;
}
@ -206,34 +308,43 @@ var WPTagAdmin = {
success: function(response) {
if (response.success) {
WPTagAdmin.showNotice('success', wptagAdmin.strings.import_success);
WPTagAdmin.formChanged = false;
WPTagAdmin.initializing = true;
setTimeout(function() {
location.reload();
}, 1000);
}, 1500);
} else {
alert('Import failed: ' + response.data.message);
WPTagAdmin.showNotice('error', 'Import failed: ' + response.data.message);
}
},
error: function() {
alert('Import failed');
error: function(xhr, status, error) {
console.error('Import error:', error);
WPTagAdmin.showNotice('error', 'Import failed. Please try again.');
}
});
};
reader.readAsText(file);
reader.onerror = function() {
WPTagAdmin.showNotice('error', 'Failed to read the file.');
};
reader.readAsText(file);
this.value = '';
});
},
handleReset: function() {
var $ = jQuery;
$('#wptag-reset-btn').on('click', function() {
$('#wptag-reset-btn').on('click', function(e) {
e.preventDefault();
if (!confirm(wptagAdmin.strings.confirm_reset)) {
return;
}
var $btn = $(this);
var originalText = $btn.text();
$btn.prop('disabled', true).text(wptagAdmin.strings.loading);
$btn.addClass('wptag-button-loading').prop('disabled', true);
$.ajax({
url: wptagAdmin.ajax_url,
@ -245,18 +356,21 @@ var WPTagAdmin = {
success: function(response) {
if (response.success) {
WPTagAdmin.showNotice('success', wptagAdmin.strings.reset_success);
WPTagAdmin.formChanged = false;
WPTagAdmin.initializing = true;
setTimeout(function() {
location.reload();
}, 1000);
}, 1500);
} else {
alert('Reset failed: ' + response.data.message);
WPTagAdmin.showNotice('error', 'Reset failed: ' + response.data.message);
}
},
error: function() {
alert('Reset failed');
error: function(xhr, status, error) {
console.error('Reset error:', error);
WPTagAdmin.showNotice('error', 'Reset failed. Please try again.');
},
complete: function() {
$btn.prop('disabled', false).text(originalText);
$btn.removeClass('wptag-button-loading').prop('disabled', false);
}
});
});
@ -264,14 +378,26 @@ var WPTagAdmin = {
handleServicesManagement: function() {
var $ = jQuery;
$('#wptag-enable-all').on('click', function() {
$('input[name="enabled_services[]"]').prop('checked', true);
$('.wptag-service-item').removeClass('disabled');
var self = this;
$('#wptag-enable-all').on('click', function(e) {
e.preventDefault();
self.changeTrackingEnabled = false;
$('input[name="enabled_services[]"]').prop('checked', true).trigger('change');
setTimeout(function() {
self.changeTrackingEnabled = true;
self.formChanged = true;
}, 100);
});
$('#wptag-disable-all').on('click', function() {
$('input[name="enabled_services[]"]').prop('checked', false);
$('.wptag-service-item').addClass('disabled');
$('#wptag-disable-all').on('click', function(e) {
e.preventDefault();
self.changeTrackingEnabled = false;
$('input[name="enabled_services[]"]').prop('checked', false).trigger('change');
setTimeout(function() {
self.changeTrackingEnabled = true;
self.formChanged = true;
}, 100);
});
$('.wptag-service-item input[type="checkbox"]').on('change', function() {
@ -279,9 +405,13 @@ var WPTagAdmin = {
var isChecked = $(this).is(':checked');
if (isChecked) {
$item.removeClass('disabled');
$item.removeClass('disabled').addClass('enabled');
} else {
$item.addClass('disabled');
$item.removeClass('enabled').addClass('disabled');
}
if (self.changeTrackingEnabled && !self.initializing) {
self.formChanged = true;
}
});
@ -290,9 +420,10 @@ var WPTagAdmin = {
handleModal: function() {
var $ = jQuery;
$('.wptag-modal-close, #wptag-preview-modal').on('click', function(e) {
$('.wptag-modal-close, .wptag-modal-backdrop').on('click', function(e) {
if (e.target === this) {
$('#wptag-preview-modal').hide();
WPTagAdmin.closeModal();
}
});
@ -302,39 +433,43 @@ var WPTagAdmin = {
$(document).on('keydown', function(e) {
if (e.keyCode === 27 && $('#wptag-preview-modal').is(':visible')) {
$('#wptag-preview-modal').hide();
WPTagAdmin.closeModal();
}
});
},
closeModal: function() {
var $ = jQuery;
$('#wptag-preview-modal').fadeOut(300, function() {
$('body').removeClass('modal-open');
});
},
handleFormSubmission: function() {
var $ = jQuery;
var self = this;
$('.wptag-settings-form').on('submit', function(e) {
var hasErrors = false;
var $form = $(this);
$('.wptag-service-card').each(function() {
$('.wptag-service-card.enabled').each(function() {
var $card = $(this);
var isEnabled = $card.find('input[name*="[enabled]"]').is(':checked');
if (!isEnabled) {
return;
}
var useTemplate = $card.find('input[name*="[use_template]"]:checked').val() === '1';
if (useTemplate) {
var idInput = $card.find('input[type="text"]').first();
if (idInput.val().trim() === '') {
alert('Please enter an ID for enabled services or disable them.');
idInput.focus();
var $idInput = $card.find('input[type="text"]').first();
if ($idInput.val().trim() === '') {
WPTagAdmin.showNotice('error', 'Please enter an ID for all enabled services or disable them.');
$idInput.focus();
hasErrors = true;
return false;
}
} else {
var customCodeTextarea = $card.find('textarea');
if (customCodeTextarea.val().trim() === '') {
alert('Please enter custom code for enabled services or disable them.');
customCodeTextarea.focus();
var $customCodeTextarea = $card.find('textarea');
if ($customCodeTextarea.val().trim() === '') {
WPTagAdmin.showNotice('error', 'Please enter custom code for all enabled services or disable them.');
$customCodeTextarea.focus();
hasErrors = true;
return false;
}
@ -346,51 +481,69 @@ var WPTagAdmin = {
return false;
}
$(this).find('input[type="submit"]').prop('disabled', true).val('Saving...');
var $saveBtn = $form.find('#wptag-save-btn');
$saveBtn.addClass('wptag-button-loading').prop('disabled', true).val('Saving...');
self.formChanged = false;
});
$('#wptag-services-form').on('submit', function() {
var $saveBtn = $(this).find('input[type="submit"]');
$saveBtn.addClass('wptag-button-loading').prop('disabled', true).val('Saving...');
WPTagAdmin.formChanged = false;
});
},
handleTabSwitching: function() {
var $ = jQuery;
var self = this;
$('.nav-tab').on('click', function(e) {
var href = $(this).attr('href');
if (href && href.indexOf('tab=') !== -1) {
var tab = href.split('tab=')[1];
var currentUrl = window.location.href.split('?')[0];
var newUrl = currentUrl + '?page=wptag-settings&tab=' + tab;
window.location.href = newUrl;
if (self.formChanged && !self.initializing) {
var confirmed = confirm('You have unsaved changes. Do you want to continue without saving?');
if (!confirmed) {
e.preventDefault();
return false;
} else {
self.formChanged = false;
}
}
});
},
handleAdvancedToggle: function() {
var $ = jQuery;
$('.wptag-toggle-advanced').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var $card = $btn.closest('.wptag-service-card');
var $advanced = $card.find('.wptag-advanced-settings');
var $icon = $btn.find('.dashicons');
if ($advanced.is(':visible')) {
$advanced.slideUp();
$advanced.slideUp(300);
$icon.removeClass('dashicons-arrow-up-alt2').addClass('dashicons-arrow-down-alt2');
$btn.html('<span class="dashicons dashicons-arrow-down-alt2"></span>Advanced Settings');
$btn.html('<span class="dashicons dashicons-arrow-down-alt2"></span>' + wptagAdmin.strings.advanced_settings);
} else {
$advanced.slideDown();
$advanced.slideDown(300);
$icon.removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-up-alt2');
$btn.html('<span class="dashicons dashicons-arrow-up-alt2"></span>Hide Advanced Settings');
$btn.html('<span class="dashicons dashicons-arrow-up-alt2"></span>' + wptagAdmin.strings.hide_advanced);
}
});
},
handleCodeEditor: function() {
var $ = jQuery;
$('.wptag-code-editor').each(function() {
var $editor = $(this);
$editor.on('input', function() {
WPTagAdmin.updateEditorHeight(this);
if (WPTagAdmin.changeTrackingEnabled && !WPTagAdmin.initializing) {
WPTagAdmin.formChanged = true;
}
});
$editor.on('keydown', function(e) {
@ -407,7 +560,9 @@ var WPTagAdmin = {
WPTagAdmin.updateEditorHeight(this);
});
$('.wptag-format-code').on('click', function() {
$('.wptag-format-code').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var $editor = $btn.closest('.wptag-code-editor-wrapper').find('.wptag-code-editor');
var code = $editor.val();
@ -417,25 +572,88 @@ var WPTagAdmin = {
var formatted = WPTagAdmin.formatCode(code);
$editor.val(formatted);
WPTagAdmin.updateEditorHeight($editor[0]);
if (WPTagAdmin.changeTrackingEnabled && !WPTagAdmin.initializing) {
WPTagAdmin.formChanged = true;
}
WPTagAdmin.showNotice('success', 'Code formatted successfully.');
} catch (e) {
console.log('Code formatting failed:', e);
WPTagAdmin.showNotice('error', 'Code formatting failed. Please check your code syntax.');
}
}
});
$('.wptag-clear-code').on('click', function() {
$('.wptag-clear-code').on('click', function(e) {
e.preventDefault();
if (confirm('Are you sure you want to clear the code?')) {
var $btn = $(this);
var $editor = $btn.closest('.wptag-code-editor-wrapper').find('.wptag-code-editor');
$editor.val('');
$editor.val('').focus();
WPTagAdmin.updateEditorHeight($editor[0]);
if (WPTagAdmin.changeTrackingEnabled && !WPTagAdmin.initializing) {
WPTagAdmin.formChanged = true;
}
}
});
},
handleFormChangeTracking: function() {
var $ = jQuery;
var self = this;
self.changeTrackingEnabled = true;
$(document).on('input keyup', 'input[type="text"], textarea', function() {
if ($(this).closest('.wptag-admin').length > 0 && self.changeTrackingEnabled && !self.initializing) {
self.formChanged = true;
}
});
$(document).on('change', 'select, input[type="radio"], input[type="checkbox"]:not([name*="enabled_services"]):not([name*="[enabled]"]):not([name*="[use_template]"])', function() {
if ($(this).closest('.wptag-admin').length > 0 && self.changeTrackingEnabled && !self.initializing) {
self.formChanged = true;
}
});
window.addEventListener('beforeunload', function(e) {
if (self.formChanged && !self.initializing) {
e.preventDefault();
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
return e.returnValue;
}
});
},
initTooltips: function() {
var $ = jQuery;
$('[title]').each(function() {
var $el = $(this);
var title = $el.attr('title');
$el.removeAttr('title').on('mouseenter', function() {
$('<div class="wptag-tooltip">' + title + '</div>')
.appendTo('body')
.fadeIn(200);
}).on('mouseleave', function() {
$('.wptag-tooltip').remove();
}).on('mousemove', function(e) {
$('.wptag-tooltip').css({
top: e.pageY + 10,
left: e.pageX + 10
});
});
});
},
updateEditorHeight: function(editor) {
var $ = jQuery;
var $editor = $(editor);
editor.style.height = 'auto';
editor.style.height = Math.max(editor.scrollHeight, 120) + 'px';
var newHeight = Math.max(editor.scrollHeight, 120);
editor.style.height = newHeight + 'px';
},
formatCode: function(code) {
@ -444,11 +662,30 @@ var WPTagAdmin = {
.replace(/></g, '>\n<')
.replace(/^\s+|\s+$/g, '')
.split('\n')
.map(function(line) {
return line.trim();
.map(function(line, index, array) {
line = line.trim();
if (line.length === 0) return '';
var indent = 0;
for (var i = 0; i < index; i++) {
var prevLine = array[i].trim();
if (prevLine.match(/<[^\/][^>]*[^\/]>$/)) {
indent += 2;
}
if (prevLine.match(/<\/[^>]+>$/)) {
indent -= 2;
}
}
if (line.match(/^<\/[^>]+>$/)) {
indent -= 2;
}
indent = Math.max(0, indent);
return ' '.repeat(indent) + line;
})
.filter(function(line) {
return line.length > 0;
return line.trim().length > 0;
})
.join('\n');
} catch (e) {
@ -458,33 +695,54 @@ var WPTagAdmin = {
},
downloadFile: function(data, filename) {
var blob = new Blob([data], { type: 'application/json' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
try {
var blob = new Blob([data], { type: 'application/json' });
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, filename);
return;
}
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(function() {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}, 100);
} catch (e) {
console.error('Download failed:', e);
WPTagAdmin.showNotice('error', 'Download failed. Please try again.');
}
},
showNotice: function(type, message) {
var $ = jQuery;
var $notice = $('<div class="notice notice-' + type + ' is-dismissible"><p>' + message + '</p></div>');
$('.wptag-admin .notice').remove();
var $notice = $('<div class="notice notice-' + type + ' is-dismissible"><p>' + message + '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button></div>');
$('.wptag-admin h1').after($notice);
$notice.find('.notice-dismiss').on('click', function() {
$notice.fadeOut(300, function() {
$(this).remove();
});
});
setTimeout(function() {
$notice.fadeOut(function() {
$notice.fadeOut(300, function() {
$(this).remove();
});
}, 5000);
$notice.find('.notice-dismiss').on('click', function() {
$notice.fadeOut(function() {
$(this).remove();
});
});
$('html, body').animate({
scrollTop: $('.wptag-admin').offset().top - 50
}, 500);
}
};

View file

@ -28,6 +28,8 @@ class Admin {
}
private function init_hooks() {
add_filter('wptag_should_output_codes', '__return_false', 999);
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'));
@ -120,7 +122,10 @@ class Admin {
'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...'
'loading' => 'Loading...',
'fill_required' => 'Please fill in required fields for enabled services.',
'advanced_settings' => 'Advanced Settings',
'hide_advanced' => 'Hide Advanced Settings'
)
));
}
@ -130,7 +135,10 @@ class Admin {
wp_die(__('You do not have sufficient permissions to access this page.'));
}
$this->handle_form_submission();
add_filter('wptag_should_output_codes', '__return_false', 999);
remove_action('wp_head', array($this->frontend ?? null, 'output_head_codes'), 1);
remove_action('wp_body_open', array($this->frontend ?? null, 'output_body_codes'), 1);
remove_action('wp_footer', array($this->frontend ?? null, 'output_footer_codes'), 1);
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'analytics';
$categories = $this->get_categories_with_services();
@ -143,13 +151,25 @@ class Admin {
<div class="wptag-header">
<div class="wptag-header-info">
<p>Manage your tracking codes and analytics services with ease.</p>
<p>Manage your tracking codes and analytics services with ease. Enable services in the Services Management tab, then configure them in their respective category tabs.</p>
<?php if (defined('WP_DEBUG') && WP_DEBUG): ?>
<p><small><strong>Debug Mode:</strong> Check your website's source code for WPTag debug comments if codes are not appearing.</small></p>
<?php endif; ?>
</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>
<button type="button" class="button" id="wptag-export-btn">
<span class="dashicons dashicons-download"></span>
Export Settings
</button>
<button type="button" class="button" id="wptag-import-btn">
<span class="dashicons dashicons-upload"></span>
Import Settings
</button>
<button type="button" class="button button-secondary" id="wptag-reset-btn">
<span class="dashicons dashicons-admin-generic"></span>
Reset All
</button>
<?php endif; ?>
</div>
</div>
@ -157,14 +177,18 @@ class Admin {
<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' : ''; ?>">
class="nav-tab <?php echo $active_tab === $category_key ? 'nav-tab-active' : ''; ?>"
data-tab="<?php echo esc_attr($category_key); ?>">
<span class="dashicons dashicons-chart-area"></span>
<?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' : ''; ?>">
class="nav-tab <?php echo $active_tab === 'services' ? 'nav-tab-active' : ''; ?>"
data-tab="services">
<span class="dashicons dashicons-admin-settings"></span>
Services Management
</a>
<?php endif; ?>
@ -181,7 +205,8 @@ class Admin {
<input type="file" id="wptag-import-file" accept=".json" style="display: none;">
<div id="wptag-preview-modal" style="display: none;">
<div id="wptag-preview-modal" class="wptag-modal" style="display: none;">
<div class="wptag-modal-backdrop"></div>
<div class="wptag-modal-content">
<div class="wptag-modal-header">
<h3>Code Preview</h3>
@ -214,14 +239,17 @@ class Admin {
?>
<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-info">
<p>Enable or disable tracking services. Only enabled services will appear in the category tabs.</p>
<p><small><strong>Custom Code Support:</strong> When using custom code, JavaScript will be preserved without filtering - perfect for gtag(), fbq(), and other tracking functions.</small></p>
</div>
<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="">
<form method="post" action="" id="wptag-services-form">
<?php wp_nonce_field('wptag_save_services', 'wptag_services_nonce'); ?>
<?php foreach ($categories as $category_name => $category_services): ?>
@ -229,7 +257,8 @@ class Admin {
<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-item <?php echo in_array($service_key, $enabled_services) ? 'enabled' : 'disabled'; ?>"
data-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>
@ -283,13 +312,17 @@ class Admin {
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 class="wptag-no-services-icon">';
echo '<span class="dashicons dashicons-admin-settings"></span>';
echo '</div>';
echo '<h3>No Services Available</h3>';
echo '<p>No services are 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">
<form method="post" action="" class="wptag-settings-form" id="wptag-settings-form">
<?php wp_nonce_field('wptag_save_settings', 'wptag_nonce'); ?>
<div class="wptag-services-grid">
@ -299,7 +332,7 @@ class Admin {
</div>
<div class="wptag-form-actions">
<?php submit_button('Save Settings', 'primary', 'save_settings', false); ?>
<?php submit_button('Save Settings', 'primary', 'save_settings', false, array('id' => 'wptag-save-btn')); ?>
</div>
</form>
<?php
@ -309,8 +342,10 @@ class Admin {
$service_settings = $this->config->get_service_settings($service_key);
$field_key = $service_config['field'];
$can_edit = $this->current_user_can_manage_codes();
$is_enabled = !empty($service_settings['enabled']);
?>
<div class="wptag-service-card" data-service="<?php echo esc_attr($service_key); ?>">
<div class="wptag-service-card <?php echo $is_enabled ? 'enabled' : 'disabled'; ?>"
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>
@ -339,7 +374,7 @@ class Admin {
value="1"
<?php checked($service_settings['use_template']); ?>
<?php disabled(!$can_edit); ?>>
Template
<span>Template</span>
</label>
<label>
<input type="radio"
@ -347,7 +382,7 @@ class Admin {
value="0"
<?php checked($service_settings['use_template'], false); ?>
<?php disabled(!$can_edit); ?>>
Custom
<span>Custom</span>
</label>
</div>
</div>
@ -356,6 +391,7 @@ class Admin {
<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))); ?>
<span class="required">*</span>
</label>
<div class="wptag-input-group">
<input type="text"
@ -364,17 +400,24 @@ class Admin {
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 disabled(!$can_edit); ?>
data-required="true">
<?php if ($can_edit): ?>
<button type="button" class="button wptag-validate-btn" data-service="<?php echo esc_attr($service_key); ?>">
<span class="dashicons dashicons-yes"></span>
Validate
</button>
<button type="button" class="button wptag-preview-btn" data-service="<?php echo esc_attr($service_key); ?>">
<span class="dashicons dashicons-visibility"></span>
Preview
</button>
<?php endif; ?>
</div>
<div class="wptag-validation-result"></div>
<div class="wptag-field-help">
<small><?php echo esc_html($this->get_template_help_text($service_key, $service_config)); ?></small>
<small><a href="<?php echo esc_url($this->get_service_docs_url($service_key)); ?>" target="_blank" rel="noopener">View <?php echo esc_html($service_config['name']); ?> documentation</a></small>
</div>
</div>
</div>
@ -382,6 +425,7 @@ class Admin {
<div class="wptag-form-row">
<label class="wptag-form-label" for="<?php echo esc_attr($service_key . '_custom_code'); ?>">
Custom Code
<span class="required">*</span>
</label>
<div class="wptag-code-editor-wrapper">
<textarea id="<?php echo esc_attr($service_key . '_custom_code'); ?>"
@ -389,62 +433,72 @@ class Admin {
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>
<?php disabled(!$can_edit); ?>
data-required="true"><?php echo esc_textarea($service_settings['custom_code']); ?></textarea>
<?php if ($can_edit): ?>
<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>
<?php endif; ?>
</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); ?>">
<span class="dashicons dashicons-yes"></span>
Validate
</button>
</div>
<?php endif; ?>
<div class="wptag-validation-result"></div>
<div class="wptag-field-help">
<small><?php echo esc_html($this->get_service_help_text($service_key, $service_config)); ?></small>
<small><a href="<?php echo esc_url($this->get_service_docs_url($service_key)); ?>" target="_blank" rel="noopener">View <?php echo esc_html($service_config['name']); ?> documentation</a></small>
</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-advanced-settings">
<div class="wptag-advanced-grid">
<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">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 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>
<div class="wptag-advanced-toggle">
<button type="button" class="button button-link wptag-toggle-advanced">
<button type="button" class="wptag-toggle-advanced">
<span class="dashicons dashicons-arrow-down-alt2"></span>
Advanced Settings
</button>
@ -455,7 +509,7 @@ class Admin {
}
private function display_admin_notices() {
$notices = get_transient('wptag_admin_notices');
$notices = get_transient('wptag_admin_notices_' . get_current_user_id());
if (!$notices) {
return;
}
@ -468,25 +522,32 @@ class Admin {
);
}
delete_transient('wptag_admin_notices');
delete_transient('wptag_admin_notices_' . get_current_user_id());
}
private function add_admin_notice($type, $message) {
$notices = get_transient('wptag_admin_notices') ?: array();
$notices = get_transient('wptag_admin_notices_' . get_current_user_id()) ?: array();
$notices[] = array('type' => $type, 'message' => $message);
set_transient('wptag_admin_notices', $notices, 30);
set_transient('wptag_admin_notices_' . get_current_user_id(), $notices, 30);
}
public function handle_form_submission() {
if (!isset($_POST['wptag_nonce']) && !isset($_POST['wptag_services_nonce'])) {
return;
}
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')) {
if (isset($_POST['wptag_nonce']) && wp_verify_nonce($_POST['wptag_nonce'], 'wptag_save_settings')) {
$this->handle_settings_form_submission();
return;
}
}
private function handle_settings_form_submission() {
if (!$this->current_user_can_manage_codes()) {
$this->add_admin_notice('error', 'You do not have permission to manage tracking codes.');
return;
@ -498,7 +559,21 @@ class Admin {
$result = $this->config->update_settings($settings);
if ($result) {
$this->add_admin_notice('success', 'Settings saved successfully.');
do_action('wptag_settings_saved', $settings);
$has_custom_code = false;
foreach ($settings as $service_settings) {
if (!empty($service_settings['enabled']) && empty($service_settings['use_template']) && !empty($service_settings['custom_code'])) {
$has_custom_code = true;
break;
}
}
if ($has_custom_code) {
$this->add_admin_notice('success', 'Settings saved successfully. Custom JavaScript codes have been preserved correctly.');
} else {
$this->add_admin_notice('success', 'Settings saved successfully.');
}
} else {
$this->add_admin_notice('error', 'Failed to save settings.');
}
@ -530,6 +605,7 @@ class Admin {
$result = $this->config->update_enabled_services($enabled_services);
if ($result) {
do_action('wptag_services_updated', $enabled_services);
$this->add_admin_notice('success', 'Services updated successfully.');
} else {
$this->add_admin_notice('error', 'Failed to update services.');
@ -555,8 +631,13 @@ class Admin {
return;
}
$service_key = sanitize_text_field($_POST['service']);
$use_template = $_POST['use_template'] === '1';
$service_key = sanitize_text_field($_POST['service'] ?? '');
$use_template = ($_POST['use_template'] ?? '0') === '1';
if (empty($service_key)) {
wp_send_json_error(array('message' => 'Service key is required.'));
return;
}
$settings = array(
'enabled' => true,
@ -567,22 +648,34 @@ class Admin {
$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']);
$settings[$field_key] = sanitize_text_field($_POST['id_value'] ?? '');
}
} else {
$settings['custom_code'] = wp_kses_post($_POST['custom_code']);
$raw_custom_code = $_POST['custom_code'] ?? '';
$settings['custom_code'] = $this->sanitize_custom_code_for_validation($raw_custom_code);
}
$is_valid = $this->validator->validate_service_code($service_key, $settings);
if ($is_valid) {
wp_send_json_success(array('message' => 'Code is valid'));
wp_send_json_success(array('message' => 'Code is valid and ready to use.'));
} else {
$errors = $this->validator->get_error_messages();
wp_send_json_error(array('message' => implode(', ', $errors)));
}
}
private function sanitize_custom_code_for_validation($custom_code) {
if (empty($custom_code)) {
return '';
}
$custom_code = stripslashes($custom_code);
$custom_code = wp_check_invalid_utf8($custom_code);
return trim($custom_code);
}
public function ajax_preview_code() {
check_ajax_referer('wptag_admin_nonce', 'nonce');
@ -591,18 +684,38 @@ class Admin {
return;
}
$service_key = sanitize_text_field($_POST['service']);
$id_value = sanitize_text_field($_POST['id_value']);
$service_key = sanitize_text_field($_POST['service'] ?? '');
$id_value = sanitize_text_field($_POST['id_value'] ?? '');
if (empty($service_key)) {
wp_send_json_error(array('message' => 'Service key is required.'));
return;
}
if (empty($id_value)) {
wp_send_json_error(array('message' => 'ID value is required for preview.'));
return;
}
$preview = $this->output_manager->get_template_preview($service_key, $id_value);
if (!empty($preview)) {
wp_send_json_success(array('preview' => $preview));
$safe_preview = $this->sanitize_preview_output($preview);
wp_send_json_success(array('preview' => $safe_preview));
} else {
wp_send_json_error(array('message' => 'Unable to generate preview'));
wp_send_json_error(array('message' => 'Unable to generate preview. Please check your ID format.'));
}
}
private function sanitize_preview_output($preview) {
$preview = htmlspecialchars($preview, ENT_QUOTES, 'UTF-8');
$preview = str_replace(array('&lt;script&gt;', '&lt;/script&gt;'), array('<script>', '</script>'), $preview);
$preview = str_replace(array('&lt;noscript&gt;', '&lt;/noscript&gt;'), array('<noscript>', '</noscript>'), $preview);
return $preview;
}
public function ajax_export_settings() {
check_ajax_referer('wptag_admin_nonce', 'nonce');
@ -611,12 +724,16 @@ class Admin {
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'
));
try {
$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'
));
} catch (Exception $e) {
wp_send_json_error(array('message' => 'Export failed: ' . $e->getMessage()));
}
}
public function ajax_import_settings() {
@ -627,19 +744,25 @@ class Admin {
return;
}
$import_data = stripslashes($_POST['import_data']);
$import_data = wp_unslash($_POST['import_data'] ?? '');
if (empty($import_data)) {
wp_send_json_error(array('message' => 'Import data is required.'));
return;
}
if ($this->validator->validate_import_data($import_data)) {
$result = $this->config->import_settings($import_data);
if (!is_wp_error($result)) {
do_action('wptag_settings_imported');
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)));
wp_send_json_error(array('message' => 'Import validation failed: ' . implode(', ', $errors)));
}
}
@ -651,9 +774,13 @@ class Admin {
return;
}
$this->config->reset_to_defaults();
wp_send_json_success(array('message' => 'Settings reset successfully'));
try {
$this->config->reset_to_defaults();
do_action('wptag_settings_reset');
wp_send_json_success(array('message' => 'Settings reset successfully'));
} catch (Exception $e) {
wp_send_json_error(array('message' => 'Reset failed: ' . $e->getMessage()));
}
}
public function add_action_links($links) {
@ -669,4 +796,36 @@ class Admin {
}
return $links;
}
private function get_template_help_text($service_key, $service_config) {
return '';
}
private function get_service_help_text($service_key, $service_config) {
return '';
}
private function get_service_docs_url($service_key) {
$service_docs_mapping = array(
'google_analytics' => 'google-analytics',
'google_tag_manager' => 'google-tag-manager',
'facebook_pixel' => 'facebook-pixel',
'google_ads' => 'google-ads',
'microsoft_clarity' => 'microsoft-clarity',
'hotjar' => 'hotjar',
'tiktok_pixel' => 'tiktok-pixel',
'linkedin_insight' => 'linkedin-insight',
'twitter_pixel' => 'twitter-pixel',
'pinterest_pixel' => 'pinterest-pixel',
'snapchat_pixel' => 'snapchat-pixel',
'google_optimize' => 'google-optimize',
'crazyegg' => 'crazy-egg',
'mixpanel' => 'mixpanel',
'amplitude' => 'amplitude',
'matomo' => 'matomo'
);
$docs_slug = isset($service_docs_mapping[$service_key]) ? $service_docs_mapping[$service_key] : $service_key;
return 'https://wptag.com/document/' . $docs_slug . '/';
}
}

View file

@ -196,6 +196,8 @@ class Config {
'description' => 'Privacy-focused web analytics platform'
)
);
$this->services_config = apply_filters('wptag_services_config', $this->services_config);
}
public function get_all_services() {
@ -204,23 +206,46 @@ class 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'));
$default_services = array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads');
$this->cached_services = get_option($this->services_option, $default_services);
if (!is_array($this->cached_services)) {
$this->cached_services = $default_services;
}
}
return $this->cached_services;
}
public function update_enabled_services($services) {
$this->cached_services = is_array($services) ? $services : array();
$services = is_array($services) ? array_filter($services) : array();
$valid_services = array();
foreach ($services as $service_key) {
if (isset($this->services_config[$service_key])) {
$valid_services[] = $service_key;
}
}
$this->cached_services = $valid_services;
$result = update_option($this->services_option, $this->cached_services);
if ($result) {
$this->cached_settings = null;
wp_cache_delete('wptag_available_services', 'wptag');
do_action('wptag_enabled_services_updated', $this->cached_services);
}
return $result;
}
public function get_available_services() {
$cache_key = 'wptag_available_services';
$cached = wp_cache_get($cache_key, 'wptag');
if (false !== $cached) {
return $cached;
}
$enabled_services = $this->get_enabled_services();
$available = array();
@ -230,6 +255,7 @@ class Config {
}
}
wp_cache_set($cache_key, $available, 'wptag', 3600);
return $available;
}
@ -252,7 +278,7 @@ class Config {
$available_services = $this->get_available_services();
$categories = array();
foreach ($available_services as $service) {
foreach ($available_services as $service_key => $service) {
if (!in_array($service['category'], $categories)) {
$categories[] = $service['category'];
}
@ -263,23 +289,61 @@ class Config {
public function get_settings() {
if (null === $this->cached_settings) {
$this->cached_settings = get_option($this->option_name, $this->get_default_settings());
$this->cached_settings = get_option($this->option_name, array());
if (!is_array($this->cached_settings)) {
$this->cached_settings = array();
}
$this->cached_settings = $this->merge_with_defaults($this->cached_settings);
}
return $this->cached_settings;
}
private function merge_with_defaults($settings) {
$available_services = $this->get_available_services();
$merged_settings = array();
foreach ($available_services as $service_key => $service_config) {
if (isset($settings[$service_key]) && is_array($settings[$service_key])) {
$merged_settings[$service_key] = array_merge(
$this->get_default_service_settings($service_key),
$settings[$service_key]
);
$merged_settings[$service_key]['updated_at'] = current_time('mysql');
} else {
$merged_settings[$service_key] = $this->get_default_service_settings($service_key);
}
}
return $merged_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) {
if (!is_array($new_settings)) {
return false;
}
$sanitized_settings = $this->sanitize_settings($new_settings);
$result = update_option($this->option_name, $sanitized_settings);
if ($result) {
$this->cached_settings = $sanitized_settings;
$this->cached_settings = null;
wp_cache_delete('wptag_available_services', 'wptag');
wp_cache_set_last_changed('wptag_codes');
if (function_exists('wp_cache_flush_group')) {
wp_cache_flush_group('wptag');
}
do_action('wptag_settings_updated', $sanitized_settings);
do_action('wptag_clear_cache');
}
return $result;
@ -287,22 +351,11 @@ class Config {
public function update_service_settings($service_key, $service_settings) {
$all_settings = $this->get_settings();
$all_settings[$service_key] = $this->sanitize_service_settings($service_settings);
$all_settings[$service_key] = $this->sanitize_service_settings($service_settings, $service_key);
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) {
@ -323,91 +376,263 @@ class Config {
$defaults[$service_config['field']] = '';
return $defaults;
return apply_filters('wptag_default_service_settings', $defaults, $service_key);
}
public function sanitize_settings($settings) {
$sanitized = array();
if (!is_array($settings)) {
return $this->get_default_settings();
return array();
}
$sanitized = array();
foreach ($settings as $service_key => $service_settings) {
if (isset($this->services_config[$service_key])) {
$sanitized[$service_key] = $this->sanitize_service_settings($service_settings);
if (isset($this->services_config[$service_key]) && is_array($service_settings)) {
$sanitized[$service_key] = $this->sanitize_service_settings($service_settings, $service_key);
}
}
return $sanitized;
}
private function sanitize_service_settings($settings) {
private function sanitize_service_settings($settings, $service_key = '') {
if (!is_array($settings)) {
return $this->get_default_service_settings($service_key);
}
$service_config = $this->get_service_config($service_key);
$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(),
'custom_code' => $this->sanitize_custom_code($settings['custom_code'] ?? ''),
'position' => $this->sanitize_position($settings['position'] ?? 'head'),
'priority' => $this->sanitize_priority($settings['priority'] ?? 10),
'device' => $this->sanitize_device($settings['device'] ?? 'all'),
'conditions' => $this->sanitize_conditions($settings['conditions'] ?? array()),
'updated_at' => current_time('mysql')
);
foreach ($this->services_config as $service_key => $service_config) {
if ($service_config && isset($service_config['field'])) {
$field_key = $service_config['field'];
if (isset($settings[$field_key])) {
$sanitized[$field_key] = sanitize_text_field($settings[$field_key]);
$sanitized[$field_key] = sanitize_text_field($settings[$field_key] ?? '');
}
if (isset($settings['created_at'])) {
$sanitized['created_at'] = sanitize_text_field($settings['created_at']);
}
return apply_filters('wptag_sanitize_service_settings', $sanitized, $service_key, $settings);
}
private function get_allowed_html() {
return array(
'script' => array(
'type' => array(),
'src' => array(),
'async' => array(),
'defer' => array(),
'id' => array(),
'class' => array(),
'crossorigin' => array(),
'integrity' => 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(),
'frameborder' => array()
)
);
}
private function sanitize_position($position) {
$valid_positions = array('head', 'body', 'footer');
return in_array($position, $valid_positions) ? $position : 'head';
}
private function sanitize_priority($priority) {
$priority = intval($priority);
return ($priority >= 1 && $priority <= 100) ? $priority : 10;
}
private function sanitize_device($device) {
$valid_devices = array('all', 'desktop', 'mobile');
return in_array($device, $valid_devices) ? $device : 'all';
}
private function sanitize_conditions($conditions) {
if (!is_array($conditions)) {
return array();
}
$sanitized = array();
foreach ($conditions as $condition) {
if (is_array($condition)) {
$sanitized[] = array(
'type' => sanitize_text_field($condition['type'] ?? ''),
'operator' => sanitize_text_field($condition['operator'] ?? 'is'),
'value' => sanitize_text_field($condition['value'] ?? '')
);
}
}
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());
private function sanitize_custom_code($custom_code) {
if (empty($custom_code)) {
return '';
}
if (empty($existing_services)) {
$custom_code = stripslashes($custom_code);
$custom_code = wp_check_invalid_utf8($custom_code);
$allowed_tags = array(
'script' => array(
'type' => array(),
'src' => array(),
'async' => array(),
'defer' => array(),
'id' => array(),
'class' => array(),
'crossorigin' => array(),
'integrity' => array(),
'charset' => array()
),
'noscript' => array(),
'img' => array(
'src' => array(),
'alt' => array(),
'width' => array(),
'height' => array(),
'style' => array(),
'border' => array()
),
'iframe' => array(
'src' => array(),
'width' => array(),
'height' => array(),
'style' => array(),
'frameborder' => array(),
'scrolling' => array(),
'marginheight' => array(),
'marginwidth' => array()
),
'div' => array(
'id' => array(),
'class' => array(),
'style' => array()
),
'span' => array(
'id' => array(),
'class' => array(),
'style' => array()
)
);
if (!current_user_can('unfiltered_html')) {
$custom_code = wp_kses($custom_code, $allowed_tags);
}
$custom_code = $this->remove_dangerous_patterns($custom_code);
return trim($custom_code);
}
private function remove_dangerous_patterns($code) {
$dangerous_patterns = array(
'/<script[^>]*>\s*eval\s*\(/i',
'/<script[^>]*>\s*Function\s*\(/i',
'/javascript\s*:\s*void/i',
'/data:text\/html/i',
'/vbscript:/i'
);
foreach ($dangerous_patterns as $pattern) {
$code = preg_replace($pattern, '', $code);
}
$code = preg_replace_callback(
'/<script[^>]*>(.*?)<\/script>/is',
function($matches) {
$script_content = $matches[1];
$blocked_functions = array(
'eval(',
'Function(',
'execScript(',
'setTimeout("',
"setTimeout('",
'setInterval("',
"setInterval('",
'document.write(',
'document.writeln(',
'window.location=',
'location.href=',
'location.replace('
);
foreach ($blocked_functions as $blocked) {
if (stripos($script_content, $blocked) !== false) {
return '<!-- Blocked potentially dangerous script -->';
}
}
return $matches[0];
},
$code
);
return $code;
}
public function install_default_settings() {
$existing_settings = get_option($this->option_name);
$existing_services = get_option($this->services_option);
if (false === $existing_settings) {
$default_settings = array();
$default_services = array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads');
foreach ($default_services as $service_key) {
$default_settings[$service_key] = $this->get_default_service_settings($service_key);
}
add_option($this->option_name, $default_settings);
}
if (false === $existing_services) {
add_option($this->services_option, array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads'));
}
do_action('wptag_settings_installed');
}
public function reset_to_defaults() {
delete_option($this->option_name);
delete_option($this->services_option);
$this->cached_settings = null;
$this->cached_services = null;
wp_cache_delete('wptag_available_services', 'wptag');
$this->install_default_settings();
do_action('wptag_settings_reset');
return true;
}
public function export_settings() {
@ -417,31 +642,133 @@ class Config {
$export_data = array(
'version' => WPTAG_VERSION,
'exported_at' => current_time('mysql'),
'site_url' => get_site_url(),
'services' => $services,
'settings' => $settings
'settings' => $settings,
'plugin_info' => array(
'name' => 'WPTag',
'version' => WPTAG_VERSION,
'wordpress_version' => get_bloginfo('version'),
'php_version' => PHP_VERSION
)
);
return wp_json_encode($export_data, JSON_PRETTY_PRINT);
return wp_json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
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');
if (!is_array($data)) {
return new \WP_Error('invalid_json', 'Invalid JSON format');
}
if (!isset($data['settings']) || !is_array($data['settings'])) {
return new \WP_Error('invalid_data', 'Invalid import data format - missing settings');
}
if (isset($data['version'])) {
$compatibility_check = $this->check_version_compatibility($data['version']);
if (is_wp_error($compatibility_check)) {
return $compatibility_check;
}
}
$settings_result = false;
$services_result = false;
if (isset($data['services']) && is_array($data['services'])) {
$services_result = $this->update_enabled_services($data['services']);
}
$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']);
do_action('wptag_settings_imported', $data);
return true;
}
return new \WP_Error('import_failed', 'Failed to import settings');
}
private function check_version_compatibility($import_version) {
$current_version = WPTAG_VERSION;
if (version_compare($import_version, $current_version, '>')) {
return new \WP_Error('version_incompatible', 'Import data is from a newer version (' . $import_version . ') and may not be compatible with the current version (' . $current_version . ')');
}
$min_compatible_version = '1.0.0';
if (version_compare($import_version, $min_compatible_version, '<')) {
return new \WP_Error('version_too_old', 'Import data is from an incompatible version (' . $import_version . '). Minimum supported version is ' . $min_compatible_version);
}
return true;
}
public function get_plugin_stats() {
$settings = $this->get_settings();
$enabled_services = $this->get_enabled_services();
$stats = array(
'total_services' => count($this->services_config),
'enabled_services' => count($enabled_services),
'active_codes' => 0,
'categories' => array(),
'last_updated' => ''
);
foreach ($settings as $service_key => $service_settings) {
if (!empty($service_settings['enabled'])) {
$stats['active_codes']++;
}
if (!empty($service_settings['updated_at'])) {
if (empty($stats['last_updated']) || $service_settings['updated_at'] > $stats['last_updated']) {
$stats['last_updated'] = $service_settings['updated_at'];
}
}
}
$available_services = $this->get_available_services();
foreach ($available_services as $service_key => $service_config) {
$category = $service_config['category'];
if (!isset($stats['categories'][$category])) {
$stats['categories'][$category] = 0;
}
$stats['categories'][$category]++;
}
return $stats;
}
public function cleanup_orphaned_settings() {
$settings = get_option($this->option_name, array());
$enabled_services = $this->get_enabled_services();
$cleaned_settings = array();
foreach ($settings as $service_key => $service_settings) {
if (in_array($service_key, $enabled_services) && isset($this->services_config[$service_key])) {
$cleaned_settings[$service_key] = $service_settings;
}
}
if (count($cleaned_settings) !== count($settings)) {
update_option($this->option_name, $cleaned_settings);
$this->cached_settings = null;
return true;
}
return false;
}
public function migrate_settings($from_version) {
do_action('wptag_before_settings_migration', $from_version, WPTAG_VERSION);
$migrated = false;
do_action('wptag_after_settings_migration', $from_version, WPTAG_VERSION, $migrated);
return $migrated;
}
}

View file

@ -9,6 +9,7 @@ if (!defined('ABSPATH')) {
class Frontend {
private $config;
private $output_manager;
private $should_load = true;
public function __construct($config) {
$this->config = $config;
@ -18,52 +19,427 @@ class Frontend {
}
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('template_redirect', array($this, 'template_redirect'));
if (!is_admin()) {
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'));
add_filter('wptag_should_output_codes', array($this, 'filter_should_output_codes'), 10, 1);
}
public function template_redirect() {
$this->should_load = $this->determine_if_should_load();
}
public function output_head_codes() {
if (!$this->should_output_codes()) {
$this->output_debug_info('head');
return;
}
$this->output_manager->output_codes('head');
try {
$this->output_manager->output_codes('head');
} catch (Exception $e) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('WPTag Head Code Output Error: ' . $e->getMessage());
}
}
}
public function output_body_codes() {
if (!$this->should_output_codes()) {
$this->output_debug_info('body');
return;
}
$this->output_manager->output_codes('body');
try {
$this->output_manager->output_codes('body');
} catch (Exception $e) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('WPTag Body Code Output Error: ' . $e->getMessage());
}
}
}
public function output_footer_codes() {
if (!$this->should_output_codes()) {
$this->output_debug_info('footer');
return;
}
$this->output_manager->output_codes('footer');
try {
$this->output_manager->output_codes('footer');
} catch (Exception $e) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('WPTag Footer Code Output Error: ' . $e->getMessage());
}
}
}
private function output_debug_info($position) {
if (!defined('WP_DEBUG') || !WP_DEBUG || !current_user_can('manage_options')) {
return;
}
$debug_info = $this->get_debug_info();
$enabled_services = $this->config->get_enabled_services();
$settings = $this->config->get_settings();
$active_services = array();
foreach ($settings as $service_key => $service_settings) {
if (!empty($service_settings['enabled'])) {
$active_services[] = $service_key;
}
}
echo "\n<!-- WPTag Debug Info for {$position} -->\n";
echo "<!-- Enabled Services: " . implode(', ', $enabled_services) . " -->\n";
echo "<!-- Active Services: " . implode(', ', $active_services) . " -->\n";
echo "<!-- Should Load: " . ($this->should_load ? 'Yes' : 'No') . " -->\n";
echo "<!-- Should Output: " . ($this->should_output_codes() ? 'Yes' : 'No') . " -->\n";
echo "<!-- Is Admin: " . (is_admin() ? 'Yes' : 'No') . " -->\n";
echo "<!-- Is User Logged In: " . (is_user_logged_in() ? 'Yes' : 'No') . " -->\n";
echo "<!-- Current User Can Manage: " . (current_user_can('manage_options') ? 'Yes' : 'No') . " -->\n";
echo "<!-- End WPTag Debug Info -->\n";
}
private function should_output_codes() {
if (is_admin()) {
global $pagenow;
if (is_admin() || $pagenow === 'wp-admin/admin.php' || strpos($_SERVER['REQUEST_URI'] ?? '', '/wp-admin/') !== false) {
return false;
}
if (!$this->should_load) {
return false;
}
if (defined('DOING_AJAX') && DOING_AJAX) {
return false;
}
if (defined('DOING_CRON') && DOING_CRON) {
return false;
}
if (defined('REST_REQUEST') && REST_REQUEST) {
return false;
}
if (wp_doing_ajax()) {
return false;
}
if (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) {
return false;
}
if (is_user_logged_in() && current_user_can('manage_options')) {
$show_for_admin = apply_filters('wptag_show_for_admin', false);
$show_for_admin = apply_filters('wptag_show_for_admin', true);
if (!$show_for_admin) {
return false;
}
}
if ($this->is_bot_or_crawler()) {
$show_for_bots = apply_filters('wptag_show_for_bots', true);
if (!$show_for_bots) {
return false;
}
}
return apply_filters('wptag_should_output_codes', true);
}
private function determine_if_should_load() {
global $wp_query;
if (is_404()) {
return apply_filters('wptag_load_on_404', true);
}
if (is_search() && !have_posts()) {
return apply_filters('wptag_load_on_empty_search', true);
}
if (is_feed()) {
return apply_filters('wptag_load_on_feed', false);
}
if (is_trackback()) {
return apply_filters('wptag_load_on_trackback', false);
}
if (is_robots()) {
return apply_filters('wptag_load_on_robots', false);
}
if (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) {
return apply_filters('wptag_load_on_xmlrpc', false);
}
return apply_filters('wptag_should_load_frontend', true);
}
private function get_admin_preview_setting() {
return get_option('wptag_admin_preview', false);
}
private function is_bot_or_crawler() {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return false;
}
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
$bot_patterns = array(
'googlebot',
'bingbot',
'slurp',
'duckduckbot',
'baiduspider',
'yandexbot',
'facebookexternalhit',
'twitterbot',
'linkedinbot',
'whatsapp',
'telegrambot',
'applebot',
'crawler',
'spider',
'robot',
'bot/'
);
foreach ($bot_patterns as $pattern) {
if (strpos($user_agent, $pattern) !== false) {
return true;
}
}
return false;
}
public function filter_should_output_codes($should_output) {
if (!$should_output) {
return false;
}
$excluded_urls = $this->get_excluded_urls();
if (!empty($excluded_urls)) {
$current_url = $this->get_current_url();
foreach ($excluded_urls as $excluded_url) {
if ($this->url_matches_pattern($current_url, $excluded_url)) {
return false;
}
}
}
$excluded_post_types = $this->get_excluded_post_types();
if (!empty($excluded_post_types) && is_singular()) {
$post_type = get_post_type();
if (in_array($post_type, $excluded_post_types)) {
return false;
}
}
$excluded_user_roles = $this->get_excluded_user_roles();
if (!empty($excluded_user_roles) && is_user_logged_in()) {
$user = wp_get_current_user();
$user_roles = $user->roles;
foreach ($excluded_user_roles as $excluded_role) {
if (in_array($excluded_role, $user_roles)) {
return false;
}
}
}
return $should_output;
}
private function get_excluded_urls() {
$excluded = get_option('wptag_excluded_urls', array());
return is_array($excluded) ? $excluded : array();
}
private function get_excluded_post_types() {
$excluded = get_option('wptag_excluded_post_types', array());
return is_array($excluded) ? $excluded : array();
}
private function get_excluded_user_roles() {
$excluded = get_option('wptag_excluded_user_roles', array());
return is_array($excluded) ? $excluded : array();
}
private function get_current_url() {
return home_url(add_query_arg(array(), $_SERVER['REQUEST_URI']));
}
private function url_matches_pattern($url, $pattern) {
$pattern = trim($pattern);
if (empty($pattern)) {
return false;
}
if ($pattern === $url) {
return true;
}
if (strpos($pattern, '*') !== false) {
$pattern = str_replace('*', '.*', preg_quote($pattern, '/'));
return preg_match('/^' . $pattern . '$/i', $url);
}
if (strpos($url, $pattern) !== false) {
return true;
}
return false;
}
public function enqueue_frontend_assets() {
if (!$this->should_output_codes()) {
return;
}
do_action('wptag_enqueue_frontend_assets');
$custom_css = get_option('wptag_custom_css', '');
if (!empty($custom_css)) {
wp_add_inline_style('wp-block-library', $custom_css);
}
$custom_js = get_option('wptag_custom_js', '');
if (!empty($custom_js)) {
wp_add_inline_script('jquery', $custom_js);
}
}
public function get_output_manager() {
return $this->output_manager;
}
public function clear_cache() {
if ($this->output_manager) {
$this->output_manager->clear_cache();
}
wp_cache_set_last_changed('wptag_codes');
if (function_exists('wp_cache_flush_group')) {
wp_cache_flush_group('wptag');
}
}
public function get_debug_info() {
if (!current_user_can('manage_options')) {
return array();
}
return array(
'should_load' => $this->should_load,
'should_output_codes' => $this->should_output_codes(),
'is_admin' => is_admin(),
'is_ajax' => wp_doing_ajax(),
'is_user_logged_in' => is_user_logged_in(),
'current_user_can_manage' => current_user_can('manage_options'),
'is_bot' => $this->is_bot_or_crawler(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'current_url' => $this->get_current_url(),
'page_type' => $this->get_page_type_debug(),
'enabled_services' => count($this->config->get_enabled_services()),
'active_codes' => $this->count_active_codes()
);
}
private function get_page_type_debug() {
$types = array();
if (is_home()) $types[] = 'home';
if (is_front_page()) $types[] = 'front_page';
if (is_single()) $types[] = 'single';
if (is_page()) $types[] = 'page';
if (is_category()) $types[] = 'category';
if (is_tag()) $types[] = 'tag';
if (is_archive()) $types[] = 'archive';
if (is_search()) $types[] = 'search';
if (is_404()) $types[] = '404';
if (is_feed()) $types[] = 'feed';
return empty($types) ? 'unknown' : implode(', ', $types);
}
private function count_active_codes() {
$count = 0;
$settings = $this->config->get_settings();
foreach ($settings as $service_settings) {
if (!empty($service_settings['enabled'])) {
$count++;
}
}
return $count;
}
public function add_debug_output() {
if (!current_user_can('manage_options') || !apply_filters('wptag_show_debug_output', false)) {
return;
}
$debug_info = $this->get_debug_info();
echo "\n<!-- WPTag Debug Info -->\n";
echo "<!-- " . wp_json_encode($debug_info, JSON_PRETTY_PRINT) . " -->\n";
echo "<!-- End WPTag Debug Info -->\n";
}
public function handle_amp_compatibility() {
if (function_exists('is_amp_endpoint') && is_amp_endpoint()) {
remove_action('wp_head', array($this, 'output_head_codes'), 1);
remove_action('wp_body_open', array($this, 'output_body_codes'), 1);
remove_action('wp_footer', array($this, 'output_footer_codes'), 1);
add_action('amp_post_template_head', array($this, 'output_amp_codes'));
}
}
public function output_amp_codes() {
$settings = $this->config->get_settings();
$amp_compatible_services = array('google_analytics', 'google_tag_manager');
foreach ($settings as $service_key => $service_settings) {
if (!empty($service_settings['enabled']) && in_array($service_key, $amp_compatible_services)) {
$this->output_amp_service_code($service_key, $service_settings);
}
}
}
private function output_amp_service_code($service_key, $service_settings) {
if ($service_key === 'google_analytics' && !empty($service_settings['tracking_id'])) {
$tracking_id = $service_settings['tracking_id'];
if (strpos($tracking_id, 'G-') === 0) {
echo '<amp-analytics type="gtag" data-credentials="include">';
echo '<script type="application/json">';
echo wp_json_encode(array(
'gtag_id' => $tracking_id,
'config' => array(
$tracking_id => array(
'groups' => 'default'
)
)
));
echo '</script>';
echo '</amp-analytics>';
}
}
}
}

View file

@ -10,42 +10,436 @@ class Loader {
private $actions = array();
private $filters = array();
private $shortcodes = array();
private $registered = false;
public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
$this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
return $this;
}
public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
$this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
return $this;
}
public function add_shortcode($tag, $component, $callback) {
$this->shortcodes = $this->add($this->shortcodes, $tag, $component, $callback);
$this->shortcodes = $this->add($this->shortcodes, $tag, $component, $callback, 10, 1);
return $this;
}
private function add($hooks, $hook, $component, $callback, $priority = 10, $accepted_args = 1) {
if (!is_string($hook) || empty($hook)) {
return $hooks;
}
if (!is_object($component) && !is_string($component)) {
return $hooks;
}
if (!is_string($callback) || empty($callback)) {
return $hooks;
}
if (!is_int($priority)) {
$priority = 10;
}
if (!is_int($accepted_args) || $accepted_args < 1) {
$accepted_args = 1;
}
$hooks[] = array(
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args
'accepted_args' => $accepted_args,
'registered' => false
);
return $hooks;
}
public function run() {
foreach ($this->filters as $hook) {
add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
if ($this->registered) {
return false;
}
$this->register_filters();
$this->register_actions();
$this->register_shortcodes();
$this->registered = true;
return true;
}
private function register_filters() {
foreach ($this->filters as &$hook) {
if ($hook['registered']) {
continue;
}
if ($this->is_valid_hook($hook)) {
$callback = $this->get_callback($hook);
if ($callback) {
add_filter(
$hook['hook'],
$callback,
$hook['priority'],
$hook['accepted_args']
);
$hook['registered'] = true;
}
}
}
}
private function register_actions() {
foreach ($this->actions as &$hook) {
if ($hook['registered']) {
continue;
}
if ($this->is_valid_hook($hook)) {
$callback = $this->get_callback($hook);
if ($callback) {
add_action(
$hook['hook'],
$callback,
$hook['priority'],
$hook['accepted_args']
);
$hook['registered'] = true;
}
}
}
}
private function register_shortcodes() {
foreach ($this->shortcodes as &$hook) {
if ($hook['registered']) {
continue;
}
if ($this->is_valid_hook($hook)) {
$callback = $this->get_callback($hook);
if ($callback) {
add_shortcode($hook['hook'], $callback);
$hook['registered'] = true;
}
}
}
}
private function is_valid_hook($hook) {
if (!is_array($hook)) {
return false;
}
$required_keys = array('hook', 'component', 'callback');
foreach ($required_keys as $key) {
if (!isset($hook[$key])) {
return false;
}
}
return true;
}
private function get_callback($hook) {
$component = $hook['component'];
$callback = $hook['callback'];
if (is_object($component)) {
if (method_exists($component, $callback)) {
return array($component, $callback);
}
} elseif (is_string($component)) {
if (class_exists($component)) {
$instance = new $component();
if (method_exists($instance, $callback)) {
return array($instance, $callback);
}
} elseif (function_exists($component)) {
return $component;
}
}
if (function_exists($callback)) {
return $callback;
}
return false;
}
public function remove_action($hook, $component, $callback, $priority = 10) {
return $this->remove_hook($this->actions, $hook, $component, $callback, $priority, 'action');
}
public function remove_filter($hook, $component, $callback, $priority = 10) {
return $this->remove_hook($this->filters, $hook, $component, $callback, $priority, 'filter');
}
public function remove_shortcode($tag) {
foreach ($this->shortcodes as $key => $hook) {
if ($hook['hook'] === $tag) {
if ($hook['registered']) {
remove_shortcode($tag);
}
unset($this->shortcodes[$key]);
return true;
}
}
return false;
}
private function remove_hook(&$hooks, $hook_name, $component, $callback, $priority, $type) {
$removed = false;
foreach ($hooks as $key => $hook) {
if ($hook['hook'] === $hook_name &&
$hook['component'] === $component &&
$hook['callback'] === $callback &&
$hook['priority'] === $priority) {
if ($hook['registered']) {
$wp_callback = $this->get_callback($hook);
if ($wp_callback) {
if ($type === 'action') {
remove_action($hook_name, $wp_callback, $priority);
} else {
remove_filter($hook_name, $wp_callback, $priority);
}
}
}
unset($hooks[$key]);
$removed = true;
}
}
return $removed;
}
public function get_actions() {
return $this->actions;
}
public function get_filters() {
return $this->filters;
}
public function get_shortcodes() {
return $this->shortcodes;
}
public function get_registered_actions() {
return array_filter($this->actions, function($hook) {
return $hook['registered'];
});
}
public function get_registered_filters() {
return array_filter($this->filters, function($hook) {
return $hook['registered'];
});
}
public function get_registered_shortcodes() {
return array_filter($this->shortcodes, function($hook) {
return $hook['registered'];
});
}
public function is_registered() {
return $this->registered;
}
public function get_hook_count() {
return array(
'actions' => count($this->actions),
'filters' => count($this->filters),
'shortcodes' => count($this->shortcodes),
'total' => count($this->actions) + count($this->filters) + count($this->shortcodes)
);
}
public function get_registered_count() {
$registered_actions = count($this->get_registered_actions());
$registered_filters = count($this->get_registered_filters());
$registered_shortcodes = count($this->get_registered_shortcodes());
return array(
'actions' => $registered_actions,
'filters' => $registered_filters,
'shortcodes' => $registered_shortcodes,
'total' => $registered_actions + $registered_filters + $registered_shortcodes
);
}
public function has_hook($hook_name, $type = 'all') {
$hooks_to_check = array();
switch ($type) {
case 'action':
$hooks_to_check = $this->actions;
break;
case 'filter':
$hooks_to_check = $this->filters;
break;
case 'shortcode':
$hooks_to_check = $this->shortcodes;
break;
case 'all':
default:
$hooks_to_check = array_merge($this->actions, $this->filters, $this->shortcodes);
break;
}
foreach ($hooks_to_check as $hook) {
if ($hook['hook'] === $hook_name) {
return true;
}
}
return false;
}
public function get_hooks_by_priority($priority = 10) {
$all_hooks = array_merge($this->actions, $this->filters, $this->shortcodes);
return array_filter($all_hooks, function($hook) use ($priority) {
return isset($hook['priority']) && $hook['priority'] === $priority;
});
}
public function get_hooks_by_component($component) {
$all_hooks = array_merge($this->actions, $this->filters, $this->shortcodes);
return array_filter($all_hooks, function($hook) use ($component) {
return $hook['component'] === $component;
});
}
public function clear_all_hooks() {
$this->unregister_all();
$this->actions = array();
$this->filters = array();
$this->shortcodes = array();
$this->registered = false;
return true;
}
private function unregister_all() {
foreach ($this->actions as $hook) {
add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
if ($hook['registered']) {
$callback = $this->get_callback($hook);
if ($callback) {
remove_action($hook['hook'], $callback, $hook['priority']);
}
}
}
foreach ($this->filters as $hook) {
if ($hook['registered']) {
$callback = $this->get_callback($hook);
if ($callback) {
remove_filter($hook['hook'], $callback, $hook['priority']);
}
}
}
foreach ($this->shortcodes as $hook) {
add_shortcode($hook['hook'], array($hook['component'], $hook['callback']));
if ($hook['registered']) {
remove_shortcode($hook['hook']);
}
}
}
public function debug_info() {
if (!current_user_can('manage_options')) {
return array();
}
return array(
'registered' => $this->registered,
'hook_counts' => $this->get_hook_count(),
'registered_counts' => $this->get_registered_count(),
'actions' => $this->format_hooks_for_debug($this->actions),
'filters' => $this->format_hooks_for_debug($this->filters),
'shortcodes' => $this->format_hooks_for_debug($this->shortcodes)
);
}
private function format_hooks_for_debug($hooks) {
$formatted = array();
foreach ($hooks as $hook) {
$component_name = is_object($hook['component']) ? get_class($hook['component']) : $hook['component'];
$formatted[] = array(
'hook' => $hook['hook'],
'component' => $component_name,
'callback' => $hook['callback'],
'priority' => $hook['priority'],
'accepted_args' => $hook['accepted_args'],
'registered' => $hook['registered']
);
}
return $formatted;
}
public function validate_hooks() {
$issues = array();
$all_hooks = array_merge(
array_map(function($h) { $h['type'] = 'action'; return $h; }, $this->actions),
array_map(function($h) { $h['type'] = 'filter'; return $h; }, $this->filters),
array_map(function($h) { $h['type'] = 'shortcode'; return $h; }, $this->shortcodes)
);
foreach ($all_hooks as $index => $hook) {
if (!$this->is_valid_hook($hook)) {
$issues[] = array(
'index' => $index,
'type' => $hook['type'],
'issue' => 'Invalid hook structure',
'hook' => $hook
);
continue;
}
if (!$this->get_callback($hook)) {
$issues[] = array(
'index' => $index,
'type' => $hook['type'],
'issue' => 'Invalid callback or component',
'hook' => $hook['hook'],
'component' => is_object($hook['component']) ? get_class($hook['component']) : $hook['component'],
'callback' => $hook['callback']
);
}
if ($hook['priority'] < 1 || $hook['priority'] > 999) {
$issues[] = array(
'index' => $index,
'type' => $hook['type'],
'issue' => 'Priority out of recommended range (1-999)',
'hook' => $hook['hook'],
'priority' => $hook['priority']
);
}
}
return $issues;
}
public function __destruct() {
if ($this->registered) {
$this->unregister_all();
}
}
}

View file

@ -182,12 +182,12 @@ amplitude.getInstance().init("{ID}");
'matomo' => '<!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq = window._paq || [];
_paq.push([\'trackPageView\']);
_paq.push([\'enableLinkTracking\']);
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function() {
var u="//your-matomo-domain.com/";
_paq.push([\'setTrackerUrl\', u+"matomo.php"]);
_paq.push([\'setSiteId\', "{ID}"]);
_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);
})();
@ -196,26 +196,40 @@ amplitude.getInstance().init("{ID}");
}
public function get_codes_for_position($position) {
if (isset($this->cached_codes[$position])) {
return $this->cached_codes[$position];
$cache_key = $position . '_' . wp_cache_get_last_changed('wptag_codes');
if (isset($this->cached_codes[$cache_key])) {
return $this->cached_codes[$cache_key];
}
$codes = array();
$all_settings = $this->config->get_settings();
if (empty($all_settings)) {
return array();
}
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
);
$service_codes = $this->get_service_codes($service_key, $service_settings, $position);
if (!empty($service_codes)) {
$priority = intval($service_settings['priority'] ?? 10);
if (!isset($codes[$priority])) {
$codes[$priority] = array();
}
foreach ($service_codes as $code) {
if (!empty(trim($code))) {
$codes[$priority][] = array(
'service' => $service_key,
'code' => $code,
'priority' => $priority
);
}
}
}
}
@ -228,20 +242,20 @@ amplitude.getInstance().init("{ID}");
}
}
$this->cached_codes[$position] = $output_codes;
$this->cached_codes[$cache_key] = $output_codes;
return $output_codes;
}
private function should_output_service($service_key, $service_settings, $position) {
if (!$service_settings['enabled']) {
if (empty($service_settings['enabled'])) {
return false;
}
if ($service_settings['position'] !== $position) {
if (!$this->check_position_condition($service_key, $service_settings, $position)) {
return false;
}
if (!$this->check_device_condition($service_settings['device'])) {
if (!$this->check_device_condition($service_settings['device'] ?? 'all')) {
return false;
}
@ -249,9 +263,35 @@ amplitude.getInstance().init("{ID}");
return false;
}
$service_config = $this->config->get_service_config($service_key);
if (!$service_config) {
return false;
}
if (!empty($service_settings['use_template'])) {
$field_key = $service_config['field'];
$id_value = trim($service_settings[$field_key] ?? '');
if (empty($id_value)) {
return false;
}
} else {
$custom_code = trim($service_settings['custom_code'] ?? '');
if (empty($custom_code)) {
return false;
}
}
return apply_filters('wptag_should_output_service', true, $service_key, $service_settings, $position);
}
private function check_position_condition($service_key, $service_settings, $position) {
if ($service_key === 'google_tag_manager') {
return ($position === 'head' || $position === 'body');
}
return $service_settings['position'] === $position;
}
private function check_device_condition($device_setting) {
if ($device_setting === 'all') {
return true;
@ -372,40 +412,51 @@ amplitude.getInstance().init("{ID}");
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);
private function get_service_codes($service_key, $service_settings, $position) {
if (!empty($service_settings['use_template'])) {
return $this->get_template_codes($service_key, $service_settings, $position);
} else {
return $service_settings['custom_code'];
$custom_code = trim($service_settings['custom_code'] ?? '');
if (!empty($custom_code)) {
return array($custom_code);
}
return array();
}
}
private function get_template_code($service_key, $service_settings, $position) {
private function get_template_codes($service_key, $service_settings, $position) {
$service_config = $this->config->get_service_config($service_key);
if (!$service_config) {
return '';
return array();
}
$template = $this->get_template_for_service($service_key, $service_settings, $position);
if (empty($template)) {
return '';
$templates = $this->get_templates_for_service($service_key, $service_settings, $position);
if (empty($templates)) {
return array();
}
$field_key = $service_config['field'];
$id_value = $service_settings[$field_key] ?? '';
if (empty($id_value)) {
return '';
return array();
}
$code = str_replace('{ID}', esc_attr($id_value), $template);
$codes = array();
foreach ($templates as $template) {
$code = str_replace('{ID}', esc_attr($id_value), $template);
$code = apply_filters('wptag_template_code', $code, $service_key, $service_settings, $position);
if (!empty($code)) {
$codes[] = $code;
}
}
return apply_filters('wptag_template_code', $code, $service_key, $service_settings, $position);
return $codes;
}
private function get_template_for_service($service_key, $service_settings, $position) {
private function get_templates_for_service($service_key, $service_settings, $position) {
if (!isset($this->templates[$service_key])) {
return '';
return array();
}
$template_data = $this->templates[$service_key];
@ -416,51 +467,72 @@ amplitude.getInstance().init("{ID}");
$id_value = $service_settings[$field_key] ?? '';
if (strpos($id_value, 'G-') === 0) {
return $template_data['G-'];
return array($template_data['G-']);
} elseif (strpos($id_value, 'UA-') === 0) {
return $template_data['UA-'];
return array($template_data['UA-']);
}
return '';
return array();
}
if ($service_key === 'google_tag_manager') {
if ($position === 'head') {
return $template_data['head'];
} elseif ($position === 'body') {
return $template_data['body'];
if ($position === 'head' && isset($template_data['head'])) {
return array($template_data['head']);
} elseif ($position === 'body' && isset($template_data['body'])) {
return array($template_data['body']);
}
return '';
return array();
}
if (is_array($template_data)) {
return $template_data['default'] ?? '';
if (isset($template_data['default'])) {
return array($template_data['default']);
}
return array();
}
return $template_data;
return array($template_data);
}
public function output_codes($position) {
$codes = $this->get_codes_for_position($position);
if (empty($codes)) {
if (defined('WP_DEBUG') && WP_DEBUG && current_user_can('manage_options')) {
echo "\n<!-- WPTag Debug: No codes found for position: {$position} -->\n";
}
return;
}
$output = "\n<!-- WPTag Codes - Position: {$position} -->\n";
foreach ($codes as $code) {
$output .= $code . "\n";
foreach ($codes as $index => $code) {
$output .= $code;
if ($index < count($codes) - 1) {
$output .= "\n";
}
}
$output .= "<!-- End WPTag Codes -->\n";
$output .= "\n<!-- End WPTag Codes -->\n";
echo apply_filters('wptag_output_codes', $output, $position);
$final_output = apply_filters('wptag_output_codes', $output, $position, $codes);
if (defined('WP_DEBUG') && WP_DEBUG && current_user_can('manage_options')) {
$debug_info = sprintf(
"\n<!-- WPTag Debug: %d codes output for position: %s -->\n",
count($codes),
$position
);
echo $debug_info;
}
echo $final_output;
}
public function clear_cache() {
$this->cached_codes = array();
wp_cache_set_last_changed('wptag_codes');
}
public function get_template_preview($service_key, $id_value) {
@ -469,11 +541,79 @@ amplitude.getInstance().init("{ID}");
return '';
}
$template = $this->get_template_for_service($service_key, array($service_config['field'] => $id_value), 'head');
if (empty($template)) {
$fake_settings = array(
$service_config['field'] => $id_value,
'use_template' => true,
'enabled' => true,
'position' => 'head'
);
$templates = $this->get_templates_for_service($service_key, $fake_settings, 'head');
if (empty($templates)) {
return '';
}
return str_replace('{ID}', esc_attr($id_value), $template);
$preview_parts = array();
foreach ($templates as $template) {
$code = str_replace('{ID}', esc_attr($id_value), $template);
if (!empty($code)) {
$preview_parts[] = $code;
}
}
return implode("\n\n", $preview_parts);
}
public function get_service_stats() {
$stats = array(
'enabled_services' => 0,
'total_codes' => 0,
'by_position' => array(
'head' => 0,
'body' => 0,
'footer' => 0
),
'by_type' => array(
'template' => 0,
'custom' => 0
)
);
$all_settings = $this->config->get_settings();
foreach ($all_settings as $service_key => $service_settings) {
if (!empty($service_settings['enabled'])) {
$stats['enabled_services']++;
$stats['total_codes']++;
$position = $service_settings['position'] ?? 'head';
if (isset($stats['by_position'][$position])) {
$stats['by_position'][$position]++;
}
if (!empty($service_settings['use_template'])) {
$stats['by_type']['template']++;
} else {
$stats['by_type']['custom']++;
}
}
}
return $stats;
}
public function validate_template($service_key, $id_value) {
$service_config = $this->config->get_service_config($service_key);
if (!$service_config) {
return false;
}
$fake_settings = array(
$service_config['field'] => $id_value,
'use_template' => true
);
$templates = $this->get_templates_for_service($service_key, $fake_settings, 'head');
return !empty($templates);
}
}

View file

@ -15,31 +15,41 @@ class Validator {
}
public function validate_service_code($service_key, $settings) {
$this->errors = array();
$this->clear_errors();
if (empty($service_key)) {
$this->add_error('empty_service_key', 'Service key cannot be empty');
return false;
}
$service_config = $this->config->get_service_config($service_key);
if (!$service_config) {
$this->add_error('service_not_found', 'Service configuration not found');
$this->add_error('service_not_found', 'Service configuration not found for: ' . $service_key);
return false;
}
if (!$settings['enabled']) {
if (!is_array($settings)) {
$this->add_error('invalid_settings_format', 'Settings must be an array');
return false;
}
if (empty($settings['enabled'])) {
return true;
}
if ($settings['use_template']) {
if (!empty($settings['use_template'])) {
return $this->validate_template_code($service_key, $settings, $service_config);
} else {
return $this->validate_custom_code($settings['custom_code']);
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] ?? '';
$id_value = trim($settings[$field_key] ?? '');
if (empty($id_value)) {
$this->add_error('empty_id', 'ID field cannot be empty');
$this->add_error('empty_id', sprintf('The %s field cannot be empty', ucfirst(str_replace('_', ' ', $field_key))));
return false;
}
@ -47,6 +57,10 @@ class Validator {
return false;
}
if (!$this->validate_id_accessibility($service_key, $id_value)) {
return false;
}
return true;
}
@ -58,44 +72,139 @@ class Validator {
}
if (!preg_match($pattern, $id_value)) {
$this->add_error('invalid_id_format', sprintf('Invalid ID format for %s', $service_config['name']));
$this->add_error(
'invalid_id_format',
sprintf(
'Invalid %s format. Expected format: %s',
$service_config['name'],
$service_config['placeholder']
)
);
return false;
}
if ($service_key === 'google_analytics') {
return $this->validate_google_analytics_id($id_value);
}
return $this->validate_service_specific_rules($service_key, $id_value);
}
return true;
private function validate_service_specific_rules($service_key, $id_value) {
switch ($service_key) {
case 'google_analytics':
return $this->validate_google_analytics_id($id_value);
case 'google_tag_manager':
return $this->validate_gtm_id($id_value);
case 'facebook_pixel':
return $this->validate_facebook_pixel_id($id_value);
case 'google_ads':
return $this->validate_google_ads_id($id_value);
case 'matomo':
return $this->validate_matomo_id($id_value);
default:
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');
$this->add_error('invalid_ga4_format', 'Invalid Google Analytics 4 ID format. Should be G- followed by 10 alphanumeric characters.');
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');
$this->add_error('invalid_ua_format', 'Invalid Universal Analytics ID format. Should be UA-XXXXXX-X.');
return false;
}
} else {
$this->add_error('invalid_ga_format', 'Google Analytics ID must start with G- or UA-');
$this->add_error('invalid_ga_format', 'Google Analytics ID must start with G- (GA4) or UA- (Universal Analytics).');
return false;
}
return true;
}
private function validate_gtm_id($id_value) {
if (!preg_match('/^GTM-[A-Z0-9]{7}$/', $id_value)) {
$this->add_error('invalid_gtm_format', 'Invalid Google Tag Manager ID format. Should be GTM- followed by 7 alphanumeric characters.');
return false;
}
return true;
}
private function validate_facebook_pixel_id($id_value) {
if (!preg_match('/^[0-9]{15}$/', $id_value)) {
$this->add_error('invalid_fb_pixel_format', 'Invalid Facebook Pixel ID format. Should be 15 digits.');
return false;
}
return true;
}
private function validate_google_ads_id($id_value) {
if (!preg_match('/^AW-[0-9]{10}$/', $id_value)) {
$this->add_error('invalid_gads_format', 'Invalid Google Ads ID format. Should be AW- followed by 10 digits.');
return false;
}
return true;
}
private function validate_matomo_id($id_value) {
if (!preg_match('/^[0-9]+$/', $id_value)) {
$this->add_error('invalid_matomo_format', 'Invalid Matomo Site ID format. Should be numeric.');
return false;
}
$site_id = intval($id_value);
if ($site_id < 1 || $site_id > 999999) {
$this->add_error('invalid_matomo_range', 'Matomo Site ID should be between 1 and 999999.');
return false;
}
return true;
}
private function validate_id_accessibility($service_key, $id_value) {
$blocked_ids = $this->get_blocked_ids($service_key);
if (in_array($id_value, $blocked_ids)) {
$this->add_error('blocked_id', 'This ID is not allowed for security reasons.');
return false;
}
return true;
}
private function get_blocked_ids($service_key) {
$blocked_ids = array();
switch ($service_key) {
case 'google_analytics':
$blocked_ids = array('G-XXXXXXXXXX', 'UA-XXXXXXX-X', 'G-TEST123456', 'UA-123456-1');
break;
case 'google_tag_manager':
$blocked_ids = array('GTM-XXXXXXX', 'GTM-TEST123');
break;
case 'facebook_pixel':
$blocked_ids = array('123456789012345', '000000000000000');
break;
}
return apply_filters('wptag_blocked_ids', $blocked_ids, $service_key);
}
private function validate_custom_code($custom_code) {
$custom_code = trim($custom_code);
if (empty($custom_code)) {
$this->add_error('empty_custom_code', 'Custom code cannot be empty');
$this->add_error('empty_custom_code', 'Custom code cannot be empty when enabled');
return false;
}
if (strlen($custom_code) > 50000) {
$this->add_error('code_too_long', 'Custom code is too long (max 50,000 characters)');
$this->add_error('code_too_long', 'Custom code is too long (maximum 50,000 characters)');
return false;
}
if (strlen($custom_code) < 10) {
$this->add_error('code_too_short', 'Custom code seems too short to be valid tracking code');
return false;
}
@ -107,19 +216,28 @@ class Validator {
return false;
}
if (!$this->validate_code_syntax($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;
$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');
$this->add_error('mismatched_script_tags', 'Mismatched script tags detected. Each <script> must have a closing </script>');
return false;
}
if (preg_match('/<script[^>]*>.*?<script/is', $custom_code)) {
$this->add_error('nested_script_tags', 'Nested script tags are not allowed');
return false;
}
}
@ -129,7 +247,7 @@ class Validator {
$noscript_close_count = substr_count($custom_code, '</noscript>');
if ($noscript_open_count !== $noscript_close_count) {
$this->add_error('mismatched_noscript_tags', 'Mismatched noscript tags');
$this->add_error('mismatched_noscript_tags', 'Mismatched noscript tags detected. Each <noscript> must have a closing </noscript>');
return false;
}
}
@ -139,19 +257,22 @@ class Validator {
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',
'/\beval\s*\(/i' => 'eval() function is not allowed for security reasons',
'/\bFunction\s*\(/i' => 'Function() constructor is not allowed for security reasons',
'/\bsetTimeout\s*\(\s*["\'][^"\']*["\']/i' => 'setTimeout with string argument is not allowed for security reasons',
'/\bsetInterval\s*\(\s*["\'][^"\']*["\']/i' => 'setInterval with string argument is not allowed for security reasons',
'/\bdocument\.write\s*\(/i' => 'document.write() is discouraged and may not work properly',
'/\bwindow\.location\s*=\s*["\'][^"\']*["\']/i' => 'Redirecting window.location is not allowed',
'/\bwindow\.open\s*\(/i' => 'window.open() is not allowed for security reasons',
'/\balert\s*\(/i' => 'alert() is not allowed in tracking codes',
'/\bconfirm\s*\(/i' => 'confirm() is not allowed in tracking codes',
'/\bprompt\s*\(/i' => 'prompt() is not allowed in tracking codes',
'/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'
'/\<\s*iframe[^>]*src\s*=\s*["\']?javascript:/i' => 'javascript: protocol in iframe src is not allowed',
'/\<\s*object[^>]*data\s*=\s*["\']?javascript:/i' => 'javascript: protocol in object data is not allowed',
'/\<\s*embed[^>]*src\s*=\s*["\']?javascript:/i' => 'javascript: protocol in embed src is not allowed',
'/\bExecScript\s*\(/i' => 'ExecScript is not allowed for security reasons',
'/\bexecCommand\s*\(/i' => 'execCommand is not allowed for security reasons'
);
foreach ($dangerous_patterns as $pattern => $message) {
@ -161,18 +282,167 @@ class Validator {
}
}
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'
);
if (!$this->validate_external_domains($custom_code)) {
return false;
}
return true;
}
private function validate_external_domains($custom_code) {
if (preg_match_all('/https?:\/\/([^\/\s"\'<>]+)/i', $custom_code, $matches)) {
$domains = array_unique($matches[1]);
$allowed_domains = $this->get_allowed_domains();
$suspicious_domains = $this->get_suspicious_domains();
foreach ($domains as $domain) {
if (in_array(strtolower($domain), $suspicious_domains)) {
$domain = strtolower(trim($domain));
if (in_array($domain, $suspicious_domains)) {
$this->add_error('suspicious_domain', 'Suspicious domain detected: ' . $domain);
return false;
}
if (!empty($allowed_domains) && !$this->is_domain_allowed($domain, $allowed_domains)) {
$this->add_error('unauthorized_domain', 'Domain not in allowed list: ' . $domain);
return false;
}
}
}
return true;
}
private function get_allowed_domains() {
$default_allowed = array(
'googletagmanager.com',
'google-analytics.com',
'googleadservices.com',
'facebook.com',
'facebook.net',
'connect.facebook.net',
'hotjar.com',
'clarity.ms',
'tiktok.com',
'linkedin.com',
'twitter.com',
'pinterest.com',
'snapchat.com',
'googleoptimize.com',
'crazyegg.com',
'mixpanel.com',
'amplitude.com',
'ads-twitter.com',
'pinimg.com',
'licdn.com',
'sc-static.net',
'snap.licdn.com',
'cdn.amplitude.com',
'cdn4.mxpnl.com',
'script.crazyegg.com'
);
return apply_filters('wptag_allowed_domains', $default_allowed);
}
private function get_suspicious_domains() {
$suspicious = array(
'bit.ly',
'tinyurl.com',
'goo.gl',
't.co',
'ow.ly',
'malware.com',
'virus.com',
'phishing.com',
'suspicious.com',
'malicious.com',
'hack.com',
'exploit.com'
);
return apply_filters('wptag_suspicious_domains', $suspicious);
}
private function is_domain_allowed($domain, $allowed_domains) {
foreach ($allowed_domains as $allowed) {
if ($domain === $allowed || strpos($domain, '.' . $allowed) !== false) {
return true;
}
}
return false;
}
private function validate_code_syntax($custom_code) {
if (preg_match('/<script[^>]*>/i', $custom_code)) {
$js_content = preg_replace('/<script[^>]*>(.*?)<\/script>/is', '$1', $custom_code);
if (!empty($js_content) && !$this->is_valid_javascript_syntax($js_content)) {
$this->add_error('invalid_js_syntax', 'JavaScript syntax appears to be invalid');
return false;
}
}
if (!$this->validate_html_structure($custom_code)) {
return false;
}
return true;
}
private function is_valid_javascript_syntax($js_code) {
$js_code = trim($js_code);
if (empty($js_code)) {
return true;
}
$basic_checks = array(
'balanced_parentheses' => $this->check_balanced_chars($js_code, '(', ')'),
'balanced_brackets' => $this->check_balanced_chars($js_code, '[', ']'),
'balanced_braces' => $this->check_balanced_chars($js_code, '{', '}'),
'no_unclosed_strings' => $this->check_unclosed_strings($js_code)
);
foreach ($basic_checks as $check => $result) {
if (!$result) {
return false;
}
}
return true;
}
private function check_balanced_chars($code, $open, $close) {
$open_count = substr_count($code, $open);
$close_count = substr_count($code, $close);
return $open_count === $close_count;
}
private function check_unclosed_strings($code) {
$single_quotes = substr_count($code, "'");
$double_quotes = substr_count($code, '"');
return ($single_quotes % 2 === 0) && ($double_quotes % 2 === 0);
}
private function validate_html_structure($custom_code) {
$doc = new \DOMDocument();
libxml_use_internal_errors(true);
$result = $doc->loadHTML('<div>' . $custom_code . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$errors = libxml_get_errors();
libxml_clear_errors();
if (!$result || !empty($errors)) {
$severe_errors = array_filter($errors, function($error) {
return $error->level === LIBXML_ERR_ERROR || $error->level === LIBXML_ERR_FATAL;
});
if (!empty($severe_errors)) {
$this->add_error('invalid_html_structure', 'HTML structure contains errors');
return false;
}
}
@ -180,7 +450,7 @@ class Validator {
}
public function validate_settings($settings) {
$this->errors = array();
$this->clear_errors();
if (!is_array($settings)) {
$this->add_error('invalid_settings_format', 'Settings must be an array');
@ -199,6 +469,11 @@ class Validator {
}
private function validate_service_settings($service_key, $service_settings) {
if (!is_array($service_settings)) {
$this->add_error($service_key . '_invalid_format', 'Service settings must be an array for ' . $service_key);
return false;
}
$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);
@ -207,55 +482,52 @@ class Validator {
$valid = true;
if (!$this->validate_boolean($service_settings['enabled'] ?? false)) {
$this->add_error($service_key . '_enabled_invalid', 'Enabled setting must be boolean');
$valid = false;
}
$checks = array(
'enabled' => array($service_settings['enabled'] ?? false, 'boolean'),
'use_template' => array($service_settings['use_template'] ?? true, 'boolean'),
'position' => array($service_settings['position'] ?? 'head', 'position'),
'priority' => array($service_settings['priority'] ?? 10, 'priority'),
'device' => array($service_settings['device'] ?? 'all', 'device')
);
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;
foreach ($checks as $field => $check_data) {
list($value, $type) = $check_data;
if (!$this->validate_field_type($value, $type)) {
$this->add_error(
$service_key . '_' . $field . '_invalid',
sprintf('Invalid %s setting for %s', $field, $service_key)
);
$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);
private function validate_field_type($value, $type) {
switch ($type) {
case 'boolean':
return is_bool($value) || in_array($value, array('1', '0', 1, 0), true);
case 'position':
return in_array($value, array('head', 'body', 'footer'));
case 'priority':
$priority = intval($value);
return $priority >= 1 && $priority <= 100;
case 'device':
return in_array($value, array('all', 'desktop', 'mobile'));
default:
return true;
}
}
public function validate_import_data($json_data) {
$this->errors = array();
$this->clear_errors();
if (empty($json_data)) {
$this->add_error('empty_import_data', 'Import data cannot be empty');
return false;
}
$data = json_decode($json_data, true);
@ -265,12 +537,17 @@ class Validator {
}
if (!is_array($data)) {
$this->add_error('invalid_data_type', 'Import data must be an array');
$this->add_error('invalid_data_type', 'Import data must be an object');
return false;
}
if (!isset($data['settings'])) {
$this->add_error('missing_settings', 'Import data missing settings');
$this->add_error('missing_settings', 'Import data missing settings section');
return false;
}
if (!is_array($data['settings'])) {
$this->add_error('invalid_settings_format', 'Settings must be an object');
return false;
}
@ -286,25 +563,44 @@ class Validator {
}
}
return $this->validate_settings($data['settings']);
return $this->validate_imported_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');
if (!is_string($version) || empty($version)) {
$this->add_error('invalid_version_format', 'Version must be a non-empty string');
return false;
}
$import_major = intval($import_version_parts[0]);
$current_major = intval($current_version_parts[0]);
if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
$this->add_error('invalid_version_format', 'Version must be in format X.Y.Z');
return false;
}
if ($import_major > $current_major) {
$this->add_error('version_incompatible', 'Import data is from a newer version and may not be compatible');
$current_version = WPTAG_VERSION;
if (version_compare($version, $current_version, '>')) {
$this->add_error(
'version_incompatible',
sprintf(
'Import data is from a newer version (%s) and may not be compatible with current version (%s)',
$version,
$current_version
)
);
return false;
}
$min_compatible_version = '1.0.0';
if (version_compare($version, $min_compatible_version, '<')) {
$this->add_error(
'version_too_old',
sprintf(
'Import data is from an incompatible version (%s). Minimum supported version is %s',
$version,
$min_compatible_version
)
);
return false;
}
@ -317,9 +613,19 @@ class Validator {
return false;
}
if (empty($services)) {
$this->add_error('empty_services_list', 'Services list cannot be empty');
return false;
}
$all_services = $this->config->get_all_services();
foreach ($services as $service_key) {
if (!is_string($service_key) || empty($service_key)) {
$this->add_error('invalid_service_key', 'Service key must be a non-empty string');
return false;
}
if (!isset($all_services[$service_key])) {
$this->add_error('unknown_service', 'Unknown service: ' . $service_key);
return false;
@ -329,10 +635,23 @@ class Validator {
return true;
}
private function validate_imported_settings($settings) {
$imported_services = array_keys($settings);
$max_services = 50;
if (count($imported_services) > $max_services) {
$this->add_error('too_many_services', sprintf('Cannot import more than %d services', $max_services));
return false;
}
return $this->validate_settings($settings);
}
private function add_error($code, $message) {
$this->errors[] = array(
'code' => $code,
'message' => $message
'message' => $message,
'timestamp' => current_time('mysql')
);
}
@ -344,6 +663,10 @@ class Validator {
return array_column($this->errors, 'message');
}
public function get_error_codes() {
return array_column($this->errors, 'code');
}
public function has_errors() {
return !empty($this->errors);
}
@ -363,6 +686,16 @@ class Validator {
return count($this->errors);
}
public function get_errors_by_code($code) {
return array_filter($this->errors, function($error) use ($code) {
return $error['code'] === $code;
});
}
public function has_error_code($code) {
return !empty($this->get_errors_by_code($code));
}
public function validate_tracking_id($service_key, $id_value) {
$service_config = $this->config->get_service_config($service_key);
if (!$service_config) {
@ -374,13 +707,12 @@ class Validator {
return true;
}
return preg_match($pattern, $id_value);
return preg_match($pattern, $id_value) === 1;
}
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;
@ -390,4 +722,23 @@ class Validator {
$service_config = $this->config->get_service_config($service_key);
return $service_config['validation_pattern'] ?? null;
}
public function get_validation_help($service_key) {
$service_config = $this->config->get_service_config($service_key);
if (!$service_config) {
return '';
}
$help_texts = array(
'google_analytics' => 'Enter your Google Analytics tracking ID. GA4 IDs start with "G-" followed by 10 characters. Universal Analytics IDs start with "UA-" followed by numbers.',
'google_tag_manager' => 'Enter your Google Tag Manager container ID. It starts with "GTM-" followed by 7 characters.',
'facebook_pixel' => 'Enter your Facebook Pixel ID. It should be a 15-digit number.',
'google_ads' => 'Enter your Google Ads conversion ID. It starts with "AW-" followed by 10 digits.',
'microsoft_clarity' => 'Enter your Microsoft Clarity project ID. It should be 10 alphanumeric characters.',
'hotjar' => 'Enter your Hotjar site ID. It should be a 7-digit number.',
'matomo' => 'Enter your Matomo site ID. It should be a positive number.'
);
return $help_texts[$service_key] ?? sprintf('Enter your %s ID in the correct format.', $service_config['name']);
}
}

View file

@ -1,16 +1,295 @@
<?php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
delete_option( 'universal_tracking_codes_settings' );
if (!current_user_can('activate_plugins')) {
exit;
}
if ( is_multisite() ) {
$sites = get_sites();
foreach ( $sites as $site ) {
switch_to_blog( $site->blog_id );
delete_option( 'universal_tracking_codes_settings' );
restore_current_blog();
if (!defined('WPTAG_VERSION')) {
define('WPTAG_VERSION', '1.0.0');
}
class WPTag_Uninstaller {
private static $options_to_delete = array(
'wptag_settings',
'wptag_services',
'wptag_version',
'wptag_admin_preview',
'wptag_excluded_urls',
'wptag_excluded_post_types',
'wptag_excluded_user_roles',
'wptag_custom_css',
'wptag_custom_js',
'wptag_debug_mode',
'wptag_cache_enabled',
'wptag_last_cleanup'
);
private static $transients_to_delete = array(
'wptag_admin_notices_',
'wptag_validation_cache_',
'wptag_output_cache_',
'wptag_stats_cache'
);
private static $user_meta_to_delete = array(
'wptag_dismissed_notices',
'wptag_last_settings_view',
'wptag_preferred_tab'
);
private static $capabilities_to_remove = array(
'wptag_manage_codes',
'wptag_manage_services'
);
public static function uninstall() {
global $wpdb;
do_action('wptag_before_uninstall');
self::delete_options();
self::delete_transients();
self::delete_user_meta();
self::remove_capabilities();
self::clear_cache();
self::cleanup_database();
if (is_multisite()) {
self::cleanup_multisite();
}
do_action('wptag_after_uninstall');
self::log_uninstall();
}
}
private static function delete_options() {
foreach (self::$options_to_delete as $option) {
delete_option($option);
}
$wpdb = $GLOBALS['wpdb'];
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
'wptag_%'
)
);
}
private static function delete_transients() {
$wpdb = $GLOBALS['wpdb'];
foreach (self::$transients_to_delete as $transient_prefix) {
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
'_transient_' . $transient_prefix . '%',
'_transient_timeout_' . $transient_prefix . '%'
)
);
}
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
'_transient_wptag_%',
'_transient_timeout_wptag_%'
)
);
}
private static function delete_user_meta() {
$wpdb = $GLOBALS['wpdb'];
foreach (self::$user_meta_to_delete as $meta_key) {
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key = %s",
$meta_key
)
);
}
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
'wptag_%'
)
);
}
private static function remove_capabilities() {
$roles = wp_roles();
if (!$roles) {
return;
}
foreach ($roles->roles as $role_name => $role_info) {
$role = get_role($role_name);
if ($role) {
foreach (self::$capabilities_to_remove as $capability) {
$role->remove_cap($capability);
}
}
}
}
private static function clear_cache() {
if (function_exists('wp_cache_flush')) {
wp_cache_flush();
}
wp_cache_delete('wptag_available_services', 'wptag');
wp_cache_delete('wptag_settings', 'wptag');
wp_cache_delete('wptag_enabled_services', 'wptag');
if (function_exists('w3tc_flush_all')) {
w3tc_flush_all();
}
if (function_exists('wp_cache_clear_cache')) {
wp_cache_clear_cache();
}
if (class_exists('WpFastestCache')) {
$cache = new WpFastestCache();
if (method_exists($cache, 'deleteCache')) {
$cache->deleteCache(true);
}
}
if (function_exists('rocket_clean_domain')) {
rocket_clean_domain();
}
if (class_exists('LiteSpeed_Cache_API')) {
LiteSpeed_Cache_API::purge_all();
}
}
private static function cleanup_database() {
$wpdb = $GLOBALS['wpdb'];
$tables_to_check = array(
$wpdb->options,
$wpdb->usermeta,
$wpdb->postmeta
);
foreach ($tables_to_check as $table) {
if ($table === $wpdb->postmeta) {
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$table} WHERE meta_key LIKE %s",
'wptag_%'
)
);
}
}
$wpdb->query("OPTIMIZE TABLE {$wpdb->options}");
$wpdb->query("OPTIMIZE TABLE {$wpdb->usermeta}");
$wpdb->query("OPTIMIZE TABLE {$wpdb->postmeta}");
}
private static function cleanup_multisite() {
if (!is_multisite()) {
return;
}
$sites = get_sites(array(
'number' => 0,
'fields' => 'ids'
));
foreach ($sites as $site_id) {
switch_to_blog($site_id);
foreach (self::$options_to_delete as $option) {
delete_option($option);
}
self::delete_transients();
self::delete_user_meta();
self::remove_capabilities();
restore_current_blog();
}
$wpdb = $GLOBALS['wpdb'];
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s",
'wptag_%'
)
);
}
private static function log_uninstall() {
if (!defined('WP_DEBUG') || !WP_DEBUG) {
return;
}
$log_data = array(
'plugin' => 'WPTag',
'version' => WPTAG_VERSION,
'uninstalled_at' => current_time('mysql'),
'user_id' => get_current_user_id(),
'site_url' => get_site_url(),
'wordpress_version' => get_bloginfo('version'),
'php_version' => PHP_VERSION,
'is_multisite' => is_multisite()
);
error_log('WPTag Uninstall: ' . wp_json_encode($log_data));
}
public static function get_cleanup_summary() {
$wpdb = $GLOBALS['wpdb'];
$summary = array(
'options_deleted' => 0,
'transients_deleted' => 0,
'user_meta_deleted' => 0,
'capabilities_removed' => count(self::$capabilities_to_remove),
'cache_cleared' => true,
'database_optimized' => true
);
$options_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
'wptag_%'
)
);
$summary['options_deleted'] = (int) $options_count;
$transients_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
'_transient_wptag_%',
'_transient_timeout_wptag_%'
)
);
$summary['transients_deleted'] = (int) $transients_count;
$user_meta_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
'wptag_%'
)
);
$summary['user_meta_deleted'] = (int) $user_meta_count;
return $summary;
}
}
WPTag_Uninstaller::uninstall();

View file

@ -3,7 +3,7 @@
* Plugin Name: WPTag
* Plugin URI: https://wptag.com
* Description: Professional tracking codes and analytics management plugin for WordPress
* Version: 1.0.0
* Version: 1.1.0
* Author: WPTag.com
* Author URI: https://wptag.com
* Text Domain: wptag
@ -74,6 +74,7 @@ final class WPTag {
add_action('init', array($this, 'init'), 0);
add_action('plugins_loaded', array($this, 'plugins_loaded'));
add_action('wptag_clear_cache', array($this, 'clear_all_cache'));
}
public function activate() {
@ -104,6 +105,16 @@ final class WPTag {
public function get_version() {
return WPTAG_VERSION;
}
public function clear_all_cache() {
if ($this->frontend) {
$this->frontend->clear_cache();
}
if (function_exists('wp_cache_flush')) {
wp_cache_flush();
}
}
}
function wptag() {