Home | History | Annotate | Download | only in extensions
      1 // Copyright 2013 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 // Custom binding for the fileSystemProvider API.
      6 
      7 var binding = require('binding').Binding.create('fileSystemProvider');
      8 var fileSystemProviderInternal =
      9     require('binding').Binding.create('fileSystemProviderInternal').generate();
     10 var eventBindings = require('event_bindings');
     11 var fileSystemNatives = requireNative('file_system_natives');
     12 var GetDOMError = fileSystemNatives.GetDOMError;
     13 
     14 /**
     15  * Maximum size of the thumbnail in bytes.
     16  * @type {number}
     17  * @const
     18  */
     19 var METADATA_THUMBNAIL_SIZE_LIMIT = 32 * 1024 * 1024;
     20 
     21 /**
     22  * Regular expression to validate if the thumbnail URI is a valid data URI,
     23  * taking into account allowed formats.
     24  * @type {RegExp}
     25  * @const
     26  */
     27 var METADATA_THUMBNAIL_FORMAT = new RegExp(
     28     '^data:image/(png|jpeg|webp);', 'i');
     29 
     30 /**
     31  * Annotates a date with its serialized value.
     32  * @param {Date} date Input date.
     33  * @return {Date} Date with an extra <code>value</code> attribute.
     34  */
     35 function annotateDate(date) {
     36   // Copy in case the input date is frozen.
     37   var result = new Date(date.getTime());
     38   result.value = result.toString();
     39   return result;
     40 }
     41 
     42 /**
     43  * Verifies if the passed image URI is valid.
     44  * @param {*} uri Image URI.
     45  * @return {boolean} True if valid, valse otherwise.
     46  */
     47 function verifyImageURI(uri) {
     48   // The URI is specified by a user, so the type may be incorrect.
     49   if (typeof uri != 'string' && !(uri instanceof String))
     50     return false;
     51 
     52   return METADATA_THUMBNAIL_FORMAT.test(uri);
     53 }
     54 
     55 /**
     56  * Annotates an entry metadata by serializing its modifiedTime value.
     57  * @param {EntryMetadata} metadata Input metadata.
     58  * @return {EntryMetadata} metadata Annotated metadata, which can be passed
     59  *     back to the C++ layer.
     60  */
     61 function annotateMetadata(metadata) {
     62   var result = {
     63     isDirectory: metadata.isDirectory,
     64     name: metadata.name,
     65     size: metadata.size,
     66     modificationTime: annotateDate(metadata.modificationTime)
     67   };
     68   if ('mimeType' in metadata)
     69     result.mimeType = metadata.mimeType;
     70   if ('thumbnail' in metadata)
     71     result.thumbnail = metadata.thumbnail;
     72   return result;
     73 }
     74 
     75 /**
     76  * Massages arguments of an event raised by the File System Provider API.
     77  * @param {Array.<*>} args Input arguments.
     78  * @param {function(Array.<*>)} dispatch Closure to be called with massaged
     79  *     arguments.
     80  */
     81 function massageArgumentsDefault(args, dispatch) {
     82   var executionStart = Date.now();
     83   var options = args[0];
     84   var onSuccessCallback = function(hasNext) {
     85     fileSystemProviderInternal.operationRequestedSuccess(
     86         options.fileSystemId, options.requestId, Date.now() - executionStart);
     87   };
     88   var onErrorCallback = function(error) {
     89     fileSystemProviderInternal.operationRequestedError(
     90         options.fileSystemId, options.requestId, error,
     91         Date.now() - executionStart);
     92   }
     93   dispatch([options, onSuccessCallback, onErrorCallback]);
     94 }
     95 
     96 
     97 binding.registerCustomHook(function(bindingsAPI) {
     98   var apiFunctions = bindingsAPI.apiFunctions;
     99 
    100   apiFunctions.setUpdateArgumentsPostValidate(
    101     'mount',
    102     function(options, successCallback, errorCallback) {
    103       // Piggyback the error callback onto the success callback,
    104       // so we can use the error callback later.
    105       successCallback.errorCallback_ = errorCallback;
    106       return [options, successCallback];
    107     });
    108 
    109   apiFunctions.setCustomCallback(
    110     'mount',
    111     function(name, request, response) {
    112       var domError = null;
    113       if (request.callback && response) {
    114         // DOMError is present only if mount() failed.
    115         if (response[0]) {
    116           // Convert a Dictionary to a DOMError.
    117           domError = GetDOMError(response[0].name, response[0].message);
    118           response.length = 1;
    119         }
    120 
    121         var successCallback = request.callback;
    122         var errorCallback = request.callback.errorCallback_;
    123         delete request.callback;
    124 
    125         if (domError)
    126           errorCallback(domError);
    127         else
    128           successCallback();
    129       }
    130     });
    131 
    132   apiFunctions.setUpdateArgumentsPostValidate(
    133     'unmount',
    134     function(options, successCallback, errorCallback) {
    135       // Piggyback the error callback onto the success callback,
    136       // so we can use the error callback later.
    137       successCallback.errorCallback_ = errorCallback;
    138       return [options, successCallback];
    139     });
    140 
    141   apiFunctions.setCustomCallback(
    142     'unmount',
    143     function(name, request, response) {
    144       var domError = null;
    145       if (request.callback) {
    146         // DOMError is present only if mount() failed.
    147         if (response && response[0]) {
    148           // Convert a Dictionary to a DOMError.
    149           domError = GetDOMError(response[0].name, response[0].message);
    150           response.length = 1;
    151         }
    152 
    153         var successCallback = request.callback;
    154         var errorCallback = request.callback.errorCallback_;
    155         delete request.callback;
    156 
    157         if (domError)
    158           errorCallback(domError);
    159         else
    160           successCallback();
    161       }
    162     });
    163 });
    164 
    165 eventBindings.registerArgumentMassager(
    166     'fileSystemProvider.onUnmountRequested',
    167     massageArgumentsDefault);
    168 
    169 eventBindings.registerArgumentMassager(
    170     'fileSystemProvider.onGetMetadataRequested',
    171     function(args, dispatch) {
    172       var executionStart = Date.now();
    173       var options = args[0];
    174       var onSuccessCallback = function(metadata) {
    175         var error;
    176         // It is invalid to return a thumbnail when it's not requested. The
    177         // restriction is added in order to avoid fetching the thumbnail while
    178         // it's not needed.
    179         if (!options.thumbnail && metadata.thumbnail)
    180           error = 'Thumbnail data provided, but not requested.';
    181 
    182         // Check the format and size. Note, that in the C++ layer, there is
    183         // another sanity check to avoid passing any evil URL.
    184         if ('thumbnail' in metadata && !verifyImageURI(metadata.thumbnail))
    185           error = 'Thumbnail format invalid.';
    186 
    187         if ('thumbnail' in metadata &&
    188             metadata.thumbnail.length > METADATA_THUMBNAIL_SIZE_LIMIT) {
    189           error = 'Thumbnail data too large.';
    190         }
    191 
    192         if (error) {
    193           console.error(error);
    194           fileSystemProviderInternal.operationRequestedError(
    195               options.fileSystemId, options.requestId, 'FAILED',
    196               Date.now() - executionStart);
    197           return;
    198         }
    199 
    200         fileSystemProviderInternal.getMetadataRequestedSuccess(
    201             options.fileSystemId,
    202             options.requestId,
    203             annotateMetadata(metadata),
    204             Date.now() - executionStart);
    205       };
    206 
    207       var onErrorCallback = function(error) {
    208         fileSystemProviderInternal.operationRequestedError(
    209             options.fileSystemId, options.requestId, error,
    210             Date.now() - executionStart);
    211       }
    212 
    213       dispatch([options, onSuccessCallback, onErrorCallback]);
    214     });
    215 
    216 eventBindings.registerArgumentMassager(
    217     'fileSystemProvider.onReadDirectoryRequested',
    218     function(args, dispatch) {
    219       var executionStart = Date.now();
    220       var options = args[0];
    221       var onSuccessCallback = function(entries, hasNext) {
    222         var annotatedEntries = entries.map(annotateMetadata);
    223         // It is invalid to return a thumbnail when it's not requested.
    224         var error;
    225         annotatedEntries.forEach(function(metadata) {
    226           if (metadata.thumbnail) {
    227             var error =
    228                 'Thumbnails must not be provided when reading a directory.';
    229             return;
    230           }
    231         });
    232 
    233         if (error) {
    234           console.error(error);
    235           fileSystemProviderInternal.operationRequestedError(
    236               options.fileSystemId, options.requestId, 'FAILED',
    237               Date.now() - executionStart);
    238           return;
    239         }
    240 
    241         fileSystemProviderInternal.readDirectoryRequestedSuccess(
    242             options.fileSystemId, options.requestId, annotatedEntries, hasNext,
    243             Date.now() - executionStart);
    244       };
    245 
    246       var onErrorCallback = function(error) {
    247         fileSystemProviderInternal.operationRequestedError(
    248             options.fileSystemId, options.requestId, error,
    249             Date.now() - executionStart);
    250       }
    251       dispatch([options, onSuccessCallback, onErrorCallback]);
    252     });
    253 
    254 eventBindings.registerArgumentMassager(
    255     'fileSystemProvider.onOpenFileRequested',
    256     massageArgumentsDefault);
    257 
    258 eventBindings.registerArgumentMassager(
    259     'fileSystemProvider.onCloseFileRequested',
    260     massageArgumentsDefault);
    261 
    262 eventBindings.registerArgumentMassager(
    263     'fileSystemProvider.onReadFileRequested',
    264     function(args, dispatch) {
    265       var executionStart = Date.now();
    266       var options = args[0];
    267       var onSuccessCallback = function(data, hasNext) {
    268         fileSystemProviderInternal.readFileRequestedSuccess(
    269             options.fileSystemId, options.requestId, data, hasNext,
    270             Date.now() - executionStart);
    271       };
    272       var onErrorCallback = function(error) {
    273         fileSystemProviderInternal.operationRequestedError(
    274             options.fileSystemId, options.requestId, error,
    275             Date.now() - executionStart);
    276       }
    277       dispatch([options, onSuccessCallback, onErrorCallback]);
    278     });
    279 
    280 eventBindings.registerArgumentMassager(
    281     'fileSystemProvider.onCreateDirectoryRequested',
    282     massageArgumentsDefault);
    283 
    284 eventBindings.registerArgumentMassager(
    285     'fileSystemProvider.onDeleteEntryRequested',
    286     massageArgumentsDefault);
    287 
    288 eventBindings.registerArgumentMassager(
    289     'fileSystemProvider.onCreateFileRequested',
    290     massageArgumentsDefault);
    291 
    292 eventBindings.registerArgumentMassager(
    293     'fileSystemProvider.onCopyEntryRequested',
    294     massageArgumentsDefault);
    295 
    296 eventBindings.registerArgumentMassager(
    297     'fileSystemProvider.onMoveEntryRequested',
    298     massageArgumentsDefault);
    299 
    300 eventBindings.registerArgumentMassager(
    301     'fileSystemProvider.onTruncateRequested',
    302     massageArgumentsDefault);
    303 
    304 eventBindings.registerArgumentMassager(
    305     'fileSystemProvider.onWriteFileRequested',
    306     massageArgumentsDefault);
    307 
    308 eventBindings.registerArgumentMassager(
    309     'fileSystemProvider.onAbortRequested',
    310     massageArgumentsDefault);
    311 
    312 exports.binding = binding.generate();
    313