// ==UserScript==
// @name ModernMonkeyConfig Enhanced Security Edition v0.4.0
// @noframes
// @version 0.4.0
// @namespace http://odyniec.net/
// @include *
// @description Enhanced Security Configuration Dialog with improved performance, validation, and security features
// @require https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js#sha384-qSFej5dZNviyoPgYJ5+Xk4bEbX8AYddxAHPuzs1aSgRiXxJ3qmyWNaPsRkpv/+x5
// ==/UserScript==
/**
* ModernMonkeyConfig Enhanced Security Edition
* نسخة محسنة أمنياً مع تحسينات شاملة للأداء والحماية
*/
class ModernMonkeyConfig {
constructor(data) {
this.version = '0.4.0';
this.data = this.validateAndSanitizeConfig(data);
this.params = this.data.parameters || this.data.params || {};
this.values = {};
this.storageKey = '';
this.displayed = false;
this.openLayer = null;
this.shadowRoot = null;
this.container = null;
this.iframeFallback = null;
this.elementCache = new Map();
this.eventListeners = new Map();
this.trustedPolicy = null;
this.validationRules = new Map();
this.init();
}
/**
* تسجيل الأخطاء والرسائل
*/
log(message, level = 'info') {
try {
const timestamp = new Date().toISOString();
const formattedMessage = `[ModernMonkeyConfig v${this.version}] ${timestamp}: ${message}`;
if (console[level]) {
console[level](formattedMessage);
} else {
console.log(`[${level.toUpperCase()}] ${formattedMessage}`);
}
} catch (e) {
console.error(`[ModernMonkeyConfig v${this.version}] Logging failed: ${e.message}`);
}
}
/**
* التحقق من صحة وتنظيف إعدادات التكوين
*/
validateAndSanitizeConfig(data) {
if (!data || typeof data !== 'object') {
throw new Error('Configuration data must be an object');
}
const sanitized = {
title: this.sanitizeString(data.title) || 'Configuration',
buttons: Array.isArray(data.buttons) ? data.buttons.filter(btn =>
['save', 'reset', 'close', 'reload', 'homepage'].includes(btn)
) : ['save', 'reset', 'close', 'reload', 'homepage'],
menuCommand: Boolean(data.menuCommand),
parameters: {},
shadowWidth: this.validateDimension(data.shadowWidth) || '600px',
shadowHeight: this.validateDimension(data.shadowHeight) || '400px',
iframeWidth: this.validateDimension(data.iframeWidth) || '600px',
iframeHeight: this.validateDimension(data.iframeHeight) || '400px',
shadowFontSize: this.validateFontSize(data.shadowFontSize) || '14px',
shadowFontColor: this.validateColor(data.shadowFontColor) || '#000000',
iframeFontSize: this.validateFontSize(data.iframeFontSize) || '14px',
iframeFontColor: this.validateColor(data.iframeFontColor) || '#000000',
onSave: typeof data.onSave === 'function' ? data.onSave : null
};
if (data.parameters && typeof data.parameters === 'object') {
for (const [key, param] of Object.entries(data.parameters)) {
if (this.isValidParameterKey(key) && this.isValidParameter(param)) {
sanitized.parameters[key] = this.sanitizeParameter(param);
} else {
this.log(`Invalid parameter skipped: ${key}`, 'warn');
}
}
}
return sanitized;
}
/**
* التحقق من صحة مفتاح المعامل
*/
isValidParameterKey(key) {
return typeof key === 'string' &&
key.length > 0 &&
key.length <= 50 &&
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
}
/**
* التحقق من صحة المعامل
*/
isValidParameter(param) {
if (!param || typeof param !== 'object') return false;
const validTypes = ['checkbox', 'number', 'text', 'color', 'textarea',
'range', 'radio', 'file', 'button', 'select', 'group', 'custom'];
return validTypes.includes(param.type);
}
/**
* تنظيف المعامل
*/
sanitizeParameter(param) {
const sanitized = {
type: param.type,
label: this.sanitizeString(param.label),
default: this.sanitizeValue(param.default, param.type),
column: this.validateColumn(param.column)
};
switch (param.type) {
case 'number':
case 'range':
sanitized.min = this.sanitizeNumber(param.min);
sanitized.max = this.sanitizeNumber(param.max);
sanitized.step = this.sanitizeNumber(param.step) || 1;
break;
case 'textarea':
sanitized.rows = Math.max(1, Math.min(20, parseInt(param.rows) || 4));
sanitized.cols = Math.max(10, Math.min(100, parseInt(param.cols) || 20));
break;
case 'radio':
case 'select':
if (param.choices && typeof param.choices === 'object') {
sanitized.choices = this.sanitizeChoices(param.choices);
}
sanitized.multiple = Boolean(param.multiple);
break;
case 'file':
sanitized.accept = this.sanitizeString(param.accept) || '*/*';
break;
case 'custom':
sanitized.html = this.sanitizeString(param.html);
if (typeof param.get === 'function') sanitized.get = param.get;
if (typeof param.set === 'function') sanitized.set = param.set;
break;
}
['fontSize', 'fontColor', 'inputWidth', 'inputHeight',
'checkboxWidth', 'checkboxHeight'].forEach(prop => {
if (param[prop]) {
sanitized[prop] = this.sanitizeString(param[prop]);
}
});
return sanitized;
}
/**
* تنظيف النص
*/
sanitizeString(str) {
if (typeof str !== 'string') return '';
return str.trim().substring(0, 1000);
}
/**
* تنظيف الرقم
*/
sanitizeNumber(num) {
const parsed = parseFloat(num);
return isNaN(parsed) ? undefined : parsed;
}
/**
* تنظيف القيمة حسب النوع
*/
sanitizeValue(value, type) {
switch (type) {
case 'number':
case 'range':
return this.sanitizeNumber(value);
case 'checkbox':
return Boolean(value);
case 'text':
case 'color':
case 'textarea':
return this.sanitizeString(value);
case 'select':
return Array.isArray(value) ?
value.map(v => this.sanitizeString(v)) :
this.sanitizeString(value);
default:
return value;
}
}
/**
* تنظيف الخيارات
*/
sanitizeChoices(choices) {
const sanitized = {};
for (const [key, value] of Object.entries(choices)) {
const cleanKey = this.sanitizeString(key);
const cleanValue = this.sanitizeString(value);
if (cleanKey && cleanValue) {
sanitized[cleanKey] = cleanValue;
}
}
return sanitized;
}
/**
* التحقق من صحة العمود
*/
validateColumn(column) {
const validColumns = ['left', 'right', 'top', 'bottom',
'left&top', 'right&top', 'left&bottom', 'right&bottom'];
return validColumns.includes(column) ? column : null;
}
/**
* التحقق من صحة الأبعاد
*/
validateDimension(dimension) {
if (typeof dimension !== 'string') return null;
return /^\d+(px|em|rem|%|vh|vw)$/.test(dimension) ? dimension : null;
}
/**
* التحقق من صحة حجم الخط
*/
validateFontSize(fontSize) {
if (typeof fontSize !== 'string') return null;
return /^\d+(px|em|rem)$/.test(fontSize) ? fontSize : null;
}
/**
* التحقق من صحة اللون
*/
validateColor(color) {
if (typeof color !== 'string') return null;
return /^#([0-9A-Fa-f]{3}){1,2}$/.test(color) ? color : null;
}
/**
* إنشاء Trusted Types Policy
*/
createTrustedPolicy() {
try {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
this.trustedPolicy = window.trustedTypes.createPolicy(`monkeyConfig-${Date.now()}`, {
createHTML: (input) => {
if (typeof DOMPurify === 'undefined') {
this.log('DOMPurify not available, using fallback sanitization', 'warn');
return this.fallbackSanitize(input);
}
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['div', 'span', 'table', 'tr', 'td', 'input', 'textarea',
'button', 'label', 'select', 'option', 'fieldset', 'legend',
'h1', 'br', 'svg', 'path', 'style'],
ALLOWED_ATTR: ['type', 'name', 'id', 'class', 'style', 'for', 'value',
'min', 'max', 'step', 'rows', 'cols', 'multiple', 'accept',
'width', 'height', 'viewBox', 'fill', 'stroke', 'stroke-width',
'stroke-linecap', 'stroke-linejoin', 'd', 'colspan', 'checked'],
ALLOW_DATA_ATTR: false,
RETURN_TRUSTED_TYPE: true
});
}
});
} else {
this.trustedPolicy = {
createHTML: (input) => this.fallbackSanitize(input)
};
}
} catch (e) {
this.log(`Failed to create Trusted Types policy: ${e.message}`, 'error');
this.trustedPolicy = {
createHTML: (input) => this.fallbackSanitize(input)
};
}
}
/**
* تنظيف احتياطي للHTML
*/
fallbackSanitize(input) {
return String(input)
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+\s*=\s*"[^"]*"/gi, '')
.replace(/on\w+\s*=\s*'[^']*'/gi, '')
.replace(/javascript:/gi, '')
.replace(/vbscript:/gi, '')
.replace(/data:/gi, '');
}
/**
* إنشاء HTML موثوق
*/
createTrustedHTML(htmlString) {
try {
return this.trustedPolicy.createHTML(htmlString);
} catch (e) {
this.log(`Failed to create TrustedHTML: ${e.message}`, 'error');
return '';
}
}
/**
* تهيئة الكلاس
*/
init() {
try {
this.createTrustedPolicy();
this.setupValidationRules();
this.storageKey = `_ModernMonkeyConfig_${this.data.title.replace(/[^a-zA-Z0-9]/g, '_')}_cfg`;
this.loadStoredValues();
if (this.data.menuCommand) {
this.registerMenuCommand();
}
this.setupPublicMethods();
this.log('ModernMonkeyConfig initialized successfully');
} catch (e) {
this.log(`Initialization failed: ${e.message}`, 'error');
throw e;
}
}
/**
* إعداد قواعد التحقق
*/
setupValidationRules() {
this.validationRules.set('email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/);
this.validationRules.set('url', /^https?:\/\/.+/);
this.validationRules.set('number', /^\d+$/);
this.validationRules.set('float', /^\d*\.?\d+$/);
}
/**
* تحميل القيم المحفوظة
*/
loadStoredValues() {
try {
let storedValues = {};
if (typeof GM_getValue !== 'undefined') {
const stored = GM_getValue(this.storageKey);
if (stored) {
storedValues = JSON.parse(stored);
}
}
this.shadowWidth = this.validateDimension(storedValues.shadowWidth) || this.data.shadowWidth;
this.shadowHeight = this.validateDimension(storedValues.shadowHeight) || this.data.shadowHeight;
this.iframeWidth = this.validateDimension(storedValues.iframeWidth) || this.data.iframeWidth;
this.iframeHeight = this.validateDimension(storedValues.iframeHeight) || this.data.iframeHeight;
this.shadowFontSize = this.validateFontSize(storedValues.shadowFontSize) || this.data.shadowFontSize;
this.shadowFontColor = this.validateColor(storedValues.shadowFontColor) || this.data.shadowFontColor;
this.iframeFontSize = this.validateFontSize(storedValues.iframeFontSize) || this.data.iframeFontSize;
this.iframeFontColor = this.validateColor(storedValues.iframeFontColor) || this.data.iframeFontColor;
for (const [key, param] of Object.entries(this.params)) {
this.values[key] = storedValues[key] !== undefined ?
this.sanitizeValue(storedValues[key], param.type) :
param.default;
}
} catch (e) {
this.log(`Failed to load stored values: ${e.message}`, 'error');
for (const [key, param] of Object.entries(this.params)) {
this.values[key] = param.default;
}
}
}
/**
* تسجيل أمر القائمة
*/
registerMenuCommand() {
try {
if (typeof GM_registerMenuCommand !== 'undefined') {
const commandText = this.data.menuCommand === true ?
this.data.title :
String(this.data.menuCommand);
GM_registerMenuCommand(commandText, () => this.open());
}
} catch (e) {
this.log(`Failed to register menu command: ${e.message}`, 'error');
}
}
/**
* إعداد الطرق العامة
*/
setupPublicMethods() {
this.open = this.open.bind(this);
this.close = this.close.bind(this);
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.validate = this.validate.bind(this);
this.reset = this.reset.bind(this);
}
/**
* الحصول على قيمة
*/
get(name) {
if (!this.params[name]) {
this.log(`Parameter '${name}' does not exist`, 'warn');
return undefined;
}
return this.values[name];
}
/**
* تعيين قيمة
*/
set(name, value) {
try {
if (!this.params[name]) {
this.log(`Parameter '${name}' does not exist`, 'warn');
return false;
}
const sanitizedValue = this.sanitizeValue(value, this.params[name].type);
if (this.validateValue(name, sanitizedValue)) {
this.values[name] = sanitizedValue;
this.updateUI();
return true;
}
return false;
} catch (e) {
this.log(`Failed to set value for ${name}: ${e.message}`, 'error');
return false;
}
}
/**
* التحقق من صحة القيمة
*/
validateValue(name, value) {
const param = this.params[name];
if (!param) return false;
switch (param.type) {
case 'number':
case 'range':
const num = parseFloat(value);
if (isNaN(num)) return false;
if (param.min !== undefined && num < param.min) return false;
if (param.max !== undefined && num > param.max) return false;
return true;
case 'text':
if (param.validation && this.validationRules.has(param.validation)) {
return this.validationRules.get(param.validation).test(value);
}
return typeof value === 'string' && value.length <= 1000;
case 'checkbox':
return typeof value === 'boolean';
default:
return true;
}
}
/**
* التحقق من صحة جميع القيم
*/
validate() {
const errors = [];
for (const [name, value] of Object.entries(this.values)) {
if (!this.validateValue(name, value)) {
errors.push(`Invalid value for parameter '${name}': ${value}`);
}
}
return errors;
}
/**
* إعادة تعيين القيم الافتراضية
*/
reset() {
try {
for (const [key, param] of Object.entries(this.params)) {
this.values[key] = param.default;
}
this.updateUI();
this.log('Configuration reset to defaults');
} catch (e) {
this.log(`Failed to reset configuration: ${e.message}`, 'error');
}
}
/**
* هروب HTML
*/
escapeHtml(string) {
if (string == null) return '';
return String(string)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* الحصول على أيقونة
*/
getIcon(type) {
const icons = {
save: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/></svg>',
reset: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
close: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
reload: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>',
homepage: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>'
};
return icons[type] || '';
}
/**
* الحصول على نص الزر
*/
getButtonText(buttonType) {
const texts = {
save: 'Save Without Reload',
reset: 'Reset',
reload: 'Save With Reload',
homepage: 'Homepage'
};
return texts[buttonType] || buttonType;
}
/**
* إنشاء HTML للواجهة
*/
render() {
try {
const title = this.escapeHtml(this.data.title);
let html = `
<div class="__MonkeyConfig_container">
<div class="__MonkeyConfig_header">
<h1>${title}</h1>
<button type="button" id="__MonkeyConfig_button_close" class="__MonkeyConfig_close_btn" aria-label="Close">
${this.getIcon('close')}
</button>
</div>
<div class="__MonkeyConfig_content">
<div class="__MonkeyConfig_sections">`;
html += this.renderSection('top');
html += this.renderColumns('top');
html += this.renderColumns('middle');
html += this.renderDefaultSection();
html += this.renderColumns('bottom');
html += this.renderSection('bottom');
html += `
</div>
</div>
<div class="__MonkeyConfig_footer">
${this.renderButtons()}
</div>
</div>`;
return this.createTrustedHTML(html);
} catch (e) {
this.log(`Failed to render HTML: ${e.message}`, 'error');
return this.createTrustedHTML('<div class="__MonkeyConfig_error">Error rendering configuration dialog</div>');
}
}
/**
* إنشاء قسم
*/
renderSection(position) {
const items = Object.entries(this.params)
.filter(([, param]) => param.column === position)
.map(([key, param]) => this.renderField(key, param))
.join('');
return items ? `<div class="__MonkeyConfig_section_${position}">${items}</div>` : '';
}
/**
* إنشاء الأعمدة
*/
renderColumns(position) {
const leftColumn = position !== 'middle' ? `left&${position}` : 'left';
const rightColumn = position !== 'middle' ? `right&${position}` : 'right';
const leftItems = Object.entries(this.params)
.filter(([, param]) => param.column === leftColumn)
.map(([key, param]) => this.renderField(key, param))
.join('');
const rightItems = Object.entries(this.params)
.filter(([, param]) => param.column === rightColumn)
.map(([key, param]) => this.renderField(key, param))
.join('');
if (!leftItems && !rightItems) return '';
return `
<div class="__MonkeyConfig_columns">
<div class="__MonkeyConfig_left_column">${leftItems}</div>
<div class="__MonkeyConfig_right_column">${rightItems}</div>
</div>`;
}
/**
* إنشاء القسم الافتراضي
*/
renderDefaultSection() {
const items = Object.entries(this.params)
.filter(([, param]) => !param.column)
.map(([key, param]) => this.renderField(key, param))
.join('');
return items ? `<table class="__MonkeyConfig_default_table">${items}</table>` : '';
}
/**
* إنشاء حقل
*/
renderField(name, param) {
const fieldId = `__MonkeyConfig_field_${this.escapeHtml(name)}`;
const parentId = `__MonkeyConfig_parent_${this.escapeHtml(name)}`;
const label = this.renderLabel(name, param, fieldId);
const field = this.renderInput(name, param, fieldId);
const isInline = ['checkbox', 'number', 'text'].includes(param.type);
if (param.type === 'group') {
return `<tr><td colspan="2">${field}</td></tr>`;
}
if (isInline) {
return `
<tr>
<td id="${parentId}" colspan="2" class="__MonkeyConfig_inline">
${label}${field}
</td>
</tr>`;
}
return `
<tr>
<td class="__MonkeyConfig_label_cell">${label}</td>
<td id="${parentId}" class="__MonkeyConfig_field_cell">${field}</td>
</tr>`;
}
/**
* إنشاء تسمية
*/
renderLabel(name, param, fieldId) {
const labelText = param.label ||
name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, ' ');
const styles = [];
if (param.fontSize) styles.push(`font-size:${this.escapeHtml(param.fontSize)}`);
if (param.fontColor) styles.push(`color:${this.escapeHtml(param.fontColor)}`);
if (param.labelAlign) styles.push(`text-align:${this.escapeHtml(param.labelAlign)}`);
const styleAttr = styles.length ? ` style="${styles.join(';')}"` : '';
return `<label for="${fieldId}"${styleAttr}>${this.escapeHtml(labelText)}</label>`;
}
/**
* إنشاء حقل إدخال
*/
renderInput(name, param, fieldId) {
const inputName = this.escapeHtml(name);
const value = this.values[name];
switch (param.type) {
case 'checkbox':
return `<input type="checkbox" id="${fieldId}" name="${inputName}" ${value ? 'checked' : ''}>`;
case 'number':
return `<input type="number" id="${fieldId}" name="${inputName}"
value="${this.escapeHtml(value || '')}"
${param.min !== undefined ? `min="${param.min}"` : ''}
${param.max !== undefined ? `max="${param.max}"` : ''}
${param.step !== undefined ? `step="${param.step}"` : ''}>`;
case 'text':
return `<input type="text" id="${fieldId}" name="${inputName}"
value="${this.escapeHtml(value || '')}">`;
case 'color':
return `<input type="color" id="${fieldId}" name="${inputName}"
value="${this.escapeHtml(value || '#000000')}">`;
case 'textarea':
return `<textarea id="${fieldId}" name="${inputName}"
rows="${param.rows || 4}" cols="${param.cols || 20}">${this.escapeHtml(value || '')}</textarea>`;
case 'range':
return `<input type="range" id="${fieldId}" name="${inputName}"
value="${this.escapeHtml(value || param.min || 0)}"
min="${param.min || 0}" max="${param.max || 100}" step="${param.step || 1}">
<span class="__MonkeyConfig_range_value">${value || param.min || 0}</span>`;
case 'select':
return this.renderSelect(fieldId, inputName, param, value);
case 'radio':
return this.renderRadio(fieldId, inputName, param, value);
case 'file':
return `<input type="file" id="${fieldId}" name="${inputName}"
accept="${this.escapeHtml(param.accept || '*/*')}">`;
case 'button':
return `<button type="button" id="${fieldId}" name="${inputName}"
class="__MonkeyConfig_custom_button">${this.escapeHtml(param.label || 'Button')}</button>`;
case 'group':
return this.renderGroup(fieldId, inputName, param);
case 'custom':
return this.renderCustom(fieldId, inputName, param);
default:
return `<span class="__MonkeyConfig_error">Unknown field type: ${this.escapeHtml(param.type)}</span>`;
}
}
/**
* إنشاء قائمة منسدلة
*/
renderSelect(fieldId, inputName, param, value) {
const multipleAttr = param.multiple ? ' multiple' : '';
const selectedValues = Array.isArray(value) ? value : [value];
let options = '';
if (param.choices) {
for (const [key, label] of Object.entries(param.choices)) {
const selected = selectedValues.includes(key) ? ' selected' : '';
options += `<option value="${this.escapeHtml(key)}"${selected}>${this.escapeHtml(label)}</option>`;
}
}
return `<select id="${fieldId}" name="${inputName}"${multipleAttr}>${options}</select>`;
}
/**
* إنشاء أزرار الراديو
*/
renderRadio(fieldId, inputName, param, value) {
let radioHtml = '<div class="__MonkeyConfig_radio_group">';
if (param.choices) {
for (const [key, label] of Object.entries(param.choices)) {
const checked = value === key ? ' checked' : '';
const radioId = `${fieldId}_${key}`;
radioHtml += `
<label class="__MonkeyConfig_radio_label">
<input type="radio" id="${radioId}" name="${inputName}"
value="${this.escapeHtml(key)}"${checked}>
${this.escapeHtml(label)}
</label>`;
}
}
radioHtml += '</div>';
return radioHtml;
}
/**
* إنشاء مجموعة
*/
renderGroup(fieldId, inputName, param) {
const legend = param.label ? `<legend>${this.escapeHtml(param.label)}</legend>` : '';
return `<fieldset id="${fieldId}" class="__MonkeyConfig_group">${legend}</fieldset>`;
}
/**
* إنشاء حقل مخصص
*/
renderCustom(fieldId, inputName, param) {
const customHtml = param.html || '';
return `<div id="${fieldId}" class="__MonkeyConfig_custom" data-name="${inputName}">${customHtml}</div>`;
}
/**
* إنشاء الأزرار
*/
renderButtons() {
let buttonsHtml = '';
for (const buttonType of this.data.buttons) {
if (buttonType === 'close') continue; // Close button is in header
const buttonId = `__MonkeyConfig_button_${buttonType}`;
const buttonText = this.getButtonText(buttonType);
const icon = this.getIcon(buttonType);
buttonsHtml += `
<button type="button" id="${buttonId}" class="__MonkeyConfig_btn __MonkeyConfig_btn_${buttonType}">
${icon}
<span>${buttonText}</span>
</button>`;
}
return buttonsHtml;
}
/**
* الحصول على CSS
*/
getCSS() {
return `
<style>
.__MonkeyConfig_container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: ${this.shadowWidth};
max-width: 90vw;
height: ${this.shadowHeight};
max-height: 90vh;
background: #ffffff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: ${this.shadowFontSize};
color: ${this.shadowFontColor};
z-index: 2147483647;
display: flex;
flex-direction: column;
overflow: hidden;
}
.__MonkeyConfig_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.__MonkeyConfig_header h1 {
margin: 0;
font-size: 18px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.__MonkeyConfig_close_btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.__MonkeyConfig_close_btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.__MonkeyConfig_content {
flex: 1;
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
}
.__MonkeyConfig_sections {
display: flex;
flex-direction: column;
gap: 20px;
}
.__MonkeyConfig_columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.__MonkeyConfig_default_table {
width: 100%;
border-collapse: collapse;
}
.__MonkeyConfig_default_table tr {
border-bottom: 1px solid #f0f0f0;
}
.__MonkeyConfig_default_table td {
padding: 12px 8px;
vertical-align: top;
}
.__MonkeyConfig_label_cell {
width: 30%;
font-weight: 500;
color: #333;
}
.__MonkeyConfig_field_cell {
width: 70%;
}
.__MonkeyConfig_inline {
display: flex;
align-items: center;
gap: 12px;
}
.__MonkeyConfig_inline label {
margin: 0;
font-weight: 500;
}
/* Form Controls */
.__MonkeyConfig_container input,
.__MonkeyConfig_container textarea,
.__MonkeyConfig_container select {
border: 2px solid #e1e5e9;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
transition: all 0.2s ease;
background: white;
}
.__MonkeyConfig_container input:focus,
.__MonkeyConfig_container textarea:focus,
.__MonkeyConfig_container select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.__MonkeyConfig_container input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
}
.__MonkeyConfig_container input[type="color"] {
width: 50px;
height: 40px;
padding: 2px;
cursor: pointer;
}
.__MonkeyConfig_container input[type="range"] {
width: 150px;
margin-right: 10px;
}
.__MonkeyConfig_range_value {
display: inline-block;
min-width: 40px;
text-align: center;
font-weight: 500;
color: #667eea;
}
.__MonkeyConfig_radio_group {
display: flex;
flex-direction: column;
gap: 8px;
}
.__MonkeyConfig_radio_label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 0;
}
.__MonkeyConfig_radio_label input[type="radio"] {
margin: 0;
}
.__MonkeyConfig_group {
border: 2px solid #e1e5e9;
border-radius: 6px;
padding: 16px;
margin: 8px 0;
}
.__MonkeyConfig_group legend {
font-weight: 600;
color: #333;
padding: 0 8px;
}
.__MonkeyConfig_custom {
padding: 8px 0;
}
.__MonkeyConfig_custom_button {
background: #f8f9fa;
border: 2px solid #e1e5e9;
color: #333;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.__MonkeyConfig_custom_button:hover {
background: #e9ecef;
border-color: #667eea;
}
/* Footer */
.__MonkeyConfig_footer {
padding: 16px 20px;
background: #f8f9fa;
border-top: 1px solid #e1e5e9;
display: flex;
gap: 12px;
justify-content: flex-end;
flex-wrap: wrap;
}
.__MonkeyConfig_btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: 2px solid transparent;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
}
.__MonkeyConfig_btn_save {
background: #28a745;
color: white;
border-color: #28a745;
}
.__MonkeyConfig_btn_save:hover {
background: #218838;
transform: translateY(-1px);
}
.__MonkeyConfig_btn_reset {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.__MonkeyConfig_btn_reset:hover {
background: #c82333;
transform: translateY(-1px);
}
.__MonkeyConfig_btn_reload {
background: #007bff;
color: white;
border-color: #007bff;
}
.__MonkeyConfig_btn_reload:hover {
background: #0056b3;
transform: translateY(-1px);
}
.__MonkeyConfig_btn_homepage {
background: #6c757d;
color: white;
border-color: #6c757d;
}
.__MonkeyConfig_btn_homepage:hover {
background: #545b62;
transform: translateY(-1px);
}
.__MonkeyConfig_error {
color: #dc3545;
font-weight: 500;
padding: 8px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
/* Responsive Design */
@media (max-width: 768px) {
.__MonkeyConfig_container {
width: 95vw;
height: 95vh;
margin: 0;
transform: translate(-50%, -50%);
}
.__MonkeyConfig_columns {
grid-template-columns: 1fr;
}
.__MonkeyConfig_footer {
flex-direction: column;
}
.__MonkeyConfig_btn {
justify-content: center;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.__MonkeyConfig_container {
background: #1a1a1a;
border-color: #444;
color: #e0e0e0;
}
.__MonkeyConfig_default_table tr {
border-color: #333;
}
.__MonkeyConfig_container input,
.__MonkeyConfig_container textarea,
.__MonkeyConfig_container select {
background: #2a2a2a;
border-color: #444;
color: #e0e0e0;
}
.__MonkeyConfig_footer {
background: #2a2a2a;
border-color: #444;
}
.__MonkeyConfig_group {
border-color: #444;
}
.__MonkeyConfig_custom_button {
background: #2a2a2a;
border-color: #444;
color: #e0e0e0;
}
}
/* Animation */
@keyframes __MonkeyConfig_fadeIn {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.9);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.__MonkeyConfig_container {
animation: __MonkeyConfig_fadeIn 0.3s ease-out;
}
</style>`;
}
/**
* فتح مربع الحوار
*/
open() {
if (this.displayed) {
this.log('Configuration dialog is already open', 'warn');
return;
}
try {
this.createShadowDOM();
this.displayed = true;
this.log('Configuration dialog opened');
} catch (e) {
this.log(`Failed to open configuration dialog: ${e.message}`, 'error');
this.createIframeFallback();
}
}
/**
* إنشاء Shadow DOM
*/
createShadowDOM() {
try {
this.openLayer = document.createElement('div');
this.openLayer.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background: rgba(0, 0, 0, 0.5) !important;
z-index: 2147483646 !important;
backdrop-filter: blur(2px) !important;
`;
if (this.openLayer.attachShadow) {
this.shadowRoot = this.openLayer.attachShadow({ mode: 'closed' });
const container = document.createElement('div');
container.innerHTML = this.getCSS() + this.render();
this.shadowRoot.appendChild(container);
this.container = this.shadowRoot.querySelector('.__MonkeyConfig_container');
} else {
throw new Error('Shadow DOM not supported');
}
document.body.appendChild(this.openLayer);
this.attachEventListeners();
// Focus management
setTimeout(() => {
const firstInput = this.shadowRoot.querySelector('input, textarea, select, button');
if (firstInput) firstInput.focus();
}, 100);
} catch (e) {
this.log(`Failed to create Shadow DOM: ${e.message}`, 'error');
throw e;
}
}
/**
* إنشاء Iframe كبديل
*/
createIframeFallback() {
try {
this.log('Using iframe fallback', 'info');
this.openLayer = document.createElement('div');
this.openLayer.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background: rgba(0, 0, 0, 0.5) !important;
z-index: 2147483646 !important;
`;
this.iframeFallback = document.createElement('iframe');
this.iframeFallback.style.cssText = `
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
width: ${this.iframeWidth} !important;
height: ${this.iframeHeight} !important;
border: none !important;
border-radius: 8px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
`;
this.openLayer.appendChild(this.iframeFallback);
document.body.appendChild(this.openLayer);
const iframeDoc = this.iframeFallback.contentDocument || this.iframeFallback.contentWindow.document;
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${this.escapeHtml(this.data.title)}</title>
${this.getCSS().replace(this.shadowFontSize, this.iframeFontSize).replace(this.shadowFontColor, this.iframeFontColor)}
</head>
<body style="margin:0;padding:0;overflow:hidden;">
${this.render()}
</body>
</html>
`;
iframeDoc.open();
iframeDoc.write(html);
iframeDoc.close();
this.container = iframeDoc.querySelector('.__MonkeyConfig_container');
this.attachEventListeners(iframeDoc);
this.displayed = true;
} catch (e) {
this.log(`Failed to create iframe fallback: ${e.message}`, 'error');
alert('Failed to open configuration dialog. Please check console for details.');
}
}
/**
* ربط مستمعي الأحداث
*/
attachEventListeners(doc = null) {
try {
const context = doc || this.shadowRoot;
if (!context) return;
// Event delegation for better performance
const container = context.querySelector('.__MonkeyConfig_container');
if (!container) return;
const handleEvent = (e) => {
const target = e.target;
const targetId = target.id;
const targetName = target.name;
// Button clicks
if (targetId && targetId.startsWith('__MonkeyConfig_button_')) {
e.preventDefault();
const buttonType = targetId.replace('__MonkeyConfig_button_', '');
this.handleButtonClick(buttonType);
return;
}
// Form field changes
if (targetName && this.params[targetName]) {
this.handleFieldChange(targetName, target);
return;
}
};
// Add single event listener with delegation
container.addEventListener('click', handleEvent);
container.addEventListener('change', handleEvent);
container.addEventListener('input', handleEvent);
// Close on backdrop click
if (this.openLayer) {
this.openLayer.addEventListener('click', (e) => {
if (e.target === this.openLayer) {
this.close();
}
});
}
// Keyboard shortcuts
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
this.close();
} else if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleButtonClick('save');
}
};
(doc || document).addEventListener('keydown', handleKeyDown);
this.eventListeners.set('keydown', handleKeyDown);
} catch (e) {
this.log(`Failed to attach event listeners: ${e.message}`, 'error');
}
}
/**
* معالجة النقر على الأزرار
*/
handleButtonClick(buttonType) {
try {
switch (buttonType) {
case 'save':
this.save();
break;
case 'reset':
if (confirm('Are you sure you want to reset all settings to default values?')) {
this.reset();
}
break;
case 'close':
this.close();
break;
case 'reload':
this.save();
if (typeof GM_getValue !== 'undefined') {
location.reload();
} else {
alert('Settings saved. Please refresh the page manually.');
}
break;
case 'homepage':
if (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.homepage) {
window.open(GM_info.script.homepage, '_blank');
} else {
alert('Homepage not available');
}
break;
default:
this.log(`Unknown button type: ${buttonType}`, 'warn');
}
} catch (e) {
this.log(`Error handling button click (${buttonType}): ${e.message}`, 'error');
}
}
/**
* معالجة تغيير الحقول
*/
handleFieldChange(name, element) {
try {
const param = this.params[name];
if (!param) return;
let value;
switch (param.type) {
case 'checkbox':
value = element.checked;
break;
case 'number':
case 'range':
value = parseFloat(element.value);
if (isNaN(value)) value = param.default;
break;
case 'select':
if (param.multiple) {
value = Array.from(element.selectedOptions).map(option => option.value);
} else {
value = element.value;
}
break;
case 'custom':
if (param.get && typeof param.get === 'function') {
value = param.get(element);
} else {
value = element.value;
}
break;
default:
value = element.value;
}
if (this.validateValue(name, value)) {
this.values[name] = value;
// Update range display
if (param.type === 'range') {
const rangeValue = element.parentNode.querySelector('.__MonkeyConfig_range_value');
if (rangeValue) {
rangeValue.textContent = value;
}
}
this.log(`Field '${name}' updated to: ${value}`);
} else {
this.log(`Invalid value for field '${name}': ${value}`, 'warn');
element.value = this.values[name]; // Revert to previous value
}
} catch (e) {
this.log(`Error handling field change (${name}): ${e.message}`, 'error');
}
}
/**
* تحديث واجهة المستخدم
*/
updateUI() {
try {
const context = this.shadowRoot || (this.iframeFallback && this.iframeFallback.contentDocument);
if (!context) return;
for (const [name, value] of Object.entries(this.values)) {
const param = this.params[name];
if (!param) continue;
const element = context.querySelector(`[name="${name}"]`);
if (!element) continue;
switch (param.type) {
case 'checkbox':
element.checked = Boolean(value);
break;
case 'select':
if (param.multiple && Array.isArray(value)) {
Array.from(element.options).forEach(option => {
option.selected = value.includes(option.value);
});
} else {
element.value = value;
}
break;
case 'radio':
const radioButton = context.querySelector(`[name="${name}"][value="${value}"]`);
if (radioButton) radioButton.checked = true;
break;
case 'range':
element.value = value;
const rangeValue = element.parentNode.querySelector('.__MonkeyConfig_range_value');
if (rangeValue) rangeValue.textContent = value;
break;
case 'custom':
if (param.set && typeof param.set === 'function') {
param.set(element, value);
} else {
element.value = value;
}
break;
default:
element.value = value;
}
}
} catch (e) {
this.log(`Error updating UI: ${e.message}`, 'error');
}
}
/**
* حفظ الإعدادات
*/
save() {
try {
const errors = this.validate();
if (errors.length > 0) {
alert('Validation errors:\n' + errors.join('\n'));
return false;
}
const dataToSave = {
...this.values,
shadowWidth: this.shadowWidth,
shadowHeight: this.shadowHeight,
iframeWidth: this.iframeWidth,
iframeHeight: this.iframeHeight,
shadowFontSize: this.shadowFontSize,
shadowFontColor: this.shadowFontColor,
iframeFontSize: this.iframeFontSize,
iframeFontColor: this.iframeFontColor
};
if (typeof GM_setValue !== 'undefined') {
GM_setValue(this.storageKey, JSON.stringify(dataToSave));
this.log('Settings saved successfully');
} else {
localStorage.setItem(this.storageKey, JSON.stringify(dataToSave));
this.log('Settings saved to localStorage');
}
if (this.data.onSave && typeof this.data.onSave === 'function') {
try {
this.data.onSave(this.values);
} catch (e) {
this.log(`Error in onSave callback: ${e.message}`, 'error');
}
}
return true;
} catch (e) {
this.log(`Failed to save settings: ${e.message}`, 'error');
alert('Failed to save settings. Please check console for details.');
return false;
}
}
/**
* إغلاق مربع الحوار
*/
close() {
try {
if (!this.displayed) return;
// Remove event listeners
for (const [event, handler] of this.eventListeners) {
document.removeEventListener(event, handler);
}
this.eventListeners.clear();
// Remove DOM elements
if (this.openLayer && this.openLayer.parentNode) {
this.openLayer.parentNode.removeChild(this.openLayer);
}
// Clear references
this.openLayer = null;
this.shadowRoot = null;
this.container = null;
this.iframeFallback = null;
this.elementCache.clear();
this.displayed = false;
this.log('Configuration dialog closed');
} catch (e) {
this.log(`Error closing dialog: ${e.message}`, 'error');
}
}
/**
* تنظيف الموارد
*/
destroy() {
try {
this.close();
// Clear all references
this.data = null;
this.params = null;
this.values = null;
this.validationRules.clear();
this.trustedPolicy = null;
this.log('ModernMonkeyConfig destroyed');
} catch (e) {
this.log(`Error during cleanup: ${e.message}`, 'error');
}
}
}
// Export for different environments
if (typeof module !== 'undefined' && module.exports) {
module.exports = ModernMonkeyConfig;
} else if (typeof window !== 'undefined') {
window.ModernMonkeyConfig = ModernMonkeyConfig;
}
/**
* Factory function for creating configuration dialogs
*/
function createMonkeyConfig(configData) {
try {
return new ModernMonkeyConfig(configData);
} catch (e) {
console.error(`[ModernMonkeyConfig] Failed to create configuration: ${e.message}`);
return null;
}
}
// Export factory function
if (typeof window !== 'undefined') {
window.createMonkeyConfig = createMonkeyConfig;
}
/**
* Example usage:
const config = createMonkeyConfig({
title: 'My Script Configuration',
menuCommand: true,
buttons: ['save', 'reset', 'close', 'reload'],
parameters: {
enabled: {
type: 'checkbox',
label: 'Enable Script',
default: true
},
maxItems: {
type: 'number',
label: 'Maximum Items',
default: 10,
min: 1,
max: 100
},
theme: {
type: 'select',
label: 'Theme',
choices: {
'light': 'Light Theme',
'dark': 'Dark Theme',
'auto': 'Auto Detect'
},
default: 'auto'
}
},
onSave: (values) => {
console.log('Settings saved:', values);
}
});
// Access values
const isEnabled = config.get('enabled');
const maxItems = config.get('maxItems');
// Set values
config.set('enabled', false);
// Open dialog
config.open();
*/