1// Copyright (c) 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7/**
8 * Protocol + host parts of extension URL.
9 * @type {string}
10 * @const
11 */
12var FILE_MANAGER_HOST = 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj';
13
14// All of these scripts could be imported with a single call to importScripts,
15// but then load and compile time errors would all be reported from the same
16// line.
17importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/metadata_parser.js');
18importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/byte_reader.js');
19importScripts(FILE_MANAGER_HOST + '/common/js/util.js');
20
21/**
22 * Dispatches metadata requests to the correct parser.
23 *
24 * @param {Object} port Worker port.
25 * @constructor
26 */
27function MetadataDispatcher(port) {
28  this.port_ = port;
29  this.port_.onmessage = this.onMessage.bind(this);
30
31  // Make sure to update component_extension_resources.grd
32  // when adding new parsers.
33  importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/exif_parser.js');
34  importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/image_parsers.js');
35  importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/mpeg_parser.js');
36  importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/id3_parser.js');
37
38  var patterns = [];
39
40  this.parserInstances_ = [];
41  for (var i = 0; i < MetadataDispatcher.parserClasses_.length; i++) {
42    var parserClass = MetadataDispatcher.parserClasses_[i];
43    var parser = new parserClass(this);
44    this.parserInstances_.push(parser);
45    patterns.push(parser.urlFilter.source);
46  }
47
48  this.parserRegexp_ = new RegExp('(' + patterns.join('|') + ')', 'i');
49
50  this.messageHandlers_ = {
51    init: this.init_.bind(this),
52    request: this.request_.bind(this)
53  };
54}
55
56/**
57 * List of registered parser classes.
58 * @private
59 */
60MetadataDispatcher.parserClasses_ = [];
61
62/**
63 * @param {function} parserClass Parser constructor function.
64 */
65MetadataDispatcher.registerParserClass = function(parserClass) {
66  MetadataDispatcher.parserClasses_.push(parserClass);
67};
68
69/**
70 * Verbose logging for the dispatcher.
71 *
72 * Individual parsers also take this as their default verbosity setting.
73 */
74MetadataDispatcher.prototype.verbose = false;
75
76/**
77 * |init| message handler.
78 * @private
79 */
80MetadataDispatcher.prototype.init_ = function() {
81  // Inform our owner that we're done initializing.
82  // If we need to pass more data back, we can add it to the param array.
83  this.postMessage('initialized', [this.parserRegexp_]);
84  this.log('initialized with URL filter ' + this.parserRegexp_);
85};
86
87/**
88 * |request| message handler.
89 * @param {string} fileURL File URL.
90 * @private
91 */
92MetadataDispatcher.prototype.request_ = function(fileURL) {
93  try {
94    this.processOneFile(fileURL, function callback(metadata) {
95        this.postMessage('result', [fileURL, metadata]);
96    }.bind(this));
97  } catch (ex) {
98    this.error(fileURL, ex);
99  }
100};
101
102/**
103 * Indicate to the caller that an operation has failed.
104 *
105 * No other messages relating to the failed operation should be sent.
106 * @param {...Object} var_args Arguments.
107 */
108MetadataDispatcher.prototype.error = function(var_args) {
109  var ary = Array.apply(null, arguments);
110  this.postMessage('error', ary);
111};
112
113/**
114 * Send a log message to the caller.
115 *
116 * Callers must not parse log messages for control flow.
117 * @param {...Object} var_args Arguments.
118 */
119MetadataDispatcher.prototype.log = function(var_args) {
120  var ary = Array.apply(null, arguments);
121  this.postMessage('log', ary);
122};
123
124/**
125 * Send a log message to the caller only if this.verbose is true.
126 * @param {...Object} var_args Arguments.
127 */
128MetadataDispatcher.prototype.vlog = function(var_args) {
129  if (this.verbose)
130    this.log.apply(this, arguments);
131};
132
133/**
134 * Post a properly formatted message to the caller.
135 * @param {string} verb Message type descriptor.
136 * @param {Array.<Object>} args Arguments array.
137 */
138MetadataDispatcher.prototype.postMessage = function(verb, args) {
139  this.port_.postMessage({verb: verb, arguments: args});
140};
141
142/**
143 * Message handler.
144 * @param {Event} event Event object.
145 */
146MetadataDispatcher.prototype.onMessage = function(event) {
147  var data = event.data;
148
149  if (this.messageHandlers_.hasOwnProperty(data.verb)) {
150    this.messageHandlers_[data.verb].apply(this, data.arguments);
151  } else {
152    this.log('Unknown message from client: ' + data.verb, data);
153  }
154};
155
156/**
157 * @param {string} fileURL File URL.
158 * @param {function(Object)} callback Completion callback.
159 */
160MetadataDispatcher.prototype.processOneFile = function(fileURL, callback) {
161  var self = this;
162  var currentStep = -1;
163
164  function nextStep(var_args) {
165    self.vlog('nextStep: ' + steps[currentStep + 1].name);
166    steps[++currentStep].apply(self, arguments);
167  }
168
169  var metadata;
170
171  function onError(err, stepName) {
172    self.error(fileURL, stepName || steps[currentStep].name, err.toString(),
173        metadata);
174  }
175
176  var steps =
177  [ // Step one, find the parser matching the url.
178    function detectFormat() {
179      for (var i = 0; i != self.parserInstances_.length; i++) {
180        var parser = self.parserInstances_[i];
181        if (fileURL.match(parser.urlFilter)) {
182          // Create the metadata object as early as possible so that we can
183          // pass it with the error message.
184          metadata = parser.createDefaultMetadata();
185          nextStep(parser);
186          return;
187        }
188      }
189      onError('unsupported format');
190    },
191
192    // Step two, turn the url into an entry.
193    function getEntry(parser) {
194      webkitResolveLocalFileSystemURL(
195          fileURL,
196          function(entry) { nextStep(entry, parser) },
197          onError);
198    },
199
200    // Step three, turn the entry into a file.
201    function getFile(entry, parser) {
202      entry.file(function(file) { nextStep(file, parser) }, onError);
203    },
204
205    // Step four, parse the file content.
206    function parseContent(file, parser) {
207      metadata.fileSize = file.size;
208      try {
209        parser.parse(file, metadata, callback, onError);
210      } catch (e) {
211        onError(e.stack);
212      }
213    }
214  ];
215
216  nextStep();
217};
218
219// Webworker spec says that the worker global object is called self.  That's
220// a terrible name since we use it all over the chrome codebase to capture
221// the 'this' keyword in lambdas.
222var global = self;
223
224if (global.constructor.name == 'SharedWorkerGlobalScope') {
225  global.addEventListener('connect', function(e) {
226    var port = e.ports[0];
227    new MetadataDispatcher(port);
228    port.start();
229  });
230} else {
231  // Non-shared worker.
232  new MetadataDispatcher(global);
233}
234