/** * Доступ к куке сессии внутри чужого фрейма (просмотрщик тепловых карт Я.Метрики). * * Safari блокирует сторонние куки всегда, точечного исключения для сайта в нём нет, * поэтому CRM во фрейме metrika.yandex.ru открывается неавторизованной. Штатное * решение — Storage Access API: по клику пользователя фрейм просит доступ к обычной * (непартиционированной) куке сайта, браузер спрашивает разрешение и после этого * отдаёт во фрейм ту же сессию, под которой человек уже работает в CRM. * * Условия со стороны сервера выполнены: кука отдаётся с SameSite=None; Secure * (см. .htaccess). Safari дополнительно требует, чтобы пользователь заходил на сайт * как на первую сторону в последние 30 дней — для рабочей CRM это всегда так. * * Скрипт ничего не делает сам по себе: он только объявляет * window.JustCRMStorageAccess.promptIfNeeded(). Вызывать её должен тот, кто знает, * что пользователь НЕ авторизован: * - index.php — по пустому $_SESSION['GUID'], когда открыт напрямую во фрейме; * - main_top.js — по пустому window.frames[0].guid внутреннего фрейма. * Наличие куки таким признаком служить не может: оболочка /JustCRM сессию не стартует, * и на момент её загрузки сессионной куки ещё нет даже у авторизованного пользователя. * * Запрос доступа отправляем только из фрейма, который является ПРЯМЫМ потомком чужого * окна: Safari отказывает вложенным фреймам. Поэтому во внутреннем фрейме под нашей же * оболочкой окно не показывается — там за него это делает оболочка. */ (function() { 'use strict'; var RELOAD_FLAG = 'JustCRM-storage-access-reloaded'; // Верхнее окно чужое? Обращение к его свойствам бросает SecurityError. function isForeignTop() { if (window.top === window) return false; try { void window.top.document; return false; } catch (e) { return true; } } // Мы — прямой потомок чужого окна (родитель недоступен). function isDirectChildOfForeign() { if (window.parent === window) return false; try { void window.parent.document; return false; } catch (e) { return true; } } function flagGet() { try { return sessionStorage.getItem(RELOAD_FLAG) === '1'; } catch (e) { return false; } } function flagSet() { try { sessionStorage.setItem(RELOAD_FLAG, '1'); } catch (e) {} } function requestAccess(onFail) { // Вызов обязан идти из обработчика клика — это требование браузера. var p; try { p = document.requestStorageAccess(); } catch (e) { onFail('Браузер отклонил запрос: ' + (e && e.message ? e.message : e)); return; } if (!p || typeof p.then !== 'function') { onFail('Браузер не вернул результат запроса.'); return; } p.then(function() { flagSet(); window.location.reload(); }, function(e) { onFail('Доступ не выдан' + (e && e.message ? ': ' + e.message : '.')); }); } function showPrompt() { if (document.querySelector('[data-justcrm-storage-access]')) return; if (!document.body) return; var box = document.createElement('div'); box.setAttribute('data-justcrm-storage-access', '1'); box.style.cssText = [ 'position:fixed', 'inset:0', 'z-index:2147483647', 'display:flex', 'align-items:center', 'justify-content:center', 'background:#ffffff', 'font:14px/1.5 -apple-system,BlinkMacSystemFont,Arial,sans-serif', 'color:#333', 'text-align:center', 'padding:20px' ].join(';'); var inner = document.createElement('div'); inner.style.cssText = 'max-width:420px'; var title = document.createElement('div'); title.textContent = 'Показать данные CRM'; title.style.cssText = 'font-size:18px;font-weight:600;margin-bottom:10px;color:#333'; var text = document.createElement('div'); text.textContent = 'Страница открыта во фрейме Яндекс.Метрики. Чтобы показать её так, ' + 'как её видят пользователи, браузер должен разрешить сайту доступ к вашей сессии CRM.'; text.style.cssText = 'margin-bottom:18px'; var btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Разрешить и показать'; // Инлайн-стилями и с !important: глобальные правила для button/input из main.css // перебивают обычные объявления. btn.style.cssText = 'display:inline-block;padding:10px 22px;font-size:15px;' + 'border:0;border-radius:4px;cursor:pointer'; btn.style.setProperty('height', 'auto', 'important'); btn.style.setProperty('background', '#a03232', 'important'); btn.style.setProperty('color', '#fff', 'important'); var err = document.createElement('div'); err.style.cssText = 'margin-top:14px;color:#a03232;min-height:20px'; // Обязательный выход: под окном лежит обычная логин-форма. Без этой ссылки // пользователь, который просто не авторизован в CRM, не смог бы войти вовсе. var skip = document.createElement('a'); skip.href = '#'; skip.textContent = 'Войти вручную'; skip.style.cssText = 'display:inline-block;margin-top:16px;color:#555;font-size:13px'; skip.addEventListener('click', function(e) { e.preventDefault(); box.parentNode.removeChild(box); }); btn.addEventListener('click', function() { btn.disabled = true; err.textContent = ''; requestAccess(function(msg) { btn.disabled = false; err.textContent = msg + ' В Safari то же самое даёт снятие галки ' + '«Не разрешать перекрёстное отслеживание» в настройках приватности.'; }); }); inner.appendChild(title); inner.appendChild(text); inner.appendChild(btn); inner.appendChild(err); inner.appendChild(skip); box.appendChild(inner); document.body.appendChild(box); } function promptIfNeeded() { if (!isForeignTop()) return; // обычная работа CRM if (!isDirectChildOfForeign()) return; // вложенный фрейм — просит оболочка if (typeof document.requestStorageAccess !== 'function') return; // браузер без API if (typeof document.hasStorageAccess !== 'function') { showPrompt(); return; } document.hasStorageAccess().then(function(granted) { if (!granted) { showPrompt(); return; } // Доступ уже выдан. Если куку не приложили к этому запросу документа — // достаточно одной перезагрузки; флаг в sessionStorage не даёт зациклиться. if (!flagGet()) { flagSet(); window.location.reload(); return; } // Перезагрузка была, доступ есть, а сессии всё равно нет — значит человек // просто не авторизован в CRM. Окно не показываем, пусть видит логин-форму. }, showPrompt); } window.JustCRMStorageAccess = { promptIfNeeded: promptIfNeeded }; })();