feat: warn when the equipped card and the selected preset disagree (v3.8.0)
Autoselection normally keeps the SCP80/RAM preset in step with the card, so
the mismatch is easy to miss. Both views now carry an amber banner above the
preset selector (the existing warning style - no new Tailwind classes):
- the equipped card matches no preset: "The equipped card <ICCID> does not
match any card preset - the selected preset may belong to another card.
Add it in the Cards tab (From card)"; with no presets at all the banner says
so instead;
- a preset is selected but it is not the equipped card's: a softer line naming
the matching preset ("check that its keys and counters belong to this card").
`spPresetWarning(iccid, presets, selectedIdx)` is pure (unit-tested with the
ICCID forms the store accepts: digits, spaced, raw EF hex), `spPresetWarningRender`
fills both banners and runs on every card-state poll, preset load, preset
selection and page init. No card session or an unreadable ICCID shows nothing
(the card-iccid gating already explains that state); the banner warns only -
it never blocks a send (preparing a packet for another card stays possible).
Tests: frontend/tests/preset_warning.test.js (5), the card-state/preset
harnesses stub the DOM-bound renderer. sw cache simple-v336.
This commit is contained in:
@@ -340,7 +340,7 @@
|
||||
</section>
|
||||
<section class="mb-10">
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка «Карты»</h2>
|
||||
<p class="mb-2">Хранит предустановки карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Предустановка содержит <strong>один набор ключей на версию ключа</strong> (ниббл <code class="font-mono text-sm">b8..b5</code> KIc/KID, TS 102 225 §5.1.2/A.2): KIc и KID набора должны содержать одну версию (01–0F; A.2 отвергает несовпадение), и у каждой версии ключа на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по наборам. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и предустановки старых сборок — плоские и с наборами ключей). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в предустановке с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<p class="mb-2">Хранит предустановки карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Предустановка содержит <strong>один набор ключей на версию ключа</strong> (ниббл <code class="font-mono text-sm">b8..b5</code> KIc/KID, TS 102 225 §5.1.2/A.2): KIc и KID набора должны содержать одну версию (01–0F; A.2 отвергает несовпадение), и у каждой версии ключа на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по наборам. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и предустановки старых сборок — плоские и с наборами ключей). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80; если подключённая карта не совпадает <strong>ни с одной</strong> предустановкой — или выбранная предустановка принадлежит другой карте — оба представления SCP80/RAM показывают жёлтое предупреждение над выбором предустановки. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в предустановке с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
+1
-1
@@ -339,7 +339,7 @@
|
||||
</section>
|
||||
<section class="mb-10">
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>one keyset per key version</strong> (the <code class="font-mono text-sm">b8..b5</code> nibble of KIc/KID, TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same version (01–0F; A.2 rejects a mismatch), and each key version has its own counter on the card (Annex A.1), so counters are tracked per keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The optional <strong>ADM</strong> field sits in the top row and is stored for later use by the file manager. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>one keyset per key version</strong> (the <code class="font-mono text-sm">b8..b5</code> nibble of KIc/KID, TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same version (01–0F; A.2 rejects a mismatch), and each key version has its own counter on the card (Annex A.1), so counters are tracked per keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — both SCP80/RAM views show an amber warning above the preset selector. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The optional <strong>ADM</strong> field sits in the top row and is stored for later use by the file manager. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
@@ -530,6 +530,7 @@
|
||||
<label class="block mt-2 mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Key version (keyset)">Key version (keyset)</label>
|
||||
<select id="sp-keyset-sel" onchange="spKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></select>
|
||||
</div>
|
||||
<div id="sp-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
||||
|
||||
<div class="flex gap-2 mb-3">
|
||||
<div class="flex-1"> <label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SPI1 (Security Parameter Indicator)</label>
|
||||
@@ -708,6 +709,7 @@
|
||||
</div>
|
||||
<button data-needs="card" data-cap-gate="ram" onclick="ramExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
|
||||
</div>
|
||||
<div id="ram-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
||||
|
||||
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02; on by default). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). Cards differ in what they accept, so three query forms are tried (tag list with EA, tag list without EA, no tag list - the GPPro form); the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). It costs a few extra secured packets per Explore; untick to skip it."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
|
||||
|
||||
@@ -8380,6 +8382,7 @@ function ramApplyCard(idx) {
|
||||
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
||||
_spTarKey = 'tar'; // RAM/GP operations target the ISD
|
||||
cardsApply(idx);
|
||||
spPresetWarningRender();
|
||||
}
|
||||
|
||||
function ramOpChanged() {
|
||||
@@ -10961,6 +10964,7 @@ async function cardsFetch() {
|
||||
cardsRebuildSelect();
|
||||
cardsRender();
|
||||
scp81PushPskMap();
|
||||
spPresetWarningRender();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11204,6 +11208,47 @@ function ramKeysetChanged() {
|
||||
spKeysetSync();
|
||||
}
|
||||
|
||||
// A warning for the SCP80/RAM views when the equipped card and the selected
|
||||
// preset disagree (pure). Autoselection keeps them in step, so this covers
|
||||
// what it cannot fix: a card with no preset at all, or a manually selected
|
||||
// preset that belongs to another card. Returns '' when all is well (or when
|
||||
// there is no card session / no readable ICCID to compare against).
|
||||
function spPresetWarning(iccid, presets, selectedIdx) {
|
||||
const list = presets || [];
|
||||
const norm = cardsNormIccid(iccid);
|
||||
if (!norm) return '';
|
||||
const matched = list.findIndex(c => cardsNormIccid(c.iccid) === norm);
|
||||
if (matched < 0) {
|
||||
if (!list.length) {
|
||||
return t('No card presets defined — the SCP80/RAM views have no keys '
|
||||
+ 'for the equipped card. Add one in the Cards tab.');
|
||||
}
|
||||
return t('The equipped card') + ' ' + iccid + ' ' +
|
||||
t('does not match any card preset — the selected preset may belong to '
|
||||
+ 'another card. Add it in the Cards tab (From card).');
|
||||
}
|
||||
const sel = Number(selectedIdx);
|
||||
if (Number.isInteger(sel) && sel >= 0 && sel < list.length && sel !== matched) {
|
||||
return t('The selected preset is not the equipped card\'s') + ' (' +
|
||||
(list[matched].name || iccid) + ') — ' +
|
||||
t('check that its keys and counters belong to this card.');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Render the banner in both SCP80 views (each has its own preset selection).
|
||||
function spPresetWarningRender() {
|
||||
const iccid = _pysimCardIccid;
|
||||
const show = (id, text) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = text || '';
|
||||
el.classList.toggle('hidden', !text);
|
||||
};
|
||||
show('sp-preset-warning', spPresetWarning(iccid, cards, spPresetIdx()));
|
||||
show('ram-preset-warning', spPresetWarning(iccid, cards, ramPresetIdx()));
|
||||
}
|
||||
|
||||
// The RAM view's preset index.
|
||||
function ramPresetIdx() {
|
||||
const sel = document.getElementById('ram-card-sel');
|
||||
@@ -11546,6 +11591,7 @@ function cardsApplyFields(idx, keepTar) {
|
||||
spInvalidate();
|
||||
updateSp();
|
||||
spKeysetSync();
|
||||
spPresetWarningRender();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -13366,6 +13412,7 @@ function pysimCardStateUpdate(status) {
|
||||
pysimUpdateAdmIndicator(status);
|
||||
pysimUpdateIccidIndicator(_pysimCardIccid);
|
||||
pysimUpdatePresetIndicators(_pysimCardIccid);
|
||||
spPresetWarningRender();
|
||||
pysimApplyAvailability();
|
||||
if (pysimProactiveSeqChanged(status.proactive_seq)
|
||||
&& isViewVisible('tab-phone') && isViewVisible('phone-sub-phone')) {
|
||||
@@ -19424,6 +19471,11 @@ const LANG_RU = {
|
||||
'Duplicate key version in the keysets': 'Дублирующаяся версия ключа в наборах',
|
||||
'Select a card preset - its keysets define the key versions': 'Выберите предустановку карты — её наборы ключей определяют версии ключей',
|
||||
'SCP80 counters saved to the preset.': 'Счётчики SCP80 сохранены в предустановке.',
|
||||
'No card presets defined — the SCP80/RAM views have no keys for the equipped card. Add one in the Cards tab.': 'Предустановки карт не заданы — в представлениях SCP80/RAM нет ключей для подключённой карты. Добавьте её на вкладке «Карты».',
|
||||
'The equipped card': 'Подключённая карта',
|
||||
'does not match any card preset — the selected preset may belong to another card. Add it in the Cards tab (From card).': 'не совпадает ни с одной предустановкой — выбранная предустановка может принадлежать другой карте. Добавьте её на вкладке «Карты» (кнопка «С карты»).',
|
||||
"The selected preset is not the equipped card's": 'Выбранная предустановка не принадлежит подключённой карте',
|
||||
'check that its keys and counters belong to this card.': 'проверьте, что её ключи и счётчики относятся к этой карте.',
|
||||
'Key version (kvn, optional)': 'Версия ключа (kvn, опционально)',
|
||||
"empty = the preset's first keyset": 'пусто = первый набор ключей предустановки',
|
||||
'Keysets (key version: KIc/KID — counter)': 'Наборы ключей (версия: KIc/KID — счётчик)',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v335';
|
||||
const CACHE = 'simple-v336';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -108,6 +108,9 @@ function setup() {
|
||||
globalThis.isViewVisible = () => true;
|
||||
globalThis.pysimProactiveLogRender = () => { calls.proactive++; };
|
||||
globalThis.cardsAutoSelectByIccid = iccid => { calls.autoIccid.push(iccid); return -1; };
|
||||
// the SCP80/RAM preset warning banner is DOM-bound (covered in
|
||||
// preset_warning.test.js) - stub it for these state tests
|
||||
globalThis.spPresetWarningRender = () => {};
|
||||
return { el, adm, iccidEl, scp80El, scp81El, calls };
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ for (const fn of ['cardsTarValue', 'cardsApplyFields', 'cardsApply',
|
||||
]) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code = 'var _cardsKeysetCount = 0;\n' + code;
|
||||
code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n' + code;
|
||||
code = 'var _genSpBuildStub = function() {};\n' + code;
|
||||
eval(code);
|
||||
globalThis._genSpBuild = () => {};
|
||||
|
||||
@@ -37,7 +37,7 @@ for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEd
|
||||
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard']) {
|
||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||
}
|
||||
code = 'var _cardsKeysetCount = 0;\n' + code;
|
||||
code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n' + code;
|
||||
eval(code);
|
||||
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-adm',
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['swapNibbles', 'encIccid', 'decIccid', 'cardsNormIccid',
|
||||
'spPresetWarning']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += 'function t(s){return s;}\n';
|
||||
eval(code);
|
||||
|
||||
const DIGITS = '8970119000004600098';
|
||||
const RAW_HEX = '980711090000640090F8';
|
||||
const OTHER = '89390100000129506903';
|
||||
const CARDS = [
|
||||
{ name: 'Card A', iccid: DIGITS },
|
||||
{ name: 'Card B', iccid: OTHER },
|
||||
];
|
||||
|
||||
test('no warning without a card session or a readable ICCID', () => {
|
||||
assert.strictEqual(spPresetWarning(null, CARDS, 0), '');
|
||||
assert.strictEqual(spPresetWarning('', CARDS, 0), '');
|
||||
assert.strictEqual(spPresetWarning(undefined, CARDS, 0), '');
|
||||
});
|
||||
|
||||
test('no warning when the equipped card matches a preset', () => {
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, 0), '');
|
||||
// the preset's ICCID may be stored in any form
|
||||
assert.strictEqual(spPresetWarning(DIGITS, [{ name: 'A', iccid: RAW_HEX }], 0), '');
|
||||
// the match does not depend on the selection
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, 1) === '', false);
|
||||
});
|
||||
|
||||
test('a card with no matching preset gets the strong warning', () => {
|
||||
const text = spPresetWarning('8945001234567890123', CARDS, 0);
|
||||
assert.match(text, /8945001234567890123/);
|
||||
assert.match(text, /does not match any card preset/);
|
||||
assert.match(text, /From card/);
|
||||
// no presets at all: the other wording
|
||||
const none = spPresetWarning(DIGITS, [], 0);
|
||||
assert.match(none, /No card presets defined/);
|
||||
assert.match(spPresetWarning(DIGITS, null, -1), /No card presets defined/);
|
||||
});
|
||||
|
||||
test('a manually selected preset of another card gets the softer hint', () => {
|
||||
const text = spPresetWarning(DIGITS, CARDS, 1);
|
||||
assert.match(text, /selected preset is not the equipped card/);
|
||||
assert.match(text, /Card A/);
|
||||
// an empty / out-of-range selection is not the softer case (the strong
|
||||
// warning above covers a card with no preset)
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, -1), '');
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, 5), '');
|
||||
});
|
||||
|
||||
test('both SCP80 views carry the warning banner', () => {
|
||||
assert.match(html, /id="sp-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400/);
|
||||
assert.match(html, /id="ram-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400/);
|
||||
// ... and the renderer is wired into the card state, the preset load and
|
||||
// the preset selection
|
||||
assert.ok(html.includes('spPresetWarningRender();'));
|
||||
assert.ok(html.includes("show('sp-preset-warning', spPresetWarning(iccid, cards, spPresetIdx()));"));
|
||||
assert.ok(html.includes("show('ram-preset-warning', spPresetWarning(iccid, cards, ramPresetIdx()));"));
|
||||
});
|
||||
@@ -48,6 +48,7 @@ const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
|
||||
if (an) code += an[0].replace(/^const /, 'var ') + '\n';
|
||||
const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/);
|
||||
if (ar) code += ar[0].replace(/^const /, 'var ') + '\n';
|
||||
globalThis.spPresetWarningRender = () => {};
|
||||
eval(code);
|
||||
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
|
||||
eval(code);
|
||||
|
||||
Reference in New Issue
Block a user