neterror.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // Copyright 2013 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. /**
  5. * @typedef {{
  6. * downloadButtonClick: function(),
  7. * reloadButtonClick: function(string),
  8. * detailsButtonClick: function(),
  9. * diagnoseErrorsButtonClick: function(),
  10. * trackEasterEgg: function(),
  11. * updateEasterEggHighScore: function(number),
  12. * resetEasterEggHighScore: function(),
  13. * launchOfflineItem: function(string, string),
  14. * savePageForLater: function(),
  15. * cancelSavePage: function(),
  16. * listVisibilityChange: function(boolean),
  17. * }}
  18. */
  19. // eslint-disable-next-line no-var
  20. var errorPageController;
  21. const HIDDEN_CLASS = 'hidden';
  22. // Decodes a UTF16 string that is encoded as base64.
  23. function decodeUTF16Base64ToString(encoded_text) {
  24. const data = atob(encoded_text);
  25. let result = '';
  26. for (let i = 0; i < data.length; i += 2) {
  27. result +=
  28. String.fromCharCode(data.charCodeAt(i) * 256 + data.charCodeAt(i + 1));
  29. }
  30. return result;
  31. }
  32. function toggleHelpBox() {
  33. const helpBoxOuter = document.getElementById('details');
  34. helpBoxOuter.classList.toggle(HIDDEN_CLASS);
  35. const detailsButton = document.getElementById('details-button');
  36. if (helpBoxOuter.classList.contains(HIDDEN_CLASS)) {
  37. /** @suppress {missingProperties} */
  38. detailsButton.innerText = detailsButton.detailsText;
  39. } else {
  40. /** @suppress {missingProperties} */
  41. detailsButton.innerText = detailsButton.hideDetailsText;
  42. }
  43. // Details appears over the main content on small screens.
  44. if (mobileNav) {
  45. document.getElementById('main-content').classList.toggle(HIDDEN_CLASS);
  46. const runnerContainer = document.querySelector('.runner-container');
  47. if (runnerContainer) {
  48. runnerContainer.classList.toggle(HIDDEN_CLASS);
  49. }
  50. }
  51. }
  52. function diagnoseErrors() {
  53. if (window.errorPageController) {
  54. errorPageController.diagnoseErrorsButtonClick();
  55. }
  56. }
  57. // Subframes use a different layout but the same html file. This is to make it
  58. // easier to support platforms that load the error page via different
  59. // mechanisms (Currently just iOS). We also use the subframe style for portals
  60. // as they are embedded like subframes and can't be interacted with by the user.
  61. let isSubFrame = false;
  62. if (window.top.location !== window.location || window.portalHost) {
  63. document.documentElement.setAttribute('subframe', '');
  64. isSubFrame = true;
  65. }
  66. // Re-renders the error page using |strings| as the dictionary of values.
  67. // Used by NetErrorTabHelper to update DNS error pages with probe results.
  68. function updateForDnsProbe(strings) {
  69. const context = new JsEvalContext(strings);
  70. jstProcess(context, document.getElementById('t'));
  71. onDocumentLoadOrUpdate();
  72. }
  73. // Adds an icon class to the list and removes classes previously set.
  74. function updateIconClass(newClass) {
  75. const frameSelector = isSubFrame ? '#sub-frame-error' : '#main-frame-error';
  76. const iconEl = document.querySelector(frameSelector + ' .icon');
  77. if (iconEl.classList.contains(newClass)) {
  78. return;
  79. }
  80. iconEl.className = 'icon ' + newClass;
  81. }
  82. // Implements button clicks. This function is needed during the transition
  83. // between implementing these in trunk chromium and implementing them in iOS.
  84. function reloadButtonClick(url) {
  85. if (window.errorPageController) {
  86. // <if expr="is_ios">
  87. errorPageController.reloadButtonClick(url);
  88. // </if>
  89. // <if expr="not is_ios">
  90. errorPageController.reloadButtonClick();
  91. // </if>
  92. } else {
  93. window.location = url;
  94. }
  95. }
  96. function downloadButtonClick() {
  97. if (window.errorPageController) {
  98. errorPageController.downloadButtonClick();
  99. const downloadButton = document.getElementById('download-button');
  100. downloadButton.disabled = true;
  101. /** @suppress {missingProperties} */
  102. downloadButton.textContent = downloadButton.disabledText;
  103. document.getElementById('download-link-wrapper')
  104. .classList.add(HIDDEN_CLASS);
  105. document.getElementById('download-link-clicked-wrapper')
  106. .classList.remove(HIDDEN_CLASS);
  107. }
  108. }
  109. function detailsButtonClick() {
  110. if (window.errorPageController) {
  111. errorPageController.detailsButtonClick();
  112. }
  113. }
  114. let primaryControlOnLeft = true;
  115. // clang-format off
  116. // <if expr="is_macosx or is_ios or is_linux or is_chromeos or is_android">
  117. // clang-format on
  118. primaryControlOnLeft = false;
  119. // </if>
  120. function setAutoFetchState(scheduled, can_schedule) {
  121. document.getElementById('cancel-save-page-button')
  122. .classList.toggle(HIDDEN_CLASS, !scheduled);
  123. document.getElementById('save-page-for-later-button')
  124. .classList.toggle(HIDDEN_CLASS, scheduled || !can_schedule);
  125. }
  126. function savePageLaterClick() {
  127. errorPageController.savePageForLater();
  128. // savePageForLater will eventually trigger a call to setAutoFetchState() when
  129. // it completes.
  130. }
  131. function cancelSavePageClick() {
  132. errorPageController.cancelSavePage();
  133. // setAutoFetchState is not called in response to cancelSavePage(), so do it
  134. // now.
  135. setAutoFetchState(false, true);
  136. }
  137. function toggleErrorInformationPopup() {
  138. document.getElementById('error-information-popup-container')
  139. .classList.toggle(HIDDEN_CLASS);
  140. }
  141. function launchOfflineItem(itemID, name_space) {
  142. errorPageController.launchOfflineItem(itemID, name_space);
  143. }
  144. function launchDownloadsPage() {
  145. errorPageController.launchDownloadsPage();
  146. }
  147. function getIconForSuggestedItem(item) {
  148. // Note: |item.content_type| contains the enum values from
  149. // chrome::mojom::AvailableContentType.
  150. switch (item.content_type) {
  151. case 1: // kVideo
  152. return 'image-video';
  153. case 2: // kAudio
  154. return 'image-music-note';
  155. case 0: // kPrefetchedPage
  156. case 3: // kOtherPage
  157. return 'image-earth';
  158. }
  159. return 'image-file';
  160. }
  161. function getSuggestedContentDiv(item, index) {
  162. // Note: See AvailableContentToValue in available_offline_content_helper.cc
  163. // for the data contained in an |item|.
  164. // TODO(carlosk): Present |snippet_base64| when that content becomes
  165. // available.
  166. let thumbnail = '';
  167. const extraContainerClasses = [];
  168. // html_inline.py will try to replace src attributes with data URIs using a
  169. // simple regex. The following is obfuscated slightly to avoid that.
  170. const source = 'src';
  171. if (item.thumbnail_data_uri) {
  172. extraContainerClasses.push('suggestion-with-image');
  173. thumbnail = `<img ${source}="${item.thumbnail_data_uri}">`;
  174. } else {
  175. extraContainerClasses.push('suggestion-with-icon');
  176. const iconClass = getIconForSuggestedItem(item);
  177. thumbnail = `<div><img class="${iconClass}"></div>`;
  178. }
  179. let favicon = '';
  180. if (item.favicon_data_uri) {
  181. favicon = `<img ${source}="${item.favicon_data_uri}">`;
  182. } else {
  183. extraContainerClasses.push('no-favicon');
  184. }
  185. if (!item.attribution_base64) {
  186. extraContainerClasses.push('no-attribution');
  187. }
  188. return `
  189. <div class="offline-content-suggestion ${extraContainerClasses.join(' ')}"
  190. onclick="launchOfflineItem('${item.ID}', '${item.name_space}')">
  191. <div class="offline-content-suggestion-texts">
  192. <div id="offline-content-suggestion-title-${index}"
  193. class="offline-content-suggestion-title">
  194. </div>
  195. <div class="offline-content-suggestion-attribution-freshness">
  196. <div id="offline-content-suggestion-favicon-${index}"
  197. class="offline-content-suggestion-favicon">
  198. ${favicon}
  199. </div>
  200. <div id="offline-content-suggestion-attribution-${index}"
  201. class="offline-content-suggestion-attribution">
  202. </div>
  203. <div class="offline-content-suggestion-freshness">
  204. ${item.date_modified}
  205. </div>
  206. <div class="offline-content-suggestion-pin-spacer"></div>
  207. <div class="offline-content-suggestion-pin"></div>
  208. </div>
  209. </div>
  210. <div class="offline-content-suggestion-thumbnail">
  211. ${thumbnail}
  212. </div>
  213. </div>`;
  214. }
  215. /**
  216. * @typedef {{
  217. * ID: string,
  218. * name_space: string,
  219. * title_base64: string,
  220. * snippet_base64: string,
  221. * date_modified: string,
  222. * attribution_base64: string,
  223. * thumbnail_data_uri: string,
  224. * favicon_data_uri: string,
  225. * content_type: number,
  226. * }}
  227. */
  228. let AvailableOfflineContent;
  229. // Populates a list of suggested offline content.
  230. // Note: For security reasons all content downloaded from the web is considered
  231. // unsafe and must be securely handled to be presented on the dino page. Images
  232. // have already been safely re-encoded but textual content -- like title and
  233. // attribution -- must be properly handled here.
  234. // @param {boolean} isShown
  235. // @param {Array<AvailableOfflineContent>} suggestions
  236. function offlineContentAvailable(isShown, suggestions) {
  237. if (!suggestions || !loadTimeData.valueExists('offlineContentList')) {
  238. return;
  239. }
  240. const suggestionsHTML = [];
  241. for (let index = 0; index < suggestions.length; index++) {
  242. suggestionsHTML.push(getSuggestedContentDiv(suggestions[index], index));
  243. }
  244. document.getElementById('offline-content-suggestions').innerHTML =
  245. suggestionsHTML.join('\n');
  246. // Sets textual web content using |textContent| to make sure it's handled as
  247. // plain text.
  248. for (let index = 0; index < suggestions.length; index++) {
  249. document.getElementById(`offline-content-suggestion-title-${index}`)
  250. .textContent =
  251. decodeUTF16Base64ToString(suggestions[index].title_base64);
  252. document.getElementById(`offline-content-suggestion-attribution-${index}`)
  253. .textContent =
  254. decodeUTF16Base64ToString(suggestions[index].attribution_base64);
  255. }
  256. const contentListElement = document.getElementById('offline-content-list');
  257. if (document.dir === 'rtl') {
  258. contentListElement.classList.add('is-rtl');
  259. }
  260. contentListElement.hidden = false;
  261. // The list is configured as hidden by default. Show it if needed.
  262. if (isShown) {
  263. toggleOfflineContentListVisibility(false);
  264. }
  265. }
  266. function toggleOfflineContentListVisibility(updatePref) {
  267. if (!loadTimeData.valueExists('offlineContentList')) {
  268. return;
  269. }
  270. const contentListElement = document.getElementById('offline-content-list');
  271. const isVisible = !contentListElement.classList.toggle('list-hidden');
  272. if (updatePref && window.errorPageController) {
  273. errorPageController.listVisibilityChanged(isVisible);
  274. }
  275. }
  276. // Called on document load, and from updateForDnsProbe().
  277. function onDocumentLoadOrUpdate() {
  278. const downloadButtonVisible = loadTimeData.valueExists('downloadButton') &&
  279. loadTimeData.getValue('downloadButton').msg;
  280. const detailsButton = document.getElementById('details-button');
  281. // If offline content suggestions will be visible, the usual buttons will not
  282. // be presented.
  283. const offlineContentVisible =
  284. loadTimeData.valueExists('suggestedOfflineContentPresentation');
  285. if (offlineContentVisible) {
  286. document.querySelector('.nav-wrapper').classList.add(HIDDEN_CLASS);
  287. detailsButton.classList.add(HIDDEN_CLASS);
  288. document.getElementById('download-link').hidden = !downloadButtonVisible;
  289. document.getElementById('download-links-wrapper')
  290. .classList.remove(HIDDEN_CLASS);
  291. document.getElementById('error-information-popup-container')
  292. .classList.add('use-popup-container', HIDDEN_CLASS);
  293. document.getElementById('error-information-button')
  294. .classList.remove(HIDDEN_CLASS);
  295. }
  296. const attemptAutoFetch = loadTimeData.valueExists('attemptAutoFetch') &&
  297. loadTimeData.getValue('attemptAutoFetch');
  298. const reloadButtonVisible = loadTimeData.valueExists('reloadButton') &&
  299. loadTimeData.getValue('reloadButton').msg;
  300. const reloadButton = document.getElementById('reload-button');
  301. const downloadButton = document.getElementById('download-button');
  302. if (reloadButton.style.display === 'none' &&
  303. downloadButton.style.display === 'none') {
  304. detailsButton.classList.add('singular');
  305. }
  306. // Show or hide control buttons.
  307. const controlButtonDiv = document.getElementById('control-buttons');
  308. controlButtonDiv.hidden =
  309. offlineContentVisible || !(reloadButtonVisible || downloadButtonVisible);
  310. const iconClass = loadTimeData.valueExists('iconClass') &&
  311. loadTimeData.getValue('iconClass');
  312. updateIconClass(iconClass);
  313. if (!isSubFrame && iconClass === 'icon-offline') {
  314. document.documentElement.classList.add('offline');
  315. new Runner('.interstitial-wrapper');
  316. }
  317. }
  318. function onDocumentLoad() {
  319. // Sets up the proper button layout for the current platform.
  320. const buttonsDiv = document.getElementById('buttons');
  321. if (primaryControlOnLeft) {
  322. buttonsDiv.classList.add('suggested-left');
  323. } else {
  324. buttonsDiv.classList.add('suggested-right');
  325. }
  326. onDocumentLoadOrUpdate();
  327. }
  328. document.addEventListener('DOMContentLoaded', onDocumentLoad);