PluginProbe
Contact Forms by Cimatti / 2.3.0
Contact Forms by Cimatti v2.3.0
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / assets / js / admin / theme-helper.js

theme-helper.js in Contact Forms by Cimatti 2.3.0, at assets/js/admin/theme-helper.js

446 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Contact Forms - Theme Helper
3 *
4 * CSS conflict scanner that detects theme rules overriding Contact Forms styling.
5 * Shows file, line number, selector, and conflicting properties.
6 *
7 * @package ContactForms
8 * @since 2.0.0-beta.6
9 */
10 (function($) {
11 'use strict';
12
13 // Configuration from PHP
14 var config = window.accuaThemeHelper || {};
15 var monitoredClasses = config.monitoredClasses || [];
16 var criticalProperties = config.criticalProperties || [];
17 var i18n = config.i18n || {};
18 var pluginCssUrl = config.pluginCssUrl || '';
19
20 // Build regex pattern for monitored classes
21 var classPattern = new RegExp('\\b(' + monitoredClasses.join('|').replace(/-/g, '\\-') + ')\\b');
22
23 /**
24 * Extract filename from URL
25 */
26 function getFilename(url) {
27 if (!url) return '(inline)';
28 try {
29 var urlObj = new URL(url, window.location.origin);
30 var pathname = urlObj.pathname;
31 var filename = pathname.split('/').pop() || pathname;
32 // Remove query string for display but keep for identification
33 return filename.split('?')[0];
34 } catch (e) {
35 return url.substring(0, 50) + '...';
36 }
37 }
38
39 // Remote site Contact Forms version info (set during URL scan)
40 var remoteCfInfo = null;
41
42 /**
43 * Check if URL is the Contact Forms plugin CSS (skip it if same version)
44 */
45 function isPluginCss(url) {
46 if (!url) return false;
47 // Match both old and new CSS paths
48 var isPlugin = url.indexOf('contact-forms/assets/css/frontend.css') !== -1 ||
49 url.indexOf('contact-forms/assets/css/admin.css') !== -1 ||
50 url.indexOf('contact-forms/assets/css/accua-form-api.css') !== -1 ||
51 url.indexOf('contact-forms/accua-form-api.css') !== -1; // Old path
52 return isPlugin;
53 }
54
55 /**
56 * Check if plugin CSS should be skipped (same version as current)
57 */
58 function shouldSkipPluginCss(url) {
59 if (!isPluginCss(url)) return false;
60
61 // If we have remote version info and it matches current, skip it
62 if (remoteCfInfo && remoteCfInfo.remoteCssVersion) {
63 return remoteCfInfo.remoteCssVersion === remoteCfInfo.currentCssVersion;
64 }
65
66 // For admin page scanning, always skip plugin CSS
67 return true;
68 }
69
70 /**
71 * Estimate line number from CSS text and rule position
72 */
73 function estimateLineNumber(cssText, ruleText) {
74 if (!cssText || !ruleText) return '?';
75
76 var selectorMatch = ruleText.match(/^[^{]+/);
77 if (!selectorMatch) return '?';
78
79 var selector = selectorMatch[0].trim();
80 var index = cssText.indexOf(selector);
81
82 if (index === -1) return '?';
83
84 // Count newlines before this position
85 var textBefore = cssText.substring(0, index);
86 var lineNumber = (textBefore.match(/\n/g) || []).length + 1;
87
88 return lineNumber;
89 }
90
91 /**
92 * Parse a CSS rule and extract conflicting properties
93 */
94 function parseConflictingProperties(rule) {
95 var conflicts = [];
96 var style = rule.style;
97
98 if (!style) return conflicts;
99
100 for (var i = 0; i < style.length; i++) {
101 var prop = style[i];
102 // Check if this property is in our critical list
103 if (criticalProperties.indexOf(prop) !== -1) {
104 var value = style.getPropertyValue(prop);
105 var priority = style.getPropertyPriority(prop);
106 conflicts.push({
107 property: prop,
108 value: value,
109 important: priority === 'important'
110 });
111 }
112 }
113
114 return conflicts;
115 }
116
117 /**
118 * Format conflicting properties for display
119 */
120 function formatConflicts(conflicts) {
121 return conflicts.map(function(c) {
122 var important = c.important ? ' <span style="color:#d63638;font-weight:bold;">!important</span>' : '';
123 return '<code>' + c.property + ': ' + c.value + important + '</code>';
124 }).join('<br>');
125 }
126
127 /**
128 * Scan a single stylesheet for conflicts
129 */
130 function scanStylesheet(sheet, cssText) {
131 var results = [];
132 var url = sheet.href || '';
133
134 // Skip Contact Forms own CSS if same version
135 if (shouldSkipPluginCss(url)) {
136 return results;
137 }
138
139 try {
140 var rules = sheet.cssRules || sheet.rules || [];
141
142 for (var i = 0; i < rules.length; i++) {
143 var rule = rules[i];
144
145 // Only process style rules (not @media, @keyframes, etc.)
146 if (rule.type !== CSSRule.STYLE_RULE) continue;
147
148 var selector = rule.selectorText || '';
149
150 // Check if selector contains any monitored class
151 if (!classPattern.test(selector)) continue;
152
153 // Get conflicting properties
154 var conflicts = parseConflictingProperties(rule);
155
156 if (conflicts.length > 0) {
157 // Estimate line number
158 var lineNumber = '?';
159 if (cssText) {
160 lineNumber = estimateLineNumber(cssText, rule.cssText);
161 }
162
163 results.push({
164 url: url,
165 filename: getFilename(url),
166 line: lineNumber,
167 selector: selector,
168 conflicts: conflicts,
169 ruleText: rule.cssText
170 });
171 }
172 }
173 } catch (e) {
174 // Cross-origin stylesheets throw security errors
175 console.log('Cannot access stylesheet:', url, e.message);
176 }
177
178 return results;
179 }
180
181 /**
182 * Scan CSS text string for conflicts (for URL scanning)
183 */
184 function scanCssText(cssText, sourceUrl) {
185 var results = [];
186
187 // Skip Contact Forms own CSS if same version
188 if (shouldSkipPluginCss(sourceUrl)) {
189 return results;
190 }
191
192 // Parse CSS text manually with regex
193 // Match rules: selector { properties }
194 var rulePattern = /([^{}]+)\{([^{}]+)\}/g;
195 var match;
196 var position = 0;
197
198 while ((match = rulePattern.exec(cssText)) !== null) {
199 var selector = match[1].trim();
200 var properties = match[2].trim();
201
202 // Check if selector contains any monitored class
203 if (!classPattern.test(selector)) continue;
204
205 // Parse properties
206 var conflicts = [];
207 var propPattern = /([\w-]+)\s*:\s*([^;]+);?/g;
208 var propMatch;
209
210 while ((propMatch = propPattern.exec(properties)) !== null) {
211 var prop = propMatch[1].trim();
212 var value = propMatch[2].trim();
213
214 if (criticalProperties.indexOf(prop) !== -1) {
215 var isImportant = value.indexOf('!important') !== -1;
216 value = value.replace(/\s*!important\s*/i, '');
217
218 conflicts.push({
219 property: prop,
220 value: value,
221 important: isImportant
222 });
223 }
224 }
225
226 if (conflicts.length > 0) {
227 // Estimate line number
228 var textBefore = cssText.substring(0, match.index);
229 var lineNumber = (textBefore.match(/\n/g) || []).length + 1;
230
231 results.push({
232 url: sourceUrl,
233 filename: getFilename(sourceUrl),
234 line: lineNumber,
235 selector: selector,
236 conflicts: conflicts,
237 ruleText: match[0]
238 });
239 }
240 }
241
242 return results;
243 }
244
245 /**
246 * Scan all stylesheets on current page
247 */
248 function scanCurrentPage() {
249 var allResults = [];
250 var sheets = document.styleSheets;
251
252 for (var i = 0; i < sheets.length; i++) {
253 var sheet = sheets[i];
254
255 // Try to get CSS text for line number estimation
256 var cssText = '';
257 try {
258 if (sheet.cssRules) {
259 cssText = Array.from(sheet.cssRules).map(function(r) {
260 return r.cssText;
261 }).join('\n');
262 }
263 } catch (e) {
264 // Cross-origin
265 }
266
267 var results = scanStylesheet(sheet, cssText);
268 allResults = allResults.concat(results);
269 }
270
271 return allResults;
272 }
273
274 /**
275 * Display results in the table
276 */
277 function displayResults(results) {
278 var $table = $('#theme-helper-results');
279 var $tbody = $('#conflicts-tbody');
280 var $summary = $('#results-summary');
281
282 $tbody.empty();
283
284 if (results.length === 0) {
285 $summary.html(
286 '<span style="color:#00a32a;font-weight:bold;">✓ ' + i18n.noConflicts + '</span>' +
287 '<p style="margin-top:10px;">All scanned stylesheets appear compatible with Contact Forms.</p>'
288 );
289 $table.show();
290 return;
291 }
292
293 // Group by file
294 var byFile = {};
295 results.forEach(function(r) {
296 var key = r.filename;
297 if (!byFile[key]) byFile[key] = [];
298 byFile[key].push(r);
299 });
300
301 var fileCount = Object.keys(byFile).length;
302
303 $summary.html(
304 '<span style="color:#d63638;font-weight:bold;">⚠ ' + i18n.conflictsFound + '</span>' +
305 '<p style="margin-top:10px;">' +
306 results.length + ' conflicting rule(s) found in ' + fileCount + ' file(s).' +
307 '</p>'
308 );
309
310 // Populate table
311 results.forEach(function(r) {
312 var $row = $('<tr>');
313
314 // Stylesheet column - make it a clickable link
315 var urlTitle = r.url || '(unknown)';
316 var filenameHtml;
317 if (r.url && r.url.indexOf('(inline') === -1) {
318 // External URL - make it clickable
319 filenameHtml = '<a href="' + encodeURI(r.url) + '" target="_blank" title="' + urlTitle + '" style="text-decoration:none;">' +
320 '<strong>' + r.filename + '</strong> <span class="dashicons dashicons-external" style="font-size:14px;vertical-align:middle;"></span></a>';
321 } else {
322 filenameHtml = '<strong title="' + urlTitle + '">' + r.filename + '</strong>';
323 }
324 $row.append('<td>' + filenameHtml + '</td>');
325
326 // Line column
327 $row.append('<td>' + r.line + '</td>');
328
329 // Selector column
330 $row.append('<td><code>' + r.selector + '</code></td>');
331
332 // Properties column
333 $row.append('<td>' + formatConflicts(r.conflicts) + '</td>');
334
335 $tbody.append($row);
336 });
337
338 $table.show();
339 }
340
341 /**
342 * Show status message
343 */
344 function showStatus(message, type) {
345 var $status = $('#theme-helper-status');
346 var borderColor = type === 'error' ? '#d63638' : (type === 'success' ? '#00a32a' : '#0073aa');
347 var bgColor = type === 'error' ? '#fcf0f1' : (type === 'success' ? '#edfaef' : '#f0f6fc');
348
349 $status.css({
350 'border-left-color': borderColor,
351 'background': bgColor
352 });
353
354 $('#status-message').html(message);
355 $status.show();
356 }
357
358 /**
359 * Scan current page button handler
360 */
361 function handleScanCurrentPage() {
362 showStatus('<span class="spinner is-active" style="float:none;margin:0 5px 0 0;"></span> ' + i18n.scanning, 'info');
363
364 // Small delay to allow spinner to render
365 setTimeout(function() {
366 var results = scanCurrentPage();
367 displayResults(results);
368 showStatus('' + i18n.scanComplete + ' - ' + results.length + ' conflict(s) found', results.length > 0 ? 'error' : 'success');
369 }, 100);
370 }
371
372 /**
373 * Scan URL button handler
374 */
375 function handleScanUrl() {
376 var url = $('#scan-url').val().trim();
377
378 if (!url) {
379 showStatus('' + i18n.enterUrl, 'error');
380 return;
381 }
382
383 showStatus('<span class="spinner is-active" style="float:none;margin:0 5px 0 0;"></span> ' + i18n.fetchingCss, 'info');
384
385 $.ajax({
386 url: config.ajaxUrl,
387 method: 'POST',
388 data: {
389 action: 'accua_forms_fetch_url_css',
390 nonce: config.nonce,
391 url: url
392 },
393 success: function(response) {
394 if (!response.success) {
395 showStatus('' + (response.data?.message || i18n.scanError), 'error');
396 return;
397 }
398
399 var stylesheets = response.data.stylesheets || [];
400 var allResults = [];
401
402 // Store remote CF version info for version comparison
403 remoteCfInfo = {
404 remoteCssVersion: response.data.remoteCssVersion,
405 remoteVersion: response.data.remoteContactFormsVersion,
406 currentCssVersion: response.data.currentCssVersion,
407 currentVersion: response.data.currentVersion
408 };
409
410 // Scan each stylesheet
411 stylesheets.forEach(function(sheet) {
412 var results = scanCssText(sheet.content, sheet.url);
413 allResults = allResults.concat(results);
414 });
415
416 // Build version info message
417 var versionInfo = '';
418 if (remoteCfInfo.remoteCssVersion && remoteCfInfo.remoteCssVersion !== remoteCfInfo.currentCssVersion) {
419 versionInfo = '<br><span style="color:#dba617;">⚠ Remote site uses Contact Forms CSS v' + remoteCfInfo.remoteCssVersion +
420 ' (you have v' + remoteCfInfo.currentCssVersion + ')</span>';
421 } else if (remoteCfInfo.remoteCssVersion) {
422 versionInfo = '<br><span style="color:#00a32a;">✓ Remote site uses same Contact Forms CSS version (v' + remoteCfInfo.remoteCssVersion + ')</span>';
423 }
424
425 displayResults(allResults);
426 showStatus('' + i18n.scanComplete + ' - Scanned ' + stylesheets.length + ' stylesheet(s), ' + allResults.length + ' conflict(s) found' + versionInfo, allResults.length > 0 ? 'error' : 'success');
427 },
428 error: function() {
429 showStatus('' + i18n.scanError, 'error');
430 }
431 });
432 }
433
434 // Initialize on document ready
435 $(function() {
436 $('#scan-current-page').on('click', handleScanCurrentPage);
437 $('#scan-url-button').on('click', handleScanUrl);
438 $('#scan-url').on('keypress', function(e) {
439 if (e.which === 13) {
440 handleScanUrl();
441 }
442 });
443 });
444
445 })(jQuery);
446