Mudanças entre as edições de "Widget:Item"
Ir para navegação
Ir para pesquisar
m |
m |
||
| Linha 1: | Linha 1: | ||
<includeonly> | <includeonly> | ||
<!-- | <!-- | ||
Tooltip: | Tooltip lazy: passivas/descrição/valor NÃO vão no HTML da página. | ||
No hover (mouse real), busca {{#invoke:Item|tooltip|key=...}} e monta na hora. | |||
Defesa (Widget:Item): rate limit, eventos não confiáveis (console/automação), | |||
tamper em fetch/XHR. Se disparar: apaga cache, remove data-item-key e tooltips | |||
do DOM — ícones ficam. Staff/editor (wgUserGroups) não é afetado. | |||
Tooltip normal continua para quem usa mouse de verdade. | |||
--> | --> | ||
<style> | <style> | ||
| Linha 265: | Linha 269: | ||
.item-tooltip.tooltip-shift-left .item-tooltip-arrow { | .item-tooltip.tooltip-shift-left .item-tooltip-arrow { | ||
margin-right: var(--arrow-offset, 12px); | margin-right: var(--arrow-offset, 12px); | ||
} | |||
.item-wrapper.item-tooltip-loading { | |||
opacity: 0.85; | |||
} | } | ||
</style> | </style> | ||
| Linha 275: | Linha 283: | ||
var activeTip = null; | var activeTip = null; | ||
var activeWrapper = null; | var activeWrapper = null; | ||
var tooltipGen = 0; | |||
var tooltipHtmlCache = Object.create(null); | |||
/* ===== Defesa anti-scrape (tooltip continua normal pro usuário) ===== */ | |||
var defenseTripped = false; | |||
var untrustedStrikes = 0; | |||
var tooltipFetchLog = []; | |||
var RATE_WINDOW_MS = 120000; | |||
var RATE_MAX = 45; | |||
var BURST_WINDOW_MS = 15000; | |||
var BURST_MAX = 18; | |||
var UNTRUSTED_MAX = 2; | |||
var nativeFetch = window.fetch ? window.fetch.bind(window) : null; | |||
var nativeXHROpen = XMLHttpRequest.prototype.open; | |||
function isStaffUser() { | |||
try { | |||
if (!window.mw || !mw.config) return false; | |||
var groups = mw.config.get("wgUserGroups") || []; | |||
for (var i = 0; i < groups.length; i++) { | |||
var g = groups[i]; | |||
if (g === "sysop" || g === "bureaucrat" || g === "editor" || g === "staff") { | |||
return true; | |||
} | |||
} | |||
} catch (e) { /* ignore */ } | |||
return false; | |||
} | |||
function clearTooltipCache() { | |||
Object.keys(tooltipHtmlCache).forEach(function (k) { | |||
delete tooltipHtmlCache[k]; | |||
}); | |||
} | |||
function | function purgeTooltipData() { | ||
hideTooltip(); | hideTooltip(); | ||
clearTooltipCache(); | |||
var tips = document.querySelectorAll(".item-tooltip"); | |||
for (var i = 0; i < tips.length; i++) { | |||
tips[i].remove(); | |||
} | |||
var wrappers = document.querySelectorAll(".item-has-tooltip"); | |||
for (var j = 0; j < wrappers.length; j++) { | |||
wrappers[j].removeAttribute("data-item-key"); | |||
wrappers[j].removeAttribute("data-item-lang"); | |||
wrappers[j].classList.remove("item-has-tooltip", "item-tooltip-loading"); | |||
} | |||
} | |||
function triggerDefense() { | |||
if (defenseTripped || isStaffUser()) return; | |||
defenseTripped = true; | |||
purgeTooltipData(); | |||
} | |||
function registerTooltipFetch() { | |||
if (defenseTripped || isStaffUser()) return true; | |||
var now = Date.now(); | |||
tooltipFetchLog.push(now); | |||
var i = 0; | |||
while (i < tooltipFetchLog.length) { | |||
if (now - tooltipFetchLog[i] > RATE_WINDOW_MS) { | |||
tooltipFetchLog.splice(i, 1); | |||
} else { | |||
i++; | |||
} | |||
} | |||
if (tooltipFetchLog.length > RATE_MAX) { | |||
triggerDefense(); | |||
return false; | |||
} | |||
var recent = 0; | |||
for (var j = tooltipFetchLog.length - 1; j >= 0; j--) { | |||
if (now - tooltipFetchLog[j] <= BURST_WINDOW_MS) recent++; | |||
} | |||
if (recent > BURST_MAX) { | |||
triggerDefense(); | |||
return false; | |||
} | |||
return true; | |||
} | |||
function noteUntrustedEvent() { | |||
if (defenseTripped || isStaffUser()) return; | |||
untrustedStrikes++; | |||
if (untrustedStrikes >= UNTRUSTED_MAX) { | |||
triggerDefense(); | |||
} | |||
} | |||
function checkIntegrity() { | |||
if (defenseTripped || isStaffUser()) return; | |||
if (nativeFetch && window.fetch !== nativeFetch) { | |||
triggerDefense(); | |||
return; | |||
} | |||
if (XMLHttpRequest.prototype.open !== nativeXHROpen) { | |||
triggerDefense(); | |||
} | |||
} | |||
setInterval(checkIntegrity, 2500); | |||
/* Console aberto: não desliga tooltip sozinho — só se combinar com abuso */ | |||
(function watchConsoleAccess() { | |||
if (isStaffUser()) return; | |||
var bait = {}; | |||
var tripped = false; | |||
try { | |||
Object.defineProperty(bait, "id", { | |||
get: function () { | |||
if (!tripped && !isStaffUser()) { | |||
tripped = true; | |||
if (tooltipFetchLog.length >= 8 || untrustedStrikes > 0) { | |||
triggerDefense(); | |||
} | |||
} | |||
return ""; | |||
} | |||
}); | |||
} catch (e) { return; } | |||
setInterval(function () { | |||
if (defenseTripped || isStaffUser()) return; | |||
try { console.debug(bait); } catch (e2) { /* ignore */ } | |||
}, 4000); | |||
})(); | |||
function getApiUrl() { | |||
if (window.mw && mw.util && mw.util.wikiScript) { | |||
return mw.util.wikiScript("api"); | |||
} | |||
var path = window.location.pathname || ""; | |||
if (path.indexOf("/index.php") !== -1) { | |||
return path.split("/index.php")[0] + "/api.php"; | |||
} | |||
return "/api.php"; | |||
} | |||
function buildTooltipInvoke(key, lang) { | |||
var safeKey = String(key).replace(/\|/g, "").replace(/\}/g, "").replace(/\{/g, ""); | |||
var safeLang = String(lang || "pt-br").replace(/\|/g, ""); | |||
return "{{#invoke:Item|tooltip|key=" + safeKey + "|lang=" + safeLang + "}}"; | |||
} | |||
function fetchParse(wikitext) { | |||
if (window.mw && mw.Api) { | |||
return new mw.Api().post({ | |||
action: "parse", | |||
text: wikitext, | |||
contentmodel: "wikitext", | |||
disablelimitreport: true | |||
}).then(function (data) { | |||
return data.parse.text["*"]; | |||
}); | |||
} | |||
var body = new URLSearchParams(); | |||
body.set("action", "parse"); | |||
body.set("format", "json"); | |||
body.set("text", wikitext); | |||
body.set("contentmodel", "wikitext"); | |||
body.set("disablelimitreport", "1"); | |||
return fetch(getApiUrl(), { | |||
method: "POST", | |||
credentials: "same-origin", | |||
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, | |||
body: body.toString() | |||
}) | |||
.then(function (res) { return res.json(); }) | |||
.then(function (data) { | |||
if (data.error) throw new Error(data.error.info || "API parse failed"); | |||
return data.parse.text["*"]; | |||
}); | |||
} | |||
function fetchTooltipHtml(key, lang) { | |||
if (defenseTripped) return Promise.reject(new Error("defense")); | |||
var cacheKey = key + "\0" + (lang || "pt-br"); | |||
if (tooltipHtmlCache[cacheKey]) { | |||
return Promise.resolve(tooltipHtmlCache[cacheKey]); | |||
} | |||
if (!registerTooltipFetch()) { | |||
return Promise.reject(new Error("rate")); | |||
} | |||
return fetchParse(buildTooltipInvoke(key, lang)).then(function (html) { | |||
if (defenseTripped) return ""; | |||
tooltipHtmlCache[cacheKey] = html || ""; | |||
return tooltipHtmlCache[cacheKey]; | |||
}); | |||
} | |||
function positionTooltip(tip, wrapper) { | |||
tip.classList.add( | tip.classList.add("tooltip-visible"); | ||
tip.classList.remove( | tip.classList.remove("tooltip-below", "tooltip-shift-right", "tooltip-shift-left"); | ||
tip.style.removeProperty( | tip.style.removeProperty("--arrow-offset"); | ||
var rect = wrapper.getBoundingClientRect(); | var rect = wrapper.getBoundingClientRect(); | ||
| Linha 303: | Linha 492: | ||
} else if (below + tipH <= window.innerHeight - MARGIN) { | } else if (below + tipH <= window.innerHeight - MARGIN) { | ||
top = below; | top = below; | ||
tip.classList.add( | tip.classList.add("tooltip-below"); | ||
} else { | } else { | ||
top = Math.max(MARGIN, Math.min(above, window.innerHeight - MARGIN - tipH)); | top = Math.max(MARGIN, Math.min(above, window.innerHeight - MARGIN - tipH)); | ||
| Linha 312: | Linha 501: | ||
if (left < MARGIN) { | if (left < MARGIN) { | ||
left = Math.max(MARGIN, rect.left); | left = Math.max(MARGIN, rect.left); | ||
tip.classList.add( | tip.classList.add("tooltip-shift-right"); | ||
tip.style.setProperty( | tip.style.setProperty("--arrow-offset", Math.max(12, centerX - left) + "px"); | ||
} else if (left + tipW > window.innerWidth - MARGIN) { | } else if (left + tipW > window.innerWidth - MARGIN) { | ||
left = Math.min(window.innerWidth - MARGIN - tipW, rect.right - tipW); | left = Math.min(window.innerWidth - MARGIN - tipW, rect.right - tipW); | ||
tip.classList.add( | tip.classList.add("tooltip-shift-left"); | ||
tip.style.setProperty( | tip.style.setProperty("--arrow-offset", Math.max(12, (left + tipW) - centerX) + "px"); | ||
} | } | ||
tip.style.top = top + | tip.style.top = top + "px"; | ||
tip.style.left = left + | tip.style.left = left + "px"; | ||
} | } | ||
function | function destroyTooltip() { | ||
if (activeTip | if (activeTip) { | ||
activeTip | activeTip.remove(); | ||
activeTip = null; | activeTip = null; | ||
} | |||
if (activeWrapper) { | |||
activeWrapper.classList.remove("item-tooltip-loading"); | |||
activeWrapper = null; | activeWrapper = null; | ||
} | } | ||
} | } | ||
document.addEventListener( | function hideTooltip() { | ||
tooltipGen++; | |||
var wrapper = | destroyTooltip(); | ||
} | |||
function showTooltip(wrapper) { | |||
if (defenseTripped) return; | |||
if (wrapper === activeWrapper && activeTip) return; | |||
hideTooltip(); | |||
var key = wrapper.getAttribute("data-item-key"); | |||
if (!key) return; | |||
var lang = wrapper.getAttribute("data-item-lang") || "pt-br"; | |||
var gen = ++tooltipGen; | |||
activeWrapper = wrapper; | |||
wrapper.classList.add("item-tooltip-loading"); | |||
fetchTooltipHtml(key, lang).then(function (html) { | |||
if (defenseTripped || gen !== tooltipGen || activeWrapper !== wrapper) return; | |||
wrapper.classList.remove("item-tooltip-loading"); | |||
if (!html || !html.trim()) return; | |||
var tmp = document.createElement("div"); | |||
tmp.innerHTML = html.trim(); | |||
var tip = tmp.querySelector(".item-tooltip") || tmp.firstElementChild; | |||
if (!tip) return; | |||
activeTip = tip; | |||
document.body.appendChild(tip); | |||
positionTooltip(tip, wrapper); | |||
}).catch(function () { | |||
if (gen === tooltipGen && activeWrapper === wrapper) { | |||
wrapper.classList.remove("item-tooltip-loading"); | |||
activeWrapper = null; | |||
} | |||
}); | |||
} | |||
document.addEventListener("mouseover", function (e) { | |||
if (e.isTrusted === false) { | |||
noteUntrustedEvent(); | |||
return; | |||
} | |||
var wrapper = e.target.closest ? e.target.closest(".item-wrapper.item-has-tooltip") : null; | |||
if (wrapper && wrapper !== activeWrapper) { | if (wrapper && wrapper !== activeWrapper) { | ||
showTooltip(wrapper); | showTooltip(wrapper); | ||
| Linha 344: | Linha 577: | ||
}); | }); | ||
document.addEventListener( | document.addEventListener("mouseout", function (e) { | ||
if (e.isTrusted === false) return; | |||
if (!activeWrapper) return; | if (!activeWrapper) return; | ||
var wrapper = e.target.closest ? e.target.closest( | var wrapper = e.target.closest ? e.target.closest(".item-wrapper") : null; | ||
if (wrapper === activeWrapper && !activeWrapper.contains(e.relatedTarget)) { | if (wrapper === activeWrapper && !activeWrapper.contains(e.relatedTarget)) { | ||
hideTooltip(); | hideTooltip(); | ||
} | } | ||
}); | }); | ||
document.addEventListener("scroll", hideTooltip, true); | |||
})(); | })(); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||