| 1 |
/* eslint-disable no-console */ |
| 2 |
let headerInsertionCount = 0; |
| 3 |
let headerTitleTagCount = 0; |
| 4 |
let headerMetaTagCount = 0; |
| 5 |
let bodyHtmlTopInsertionCount = 0; |
| 6 |
let bodyHtmlBottomInsertionCount = 0; |
| 7 |
let linksUpdationCount = 0; |
| 8 |
const headersUpdationCount = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 }; |
| 9 |
let imagesAltsUpdationCount = 0; |
| 10 |
let footerHtmlInsertionCount = 0; |
| 11 |
|
| 12 |
const urlParams = new URLSearchParams(window.location.search); |
| 13 |
const diagnosticsExist = urlParams.has('diagnostics'); |
| 14 |
|
| 15 |
const consolePrint = strData => { |
| 16 |
if (diagnosticsExist) { |
| 17 |
console.log(strData); |
| 18 |
} |
| 19 |
}; |
| 20 |
|
| 21 |
// Fixed: Add HTML validation function to prevent XSS while allowing legitimate OTTO content |
| 22 |
const isValidHTMLContent = (htmlContent) => { |
| 23 |
if (!htmlContent || typeof htmlContent !== 'string') { |
| 24 |
return false; |
| 25 |
} |
| 26 |
|
| 27 |
// Only reject obviously malicious patterns while allowing legitimate SEO content |
| 28 |
|
| 29 |
// Reject content with potentially dangerous script content (but allow analytics/tracking) |
| 30 |
// Block inline javascript execution patterns but allow legitimate tracking scripts |
| 31 |
if (/<script[^>]*>[\s\S]*?(alert|prompt|confirm|eval|document\.write|document\.writeln)[\s\S]*?<\/script>/i.test(htmlContent)) { |
| 32 |
return false; |
| 33 |
} |
| 34 |
|
| 35 |
// Block dangerous javascript: protocols in links but allow legitimate ones |
| 36 |
if (/href\s*=\s*["']javascript:(?!void\(0\)|;)[\s\S]*?["']/i.test(htmlContent)) { |
| 37 |
return false; |
| 38 |
} |
| 39 |
|
| 40 |
// Block dangerous event handlers but allow common legitimate ones |
| 41 |
if (/\son(error|load)\s*=\s*["'][^"']*?(alert|prompt|eval|document\.write)[^"']*?["']/i.test(htmlContent)) { |
| 42 |
return false; |
| 43 |
} |
| 44 |
|
| 45 |
// Block attempts to access sensitive objects |
| 46 |
if (/(document\.cookie|localStorage|sessionStorage|window\.location\s*=)/i.test(htmlContent)) { |
| 47 |
return false; |
| 48 |
} |
| 49 |
|
| 50 |
return true; |
| 51 |
}; |
| 52 |
|
| 53 |
|
| 54 |
const replaceMetaData = metaData => { |
| 55 |
let headerElementExist = false; |
| 56 |
const { type, name, property, recommended_value } = metaData; |
| 57 |
|
| 58 |
// Meta descriptions are always enabled by default |
| 59 |
|
| 60 |
if (type === 'title') { |
| 61 |
headerElementExist = document.querySelector('title') || false; |
| 62 |
} else { |
| 63 |
const metaSelector = `meta[name="${(name || property)?.trim()}"], meta[property="${(name || property)?.trim()}"]`; |
| 64 |
headerElementExist = document.querySelector(metaSelector) || false; |
| 65 |
} |
| 66 |
|
| 67 |
if (headerElementExist) { |
| 68 |
consolePrint(`Header Meta - ${headerElementExist}`); |
| 69 |
if (type === 'title') { |
| 70 |
headerTitleTagCount++; |
| 71 |
consolePrint(`Replacing existing title content - ${recommended_value}`); |
| 72 |
// headerElementExist.innerHTML = recommended_value; |
| 73 |
// Fixed: Use textContent instead of innerHTML to prevent XSS |
| 74 |
headerElementExist.textContent = recommended_value; |
| 75 |
} else { |
| 76 |
consolePrint(`Replacing existing Meta content - ${recommended_value}`); |
| 77 |
headerMetaTagCount++; |
| 78 |
headerElementExist.setAttribute('content', recommended_value); |
| 79 |
} |
| 80 |
} else { |
| 81 |
if (type === 'title') { |
| 82 |
consolePrint(`Header Title Not Found - ${type}`); |
| 83 |
headerTitleTagCount++; |
| 84 |
// const titleTag = `<title>${recommended_value}</title>`; |
| 85 |
// consolePrint(`Inserting Title tag element: ${titleTag}`); |
| 86 |
// document.head.insertAdjacentHTML('afterbegin', titleTag); |
| 87 |
|
| 88 |
// Fixed: Create title element safely to prevent XSS |
| 89 |
const titleElement = document.createElement('title'); |
| 90 |
titleElement.textContent = recommended_value; |
| 91 |
consolePrint('Inserting Title tag element safely'); |
| 92 |
document.head.insertBefore(titleElement, document.head.firstChild); |
| 93 |
} else { |
| 94 |
consolePrint(`Header Meta Not Found - ${type}`); |
| 95 |
|
| 96 |
// Fixed: Create meta element safely to prevent XSS |
| 97 |
const metaElement = document.createElement('meta'); |
| 98 |
const metaAttribute = property ? 'property' : 'name'; |
| 99 |
// const metaTag = `<meta ${metaAttribute}="${property ? property : name}" content="${recommended_value}">`; |
| 100 |
|
| 101 |
metaElement.setAttribute(metaAttribute, property ? property : name); |
| 102 |
metaElement.setAttribute('content', recommended_value); |
| 103 |
headerMetaTagCount++; |
| 104 |
// consolePrint(`Inserting tag element: ${metaTag}`); |
| 105 |
// document.head.insertAdjacentHTML('afterbegin', metaTag); |
| 106 |
consolePrint('Inserting meta tag element safely'); |
| 107 |
document.head.insertBefore(metaElement, document.head.firstChild); |
| 108 |
} |
| 109 |
} |
| 110 |
}; |
| 111 |
|
| 112 |
const addAltTextToImages = images => { |
| 113 |
const allImgsElements = document.querySelectorAll('img'); |
| 114 |
allImgsElements?.forEach(imgElement => { |
| 115 |
images?.forEach(([imageUrl, altText]) => { |
| 116 |
Array.from(imgElement?.attributes).forEach(attribute => { |
| 117 |
if (attribute.value === imageUrl) { |
| 118 |
imagesAltsUpdationCount++; |
| 119 |
consolePrint(`Adding/updating alt text for ${imgElement} with: ${altText}`); |
| 120 |
imgElement.alt = altText; |
| 121 |
} |
| 122 |
}); |
| 123 |
}); |
| 124 |
}); |
| 125 |
consolePrint(`Total alt texts updated: ${imagesAltsUpdationCount}`); |
| 126 |
}; |
| 127 |
|
| 128 |
const applyPageData = pageData => { |
| 129 |
consolePrint(`Header HTML insertion started: ${pageData.header_html_insertion}`); |
| 130 |
//const htmlToInject = pageData.header_html_insertion ? `<!-- OTTO SEO Header Injection Start --> ${pageData.header_html_insertion} <!-- OTTO SEO Header Injection Ended --> ` : ''; |
| 131 |
// if (htmlToInject) { |
| 132 |
// document.head.insertAdjacentHTML('afterbegin', htmlToInject); |
| 133 |
// headerInsertionCount++; |
| 134 |
// } |
| 135 |
consolePrint(`Header HTML insertion ended: ${htmlToInject}`); |
| 136 |
|
| 137 |
try { |
| 138 |
consolePrint(`Header replacements started: ${pageData.header_replacements}`); |
| 139 |
if (Array.isArray(pageData.header_replacements)) { |
| 140 |
pageData.header_replacements?.forEach(data => { |
| 141 |
replaceMetaData(data); |
| 142 |
}); |
| 143 |
} |
| 144 |
consolePrint(`Header replacements ended: ${pageData.header_replacements}`); |
| 145 |
} catch (error) { |
| 146 |
consolePrint(`Header replacements Error: ${error}`); |
| 147 |
} |
| 148 |
|
| 149 |
const bodyElement = document.querySelector('body'); |
| 150 |
|
| 151 |
consolePrint(`Body top HTML insertion started: ${pageData.body_top_html_insertion}`); |
| 152 |
const topHtmlToInject = pageData.body_top_html_insertion ? `<!-- OTTO SEO Body Top Injection Start --> ${pageData.body_top_html_insertion} <!-- OTTO SEO Body Top Injection Ended --> ` : ''; |
| 153 |
if (topHtmlToInject) { |
| 154 |
bodyHtmlTopInsertionCount++; |
| 155 |
// bodyElement.insertAdjacentHTML('afterbegin', topHtmlToInject); |
| 156 |
// Fixed: Validate HTML content before insertion to prevent XSS |
| 157 |
if (isValidHTMLContent(pageData.body_top_html_insertion)) { |
| 158 |
bodyElement.insertAdjacentHTML('afterbegin', topHtmlToInject); |
| 159 |
} else { |
| 160 |
consolePrint('WARNING: Invalid HTML content detected, skipping injection'); |
| 161 |
} |
| 162 |
} |
| 163 |
consolePrint(`Body top HTML insertion ended: ${topHtmlToInject}`); |
| 164 |
|
| 165 |
consolePrint(`Body bottom HTML insertion started: ${pageData.body_bottom_html_insertion}`); |
| 166 |
const bottomHtmlToInject = pageData.body_bottom_html_insertion ? `<!-- OTTO SEO Body Bottom Injection Start --> ${pageData.body_bottom_html_insertion} <!-- OTTO SEO Body Bottom Injection Ended --> ` : ''; |
| 167 |
if (bottomHtmlToInject) { |
| 168 |
bodyHtmlBottomInsertionCount++; |
| 169 |
// bodyElement.insertAdjacentHTML('beforeend', bottomHtmlToInject); |
| 170 |
// Fixed: Validate HTML content before insertion to prevent XSS |
| 171 |
if (isValidHTMLContent(pageData.body_bottom_html_insertion)) { |
| 172 |
bodyElement.insertAdjacentHTML('beforeend', bottomHtmlToInject); |
| 173 |
} else { |
| 174 |
consolePrint('WARNING: Invalid HTML content detected, skipping injection'); |
| 175 |
} |
| 176 |
} |
| 177 |
consolePrint(`Body bottom HTML insertion ended: ${bottomHtmlToInject}`); |
| 178 |
|
| 179 |
consolePrint('Body substitutions started'); |
| 180 |
for (const [bodySubstitutionsKey, data] of Object.entries(pageData.body_substitutions)) { |
| 181 |
if (bodySubstitutionsKey !== 'images') { |
| 182 |
consolePrint(`${bodySubstitutionsKey} - updating element value: ${data}`); |
| 183 |
if (bodySubstitutionsKey === 'links') { |
| 184 |
const listData = Object.entries(data); |
| 185 |
if (listData.length) { |
| 186 |
document.querySelectorAll(`a[href="${listData[0][0]}"]`)?.forEach(element => { |
| 187 |
linksUpdationCount++; |
| 188 |
element.href = listData[0][1]; |
| 189 |
}); |
| 190 |
} |
| 191 |
} else if (bodySubstitutionsKey === 'headings') { |
| 192 |
data?.forEach(item => { |
| 193 |
document.querySelectorAll(item.type)?.forEach(element => { |
| 194 |
if (element.textContent.trim() === item.current_value.trim()) { |
| 195 |
consolePrint(`${item.type} - heading - ${element.textContent} - Recommended heading - ${item.recommended_value}`); |
| 196 |
headersUpdationCount[item.type]++; |
| 197 |
// element.innerHTML = element.innerHTML.replace(item.current_value, item.recommended_value); |
| 198 |
// Fixed: Safely replace content while preserving formatting and preventing XSS |
| 199 |
if (item.recommended_value && typeof item.recommended_value === 'string') { |
| 200 |
// If the recommended value contains HTML tags, validate it first |
| 201 |
if (/<[^>]+>/.test(item.recommended_value)) { |
| 202 |
if (isValidHTMLContent(item.recommended_value)) { |
| 203 |
element.innerHTML = element.innerHTML.replace(item.current_value, item.recommended_value); |
| 204 |
} else { |
| 205 |
consolePrint('WARNING: Invalid HTML in heading recommendation, using text only'); |
| 206 |
element.textContent = element.textContent.replace(item.current_value, item.recommended_value); |
| 207 |
} |
| 208 |
} else { |
| 209 |
// Safe text replacement |
| 210 |
element.textContent = element.textContent.replace(item.current_value, item.recommended_value); |
| 211 |
} |
| 212 |
} |
| 213 |
} |
| 214 |
}); |
| 215 |
}); |
| 216 |
} |
| 217 |
} |
| 218 |
} |
| 219 |
consolePrint('Body substitutions ended'); |
| 220 |
|
| 221 |
if (pageData.header_replacements?.length > 0) { |
| 222 |
const canonicalLink = pageData.header_replacements.find(item=> item?.type === 'link' && item?.rel === 'canonical'); |
| 223 |
const element = document.querySelector('link[rel="canonical"]'); |
| 224 |
if (canonicalLink && element) { |
| 225 |
element.href = canonicalLink?.recommended_value; |
| 226 |
} |
| 227 |
} |
| 228 |
|
| 229 |
consolePrint('Intersection of missing alt tags started'); |
| 230 |
pageData?.body_substitutions?.images && addAltTextToImages(Object.entries(pageData?.body_substitutions?.images)); |
| 231 |
consolePrint('Intersection of missing alt tags ended'); |
| 232 |
|
| 233 |
consolePrint(`Footer HTML insertion started: ${pageData.footer_html_insertion}`); |
| 234 |
if (pageData.footer_html_insertion) { |
| 235 |
let footerElement = document.querySelector('footer'); |
| 236 |
if (!footerElement) { |
| 237 |
consolePrint('Footer element not found'); |
| 238 |
footerElement = document.createElement('footer'); |
| 239 |
document.body.appendChild(footerElement); |
| 240 |
consolePrint('Footer element added to footer'); |
| 241 |
} |
| 242 |
|
| 243 |
const footerHtmlToInject = pageData.footer_html_insertion ? `<!-- OTTO SEO Footer Injection Start --> ${pageData.footer_html_insertion} <!-- OTTO SEO Footer Injection Ended --> ` : ''; |
| 244 |
footerHtmlInsertionCount++; |
| 245 |
// footerElement.insertAdjacentHTML('beforeend', footerHtmlToInject); |
| 246 |
// consolePrint(`Footer HTML inserted: ${footerHtmlToInject}`); |
| 247 |
// Fixed: Validate HTML content before insertion to prevent XSS |
| 248 |
if (isValidHTMLContent(pageData.footer_html_insertion)) { |
| 249 |
footerElement.insertAdjacentHTML('beforeend', footerHtmlToInject); |
| 250 |
consolePrint('Footer HTML inserted safely'); |
| 251 |
} else { |
| 252 |
consolePrint('WARNING: Invalid footer HTML content detected, skipping injection'); |
| 253 |
} |
| 254 |
} |
| 255 |
consolePrint(`Footer HTML insertion ended: ${pageData.footer_html_insertion}`); |
| 256 |
|
| 257 |
consolePrint('*************************************************************'); |
| 258 |
consolePrint('******************** Diagnostics Summary ********************'); |
| 259 |
consolePrint(`Header Insertions: ${headerInsertionCount}`); |
| 260 |
consolePrint(`Header Title Updated: ${headerTitleTagCount}`); |
| 261 |
consolePrint(`Body Top HTML Insertions: ${bodyHtmlTopInsertionCount}`); |
| 262 |
consolePrint(`Body Bottom HTML Insertions: ${bodyHtmlBottomInsertionCount}`); |
| 263 |
consolePrint(`Footer HTML Insertions: ${footerHtmlInsertionCount}`); |
| 264 |
|
| 265 |
consolePrint(`Header Meta Tag Updated: ${headerMetaTagCount} out of ${Array.isArray(pageData.header_replacements) ? pageData.header_replacements?.filter(item => item.type !== 'title')?.length : 0}`); |
| 266 |
consolePrint(`Image Alt Replacements: ${imagesAltsUpdationCount} out of ${('images' in pageData.body_substitutions) ? Object.keys(pageData.body_substitutions?.images)?.length : 0}`); |
| 267 |
consolePrint(`Link Replacements: ${linksUpdationCount} out of ${('links' in pageData.body_substitutions) ? Object.keys(pageData.body_substitutions?.links)?.length : 0}`); |
| 268 |
Object.entries(headersUpdationCount).forEach(value => { |
| 269 |
consolePrint(`${value[0]} headings replaced - ${value[1]}`); |
| 270 |
}); |
| 271 |
consolePrint('*************************************************************'); |
| 272 |
consolePrint('*************************************************************'); |
| 273 |
}; |
| 274 |
|
| 275 |
const initializeScript = () => { |
| 276 |
consolePrint('Script initialization'); |
| 277 |
const uuid = document.getElementById('sa-otto')?.getAttribute('data-uuid') || document.getElementById('searchatlas')?.getAttribute('data-uuid') || document.getElementById('sa-dynamic-optimization')?.getAttribute('data-uuid'); |
| 278 |
window.otto_js_installed = true; |
| 279 |
window.otto_js_uuid = uuid; |
| 280 |
consolePrint(`UUID: ${uuid}`); |
| 281 |
// const fetchData = async (pageUrl, uuid) => { |
| 282 |
// let apiUrl = `https://sa.searchatlas.com/api/v2/otto-url-details/?url=${pageUrl}`; |
| 283 |
// if (uuid) apiUrl += `&uuid=${uuid}`; |
| 284 |
|
| 285 |
// try { |
| 286 |
// const response = await fetch(apiUrl); |
| 287 |
// if (!response.ok) throw new Error('API call failed.'); |
| 288 |
// const pageData = await response.json(); |
| 289 |
// consolePrint(`API response: ${pageData}`); |
| 290 |
// applyPageData(pageData); |
| 291 |
// } catch (error) { |
| 292 |
// console.error('Fetch error:', error); |
| 293 |
// } |
| 294 |
// }; |
| 295 |
jQuery.ajax({ |
| 296 |
url: otto_ajax_object.ajax_url, |
| 297 |
type: 'POST', |
| 298 |
data: { |
| 299 |
action: 'metasyn_otto_ajax', |
| 300 |
nonce: otto_ajax_object.nonce, |
| 301 |
post_id: otto_ajax_object.post_id |
| 302 |
}, |
| 303 |
success: function (response) { |
| 304 |
applyPageData(pageData); |
| 305 |
}, |
| 306 |
error: function (xhr, status, error) { |
| 307 |
console.log('Error:', error); |
| 308 |
} |
| 309 |
}); |
| 310 |
|
| 311 |
consolePrint('Script ended'); |
| 312 |
}; |
| 313 |
|
| 314 |
initializeScript(); |