Repository Public Research preview
1/* In-page helper for safari-mcp. Injected with every call; defines window.__smcp once per document.2 Plain ES2017, no dependencies. Everything it returns is JSON-serialisable. */3(function () {4 if (window.__smcp && window.__smcp.v === 3) return;5 var S = { v: 3, refs: new Map(), next: 1, dialogs: [], jobs: {}, bufs: {} };67 /* ---------- traversal: light DOM + open shadow roots + same-origin iframes ---------- */8 function roots(doc, out, path) {9 out.push({ root: doc, path: path });10 var all = doc.querySelectorAll("*");11 for (var i = 0; i < all.length; i++) {12 var el = all[i];13 if (el.shadowRoot) roots(el.shadowRoot, out, path);14 if (el.tagName === "IFRAME" || el.tagName === "FRAME") {15 try { if (el.contentDocument && el.contentDocument.documentElement) roots(el.contentDocument, out, path.concat([i])); } catch (e) { /* cross-origin */ }16 }17 }18 return out;19 }20 function deepAll(selector) {21 var out = [], rs = roots(document, [], []);22 for (var i = 0; i < rs.length; i++) { try { var f = rs[i].root.querySelectorAll(selector); for (var j = 0; j < f.length; j++) out.push(f[j]); } catch (e) { if (i === 0) throw new Error("invalid selector: " + selector); } }23 return out;24 }25 function crossOriginFrames() {26 var out = [], fr = document.querySelectorAll("iframe,frame");27 for (var i = 0; i < fr.length; i++) { var ok = false; try { ok = !!fr[i].contentDocument; } catch (e) {} if (!ok && fr[i].src) out.push(fr[i].src); }28 return out;29 }3031 /* ---------- element description ---------- */32 function visible(el) {33 if (!el.isConnected) return false;34 var w = el.ownerDocument.defaultView, st = w.getComputedStyle(el);35 if (st.visibility === "hidden" || st.display === "none" || Number(st.opacity) === 0) return false;36 var r = el.getBoundingClientRect();37 return r.width > 0 && r.height > 0;38 }39 function clean(s, n) { s = String(s == null ? "" : s).replace(/\s+/g, " ").trim(); return n && s.length > n ? s.slice(0, n) + "…" : s; }40 function labelFor(el) {41 var d = el.ownerDocument, a = el.getAttribute("aria-label");42 if (a) return clean(a, 120);43 var lb = el.getAttribute("aria-labelledby");44 if (lb) { var t = lb.split(/\s+/).map(function (id) { var x = d.getElementById(id); return x ? x.textContent : ""; }).join(" "); if (clean(t)) return clean(t, 120); }45 if (el.labels && el.labels.length) return clean(el.labels[0].textContent, 120);46 if (el.id) { try { var l = d.querySelector('label[for="' + CSS.escape(el.id) + '"]'); if (l) return clean(l.textContent, 120); } catch (e) {} }47 var p = el.closest("label"); if (p) return clean(p.textContent, 120);48 return "";49 }50 function roleOf(el) {51 var r = el.getAttribute("role"); if (r) return r;52 var t = el.tagName.toLowerCase();53 if (t === "a") return "link";54 if (t === "button" || t === "summary") return "button";55 if (t === "select") return "combobox";56 if (t === "textarea") return "textbox";57 if (t === "input") { var ty = (el.type || "text").toLowerCase(); return ty === "checkbox" ? "checkbox" : ty === "radio" ? "radio" : ty === "submit" || ty === "button" || ty === "reset" || ty === "image" ? "button" : ty === "range" ? "slider" : ty === "file" ? "file" : "textbox"; }58 if (el.isContentEditable) return "textbox";59 return t;60 }61 function nameOf(el) {62 var t = el.tagName.toLowerCase(), l = labelFor(el);63 if (l) return l;64 if (t === "input") { var ty = (el.type || "").toLowerCase(); if (ty === "submit" || ty === "button" || ty === "reset") return clean(el.value || ty, 120); return clean(el.placeholder || el.name || el.title || "", 120); }65 if (t === "img") return clean(el.alt || el.title, 120);66 var txt = clean(el.innerText || el.textContent, 120);67 if (txt) return txt;68 var img = el.querySelector && el.querySelector("img[alt]"); if (img) return clean(img.alt, 120);69 return clean(el.title || el.getAttribute("name") || el.placeholder || "", 120);70 }71 function sensitive(el) {72 var ty = (el.type || "").toLowerCase(), ac = (el.getAttribute && el.getAttribute("autocomplete") || "").toLowerCase(), nm = ((el.name || "") + " " + (el.id || "")).toLowerCase();73 return ty === "password" || /cc-|current-password|new-password|one-time-code/.test(ac) || /passw|card.?num|cvv|cvc|ssn|\bnas\b|secret|token/.test(nm);74 }75 function refOf(el) {76 var found = null;77 S.refs.forEach(function (v, k) { if (v === el) found = k; });78 if (found) return found;79 var k = "e" + S.next++; S.refs.set(k, el); return k;80 }81 function describe(el) {82 var t = el.tagName.toLowerCase(), role = roleOf(el), r = el.getBoundingClientRect(), w = el.ownerDocument.defaultView;83 var o = { ref: refOf(el), role: role, tag: t, name: nameOf(el) };84 if (t === "a" && el.href) o.href = el.href;85 if (t === "input" || t === "textarea" || t === "select") {86 if (el.type) o.type = el.type;87 if (role === "checkbox" || role === "radio") o.checked = !!el.checked;88 else if (role === "file") o.files = el.files ? el.files.length : 0;89 else o.value = sensitive(el) ? (el.value ? "[redacted]" : "") : clean(t === "select" && el.selectedOptions[0] ? el.selectedOptions[0].textContent : el.value, 200);90 if (t === "select") o.options = Array.prototype.slice.call(el.options, 0, 50).map(function (x) { return clean(x.textContent, 60); });91 if (el.required) o.required = true;92 if (sensitive(el)) o.sensitive = true;93 }94 if (el.disabled || el.getAttribute("aria-disabled") === "true") o.disabled = true;95 if (el.getAttribute("aria-expanded")) o.expanded = el.getAttribute("aria-expanded") === "true";96 o.in_viewport = r.bottom > 0 && r.right > 0 && r.top < w.innerHeight && r.left < w.innerWidth;97 if (el.ownerDocument !== document) o.in_frame = true;98 return o;99 }100 var INTERACTIVE = 'a[href],button,input:not([type="hidden"]),select,textarea,summary,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="radio"],[role="switch"],[role="option"],[role="combobox"],[contenteditable=""],[contenteditable="true"],[onclick],[tabindex]:not([tabindex="-1"])';101102 /* ---------- target resolution ---------- */103 function resolve(target, opts) {104 opts = opts || {};105 var list;106 if (!target || typeof target !== "object") throw new Error("target is required: {ref} | {selector} | {text, role?} | {label}");107 if (target.ref) {108 var el = S.refs.get(target.ref);109 if (!el || !el.isConnected) throw new Error("ref " + target.ref + " is stale (the page changed or reloaded). Take a new snapshot.");110 return el;111 }112 if (target.selector) list = deepAll(target.selector);113 else if (target.label != null) {114 var want = clean(target.label).toLowerCase();115 list = deepAll("input,select,textarea,[contenteditable=''],[contenteditable='true']").filter(function (e) {116 var n = (labelFor(e) || e.placeholder || e.name || e.getAttribute("aria-label") || "").toLowerCase();117 return target.exact ? clean(n) === want : n.indexOf(want) !== -1;118 });119 } else if (target.text != null) {120 var wt = clean(target.text).toLowerCase(), sel = target.role === "link" ? 'a[href],[role="link"]' : target.role === "button" ? 'button,input[type="submit"],input[type="button"],[role="button"],summary' : INTERACTIVE;121 list = deepAll(sel).filter(function (e) { var n = nameOf(e).toLowerCase(); return target.exact ? n === wt : n.indexOf(wt) !== -1; });122 // Prefer exact matches when a loose search returns several.123 var ex = list.filter(function (e) { return nameOf(e).toLowerCase() === wt; }); if (ex.length) list = ex;124 } else throw new Error("target needs ref, selector, text or label");125 var vis = list.filter(visible);126 if (opts.allowHidden && vis.length === 0) vis = list;127 if (vis.length === 0) throw new Error(list.length ? "target matched " + list.length + " element(s) but none is visible" : "no element matches the target");128 if (typeof target.index === "number") { if (!vis[target.index]) throw new Error("index " + target.index + " out of range (" + vis.length + " matches)"); return vis[target.index]; }129 if (vis.length > 1 && !opts.first) {130 var e2 = new Error("target is ambiguous: " + vis.length + " visible matches. Add index, or use a ref.");131 e2.candidates = vis.slice(0, 8).map(describe); throw e2;132 }133 return vis[0];134 }135136 /* ---------- actions ---------- */137 function fire(el, type, init) {138 var w = el.ownerDocument.defaultView, r = el.getBoundingClientRect();139 var base = { bubbles: true, cancelable: true, composed: true, view: w, clientX: r.left + r.width / 2, clientY: r.top + r.height / 2, button: 0 };140 for (var k in init || {}) base[k] = init[k];141 var Ev = /^pointer/.test(type) && w.PointerEvent ? w.PointerEvent : w.MouseEvent;142 return el.dispatchEvent(new Ev(type, base));143 }144 function setValue(el, value) {145 var w = el.ownerDocument.defaultView, proto = el.tagName === "TEXTAREA" ? w.HTMLTextAreaElement.prototype : el.tagName === "SELECT" ? w.HTMLSelectElement.prototype : w.HTMLInputElement.prototype;146 var d = Object.getOwnPropertyDescriptor(proto, "value");147 if (d && d.set) d.set.call(el, value); else el.value = value; // native setter: frameworks that patch `value` (React) still see the change148 el.dispatchEvent(new w.Event("input", { bubbles: true, composed: true }));149 el.dispatchEvent(new w.Event("change", { bubbles: true, composed: true }));150 }151 S.api = {152 info: function () {153 return { url: location.href, title: document.title, ready_state: document.readyState, scroll: { x: Math.round(scrollX), y: Math.round(scrollY), max_y: Math.max(0, document.documentElement.scrollHeight - innerHeight) }, viewport: { width: innerWidth, height: innerHeight }, dialogs: S.dialogs.splice(0), cross_origin_frames: crossOriginFrames().slice(0, 10) };154 },155 snapshot: function (a) {156 var max = a.limit || 150, offset = a.offset || 0;157 var els = deepAll(INTERACTIVE).filter(visible);158 if (a.viewport_only) els = els.filter(function (e) { var r = e.getBoundingClientRect(); return r.bottom > 0 && r.top < innerHeight; });159 var heads = deepAll("h1,h2,h3").filter(visible).slice(0, 40).map(function (h) { return { level: Number(h.tagName[1]), text: clean(h.textContent, 120) }; });160 return { url: location.href, title: document.title, total_interactive: els.length, offset: offset, elements: els.slice(offset, offset + max).map(describe), headings: heads, truncated: els.length > offset + max, cross_origin_frames: crossOriginFrames().slice(0, 10) };161 },162 text: function (a) {163 var el = a.target ? resolve(a.target, { first: true, allowHidden: true }) : document.body;164 var t = (el.innerText || el.textContent || "").replace(/\n{3,}/g, "\n\n").trim(), max = a.max_chars || 20000, off = a.offset || 0;165 return { url: location.href, title: document.title, length: t.length, offset: off, text: t.slice(off, off + max), truncated: t.length > off + max };166 },167 html: function (a) {168 var el = a.target ? resolve(a.target, { first: true, allowHidden: true }) : document.documentElement, h = el.outerHTML, max = a.max_chars || 20000;169 return { length: h.length, html: h.slice(0, max), truncated: h.length > max };170 },171 query: function (a) {172 var list = deepAll(a.selector), max = a.limit || 50;173 return { count: list.length, elements: list.slice(0, max).map(function (e) {174 var o = { ref: refOf(e), tag: e.tagName.toLowerCase(), text: clean(e.innerText || e.textContent, a.text_chars || 200), visible: visible(e) };175 if (a.attributes) { o.attributes = {}; a.attributes.forEach(function (n) { var v = e.getAttribute(n); if (v != null) o.attributes[n] = /^(value)$/.test(n) && sensitive(e) ? "[redacted]" : clean(v, 300); }); }176 if (e.href) o.href = e.href; if (e.src) o.src = e.src;177 return o;178 }), truncated: list.length > max };179 },180 links: function (a) {181 var seen = {}, out = [], f = a.filter ? a.filter.toLowerCase() : null;182 deepAll("a[href]").forEach(function (e) {183 if (!/^https?:/.test(e.href) || seen[e.href]) return;184 var t = clean(e.innerText || e.textContent || (e.querySelector("img[alt]") || {}).alt, 100);185 if (f && e.href.toLowerCase().indexOf(f) === -1 && t.toLowerCase().indexOf(f) === -1) return;186 if (a.same_origin && new URL(e.href).origin !== location.origin) return;187 seen[e.href] = 1; out.push({ text: t, href: e.href });188 });189 var max = a.limit || 200; return { count: out.length, links: out.slice(0, max), truncated: out.length > max };190 },191 tables: function (a) {192 var ts = deepAll(a.selector || "table").filter(function (t) { return t.tagName === "TABLE"; }), max = a.max_rows || 200;193 return { count: ts.length, tables: ts.slice(0, a.limit || 5).map(function (t) {194 var rows = Array.prototype.map.call(t.rows, function (r) { return Array.prototype.map.call(r.cells, function (c) { return clean(c.innerText || c.textContent, 300); }); });195 var hasHead = t.tHead || (t.rows[0] && t.rows[0].querySelector("th"));196 return { caption: t.caption ? clean(t.caption.textContent, 120) : null, headers: hasHead ? rows[0] : null, rows: rows.slice(hasHead ? 1 : 0, (hasHead ? 1 : 0) + max), total_rows: rows.length - (hasHead ? 1 : 0), truncated: rows.length - (hasHead ? 1 : 0) > max };197 }) };198 },199 forms: function () {200 return { forms: deepAll("form").slice(0, 20).map(function (f, i) {201 return { index: i, ref: refOf(f), action: f.action || null, method: (f.method || "get").toLowerCase(), name: f.getAttribute("name") || f.id || null,202 fields: Array.prototype.filter.call(f.elements, function (e) { return e.type !== "hidden" && e.tagName !== "FIELDSET"; }).slice(0, 80).map(describe) };203 }) };204 },205 click: function (a) {206 var el = resolve(a.target), before = location.href;207 if (el.disabled) throw new Error("element is disabled");208 el.scrollIntoView({ block: "center", inline: "center" });209 var d = describe(el);210 fire(el, "pointerdown"); fire(el, "mousedown");211 if (el.focus) try { el.focus({ preventScroll: true }); } catch (e) {}212 fire(el, "pointerup"); fire(el, "mouseup");213 if (a.double) { el.click(); fire(el, "dblclick"); } else el.click();214 return { clicked: d, url_before: before, opens_new_tab: el.tagName === "A" && el.target === "_blank" };215 },216 hover: function (a) { var el = resolve(a.target); el.scrollIntoView({ block: "center" }); fire(el, "pointerover"); fire(el, "mouseover"); fire(el, "pointerenter", { bubbles: false }); fire(el, "mouseenter", { bubbles: false }); fire(el, "pointermove"); fire(el, "mousemove"); return { hovered: describe(el) }; },217 fill: function (a) {218 var el = resolve(a.target), t = el.tagName.toLowerCase(), value = String(a.value);219 if (el.disabled || el.readOnly) throw new Error("field is disabled or read-only");220 if (sensitive(el) && !a.confirm_sensitive) { var e = new Error("this looks like a password, payment or secret field. Ask the user, then retry with confirm_sensitive: true."); e.code = "sensitive_field"; throw e; }221 el.scrollIntoView({ block: "center" }); try { el.focus({ preventScroll: true }); } catch (e3) {}222 if (t === "select") return S.api.select(a);223 if (el.isContentEditable) { el.textContent = a.append ? el.textContent + value : value; el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value })); }224 else if (t === "input" || t === "textarea") {225 var ty = (el.type || "text").toLowerCase();226 if (ty === "checkbox" || ty === "radio") throw new Error("use set_checked for checkboxes and radio buttons");227 if (ty === "file") throw new Error("use upload_file for file inputs");228 setValue(el, a.append ? el.value + value : value);229 } else throw new Error("element is not a fillable field (" + t + ")");230 var d = describe(el); return { filled: d, value_length: value.length };231 },232 select: function (a) {233 var el = resolve(a.target); if (el.tagName !== "SELECT") throw new Error("target is not a <select>");234 var want = String(a.value).toLowerCase(), opt = null, opts = el.options;235 for (var i = 0; i < opts.length; i++) if (opts[i].value.toLowerCase() === want || clean(opts[i].textContent).toLowerCase() === want) { opt = opts[i]; break; }236 if (!opt) for (var j = 0; j < opts.length; j++) if (clean(opts[j].textContent).toLowerCase().indexOf(want) !== -1) { opt = opts[j]; break; }237 if (!opt) throw new Error("no option matches \"" + a.value + "\". Options: " + Array.prototype.slice.call(opts, 0, 30).map(function (x) { return clean(x.textContent, 40); }).join(" | "));238 setValue(el, opt.value); return { selected: clean(opt.textContent, 80), value: opt.value, field: describe(el) };239 },240 check: function (a) {241 var el = resolve(a.target), ty = (el.type || "").toLowerCase(), role = el.getAttribute("role");242 var cur = ty === "checkbox" || ty === "radio" ? el.checked : el.getAttribute("aria-checked") === "true";243 if (!(ty === "checkbox" || ty === "radio" || role === "checkbox" || role === "switch" || role === "radio")) throw new Error("target is not a checkbox, radio or switch");244 if (cur !== !!a.checked) { el.scrollIntoView({ block: "center" }); el.click(); }245 return { field: describe(el), changed: cur !== !!a.checked };246 },247 key: function (a) {248 var el = a.target ? resolve(a.target) : (document.activeElement || document.body), w = el.ownerDocument.defaultView;249 var init = { key: a.key, code: a.code || a.key, bubbles: true, cancelable: true, composed: true, shiftKey: !!a.shift, metaKey: !!a.meta, ctrlKey: !!a.ctrl, altKey: !!a.alt };250 var ok = el.dispatchEvent(new w.KeyboardEvent("keydown", init));251 el.dispatchEvent(new w.KeyboardEvent("keypress", init));252 el.dispatchEvent(new w.KeyboardEvent("keyup", init));253 var submitted = false;254 // Synthetic key events never trigger default actions, so Enter-to-submit is done explicitly.255 if (ok && a.key === "Enter" && el.form && el.tagName !== "TEXTAREA") { if (el.form.requestSubmit) el.form.requestSubmit(); else el.form.submit(); submitted = true; }256 if (ok && a.key === "Escape" && el.blur) el.blur();257 return { key: a.key, on: describe(el), default_prevented: !ok, form_submitted: submitted };258 },259 submit: function (a) {260 var el = resolve(a.target, { first: true, allowHidden: true }), f = el.tagName === "FORM" ? el : el.form || el.closest("form");261 if (!f) throw new Error("no form found for the target");262 var before = location.href; if (f.requestSubmit) f.requestSubmit(); else f.submit();263 return { submitted: true, action: f.action || null, url_before: before };264 },265 scroll: function (a) {266 if (a.target) { resolve(a.target, { first: true }).scrollIntoView({ block: a.block || "center" }); }267 else if (a.to === "top") scrollTo(0, 0);268 else if (a.to === "bottom") scrollTo(0, document.documentElement.scrollHeight);269 else scrollBy(a.dx || 0, a.dy != null ? a.dy : Math.round(innerHeight * 0.85));270 return { scroll: { x: Math.round(scrollX), y: Math.round(scrollY), max_y: Math.max(0, document.documentElement.scrollHeight - innerHeight) } };271 },272 check_condition: function (a) {273 var r = { url: location.href, ready_state: document.readyState };274 if (a.selector) { var l = deepAll(a.selector); r.selector_count = l.length; r.selector_visible = l.filter(visible).length; }275 if (a.text != null) r.text_found = (document.body ? document.body.innerText : "").toLowerCase().indexOf(String(a.text).toLowerCase()) !== -1;276 if (a.text_gone != null) r.text_gone = (document.body ? document.body.innerText : "").toLowerCase().indexOf(String(a.text_gone).toLowerCase()) === -1;277 if (a.url_contains != null) r.url_matches = location.href.indexOf(a.url_contains) !== -1;278 r.pending_images = Array.prototype.filter.call(document.images, function (i) { return !i.complete; }).length;279 return r;280 },281 /* ---------- chunked binary transfer (uploads to a file input, downloads through fetch) ---------- */282 buf_append: function (a) { S.bufs[a.id] = (S.bufs[a.id] || "") + a.chunk; return { length: S.bufs[a.id].length }; },283 upload_commit: function (a) {284 var el = resolve(a.target, { allowHidden: true }); if (el.tagName !== "INPUT" || el.type !== "file") throw new Error("target is not a file input");285 var bin = atob(S.bufs[a.id] || ""); delete S.bufs[a.id];286 var bytes = new Uint8Array(bin.length); for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);287 var dt = new DataTransfer(); if (a.append) for (var k = 0; k < el.files.length; k++) dt.items.add(el.files[k]);288 dt.items.add(new File([bytes], a.name, { type: a.type || "application/octet-stream" }));289 el.files = dt.files;290 el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true }));291 return { files: Array.prototype.map.call(el.files, function (f) { return { name: f.name, size: f.size, type: f.type }; }) };292 },293 fetch_start: function (a) {294 var u = new URL(a.url, location.href);295 if (!a.allow_cross_origin && u.origin !== location.origin) throw new Error("cross-origin fetch refused (" + u.origin + "). Navigate to that site first, or pass allow_cross_origin: true.");296 S.jobs[a.id] = { done: false };297 fetch(u.href, { credentials: "include", method: a.method || "GET", headers: a.headers || {}, body: a.body == null ? undefined : a.body }).then(function (res) {298 var meta = { status: res.status, ok: res.ok, url: res.url, type: res.headers.get("content-type") || "", disposition: res.headers.get("content-disposition") || "" };299 if (a.as === "text") return res.text().then(function (t) { S.jobs[a.id] = { done: true, meta: meta, text: t }; });300 return res.arrayBuffer().then(function (buf) {301 var b = new Uint8Array(buf), s = "", CH = 0x8000;302 if (b.length > (a.max_bytes || 52428800)) throw new Error("response is larger than the limit (" + b.length + " bytes)");303 for (var i = 0; i < b.length; i += CH) s += String.fromCharCode.apply(null, b.subarray(i, i + CH));304 S.jobs[a.id] = { done: true, meta: meta, b64: btoa(s), size: b.length };305 });306 }).catch(function (e) { S.jobs[a.id] = { done: true, error: String(e && e.message || e) }; });307 return { started: true };308 },309 job_poll: function (a) {310 var j = S.jobs[a.id]; if (!j) throw new Error("unknown job (the page reloaded?)");311 if (!j.done) return { done: false };312 if (j.error) { delete S.jobs[a.id]; return { done: true, error: j.error }; }313 if (j.text != null) { var t = j.text, max = a.max_chars || 200000; delete S.jobs[a.id]; return { done: true, meta: j.meta, length: t.length, text: t.slice(0, max), truncated: t.length > max }; }314 var off = a.offset || 0, CHUNK = 262144, part = j.b64.slice(off, off + CHUNK), last = off + CHUNK >= j.b64.length;315 if (last) delete S.jobs[a.id];316 return { done: true, meta: j.meta, size: j.size, chunk: part, next_offset: off + part.length, last: last };317 },318 eval: function (a) { var v = (0, eval)(a.code); return { result: v === undefined ? null : v }; },319 };320321 /* Page dialogs block every script, including ours: record them and answer without blocking. */322 try {323 var mode = window.__smcp_dialog_mode || "accept";324 window.alert = function (m) { S.dialogs.push({ type: "alert", message: clean(m, 500) }); };325 window.confirm = function (m) { var r = (window.__smcp_dialog_mode || mode) !== "dismiss"; S.dialogs.push({ type: "confirm", message: clean(m, 500), answered: r }); return r; };326 window.prompt = function (m, d) { S.dialogs.push({ type: "prompt", message: clean(m, 500), answered: null }); return null; };327 } catch (e) {}328 window.__smcp = S;329})();330