enhance sandbox security and optimize share hint toast

Signed-off-by: plainheart <yhen@all-my-life.cn>
diff --git a/src/editor/Editor.vue b/src/editor/Editor.vue
index 6797a3f..a14442e 100644
--- a/src/editor/Editor.vue
+++ b/src/editor/Editor.vue
@@ -251,7 +251,6 @@
       loadExampleCode().then((code) => {
         // No editor available. Set to runCode directly.
         store.runCode = parseSourceCode(code);
-        this.showShareHint();
       });
     } else {
       loadExampleCode().then((code) => {
@@ -260,7 +259,6 @@
         if (store.initialCode !== CODE_CHANGED_FLAG) {
           store.initialCode = this.initialCode;
         }
-        this.showShareHint();
       });
 
       window.addEventListener('mousemove', (e) => {
@@ -278,17 +276,6 @@
   },
 
   methods: {
-    showShareHint() {
-      if (store.isSharedCode) {
-        this.$message.closeAll();
-        this.$message({
-          type: 'warning',
-          message: this.$t('editor.share.hint'),
-          duration: 5000,
-          showClose: true
-        });
-      }
-    },
     toExternalEditor(vendor) {
       const previewRef = this.$refs.preview;
       if (!previewRef) {
diff --git a/src/editor/Preview.vue b/src/editor/Preview.vue
index 2dafe44..889933f 100644
--- a/src/editor/Preview.vue
+++ b/src/editor/Preview.vue
@@ -329,9 +329,6 @@
 
   mounted() {
     this.run();
-    if (store.isSharedCode) {
-      this.showShareHint();
-    }
 
     this.fetchVersionList();
   },
@@ -368,6 +365,8 @@
         if (!this.debouncedRun) {
           // First run
           this.run();
+          // show share hint on first run if code is user-shared
+          store.isSharedCode && this.showShareHint();
         } else {
           this.debouncedRun();
         }
@@ -406,7 +405,9 @@
         this.sandbox = null;
       }
     },
-    download,
+    download() {
+      download(store.isSharedCode && this.$t('editor.share.hint'));
+    },
     screenshot() {
       this.sandbox &&
         this.sandbox.screenshot(
@@ -421,7 +422,7 @@
         type: 'warning',
         message: this.$t('editor.share.hint'),
         customClass: 'toast-shared-url',
-        duration: 5000,
+        duration: 8000,
         showClose: true
       });
     },
diff --git a/src/editor/View.vue b/src/editor/View.vue
index 6b43664..e6e96ad 100644
--- a/src/editor/View.vue
+++ b/src/editor/View.vue
@@ -15,24 +15,6 @@
     loadExampleCode().then((code) => {
       store.runCode = parseSourceCode(code);
     });
-
-    this.showShareHint();
-  },
-
-  methods: {
-    showShareHint() {
-      if (store.isSharedCode) {
-        this.$message.closeAll();
-        this.$message({
-          type: 'warning',
-          message: this.$t('editor.share.hint'),
-          duration: 5000,
-          showClose: true
-        });
-      }
-    }
   }
 };
 </script>
-
-<style lang="scss"></style>
diff --git a/src/editor/downloadExample.js b/src/editor/downloadExample.js
index de3dc38..f33d12e 100644
--- a/src/editor/downloadExample.js
+++ b/src/editor/downloadExample.js
@@ -2,7 +2,7 @@
 import { URL_PARAMS, SCRIPT_URLS } from '../common/config';
 import { downloadBlob } from '../common/helper';
 
-export function download() {
+export function download(shareHint) {
   const hasRootPath = store.sourceCode.indexOf('ROOT_PATH') > -1;
   const rootPathCode = hasRootPath ? `var ROOT_PATH = '${store.cdnRoot}';` : '';
   const lang = store.locale && store.locale.indexOf('zh') > -1 ? 'zh-CN' : 'en';
@@ -18,7 +18,7 @@
   );
   const echarts4Dir = SCRIPT_URLS.echartsDir.replace('{{version}}', '4.9.0');
   const code = `<!--
-  THIS EXAMPLE WAS DOWNLOADED FROM ${window.location.href}
+  ${shareHint || `THIS EXAMPLE WAS DOWNLOADED FROM ${window.location.href}`}
 -->
 <!DOCTYPE html>
 <html lang="${lang}" style="height: 100%">
diff --git a/src/editor/sandbox/setup.js b/src/editor/sandbox/setup.js
index 4ed795e..ed889d5 100644
--- a/src/editor/sandbox/setup.js
+++ b/src/editor/sandbox/setup.js
@@ -84,8 +84,51 @@
       {
         location: Object.freeze(JSON.parse(JSON.stringify(location))),
         document: (() => {
-          const nativeCreateElement = document.createElement;
-          const nativeCreateElementNS = document.createElementNS;
+          const disallowedElements = [
+            'script',
+            'video',
+            'audio',
+            'iframe',
+            'frame',
+            'frameset',
+            'embed',
+            'object',
+            // PENDING
+            'foreignobject'
+          ];
+          const disallowedElementsMatcher = new RegExp(
+            `<(${disallowedElements.join('|')}).*>`
+          );
+          const nativeSetters = {
+            innerHTML: Object.getOwnPropertyDescriptor(
+              Element.prototype,
+              'innerHTML'
+            ).set,
+            outerHTML: Object.getOwnPropertyDescriptor(
+              Element.prototype,
+              'outerHTML'
+            ).set,
+            innerText: Object.getOwnPropertyDescriptor(
+              HTMLElement.prototype,
+              'innerText'
+            ).set,
+            outerText: Object.getOwnPropertyDescriptor(
+              HTMLElement.prototype,
+              'outerText'
+            ).set
+          };
+          ['inner', 'outer'].forEach((prop) => {
+            const htmlProp = prop + 'HTML';
+            Object.defineProperty(Element.prototype, htmlProp, {
+              set(value) {
+                return (
+                  disallowedElementsMatcher.test(value)
+                    ? nativeSetters[prop + 'Text']
+                    : nativeSetters[htmlProp]
+                ).call(this, value);
+              }
+            });
+          });
           const fakeDoc = document.cloneNode();
           // To enable the created elements to be inserted to body
           // Object.defineProperties(fakeDoc, {
@@ -100,24 +143,32 @@
           //     }
           //   }
           // });
-          fakeDoc.createElement = function () {
-            const tagName = arguments[0];
-            if (tagName && tagName.toLowerCase() === 'script') {
-              return console.error(
-                `Disallowed attempting to create dynamic script!`
-              );
-            }
-            return nativeCreateElement.apply(document, arguments);
-          };
-          fakeDoc.nativeCreateElementNS = function () {
-            const tagName = arguments[0];
-            if (tagName && tagName.toLowerCase() === 'script') {
-              return console.error(
-                `Disallowed attempting to create dynamic script!`
-              );
-            }
-            return nativeCreateElementNS.apply(document, arguments);
-          };
+          [
+            ['write', document.write, 0, true],
+            ['writeln', document.writeln, 0, true],
+            ['createElement', document.createElement, 0],
+            ['createElementNS', document.createElementNS, 1]
+          ].forEach((api) => {
+            const nativeFn = api[1];
+            const argIndx = api[2];
+            const fullTextSearch = api[3];
+            fakeDoc[api[0]] = function () {
+              let val = arguments[argIndx];
+              val && (val = val.toLowerCase());
+              if (
+                val &&
+                (fullTextSearch
+                  ? ((val = val.match(disallowedElementsMatcher)),
+                    (val = val && val[1]))
+                  : disallowedElements.includes(val))
+              ) {
+                return console.error(
+                  `Disallowed attempting to create ${val} element!`
+                );
+              }
+              return nativeFn.apply(document, arguments);
+            };
+          });
           return fakeDoc;
         })(),
         history: void 0,