Eric Charles 5 лет назад
Родитель
Сommit
913985a116

+ 2 - 2
examples/listings/Makefile

@@ -1,6 +1,6 @@
 print-listings:
-	@exec echo Local Blacklist URI: http://localhost:8888/listings/blacklist.json
-	@exec echo Local Whitelist URI: http://localhost:8888/listings/whitelist.json
+#	@exec echo Local Blacklist URI: http://localhost:8888/listings/blacklist.json
+#	@exec echo Local Whitelist URI: http://localhost:8888/listings/whitelist.json
 
 #	  --LabServerApp.blacklist_uris=http://localhost:8888/listings/blacklist.json \
 #	  --LabServerApp.whitelist_uris=http://localhost:8888/listings/whitelist.json

+ 4 - 0
examples/listings/settings/@jupyterlab/extensionmanager-extension/plugin.jupyterlab-settings

@@ -4,6 +4,10 @@
     // Extension manager settings.
     // *********************************************
 
+    // Disclaimer Status
+    // Whether the user understand that extensions managed through this interface run arbitrary code that may be dangerous
+    "disclaimed": false,
+
     // Enabled Status
     // Enables extension manager (requires Node.js/npm).
     // WARNING: installing untrusted extensions may be unsafe.

+ 6 - 0
packages/extensionmanager-extension/schema/plugin.json

@@ -9,6 +9,12 @@
       "description": "Enables extension manager (requires Node.js/npm).\nWARNING: installing untrusted extensions may be unsafe.",
       "default": false,
       "type": "boolean"
+    },
+    "disclaimed": {
+      "title": "Disclaimed Status",
+      "description": "Whether the user understand that extensions managed through this interface run arbitrary code that may be dangerous",
+      "default": false,
+      "type": "boolean"
     }
   },
   "additionalProperties": false,

+ 26 - 17
packages/extensionmanager-extension/src/index.ts

@@ -13,6 +13,8 @@ import { IMainMenu } from '@jupyterlab/mainmenu';
 import { ISettingRegistry } from '@jupyterlab/settingregistry';
 import { extensionIcon } from '@jupyterlab/ui-components';
 
+const PLUGIN_ID = '@jupyterlab/extensionmanager-extension:plugin';
+
 /**
  * IDs of the commands added by this extension.
  */
@@ -24,7 +26,7 @@ namespace CommandIDs {
  * The extension manager plugin.
  */
 const plugin: JupyterFrontEndPlugin<void> = {
-  id: '@jupyterlab/extensionmanager-extension:plugin',
+  id: PLUGIN_ID,
   autoStart: true,
   requires: [ISettingRegistry],
   optional: [ILabShell, ILayoutRestorer, IMainMenu, ICommandPalette],
@@ -38,12 +40,13 @@ const plugin: JupyterFrontEndPlugin<void> = {
   ) => {
     const settings = await registry.load(plugin.id);
     let enabled = settings.composite['enabled'] === true;
+    let disclaimed = settings.composite['disclaimed'] === true;
 
     const { commands, serviceManager, shell } = app;
     let view: ExtensionView | undefined;
 
     const createView = () => {
-      const v = new ExtensionView(serviceManager);
+      const v = new ExtensionView(serviceManager, settings);
       v.id = 'extensionmanager.main-view';
       v.title.icon = extensionIcon;
       v.title.caption = 'Extension Manager';
@@ -60,23 +63,29 @@ const plugin: JupyterFrontEndPlugin<void> = {
 
     // If the extension is enabled or disabled,
     // add or remove it from the left area.
-    void app.restored.then(() => {
-      settings.changed.connect(async () => {
-        enabled = settings.composite['enabled'] === true;
-        if (enabled && (!view || (view && !view.isAttached))) {
-          const accepted = await Private.showWarning();
-          if (!accepted) {
-            void settings.set('enabled', false);
-            return;
+    Promise.all([app.restored, registry.load(PLUGIN_ID)])
+      .then(([, settings]) => {
+        settings.changed.connect(async () => {
+          enabled = settings.composite['enabled'] === true;
+          if (enabled && (!view || (view && !view.isAttached))) {
+            const accepted = await Private.showWarning();
+            if (!accepted) {
+              void settings.set('enabled', false);
+              return;
+            }
+            view = view || createView();
+            shell.add(view, 'left');
+          } else if (!enabled && view && view.isAttached) {
+            app.commands.notifyCommandChanged(CommandIDs.toggle);
+            view.close();
           }
-          view = view || createView();
-          shell.add(view, 'left');
-        } else if (!enabled && view && view.isAttached) {
-          app.commands.notifyCommandChanged(CommandIDs.toggle);
-          view.close();
-        }
+        });
+      })
+      .catch(reason => {
+        console.error(
+          `Something went wrong when reading the settings.\n${reason}`
+        );
       });
-    });
 
     commands.addCommand(CommandIDs.toggle, {
       label: 'Enable Extension Manager (experimental)',

+ 34 - 31
packages/extensionmanager/src/listings.ts

@@ -12,6 +12,16 @@ import { ServerConnection } from '@jupyterlab/services';
  *
  */
 export interface IListResult {
+  /**
+   * A collection of URIs for black listings.
+   */
+  blacklistUris: string[];
+
+  /**
+   * A collection of URIs for white listings.
+   */
+  whitelistUris: string[];
+
   /**
    * A collection of back listed extensions.
    */
@@ -21,25 +31,17 @@ export interface IListResult {
    * A collection of white listed extensions.
    */
   whitelist: string[];
-
-  /**
-   * Timestamp of the search result creation.
-   */
-  time: string;
 }
 
 export interface IListingApi {
-  listings: {
-    blacklist_uri: string;
-    whitelist_uri: string;
-    builtin_blacklist: string[];
-    builtin_whitelist: string[];
-  };
+  blacklist_uris: string[];
+  whitelist_uris: string[];
+  blacklist: string[];
+  whitelist: string[];
 }
 
 /**
- * An object for searching an List registry.
- *
+ * An object for getting listings from URIs.
  */
 export class Lister {
   /**
@@ -50,17 +52,21 @@ export class Lister {
       '@jupyterlab/extensionmanager-extension/listings.json'
     )
       .then(data => {
-        this.blackListUri = data.listings.blacklist_uri;
-        this.whiteListUri = data.listings.whitelist_uri;
-        this._listingUrisLoaded.emit(void 0);
+        this._listings = {
+          blacklistUris: data.blacklist_uris,
+          whitelistUris: data.whitelist_uris,
+          blacklist: data.blacklist,
+          whitelist: data.whitelist
+        };
+        this._listingsLoaded.emit(this._listings);
       })
       .catch(error => {
         console.error(error);
       });
   }
 
-  get listingUrisLoaded(): ISignal<Lister, void> {
-    return this._listingUrisLoaded;
+  get listingsLoaded(): ISignal<this, IListResult> {
+    return this._listingsLoaded;
   }
 
   /**
@@ -68,9 +74,8 @@ export class Lister {
    *
    * @param page The page of results to fetch.
    * @param pageination The pagination size to use. See registry API documentation for acceptable values.
-   */
   getBlackList(): Promise<IListResult> {
-    const uri = new URL(this.blackListUri);
+    const uri = new URL(this.blackListUri[0]);
     return fetch(uri.toString()).then((response: Response) => {
       if (response.ok) {
         return response.json();
@@ -78,15 +83,15 @@ export class Lister {
       return [];
     });
   }
+   */
 
   /**
    * Get the white list.
    *
    * @param page The page of results to fetch.
    * @param pageination The pagination size to use. See registry API documentation for acceptable values.
-   */
   getWhiteList(): Promise<IListResult> {
-    const uri = new URL(this.whiteListUri);
+    const uri = new URL(this.whiteListUris[0]);
     return fetch(uri.toString()).then((response: Response) => {
       if (response.ok) {
         return response.json();
@@ -94,20 +99,18 @@ export class Lister {
       return [];
     });
   }
-
-  /**
-   * The URI of the black listing registry to use.
    */
-  private blackListUri: string;
 
-  /**
-   * The URI of the white listing registry to use.
-   */
-  private whiteListUri: string;
+  private _listings: IListResult = {
+    blacklistUris: [],
+    whitelistUris: [],
+    blacklist: [],
+    whitelist: []
+  };
 
   /**
    */
-  private _listingUrisLoaded = new Signal<Lister, void>(this);
+  private _listingsLoaded = new Signal<this, IListResult>(this);
 }
 
 /**

+ 67 - 53
packages/extensionmanager/src/model.ts

@@ -8,6 +8,7 @@ import {
   ServerConnection,
   ServiceManager
 } from '@jupyterlab/services';
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
 
 import { Debouncer } from '@lumino/polling';
 
@@ -160,29 +161,51 @@ export type Action = 'install' | 'uninstall' | 'enable' | 'disable';
  * Model for an extension list.
  */
 export class ListModel extends VDomModel {
-  constructor(serviceManager: ServiceManager) {
+  constructor(
+    serviceManager: ServiceManager,
+    settings: ISettingRegistry.ISettings
+  ) {
     super();
     this._installed = [];
     this._searchResult = [];
     this.serviceManager = serviceManager;
     this.serverConnectionSettings = ServerConnection.makeSettings();
     this._debouncedUpdate = new Debouncer(this.update.bind(this), 1000);
-    this.lister.listingUrisLoaded.connect(() => {
-      this.performGetBlacklist()
-        .then(data => {
-          this._blacklistingMap = data;
-        })
-        .catch(error => {
-          console.error(error);
-        });
-      this.performGetWhitelist()
-        .then(data => {
-          this._whitelistingMap = data;
-        })
-        .catch(error => {
-          console.error(error);
-        });
+    this.lister.listingsLoaded.connect(this._listingIsLoaded, this);
+    _isDisclaimed = settings.composite['disclaimed'] === true;
+    settings.changed.connect(() => {
+      const disclaimed = settings.composite['disclaimed'] === true;
+      _isDisclaimed = disclaimed;
+      void this.update();
+    });
+  }
+
+  _listingIsLoaded(_: Lister, listings: IListResult) {
+    this._blacklistMap = new Map<string, IEntry>();
+    listings.blacklist.map(e => {
+      this._blacklistMap.set(e, { name: e });
+    });
+    this._whitelistMap = new Map<string, IEntry>();
+    listings.whitelist.map(e => {
+      this._whitelistMap.set(e, { name: e });
     });
+    void this.initialize();
+    /*
+    this.performGetBlacklist()
+      .then(data => {
+        this._blacklistingMap = data;
+      })
+      .catch(error => {
+        console.error(error);
+      });
+    this.performGetWhitelist()
+      .then(data => {
+        this._whitelistingMap = data;
+      })
+      .catch(error => {
+        console.error(error);
+      });
+    */
   }
 
   /**
@@ -431,9 +454,7 @@ export class ListModel extends VDomModel {
    * @param res Promise to an npm query result.
    */
   protected async translateSearchResult(
-    res: Promise<ISearchResult>,
-    blacklistMap: Map<string, IListEntry>,
-    whitelistMap: Map<string, IListEntry>
+    res: Promise<ISearchResult>
   ): Promise<{ [key: string]: IEntry }> {
     let entries: { [key: string]: IEntry } = {};
     for (let obj of (await res).objects) {
@@ -455,8 +476,8 @@ export class ListModel extends VDomModel {
         status: null,
         latest_version: pkg.version,
         installed_version: '',
-        isBlacklisted: blacklistMap.has(pkg.name),
-        isWhitelisted: whitelistMap.has(pkg.name)
+        isBlacklisted: this._blacklistMap.has(pkg.name),
+        isWhitelisted: this._whitelistMap.has(pkg.name)
       };
     }
     return entries;
@@ -488,9 +509,7 @@ export class ListModel extends VDomModel {
    * @param res Promise to the server reply data.
    */
   protected async translateInstalled(
-    res: Promise<IInstalledEntry[]>,
-    blacklistMap: Map<string, IListEntry>,
-    whitelistMap: Map<string, IListEntry>
+    res: Promise<IInstalledEntry[]>
   ): Promise<{ [key: string]: IEntry }> {
     const promises = [];
     const entries: { [key: string]: IEntry } = {};
@@ -506,8 +525,8 @@ export class ListModel extends VDomModel {
             status: pkg.status,
             latest_version: pkg.latest_version,
             installed_version: pkg.installed_version,
-            isBlacklisted: blacklistMap.has(pkg.name),
-            isWhitelisted: whitelistMap.has(pkg.name)
+            isBlacklisted: this._blacklistMap.has(pkg.name),
+            isWhitelisted: this._whitelistMap.has(pkg.name)
           };
         })
       );
@@ -556,10 +575,7 @@ export class ListModel extends VDomModel {
    *
    * @returns {Promise<{ [key: string]: IEntry; }>} The search result as a map of entries.
    */
-  protected async performSearch(
-    blacklistingMap: Map<string, IListEntry>,
-    whitelistingMap: Map<string, IListEntry>
-  ): Promise<{ [key: string]: IEntry }> {
+  protected async performSearch(): Promise<{ [key: string]: IEntry }> {
     if (this.query === null) {
       this._searchResult = [];
       this._totalEntries = 0;
@@ -573,11 +589,7 @@ export class ListModel extends VDomModel {
       this.page,
       this.pagination
     );
-    let searchMapPromise = this.translateSearchResult(
-      search,
-      blacklistingMap,
-      whitelistingMap
-    );
+    let searchMapPromise = this.translateSearchResult(search);
 
     let searchMap: { [key: string]: IEntry };
     try {
@@ -590,7 +602,7 @@ export class ListModel extends VDomModel {
 
     return searchMap;
   }
-
+  /*
   protected async performGetBlacklist(): Promise<Map<string, IListEntry>> {
     // Start the fetch without waiting for it:
     let blacklist = this.lister.getBlackList();
@@ -607,7 +619,9 @@ export class ListModel extends VDomModel {
 
     return blacklistMap;
   }
+  */
 
+  /*
   protected async performGetWhitelist(): Promise<Map<string, IListEntry>> {
     // Start the fetch without waiting for it:
     let whitelist = this.lister.getWhiteList();
@@ -624,6 +638,7 @@ export class ListModel extends VDomModel {
 
     return whitelisttMap;
   }
+  */
 
   /**
    * Query the installed extensions.
@@ -633,16 +648,12 @@ export class ListModel extends VDomModel {
    * @returns {Promise<{ [key: string]: IEntry; }>} A map of installed extensions.
    */
   protected async queryInstalled(
-    refreshInstalled: boolean,
-    blacklistMap: Map<string, IListEntry>,
-    whitelisttMap: Map<string, IListEntry>
+    refreshInstalled: boolean
   ): Promise<{ [key: string]: IEntry }> {
     let installedMap;
     try {
       installedMap = await this.translateInstalled(
-        this.fetchInstalled(refreshInstalled),
-        blacklistMap,
-        whitelisttMap
+        this.fetchInstalled(refreshInstalled)
       );
       this.installedError = null;
     } catch (reason) {
@@ -662,15 +673,8 @@ export class ListModel extends VDomModel {
   protected async update(refreshInstalled = false) {
     // Start both queries before awaiting:
 
-    const searchMapPromise = this.performSearch(
-      this._blacklistingMap,
-      this._whitelistingMap
-    );
-    const installedMapPromise = this.queryInstalled(
-      refreshInstalled,
-      this._blacklistingMap,
-      this._whitelistingMap
-    );
+    const searchMapPromise = this.performSearch();
+    const installedMapPromise = this.queryInstalled(refreshInstalled);
 
     // Await results:
     const searchMap = await searchMapPromise;
@@ -822,10 +826,12 @@ export class ListModel extends VDomModel {
   private _searchResult: IEntry[];
   private _pendingActions: Promise<any>[] = [];
   private _debouncedUpdate: Debouncer<void, void>;
-  private _blacklistingMap: Map<string, IListEntry>;
-  private _whitelistingMap: Map<string, IListEntry>;
+  private _blacklistMap: Map<string, IListEntry>;
+  private _whitelistMap: Map<string, IListEntry>;
 }
 
+let _isDisclaimed = false;
+
 /**
  * ListModel statics.
  */
@@ -841,6 +847,14 @@ export namespace ListModel {
     }
     return semver.lt(entry.installed_version, entry.latest_version);
   }
+
+  export function isDisclaimed() {
+    return _isDisclaimed;
+  }
+
+  export function toogleDisclaimed() {
+    _isDisclaimed = !_isDisclaimed;
+  }
 }
 
 /**

+ 57 - 23
packages/extensionmanager/src/widget.tsx

@@ -3,6 +3,7 @@
 
 import { VDomRenderer, ToolbarButtonComponent } from '@jupyterlab/apputils';
 import { ServiceManager } from '@jupyterlab/services';
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
 import {
   Button,
   caretDownIcon,
@@ -64,9 +65,22 @@ export class SearchBar extends React.Component<
           onChange={this.handleChange}
           value={this.state.value}
           rightIcon="search"
+          disabled={this.props.disabled}
         />
         <br />
-        <Checkbox label="I understand that extensions managed through this interface run arbitrary code that may be dangerous." />
+        <Checkbox
+          label="I understand that extensions managed through this interface run arbitrary code that may be dangerous."
+          checked={ListModel.isDisclaimed()}
+          onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+            this.props.settings
+              .set('disclaimed', e.target.checked)
+              .catch(reason => {
+                console.error(
+                  `Something went wrong when setting disclaimed.\n${reason}`
+                );
+              });
+          }}
+        />
       </div>
     );
   }
@@ -94,6 +108,10 @@ export namespace SearchBar {
      * The placeholder string to use in the search bar input field when empty.
      */
     placeholder: string;
+
+    disabled: boolean;
+
+    settings: ISettingRegistry.ISettings;
   }
 
   /**
@@ -202,24 +220,28 @@ function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
           {entry.description}
         </div>
         <div className="jp-extensionmanager-entry-buttons">
-          {!entry.installed && (
-            <Button
-              onClick={() => props.performAction('install', entry)}
-              minimal
-              small
-            >
-              Install
-            </Button>
-          )}
-          {ListModel.entryHasUpdate(entry) && (
-            <Button
-              onClick={() => props.performAction('install', entry)}
-              minimal
-              small
-            >
-              Update
-            </Button>
-          )}
+          {!entry.installed &&
+            !entry.isBlacklisted &&
+            ListModel.isDisclaimed() && (
+              <Button
+                onClick={() => props.performAction('install', entry)}
+                minimal
+                small
+              >
+                Install
+              </Button>
+            )}
+          {ListModel.entryHasUpdate(entry) &&
+            !entry.isBlacklisted &&
+            ListModel.isDisclaimed() && (
+              <Button
+                onClick={() => props.performAction('install', entry)}
+                minimal
+                small
+              >
+                Update
+              </Button>
+            )}
           {entry.installed && (
             <Button
               onClick={() => props.performAction('uninstall', entry)}
@@ -468,8 +490,13 @@ export namespace CollapsibleSection {
  * The main view for the discovery extension.
  */
 export class ExtensionView extends VDomRenderer<ListModel> {
-  constructor(serviceManager: ServiceManager) {
-    super(new ListModel(serviceManager));
+  private _settings: ISettingRegistry.ISettings;
+  constructor(
+    serviceManager: ServiceManager,
+    settings: ISettingRegistry.ISettings
+  ) {
+    super(new ListModel(serviceManager, settings));
+    this._settings = settings;
     this.addClass('jp-extensionmanager-view');
   }
 
@@ -488,7 +515,14 @@ export class ExtensionView extends VDomRenderer<ListModel> {
   protected render(): React.ReactElement<any>[] {
     const model = this.model!;
     let pages = Math.ceil(model.totalEntries / model.pagination);
-    let elements = [<SearchBar key="searchbar" placeholder="SEARCH" />];
+    let elements = [
+      <SearchBar
+        key="searchbar"
+        placeholder="SEARCH"
+        disabled={!ListModel.isDisclaimed()}
+        settings={this._settings}
+      />
+    ];
     if (model.promptBuild) {
       elements.push(
         <BuildPrompt
@@ -513,7 +547,7 @@ export class ExtensionView extends VDomRenderer<ListModel> {
     );
     const content = [];
     if (!model.initialized) {
-      void model.initialize();
+      //      void model.initialize();
       content.push(
         <div key="loading-placeholder" className="jp-extensionmanager-loader">
           Updating extensions list