main.ts 14.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*******************************************************************************
 * Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 * contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 *
 * This file is part of CubicWeb.
 *
 * CubicWeb is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * CubicWeb is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

21
import { definition } from "@logilab/libview";
22
import {
23
  DocumentSourceLinked,
24
  tryNegotiateData,
Laurent Wouters's avatar
Laurent Wouters committed
25
26
27
  ObservedResourceRegistry,
  resolveObservationsForTab,
  hasDetectedData,
Laurent Wouters's avatar
Laurent Wouters committed
28
29
30
31
  detectTopicOnlinks,
  detectDataOnLinks,
  fetchObservableAt,
  ObservableContent,
Laurent Wouters's avatar
Laurent Wouters committed
32
  observeContent,
33
  getAcceptRdf,
34
  DocumentStatus,
35
36
  DocumentObservations,
  OriginKind
Laurent Wouters's avatar
Laurent Wouters committed
37
} from "../common/data";
38
import { Message, activateTab } from "../common/messages";
Laurent Wouters's avatar
Laurent Wouters committed
39
40
/// <reference path="./fallback.d.ts"/>
let F = require("./fallback");
41
import "chrome";
42
43
44
45
46
import {
  reloadRegistryFromStorage,
  addViewSource,
  removeViewSource
} from "../common/registry";
47
48
49
50

/**
 * The data about the tabs
 */
Laurent Wouters's avatar
Laurent Wouters committed
51
let allObservations: ObservedResourceRegistry = {};
52
53
54
/**
 * The observations for an opening tab
 */
55
let openingObservations: DocumentObservations | null = null;
Laurent Wouters's avatar
Laurent Wouters committed
56
57
58
/**
 * The reference view registry
 */
59
let registry: definition.ViewRegistry = new definition.ViewRegistry();
60
61
62
63
64
65
/**
 * The cache for the remote resources
 */
let cache: definition.ViewResourceCache = {};

reloadRegistryFromStorage(registry)
66
67
68
  .then((registry: definition.ViewRegistry) => {
    /* do nothing */
  })
69
70
71
  .catch((reason: string) => {
    console.log(reason);
  });
Laurent Wouters's avatar
Laurent Wouters committed
72

73
/**
Laurent Wouters's avatar
Laurent Wouters committed
74
 * When the observed data about a tab has been updated
75
76
 * @param id The identifier of a tab
 */
Laurent Wouters's avatar
Laurent Wouters committed
77
78
function onObservedTabUpdated(tabId: number): void {
  let observation = resolveObservationsForTab(allObservations, tabId);
79
  let isLit =
80
81
    observation.status !== DocumentStatus.off || hasDetectedData(observation);
  let hasPopup = observation.status === DocumentStatus.active;
82
83

  if (isLit) {
Laurent Wouters's avatar
Laurent Wouters committed
84
85
86
87
    chrome.pageAction.show(tabId);
  } else {
    chrome.pageAction.hide(tabId);
  }
88
89
90
91
  chrome.pageAction.setPopup({
    tabId: tabId,
    popup: hasPopup ? chrome.extension.getURL("popup/index.html") : ""
  });
92
93
94
95
96
97
98
99
}

/**
 * Get the value of a header
 * @param headers The headers
 * @param name The name of a header
 */
function getHeader(
100
  headers: chrome.webRequest.HttpHeader[] | undefined,
101
  name: string
102
103
104
105
106
107
): string | null {
  if (headers === undefined) return null;
  for (let i = 0; i !== headers.length; i++) {
    if (headers[i].name === name) {
      let v = headers[i].value;
      if (v !== undefined) return v;
108
109
110
111
112
    }
  }
  return null;
}

113
114
115
116
117
118
119
120
121
122
123
/**
 * Set the value of a header
 * @param headers The headers
 * @param name The name of a header
 * @param value The value for the header
 */
function setHeader(
  headers: chrome.webRequest.HttpHeader[],
  name: string,
  value: string
): void {
124
125
  for (let i = 0; i !== headers.length; i++) {
    if (headers[i].name === name) {
126
127
128
129
130
131
      headers[i].value = value;
    }
  }
  headers.push({ name: name, value: value });
}

132
133
134
135
136
137
138
139
/**
 * When headers are being sent for a request
 * @param details The details
 */
function onSendingHeaders(
  details: chrome.webRequest.WebRequestHeadersDetails
): chrome.webRequest.BlockingResponse {
  if (
140
141
142
143
144
    details.tabId === -1 || // invalid tab
    details.frameId !== 0 || // not the main top-level frame
    details.method !== "GET" || // not a GET method
    details.type !== "main_frame" // not the top-level document
  ) {
145
    return {};
146
  }
147
  let observation = allObservations[details.tabId];
148
149
  if (observation === undefined) {
    if (openingObservations === null) return {};
150
151
152
    // this is tab opening from the extension
    observation = openingObservations;
  }
153
  // do not request RDF for off and asleep tabs
154
  if (
155
156
157
    observation.status === DocumentStatus.off ||
    observation.status === DocumentStatus.asleep
  ) {
158
    return {};
159
  }
160

161
  if (observation.origin.kind !== OriginKind.Redirected) {
162
    observation.origin = {
163
      kind: OriginKind.Direct,
164
165
166
      url: details.url
    };
  }
167

168
  if (observation.preemptable) {
169
    // this tab has been preempted
170
    let headers = details.requestHeaders;
171
    if (headers === undefined) headers = [];
172
173
    setHeader(headers, "Accept", getAcceptRdf(false));
    return { requestHeaders: headers };
174
  } else if (observation.negotiated !== null) {
175
176
    // this tab has negotiated content
    let headers = details.requestHeaders;
177
    if (headers === undefined) headers = [];
178
179
    setHeader(headers, "Accept", observation.negotiated.contentType);
    return { requestHeaders: headers };
180
181
182
183
  }
  return {};
}

184
185
186
187
188
189
/**
 * When headers are received for a tab
 * @param details The details
 */
function onHeadersReceived(
  details: chrome.webRequest.WebResponseHeadersDetails
190
): chrome.webRequest.BlockingResponse {
191
  if (
192
193
194
    details.tabId === -1 || // invalid tab
    details.frameId !== 0 || // not the main top-level frame
    details.method !== "GET" || // not a GET method
195
    details.statusCode < 200 || // not an OK
196
    details.statusCode >= 400 || // redirect or failure no handled
197
198
    details.type !== "main_frame" // not the top-level document
  ) {
199
    return {};
200
  }
Laurent Wouters's avatar
Laurent Wouters committed
201

202
203
  if (details.statusCode >= 300) {
    let observation = allObservations[details.tabId];
204
    if (details.statusCode === 303 && observation) {
205
206
      // mark as negotiated
      observation.origin = {
207
        kind: OriginKind.Redirected,
208
209
210
211
212
213
        url: details.url
      };
    }
    return {};
  }

214
215
216
217
218
219
220
221
222
223
224
  let index = details.url.indexOf("?protocol=ldviews&uri=");
  if (index >= 0) {
    // preempts the protocol ldviews
    let uri = details.url.substring(
      index + "?protocol=ldviews&uri=".length,
      details.url.length
    );
    return {
      redirectUrl: chrome.extension.getURL("ldviews/index.html") + "?uri=" + uri
    };
  }
Laurent Wouters's avatar
Laurent Wouters committed
225

Laurent Wouters's avatar
Laurent Wouters committed
226
  let contentType = getHeader(details.responseHeaders, "Content-Type");
227
  if (contentType === null) {
228
229
230
    // no content
    return {};
  }
Laurent Wouters's avatar
Laurent Wouters committed
231
232
  index = contentType.indexOf(";");
  if (index >= 0) contentType = contentType.substring(0, index);
233
  let linkHeader = getHeader(details.responseHeaders, "Link");
Laurent Wouters's avatar
Laurent Wouters committed
234
  let observable: ObservableContent = {
235
    content: "",
Laurent Wouters's avatar
Laurent Wouters committed
236
    contentType: contentType,
237
    linkHeader: linkHeader !== null ? linkHeader : "",
Laurent Wouters's avatar
Laurent Wouters committed
238
239
    url: details.url
  };
240
  let obs = observeContent(observable, {
241
    kind: OriginKind.Direct,
242
243
    url: details.url
  });
Laurent Wouters's avatar
Laurent Wouters committed
244
  let observation = resolveObservationsForTab(allObservations, details.tabId);
245
246
247
  if (observation.origin.kind === OriginKind.Unkown) {
    observation.origin = obs.origin;
  }
Laurent Wouters's avatar
Laurent Wouters committed
248
249
250
  observation.url = obs.url;
  observation.primaryTopic = obs.primaryTopic;
  observation.sources = obs.sources;
251
  observation.preemptable = obs.preemptable;
252
  observation.negotiated = obs.negotiated;
253
254
255
  if (obs.preemptable) {
    // modify header
    let headers = details.responseHeaders;
256
    if (headers === undefined) headers = [];
257
258
259
    setHeader(headers, "Content-Type", "text/plain");
    return { responseHeaders: headers };
  }
260
  // if there is still nothing, try to probe with HTTP content negotiation
Laurent Wouters's avatar
Laurent Wouters committed
261
  if (!hasDetectedData(observation)) {
262
    // look for data through content negotiation
263
    tryNegotiateData(details.url)
264
      .then((source: DocumentSourceLinked) => {
265
        observation.negotiated = source;
Laurent Wouters's avatar
Laurent Wouters committed
266
267
        observation.sources.push(source);
        onObservedTabUpdated(details.tabId);
268
269
270
271
272
      })
      .catch((reason: string) => {
        // do nothing
      });
  }
273
  onObservedTabUpdated(details.tabId);
274
  return {};
275
276
277
278
279
280
281
282
283
}

/**
 * Event after tab navigation occurred
 * @param details the event details
 */
function onCompleted(
  details: chrome.webNavigation.WebNavigationFramedCallbackDetails
): void {
284
  if (details.tabId === -1 || details.frameId !== 0) return;
Laurent Wouters's avatar
Laurent Wouters committed
285
  onObservedTabUpdated(details.tabId);
286
287
288
289
290
291
}

// listen to received headers
chrome.webRequest.onHeadersReceived.addListener(
  onHeadersReceived,
  { urls: ["<all_urls>"] },
292
  ["blocking", "responseHeaders"]
293
);
294
295
296
297
298
299
// listen to sent headers
chrome.webRequest.onBeforeSendHeaders.addListener(
  onSendingHeaders,
  { urls: ["<all_urls>"] },
  ["blocking", "requestHeaders"]
);
300
// listen to navigation
301
F.registerNavigations(onCompleted);
302

Laurent Wouters's avatar
Laurent Wouters committed
303
304
chrome.runtime.onMessage.addListener(
  (request: Message<any>, sender, sendResponse) => {
305
306
    if (request.requestType === "SubmitHtmlLinks") {
      if (sender.tab === undefined || sender.tab.id === undefined) return;
Laurent Wouters's avatar
Laurent Wouters committed
307
308
309
310
      let observation = resolveObservationsForTab(
        allObservations,
        sender.tab.id
      );
311
312
313
      if (observation.primaryTopic === "") {
        let topic = detectTopicOnlinks(request.payload);
        observation.primaryTopic = topic !== null ? topic : "";
Laurent Wouters's avatar
Laurent Wouters committed
314
      }
Laurent Wouters's avatar
Laurent Wouters committed
315
      observation.sources = observation.sources.concat(
316
        detectDataOnLinks(request.payload, {
317
          kind: OriginKind.HtmlLink,
318
319
          url: observation.url
        })
Laurent Wouters's avatar
Laurent Wouters committed
320
321
      );
      onObservedTabUpdated(sender.tab.id);
322
323
    } else if (request.requestType === "GetTabId") {
      if (sender.tab === undefined || sender.tab.id === undefined) return;
Laurent Wouters's avatar
Laurent Wouters committed
324
      sendResponse(sender.tab.id);
325
    } else if (request.requestType === "GetObservations") {
326
      let tabId = request.payload;
327
      if (tabId === null || tabId === undefined) {
328
        // deduce from the sender
329
        if (sender.tab === undefined || sender.tab.id === undefined) return;
330
331
        tabId = sender.tab.id;
      }
Laurent Wouters's avatar
Laurent Wouters committed
332
333
      let observation = resolveObservationsForTab(allObservations, tabId);
      sendResponse(observation);
334
    } else if (request.requestType === "GetViewRegistry") {
Laurent Wouters's avatar
Laurent Wouters committed
335
      sendResponse(registry);
336
    } else if (request.requestType === "AddNewSource") {
337
      addViewSource(registry, request.payload)
338
339
340
341
342
343
344
        .then((newRegistry: definition.ViewRegistry) => {
          sendResponse(newRegistry);
        })
        .catch((reason: any) => {
          console.log("Failed to reload registry: " + reason);
        });
      return true;
345
    } else if (request.requestType === "RemoveSource") {
346
      removeViewSource(registry, request.payload)
347
348
349
350
351
352
353
        .then((newRegistry: definition.ViewRegistry) => {
          sendResponse(newRegistry);
        })
        .catch((reason: any) => {
          console.log("Failed to reload registry: " + reason);
        });
      return true;
354
    } else if (request.requestType === "ReloadViewRegistry") {
355
356
      cache = {};
      reloadRegistryFromStorage(registry)
357
358
359
360
361
362
363
        .then((newRegistry: definition.ViewRegistry) => {
          sendResponse(newRegistry);
        })
        .catch((reason: any) => {
          console.log("Failed to reload registry: " + reason);
        });
      return true;
364
    } else if (request.requestType === "GetResourceContent") {
365
366
367
368
369
370
371
372
373
      definition
        .getResourceContent(cache, request.payload)
        .then((content: string) => {
          sendResponse(content);
        })
        .catch((reason: any) => {
          console.log("Failed to fetch the resource: " + reason);
        });
      return true;
374
    } else if (request.requestType === "FetchObservable") {
Laurent Wouters's avatar
Laurent Wouters committed
375
376
377
378
379
380
381
382
      fetchObservableAt(request.payload)
        .then((content: ObservableContent) => {
          sendResponse({ ok: content, error: null });
        })
        .catch((reason: any) => {
          sendResponse({ ok: null, error: reason });
        });
      return true;
383
    } else if (request.requestType === "TurnOffForTab") {
384
      let tabId = request.payload;
385
      if (tabId === null || tabId === undefined) {
386
        // deduce from the sender
387
        if (sender.tab === undefined || sender.tab.id === undefined) return;
388
389
390
        tabId = sender.tab.id;
      }
      let observation = allObservations[tabId];
391
392
393
394
      if (observation === undefined) {
        sendResponse(null);
        return;
      }
395
396
397
      observation.status = DocumentStatus.asleep;
      onObservedTabUpdated(tabId);
      sendResponse(observation);
398
    } else if (request.requestType === "ToggleRawForTab") {
399
      let tabId = request.payload;
400
      if (tabId === null || tabId === undefined) {
401
        // deduce from the sender
402
        if (sender.tab === undefined || sender.tab.id === undefined) return;
403
404
405
        tabId = sender.tab.id;
      }
      let observation = allObservations[tabId];
406
407
408
409
      if (observation === undefined) {
        sendResponse(null);
        return;
      }
410
411
      observation.status = DocumentStatus.activeRaw;
      onObservedTabUpdated(tabId);
412
      sendResponse(observation);
413
    }
414
  }
Laurent Wouters's avatar
Laurent Wouters committed
415
);
Laurent Wouters's avatar
Laurent Wouters committed
416
417

chrome.pageAction.onClicked.addListener((tab: chrome.tabs.Tab) => {
418
  if (tab.id === undefined) return;
Laurent Wouters's avatar
Laurent Wouters committed
419
  let observation = resolveObservationsForTab(allObservations, tab.id);
420
  if (observation.status === DocumentStatus.off) {
421
    // this tab was off
422
    let isNegotiated = observation.negotiated !== null;
423
    let callback = (openedTab: chrome.tabs.Tab) => {
424
      if (openedTab.id === undefined || openingObservations === null) return;
425
426
      allObservations[openedTab.id] = openingObservations;
      openingObservations = null;
427
      onObservedTabUpdated(openedTab.id);
428
      activateTab(openedTab.id, observation);
429
    };
430
431
432
433
434
    let url = isNegotiated
      ? observation.url
      : chrome.extension.getURL(
          "ldbrowser/index.html?target=" + encodeURIComponent(observation.url)
        );
435
    openingObservations = JSON.parse(JSON.stringify(observation));
436
    if (openingObservations === null) return;
437
    openingObservations.status = DocumentStatus.active;
438
439
440
441
442
443
444
445
446
447
    let promise: any = chrome.tabs.create(
      {
        windowId: tab.windowId,
        index: tab.index + 1,
        url: url,
        active: true,
        openerTabId: tab.id
      },
      callback
    );
448
    if (promise !== undefined && promise !== null) {
449
450
      promise.then(callback);
    }
451
  } else if (
452
453
    observation.status === DocumentStatus.asleep ||
    observation.status === DocumentStatus.activeRaw
454
455
456
457
  ) {
    observation.status = DocumentStatus.active;
    onObservedTabUpdated(tab.id);
    chrome.tabs.reload(tab.id);
Laurent Wouters's avatar
Laurent Wouters committed
458
459
  }
});