index.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. /**
  6. * @packageDocumentation
  7. * @module htmlviewer
  8. */
  9. import { IFrame, ReactWidget, ToolbarButton, ToolbarButtonComponent, UseSignal } from '@jupyterlab/apputils';
  10. import { ActivityMonitor } from '@jupyterlab/coreutils';
  11. import { ABCWidgetFactory, DocumentWidget } from '@jupyterlab/docregistry';
  12. import { nullTranslator } from '@jupyterlab/translation';
  13. import { refreshIcon } from '@jupyterlab/ui-components';
  14. import { Token } from '@lumino/coreutils';
  15. import { Signal } from '@lumino/signaling';
  16. import * as React from 'react';
  17. /**
  18. * The HTML viewer tracker token.
  19. */
  20. export const IHTMLViewerTracker = new Token('@jupyterlab/htmlviewer:IHTMLViewerTracker');
  21. /**
  22. * The timeout to wait for change activity to have ceased before rendering.
  23. */
  24. const RENDER_TIMEOUT = 1000;
  25. /**
  26. * The CSS class to add to the HTMLViewer Widget.
  27. */
  28. const CSS_CLASS = 'jp-HTMLViewer';
  29. /**
  30. * A viewer widget for HTML documents.
  31. *
  32. * #### Notes
  33. * The iframed HTML document can pose a potential security risk,
  34. * since it can execute Javascript, and make same-origin requests
  35. * to the server, thereby executing arbitrary Javascript.
  36. *
  37. * Here, we sandbox the iframe so that it can't execute Javascript
  38. * or launch any popups. We allow one exception: 'allow-same-origin'
  39. * requests, so that local HTML documents can access CSS, images,
  40. * etc from the files system.
  41. */
  42. export class HTMLViewer extends DocumentWidget {
  43. /**
  44. * Create a new widget for rendering HTML.
  45. */
  46. constructor(options) {
  47. super(Object.assign(Object.assign({}, options), { content: new IFrame({ sandbox: ['allow-same-origin'] }) }));
  48. this._renderPending = false;
  49. this._parser = new DOMParser();
  50. this._monitor = null;
  51. this._objectUrl = '';
  52. this._trustedChanged = new Signal(this);
  53. this.translator = options.translator || nullTranslator;
  54. const trans = this.translator.load('jupyterlab');
  55. this.content.addClass(CSS_CLASS);
  56. void this.context.ready.then(() => {
  57. this.update();
  58. // Throttle the rendering rate of the widget.
  59. this._monitor = new ActivityMonitor({
  60. signal: this.context.model.contentChanged,
  61. timeout: RENDER_TIMEOUT
  62. });
  63. this._monitor.activityStopped.connect(this.update, this);
  64. });
  65. // Make a refresh button for the toolbar.
  66. this.toolbar.addItem('refresh', new ToolbarButton({
  67. icon: refreshIcon,
  68. onClick: async () => {
  69. if (!this.context.model.dirty) {
  70. await this.context.revert();
  71. this.update();
  72. }
  73. },
  74. tooltip: trans.__('Rerender HTML Document')
  75. }));
  76. // Make a trust button for the toolbar.
  77. this.toolbar.addItem('trust', ReactWidget.create(React.createElement(Private.TrustButtonComponent, { htmlDocument: this, translator: this.translator })));
  78. }
  79. /**
  80. * Whether the HTML document is trusted. If trusted,
  81. * it can execute Javascript in the iframe sandbox.
  82. */
  83. get trusted() {
  84. return this.content.sandbox.indexOf('allow-scripts') !== -1;
  85. }
  86. set trusted(value) {
  87. if (this.trusted === value) {
  88. return;
  89. }
  90. if (value) {
  91. this.content.sandbox = Private.trusted;
  92. }
  93. else {
  94. this.content.sandbox = Private.untrusted;
  95. }
  96. // eslint-disable-next-line
  97. this.content.url = this.content.url; // Force a refresh.
  98. this._trustedChanged.emit(value);
  99. }
  100. /**
  101. * Emitted when the trust state of the document changes.
  102. */
  103. get trustedChanged() {
  104. return this._trustedChanged;
  105. }
  106. /**
  107. * Dispose of resources held by the html viewer.
  108. */
  109. dispose() {
  110. if (this._objectUrl) {
  111. try {
  112. URL.revokeObjectURL(this._objectUrl);
  113. }
  114. catch (error) {
  115. /* no-op */
  116. }
  117. }
  118. super.dispose();
  119. }
  120. /**
  121. * Handle and update request.
  122. */
  123. onUpdateRequest() {
  124. if (this._renderPending) {
  125. return;
  126. }
  127. this._renderPending = true;
  128. void this._renderModel().then(() => (this._renderPending = false));
  129. }
  130. /**
  131. * Render HTML in IFrame into this widget's node.
  132. */
  133. async _renderModel() {
  134. let data = this.context.model.toString();
  135. data = await this._setBase(data);
  136. // Set the new iframe url.
  137. const blob = new Blob([data], { type: 'text/html' });
  138. const oldUrl = this._objectUrl;
  139. this._objectUrl = URL.createObjectURL(blob);
  140. this.content.url = this._objectUrl;
  141. // Release reference to any previous object url.
  142. if (oldUrl) {
  143. try {
  144. URL.revokeObjectURL(oldUrl);
  145. }
  146. catch (error) {
  147. /* no-op */
  148. }
  149. }
  150. return;
  151. }
  152. /**
  153. * Set a <base> element in the HTML string so that the iframe
  154. * can correctly dereference relative links.
  155. */
  156. async _setBase(data) {
  157. const doc = this._parser.parseFromString(data, 'text/html');
  158. let base = doc.querySelector('base');
  159. if (!base) {
  160. base = doc.createElement('base');
  161. doc.head.insertBefore(base, doc.head.firstChild);
  162. }
  163. const path = this.context.path;
  164. const baseUrl = await this.context.urlResolver.getDownloadUrl(path);
  165. // Set the base href, plus a fake name for the url of this
  166. // document. The fake name doesn't really matter, as long
  167. // as the document can dereference relative links to resources
  168. // (e.g. CSS and scripts).
  169. base.href = baseUrl;
  170. base.target = '_self';
  171. return doc.documentElement.innerHTML;
  172. }
  173. }
  174. /**
  175. * A widget factory for HTMLViewers.
  176. */
  177. export class HTMLViewerFactory extends ABCWidgetFactory {
  178. /**
  179. * Create a new widget given a context.
  180. */
  181. createNewWidget(context) {
  182. return new HTMLViewer({ context });
  183. }
  184. }
  185. /**
  186. * A namespace for private data.
  187. */
  188. var Private;
  189. (function (Private) {
  190. /**
  191. * Sandbox exceptions for untrusted HTML.
  192. */
  193. Private.untrusted = [];
  194. /**
  195. * Sandbox exceptions for trusted HTML.
  196. */
  197. Private.trusted = ['allow-scripts'];
  198. /**
  199. * React component for a trusted button.
  200. *
  201. * This wraps the ToolbarButtonComponent and watches for trust changes.
  202. */
  203. function TrustButtonComponent(props) {
  204. const translator = props.translator || nullTranslator;
  205. const trans = translator.load('jupyterlab');
  206. return (React.createElement(UseSignal, { signal: props.htmlDocument.trustedChanged, initialSender: props.htmlDocument }, session => (React.createElement(ToolbarButtonComponent, { className: "", onClick: () => (props.htmlDocument.trusted = !props.htmlDocument.trusted), tooltip: trans.__(`Whether the HTML file is trusted.
  207. Trusting the file allows scripts to run in it,
  208. which may result in security risks.
  209. Only enable for files you trust.`), label: props.htmlDocument.trusted
  210. ? trans.__('Distrust HTML')
  211. : trans.__('Trust HTML') }))));
  212. }
  213. Private.TrustButtonComponent = TrustButtonComponent;
  214. })(Private || (Private = {}));
  215. //# sourceMappingURL=index.js.map