index.tsx 7.3 KB

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