tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ext-bookmarks.js (13475B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     11 });
     12 
     13 var { ExtensionError } = ExtensionUtils;
     14 
     15 const { TYPE_BOOKMARK, TYPE_FOLDER, TYPE_SEPARATOR } = PlacesUtils.bookmarks;
     16 
     17 const BOOKMARKS_TYPES_TO_API_TYPES_MAP = new Map([
     18  [TYPE_BOOKMARK, "bookmark"],
     19  [TYPE_FOLDER, "folder"],
     20  [TYPE_SEPARATOR, "separator"],
     21 ]);
     22 
     23 const BOOKMARK_SEPERATOR_URL = "data:";
     24 
     25 ChromeUtils.defineLazyGetter(this, "API_TYPES_TO_BOOKMARKS_TYPES_MAP", () => {
     26  let theMap = new Map();
     27 
     28  for (let [code, name] of BOOKMARKS_TYPES_TO_API_TYPES_MAP) {
     29    theMap.set(name, code);
     30  }
     31  return theMap;
     32 });
     33 
     34 let listenerCount = 0;
     35 
     36 function getUrl(type, url) {
     37  switch (type) {
     38    case TYPE_BOOKMARK:
     39      return url;
     40    case TYPE_SEPARATOR:
     41      return BOOKMARK_SEPERATOR_URL;
     42    default:
     43      return undefined;
     44  }
     45 }
     46 
     47 const getTree = (rootGuid, onlyChildren) => {
     48  function convert(node, parent) {
     49    let treenode = {
     50      id: node.guid,
     51      title: PlacesUtils.bookmarks.getLocalizedTitle(node) || "",
     52      index: node.index,
     53      dateAdded: node.dateAdded / 1000,
     54      type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(node.typeCode),
     55      url: getUrl(node.typeCode, node.uri),
     56    };
     57 
     58    if (parent && node.guid != PlacesUtils.bookmarks.rootGuid) {
     59      treenode.parentId = parent.guid;
     60    }
     61 
     62    if (node.typeCode == TYPE_FOLDER) {
     63      treenode.dateGroupModified = node.lastModified / 1000;
     64 
     65      if (!onlyChildren) {
     66        treenode.children = node.children
     67          ? node.children.map(child => convert(child, node))
     68          : [];
     69      }
     70    }
     71 
     72    return treenode;
     73  }
     74 
     75  return PlacesUtils.promiseBookmarksTree(rootGuid)
     76    .then(root => {
     77      if (onlyChildren) {
     78        let children = root.children || [];
     79        return children.map(child => convert(child, root));
     80      }
     81      let treenode = convert(root, null);
     82      treenode.parentId = root.parentGuid;
     83      // It seems like the array always just contains the root node.
     84      return [treenode];
     85    })
     86    .catch(e => Promise.reject({ message: e.message }));
     87 };
     88 
     89 const convertBookmarks = result => {
     90  let node = {
     91    id: result.guid,
     92    title: PlacesUtils.bookmarks.getLocalizedTitle(result) || "",
     93    index: result.index,
     94    dateAdded: result.dateAdded.getTime(),
     95    type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(result.type),
     96    url: getUrl(result.type, result.url && result.url.href),
     97  };
     98 
     99  if (result.guid != PlacesUtils.bookmarks.rootGuid) {
    100    node.parentId = result.parentGuid;
    101  }
    102 
    103  if (result.type == TYPE_FOLDER) {
    104    node.dateGroupModified = result.lastModified.getTime();
    105  }
    106 
    107  return node;
    108 };
    109 
    110 const throwIfRootId = id => {
    111  if (id == PlacesUtils.bookmarks.rootGuid) {
    112    throw new ExtensionError("The bookmark root cannot be modified");
    113  }
    114 };
    115 
    116 let observer = new (class extends EventEmitter {
    117  constructor() {
    118    super();
    119    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
    120  }
    121 
    122  handlePlacesEvents(events) {
    123    for (let event of events) {
    124      switch (event.type) {
    125        case "bookmark-added": {
    126          if (event.isTagging) {
    127            continue;
    128          }
    129          let bookmark = {
    130            id: event.guid,
    131            parentId: event.parentGuid,
    132            index: event.index,
    133            title: event.title,
    134            dateAdded: event.dateAdded,
    135            type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType),
    136            url: getUrl(event.itemType, event.url),
    137          };
    138 
    139          if (event.itemType == TYPE_FOLDER) {
    140            bookmark.dateGroupModified = bookmark.dateAdded;
    141          }
    142 
    143          this.emit("created", bookmark);
    144          break;
    145        }
    146        case "bookmark-removed": {
    147          if (event.isTagging || event.isDescendantRemoval) {
    148            continue;
    149          }
    150          let node = {
    151            id: event.guid,
    152            parentId: event.parentGuid,
    153            index: event.index,
    154            type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType),
    155            url: getUrl(event.itemType, event.url),
    156            title: event.title,
    157          };
    158 
    159          this.emit("removed", {
    160            guid: event.guid,
    161            info: { parentId: event.parentGuid, index: event.index, node },
    162          });
    163          break;
    164        }
    165        case "bookmark-moved":
    166          this.emit("moved", {
    167            guid: event.guid,
    168            info: {
    169              parentId: event.parentGuid,
    170              index: event.index,
    171              oldParentId: event.oldParentGuid,
    172              oldIndex: event.oldIndex,
    173            },
    174          });
    175          break;
    176        case "bookmark-title-changed":
    177          if (event.isTagging) {
    178            continue;
    179          }
    180 
    181          this.emit("changed", {
    182            guid: event.guid,
    183            info: { title: event.title },
    184          });
    185          break;
    186        case "bookmark-url-changed":
    187          if (event.isTagging) {
    188            continue;
    189          }
    190 
    191          this.emit("changed", {
    192            guid: event.guid,
    193            info: { url: event.url },
    194          });
    195          break;
    196      }
    197    }
    198  }
    199 })();
    200 
    201 const decrementListeners = () => {
    202  listenerCount -= 1;
    203  if (!listenerCount) {
    204    PlacesUtils.observers.removeListener(
    205      [
    206        "bookmark-added",
    207        "bookmark-removed",
    208        "bookmark-moved",
    209        "bookmark-title-changed",
    210        "bookmark-url-changed",
    211      ],
    212      observer.handlePlacesEvents
    213    );
    214  }
    215 };
    216 
    217 const incrementListeners = () => {
    218  listenerCount++;
    219  if (listenerCount == 1) {
    220    PlacesUtils.observers.addListener(
    221      [
    222        "bookmark-added",
    223        "bookmark-removed",
    224        "bookmark-moved",
    225        "bookmark-title-changed",
    226        "bookmark-url-changed",
    227      ],
    228      observer.handlePlacesEvents
    229    );
    230  }
    231 };
    232 
    233 this.bookmarks = class extends ExtensionAPIPersistent {
    234  PERSISTENT_EVENTS = {
    235    onCreated({ fire }) {
    236      let listener = (event, bookmark) => {
    237        fire.sync(bookmark.id, bookmark);
    238      };
    239 
    240      observer.on("created", listener);
    241      incrementListeners();
    242      return {
    243        unregister() {
    244          observer.off("created", listener);
    245          decrementListeners();
    246        },
    247        convert(_fire) {
    248          fire = _fire;
    249        },
    250      };
    251    },
    252 
    253    onRemoved({ fire }) {
    254      let listener = (event, data) => {
    255        fire.sync(data.guid, data.info);
    256      };
    257 
    258      observer.on("removed", listener);
    259      incrementListeners();
    260      return {
    261        unregister() {
    262          observer.off("removed", listener);
    263          decrementListeners();
    264        },
    265        convert(_fire) {
    266          fire = _fire;
    267        },
    268      };
    269    },
    270 
    271    onChanged({ fire }) {
    272      let listener = (event, data) => {
    273        fire.sync(data.guid, data.info);
    274      };
    275 
    276      observer.on("changed", listener);
    277      incrementListeners();
    278      return {
    279        unregister() {
    280          observer.off("changed", listener);
    281          decrementListeners();
    282        },
    283        convert(_fire) {
    284          fire = _fire;
    285        },
    286      };
    287    },
    288 
    289    onMoved({ fire }) {
    290      let listener = (event, data) => {
    291        fire.sync(data.guid, data.info);
    292      };
    293 
    294      observer.on("moved", listener);
    295      incrementListeners();
    296      return {
    297        unregister() {
    298          observer.off("moved", listener);
    299          decrementListeners();
    300        },
    301        convert(_fire) {
    302          fire = _fire;
    303        },
    304      };
    305    },
    306  };
    307 
    308  getAPI(context) {
    309    return {
    310      bookmarks: {
    311        async get(idOrIdList) {
    312          let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
    313 
    314          try {
    315            let bookmarks = [];
    316            for (let id of list) {
    317              let bookmark = await PlacesUtils.bookmarks.fetch({ guid: id });
    318              if (!bookmark) {
    319                throw new Error("Bookmark not found");
    320              }
    321              bookmarks.push(convertBookmarks(bookmark));
    322            }
    323            return bookmarks;
    324          } catch (error) {
    325            return Promise.reject({ message: error.message });
    326          }
    327        },
    328 
    329        getChildren: function (id) {
    330          // TODO: We should optimize this.
    331          return getTree(id, true);
    332        },
    333 
    334        getTree: function () {
    335          return getTree(PlacesUtils.bookmarks.rootGuid, false);
    336        },
    337 
    338        getSubTree: function (id) {
    339          return getTree(id, false);
    340        },
    341 
    342        search: function (query) {
    343          return PlacesUtils.bookmarks
    344            .search(query)
    345            .then(result => result.map(convertBookmarks));
    346        },
    347 
    348        getRecent: function (numberOfItems) {
    349          return PlacesUtils.bookmarks
    350            .getRecent(numberOfItems)
    351            .then(result => result.map(convertBookmarks));
    352        },
    353 
    354        create: function (bookmark) {
    355          let info = {
    356            title: bookmark.title || "",
    357          };
    358 
    359          info.type = API_TYPES_TO_BOOKMARKS_TYPES_MAP.get(bookmark.type);
    360          if (!info.type) {
    361            // If url is NULL or missing, it will be a folder.
    362            if (bookmark.url !== null) {
    363              info.type = TYPE_BOOKMARK;
    364            } else {
    365              info.type = TYPE_FOLDER;
    366            }
    367          }
    368 
    369          if (info.type === TYPE_BOOKMARK) {
    370            info.url = bookmark.url || "";
    371          }
    372 
    373          if (bookmark.index !== null) {
    374            info.index = bookmark.index;
    375          }
    376 
    377          if (bookmark.parentId !== null) {
    378            throwIfRootId(bookmark.parentId);
    379            info.parentGuid = bookmark.parentId;
    380          } else {
    381            info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
    382          }
    383 
    384          try {
    385            return PlacesUtils.bookmarks
    386              .insert(info)
    387              .then(convertBookmarks)
    388              .catch(error => Promise.reject({ message: error.message }));
    389          } catch (e) {
    390            return Promise.reject({
    391              message: `Invalid bookmark: ${JSON.stringify(info)}`,
    392            });
    393          }
    394        },
    395 
    396        move: function (id, destination) {
    397          throwIfRootId(id);
    398          let info = {
    399            guid: id,
    400          };
    401 
    402          if (destination.parentId !== null) {
    403            throwIfRootId(destination.parentId);
    404            info.parentGuid = destination.parentId;
    405          }
    406          info.index =
    407            destination.index === null
    408              ? PlacesUtils.bookmarks.DEFAULT_INDEX
    409              : destination.index;
    410 
    411          try {
    412            return PlacesUtils.bookmarks
    413              .update(info)
    414              .then(convertBookmarks)
    415              .catch(error => Promise.reject({ message: error.message }));
    416          } catch (e) {
    417            return Promise.reject({
    418              message: `Invalid bookmark: ${JSON.stringify(info)}`,
    419            });
    420          }
    421        },
    422 
    423        update: function (id, changes) {
    424          throwIfRootId(id);
    425          let info = {
    426            guid: id,
    427          };
    428 
    429          if (changes.title !== null) {
    430            info.title = changes.title;
    431          }
    432          if (changes.url !== null) {
    433            info.url = changes.url;
    434          }
    435 
    436          try {
    437            return PlacesUtils.bookmarks
    438              .update(info)
    439              .then(convertBookmarks)
    440              .catch(error => Promise.reject({ message: error.message }));
    441          } catch (e) {
    442            return Promise.reject({
    443              message: `Invalid bookmark: ${JSON.stringify(info)}`,
    444            });
    445          }
    446        },
    447 
    448        remove: function (id) {
    449          throwIfRootId(id);
    450          let info = {
    451            guid: id,
    452          };
    453 
    454          // The API doesn't give you the old bookmark at the moment
    455          try {
    456            return PlacesUtils.bookmarks
    457              .remove(info, { preventRemovalOfNonEmptyFolders: true })
    458              .catch(error => Promise.reject({ message: error.message }));
    459          } catch (e) {
    460            return Promise.reject({
    461              message: `Invalid bookmark: ${JSON.stringify(info)}`,
    462            });
    463          }
    464        },
    465 
    466        removeTree: function (id) {
    467          throwIfRootId(id);
    468          let info = {
    469            guid: id,
    470          };
    471 
    472          try {
    473            return PlacesUtils.bookmarks
    474              .remove(info)
    475              .catch(error => Promise.reject({ message: error.message }));
    476          } catch (e) {
    477            return Promise.reject({
    478              message: `Invalid bookmark: ${JSON.stringify(info)}`,
    479            });
    480          }
    481        },
    482 
    483        onCreated: new EventManager({
    484          context,
    485          module: "bookmarks",
    486          event: "onCreated",
    487          extensionApi: this,
    488        }).api(),
    489 
    490        onRemoved: new EventManager({
    491          context,
    492          module: "bookmarks",
    493          event: "onRemoved",
    494          extensionApi: this,
    495        }).api(),
    496 
    497        onChanged: new EventManager({
    498          context,
    499          module: "bookmarks",
    500          event: "onChanged",
    501          extensionApi: this,
    502        }).api(),
    503 
    504        onMoved: new EventManager({
    505          context,
    506          module: "bookmarks",
    507          event: "onMoved",
    508          extensionApi: this,
    509        }).api(),
    510      },
    511    };
    512  }
    513 };