/* 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'; module.metadata = { "stability": "experimental", "engines": { "Palemoon": "*", "Firefox": "*", "SeaMonkey": "*" } }; const { Cc, Ci, Cu } = require('chrome'); const { Class } = require('../core/heritage'); const { method } = require('../lang/functional'); const { defer, promised, all } = require('../core/promise'); const { send } = require('../addon/events'); const { EventTarget } = require('../event/target'); const { merge } = require('../util/object'); const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); Cu.importGlobalProperties(["URL"]); /* * TreeNodes are used to construct dependency trees * for BookmarkItems */ var TreeNode = Class({ initialize: function (value) { this.value = value; this.children = []; }, add: function (values) { [].concat(values).forEach(value => { this.children.push(value instanceof TreeNode ? value : TreeNode(value)); }); }, get length () { let count = 0; this.walk(() => count++); // Do not count the current node return --count; }, get: method(get), walk: method(walk), toString: () => '[object TreeNode]' }); exports.TreeNode = TreeNode; /* * Descends down from `node` applying `fn` to each in order. * `fn` can return values or promises -- if promise returned, * children are not processed until resolved. `fn` is passed * one argument, the current node, `curr`. */ function walk (curr, fn) { return promised(fn)(curr).then(val => { return all(curr.children.map(child => walk(child, fn))); }); } /* * Descends from the TreeNode `node`, returning * the node with value `value` if found or `null` * otherwise */ function get (node, value) { if (node.value === value) return node; for (let child of node.children) { let found = get(child, value); if (found) return found; } return null; } /* * Constructs a tree of bookmark nodes * returning the root (value: null); */ function constructTree (items) { let root = TreeNode(null); items.forEach(treeify.bind(null, root)); function treeify (root, item) { // If node already exists, skip let node = root.get(item); if (node) return node; node = TreeNode(item); let parentNode = item.group ? treeify(root, item.group) : root; parentNode.add(node); return node; } return root; } exports.constructTree = constructTree; /* * Shortcut for converting an id, or an object with an id, into * an object with corresponding bookmark data */ function fetchItem (item) { return send('sdk-places-bookmarks-get', { id: item.id || item }); } exports.fetchItem = fetchItem; /* * Takes an ID or an object with ID and checks it against * the root bookmark folders */ function isRootGroup (id) { id = id && id.id; return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder, bmsrv.unfiledBookmarksFolder ].indexOf(id); } exports.isRootGroup = isRootGroup; /* * Merges appropriate options into query based off of url * 4 scenarios: * * 'moz.com' // domain: moz.com, domainIsHost: true * --> 'http://moz.com', 'http://moz.com/thunderbird' * '*.moz.com' // domain: moz.com, domainIsHost: false * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test' * 'http://moz.com' // uri: http://moz.com/ * --> 'http://moz.com/' * 'http://moz.com/*' // uri: http://moz.com/, domain: moz.com, domainIsHost: true * --> 'http://moz.com/', 'http://moz.com/thunderbird' */ function urlQueryParser (query, url) { if (!url) return; if (/^https?:\/\//.test(url)) { query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/'; if (/\*$/.test(url)) { // Wildcard searches on URIs are not supported, so try to extract a // domain and filter the data later. url = url.replace(/\*$/, ''); try { query.domain = new URL(url).hostname; query.domainIsHost = true; // Unfortunately here we cannot use an expando to store the wildcard, // cause the query is a wrapped native XPCOM object, so we reuse uri. // We clearly don't want to query for both uri and domain, thus we'll // have to handle this in host-query.js::execute() query.uri = url; } catch (ex) { // Cannot extract an host cause it's not a valid uri, the query will // just return nothing. } } } else { if (/^\*/.test(url)) { query.domain = url.replace(/^\*\./, ''); query.domainIsHost = false; } else { query.domain = url; query.domainIsHost = true; } } } exports.urlQueryParser = urlQueryParser; /* * Takes an EventEmitter and returns a promise that * aggregates results and handles a bulk resolve and reject */ function promisedEmitter (emitter) { let { promise, resolve, reject } = defer(); let errors = []; emitter.on('error', error => errors.push(error)); emitter.on('end', (items) => { if (errors.length) reject(errors[0]); else resolve(items); }); return promise; } exports.promisedEmitter = promisedEmitter; // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions function createQuery (type, query) { query = query || {}; let qObj = { searchTerms: query.query }; urlQueryParser(qObj, query.url); // 0 === history if (type === 0) { // PRTime used by query is in microseconds, not milliseconds qObj.beginTime = (query.from || 0) * 1000; qObj.endTime = (query.to || new Date()) * 1000; // Set reference time to Epoch qObj.beginTimeReference = 0; qObj.endTimeReference = 0; } // 1 === bookmarks else if (type === 1) { qObj.tags = query.tags; qObj.folder = query.group && query.group.id; } // 2 === unified (not implemented on platform) else if (type === 2) { } return qObj; } exports.createQuery = createQuery; // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions const SORT_MAP = { title: 1, date: 3, // sort by visit date url: 5, visitCount: 7, // keywords currently unsupported // keyword: 9, dateAdded: 11, // bookmarks only lastModified: 13 // bookmarks only }; function createQueryOptions (type, options) { options = options || {}; let oObj = {}; oObj.sortingMode = SORT_MAP[options.sort] || 0; if (options.descending && options.sort) oObj.sortingMode++; // Resolve to default sort if ineligible based on query type if (type === 0 && // history (options.sort === 'dateAdded' || options.sort === 'lastModified')) oObj.sortingMode = 0; oObj.maxResults = typeof options.count === 'number' ? options.count : 0; oObj.queryType = type; return oObj; } exports.createQueryOptions = createQueryOptions; function mapBookmarkItemType (type) { if (typeof type === 'number') { if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark'; if (bmsrv.TYPE_FOLDER === type) return 'group'; if (bmsrv.TYPE_SEPARATOR === type) return 'separator'; } else { if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK; if ('group' === type) return bmsrv.TYPE_FOLDER; if ('separator' === type) return bmsrv.TYPE_SEPARATOR; } } exports.mapBookmarkItemType = mapBookmarkItemType;