forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			511 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			511 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(this, {
 | |
|   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| var { ExtensionError } = ExtensionUtils;
 | |
| 
 | |
| const { TYPE_BOOKMARK, TYPE_FOLDER, TYPE_SEPARATOR } = PlacesUtils.bookmarks;
 | |
| 
 | |
| const BOOKMARKS_TYPES_TO_API_TYPES_MAP = new Map([
 | |
|   [TYPE_BOOKMARK, "bookmark"],
 | |
|   [TYPE_FOLDER, "folder"],
 | |
|   [TYPE_SEPARATOR, "separator"],
 | |
| ]);
 | |
| 
 | |
| const BOOKMARK_SEPERATOR_URL = "data:";
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(this, "API_TYPES_TO_BOOKMARKS_TYPES_MAP", () => {
 | |
|   let theMap = new Map();
 | |
| 
 | |
|   for (let [code, name] of BOOKMARKS_TYPES_TO_API_TYPES_MAP) {
 | |
|     theMap.set(name, code);
 | |
|   }
 | |
|   return theMap;
 | |
| });
 | |
| 
 | |
| let listenerCount = 0;
 | |
| 
 | |
| function getUrl(type, url) {
 | |
|   switch (type) {
 | |
|     case TYPE_BOOKMARK:
 | |
|       return url;
 | |
|     case TYPE_SEPARATOR:
 | |
|       return BOOKMARK_SEPERATOR_URL;
 | |
|     default:
 | |
|       return undefined;
 | |
|   }
 | |
| }
 | |
| 
 | |
| const getTree = (rootGuid, onlyChildren) => {
 | |
|   function convert(node, parent) {
 | |
|     let treenode = {
 | |
|       id: node.guid,
 | |
|       title: PlacesUtils.bookmarks.getLocalizedTitle(node) || "",
 | |
|       index: node.index,
 | |
|       dateAdded: node.dateAdded / 1000,
 | |
|       type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(node.typeCode),
 | |
|       url: getUrl(node.typeCode, node.uri),
 | |
|     };
 | |
| 
 | |
|     if (parent && node.guid != PlacesUtils.bookmarks.rootGuid) {
 | |
|       treenode.parentId = parent.guid;
 | |
|     }
 | |
| 
 | |
|     if (node.typeCode == TYPE_FOLDER) {
 | |
|       treenode.dateGroupModified = node.lastModified / 1000;
 | |
| 
 | |
|       if (!onlyChildren) {
 | |
|         treenode.children = node.children
 | |
|           ? node.children.map(child => convert(child, node))
 | |
|           : [];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return treenode;
 | |
|   }
 | |
| 
 | |
|   return PlacesUtils.promiseBookmarksTree(rootGuid)
 | |
|     .then(root => {
 | |
|       if (onlyChildren) {
 | |
|         let children = root.children || [];
 | |
|         return children.map(child => convert(child, root));
 | |
|       }
 | |
|       let treenode = convert(root, null);
 | |
|       treenode.parentId = root.parentGuid;
 | |
|       // It seems like the array always just contains the root node.
 | |
|       return [treenode];
 | |
|     })
 | |
|     .catch(e => Promise.reject({ message: e.message }));
 | |
| };
 | |
| 
 | |
| const convertBookmarks = result => {
 | |
|   let node = {
 | |
|     id: result.guid,
 | |
|     title: PlacesUtils.bookmarks.getLocalizedTitle(result) || "",
 | |
|     index: result.index,
 | |
|     dateAdded: result.dateAdded.getTime(),
 | |
|     type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(result.type),
 | |
|     url: getUrl(result.type, result.url && result.url.href),
 | |
|   };
 | |
| 
 | |
|   if (result.guid != PlacesUtils.bookmarks.rootGuid) {
 | |
|     node.parentId = result.parentGuid;
 | |
|   }
 | |
| 
 | |
|   if (result.type == TYPE_FOLDER) {
 | |
|     node.dateGroupModified = result.lastModified.getTime();
 | |
|   }
 | |
| 
 | |
|   return node;
 | |
| };
 | |
| 
 | |
| const throwIfRootId = id => {
 | |
|   if (id == PlacesUtils.bookmarks.rootGuid) {
 | |
|     throw new ExtensionError("The bookmark root cannot be modified");
 | |
|   }
 | |
| };
 | |
| 
 | |
| let observer = new (class extends EventEmitter {
 | |
|   constructor() {
 | |
|     super();
 | |
|     this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
 | |
|   }
 | |
| 
 | |
|   handlePlacesEvents(events) {
 | |
|     for (let event of events) {
 | |
|       switch (event.type) {
 | |
|         case "bookmark-added":
 | |
|           if (event.isTagging) {
 | |
|             continue;
 | |
|           }
 | |
|           let bookmark = {
 | |
|             id: event.guid,
 | |
|             parentId: event.parentGuid,
 | |
|             index: event.index,
 | |
|             title: event.title,
 | |
|             dateAdded: event.dateAdded,
 | |
|             type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType),
 | |
|             url: getUrl(event.itemType, event.url),
 | |
|           };
 | |
| 
 | |
|           if (event.itemType == TYPE_FOLDER) {
 | |
|             bookmark.dateGroupModified = bookmark.dateAdded;
 | |
|           }
 | |
| 
 | |
|           this.emit("created", bookmark);
 | |
|           break;
 | |
|         case "bookmark-removed":
 | |
|           if (event.isTagging || event.isDescendantRemoval) {
 | |
|             continue;
 | |
|           }
 | |
|           let node = {
 | |
|             id: event.guid,
 | |
|             parentId: event.parentGuid,
 | |
|             index: event.index,
 | |
|             type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType),
 | |
|             url: getUrl(event.itemType, event.url),
 | |
|             title: event.title,
 | |
|           };
 | |
| 
 | |
|           this.emit("removed", {
 | |
|             guid: event.guid,
 | |
|             info: { parentId: event.parentGuid, index: event.index, node },
 | |
|           });
 | |
|           break;
 | |
|         case "bookmark-moved":
 | |
|           this.emit("moved", {
 | |
|             guid: event.guid,
 | |
|             info: {
 | |
|               parentId: event.parentGuid,
 | |
|               index: event.index,
 | |
|               oldParentId: event.oldParentGuid,
 | |
|               oldIndex: event.oldIndex,
 | |
|             },
 | |
|           });
 | |
|           break;
 | |
|         case "bookmark-title-changed":
 | |
|           if (event.isTagging) {
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           this.emit("changed", {
 | |
|             guid: event.guid,
 | |
|             info: { title: event.title },
 | |
|           });
 | |
|           break;
 | |
|         case "bookmark-url-changed":
 | |
|           if (event.isTagging) {
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           this.emit("changed", {
 | |
|             guid: event.guid,
 | |
|             info: { url: event.url },
 | |
|           });
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| })();
 | |
| 
 | |
| const decrementListeners = () => {
 | |
|   listenerCount -= 1;
 | |
|   if (!listenerCount) {
 | |
|     PlacesUtils.observers.removeListener(
 | |
|       [
 | |
|         "bookmark-added",
 | |
|         "bookmark-removed",
 | |
|         "bookmark-moved",
 | |
|         "bookmark-title-changed",
 | |
|         "bookmark-url-changed",
 | |
|       ],
 | |
|       observer.handlePlacesEvents
 | |
|     );
 | |
|   }
 | |
| };
 | |
| 
 | |
| const incrementListeners = () => {
 | |
|   listenerCount++;
 | |
|   if (listenerCount == 1) {
 | |
|     PlacesUtils.observers.addListener(
 | |
|       [
 | |
|         "bookmark-added",
 | |
|         "bookmark-removed",
 | |
|         "bookmark-moved",
 | |
|         "bookmark-title-changed",
 | |
|         "bookmark-url-changed",
 | |
|       ],
 | |
|       observer.handlePlacesEvents
 | |
|     );
 | |
|   }
 | |
| };
 | |
| 
 | |
| this.bookmarks = class extends ExtensionAPIPersistent {
 | |
|   PERSISTENT_EVENTS = {
 | |
|     onCreated({ fire }) {
 | |
|       let listener = (event, bookmark) => {
 | |
|         fire.sync(bookmark.id, bookmark);
 | |
|       };
 | |
| 
 | |
|       observer.on("created", listener);
 | |
|       incrementListeners();
 | |
|       return {
 | |
|         unregister() {
 | |
|           observer.off("created", listener);
 | |
|           decrementListeners();
 | |
|         },
 | |
|         convert(_fire) {
 | |
|           fire = _fire;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     onRemoved({ fire }) {
 | |
|       let listener = (event, data) => {
 | |
|         fire.sync(data.guid, data.info);
 | |
|       };
 | |
| 
 | |
|       observer.on("removed", listener);
 | |
|       incrementListeners();
 | |
|       return {
 | |
|         unregister() {
 | |
|           observer.off("removed", listener);
 | |
|           decrementListeners();
 | |
|         },
 | |
|         convert(_fire) {
 | |
|           fire = _fire;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     onChanged({ fire }) {
 | |
|       let listener = (event, data) => {
 | |
|         fire.sync(data.guid, data.info);
 | |
|       };
 | |
| 
 | |
|       observer.on("changed", listener);
 | |
|       incrementListeners();
 | |
|       return {
 | |
|         unregister() {
 | |
|           observer.off("changed", listener);
 | |
|           decrementListeners();
 | |
|         },
 | |
|         convert(_fire) {
 | |
|           fire = _fire;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     onMoved({ fire }) {
 | |
|       let listener = (event, data) => {
 | |
|         fire.sync(data.guid, data.info);
 | |
|       };
 | |
| 
 | |
|       observer.on("moved", listener);
 | |
|       incrementListeners();
 | |
|       return {
 | |
|         unregister() {
 | |
|           observer.off("moved", listener);
 | |
|           decrementListeners();
 | |
|         },
 | |
|         convert(_fire) {
 | |
|           fire = _fire;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   getAPI(context) {
 | |
|     return {
 | |
|       bookmarks: {
 | |
|         async get(idOrIdList) {
 | |
|           let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
 | |
| 
 | |
|           try {
 | |
|             let bookmarks = [];
 | |
|             for (let id of list) {
 | |
|               let bookmark = await PlacesUtils.bookmarks.fetch({ guid: id });
 | |
|               if (!bookmark) {
 | |
|                 throw new Error("Bookmark not found");
 | |
|               }
 | |
|               bookmarks.push(convertBookmarks(bookmark));
 | |
|             }
 | |
|             return bookmarks;
 | |
|           } catch (error) {
 | |
|             return Promise.reject({ message: error.message });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         getChildren: function (id) {
 | |
|           // TODO: We should optimize this.
 | |
|           return getTree(id, true);
 | |
|         },
 | |
| 
 | |
|         getTree: function () {
 | |
|           return getTree(PlacesUtils.bookmarks.rootGuid, false);
 | |
|         },
 | |
| 
 | |
|         getSubTree: function (id) {
 | |
|           return getTree(id, false);
 | |
|         },
 | |
| 
 | |
|         search: function (query) {
 | |
|           return PlacesUtils.bookmarks
 | |
|             .search(query)
 | |
|             .then(result => result.map(convertBookmarks));
 | |
|         },
 | |
| 
 | |
|         getRecent: function (numberOfItems) {
 | |
|           return PlacesUtils.bookmarks
 | |
|             .getRecent(numberOfItems)
 | |
|             .then(result => result.map(convertBookmarks));
 | |
|         },
 | |
| 
 | |
|         create: function (bookmark) {
 | |
|           let info = {
 | |
|             title: bookmark.title || "",
 | |
|           };
 | |
| 
 | |
|           info.type = API_TYPES_TO_BOOKMARKS_TYPES_MAP.get(bookmark.type);
 | |
|           if (!info.type) {
 | |
|             // If url is NULL or missing, it will be a folder.
 | |
|             if (bookmark.url !== null) {
 | |
|               info.type = TYPE_BOOKMARK;
 | |
|             } else {
 | |
|               info.type = TYPE_FOLDER;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (info.type === TYPE_BOOKMARK) {
 | |
|             info.url = bookmark.url || "";
 | |
|           }
 | |
| 
 | |
|           if (bookmark.index !== null) {
 | |
|             info.index = bookmark.index;
 | |
|           }
 | |
| 
 | |
|           if (bookmark.parentId !== null) {
 | |
|             throwIfRootId(bookmark.parentId);
 | |
|             info.parentGuid = bookmark.parentId;
 | |
|           } else {
 | |
|             info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
 | |
|           }
 | |
| 
 | |
|           try {
 | |
|             return PlacesUtils.bookmarks
 | |
|               .insert(info)
 | |
|               .then(convertBookmarks)
 | |
|               .catch(error => Promise.reject({ message: error.message }));
 | |
|           } catch (e) {
 | |
|             return Promise.reject({
 | |
|               message: `Invalid bookmark: ${JSON.stringify(info)}`,
 | |
|             });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         move: function (id, destination) {
 | |
|           throwIfRootId(id);
 | |
|           let info = {
 | |
|             guid: id,
 | |
|           };
 | |
| 
 | |
|           if (destination.parentId !== null) {
 | |
|             throwIfRootId(destination.parentId);
 | |
|             info.parentGuid = destination.parentId;
 | |
|           }
 | |
|           info.index =
 | |
|             destination.index === null
 | |
|               ? PlacesUtils.bookmarks.DEFAULT_INDEX
 | |
|               : destination.index;
 | |
| 
 | |
|           try {
 | |
|             return PlacesUtils.bookmarks
 | |
|               .update(info)
 | |
|               .then(convertBookmarks)
 | |
|               .catch(error => Promise.reject({ message: error.message }));
 | |
|           } catch (e) {
 | |
|             return Promise.reject({
 | |
|               message: `Invalid bookmark: ${JSON.stringify(info)}`,
 | |
|             });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         update: function (id, changes) {
 | |
|           throwIfRootId(id);
 | |
|           let info = {
 | |
|             guid: id,
 | |
|           };
 | |
| 
 | |
|           if (changes.title !== null) {
 | |
|             info.title = changes.title;
 | |
|           }
 | |
|           if (changes.url !== null) {
 | |
|             info.url = changes.url;
 | |
|           }
 | |
| 
 | |
|           try {
 | |
|             return PlacesUtils.bookmarks
 | |
|               .update(info)
 | |
|               .then(convertBookmarks)
 | |
|               .catch(error => Promise.reject({ message: error.message }));
 | |
|           } catch (e) {
 | |
|             return Promise.reject({
 | |
|               message: `Invalid bookmark: ${JSON.stringify(info)}`,
 | |
|             });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         remove: function (id) {
 | |
|           throwIfRootId(id);
 | |
|           let info = {
 | |
|             guid: id,
 | |
|           };
 | |
| 
 | |
|           // The API doesn't give you the old bookmark at the moment
 | |
|           try {
 | |
|             return PlacesUtils.bookmarks
 | |
|               .remove(info, { preventRemovalOfNonEmptyFolders: true })
 | |
|               .catch(error => Promise.reject({ message: error.message }));
 | |
|           } catch (e) {
 | |
|             return Promise.reject({
 | |
|               message: `Invalid bookmark: ${JSON.stringify(info)}`,
 | |
|             });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         removeTree: function (id) {
 | |
|           throwIfRootId(id);
 | |
|           let info = {
 | |
|             guid: id,
 | |
|           };
 | |
| 
 | |
|           try {
 | |
|             return PlacesUtils.bookmarks
 | |
|               .remove(info)
 | |
|               .catch(error => Promise.reject({ message: error.message }));
 | |
|           } catch (e) {
 | |
|             return Promise.reject({
 | |
|               message: `Invalid bookmark: ${JSON.stringify(info)}`,
 | |
|             });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         onCreated: new EventManager({
 | |
|           context,
 | |
|           module: "bookmarks",
 | |
|           event: "onCreated",
 | |
|           extensionApi: this,
 | |
|         }).api(),
 | |
| 
 | |
|         onRemoved: new EventManager({
 | |
|           context,
 | |
|           module: "bookmarks",
 | |
|           event: "onRemoved",
 | |
|           extensionApi: this,
 | |
|         }).api(),
 | |
| 
 | |
|         onChanged: new EventManager({
 | |
|           context,
 | |
|           module: "bookmarks",
 | |
|           event: "onChanged",
 | |
|           extensionApi: this,
 | |
|         }).api(),
 | |
| 
 | |
|         onMoved: new EventManager({
 | |
|           context,
 | |
|           module: "bookmarks",
 | |
|           event: "onMoved",
 | |
|           extensionApi: this,
 | |
|         }).api(),
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| };
 | 
