(function () { 'use strict'; /* Версия сборки. Поднимать при каждой выкладке: в консоли видно, какая именно копия скрипта работает (README → «Разработка и проверка»). */ var VERSION = 8; var PREFIX = '[GSOC]'; var LOG_BACK_ON = "GSOC.setLog('off')"; /*`debug` (по умолчанию — печатается всё) · `warn` (только предупреждения и ошибки) · `error` (только ошибки) · `off` (полностью тихо). При `error`/`off` не печатается даже строка `itteration: N`.*/ /* ---- Уровень логирования (README → «Как отключить логи») ---- debug — всё (по умолчанию), warn — предупреждения и ошибки, error — только ошибки, off — тихо. Переключается БЕЗ правки кода и перевыкладки: ?gsoc=off в адресе страницы · window.GSOC_LOG = 'off' до загрузки скрипта · localStorage.gsoc_log = 'off' · GSOC.setLog('off') в консоли */ var LOG_LEVELS = { debug: 4, warn: 3, error: 2, off: 0 }; var LOG_LEVEL = resolveLogLevel(); function normalizeLogLevel(value) { var v = String(value == null ? '' : value).toLowerCase().replace(/^\s+|\s+$/g, ''); if (v === 'all' || v === 'true' || v === '1' || v === 'on') return 'debug'; if (v === 'false' || v === '0' || v === 'none' || v === 'silent' || v === 'quiet') return 'off'; return LOG_LEVELS.hasOwnProperty(v) ? v : null; } function resolveLogLevel() { try { var m = /[?&](?:gsoc|gsoc_level)=([a-z0-9]+)/i.exec(safe(function () { return location.search; }, '') || ''); var fromUrl = m ? normalizeLogLevel(m[1]) : null; if (fromUrl) return fromUrl; } catch (e) {} try { var fromWindow = normalizeLogLevel(window.GSOC_LOG); if (fromWindow) return fromWindow; } catch (e) {} try { var fromStore = normalizeLogLevel(localStorage.getItem('gsoc_log')); if (fromStore) return fromStore; } catch (e) {} return 'debug'; } function allowLog(level) { return LOG_LEVELS[LOG_LEVEL] >= LOG_LEVELS[level]; } function log() { if (!allowLog('debug')) return; try { console.log.apply(console, [PREFIX].concat([].slice.call(arguments))); } catch (e) {} } function warn() { if (!allowLog('warn')) return; try { console.warn.apply(console, [PREFIX].concat([].slice.call(arguments))); } catch (e) {} } function err() { if (!allowLog('error')) return; try { console.error.apply(console, [PREFIX].concat([].slice.call(arguments))); } catch (e) {} } /* Включить/выключить логи на живой странице: GSOC.setLog('off' | 'warn' | 'error' | 'debug'). */ function setLogLevel(value) { var level = normalizeLogLevel(value); if (!level) return LOG_LEVEL; LOG_LEVEL = level; try { localStorage.setItem('gsoc_log', level); } catch (e) {} if (allowLog('warn')) { try { console.log(PREFIX, 'уровень логов: ' + level + ' (запомнил в localStorage.gsoc_log; вернуть полностью — ' + LOG_BACK_ON + ' или ?gsoc=debug в адресе)'); } catch (e) {} } return LOG_LEVEL; } /* Первая строка — версия сборки: печатается, пока уровень не ниже warn. */ if (allowLog('warn')) { try { console.log('itteration: ' + VERSION); } catch (e) {} } /* --- вспомогательные функции для логов (не должны ломать работу скрипта) --- */ /* Безопасный вызов: нет API в окружении — лог всё равно не падает. */ function safe(fn, fallback) { try { var v = fn(); return (v == null || v === '') ? (fallback == null ? '—' : fallback) : String(v); } catch (e) { return fallback == null ? '—' : fallback; } } /* Короткая строка для логов: JSON или текст с обрезкой «хвоста». */ function brief(value, limit) { var s; try { s = (typeof value === 'string') ? value : JSON.stringify(value); } catch (e) { s = String(value); } s = String(s == null ? '' : s); var max = limit || 160; return s.length > max ? s.slice(0, max) + '…(всего ' + s.length + ' симв.)' : s; } /* Что за скрипт, откуда запущен и где выполняется — первое, что нужно видеть в консоли. */ function envInfo() { var place; try { place = (typeof window.parent === 'undefined') ? 'окно' : (window.parent === window ? 'top-окно' : 'iframe'); } catch (e) { place = 'iframe?'; } return 'версия=' + VERSION + ', url=' + safe(function () { return location.href; }) + ', путь=' + safe(function () { return location.pathname; }) + ', окно=' + place + ', readyState=' + safe(function () { return document.readyState; }) + ', скрипт=' + safe(function () { return (document.currentScript && document.currentScript.src) || 'inline (вшит в страницу)'; }) + ', пользователь=' + safe(function () { return currentUserId(); }, 'гость') + ', лог=' + LOG_LEVEL; } var CONFIG = { STORE_PATH: '/_ofert-admin', MARKER: '.gsoc-settings', KEY_SETTINGS: 'gsoc_settings_cache', SETTINGS_TTL: 5 * 60 * 1000, DEFAULT_STYLE: { bg: '#FFFFFF', text: '#333333', link: '#337AB7', fontSize: 14, radius: 4, borderColor: '#DDDDDD', borderWidth: 1 }, SNAPSHOT_TITLE: 'Вы дали следующие согласия', PAY_PATH: /\/sales\/shop\/dealPay\//, PAY_ROOTS: ['.allPayments', '.xdget-payform', '#pay-methods', 'form[action*="Init"]', '#payment-page', '.payment-page'], PAY_BUTTONS: '.gc-payment-method-button, button.pay-button, button[type=submit], a[href*="Init"], .pay-btn-sber-pay, .pay-btn-sbp-pay, .pay-btn-alpha-pay, .pay-btn-split, .pay-btn-fresh-credit', LOCK_CLASS: 'gsoc-pay-locked', HIDE_CLASS: 'gsoc-pay-hidden', ORDER_CARD_PATH: '/sales/control/userProduct/update/id/', ADD_FIELDS_URL: '/pl/user/user/get-user-additional-form-data?id=', ORDER_BLOCK_ATTR: 'data-gsoc-order-block', CONFIRM_PAGE: '/_ofert-confirm', FIELD: 'user_order_details', /* true — блокируем оплату, пока не отмечены пункты с тумблером «обязательный»; false — требуем отметку ВСЕХ видимых пунктов набора. Переключается без правки кода: поставьте в блоке (или в «счётчиках») перед подключением этого скрипта `window.GSOC_REQUIRE_ALL = true;` — тогда флаг будет выключен. Если в наборе вообще нет обязательных пунктов, оплата не блокируется (в консоль пишется предупреждение с подсказкой, что поправить в админке). */ LOCK_ONLY_REQUIRED: !(typeof window !== 'undefined' && window.GSOC_REQUIRE_ALL === true), READY_TIMEOUT: 8000, SAVE_TIMEOUT: 15000, READ_TIMEOUT: 15000, STYLES_ID: 'gsoc-styles' }; function b64encode(str) { var bytes = new TextEncoder().encode(String(str == null ? '' : str)); var bin = ''; var chunk = 0x8000; for (var i = 0; i < bytes.length; i += chunk) { bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); } return btoa(bin); } function b64decode(b64) { var bin = atob(b64); var bytes = new Uint8Array(bin.length); for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return new TextDecoder().decode(bytes); } function esc(str) { return String(str == null ? '' : str) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function currentUserId() { if (typeof window.accountUserId !== 'undefined' && window.accountUserId) return String(window.accountUserId); if (window.gcUser && window.gcUser.id) return String(window.gcUser.id); return null; } function isAdminViewer() { var ui = window.userInfo || {}; return !!(ui.isAdmin || ui.canCms); } /* Описание DOM-элемента для логов (тег, id, классы). */ function describeEl(el) { if (!el) return '—'; var cls = el.className ? '.' + String(el.className).trim().split(/\s+/).join('.') : ''; var id = el.id ? '#' + el.id : ''; var tag = String(el.tagName || '?').toLowerCase(); return tag + id + cls; } /* Почему скрипт решил, что это (или не это) страница оплаты / карточка заказа. */ function pageSignals() { return brief({ путь: safe(function () { return location.pathname; }), 'PAY_PATH совпал': safe(function () { return CONFIG.PAY_PATH.test(location.pathname); }), 'input[name=deal_id]': safe(function () { return !!document.querySelector('input[name="deal_id"]'); }), 'кнопок .gc-payment-method-button': safe(function () { return document.querySelectorAll('.gc-payment-method-button').length; }), '.xdget-payform': safe(function () { return !!document.querySelector('.xdget-payform'); }), 'ORDER_CARD_PATH совпал': safe(function () { return location.pathname.indexOf(CONFIG.ORDER_CARD_PATH) !== -1; }) }, 240); } function injectStyles() { if (document.getElementById(CONFIG.STYLES_ID)) return; var css = [ '.gsoc-pay{margin:0 0 24px}', '.gsoc-pay-body{border-radius:8px;padding:20px 22px}', '.gsoc-pay-title{font-weight:700;margin:0 0 14px;font-size:1.08em;line-height:1.4}', '.gsoc-pay-item{display:flex;align-items:flex-start;gap:10px;margin:0 0 12px;line-height:1.5;cursor:pointer}', '.gsoc-pay-item:last-of-type{margin-bottom:0}', '.gsoc-pay-item input{flex:0 0 auto;width:18px;height:18px;margin:3px 0 0;cursor:pointer}', '.gsoc-pay-body a{color:var(--gsoc-link, inherit);text-decoration:underline;text-underline-offset:2px}', '.gsoc-pay-status{margin-top:14px;font-size:13px;line-height:1.45;opacity:.85}', '.gsoc-pay-status.error{color:#c0392b;opacity:1}', '.gsoc-pay-hidden{display:none !important}', '.gsoc-frame{position:absolute;width:1px;height:1px;left:-9999px;top:0;border:0}', '.gsoc-order-plain .gsoc-pay-body{padding:0;background:transparent;border:0;border-radius:0;color:inherit;font-size:inherit}', '.gsoc-order-plain .gsoc-pay-title{font-weight:700;margin:0 0 8px}', '.gsoc-order-plain .gsoc-pay-item{display:block;cursor:default;margin:0 0 8px;line-height:1.45}', '.gsoc-order-when{margin-top:6px}', '.gsoc-order-empty{font-size:13px;color:#8b8b9e}' ].join(''); var el = document.createElement('style'); el.id = CONFIG.STYLES_ID; el.textContent = css; document.head.appendChild(el); log('стили блока согласий добавлены: ' + CONFIG.STYLES_ID + ', ' + css.length + ' симв.'); } var frame = null; var frameReady = null; /* Диагностика скрытого iframe. Он same-origin, поэтому внутрь можно заглянуть и точно сказать, почему страница подтверждений молчит (нет скрипта / нет формы / нет кода). */ function frameState() { var out = []; try { out.push('src=' + safe(function () { return frame && frame.getAttribute('src'); })); var cdoc = null; var cwin = null; try { cdoc = frame && frame.contentDocument; } catch (e) { cdoc = null; } try { cwin = frame && frame.contentWindow; } catch (e) { cwin = null; } if (!cdoc) { out.push('документ iframe недоступен (другой origin?)'); return out.join(', '); } out.push('readyState=' + safe(function () { return cdoc.readyState; })); out.push('title=' + brief(safe(function () { return cdoc.title; }), 40)); out.push('GSOCConfirm в iframe: ' + (cwin && cwin.GSOCConfirm ? 'есть' : 'НЕТ')); out.push('маркер data-gsoc-confirm=' + safe(function () { return cdoc.documentElement.getAttribute('data-gsoc-confirm'); }, 'НЕТ')); out.push('маркер data-gsoc-admin=' + safe(function () { return cdoc.documentElement.getAttribute('data-gsoc-admin'); }, 'нет')); out.push('GSOC в iframe: ' + (cwin && cwin.GSOC ? 'есть' : 'нет') + ' (его ставит и админка — смотреть на маркеры выше)'); out.push('textarea[name^=formParams[userCustomFields]]: ' + safe(function () { return cdoc.querySelectorAll('textarea[name^="formParams[userCustomFields]"]').length; })); out.push('форма block-public/process: ' + safe(function () { return cdoc.querySelector('form[action*="block-public/process"]') ? 'есть' : 'нет'; })); var html = safe(function () { return cdoc.documentElement ? cdoc.documentElement.innerHTML : ''; }, ''); out.push('упоминания в HTML iframe: postMessage=' + (html.indexOf('postMessage') !== -1) + ', gsoc:ready=' + (html.indexOf('gsoc:ready') !== -1) + ', GSOC-CONFIRM=' + (html.indexOf('GSOC-CONFIRM') !== -1) + ', GSOC(' + PREFIX + ')=' + (html.indexOf(PREFIX) !== -1) + ', update-param=' + (html.indexOf('update-param') !== -1) + ', allPayments=' + (html.indexOf('allPayments') !== -1)); var srcs = []; safe(function () { var list = cdoc.querySelectorAll('script[src]'); for (var i = 0; i < list.length && i < 8; i++) srcs.push(brief(list[i].getAttribute('src'), 60)); return ''; }); if (srcs.length) out.push('внешние скрипты iframe=[' + srcs.join(' | ') + ']'); } catch (e) { out.push('диагностика iframe недоступна: ' + (e && e.message)); } return out.join(', '); } function ensureFrame() { if (frame && frame.parentNode && frameReady) { log('iframe подтверждений уже создан — переиспользую (' + frameState() + ')'); return frameReady; } frame = document.createElement('iframe'); frame.className = 'gsoc-frame'; frame.setAttribute('aria-hidden', 'true'); frame.setAttribute('tabindex', '-1'); frame.src = CONFIG.CONFIRM_PAGE; document.body.appendChild(frame); log('iframe подтверждений: создаю ' + CONFIG.CONFIRM_PAGE + ', жду gsoc:ready/gsoc:pong не дольше ' + CONFIG.READY_TIMEOUT + ' мс'); frameReady = new Promise(function (resolve, reject) { var done = false; var t0 = Date.now(); var timer = setTimeout(function () { if (done) return; done = true; window.removeEventListener('message', onReady); warn('страница ' + CONFIG.CONFIRM_PAGE + ' не ответила за ' + CONFIG.READY_TIMEOUT + ' мс (ни gsoc:ready, ни gsoc:pong). Диагностика iframe: ' + frameState()); reject(new Error('страница ' + CONFIG.CONFIRM_PAGE + ' не ответила')); }, CONFIG.READY_TIMEOUT); function onReady(e) { if (done) return; if (e.origin !== location.origin) { log('сообщение из чужого origin (' + e.origin + ') — игнорирую'); return; } if (e.source !== frame.contentWindow) return; var d = e.data || {}; if (d.type !== 'gsoc:ready' && d.type !== 'gsoc:pong') return; done = true; clearTimeout(timer); window.removeEventListener('message', onReady); log('страница подтверждений готова: тип=' + d.type + ', за ' + (Date.now() - t0) + ' мс (' + frameState() + ')'); resolve(frame); } window.addEventListener('message', onReady); frame.addEventListener('load', function () { log('iframe подтверждений загружен (load через ' + (Date.now() - t0) + ' мс): ' + frameState() + ' → отправляю gsoc:ping'); try { frame.contentWindow.postMessage({ type: 'gsoc:ping' }, location.origin); } catch (e) { warn('не удалось отправить gsoc:ping: ' + (e && e.message)); } }); }); return frameReady; } function frameRequest(type, payload, answerType, timeout) { var t0 = Date.now(); var limit = timeout || CONFIG.SAVE_TIMEOUT; log('frameRequest: ' + type + ' → ' + CONFIG.CONFIRM_PAGE + ', жду ' + answerType + ' до ' + limit + ' мс' + (payload ? ', payload=' + brief(payload, 120) : '')); return ensureFrame().then(function (fr) { return new Promise(function (resolve, reject) { var timer = setTimeout(function () { window.removeEventListener('message', onReply); warn('frameRequest: ' + answerType + ' не пришёл за ' + (Date.now() - t0) + ' мс. Диагностика iframe: ' + frameState()); reject(new Error('таймаут ответа страницы подтверждений')); }, limit); function onReply(e) { if (e.origin !== location.origin) return; if (e.source !== fr.contentWindow) return; var d = e.data || {}; if (d.type !== answerType) { log('frameRequest: получено сообщение типа ' + d.type + ' — жду ' + answerType); return; } clearTimeout(timer); window.removeEventListener('message', onReply); log('frameRequest: ' + d.type + ' получен за ' + (Date.now() - t0) + ' мс, ok=' + !!d.ok + (d.error ? ', error=' + d.error : '') + (d.result ? ', result=' + brief(d.result, 160) : '')); if (d.ok) resolve(d); else reject(new Error(d.error || 'ошибка страницы подтверждений')); } window.addEventListener('message', onReply); var msg = { type: type }; if (payload) msg.payload = payload; log('запрос к ' + CONFIG.CONFIRM_PAGE + ': ' + type); try { fr.contentWindow.postMessage(msg, location.origin); } catch (e) { warn('frameRequest: postMessage не удался — ' + (e && e.message)); reject(e); } }); }); } function saveToConfirm(payload) { log('saveToConfirm: заказ=' + ((payload && payload.dealId) || '—') + ', предложений=[' + (((payload && payload.offerIds) || []).join(', ') || '—') + ']' + ', пунктов=' + (((payload && payload.items) || []).length) + ', savedAt=' + ((payload && payload.savedAt) || '—')); return frameRequest('gsoc:save', payload, 'gsoc:saved', CONFIG.SAVE_TIMEOUT).then(function (d) { var result = d.result || { ok: true }; log('saveToConfirm: ответ ok=' + !!result.ok + ', formProcessed=' + (result.formProcessed === true) + ', поле=' + (result.field || '—') + ', rawHead=' + brief(result.rawHead, 80)); return result; }); } function readFromConfirm() { log('readFromConfirm: читаю ДП через ' + CONFIG.CONFIRM_PAGE); return frameRequest('gsoc:read', null, 'gsoc:value', CONFIG.READ_TIMEOUT).then(function (d) { log('readFromConfirm: значение получено (' + brief(d.value, 120) + ')'); return d.value || null; }); } function unescapeDeep(text) { var out = String(text || ''); for (var i = 0; i < 6; i++) { var next = out.replace(/\\\\/g, '\\').replace(/\\"/g, '"').replace(/\\'/g, "'"); if (next === out) break; out = next; } return out; } function parseRecordJson(text) { var marker = "'orders':"; var idx = text.indexOf(marker); if (idx === -1) return null; var start = text.lastIndexOf('{', idx); if (start === -1) return null; var depth = 0; var inStr = false; for (var i = start; i < text.length; i++) { var ch = text.charAt(i); if (inStr) { if (ch === '\\') { i++; continue; } if (ch === "'") inStr = false; continue; } if (ch === "'") { inStr = true; continue; } if (ch === '{') depth++; else if (ch === '}') { depth--; if (depth === 0) { try { return JSON.parse(text.slice(start, i + 1).replace(/'/g, '"')); } catch (e) { return null; } } } } return null; } function decodeStoredValue(rawValue) { var raw = String(rawValue || '').trim(); if (raw.indexOf('\\"') === 0 && raw.lastIndexOf('\\"') === raw.length - 2) raw = raw.slice(2, -2); return parseRecordJson(unescapeDeep(raw)); } function recordsForDeals(data, dealIds) { var orders = (data && data.orders) || {}; var found = []; dealIds.forEach(function (id) { if (orders[id]) found.push({ dealId: id, record: orders[id] }); }); return found; } function formatDate(iso) { if (!iso) return ''; var d = new Date(iso); if (isNaN(d.getTime())) return iso; var pad = function (n) { return (n < 10 ? '0' : '') + n; }; return pad(d.getDate()) + '.' + pad(d.getMonth() + 1) + '.' + d.getFullYear() + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); } var pay = { ready: false, saving: false, saved: false, locked: false, dealId: null, offerIds: [], userId: null, setName: null, settings: null, items: [], payRoot: null, blockEl: null }; function isPayPage() { if (CONFIG.PAY_PATH.test(location.pathname)) return true; var signals = 0; if (document.querySelector('input[name="deal_id"]')) signals++; if (document.querySelector('.gc-payment-method-button')) signals++; if (document.querySelector('.xdget-payform')) signals++; log('isPayPage: путь не похож на оплату, признаков разметки=' + signals + '/3 (нужно ≥2)'); return signals >= 2; } function findPayRoot() { for (var i = 0; i < CONFIG.PAY_ROOTS.length; i++) { var el = document.querySelector(CONFIG.PAY_ROOTS[i]); if (el) { log('зона оплаты найдена по селектору "' + CONFIG.PAY_ROOTS[i] + '" → ' + describeEl(el)); return el; } } warn('зона оплаты не найдена. Проверенные селекторы: ' + CONFIG.PAY_ROOTS.join(' | ')); return null; } function payContext() { var m = /\/id\/(\d+)/.exec(location.pathname); var input = document.querySelector('input[name="deal_id"]'); var dealId = m ? m[1] : (input ? String(input.value) : ''); var offers = []; var nodes = document.querySelectorAll('[data-offer-id]'); for (var i = 0; i < nodes.length; i++) { var v = String(nodes[i].getAttribute('data-offer-id') || '').trim(); if (v && offers.indexOf(v) === -1) offers.push(v); } log('payContext: заказ=' + (dealId || '—') + ' (источник: ' + (m ? 'URL /id/<число>' : (input ? 'input[name=deal_id]' : 'не найден')) + '), предложений по [data-offer-id]=' + nodes.length + ' → [' + offers.join(', ') + ']'); if (!offers.length) { var lis = document.querySelectorAll('li[class*="offer-position-"]'); for (var j = 0; j < lis.length; j++) { var id = (String(lis[j].className).match(/offer-position-(\d+)/) || [])[1]; if (id && offers.indexOf(id) === -1) offers.push(id); } log('payContext: резервный поиск по li.offer-position-* — найдено ' + lis.length + ' элементов → [' + offers.join(', ') + ']'); } return { dealId: dealId, offerIds: offers, userId: currentUserId() }; } function readSettingsFromDom() { var marker = document.querySelector(CONFIG.MARKER); if (!marker) { log('блок настроек (' + CONFIG.MARKER + ') на этой странице не найден'); return null; } var raw = marker.getAttribute('data-gsoc'); if (!raw) { warn('блок настроек найден, но у него нет атрибута data-gsoc (пустой блок?)'); return null; } log('блок настроек найден: data-gsoc ' + raw.length + ' симв., элемент ' + describeEl(marker)); try { var parsed = JSON.parse(b64decode(raw)); log('настройки из блока: ' + describeSettings(parsed)); return parsed; } catch (e) { warn('data-gsoc не разобрался (base64/JSON): ' + (e && e.message)); return null; } } /* Краткая сводка настроек для консоли: версия, наборы, обязательные пункты. */ function describeSettings(settings) { if (!settings || typeof settings !== 'object') return 'нет настроек'; var offers = settings.offers || {}; var keys = Object.keys(offers); var def = (settings.default && settings.default.items) || []; var visible = def.filter(function (it) { return it.active !== false; }).length; var required = def.filter(function (it) { return it.active !== false && it.required !== false; }).length; return 'updatedAt=' + (settings.updatedAt || '—') + ', title="' + brief(settings.title, 40) + '"' + ', наборов=' + keys.length + ' [' + keys.join(', ') + ']' + ', пунктов по умолчанию=' + def.length + ' (видимых ' + visible + ', обязательных ' + required + ')'; } function cacheSettings(settings) { try { localStorage.setItem(CONFIG.KEY_SETTINGS, JSON.stringify({ at: Date.now(), settings: settings })); } catch (e) {} } function cachedSettings() { try { var parsed = JSON.parse(localStorage.getItem(CONFIG.KEY_SETTINGS) || 'null'); if (!parsed || !parsed.at || !parsed.settings) return null; var age = Date.now() - parsed.at; if (age > CONFIG.SETTINGS_TTL) { log('кэш настроек устарел (' + Math.round(age / 1000) + ' с > ' + (CONFIG.SETTINGS_TTL / 1000) + ' с)'); return null; } return parsed.settings; } catch (e) { return null; } } function loadSettings() { var fromDom = readSettingsFromDom(); if (fromDom) { pay.settings = fromDom; cacheSettings(fromDom); log('настройки взяты из блока на этой странице'); return Promise.resolve(fromDom); } var cached = cachedSettings(); log('настройки: читаю страницу-хранилище ' + CONFIG.STORE_PATH + (cached ? ' (кэш localStorage есть — ' + describeSettings(cached) + ')' : ' (кэша localStorage нет)')); var t0 = Date.now(); return fetch(CONFIG.STORE_PATH + '?gsoc=' + Date.now(), { credentials: 'include' }) .then(function (r) { log('страница-хранилище: ответ ' + r.status + ' за ' + (Date.now() - t0) + ' мс'); return r.text(); }) .then(function (html) { log('страница-хранилище: HTML ' + String(html || '').length + ' симв.'); var m = /data-gsoc="([A-Za-z0-9+/=]+)"/.exec(html || ''); if (!m) throw new Error('в ' + CONFIG.STORE_PATH + ' не найден блок настроек'); var settings = JSON.parse(b64decode(m[1])); pay.settings = settings; cacheSettings(settings); log('настройки прочитаны со страницы-хранилища: ' + describeSettings(settings)); return settings; })['catch'](function (error) { if (cached) { warn('нет доступа к настройкам, беру кэш (' + (error.message || error) + ')'); pay.settings = cached; return cached; } throw error; }); } /* Поле «ID предложения» в админке допускает список: «3929853, 3928543». Разбираем значение по запятым, точкам с запятой и пробелам/переносам. */ function offerIdList(value) { return String(value == null ? '' : value) .split(/[\s,;]+/) .filter(function (v) { return !!v; }); } /* Возвращает ID набора, который есть в предложениях заказа (или null). */ function matchOfferSet(set, offerIds) { var ids = offerIdList(set && set.id); for (var i = 0; i < ids.length; i++) { if (offerIds.indexOf(ids[i]) !== -1) return ids[i]; } return null; } function pickItems(settings, offerIds) { var wanted = (Array.isArray(offerIds) ? offerIds : offerIdList(offerIds)).map(String); var offers = settings.offers || {}; var keys = Object.keys(offers); log('pickItems: предложения заказа=[' + wanted.join(', ') + '], наборов в настройках=' + keys.length + ' [' + keys.map(function (k) { return k + '→' + (offers[k] && offers[k].id); }).join(' | ') + ']'); for (var i = 0; i < keys.length; i++) { var set = offers[keys[i]] || {}; var hit = matchOfferSet(set, wanted); if (!hit) continue; var ids = offerIdList(set.id); log('набор предложения ' + hit + ' (' + (set.name || '') + ')' + (ids.length > 1 ? ' [совпал из списка: ' + ids.join(', ') + ']' : '')); return (set.items || []).filter(function (it) { return it.active !== false; }); } log('набор по умолчанию (совпадений с предложениями заказа нет)'); var def = settings.default && settings.default.items ? settings.default.items : []; return def.filter(function (it) { return it.active !== false; }); } function itemMarkup(item) { var html = esc(item.prefix || ''); if (item.linkText) { html += (html ? ' ' : '') + '' + esc(item.linkText) + ''; } if (item.suffix) html += ' ' + esc(item.suffix); return html; } function payStyleCss() { var def = CONFIG.DEFAULT_STYLE; var st = (pay.settings && pay.settings.style) || {}; return 'background:' + (st.bg || def.bg) + ';color:' + (st.text || def.text) + ';--gsoc-link:' + (st.link || def.link) + ';border-radius:' + (st.radius == null ? def.radius : st.radius) + 'px' + ';border:' + (st.borderWidth == null ? def.borderWidth : st.borderWidth) + 'px solid ' + (st.borderColor || def.borderColor) + ';font-size:' + (st.fontSize || def.fontSize) + 'px'; } function payBlockHtml() { var items = pay.items.map(function (item) { return '