From 98353b00323e04a9276a33a7341440b9a3abe4d7 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sun, 7 May 2017 17:01:53 +0200 Subject: [PATCH] Make the exception pages work when the WebProfilerBundle is not installed --- .../Resources/views/base_js.html.twig | 520 ++++++++++++++++++ .../Resources/views/layout.html.twig | 1 + .../views/Profiler/base_js.html.twig | 2 + 3 files changed, 523 insertions(+) create mode 100644 src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig new file mode 100644 index 0000000000000..d85d2e0ea32ae --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig @@ -0,0 +1,520 @@ +{# This file is duplicated in WebProfilerBundle/Resources/views/Profiler/base_js.html.twig. + If you make any change in this file, do the same change in the other file. #} +/* 1) { + setTimeout(function(){ + options.maxTries--; + request(url, onSuccess, onError, payload, options); + }, 500); + + return null; + } + + if (200 === xhr.status) { + (onSuccess || noop)(xhr); + } else { + (onError || noop)(xhr); + } + }; + xhr.send(payload || ''); + }; + + var getPreference = function(name) { + if (!window.localStorage) { + return null; + } + + return localStorage.getItem(profilerStorageKey + name); + }; + + var setPreference = function(name, value) { + if (!window.localStorage) { + return null; + } + + localStorage.setItem(profilerStorageKey + name, value); + }; + + var requestStack = []; + + var extractHeaders = function(xhr, stackElement) { + /* Here we avoid to call xhr.getResponseHeader in order to */ + /* prevent polluting the console with CORS security errors */ + var allHeaders = xhr.getAllResponseHeaders(); + var ret; + + if (ret = allHeaders.match(/^x-debug-token:\s+(.*)$/im)) { + stackElement.profile = ret[1]; + } + if (ret = allHeaders.match(/^x-debug-token-link:\s+(.*)$/im)) { + stackElement.profilerUrl = ret[1]; + } + }; + + var successStreak = 4; + var pendingRequests = 0; + var renderAjaxRequests = function() { + var requestCounter = document.querySelector('.sf-toolbar-ajax-request-counter'); + if (!requestCounter) { + return; + } + requestCounter.textContent = requestStack.length; + + var infoSpan = document.querySelector(".sf-toolbar-ajax-info"); + if (infoSpan) { + infoSpan.textContent = requestStack.length + ' AJAX request' + (requestStack.length !== 1 ? 's' : ''); + } + + var ajaxToolbarPanel = document.querySelector('.sf-toolbar-block-ajax'); + if (requestStack.length) { + ajaxToolbarPanel.style.display = 'block'; + } else { + ajaxToolbarPanel.style.display = 'none'; + } + if (pendingRequests > 0) { + addClass(ajaxToolbarPanel, 'sf-ajax-request-loading'); + } else if (successStreak < 4) { + addClass(ajaxToolbarPanel, 'sf-toolbar-status-red'); + removeClass(ajaxToolbarPanel, 'sf-ajax-request-loading'); + } else { + removeClass(ajaxToolbarPanel, 'sf-ajax-request-loading'); + removeClass(ajaxToolbarPanel, 'sf-toolbar-status-red'); + } + }; + + var startAjaxRequest = function(index) { + var tbody = document.querySelector('.sf-toolbar-ajax-request-list'); + if (!tbody) { + return; + } + + var request = requestStack[index]; + pendingRequests++; + var row = document.createElement('tr'); + request.DOMNode = row; + + var methodCell = document.createElement('td'); + methodCell.textContent = request.method; + row.appendChild(methodCell); + + var typeCell = document.createElement('td'); + typeCell.textContent = request.type; + row.appendChild(typeCell); + + var statusCodeCell = document.createElement('td'); + var statusCode = document.createElement('span'); + statusCode.textContent = 'n/a'; + statusCodeCell.appendChild(statusCode); + row.appendChild(statusCodeCell); + + var pathCell = document.createElement('td'); + pathCell.className = 'sf-ajax-request-url'; + if ('GET' === request.method) { + var pathLink = document.createElement('a'); + pathLink.setAttribute('href', request.url); + pathLink.textContent = request.url; + pathCell.appendChild(pathLink); + } else { + pathCell.textContent = request.url; + } + pathCell.setAttribute('title', request.url); + row.appendChild(pathCell); + + var durationCell = document.createElement('td'); + durationCell.className = 'sf-ajax-request-duration'; + durationCell.textContent = 'n/a'; + row.appendChild(durationCell); + + var profilerCell = document.createElement('td'); + profilerCell.textContent = 'n/a'; + row.appendChild(profilerCell); + + row.className = 'sf-ajax-request sf-ajax-request-loading'; + tbody.insertBefore(row, tbody.firstChild); + + renderAjaxRequests(); + }; + + var finishAjaxRequest = function(index) { + var request = requestStack[index]; + if (!request.DOMNode) { + return; + } + pendingRequests--; + var row = request.DOMNode; + /* Unpack the children from the row */ + var methodCell = row.children[0]; + var statusCodeCell = row.children[2]; + var statusCodeElem = statusCodeCell.children[0]; + var durationCell = row.children[4]; + var profilerCell = row.children[5]; + + if (request.error) { + row.className = 'sf-ajax-request sf-ajax-request-error'; + methodCell.className = 'sf-ajax-request-error'; + successStreak = 0; + } else { + row.className = 'sf-ajax-request sf-ajax-request-ok'; + successStreak++; + } + + if (request.statusCode) { + if (request.statusCode < 300) { + statusCodeElem.setAttribute('class', 'sf-toolbar-status'); + } else if (request.statusCode < 400) { + statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-yellow'); + } else { + statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-red'); + } + statusCodeElem.textContent = request.statusCode; + } else { + statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-red'); + } + + if (request.duration) { + durationCell.textContent = request.duration + 'ms'; + } + + if (request.profilerUrl) { + profilerCell.textContent = ''; + var profilerLink = document.createElement('a'); + profilerLink.setAttribute('href', request.profilerUrl); + profilerLink.textContent = request.profile; + profilerCell.appendChild(profilerLink); + } + + renderAjaxRequests(); + }; + + var addEventListener; + + var el = document.createElement('div'); + if (!('addEventListener' in el)) { + addEventListener = function (element, eventName, callback) { + element.attachEvent('on' + eventName, callback); + }; + } else { + addEventListener = function (element, eventName, callback) { + element.addEventListener(eventName, callback, false); + }; + } + + {% if excluded_ajax_paths is defined %} + if (window.fetch && window.fetch.polyfill === undefined) { + var oldFetch = window.fetch; + window.fetch = function () { + var promise = oldFetch.apply(this, arguments); + var url = arguments[0]; + var params = arguments[1]; + var paramType = Object.prototype.toString.call(arguments[0]); + if (paramType === '[object Request]') { + url = arguments[0].url; + params = { + method: arguments[0].method, + credentials: arguments[0].credentials, + headers: arguments[0].headers, + mode: arguments[0].mode, + redirect: arguments[0].redirect + }; + } + if (!url.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) { + var method = 'GET'; + if (params && params.method !== undefined) { + method = params.method; + } + + var stackElement = { + error: false, + url: url, + method: method, + type: 'fetch', + start: new Date() + }; + + var idx = requestStack.push(stackElement) - 1; + promise.then(function (r) { + stackElement.duration = new Date() - stackElement.start; + stackElement.error = r.status < 200 || r.status >= 400; + stackElement.statusCode = r.status; + stackElement.profile = r.headers.get('x-debug-token'); + stackElement.profilerUrl = r.headers.get('x-debug-token-link'); + finishAjaxRequest(idx); + }, function (e){ + stackElement.error = true; + finishAjaxRequest(idx); + }); + startAjaxRequest(idx); + } + + return promise; + }; + } + if (window.XMLHttpRequest && XMLHttpRequest.prototype.addEventListener) { + var proxied = XMLHttpRequest.prototype.open; + + XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { + var self = this; + + /* prevent logging AJAX calls to static and inline files, like templates */ + var path = url; + if (url.substr(0, 1) === '/') { + if (0 === url.indexOf('{{ request.basePath|e('js') }}')) { + path = url.substr({{ request.basePath|length }}); + } + } + else if (0 === url.indexOf('{{ (request.schemeAndHttpHost ~ request.basePath)|e('js') }}')) { + path = url.substr({{ (request.schemeAndHttpHost ~ request.basePath)|length }}); + } + + if (!path.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) { + var stackElement = { + error: false, + url: url, + method: method, + type: 'xhr', + start: new Date() + }; + + var idx = requestStack.push(stackElement) - 1; + + this.addEventListener('readystatechange', function() { + if (self.readyState == 4) { + stackElement.duration = new Date() - stackElement.start; + stackElement.error = self.status < 200 || self.status >= 400; + stackElement.statusCode = self.status; + extractHeaders(self, stackElement); + + finishAjaxRequest(idx); + } + }, false); + + startAjaxRequest(idx); + } + + proxied.apply(this, Array.prototype.slice.call(arguments)); + }; + } + {% endif %} + + return { + hasClass: hasClass, + + removeClass: removeClass, + + addClass: addClass, + + toggleClass: toggleClass, + + getPreference: getPreference, + + setPreference: setPreference, + + addEventListener: addEventListener, + + request: request, + + renderAjaxRequests: renderAjaxRequests, + + load: function(selector, url, onSuccess, onError, options) { + var el = document.getElementById(selector); + + if (el && el.getAttribute('data-sfurl') !== url) { + request( + url, + function(xhr) { + el.innerHTML = xhr.responseText; + el.setAttribute('data-sfurl', url); + removeClass(el, 'loading'); + for (var i = 0; i < requestStack.length; i++) { + startAjaxRequest(i); + if (requestStack[i].duration) { + finishAjaxRequest(i); + } + } + (onSuccess || noop)(xhr, el); + }, + function(xhr) { (onError || noop)(xhr, el); }, + '', + options + ); + } + + return this; + }, + + toggle: function(selector, elOn, elOff) { + var tmp = elOn.style.display, + el = document.getElementById(selector); + + elOn.style.display = elOff.style.display; + elOff.style.display = tmp; + + if (el) { + el.style.display = 'none' === tmp ? 'none' : 'block'; + } + + return this; + }, + + createTabs: function() { + var tabGroups = document.querySelectorAll('.sf-tabs'); + + /* create the tab navigation for each group of tabs */ + for (var i = 0; i < tabGroups.length; i++) { + var tabs = tabGroups[i].querySelectorAll('.tab'); + var tabNavigation = document.createElement('ul'); + tabNavigation.className = 'tab-navigation'; + + for (var j = 0; j < tabs.length; j++) { + var tabId = 'tab-' + i + '-' + j; + var tabTitle = tabs[j].querySelector('.tab-title').innerHTML; + + var tabNavigationItem = document.createElement('li'); + tabNavigationItem.setAttribute('data-tab-id', tabId); + if (j == 0) { addClass(tabNavigationItem, 'active'); } + if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); } + tabNavigationItem.innerHTML = tabTitle; + tabNavigation.appendChild(tabNavigationItem); + + var tabContent = tabs[j].querySelector('.tab-content'); + tabContent.parentElement.setAttribute('id', tabId); + } + + tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild); + } + + /* display the active tab and add the 'click' event listeners */ + for (i = 0; i < tabGroups.length; i++) { + tabNavigation = tabGroups[i].querySelectorAll('.tab-navigation li'); + + for (j = 0; j < tabNavigation.length; j++) { + tabId = tabNavigation[j].getAttribute('data-tab-id'); + document.getElementById(tabId).querySelector('.tab-title').className = 'hidden'; + + if (hasClass(tabNavigation[j], 'active')) { + document.getElementById(tabId).className = 'block'; + } else { + document.getElementById(tabId).className = 'hidden'; + } + + tabNavigation[j].addEventListener('click', function(e) { + var activeTab = e.target || e.srcElement; + + /* needed because when the tab contains HTML contents, user can click */ + /* on any of those elements instead of their parent '
  • ' element */ + while (activeTab.tagName.toLowerCase() !== 'li') { + activeTab = activeTab.parentNode; + } + + /* get the full list of tabs through the parent of the active tab element */ + var tabNavigation = activeTab.parentNode.children; + for (var k = 0; k < tabNavigation.length; k++) { + var tabId = tabNavigation[k].getAttribute('data-tab-id'); + document.getElementById(tabId).className = 'hidden'; + removeClass(tabNavigation[k], 'active'); + } + + addClass(activeTab, 'active'); + var activeTabId = activeTab.getAttribute('data-tab-id'); + document.getElementById(activeTabId).className = 'block'; + }); + } + } + }, + + createToggles: function() { + var toggles = document.querySelectorAll('.sf-toggle'); + + for (var i = 0; i < toggles.length; i++) { + var elementSelector = toggles[i].getAttribute('data-toggle-selector'); + var element = document.querySelector(elementSelector); + + addClass(element, 'sf-toggle-content'); + + if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') { + addClass(toggles[i], 'sf-toggle-on'); + addClass(element, 'sf-toggle-visible'); + } else { + addClass(toggles[i], 'sf-toggle-off'); + addClass(element, 'sf-toggle-hidden'); + } + + addEventListener(toggles[i], 'click', function(e) { + e.preventDefault(); + + var toggle = e.target || e.srcElement; + + /* needed because when the toggle contains HTML contents, user can click */ + /* on any of those elements instead of their parent '.sf-toggle' element */ + while (!hasClass(toggle, 'sf-toggle')) { + toggle = toggle.parentNode; + } + + var element = document.querySelector(toggle.getAttribute('data-toggle-selector')); + + toggleClass(toggle, 'sf-toggle-on'); + toggleClass(toggle, 'sf-toggle-off'); + toggleClass(element, 'sf-toggle-hidden'); + toggleClass(element, 'sf-toggle-visible'); + + /* the toggle doesn't change its contents when clicking on it */ + if (!toggle.hasAttribute('data-toggle-alt-content')) { + return; + } + + if (!toggle.hasAttribute('data-toggle-original-content')) { + toggle.setAttribute('data-toggle-original-content', toggle.innerHTML); + } + + var currentContent = toggle.innerHTML; + var originalContent = toggle.getAttribute('data-toggle-original-content'); + var altContent = toggle.getAttribute('data-toggle-alt-content'); + toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; + }); + } + } + }; + })(); + + Sfjs.addEventListener(window, 'load', function() { + Sfjs.createTabs(); + Sfjs.createToggles(); + }); + +/*]]>*/ diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig index 06af1480d8925..d3a6786c59b2c 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig @@ -31,5 +31,6 @@ {% block body %}{% endblock %} + {{ include('@Twig/base_js.html.twig') }} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 1b4ab193e6abc..79da38a230f01 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -1,3 +1,5 @@ +{# This file is duplicated in TwigBundle/Resources/views/base_js.html.twig. If you + make any change in this file, do the same change in the other file. #} /*