PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | admin/js/metasync-admin.js +225 -30 2.6.23 → 2.7.2 View file →
@@ -202,8 +202,134 @@
202 202 return whitelabelFields.join('&');
203 203 }
204 204
205 205 /**
206 + * White-label branding fields that are mirrored into the metasync.php
207 + * plugin header. Keyed by the form input name.
208 + */
209 + var brandingFieldConfigs = {
210 + 'metasync_options[general][white_label_plugin_name]': { label: 'Plugin Name', url: false },
211 + 'metasync_options[general][white_label_plugin_description]': { label: 'Plugin Description', url: false },
212 + 'metasync_options[general][white_label_plugin_author]': { label: 'Author', url: false },
213 + 'metasync_options[general][white_label_plugin_author_uri]': { label: 'Author URL', url: true },
214 + 'metasync_options[general][white_label_plugin_uri]': { label: 'Plugin URL', url: true }
215 + };
216 +
217 + /**
218 + * Client-side mirror of the server-side plugin-header contract for the
219 + * white-label branding fields, for immediate feedback while typing or
220 + * submitting. The server-side validation stays authoritative.
221 + *
222 + * @param {string} value Trimmed field value.
223 + * @param {boolean} isUrl Whether the value must be an http(s) URL.
224 + * @returns {string} Empty when valid, otherwise the problem description.
225 + */
226 + function validateBrandingValue(value, isUrl) {
227 + if (value === '') {
228 + return '';
229 + }
230 + if (/[\r\n]/.test(value)) {
231 + return 'must be a single line';
232 + }
233 + if (value.indexOf('*/') !== -1) {
234 + return 'must not contain "*/"';
235 + }
236 + if (/<\?php|\?>/i.test(value)) {
237 + return 'must not contain PHP tags';
238 + }
239 + // Control characters, line/paragraph separators, and invisible format
240 + // characters (including the bidirectional marks used for brand spoofing).
241 + // Unicode property escapes cover exactly the ranges the server checks
242 + // (Cc, Cf, Zl, Zp) without literal control-character sequences.
243 + try {
244 + if (/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value)) {
245 + return 'must not contain control or invisible characters';
246 + }
247 + } catch (e) {
248 + // Very old engines without Unicode property escapes: fall through,
249 + // the server-side check remains authoritative.
250 + }
251 + // Mirror of the server allow-list: letters (incl. Unicode), marks,
252 + // numbers, punctuation, symbols and space separators (incl. NBSP and
253 + // the ideographic space, which the server-side contract accepts).
254 + try {
255 + if (!/^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]+$/u.test(value)) {
256 + return 'contains characters that are not allowed';
257 + }
258 + } catch (e) {
259 + // Very old engines without Unicode property escapes: fall through,
260 + // the server-side check remains authoritative.
261 + }
262 + if (isUrl) {
263 + var parsed;
264 + try {
265 + parsed = new URL(value);
266 + } catch (e) {
267 + return 'must be a valid http:// or https:// address';
268 + }
269 + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
270 + return 'must be a valid http:// or https:// address';
271 + }
272 + }
273 + return '';
274 + }
275 +
276 + /**
277 + * Remove all inline white-label branding field errors.
278 + */
279 + function clearBrandingFieldErrors() {
280 + $('.metasync-branding-field-error').remove();
281 + }
282 +
283 + /**
284 + * Show an inline error beside a branding input. Accepts either the full
285 + * input name or the bare setting key (as returned by the server).
286 + *
287 + * @param {string} fieldKey Input name or white-label setting key.
288 + * @param {string} message Error message to display.
289 + */
290 + function showBrandingFieldError(fieldKey, message) {
291 + var inputName = brandingFieldConfigs[fieldKey]
292 + ? fieldKey
293 + : 'metasync_options[general][' + fieldKey + ']';
294 + var input = $('input[name="' + inputName + '"]');
295 + if (input.length === 0) {
296 + return;
297 + }
298 + input.last().after(
299 + $('<p>').addClass('description metasync-branding-field-error')
300 + .css({ color: 'var(--dashboard-error, #b32d2e)', fontWeight: 600, marginTop: '4px' })
301 + .text(message)
302 + );
303 + }
304 +
305 + /**
306 + * Validate every branding input currently in the DOM (both save paths —
307 + * the form submit handler and the floating "Save Now" button — call this).
308 + * Renders the inline per-field errors and returns the message list.
309 + *
310 + * @returns {string[]} Error messages; empty when everything is valid.
311 + */
312 + function validateBrandingFieldsInDom() {
313 + clearBrandingFieldErrors();
314 + var problems = [];
315 + Object.keys(brandingFieldConfigs).forEach(function (inputName) {
316 + var config = brandingFieldConfigs[inputName];
317 + var input = $('input[name="' + inputName + '"]');
318 + if (input.length === 0) {
319 + return;
320 + }
321 + var problem = validateBrandingValue(String(input.val() || '').trim(), config.url);
322 + if (problem) {
323 + var fullMessage = config.label + ' ' + problem + '.';
324 + showBrandingFieldError(inputName, fullMessage);
325 + problems.push(fullMessage);
326 + }
327 + });
328 + return problems;
329 + }
330 +
331 + /**
206 332 * Display notice message in plugin area
207 333 * @param {string} type - Notice type: 'success' or 'error'
208 334 * @param {string} title - Notice title
209 335 * @param {string} message - Notice message
@@ -271,9 +397,10 @@
271 397
272 398 function metasyncLGLogin(user, pass) {
273 399 jQuery.post(ajaxurl, {
274 400 action: 'metasync_lglogin',
275 - username: user, password: pass
401 + username: user, password: pass,
402 + nonce: metaSync.lglogin_nonce || ''
276 403 }, function (response) {
277 404 if (typeof response.token !== 'undefined') {
278 405 $('#linkgraph_token').val(response.token);
279 406 $('#linkgraph_customer_id').val(response.customer_id);
@@ -920,9 +1047,18 @@
920 1047 }, {
921 1048 text: '📚 Learn More About Registration',
922 1049 action: function () {
923 1050 var docDomain = metaSync.documentation_domain || 'https://searchatlas.com';
924 - window.open(docDomain, '_blank');
1051 + try {
1052 + var parsedDoc = new URL(docDomain);
1053 + if (parsedDoc.protocol === 'https:' || parsedDoc.protocol === 'http:') {
1054 + var a = document.createElement('a');
1055 + a.href = parsedDoc.href;
1056 + a.target = '_blank';
1057 + a.rel = 'noopener';
1058 + a.click();
1059 + }
1060 + } catch (e) { /* invalid URL */ }
925 1061 }
926 1062 }, {
927 1063 text: '🔄 Try Authentication Again',
928 1064 action: function () {
@@ -1639,9 +1775,23 @@
1639 1775 $('#' + id).val('');
1640 1776 }
1641 1777 }
1642 1778
1779 + function updateAnalyticsConsentStatus() {
1780 + var $toggle = $('#metasync_analytics_opt_in');
1781 + var $status = $('.metasync-consent-status');
1782 + if (!$toggle.length || !$status.length) {
1783 + return;
1784 + }
1785 +
1786 + var label = $toggle.is(':checked') ? $status.data('enabled-label') : $status.data('disabled-label');
1787 + $status.text(label);
1788 + }
1789 +
1790 + $(document).on('change', '#metasync_analytics_opt_in', updateAnalyticsConsentStatus);
1791 +
1643 1792 $(function () {
1793 + updateAnalyticsConsentStatus();
1644 1794 $('#addNewTime').on('click', function () {
1645 1795 $('#daysTime').append(
1646 1796 '<li>' +
1647 1797 '<select name="metasync_options[localseo][days][]">' +
@@ -1689,31 +1839,10 @@
1689 1839 });
1690 1840 $(document).on('click', '#number-delete', deleteNumber);
1691 1841 });
1692 1842
1693 - function deleteSourceUrl() {
1694 - $(this).parent().remove();
1695 - }
1696 1843 $(function () {
1697 - $('#addNewSourceUrl').on('click', function () {
1698 - $('#source_urls').append(
1699 - '<li>' +
1700 - '<input type="text" class="regular-text" name="source_url[]">' +
1701 - '<select name="search_type[]">' +
1702 - '<option value="exact">Exact</option>' +
1703 - '<option value="contain">Contain</option>' +
1704 - '<option value="start">Start With</option>' +
1705 - '<option value="end">End With</option>' +
1706 - '</select>' +
1707 - '<button id="source_url_delete">Remove</button>' +
1708 - '</li>');
1709 - return;
1710 - });
1711 - $(document).on('click', '#source_url_delete', deleteSourceUrl);
1712 - });
1713 1844
1714 - $(function () {
1715 -
1716 1845 setToken();
1717 1846
1718 1847 $('body').on('click', '#wp_metasync_sync', function (e) {
1719 1848 e.preventDefault();
@@ -2179,9 +2308,10 @@
2179 2308 url: ajaxurl,
2180 2309 type: 'GET',
2181 2310 data: {
2182 2311 action: 'metasync_clear_otto_cache',
2183 - clear_otto_cache: 1
2312 + clear_otto_cache: 1,
2313 + nonce: metaSync.clear_otto_cache_nonce || ''
2184 2314 },
2185 2315 success: function (response) {
2186 2316 const now = new Date();
2187 2317 $('#clear_otto_caches').text('Cache Cleared ' + now.toLocaleTimeString());
@@ -2281,8 +2411,27 @@
2281 2411 return true; // Let the form submit normally
2282 2412 }
2283 2413
2284 2414 e.preventDefault(); // Prevent the default form submission for regular settings
2415 +
2416 + // Immediate client-side validation of the white-label branding
2417 + // fields (the server-side contract stays authoritative): block the
2418 + // submit and show an error beside each invalid field instead of
2419 + // letting an invalid value reach the server and get rejected there.
2420 + var brandingValidationErrors = validateBrandingFieldsInDom();
2421 + if (brandingValidationErrors.length > 0) {
2422 + var $brandingErrorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap');
2423 + var $brandingErrorList = $('<ul>');
2424 + brandingValidationErrors.forEach(function (errorMessage) {
2425 + $brandingErrorList.append($('<li>').text(errorMessage));
2426 + });
2427 + $brandingErrorNotice.append($brandingErrorList);
2428 + $('.metasync-error-wrap').remove();
2429 + $('#metaSyncGeneralSetting').before($brandingErrorNotice);
2430 + $('html, body').animate({ scrollTop: 0 }, 'slow');
2431 + return;
2432 + }
2433 +
2285 2434 var actionField = $(this).find('input[name="action"]');
2286 2435 var optionPage= $(this).find('input[name="option_page"]');
2287 2436 var wpHttpReferer= $(this).find('input[name="_wp_http_referer"]');
2288 2437 var wpnonce= $(this).find('input[name="_wpnonce"]');
@@ -2358,12 +2507,13 @@
2358 2507 } catch (e) { /* invalid URL */ }
2359 2508 }else {
2360 2509 // Handle error response
2361 2510 const errors = response.data?.errors || [];
2511 + const fieldErrors = response.data?.field_errors || {};
2362 2512
2363 2513 // Build error notice using DOM methods to avoid XSS
2364 2514 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap');
2365 - if (Array.isArray(errors)) {
2515 + if (Array.isArray(errors) && errors.length > 0) {
2366 2516 var $ul = $('<ul>');
2367 2517 errors.forEach(function (err) {
2368 2518 $ul.append($('<li>').text(err));
2369 2519 });
@@ -2375,8 +2525,15 @@
2375 2525
2376 2526 // Insert the error message before the form
2377 2527 $('#metaSyncGeneralSetting').before($errorNotice);
2378 2528
2529 + // Field-specific feedback: show the server's per-field
2530 + // error beside each rejected white-label branding input.
2531 + clearBrandingFieldErrors();
2532 + Object.keys(fieldErrors).forEach(function (settingKey) {
2533 + showBrandingFieldError(settingKey, fieldErrors[settingKey]);
2534 + });
2535 +
2379 2536 // Scroll to the top to ensure visibility
2380 2537 $('html, body').animate({ scrollTop: 0 }, 'slow');
2381 2538 }
2382 2539
@@ -2613,17 +2770,47 @@
2613 2770
2614 2771 // Save Changes function - use AJAX instead of form submission
2615 2772 window.saveChanges = function () {
2616 2773 isSaving = true;
2617 -
2618 - // Completely remove the floating notification immediately when clicked
2774 +
2775 + var $form = $('#metaSyncGeneralSetting');
2776 + if ($form.length > 0) {
2777 + // Immediate client-side validation of the white-label branding
2778 + // fields — the floating save bypasses the form submit handler,
2779 + // so the same contract is enforced here too. The server-side
2780 + // validation stays authoritative. Runs BEFORE the notification
2781 + // is removed so a blocked save keeps the persistent unsaved-
2782 + // changes cue visible.
2783 + var brandingProblems = validateBrandingFieldsInDom();
2784 + if (brandingProblems.length > 0) {
2785 + var $brandingNotice = $('<div>').addClass('notice notice-error metasync-save-notice').css({ margin: '20px 0', padding: '12px' });
2786 + var $brandingList = $('<ul>');
2787 + brandingProblems.forEach(function (errorMessage) {
2788 + $brandingList.append($('<li>').text(errorMessage));
2789 + });
2790 + $brandingNotice.append($brandingList);
2791 + $('.metasync-save-notice').remove();
2792 + var $navWrapperForBranding = $('.metasync-nav-wrapper');
2793 + if ($navWrapperForBranding.length > 0) {
2794 + $navWrapperForBranding.after($brandingNotice);
2795 + } else {
2796 + $('.metasync-dashboard-wrap').prepend($brandingNotice);
2797 + }
2798 + $('html, body').animate({ scrollTop: 0 }, 'slow');
2799 + isSaving = false;
2800 + return;
2801 + }
2802 + }
2803 +
2804 + // Completely remove the floating notification immediately when the
2805 + // save is actually proceeding (a blocked save left it in place).
2619 2806 var $notification = $('.metasync-unsaved-notification');
2620 2807 if ($notification.length > 0) {
2621 2808 $notification.remove(); // Completely remove from DOM, no animations
2622 2809 }
2623 -
2624 - var $form = $('#metaSyncGeneralSetting');
2810 +
2625 2811 if ($form.length > 0) {
2812 +
2626 2813 // Get form data and submit via AJAX
2627 2814 var formData = $form.serialize();
2628 2815
2629 2816 // Ensure ALL whitelabel data is included using helper function
@@ -2714,8 +2901,9 @@
2714 2901 }
2715 2902 } else {
2716 2903 // Handle validation errors
2717 2904 var errors = response.data && response.data.errors ? response.data.errors : [];
2905 + var fieldErrors = (response.data && response.data.field_errors) ? response.data.field_errors : {};
2718 2906
2719 2907 // Build error notice using DOM methods to avoid XSS
2720 2908 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap').css({ margin: '20px', padding: '12px' });
2721 2909 if (Array.isArray(errors)) {
@@ -2739,9 +2927,16 @@
2739 2927 } else {
2740 2928 // Fallback: insert at top of plugin content area
2741 2929 $('.metasync-dashboard-wrap').prepend($errorNotice);
2742 2930 }
2743 -
2931 +
2932 + // Field-specific feedback: show the server's per-field
2933 + // error beside each rejected white-label branding input.
2934 + clearBrandingFieldErrors();
2935 + Object.keys(fieldErrors).forEach(function (settingKey) {
2936 + showBrandingFieldError(settingKey, fieldErrors[settingKey]);
2937 + });
2938 +
2744 2939 // Scroll to the error message for better visibility
2745 2940 $('html, body').animate({ scrollTop: 0 }, 'slow');
2746 2941
2747 2942 setTimeout(function () {