Browse Source

improve(plugin): run installed plugin in isolated domain

charlie 4 years ago
parent
commit
d8d69c0e51

+ 68 - 44
libs/src/LSPlugin.core.ts

@@ -7,7 +7,11 @@ import {
   setupInjectedUI,
   deferred,
   invokeHostExportedApi,
-  isObject, withFileProtocol, IS_DEV, getSDKPathRoot, PROTOCOL_FILE, URL_LSP
+  isObject, withFileProtocol,
+  getSDKPathRoot,
+  PROTOCOL_FILE, URL_LSP,
+  safetyPathJoin,
+  path, safetyPathNormalize
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -27,9 +31,9 @@ import {
 } from './LSPlugin'
 import { snakeCase } from 'snake-case'
 import DOMPurify from 'dompurify'
-import * as path from 'path'
 
 const debug = Debug('LSPlugin:core')
+const DIR_PLUGINS = 'plugins'
 
 declare global {
   interface Window {
@@ -39,7 +43,7 @@ declare global {
 
 type DeferredActor = ReturnType<typeof deferred>
 type LSPluginCoreOptions = {
-  localUserConfigRoot: string
+  dotConfigRoot: string
 }
 
 /**
@@ -134,7 +138,7 @@ type UserPreferences = {
 type PluginLocalOptions = {
   key?: string // Unique from Logseq Plugin Store
   entry: string // Plugin main file
-  url: string // Plugin package fs location
+  url: string // Plugin package absolute fs location
   name: string
   version: string
   mode: 'shadow' | 'iframe'
@@ -292,9 +296,13 @@ function initApiProxyHandlers (pluginLocal: PluginLocal) {
   })
 }
 
-function provideLSPEntry (fullUrl: string, userPluginRoot: string) {
-  if (userPluginRoot && fullUrl.startsWith(PROTOCOL_FILE + userPluginRoot)) {
-    fullUrl = URL_LSP + fullUrl.substr(PROTOCOL_FILE.length + userPluginRoot.length)
+function convertToLSPResource (fullUrl: string, dotPluginRoot: string) {
+  if (
+    dotPluginRoot &&
+    fullUrl.startsWith(PROTOCOL_FILE + dotPluginRoot)
+  ) {
+    fullUrl = safetyPathJoin(
+      URL_LSP, fullUrl.substr(PROTOCOL_FILE.length + dotPluginRoot.length))
   }
   return fullUrl
 }
@@ -324,7 +332,7 @@ class PluginLocal
   private _status: PluginLocalLoadStatus = PluginLocalLoadStatus.UNLOADED
   private _loadErr?: Error
   private _localRoot?: string
-  private _userSettingsFile?: string
+  private _dotSettingsFile?: string
   private _caller?: LSPluginCaller
 
   /**
@@ -354,7 +362,7 @@ class PluginLocal
 
     try {
       const [userSettingsFilePath, userSettings] = await invokeHostExportedApi('load_plugin_user_settings', key)
-      this._userSettingsFile = userSettingsFilePath
+      this._dotSettingsFile = userSettingsFilePath
 
       const settings = _options.settings = new PluginSettings(userSettings)
 
@@ -390,6 +398,17 @@ class PluginLocal
     return this.caller?._getSandboxIframeContainer()
   }
 
+  _resolveResourceFullUrl (filePath: string, localRoot?: string) {
+    localRoot = localRoot || this._localRoot
+    const reg = /^(http|file)/
+    if (!reg.test(filePath)) {
+      const url = path.join(localRoot, filePath)
+      filePath = reg.test(url) ? url : (PROTOCOL_FILE + url)
+    }
+    return this.isInstalledInDotRoot ?
+      convertToLSPResource(filePath, this.dotPluginsRoot) : filePath
+  }
+
   async _preparePackageConfigs () {
     const { url } = this._options
     let pkg: any
@@ -415,22 +434,13 @@ class PluginLocal
       this._options[k] = pkg[k]
     })
 
-    // TODO: How with local protocol
-    const localRoot = this._localRoot = url
+    const localRoot = this._localRoot = safetyPathNormalize(url)
     const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
-    const makeFullUrl = (loc: string) => {
-      const reg = /^(http|file)/
-      if (!reg.test(loc)) {
-        const url = path.join(localRoot, loc)
-        loc = reg.test(url) ? url : (PROTOCOL_FILE + url)
-      }
-      return provideLSPEntry(loc, this.userPluginRoot)
-    }
     const validateMain = (main) => main && /\.(js|html)$/.test(main)
 
     // Entry from main
-    if (validateMain(pkg.main)) {
-      this._options.entry = makeFullUrl(pkg.main)
+    if (validateMain(pkg.main)) { // Theme has no main
+      this._options.entry = this._resolveResourceFullUrl(pkg.main, localRoot)
 
       if (logseq.mode) {
         this._options.mode = logseq.mode
@@ -440,7 +450,7 @@ class PluginLocal
     const icon = logseq.icon || pkg.icon
 
     if (icon) {
-      this._options.icon = makeFullUrl(icon)
+      this._options.icon = this._resolveResourceFullUrl(icon)
     }
 
     // TODO: strategy for Logseq plugins center
@@ -488,9 +498,16 @@ class PluginLocal
 
     if (!entry.endsWith('.js')) return
 
+    let dirPathInstalled = null
+    let tmp_file_method = 'write_user_tmp_file'
+    if (this.isInstalledInDotRoot) {
+      tmp_file_method = 'write_dotdir_file'
+      dirPathInstalled = this._localRoot.replace(this.dotPluginsRoot, '')
+      dirPathInstalled = path.join(DIR_PLUGINS, dirPathInstalled)
+    }
     let sdkPathRoot = await getSDKPathRoot()
     let entryPath = await invokeHostExportedApi(
-      'write_user_tmp_file',
+      tmp_file_method,
       `${this._id}_index.html`,
       `<!doctype html>
 <html lang="en">
@@ -503,12 +520,14 @@ class PluginLocal
   <div id="app"></div>
   <script src="${entry}"></script>
   </body>
-</html>`)
+</html>`, dirPathInstalled)
 
-    this._options.entry = provideLSPEntry(
-      withFileProtocol(entryPath),
-      this.userPluginRoot
+    entry = convertToLSPResource(
+      withFileProtocol(path.normalize(entryPath)),
+      this.dotPluginsRoot
     )
+
+    this._options.entry = entry
   }
 
   async _loadConfigThemes (themes: Array<ThemeOptions>) {
@@ -537,11 +556,10 @@ class PluginLocal
     this._loadErr = undefined
 
     try {
-      let installPackageThemes: () => Promise<void> = () => Promise.resolve()
+      // if (!this.options.entry) { // Themes package no entry field
+      // }
 
-      if (!this.options.entry) { // Themes package no entry field
-        installPackageThemes = await this._preparePackageConfigs()
-      }
+      let installPackageThemes = await this._preparePackageConfigs()
 
       if (!this.settings) {
         await this._setupUserSettings()
@@ -605,7 +623,7 @@ class PluginLocal
     if (unregister) {
       await this.unload()
 
-      if (this.isInstalledInUserRoot) {
+      if (this.isInstalledInDotRoot) {
         debug('TODO: remove plugin local files from user home root :)')
       }
 
@@ -663,10 +681,10 @@ class PluginLocal
     }
   }
 
-  get isInstalledInUserRoot () {
-    const userRoot = this._ctx.options.localUserConfigRoot
-    const plugRoot = this._localRoot
-    return userRoot && plugRoot && plugRoot.startsWith(userRoot)
+  get isInstalledInDotRoot () {
+    const dotRoot = this.dotConfigRoot
+    const plgRoot = this.localRoot
+    return dotRoot && plgRoot && plgRoot.startsWith(dotRoot)
   }
 
   get loaded () {
@@ -727,19 +745,25 @@ class PluginLocal
     return this._loadErr
   }
 
-  get userSettingsFile (): string | undefined {
-    return this._userSettingsFile
+  get dotConfigRoot () {
+    return path.normalize(this._ctx.options.dotConfigRoot)
+  }
+
+  get dotSettingsFile (): string | undefined {
+    return this._dotSettingsFile
   }
 
-  get userPluginRoot () {
-    return this._ctx.options.localUserConfigRoot + '/plugins/'
+  get dotPluginsRoot () {
+    return path.join(this.dotConfigRoot, DIR_PLUGINS)
   }
 
   toJSON () {
     const json = { ...this.options } as any
     json.id = this.id
     json.err = this.loadErr
-    json.usf = this.userSettingsFile
+    json.usf = this.dotSettingsFile
+    json.iir = this.isInstalledInDotRoot
+    json.lsr = this._resolveResourceFullUrl('')
     return json
   }
 }
@@ -837,7 +861,7 @@ class LSPluginCore
     try {
       this._isRegistering = true
 
-      const userConfigRoot = this._options.localUserConfigRoot
+      const userConfigRoot = this._options.dotConfigRoot
       const readyIndicator = this._readyIndicator = deferred()
 
       await this.loadUserPreferences()
@@ -885,7 +909,7 @@ class LSPluginCore
         this.emit('registered', pluginLocal)
 
         // external plugins
-        if (!pluginLocal.isInstalledInUserRoot) {
+        if (!pluginLocal.isInstalledInDotRoot) {
           externals.add(url)
         }
       }
@@ -925,7 +949,7 @@ class LSPluginCore
     for (const identity of plugins) {
       const p = this.ensurePlugin(identity)
 
-      if (!p.isInstalledInUserRoot) {
+      if (!p.isInstalledInDotRoot) {
         unregisteredExternals.push(p.options.url)
       }
 

+ 2 - 0
libs/src/LSPlugin.ts

@@ -528,6 +528,8 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
 
   isMainUIVisible: boolean
 
+  resolveResourceFullUrl (filePath: string): string
+
   App: IAppProxy & Record<string, any>
   Editor: IEditorProxy & Record<string, any>
   DB: IDBProxy

+ 14 - 1
libs/src/LSPlugin.user.ts

@@ -1,4 +1,4 @@
-import { deepMerge } from './helpers'
+import { deepMerge, safetyPathJoin } from './helpers'
 import { LSPluginCaller } from './LSPlugin.caller'
 import {
   IAppProxy, IDBProxy,
@@ -271,6 +271,12 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
     }
   }
 
+  ensureConnected () {
+    if (!this._connected) {
+      throw new Error('not connected')
+    }
+  }
+
   beforeunload (callback: (e: any) => Promise<void>): void {
     if (typeof callback !== 'function') return
     this._beforeunloadCallback = callback
@@ -354,6 +360,13 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
     return this._caller
   }
 
+  resolveResourceFullUrl (filePath: string) {
+    this.ensureConnected()
+    if (!filePath) return
+    filePath = filePath.replace(/^[.\\/]+/, '')
+    return safetyPathJoin(this._baseInfo.lsr, filePath)
+  }
+
   /**
    * @internal
    */

+ 10 - 2
libs/src/helpers.ts

@@ -1,7 +1,7 @@
 import { StyleString, UIOptions } from './LSPlugin'
 import { PluginLocal } from './LSPlugin.core'
 import { snakeCase } from 'snake-case'
-import * as path from 'path'
+import * as nodePath from 'path'
 
 interface IObject {
   [key: string]: any;
@@ -14,6 +14,7 @@ declare global {
   }
 }
 
+export const path = navigator.platform.toLowerCase() === 'win32' ? nodePath.win32 : nodePath.posix
 export const IS_DEV = process.env.NODE_ENV === 'development'
 export const PROTOCOL_FILE = 'file://'
 export const PROTOCOL_LSP = 'lsp://'
@@ -104,7 +105,7 @@ export function withFileProtocol (path: string) {
   const reg = /^(http|file|lsp)/
 
   if (!reg.test(path)) {
-    path = 'file://' + path
+    path = PROTOCOL_FILE + path
   }
 
   return path
@@ -121,6 +122,13 @@ export function safetyPathJoin (basePath: string, ...parts: Array<string>) {
   }
 }
 
+export function safetyPathNormalize (basePath: string) {
+  if (!basePath?.match(/^(http?|lsp|assets):/)) {
+    basePath = path.normalize(basePath)
+  }
+  return basePath
+}
+
 /**
  * @param timeout milliseconds
  * @param tag string

+ 1 - 1
src/main/frontend/handler/plugin.cljs

@@ -180,7 +180,7 @@
 
   (p/then
    (p/let [root (get-ls-dotdir-root)
-           _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root}))
+           _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
            _ (doto js/LSPluginCore
                (.on "registered"
                     (fn [^js pl]

+ 3 - 3
src/main/logseq/api.cljs

@@ -95,7 +95,7 @@
           path (util/node-path.join path "package.json")]
       (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-mtime? true}))))
 
-(defn ^:private write_dotdir_file!
+(defn ^:private write_dotdir_file
   [file content sub-root]
   (p/let [repo ""
           path (plugin-handler/get-ls-dotdir-root)
@@ -138,11 +138,11 @@
 
 (def ^:export write_user_tmp_file
   (fn [file content]
-    (write_dotdir_file! file content "tmp")))
+    (write_dotdir_file file content "tmp")))
 
 (def ^:export write_plugin_storage_file
   (fn [plugin-id file content]
-    (write_dotdir_file!
+    (write_dotdir_file
       file content
       (let [plugin-id (util/node-path.basename plugin-id)]
         (util/node-path.join "storages" plugin-id)))))