Bug 1258489 - Implement HTMLInputElement.webkitdirectory, r=smaug
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 01 Jun 2016 12:29:00 +0200
changeset 299946 133af19777be91b1497c5ac19402d8f12349de52
parent 299945 6b013082eb56784fe35640a8b6f4e596da843c8b
child 299947 045cdefdb9410e0ed47cb479d410700d522ca7ba
push id30305
push usercbook@mozilla.com
push dateThu, 02 Jun 2016 09:56:32 +0000
treeherdermozilla-central@34a8be4346a9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1258489
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
@@ -1304,16 +1304,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());
@@ -505,16 +540,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.
@@ -526,16 +571,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
 
@@ -860,29 +977,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
 {
@@ -2916,16 +3036,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);
@@ -2978,17 +3111,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();
   }
 
@@ -4068,18 +4203,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();
@@ -5279,17 +5416,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) {
@@ -5415,51 +5553,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();
 }
@@ -7970,12 +8085,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;