From 3b54ae648b9af1aca62549c103b8dbceb676d47f Mon Sep 17 00:00:00 2001 From: carlospolop Date: Tue, 25 Nov 2025 10:15:54 +0100 Subject: [PATCH] Sync theme/ with master --- theme/ai.js | 90 +++++++++++++++------- theme/ht_searcher.js | 172 ++++++++++++++++++++++++++++++++++--------- theme/index.hbs | 28 +++---- theme/sponsor.js | 3 +- 4 files changed, 216 insertions(+), 77 deletions(-) diff --git a/theme/ai.js b/theme/ai.js index c09116c72..761454181 100644 --- a/theme/ai.js +++ b/theme/ai.js @@ -5,17 +5,33 @@ (() => { const KEY = 'htSummerDiscountsDismissed'; - const IMG = '/images/discount.jpeg'; + const IMG = '/ima * HackTricks AI Chat Widget v1.17 – enhanced resizable sidebar + * --------------------------------------------------- + * ❶ Markdown rendering + sanitised (same as before) + * ❷ ENHANCED: improved drag‑to‑resize panel with better UXdiscount.jpeg'; const TXT = 'Click here for HT Summer Discounts, Last Days!'; const URL = 'https://training.hacktricks.xyz'; - # Stop if user already dismissed + // Stop if user already dismissed if (localStorage.getItem(KEY) === 'true') return; - # Quick helper - const $ = (tag, css = '') => Object.assign(document.createElement(tag), { style: css }); + // Quick helper + const $ = (tag, css = '') => Object.assign(document.cr p.innerHTML = ` +
+ HackTricks AI Chat + ↔ Drag edge to resize +
+ + +
+
+
+
+ + +
`;tag), { style: css }); - # --- Overlay (blur + dim) --- + // --- Overlay (blur + dim) --- const overlay = $('div', ` position: fixed; inset: 0; background: rgba(0,0,0,.4); @@ -24,7 +40,7 @@ z-index: 10000; `); - # --- Modal --- + // --- Modal --- const modal = $('div', ` max-width: 90vw; width: 480px; background: #fff; border-radius: 12px; overflow: hidden; @@ -33,10 +49,10 @@ display: flex; flex-direction: column; align-items: stretch; `); - # --- Title bar (link + close) --- + // --- Title bar (link + close) --- const titleBar = $('div', ` position: relative; - padding: 1rem 2.5rem 1rem 1rem; # room for the close button + padding: 1rem 2.5rem 1rem 1rem; // room for the close button text-align: center; background: #222; color: #fff; font-size: 1.3rem; font-weight: 700; @@ -53,7 +69,7 @@ link.textContent = TXT; titleBar.appendChild(link); - # Close "X" (no persistence) + // Close "X" (no persistence) const closeBtn = $('button', ` position: absolute; top: .25rem; right: .5rem; background: transparent; border: none; @@ -65,11 +81,11 @@ closeBtn.onclick = () => overlay.remove(); titleBar.appendChild(closeBtn); - # --- Image --- + // --- Image --- const img = $('img'); img.src = IMG; img.alt = TXT; img.style.width = '100%'; - # --- Checkbox row --- + // --- Checkbox row --- const label = $('label', ` display: flex; align-items: center; justify-content: center; gap: .6rem; padding: 1rem; font-size: 1rem; color: #222; cursor: pointer; @@ -83,7 +99,7 @@ }; label.append(cb, document.createTextNode("Don't show again")); - # --- Assemble & inject --- + // --- Assemble & inject --- modal.append(titleBar, img, label); overlay.appendChild(modal); @@ -93,28 +109,28 @@ document.body.appendChild(overlay); } })(); - */ - - /** * HackTricks AI Chat Widget v1.16 – resizable sidebar * --------------------------------------------------- * ❶ Markdown rendering + sanitised (same as before) * ❷ NEW: drag‑to‑resize panel, width persists via localStorage */ + + + (function () { - const LOG = "[HackTricks‑AI]"; + const LOG = "[HackTricks-AI]"; /* ---------------- User‑tunable constants ---------------- */ const MAX_CONTEXT = 3000; // highlighted‑text char limit const MAX_QUESTION = 500; // question char limit const MIN_W = 250; // ← resize limits → - const MAX_W = 600; + const MAX_W = 800; const DEF_W = 350; // default width (if nothing saved) const TOOLTIP_TEXT = - "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; + "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; const API_BASE = "https://www.hacktricks.ai/api/assistants/threads"; const BRAND_RED = "#b31328"; @@ -345,8 +361,9 @@ #ht-ai-panel{position:fixed;top:0;right:0;height:100%;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif} #ht-ai-panel.open{transform:translateX(0)} @media(max-width:768px){#ht-ai-panel{display:none}} -#ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333} -#ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center} +#ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333;flex-wrap:wrap} +#ht-ai-header strong{flex-shrink:0} +#ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center;margin-left:auto} #ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0} #ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7} #ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px} @@ -367,8 +384,10 @@ ::selection{background:#ffeb3b;color:#000} ::-moz-selection{background:#ffeb3b;color:#000} /* NEW: resizer handle */ -#ht-ai-resizer{position:absolute;left:0;top:0;width:6px;height:100%;cursor:ew-resize;background:transparent} -#ht-ai-resizer:hover{background:rgba(255,255,255,.05)}`; +#ht-ai-resizer{position:absolute;left:0;top:0;width:8px;height:100%;cursor:ew-resize;background:rgba(255,255,255,.08);border-right:1px solid rgba(255,255,255,.15);transition:background .2s ease} +#ht-ai-resizer:hover{background:rgba(255,255,255,.15);border-right:1px solid rgba(255,255,255,.3)} +#ht-ai-resizer:active{background:rgba(255,255,255,.25)} +#ht-ai-resizer::before{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:2px;height:20px;background:rgba(255,255,255,.4);border-radius:1px}`; const s = document.createElement("style"); s.id = "ht-ai-style"; s.textContent = css; @@ -432,24 +451,43 @@ const onMove = (e) => { if (!dragging) return; - const dx = startX - e.clientX; // dragging leftwards ⇒ +dx + e.preventDefault(); + const clientX = e.clientX || (e.touches && e.touches[0].clientX); + const dx = startX - clientX; // dragging leftwards ⇒ +dx let newW = startW + dx; newW = Math.min(Math.max(newW, MIN_W), MAX_W); panel.style.width = newW + "px"; }; + const onUp = () => { if (!dragging) return; dragging = false; + handle.style.background = ""; + document.body.style.userSelect = ""; + document.body.style.cursor = ""; localStorage.setItem("htAiWidth", parseInt(panel.style.width, 10)); document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); + document.removeEventListener("touchmove", onMove); + document.removeEventListener("touchend", onUp); }; - handle.addEventListener("mousedown", (e) => { + + const onStart = (e) => { + e.preventDefault(); dragging = true; - startX = e.clientX; + startX = e.clientX || (e.touches && e.touches[0].clientX); startW = parseInt(window.getComputedStyle(panel).width, 10); + handle.style.background = "rgba(255,255,255,.25)"; + document.body.style.userSelect = "none"; + document.body.style.cursor = "ew-resize"; + document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); - }); + document.addEventListener("touchmove", onMove, { passive: false }); + document.addEventListener("touchend", onUp); + }; + + handle.addEventListener("mousedown", onStart); + handle.addEventListener("touchstart", onStart, { passive: false }); } })(); diff --git a/theme/ht_searcher.js b/theme/ht_searcher.js index f2c7100db..9bba377a5 100644 --- a/theme/ht_searcher.js +++ b/theme/ht_searcher.js @@ -21,37 +21,112 @@ try { importScripts('https://cdn.jsdelivr.net/npm/elasticlunr@0.9.5/elasticlunr.min.js'); } catch { importScripts(abs('/elasticlunr.min.js')); } - /* 2 — load a single index (remote → local) */ - async function loadIndex(remote, local, isCloud=false){ - let rawLoaded = false; + /* 2 — XOR decryption function */ + function xorDecrypt(encryptedData, key){ + const keyBytes = new TextEncoder().encode(key); + const decrypted = new Uint8Array(encryptedData.length); + for(let i = 0; i < encryptedData.length; i++){ + decrypted[i] = encryptedData[i] ^ keyBytes[i % keyBytes.length]; + } + return decrypted.buffer; + } + + /* 3 — decompress gzip data */ + async function decompressGzip(arrayBuffer){ + if(typeof DecompressionStream !== 'undefined'){ + /* Modern browsers: use native DecompressionStream */ + const stream = new Response(arrayBuffer).body.pipeThrough(new DecompressionStream('gzip')); + const decompressed = await new Response(stream).arrayBuffer(); + return new TextDecoder().decode(decompressed); + } else { + /* Fallback: use pako library */ + if(typeof pako === 'undefined'){ + try { importScripts('https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js'); } + catch(e){ throw new Error('pako library required for decompression: '+e); } + } + const uint8Array = new Uint8Array(arrayBuffer); + const decompressed = pako.ungzip(uint8Array, {to: 'string'}); + return decompressed; + } + } + + /* 4 — load a single index (remote → local) */ + async function loadIndex(remote, local, isCloud=false){ + const XOR_KEY = "Prevent_Online_AVs_From_Flagging_HackTricks_Search_Gzip_As_Malicious_394h7gt8rf9u3rf9g"; + let rawLoaded = false; + if(remote){ + /* Try ONLY compressed version from GitHub (remote already includes .js.gz) */ try { const r = await fetch(remote,{mode:'cors'}); - if (!r.ok) throw new Error('HTTP '+r.status); - importScripts(URL.createObjectURL(new Blob([await r.text()],{type:'application/javascript'}))); - rawLoaded = true; - } catch(e){ console.warn('remote',remote,'failed →',e); } - if(!rawLoaded){ - try { importScripts(abs(local)); rawLoaded = true; } - catch(e){ console.error('local',local,'failed →',e); } - } - if(!rawLoaded) return null; /* give up on this index */ - const data = { json:self.search.index, urls:self.search.doc_urls, cloud:isCloud }; - delete self.search.index; delete self.search.doc_urls; - return data; + if (r.ok) { + const encryptedCompressed = await r.arrayBuffer(); + /* Decrypt first */ + const compressed = xorDecrypt(new Uint8Array(encryptedCompressed), XOR_KEY); + /* Then decompress */ + const text = await decompressGzip(compressed); + importScripts(URL.createObjectURL(new Blob([text],{type:'application/javascript'}))); + rawLoaded = true; + console.log('Loaded encrypted+compressed from GitHub:',remote); + } + } catch(e){ console.warn('encrypted+compressed GitHub',remote,'failed →',e); } } - - (async () => { - const MAIN_RAW = 'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/master/searchindex.js'; - const CLOUD_RAW = 'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks-cloud/refs/heads/master/searchindex.js'; - + /* If remote (GitHub) failed, fall back to local uncompressed file */ + if(!rawLoaded && local){ + try { + importScripts(abs(local)); + rawLoaded = true; + console.log('Loaded local fallback:',local); + } + catch(e){ console.error('local',local,'failed →',e); } + } + if(!rawLoaded) return null; /* give up on this index */ + const data = { json:self.search.index, urls:self.search.doc_urls, cloud:isCloud }; + delete self.search.index; delete self.search.doc_urls; + return data; + } + + async function loadWithFallback(remotes, local, isCloud=false){ + if(remotes.length){ + const [primary, ...secondary] = remotes; + const primaryData = await loadIndex(primary, null, isCloud); + if(primaryData) return primaryData; + + if(local){ + const localData = await loadIndex(null, local, isCloud); + if(localData) return localData; + } + + for (const remote of secondary){ + const data = await loadIndex(remote, null, isCloud); + if(data) return data; + } + } + + return local ? loadIndex(null, local, isCloud) : null; + } + + let built = []; + const MAX = 30, opts = {bool:'AND', expand:true}; + + self.onmessage = async ({data}) => { + if(data.type === 'init'){ + const lang = data.lang || 'en'; + const searchindexBase = 'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks-searchindex/master'; + + /* Remote sources are .js.gz (compressed), local fallback is .js (uncompressed) */ + const mainFilenames = Array.from(new Set(['searchindex-' + lang + '.js.gz', 'searchindex-en.js.gz'])); + const cloudFilenames = Array.from(new Set(['searchindex-cloud-' + lang + '.js.gz', 'searchindex-cloud-en.js.gz'])); + + const MAIN_REMOTE_SOURCES = mainFilenames.map(function(filename) { return searchindexBase + '/' + filename; }); + const CLOUD_REMOTE_SOURCES = cloudFilenames.map(function(filename) { return searchindexBase + '/' + filename; }); + const indices = []; - const main = await loadIndex(MAIN_RAW , '/searchindex-book.js', false); if(main) indices.push(main); - const cloud= await loadIndex(CLOUD_RAW, '/searchindex.js', true ); if(cloud) indices.push(cloud); - + const main = await loadWithFallback(MAIN_REMOTE_SOURCES , '/searchindex-book.js', false); if(main) indices.push(main); + const cloud= await loadWithFallback(CLOUD_REMOTE_SOURCES, '/searchindex.js', true ); if(cloud) indices.push(cloud); if(!indices.length){ postMessage({ready:false, error:'no-index'}); return; } /* build index objects */ - const built = indices.map(d => ({ + built = indices.map(d => ({ idx : elasticlunr.Index.load(d.json), urls: d.urls, cloud: d.cloud, @@ -59,10 +134,11 @@ })); postMessage({ready:true}); - const MAX = 30, opts = {bool:'AND', expand:true}; - - self.onmessage = ({data:q}) => { - if(!q){ postMessage([]); return; } + return; + } + + const q = data.query || data; + if(!q){ postMessage([]); return; } const all = []; for(const s of built){ @@ -83,12 +159,16 @@ } all.sort((a,b)=>b.norm-a.norm); postMessage(all.slice(0,MAX)); - }; - })(); + }; `; /* ───────────── 2. spawn worker ───────────── */ const worker = new Worker(URL.createObjectURL(new Blob([workerCode],{type:'application/javascript'}))); + + /* ───────────── 2.1. initialize worker with language ───────────── */ + const htmlLang = (document.documentElement.lang || 'en').toLowerCase(); + const lang = htmlLang.split('-')[0]; + worker.postMessage({type: 'init', lang: lang}); /* ───────────── 3. DOM refs ─────────────── */ const wrap = document.getElementById('search-wrapper'); @@ -97,11 +177,32 @@ const listOut = document.getElementById('searchresults-outer'); const header = document.getElementById('searchresults-header'); const icon = document.getElementById('search-toggle'); - - const READY_ICON = icon.innerHTML; + + if(!wrap || !bar || !list || !listOut || !header || !icon) { + console.error('[HT Search] Missing DOM elements:', {wrap:!!wrap, bar:!!bar, list:!!list, listOut:!!listOut, header:!!header, icon:!!icon}); + return; + } + + /* Clear icon content and use emoji states directly */ icon.textContent = '⏳'; icon.setAttribute('aria-label','Loading search …'); - icon.setAttribute('title','Search is loading, please wait...'); + icon.setAttribute('title','Search is loading, please wait...'); + + const setIconState = state => { + if(state === 'ready'){ + icon.textContent = '🔍'; + icon.setAttribute('aria-label','Open search (S)'); + icon.removeAttribute('title'); + } else if(state === 'error'){ + icon.textContent = '❌'; + icon.setAttribute('aria-label','Search unavailable'); + icon.setAttribute('title','Search is unavailable'); + } else { + icon.textContent = '⏳'; + icon.setAttribute('aria-label','Loading search …'); + icon.setAttribute('title','Search is loading, please wait...'); + } + }; const HOT=83, ESC=27, DOWN=40, UP=38, ENTER=13; @@ -155,13 +256,12 @@ else if([DOWN,UP,ENTER].includes(e.keyCode) && document.activeElement!==bar){const cur=list.querySelector('li.focus'); if(!cur) return; e.preventDefault(); if(e.keyCode===DOWN){const nxt=cur.nextElementSibling; if(nxt){cur.classList.remove('focus'); nxt.classList.add('focus');}} else if(e.keyCode===UP){const prv=cur.previousElementSibling; cur.classList.remove('focus'); if(prv){prv.classList.add('focus');} else {bar.focus();}} else {const a=cur.querySelector('a'); if(a) window.location.assign(a.href);}} }); - bar.addEventListener('input',e=>{ clearTimeout(debounce); debounce=setTimeout(()=>worker.postMessage(e.target.value.trim()),120); }); + bar.addEventListener('input',e=>{ clearTimeout(debounce); debounce=setTimeout(()=>worker.postMessage({query: e.target.value.trim()}),120); }); /* ───────────── worker messages ───────────── */ worker.onmessage = ({data}) => { if(data && data.ready!==undefined){ - if(data.ready){ icon.innerHTML=READY_ICON; icon.setAttribute('aria-label','Open search (S)'); } - else { icon.textContent='❌'; icon.setAttribute('aria-label','Search unavailable'); } + setIconState(data.ready ? 'ready' : 'error'); return; } const docs=data, q=bar.value.trim(), terms=q.split(/\s+/).filter(Boolean); diff --git a/theme/index.hbs b/theme/index.hbs index 9c7fa3155..a0d4b65bf 100644 --- a/theme/index.hbs +++ b/theme/index.hbs @@ -255,33 +255,33 @@ diff --git a/theme/sponsor.js b/theme/sponsor.js index b730cf9f5..fb25a468a 100644 --- a/theme/sponsor.js +++ b/theme/sponsor.js @@ -15,7 +15,8 @@ var mobilesponsorCTA = mobilesponsorSide.querySelector(".mobilesponsor-cta") async function getSponsor() { - const url = "https://cloud.hacktricks.wiki/sponsor" + const currentUrl = encodeURIComponent(window.location.href); + const url = `https://cloud.hacktricks.wiki/sponsor?current_url=${currentUrl}`; try { const response = await fetch(url, { method: "GET" }) if (!response.ok) {