summaryrefslogtreecommitdiff
path: root/application/basilisk/components/newtab/PlacesProvider.jsm
blob: 1a0e991412704d8d8ccaea9602b10d6a88b3308f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/* 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/. */

/* global XPCOMUtils, Services, PlacesUtils, EventEmitter */
/* global gLinks */
/* exported PlacesProvider */

"use strict";

this.EXPORTED_SYMBOLS = ["PlacesProvider"];

const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
  "resource://gre/modules/PlacesUtils.jsm");

XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
  const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
  return EventEmitter;
});

XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                  "resource://gre/modules/NewTabUtils.jsm");

// The maximum number of results PlacesProvider retrieves from history.
const HISTORY_RESULTS_LIMIT = 100;

/* Queries history to retrieve the most visited sites. Emits events when the
 * history changes.
 * Implements the EventEmitter interface.
 */
let Links = function Links() {
  EventEmitter.decorate(this);
};

Links.prototype = {
  /**
   * Set this to change the maximum number of links the provider will provide.
   */
  get maxNumLinks() {
    // getter, so it can't be replaced dynamically
    return HISTORY_RESULTS_LIMIT;
  },

  /**
   * A set of functions called by @mozilla.org/browser/nav-historyservice
   * All history events are emitted from this object.
   */
  historyObserver: {
    _batchProcessingDepth: 0,
    _batchCalledFrecencyChanged: false,

    /**
     * Called by the history service.
     */
    onBeginUpdateBatch() {
      this._batchProcessingDepth += 1;
    },

    onEndUpdateBatch() {
      this._batchProcessingDepth -= 1;
      if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) {
        this.onManyFrecenciesChanged();
        this._batchCalledFrecencyChanged = false;
      }
    },

    onDeleteURI: function historyObserver_onDeleteURI(aURI) {
      // let observers remove sensetive data associated with deleted visit
      gLinks.emit("deleteURI", {
        url: aURI.spec,
      });
    },

    onClearHistory: function historyObserver_onClearHistory() {
      gLinks.emit("clearHistory");
    },

    onFrecencyChanged: function historyObserver_onFrecencyChanged(aURI,
                           aNewFrecency, aGUID, aHidden, aLastVisitDate) { // jshint ignore:line

      // If something is doing a batch update of history entries we don't want
      // to do lots of work for each record. So we just track the fact we need
      // to call onManyFrecenciesChanged() once the batch is complete.
      if (this._batchProcessingDepth > 0) {
        this._batchCalledFrecencyChanged = true;
        return;
      }

      // The implementation of the query in getLinks excludes hidden and
      // unvisited pages, so it's important to exclude them here, too.
      if (!aHidden && aLastVisitDate &&
          NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
        gLinks.emit("linkChanged", {
          url: aURI.spec,
          frecency: aNewFrecency,
          lastVisitDate: aLastVisitDate,
          type: "history",
        });
      }
    },

    onManyFrecenciesChanged: function historyObserver_onManyFrecenciesChanged() {
      // Called when frecencies are invalidated and also when clearHistory is called
      // See toolkit/components/places/tests/unit/test_frecency_observers.js
      gLinks.emit("manyLinksChanged");
    },

    onVisit(aURI, aVisitId, aTime, aSessionId, aReferrerVisitId, aTransitionType,
            aGuid, aHidden, aVisitCount, aTyped, aLastKnownTitle) {
      // For new visits, if we're not batch processing, notify for a title update
      if (!this._batchProcessingDepth && aVisitCount == 1 && aLastKnownTitle) {
        this.onTitleChanged(aURI, aLastKnownTitle, aGuid);
      }
    },

    onTitleChanged: function historyObserver_onTitleChanged(aURI, aNewTitle) {
      if (NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
        gLinks.emit("linkChanged", {
          url: aURI.spec,
          title: aNewTitle
        });
      }
    },

    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
                                           Ci.nsISupportsWeakReference])
  },

  /**
   * Must be called before the provider is used.
   * Makes it easy to disable under pref
   */
  init: function PlacesProvider_init() {
    try {
      PlacesUtils.history.addObserver(this.historyObserver, true);
    } catch (e) {
      Cu.reportError(e);
    }
  },

  /**
   * Gets the current set of links delivered by this provider.
   *
   * @returns {Promise} Returns a promise with the array of links as payload.
   */
  getLinks: Task.async(function*() {
    // Select a single page per host with highest frecency, highest recency.
    // Choose N top such pages. Note +rev_host, to turn off optimizer per :mak
    // suggestion.
    let sqlQuery = `SELECT url, title, frecency,
                          last_visit_date as lastVisitDate,
                          "history" as type
                   FROM moz_places
                   WHERE frecency in (
                     SELECT MAX(frecency) as frecency
                     FROM moz_places
                     WHERE hidden = 0 AND last_visit_date NOTNULL
                     GROUP BY +rev_host
                     ORDER BY frecency DESC
                     LIMIT :limit
                   )
                   GROUP BY rev_host HAVING MAX(lastVisitDate)
                   ORDER BY frecency DESC, lastVisitDate DESC, url`;

    let links = yield this.executePlacesQuery(sqlQuery, {
                  columns: ["url", "title", "lastVisitDate", "frecency", "type"],
                  params: {limit: this.maxNumLinks}
                });

    return links.filter(link => NewTabUtils.linkChecker.checkLoadURI(link.url));
  }),

  /**
   * Executes arbitrary query against places database
   *
   * @param {String} aSql
   *        SQL query to execute
   * @param {Object} [optional] aOptions
   *        aOptions.columns - an array of column names. if supplied the returned
   *        items will consist of objects keyed on column names. Otherwise
   *        an array of raw values is returned in the select order
   *        aOptions.param - an object of SQL binding parameters
   *        aOptions.callback - a callback to handle query rows
   *
   * @returns {Promise} Returns a promise with the array of retrieved items
   */
  executePlacesQuery: Task.async(function*(aSql, aOptions = {}) {
    let {columns, params, callback} = aOptions;
    let items = [];
    let queryError = null;
    let conn = yield PlacesUtils.promiseDBConnection();
    yield conn.executeCached(aSql, params, aRow => {
      try {
        // check if caller wants to handle query raws
        if (callback) {
          callback(aRow);
        } else {
          // otherwise fill in the item and add items array
          let item = null;
          // if columns array is given construct an object
          if (columns && Array.isArray(columns)) {
            item = {};
            columns.forEach(column => {
              item[column] = aRow.getResultByName(column);
            });
          } else {
            // if no columns - make an array of raw values
            item = [];
            for (let i = 0; i < aRow.numEntries; i++) {
              item.push(aRow.getResultByIndex(i));
            }
          }
          items.push(item);
        }
      } catch (e) {
        queryError = e;
        throw StopIteration;
      }
    });
    if (queryError) {
      throw new Error(queryError);
    }
    return items;
  }),
};

/**
 * Singleton that serves as the default link provider for the grid.
 */
const gLinks = new Links(); // jshint ignore:line

let PlacesProvider = {
  links: gLinks,
};

// Kept only for backwards-compatibility
XPCOMUtils.defineLazyGetter(PlacesProvider, "LinkChecker",
  () => NewTabUtils.linkChecker);