Revert "Zip Archiver: Disable storing mount and password info."

This reverts commit c3169366954d82dc6b35ad6a869907cde0d714cd.

Reason for revert: This caused regression Issue 812123.
This had caused that mount resuming succeeds superficially by
the caller despite it actually failed, thus leaving inaccessible
zip mount point.

Original change's description:
> Zip Archiver: Disable storing mount and password info.
>
> We are not restoring mount files at session restart anymore.
> Therefore the mount information and passwords should not be persisted.
> Othrewise it will be persisted until the user opens the same archive
> again manually after a long time and the files will be opened without
> requiring passwords.
>
> Bug: 789073,803752
> Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
> Change-Id: Idadcff694b20109eb7e31710b57d88400601d30a
> Reviewed-on: https://chromium-review.googlesource.com/876705
> Commit-Queue: Tatsuhisa Yamaguchi <yamaguchi@chromium.org>
> Reviewed-by: Yuki Awano <yawano@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#531467}

TBR=yawano@chromium.org,yamaguchi@chromium.org

Bug: 789073, 803752
Change-Id: I387e16717b5187389d01c5d5bc4a8b6884dcf035
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Reviewed-on: https://chromium-review.googlesource.com/928063
Reviewed-by: Naoki Fukino <fukino@chromium.org>
Commit-Queue: Tatsuhisa Yamaguchi <yamaguchi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#538338}(cherry picked from commit 92067a9e7c3867cbfb832dd8d953a4ec54a59238)


Bug: 812123
Change-Id: I387e16717b5187389d01c5d5bc4a8b6884dcf035
Reviewed-on: https://chromium-review.googlesource.com/933681
Reviewed-by: Tatsuhisa Yamaguchi <yamaguchi@chromium.org>
Cr-Commit-Position: refs/branch-heads/3325@{#565}
Cr-Branched-From: bc084a8b5afa3744a74927344e304c02ae54189f-refs/heads/master@{#530369}
diff --git a/chrome/browser/resources/chromeos/zip_archiver/css/passphrase-dialog.css b/chrome/browser/resources/chromeos/zip_archiver/css/passphrase-dialog.css
index dc9dc11..fb2bea6 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/css/passphrase-dialog.css
+++ b/chrome/browser/resources/chromeos/zip_archiver/css/passphrase-dialog.css
@@ -16,9 +16,14 @@
   justify-content: space-between;
 }
 
-#buttons {
+#bar {
   align-items: center;
   display: flex;
+  justify-content: space-between;
+}
+
+#buttons {
+  display: flex;
   justify-content: flex-end;
 }
 
diff --git a/chrome/browser/resources/chromeos/zip_archiver/html/passphrase-dialog.html b/chrome/browser/resources/chromeos/zip_archiver/html/passphrase-dialog.html
index 2659bb9..3efca3c 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/html/passphrase-dialog.html
+++ b/chrome/browser/resources/chromeos/zip_archiver/html/passphrase-dialog.html
@@ -14,17 +14,22 @@
              autofocus
              i18n-values="placeholder:ZIP_ARCHIVER_PASSPHRASE_INPUT_LABEL;aria-label:ZIP_ARCHIVER_PASSPHRASE_INPUT_LABEL">
     </paper-input-container>
-    <div id="buttons">
-      <paper-button noink
-                    on-click="accept"
-                    id="acceptButton"
-                    i18n-content="ZIP_ARCHIVER_PASSPHRASE_ACCEPT">
-      </paper-button>
-      <paper-button noink
-                    on-click="cancel"
-                    id="cancelButton"
-                    i18n-content="ZIP_ARCHIVER_PASSPHRASE_CANCEL">
-      </paper-button>
+    <div id="bar">
+      <paper-checkbox id="remember"
+                      i18n-content="ZIP_ARCHIVER_PASSPHRASE_REMEMBER">
+      </paper-checkbox>
+      <div id="buttons">
+        <paper-button noink
+                      on-click="accept"
+                      id="acceptButton"
+                      i18n-content="ZIP_ARCHIVER_PASSPHRASE_ACCEPT">
+        </paper-button>
+        <paper-button noink
+                      on-click="cancel"
+                      id="cancelButton"
+                      i18n-content="ZIP_ARCHIVER_PASSPHRASE_CANCEL">
+        </paper-button>
+      </div>
     </div>
   </template>
 </dom-module>
diff --git a/chrome/browser/resources/chromeos/zip_archiver/js/app.js b/chrome/browser/resources/chromeos/zip_archiver/js/app.js
index da1316b..7844bb7 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/js/app.js
+++ b/chrome/browser/resources/chromeos/zip_archiver/js/app.js
@@ -10,6 +10,12 @@
  */
 unpacker.app = {
   /**
+   * The key used by chrome.storage.local to save and restore the volumes state.
+   * @const {string}
+   */
+  STORAGE_KEY: 'state',
+
+  /**
    * The default id for the NaCl module.
    * @const {string}
    */
@@ -167,6 +173,98 @@
   },
 
   /**
+   * Saves state in case of restarts, event page suspend, crashes, etc. This
+   * method does nothing when context is in incognito mode.
+   * @param {!Array<!unpacker.types.FileSystemId>} fileSystemIdsArray
+   * @private
+   */
+  saveState_: function(fileSystemIdsArray) {
+    // If current context is in incognito mode, then skip save state because
+    // retainEntry is not available in incognito mode.
+    if (chrome.extension.inIncognitoContext)
+      return;
+
+    chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) {
+      if (!result[unpacker.app.STORAGE_KEY])  // First save state call.
+        result[unpacker.app.STORAGE_KEY] = {};
+
+      // Overwrite state only for the volumes that have their file system id
+      // present in the input array. Leave the rest of the volumes state
+      // untouched.
+      fileSystemIdsArray.forEach(function(fileSystemId) {
+        var entryId = chrome.fileSystem.retainEntry(
+            unpacker.app.volumes[fileSystemId].entry);
+        result[unpacker.app.STORAGE_KEY][fileSystemId] = {
+          entryId: entryId,
+          passphrase: unpacker.app.volumes[fileSystemId]
+                          .decompressor.passphraseManager.rememberedPassphrase
+        };
+      });
+
+      chrome.storage.local.set(result);
+    });
+  },
+
+  /**
+   * Removes state from local storage for a single volume. This method does
+   * nothing when context is in incognito mode.
+   * @param {!unpacker.types.FileSystemId} fileSystemId
+   */
+  removeState_: function(fileSystemId) {
+    if (chrome.extension.inIncognitoContext)
+      return;
+
+    chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) {
+      console.assert(
+          result[unpacker.app.STORAGE_KEY] &&
+              result[unpacker.app.STORAGE_KEY][fileSystemId],
+          'Should call removeState_ only for file systems that ',
+          'have previously called saveState_.');
+
+      delete result[unpacker.app.STORAGE_KEY][fileSystemId];
+      chrome.storage.local.set(result);
+    });
+  },
+
+  /**
+   * Restores archive's entry and opened files for the passed file system id.
+   * @param {!unpacker.types.FileSystemId} fileSystemId
+   * @return {!Promise<!Object>} Promise fulfilled with the entry and list of
+   *     opened files.
+   * @private
+   */
+  restoreVolumeState_: function(fileSystemId) {
+    if (chrome.extension.inIncognitoContext)
+      return new Promise.reject('No state restored due to incognito context');
+    return new Promise(function(fulfill, reject) {
+      chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) {
+        if (!result[unpacker.app.STORAGE_KEY]) {
+          reject('FAILED');
+          return;
+        }
+
+        var volumeState = result[unpacker.app.STORAGE_KEY][fileSystemId];
+        if (!volumeState) {
+          console.error('No state for: ' + fileSystemId + '.');
+          reject('FAILED');
+          return;
+        }
+
+        chrome.fileSystem.restoreEntry(volumeState.entryId, function(entry) {
+          if (chrome.runtime.lastError) {
+            console.error(
+                'Restore entry error for <', fileSystemId,
+                '>: ' + chrome.runtime.lastError.message);
+            reject('FAILED');
+            return;
+          }
+          fulfill({entry: entry, passphrase: volumeState.passphrase});
+        });
+      });
+    });
+  },
+
+  /**
    * Creates a volume and loads its metadata from NaCl.
    * @param {!unpacker.types.FileSystemId} fileSystemId
    * @param {!Entry} entry The volume's archive entry.
@@ -225,9 +323,68 @@
   },
 
   /**
+   * Restores a volume mounted previously to a suspend / restart. In case of
+   * failure of the load promise for fileSystemId, the corresponding volume is
+   * forcely unmounted.
+   * @param {!unpacker.types.FileSystemId} fileSystemId
+   * @return {!Promise} A promise that restores state and loads volume.
+   * @private
+   */
+  restoreSingleVolume_: function(fileSystemId) {
+    // Load volume after restart / suspend page event.
+    return unpacker.app.restoreVolumeState_(fileSystemId)
+        .then(function(state) {
+          return new Promise(function(fulfill, reject) {
+            // Check if the file system is compatible with this version of the
+            // ZIP unpacker.
+            // TODO(mtomasz): Implement remounting instead of unmounting.
+            chrome.fileSystemProvider.get(fileSystemId, function(fileSystem) {
+              if (chrome.runtime.lastError) {
+                console.error(chrome.runtime.lastError.name);
+                reject('FAILED');
+                return;
+              }
+              if (!fileSystem || fileSystem.openedFilesLimit != 1) {
+                console.error('No compatible mounted file system found.');
+                reject('FAILED');
+                return;
+              }
+              fulfill({state: state, fileSystem: fileSystem});
+            });
+          });
+        })
+        .then(function(stateWithFileSystem) {
+          var openedFilesOptions = {};
+          stateWithFileSystem.fileSystem.openedFiles.forEach(function(
+              openedFile) {
+            openedFilesOptions[openedFile.openRequestId] = {
+              fileSystemId: fileSystemId,
+              requestId: openedFile.openRequestId,
+              mode: openedFile.mode,
+              filePath: openedFile.filePath
+            };
+          });
+          return unpacker.app.loadVolume_(
+              fileSystemId, stateWithFileSystem.state.entry, openedFilesOptions,
+              stateWithFileSystem.state.passphrase);
+        })
+        .catch(function(error) {
+          console.error(error.stack || error);
+          // Force unmount in case restore failed. All resources related to the
+          // volume will be cleanup from both memory and local storage.
+          // TODO(523195): Show a notification that the source file is gone.
+          return unpacker.app.unmountVolume(fileSystemId, true)
+              .then(function() {
+                return Promise.reject('FAILED');
+              });
+        });
+  },
+
+  /**
    * Ensures a volume is loaded by returning its corresponding loaded promise
    * from unpacker.app.volumeLoadedPromises. In case there is no such promise,
-   * then this simply returns a rejected Promise.
+   * then this is a call after suspend / restart and a new volume loaded promise
+   * that restores state is returned.
    * @param {!unpacker.types.FileSystemId} fileSystemId
    * @return {!Promise} The loading volume promise.
    * @private
@@ -243,9 +400,13 @@
     }
 
     return unpacker.app.moduleLoadedPromise.then(function() {
+      // In case there is no volume promise for fileSystemId then we
+      // received a call after restart / suspend as load promises are
+      // created on launched. In this case we will restore volume state
+      // from local storage and create a new load promise.
       if (!unpacker.app.volumeLoadedPromises[fileSystemId]) {
-        console.error(fileSystemId + ' requested before mounting');
-        return Promise.reject('NOT_FOUND');
+        unpacker.app.volumeLoadedPromises[fileSystemId] =
+            unpacker.app.restoreSingleVolume_(fileSystemId);
       }
 
       // Decrement the counter when the mounting process ends.
@@ -340,7 +501,8 @@
   },
 
   /**
-   * Cleans up the resources for a volume.
+   * Cleans up the resources for a volume, except for the local storage. If
+   * necessary that can be done using unpacker.app.removeState_.
    * @param {!unpacker.types.FileSystemId} fileSystemId
    */
   cleanupVolume: function(fileSystemId) {
@@ -390,6 +552,16 @@
   },
 
   /**
+   * Updates the state in case of restarts, event page suspend, crashes, etc.
+   * Use this method to update or save the state out side of the object in case
+   * when password changes, etc.
+   * @param {!Array<!unpacker.types.FileSystemId>} fileSystemIdsArray
+   */
+  updateState: function(fileSystemIdsArray) {
+    unpacker.app.saveState_(fileSystemIdsArray);
+  },
+
+  /**
    * Unmounts a volume and removes any resources related to the volume from both
    * the extension and the local storage state.
    * @param {!unpacker.types.FileSystemId} fileSystemId
@@ -430,6 +602,8 @@
         else
           unpacker.app.cleanupVolume(fileSystemId);
 
+        // Remove volume from local storage.
+        unpacker.app.removeState_(fileSystemId);
         fulfill();
       });
     });
@@ -769,6 +943,9 @@
                   loadPromise
                       .then(function() {
                         unpacker.app.volumeLoadFinished[fileSystemId] = true;
+                        // Mount the volume and save its information in local
+                        // storage in order to be able to recover the metadata
+                        // in case of restarts, system crashes, etc.
                         chrome.fileSystemProvider.mount(
                             {
                               fileSystemId: fileSystemId,
@@ -781,6 +958,9 @@
                                 onError(chrome.runtime.lastError, fileSystemId);
                                 return;
                               }
+                              // Save state so in case of restarts we are able
+                              // to correctly get the archive's metadata.
+                              unpacker.app.saveState_([fileSystemId]);
                               onSuccess(fileSystemId);
                             });
                       })
@@ -819,5 +999,13 @@
       unpacker.app.onLaunchedWithPack(launchData);
     else
       unpacker.app.onLaunchedWithUnpack(launchData, opt_onSuccess, opt_onError);
+  },
+
+  /**
+   * Saves the state before suspending the event page, so we can resume it
+   * once new events arrive.
+   */
+  onSuspend: function() {
+    unpacker.app.saveState_(Object.keys(unpacker.app.volumes));
   }
 };
diff --git a/chrome/browser/resources/chromeos/zip_archiver/js/background.js b/chrome/browser/resources/chromeos/zip_archiver/js/background.js
index 173598a..c9fe20e 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/js/background.js
+++ b/chrome/browser/resources/chromeos/zip_archiver/js/background.js
@@ -8,6 +8,10 @@
 // declared in the manifest file.
 chrome.app.runtime.onLaunched.addListener(unpacker.app.onLaunched);
 
+// Save the state before suspending the event page, so we can resume it
+// once new events arrive.
+chrome.runtime.onSuspend.addListener(unpacker.app.onSuspend);
+
 chrome.fileSystemProvider.onUnmountRequested.addListener(
     unpacker.app.onUnmountRequested);
 chrome.fileSystemProvider.onGetMetadataRequested.addListener(
diff --git a/chrome/browser/resources/chromeos/zip_archiver/js/decompressor.js b/chrome/browser/resources/chromeos/zip_archiver/js/decompressor.js
index 87b0453..aa1e986 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/js/decompressor.js
+++ b/chrome/browser/resources/chromeos/zip_archiver/js/decompressor.js
@@ -42,7 +42,8 @@
   this.passphraseManager = passphraseManager;
 
   /**
-   * Requests in progress.
+   * Requests in progress. No need to save them onSuspend for now as metadata
+   * reads are restarted from start.
    * @public {!Object<!unpacker.types.RequestId, !Object>}
    * @const
    */
@@ -295,6 +296,8 @@
 unpacker.Decompressor.prototype.readPassphrase_ = function(data, requestId) {
   this.passphraseManager.getPassphrase()
       .then(function(passphrase) {
+        // Update remembered password
+        unpacker.app.updateState([this.fileSystemId_]);
         this.naclModule_.postMessage(
             unpacker.request.createReadPassphraseDoneResponse(
                 this.fileSystemId_, requestId, passphrase));
diff --git a/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-dialog.js b/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-dialog.js
index 2df9854..4cc7936 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-dialog.js
+++ b/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-dialog.js
@@ -19,7 +19,7 @@
   },
 
   accept: function() {
-    window.onPassphraseSuccess(this.$.input.value);
+    window.onPassphraseSuccess(this.$.input.value, this.$.remember.checked);
     window.close();
   },
 
diff --git a/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-manager.js b/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-manager.js
index 9c37e57..71755e73 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-manager.js
+++ b/chrome/browser/resources/chromeos/zip_archiver/js/passphrase-manager.js
@@ -4,15 +4,38 @@
 
 /**
  * @constructor
+ * @param {?string} initPassphrase Initial passphrase for the first passphrase
+ *     request or NULL if not available. In such case, call to getPassphrase()
+ *     will invoke a dialog.
  */
-unpacker.PassphraseManager = function() {};
+unpacker.PassphraseManager = function(initPassphrase) {
+  /**
+   * @private {?string}
+   */
+  this.initPassphrase_ = initPassphrase;
+
+  /**
+   * @public {?string}
+   */
+  this.rememberedPassphrase = initPassphrase;
+};
 
 /**
- * Requests a passphrase from the user.
+ * Requests a passphrase from the user. If a passphrase was previously
+ * remembered, then tries it first. Otherwise shows a passphrase dialog.
  * @return {!Promise<string>}
  */
 unpacker.PassphraseManager.prototype.getPassphrase = function() {
   return new Promise(function(fulfill, reject) {
+    // For the first passphrase request try the init passphrase (which may be
+    // incorrect though, so do it only once).
+    if (this.initPassphrase_ != null) {
+      fulfill(this.initPassphrase_);
+      this.initPassphrase_ = null;
+      return;
+    }
+
+    // Ask user for a passphrase.
     chrome.app.window.create(
         '../html/passphrase.html',
         /** @type {!chrome.app.window.CreateWindowOptions} */ ({
@@ -32,8 +55,10 @@
           }.bind(this));
 
           passphraseWindow.contentWindow.onPassphraseSuccess = function(
-                                                                   passphrase) {
+                                                                   passphrase,
+                                                                   remember) {
             passphraseSucceeded = true;
+            this.rememberedPassphrase = remember ? passphrase : null;
             fulfill(passphrase);
           }.bind(this);
         }.bind(this));
diff --git a/chrome/browser/resources/chromeos/zip_archiver/manifest.json b/chrome/browser/resources/chromeos/zip_archiver/manifest.json
index 3478c1c..456a436 100644
--- a/chrome/browser/resources/chromeos/zip_archiver/manifest.json
+++ b/chrome/browser/resources/chromeos/zip_archiver/manifest.json
@@ -22,7 +22,8 @@
         "directory"
       ]
     },
-    "notifications"
+    "notifications",
+    "storage"
   ],
   "file_system_provider_capabilities": {
     "multipleMounts": true,