Bug 1258489 - Implement HTMLInputElement.webkitdirectory, r=smaug
☠☠ backed out by 7aba31d799f2 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Mon, 23 May 2016 17:02:18 +0200
changeset 323023 17e20404362d916a9034c6f67895748878e599dd
parent 323022 77c60a79313d1f2733e4b71a223e085f3086476f
child 323024 10a48aecf9947f3173641fffc1c390be0c28df99
push id9671
push userraliiev@mozilla.com
push dateMon, 06 Jun 2016 20:27:52 +0000
treeherdermozilla-aurora@cea65ca3d0bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1258489
milestone49.0a1
Bug 1258489 - Implement HTMLInputElement.webkitdirectory, r=smaug
dom/base/nsGkAtomList.h
dom/filesystem/Directory.cpp
dom/filesystem/Directory.h
dom/filesystem/tests/mochitest.ini
dom/filesystem/tests/test_webkitdirectory.html
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/webidl/Directory.webidl
dom/webidl/HTMLInputElement.webidl
testing/specialpowers/content/MockFilePicker.jsm
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -1299,16 +1299,17 @@ GK_ATOM(viewport_maximum_scale, "viewpor
 GK_ATOM(viewport_minimum_scale, "viewport-minimum-scale")
 GK_ATOM(viewport_user_scalable, "viewport-user-scalable")
 GK_ATOM(viewport_width, "viewport-width")
 GK_ATOM(visibility, "visibility")
 GK_ATOM(visuallyselected, "visuallyselected")
 GK_ATOM(vlink, "vlink")
 GK_ATOM(vspace, "vspace")
 GK_ATOM(wbr, "wbr")
+GK_ATOM(webkitdirectory, "webkitdirectory")
 GK_ATOM(when, "when")
 GK_ATOM(where, "where")
 GK_ATOM(widget, "widget")
 GK_ATOM(width, "width")
 GK_ATOM(window, "window")
 GK_ATOM(headerWindowTarget, "window-target")
 GK_ATOM(windowtype, "windowtype")
 GK_ATOM(withParam, "with-param")
--- a/dom/filesystem/Directory.cpp
+++ b/dom/filesystem/Directory.cpp
@@ -161,16 +161,31 @@ Directory::GetRoot(FileSystemBase* aFile
     return nullptr;
   }
 
   FileSystemPermissionRequest::RequestForTask(task);
   return task->GetPromise();
 }
 
 /* static */ already_AddRefed<Directory>
+Directory::Constructor(const GlobalObject& aGlobal,
+                       const nsAString& aRealPath,
+                       ErrorResult& aRv)
+{
+  nsCOMPtr<nsIFile> path;
+  aRv = NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(aRealPath),
+                              true, getter_AddRefs(path));
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
+  return Create(aGlobal.GetAsSupports(), path);
+}
+
+/* static */ already_AddRefed<Directory>
 Directory::Create(nsISupports* aParent, nsIFile* aFile,
                   FileSystemBase* aFileSystem)
 {
   MOZ_ASSERT(aParent);
   MOZ_ASSERT(aFile);
 
 #ifdef DEBUG
   bool isDir;
--- a/dom/filesystem/Directory.h
+++ b/dom/filesystem/Directory.h
@@ -58,16 +58,21 @@ public:
 
   static bool
   WebkitBlinkDirectoryPickerEnabled(JSContext* aCx, JSObject* aObj);
 
   static already_AddRefed<Promise>
   GetRoot(FileSystemBase* aFileSystem, ErrorResult& aRv);
 
   static already_AddRefed<Directory>
+  Constructor(const GlobalObject& aGlobal,
+              const nsAString& aRealPath,
+              ErrorResult& aRv);
+
+  static already_AddRefed<Directory>
   Create(nsISupports* aParent, nsIFile* aDirectory,
          FileSystemBase* aFileSystem = 0);
 
   // ========= Begin WebIDL bindings. ===========
 
   nsISupports*
   GetParentObject() const;
 
--- a/dom/filesystem/tests/mochitest.ini
+++ b/dom/filesystem/tests/mochitest.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 support-files =
   filesystem_commons.js
   script_fileList.js
   worker_basic.js
 
 [test_basic.html]
+[test_webkitdirectory.html]
 [test_worker_basic.html]
new file mode 100644
--- /dev/null
+++ b/dom/filesystem/tests/test_webkitdirectory.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for webkitdirectory and webkitRelativePath</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<input id="inputFileWebkitDirectory" type="file" webkitdirectory></input>
+<input id="inputFileWebkitDirectoryAndDirectory" type="file" webkitdirectory directory></input>
+<input id="inputFileDirectory" type="file" directory></input>
+
+<script type="application/javascript;version=1.7">
+
+function populateInputFile(aInputFile) {
+  var url = SimpleTest.getTestFileURL("script_fileList.js");
+  var script = SpecialPowers.loadChromeScript(url);
+
+  var MockFilePicker = SpecialPowers.MockFilePicker;
+  MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeOpen);
+
+  function onOpened(message) {
+    MockFilePicker.useDirectory(message.dir);
+
+    var input = document.getElementById(aInputFile);
+    input.addEventListener('change', function() {
+      MockFilePicker.cleanup();
+      script.destroy();
+      next();
+    });
+
+    input.click();
+  }
+
+  script.addMessageListener("dir.opened", onOpened);
+  script.sendAsyncMessage("dir.open", { path: 'test' });
+}
+
+function checkFile(file, fileList) {
+  for (var i = 0; i < fileList.length; ++i) {
+    ok(fileList[i] instanceof File, "We want just files.");
+    if (fileList[i].name == file.name) {
+      is(fileList[i].webkitRelativePath, file.path, "Path matches");
+      return;
+    }
+  }
+
+  ok(false, "File not found.");
+}
+
+function test_fileList(aInputFile, aWhat) {
+  var input = document.getElementById(aInputFile);
+  var fileList = input.files;
+
+  if (aWhat == null) {
+    is(fileList, null, "We want a null fileList for " + aInputFile);
+    next();
+    return;
+  }
+
+  is(fileList.length, aWhat.length, "We want just " + aWhat.length + " elements for " + aInputFile);
+  for (var i = 0; i < aWhat.length; ++i) {
+    checkFile(aWhat[i], fileList);
+  }
+
+  next();
+}
+
+function test_webkitdirectory_attribute() {
+  var a = document.createElement("input");
+  a.setAttribute("type", "file");
+
+  ok("webkitdirectory" in a, "HTMLInputElement.webkitdirectory exists");
+
+  ok(!a.hasAttribute("webkitdirectory"), "No webkitdirectory DOM attribute by default");
+  ok(!a.webkitdirectory, "No webkitdirectory attribute by default");
+
+  a.webkitdirectory = true;
+
+  ok(a.hasAttribute("webkitdirectory"), "Webkitdirectory DOM attribute is set");
+  ok(a.webkitdirectory, "Webkitdirectory attribute is set");
+
+  next();
+}
+
+function test_setup() {
+  SpecialPowers.pushPrefEnv({"set": [["dom.input.dirpicker", true],
+                                     ["dom.webkitBlink.dirPicker.enabled", true]]}, next);
+}
+
+var tests = [
+  test_setup,
+
+  function() { populateInputFile('inputFileWebkitDirectory'); },
+  function() { populateInputFile('inputFileWebkitDirectoryAndDirectory'); },
+  function() { populateInputFile('inputFileDirectory'); },
+
+  function() { test_fileList('inputFileWebkitDirectory', [ { name: 'foo.txt', path: '/foo.txt' },
+                                                           { name: 'bar.txt', path: '/subdir/bar.txt' }]); },
+  function() { test_fileList('inputFileWebkitDirectoryAndDirectory', [ { name: 'foo.txt', path: '/foo.txt' },
+                                                                       { name: 'bar.txt', path: '/subdir/bar.txt' }]); },
+  function() { test_fileList('inputFileDirectory', null); },
+
+  test_webkitdirectory_attribute,
+];
+
+function next() {
+  if (!tests.length) {
+    SimpleTest.finish();
+    return;
+  }
+
+  var test = tests.shift();
+  test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -213,16 +213,28 @@ const Decimal HTMLInputElement::kStepAny
   0x23e2,                                          \
   0x4479,                                          \
   {0xb5, 0x13, 0x7b, 0x36, 0x93, 0x43, 0xe3, 0xa0} \
 }
 
 #define PROGRESS_STR "progress"
 static const uint32_t kProgressEventInterval = 50; // ms
 
+class GetFilesCallback
+{
+public:
+  NS_INLINE_DECL_REFCOUNTING(GetFilesCallback);
+
+  virtual void
+  Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) = 0;
+
+protected:
+  virtual ~GetFilesCallback() {}
+};
+
 // Retrieving the list of files can be very time/IO consuming. We use this
 // helper class to do it just once.
 class GetFilesHelper final : public Runnable
 {
 public:
   static already_AddRefed<GetFilesHelper>
   Create(nsIGlobalObject* aGlobal,
          const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory,
@@ -290,16 +302,31 @@ public:
       mPromises.AppendElement(aPromise);
       return;
     }
 
     MOZ_ASSERT(mPromises.IsEmpty());
     ResolveOrRejectPromise(aPromise);
   }
 
+  void
+  AddCallback(GetFilesCallback* aCallback)
+  {
+    MOZ_ASSERT(aCallback);
+
+    // Still working.
+    if (!mListingCompleted) {
+      mCallbacks.AppendElement(aCallback);
+      return;
+    }
+
+    MOZ_ASSERT(mCallbacks.IsEmpty());
+    RunCallback(aCallback);
+  }
+
   // CC methods
   void Unlink()
   {
     mGlobal = nullptr;
     mFiles.Clear();
     mPromises.Clear();
   }
 
@@ -348,16 +375,24 @@ private:
     // Let's process the pending promises.
     nsTArray<RefPtr<Promise>> promises;
     promises.SwapElements(mPromises);
 
     for (uint32_t i = 0; i < promises.Length(); ++i) {
       ResolveOrRejectPromise(promises[i]);
     }
 
+    // Let's process the pending callbacks.
+    nsTArray<RefPtr<GetFilesCallback>> callbacks;
+    callbacks.SwapElements(mCallbacks);
+
+    for (uint32_t i = 0; i < callbacks.Length(); ++i) {
+      RunCallback(callbacks[i]);
+    }
+
     return NS_OK;
   }
 
   void
   RunIO()
   {
     MOZ_ASSERT(!NS_IsMainThread());
     MOZ_ASSERT(!mDirectoryPath.IsEmpty());
@@ -502,16 +537,26 @@ private:
     if (NS_FAILED(mErrorResult)) {
       aPromise->MaybeReject(mErrorResult);
       return;
     }
 
     aPromise->MaybeResolve(mFiles);
   }
 
+  void
+  RunCallback(GetFilesCallback* aCallback)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mListingCompleted);
+    MOZ_ASSERT(aCallback);
+
+    aCallback->Callback(mErrorResult, mFiles);
+  }
+
   nsCOMPtr<nsIGlobalObject> mGlobal;
 
   bool mRecursiveFlag;
   bool mListingCompleted;
   nsString mDirectoryPath;
 
   // We populate this array in the I/O thread with the paths of the Files that
   // we want to send as result to the promise objects.
@@ -523,16 +568,88 @@ private:
 
   // This is the real File sequence that we expose via Promises.
   Sequence<RefPtr<File>> mFiles;
 
   // Error code to propagate.
   nsresult mErrorResult;
 
   nsTArray<RefPtr<Promise>> mPromises;
+  nsTArray<RefPtr<GetFilesCallback>> mCallbacks;
+};
+
+// An helper class for the dispatching of the 'change' event.
+class DispatchChangeEventCallback final : public GetFilesCallback
+{
+public:
+  explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement)
+    : mInputElement(aInputElement)
+  {
+    MOZ_ASSERT(aInputElement);
+  }
+
+  virtual void
+  Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) override
+  {
+    nsTArray<OwningFileOrDirectory> array;
+    for (uint32_t i = 0; i < aFiles.Length(); ++i) {
+      OwningFileOrDirectory* element = array.AppendElement();
+      element->SetAsFile() = aFiles[i];
+    }
+
+    mInputElement->SetFilesOrDirectories(array, true);
+    NS_WARN_IF(NS_FAILED(DispatchEvents()));
+  }
+
+  nsresult
+  DispatchEvents()
+  {
+    nsresult rv = NS_OK;
+    rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
+                                              static_cast<nsIDOMHTMLInputElement*>(mInputElement.get()),
+                                              NS_LITERAL_STRING("input"), true,
+                                              false);
+    NS_WARN_IF(NS_FAILED(rv));
+
+    rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
+                                              static_cast<nsIDOMHTMLInputElement*>(mInputElement.get()),
+                                              NS_LITERAL_STRING("change"), true,
+                                              false);
+
+    return rv;
+  }
+
+private:
+  RefPtr<HTMLInputElement> mInputElement;
+};
+
+// This callback is used for postponing the calling of SetFilesOrDirectories
+// when the exploration of the directory is completed.
+class AfterSetFilesOrDirectoriesCallback : public GetFilesCallback
+{
+public:
+  AfterSetFilesOrDirectoriesCallback(HTMLInputElement* aInputElement,
+                                     bool aSetValueChanged)
+    : mInputElement(aInputElement)
+    , mSetValueChanged(aSetValueChanged)
+  {
+    MOZ_ASSERT(aInputElement);
+  }
+
+  void
+  Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) override
+  {
+    if (NS_SUCCEEDED(aStatus)) {
+      mInputElement->AfterSetFilesOrDirectoriesInternal(mSetValueChanged);
+    }
+  }
+
+private:
+  RefPtr<HTMLInputElement> mInputElement;
+  bool mSetValueChanged;
 };
 
 class HTMLInputElementState final : public nsISupports
 {
   public:
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_INPUT_ELEMENT_STATE_IID)
     NS_DECL_ISUPPORTS
 
@@ -857,29 +974,32 @@ HTMLInputElement::nsFilePickerShownCallb
       mInput->OwnerDoc(), lastUsedDir);
   }
 
   // The text control frame (if there is one) isn't going to send a change
   // event because it will think this is done by a script.
   // So, we can safely send one by ourself.
   mInput->SetFilesOrDirectories(newFilesOrDirectories, true);
 
-  nsresult rv = NS_OK;
-  rv = nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(),
-                                            static_cast<nsIDOMHTMLInputElement*>(mInput.get()),
-                                            NS_LITERAL_STRING("input"), true,
-                                            false);
-  NS_WARN_IF(NS_FAILED(rv));
-
-  rv = nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(),
-                                            static_cast<nsIDOMHTMLInputElement*>(mInput.get()),
-                                            NS_LITERAL_STRING("change"), true,
-                                            false);
-
-  return rv;
+  RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
+    new DispatchChangeEventCallback(mInput);
+
+  if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+      mInput->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
+    ErrorResult error;
+    GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
+    }
+
+    helper->AddCallback(dispatchChangeEventCallback);
+    return NS_OK;
+  }
+
+  return dispatchChangeEventCallback->DispatchEvents();
 }
 
 NS_IMPL_ISUPPORTS(HTMLInputElement::nsFilePickerShownCallback,
                   nsIFilePickerShownCallback)
 
 class nsColorPickerShownCallback final
   : public nsIColorPickerShownCallback
 {
@@ -2913,16 +3033,29 @@ HTMLInputElement::SetFiles(nsIDOMFileLis
   }
 
   AfterSetFilesOrDirectories(aSetValueChanged);
 }
 
 void
 HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged)
 {
+  if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+      HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
+    // This will call AfterSetFilesOrDirectoriesInternal eventually.
+    ExploreDirectoryRecursively(aSetValueChanged);
+    return;
+  }
+
+  AfterSetFilesOrDirectoriesInternal(aSetValueChanged);
+}
+
+void
+HTMLInputElement::AfterSetFilesOrDirectoriesInternal(bool aSetValueChanged)
+{
   // No need to flush here, if there's no frame at this point we
   // don't need to force creation of one just to tell it about this
   // new value.  We just want the display to update as needed.
   nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
   if (formControlFrame) {
     nsAutoString readableValue;
     GetDisplayFileName(readableValue);
     formControlFrame->SetFormProperty(nsGkAtoms::value, readableValue);
@@ -2975,17 +3108,19 @@ HTMLInputElement::FireChangeEventIfNeede
 FileList*
 HTMLInputElement::GetFiles()
 {
   if (mType != NS_FORM_INPUT_FILE) {
     return nullptr;
   }
 
   if (Preferences::GetBool("dom.input.dirpicker", false) &&
-      HasAttr(kNameSpaceID_None, nsGkAtoms::directory)) {
+      HasAttr(kNameSpaceID_None, nsGkAtoms::directory) &&
+      (!Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) ||
+       !HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
     return nullptr;
   }
 
   if (!mFileList) {
     mFileList = new FileList(static_cast<nsIContent*>(this));
     UpdateFileList();
   }
 
@@ -4065,18 +4200,20 @@ HTMLInputElement::MaybeInitPickers(Event
     // If the user clicked on the "Choose folder..." button we open the
     // directory picker, else we open the file picker.
     FilePickerType type = FILE_PICKER_FILE;
     nsCOMPtr<nsIContent> target =
       do_QueryInterface(aVisitor.mEvent->mOriginalTarget);
     if (target &&
         target->GetParent() == this &&
         target->IsRootOfNativeAnonymousSubtree() &&
-        target->HasAttr(kNameSpaceID_None, nsGkAtoms::directory)) {
-      MOZ_ASSERT(Preferences::GetBool("dom.input.dirpicker", false),
+        (target->HasAttr(kNameSpaceID_None, nsGkAtoms::directory) ||
+         target->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
+      MOZ_ASSERT(Preferences::GetBool("dom.input.dirpicker", false) ||
+                 Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false),
                  "No API or UI should have been exposed to allow this code to "
                  "be reached");
       type = FILE_PICKER_DIRECTORY;
     }
     return InitFilePicker(type);
   }
   if (mType == NS_FORM_INPUT_COLOR) {
     return InitColorPicker();
@@ -5276,17 +5413,18 @@ nsChangeHint
 HTMLInputElement::GetAttributeChangeHint(const nsIAtom* aAttribute,
                                          int32_t aModType) const
 {
   nsChangeHint retval =
     nsGenericHTMLFormElementWithState::GetAttributeChangeHint(aAttribute, aModType);
   if (aAttribute == nsGkAtoms::type ||
       // The presence or absence of the 'directory' attribute determines what
       // buttons we show for type=file.
-      aAttribute == nsGkAtoms::directory) {
+      aAttribute == nsGkAtoms::directory ||
+      aAttribute == nsGkAtoms::webkitdirectory) {
     retval |= NS_STYLE_HINT_FRAMECHANGE;
   } else if (mType == NS_FORM_INPUT_IMAGE &&
              (aAttribute == nsGkAtoms::alt ||
               aAttribute == nsGkAtoms::value)) {
     // We might need to rebuild our alt text.  Just go ahead and
     // reconstruct our frame.  This should be quite rare..
     retval |= NS_STYLE_HINT_FRAMECHANGE;
   } else if (aAttribute == nsGkAtoms::value) {
@@ -5412,51 +5550,28 @@ HTMLInputElement::GetFilesAndDirectories
 already_AddRefed<Promise>
 HTMLInputElement::GetFiles(bool aRecursiveFlag, ErrorResult& aRv)
 {
   if (mType != NS_FORM_INPUT_FILE) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
+  GetFilesHelper* helper = GetOrCreateGetFilesHelper(aRecursiveFlag, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+   }
+  MOZ_ASSERT(helper);
+
   nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
   MOZ_ASSERT(global);
   if (!global) {
     return nullptr;
   }
 
-  RefPtr<GetFilesHelper> helper;
-  if (aRecursiveFlag) {
-    if (!mGetFilesRecursiveHelper) {
-      mGetFilesRecursiveHelper =
-       GetFilesHelper::Create(global,
-                              GetFilesOrDirectoriesInternal(),
-                              aRecursiveFlag, aRv);
-      if (NS_WARN_IF(aRv.Failed())) {
-        return nullptr;
-      }
-    }
-
-    helper = mGetFilesRecursiveHelper;
-  } else {
-    if (!mGetFilesNonRecursiveHelper) {
-      mGetFilesNonRecursiveHelper =
-       GetFilesHelper::Create(global,
-                              GetFilesOrDirectoriesInternal(),
-                              aRecursiveFlag, aRv);
-      if (NS_WARN_IF(aRv.Failed())) {
-        return nullptr;
-      }
-    }
-
-    helper = mGetFilesNonRecursiveHelper;
-  }
-
-  MOZ_ASSERT(helper);
-
   RefPtr<Promise> p = Promise::Create(global, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
   helper->AddPromise(p);
   return p.forget();
 }
@@ -7967,12 +8082,66 @@ HTMLInputElement::ClearGetFilesHelpers()
   }
 
   if (mGetFilesNonRecursiveHelper) {
     mGetFilesNonRecursiveHelper->Unlink();
     mGetFilesNonRecursiveHelper = nullptr;
   }
 }
 
+GetFilesHelper*
+HTMLInputElement::GetOrCreateGetFilesHelper(bool aRecursiveFlag,
+                                            ErrorResult& aRv)
+{
+  nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+  MOZ_ASSERT(global);
+  if (!global) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  if (aRecursiveFlag) {
+    if (!mGetFilesRecursiveHelper) {
+      mGetFilesRecursiveHelper =
+       GetFilesHelper::Create(global,
+                              GetFilesOrDirectoriesInternal(),
+                              aRecursiveFlag, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return nullptr;
+      }
+    }
+
+    return mGetFilesRecursiveHelper;
+  }
+
+  if (!mGetFilesNonRecursiveHelper) {
+    mGetFilesNonRecursiveHelper =
+     GetFilesHelper::Create(global,
+                            GetFilesOrDirectoriesInternal(),
+                            aRecursiveFlag, aRv);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return nullptr;
+    }
+  }
+
+  return mGetFilesNonRecursiveHelper;
+}
+
+void
+HTMLInputElement::ExploreDirectoryRecursively(bool aSetValueChanged)
+{
+  ErrorResult rv;
+  GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */,
+                                                     rv);
+  if (NS_WARN_IF(rv.Failed())) {
+    AfterSetFilesOrDirectoriesInternal(aSetValueChanged);
+    return;
+  }
+
+  RefPtr<AfterSetFilesOrDirectoriesCallback> callback =
+    new AfterSetFilesOrDirectoriesCallback(this, aSetValueChanged);
+  helper->AddCallback(callback);
+}
+
 } // namespace dom
 } // namespace mozilla
 
 #undef NS_ORIGINAL_CHECKED_VALUE
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -32,17 +32,19 @@ class nsIRadioVisitor;
 
 namespace mozilla {
 
 class EventChainPostVisitor;
 class EventChainPreVisitor;
 
 namespace dom {
 
+class AfterSetFilesOrDirectoriesRunnable;
 class Date;
+class DispatchChangeEventCallback;
 class File;
 class FileList;
 class GetFilesHelper;
 
 /**
  * A class we use to create a singleton object that is used to keep track of
  * the last directory from which the user has picked files (via
  * <input type=file>) on a per-domain basis. The implementation uses
@@ -102,16 +104,19 @@ public:
 class HTMLInputElement final : public nsGenericHTMLFormElementWithState,
                                public nsImageLoadingContent,
                                public nsIDOMHTMLInputElement,
                                public nsITextControlElement,
                                public nsIPhonetic,
                                public nsIDOMNSEditableElement,
                                public nsIConstraintValidation
 {
+  friend class AfterSetFilesOrDirectoriesCallback;
+  friend class DispatchChangeEventCallback;
+
 public:
   using nsIConstraintValidation::GetValidationMessage;
   using nsIConstraintValidation::CheckValidity;
   using nsIConstraintValidation::ReportValidity;
   using nsIConstraintValidation::WillValidate;
   using nsIConstraintValidation::Validity;
   using nsGenericHTMLFormElementWithState::GetForm;
 
@@ -695,16 +700,26 @@ public:
     return HasAttr(kNameSpaceID_None, nsGkAtoms::directory);
   }
 
   void SetDirectoryAttr(bool aValue, ErrorResult& aRv)
   {
     SetHTMLBoolAttr(nsGkAtoms::directory, aValue, aRv);
   }
 
+  bool WebkitDirectoryAttr() const
+  {
+    return HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory);
+  }
+
+  void SetWebkitDirectoryAttr(bool aValue, ErrorResult& aRv)
+  {
+    SetHTMLBoolAttr(nsGkAtoms::webkitdirectory, aValue, aRv);
+  }
+
   bool IsFilesAndDirectoriesSupported() const;
 
   already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv);
 
   already_AddRefed<Promise> GetFiles(bool aRecursiveFlag, ErrorResult& aRv);
 
   void ChooseDirectory(ErrorResult& aRv);
 
@@ -933,18 +948,26 @@ protected:
 
   /**
    * Update mFileList with the currently selected file.
    */
   void UpdateFileList();
 
   /**
    * Called after calling one of the SetFilesOrDirectories() functions.
+   * This method can explore the directory recursively if needed.
    */
   void AfterSetFilesOrDirectories(bool aSetValueChanged);
+  void AfterSetFilesOrDirectoriesInternal(bool aSetValueChanged);
+
+  /**
+   * Recursively explore the directory and populate mFileOrDirectories correctly
+   * for webkitdirectory.
+   */
+  void ExploreDirectoryRecursively(bool aSetValuechanged);
 
   /**
    * Determine whether the editor needs to be initialized explicitly for
    * a particular event.
    */
   bool NeedToInitializeEditorForEvent(EventChainPreVisitor& aVisitor) const;
 
   /**
@@ -1251,16 +1274,19 @@ protected:
    * Use this function before trying to open a picker.
    * It checks if the page is allowed to open a new pop-up.
    * If it returns true, you should not create the picker.
    *
    * @return true if popup should be blocked, false otherwise
    */
   bool IsPopupBlocked() const;
 
+  GetFilesHelper* GetOrCreateGetFilesHelper(bool aRecursiveFlag,
+                                            ErrorResult& aRv);
+
   void ClearGetFilesHelpers();
 
   nsCOMPtr<nsIControllers> mControllers;
 
   /*
    * In mInputData, the mState field is used if IsSingleLineTextControl returns
    * true and mValue is used otherwise.  We have to be careful when handling it
    * on a type change.
--- a/dom/webidl/Directory.webidl
+++ b/dom/webidl/Directory.webidl
@@ -10,17 +10,20 @@
  * path should be a descendent path like "path/to/file.txt" and not contain a
  * segment of ".." or ".". So the paths aren't allowed to walk up the directory
  * tree. For example, paths like "../foo", "..", "/foo/bar" or "foo/../bar" are
  * not allowed.
  *
  * http://w3c.github.io/filesystem-api/#idl-def-Directory
  * https://microsoftedge.github.io/directory-upload/proposal.html#directory-interface
  */
-[Exposed=(Window,Worker)]
+
+// This chromeConstructor is used by the MockFilePicker for testing only.
+[ChromeConstructor(DOMString path),
+ Exposed=(Window,Worker)]
 interface Directory {
   /*
    * The leaf name of the directory.
    */
   [Throws]
   readonly attribute DOMString name;
 
   /*
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -204,16 +204,19 @@ partial interface HTMLInputElement {
   [Throws, Pref="dom.input.dirpicker"]
   Promise<sequence<(File or Directory)>> getFilesAndDirectories();
 
   [Throws, Pref="dom.input.dirpicker"]
   Promise<sequence<File>> getFiles(optional boolean recursiveFlag = false);
 
   [Throws, Pref="dom.input.dirpicker"]
   void chooseDirectory();
+
+  [Pref="dom.webkitBlink.dirPicker.enabled", BinaryName="WebkitDirectoryAttr", SetterThrows]
+  attribute boolean webkitdirectory;
 };
 
 [NoInterfaceObject]
 interface MozPhonetic {
   [Pure, ChromeOnly]
   readonly attribute DOMString phonetic;
 };
 
--- a/testing/specialpowers/content/MockFilePicker.jsm
+++ b/testing/specialpowers/content/MockFilePicker.jsm
@@ -98,16 +98,21 @@ this.MockFilePicker = {
   },
 
   useBlobFile: function() {
     var blob = new this.window.Blob([]);
     var file = new this.window.File([blob], 'helloworld.txt', { type: 'plain/text' });
     this.returnFiles = [file];
   },
 
+  useDirectory: function(aPath) {
+    var directory = new this.window.Directory(aPath);
+    this.returnFiles = [directory];
+  },
+
   isNsIFile: function(aFile) {
     let ret = false;
     try {
       if (aFile.QueryInterface(Ci.nsIFile))
         ret = true;
     } catch(e) {}
 
     return ret;