Add experimental JavaScript retrieval APIs.

Add the following set of APIs to retrieve the JavaScript code from
actions in a document's name tree:

- FPDFDoc_GetJavaScriptActionCount()
- FPDFDoc_GetJavaScriptAction()
- FPDFDoc_CloseJavaScriptAction()
- FPDFJavaScriptAction_GetName()
- FPDFJavaScriptAction_GetScript()

This also adds the FPDF_JAVASCRIPT_ACTION type and
ScopedFPDFJavaScriptAction to help manage the lifetime of
FPDF_JAVASCRIPT_ACTION.

This is implemented on top of CPDF_Action, and improves validation in
CPDF_Action to better follow the spec.

Bug: pdfium:1253
Change-Id: I2b3ba83bb4f661e6c996c9d057fbf7d3a0c20e70
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/55750
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index bf20f6e..3ce1e08 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -120,6 +120,7 @@
     "public/fpdf_flatten.h",
     "public/fpdf_formfill.h",
     "public/fpdf_fwlevent.h",
+    "public/fpdf_javascript.h",
     "public/fpdf_ppo.h",
     "public/fpdf_progressive.h",
     "public/fpdf_save.h",
diff --git a/core/fpdfdoc/cpdf_action.cpp b/core/fpdfdoc/cpdf_action.cpp
index 18e4927..74a7c6f 100644
--- a/core/fpdfdoc/cpdf_action.cpp
+++ b/core/fpdfdoc/cpdf_action.cpp
@@ -10,6 +10,7 @@
 #include "core/fpdfapi/parser/cpdf_array.h"
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfapi/parser/cpdf_document.h"
+#include "core/fpdfapi/parser/cpdf_name.h"
 #include "core/fpdfdoc/cpdf_filespec.h"
 #include "core/fpdfdoc/cpdf_nametree.h"
 
@@ -33,6 +34,14 @@
   if (!m_pDict)
     return Unknown;
 
+  // Validate |m_pDict|. Type is optional, but must be valid if present.
+  const CPDF_Object* pType = m_pDict->GetObjectFor("Type");
+  if (pType) {
+    const CPDF_Name* pName = pType->AsName();
+    if (!pName || pName->GetString() != "Action")
+      return Unknown;
+  }
+
   ByteString csType = m_pDict->GetStringFor("S");
   if (csType.IsEmpty())
     return Unknown;
@@ -115,13 +124,16 @@
   return m_pDict->GetIntegerFor("Flags");
 }
 
-WideString CPDF_Action::GetJavaScript() const {
-  if (!m_pDict)
-    return WideString();
+Optional<WideString> CPDF_Action::MaybeGetJavaScript() const {
+  const CPDF_Object* pObject = GetJavaScriptObject();
+  if (!pObject)
+    return pdfium::nullopt;
+  return pObject->GetUnicodeText();
+}
 
-  const CPDF_Object* pJS = m_pDict->GetDirectObjectFor("JS");
-  return (pJS && (pJS->IsString() || pJS->IsStream())) ? pJS->GetUnicodeText()
-                                                       : WideString();
+WideString CPDF_Action::GetJavaScript() const {
+  const CPDF_Object* pObject = GetJavaScriptObject();
+  return pObject ? pObject->GetUnicodeText() : WideString();
 }
 
 size_t CPDF_Action::GetSubActionsCount() const {
@@ -150,3 +162,11 @@
   }
   return CPDF_Action(nullptr);
 }
+
+const CPDF_Object* CPDF_Action::GetJavaScriptObject() const {
+  if (!m_pDict)
+    return nullptr;
+
+  const CPDF_Object* pJS = m_pDict->GetDirectObjectFor("JS");
+  return (pJS && (pJS->IsString() || pJS->IsStream())) ? pJS : nullptr;
+}
diff --git a/core/fpdfdoc/cpdf_action.h b/core/fpdfdoc/cpdf_action.h
index ccc40aa..391f175 100644
--- a/core/fpdfdoc/cpdf_action.h
+++ b/core/fpdfdoc/cpdf_action.h
@@ -10,9 +10,11 @@
 #include "core/fpdfdoc/cpdf_dest.h"
 #include "core/fxcrt/fx_string.h"
 #include "core/fxcrt/retain_ptr.h"
+#include "third_party/base/optional.h"
 
 class CPDF_Dictionary;
 class CPDF_Document;
+class CPDF_Object;
 
 class CPDF_Action {
  public:
@@ -51,11 +53,18 @@
   bool GetHideStatus() const;
   ByteString GetNamedAction() const;
   uint32_t GetFlags() const;
+
+  // Differentiates between empty JS entry and no JS entry.
+  Optional<WideString> MaybeGetJavaScript() const;
+  // Returns empty string for empty JS entry and no JS entry.
   WideString GetJavaScript() const;
+
   size_t GetSubActionsCount() const;
   CPDF_Action GetSubAction(size_t iIndex) const;
 
  private:
+  const CPDF_Object* GetJavaScriptObject() const;
+
   RetainPtr<const CPDF_Dictionary> const m_pDict;
 };
 
diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn
index 62df18b..463d5ca 100644
--- a/fpdfsdk/BUILD.gn
+++ b/fpdfsdk/BUILD.gn
@@ -58,6 +58,7 @@
     "fpdf_ext.cpp",
     "fpdf_flatten.cpp",
     "fpdf_formfill.cpp",
+    "fpdf_javascript.cpp",
     "fpdf_ppo.cpp",
     "fpdf_progressive.cpp",
     "fpdf_save.cpp",
@@ -149,6 +150,7 @@
     "fpdf_ext_embeddertest.cpp",
     "fpdf_flatten_embeddertest.cpp",
     "fpdf_formfill_embeddertest.cpp",
+    "fpdf_javascript_embeddertest.cpp",
     "fpdf_ppo_embeddertest.cpp",
     "fpdf_save_embeddertest.cpp",
     "fpdf_searchex_embeddertest.cpp",
diff --git a/fpdfsdk/cpdfsdk_helpers.h b/fpdfsdk/cpdfsdk_helpers.h
index 8eca854..a06cc39 100644
--- a/fpdfsdk/cpdfsdk_helpers.h
+++ b/fpdfsdk/cpdfsdk_helpers.h
@@ -42,6 +42,7 @@
 class CPDFSDK_InteractiveForm;
 class IPDFSDK_PauseAdapter;
 class FX_PATHPOINT;
+struct CPDF_JavaScript;
 
 #ifdef PDF_ENABLE_XFA
 class CPDFXFA_Context;
@@ -117,6 +118,15 @@
   return reinterpret_cast<CPDF_Font*>(font);
 }
 
+inline FPDF_JAVASCRIPT_ACTION FPDFJavaScriptActionFromCPDFJavaScriptAction(
+    CPDF_JavaScript* javascript) {
+  return reinterpret_cast<FPDF_JAVASCRIPT_ACTION>(javascript);
+}
+inline CPDF_JavaScript* CPDFJavaScriptActionFromFPDFJavaScriptAction(
+    FPDF_JAVASCRIPT_ACTION javascript) {
+  return reinterpret_cast<CPDF_JavaScript*>(javascript);
+}
+
 inline FPDF_LINK FPDFLinkFromCPDFDictionary(CPDF_Dictionary* link) {
   return reinterpret_cast<FPDF_LINK>(link);
 }
diff --git a/fpdfsdk/fpdf_javascript.cpp b/fpdfsdk/fpdf_javascript.cpp
new file mode 100644
index 0000000..c2d119b
--- /dev/null
+++ b/fpdfsdk/fpdf_javascript.cpp
@@ -0,0 +1,85 @@
+// Copyright 2019 PDFium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "public/fpdf_javascript.h"
+
+#include <memory>
+
+#include "core/fpdfapi/parser/cpdf_dictionary.h"
+#include "core/fpdfapi/parser/cpdf_document.h"
+#include "core/fpdfdoc/cpdf_action.h"
+#include "core/fpdfdoc/cpdf_nametree.h"
+#include "fpdfsdk/cpdfsdk_helpers.h"
+#include "third_party/base/ptr_util.h"
+
+struct CPDF_JavaScript {
+  WideString name;
+  WideString script;
+};
+
+FPDF_EXPORT int FPDF_CALLCONV
+FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document) {
+  CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document);
+  return doc ? CPDF_NameTree(doc, "JavaScript").GetCount() : -1;
+}
+
+FPDF_EXPORT FPDF_JAVASCRIPT_ACTION FPDF_CALLCONV
+FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index) {
+  CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document);
+  if (!doc || index < 0)
+    return nullptr;
+
+  CPDF_NameTree name_tree(doc, "JavaScript");
+  if (static_cast<size_t>(index) >= name_tree.GetCount())
+    return nullptr;
+
+  WideString name;
+  CPDF_Dictionary* obj =
+      ToDictionary(name_tree.LookupValueAndName(index, &name));
+  if (!obj)
+    return nullptr;
+
+  // Validate |obj|. Type is optional, but must be valid if present.
+  CPDF_Action action(obj);
+  if (action.GetType() != CPDF_Action::JavaScript)
+    return nullptr;
+
+  Optional<WideString> script = action.MaybeGetJavaScript();
+  if (!script.has_value())
+    return nullptr;
+
+  auto js = pdfium::MakeUnique<CPDF_JavaScript>();
+  js->name = name;
+  js->script = script.value();
+  return FPDFJavaScriptActionFromCPDFJavaScriptAction(js.release());
+}
+
+FPDF_EXPORT void FPDF_CALLCONV
+FPDFDoc_CloseJavaScriptAction(FPDF_JAVASCRIPT_ACTION javascript) {
+  // Take object back across API and destroy it.
+  std::unique_ptr<CPDF_JavaScript>(
+      CPDFJavaScriptActionFromFPDFJavaScriptAction(javascript));
+}
+
+FPDF_EXPORT unsigned long FPDF_CALLCONV
+FPDFJavaScriptAction_GetName(FPDF_JAVASCRIPT_ACTION javascript,
+                             FPDF_WCHAR* buffer,
+                             unsigned long buflen) {
+  CPDF_JavaScript* js =
+      CPDFJavaScriptActionFromFPDFJavaScriptAction(javascript);
+  if (!js)
+    return 0;
+  return Utf16EncodeMaybeCopyAndReturnLength(js->name, buffer, buflen);
+}
+
+FPDF_EXPORT unsigned long FPDF_CALLCONV
+FPDFJavaScriptAction_GetScript(FPDF_JAVASCRIPT_ACTION javascript,
+                               FPDF_WCHAR* buffer,
+                               unsigned long buflen) {
+  CPDF_JavaScript* js =
+      CPDFJavaScriptActionFromFPDFJavaScriptAction(javascript);
+  if (!js)
+    return 0;
+  return Utf16EncodeMaybeCopyAndReturnLength(js->script, buffer, buflen);
+}
diff --git a/fpdfsdk/fpdf_javascript_embeddertest.cpp b/fpdfsdk/fpdf_javascript_embeddertest.cpp
new file mode 100644
index 0000000..35deb69
--- /dev/null
+++ b/fpdfsdk/fpdf_javascript_embeddertest.cpp
@@ -0,0 +1,130 @@
+// Copyright 2019 PDFium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "core/fxcrt/fx_memory.h"
+#include "public/fpdf_javascript.h"
+#include "public/fpdfview.h"
+#include "testing/embedder_test.h"
+#include "testing/fx_string_testhelpers.h"
+#include "testing/utils/hash.h"
+
+class FPDFJavaScriptEmbedderTest : public EmbedderTest {};
+
+TEST_F(FPDFJavaScriptEmbedderTest, CountJS) {
+  // Open a file with JS.
+  ASSERT_TRUE(OpenDocument("bug_679649.pdf"));
+  EXPECT_EQ(1, FPDFDoc_GetJavaScriptActionCount(document()));
+}
+
+TEST_F(FPDFJavaScriptEmbedderTest, CountNoJS) {
+  // Open a file without JS.
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  EXPECT_EQ(0, FPDFDoc_GetJavaScriptActionCount(document()));
+
+  // Provide no document.
+  EXPECT_EQ(-1, FPDFDoc_GetJavaScriptActionCount(nullptr));
+}
+
+TEST_F(FPDFJavaScriptEmbedderTest, GetJS) {
+  ASSERT_TRUE(OpenDocument("js.pdf"));
+  EXPECT_EQ(6, FPDFDoc_GetJavaScriptActionCount(document()));
+
+  ScopedFPDFJavaScriptAction js;
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), -1));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 6));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(nullptr, -1));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(nullptr, 0));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(nullptr, 1));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(nullptr, 2));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(nullptr, 5));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(nullptr, 6));
+  EXPECT_FALSE(js);
+
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 0));
+  EXPECT_TRUE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 1));
+  EXPECT_TRUE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 2));
+  EXPECT_TRUE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 3));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 4));
+  EXPECT_FALSE(js);
+  js.reset(FPDFDoc_GetJavaScriptAction(document(), 5));
+  EXPECT_FALSE(js);
+}
+
+TEST_F(FPDFJavaScriptEmbedderTest, GetJSName) {
+  ASSERT_TRUE(OpenDocument("bug_679649.pdf"));
+  ScopedFPDFJavaScriptAction js(FPDFDoc_GetJavaScriptAction(document(), 0));
+  ASSERT_TRUE(js);
+
+  {
+    FPDF_WCHAR buf[10];
+    EXPECT_EQ(0u, FPDFJavaScriptAction_GetName(nullptr, nullptr, 0));
+    EXPECT_EQ(0u, FPDFJavaScriptAction_GetName(nullptr, buf, 0));
+    EXPECT_EQ(0u, FPDFJavaScriptAction_GetName(nullptr, buf, sizeof(buf)));
+  }
+
+  constexpr size_t kExpectedLength = 22;
+  ASSERT_EQ(kExpectedLength,
+            FPDFJavaScriptAction_GetName(js.get(), nullptr, 0));
+
+  // Check that the name not returned if the buffer is too small.
+  // The result buffer should be overwritten with an empty string.
+  std::vector<FPDF_WCHAR> buf = GetFPDFWideStringBuffer(kExpectedLength);
+  // Write in the buffer to verify it's not overwritten.
+  memcpy(buf.data(), "abcdefgh", 8);
+  EXPECT_EQ(kExpectedLength, FPDFJavaScriptAction_GetName(js.get(), buf.data(),
+                                                          kExpectedLength - 1));
+  EXPECT_EQ(0, memcmp(buf.data(), "abcdefgh", 8));
+
+  EXPECT_EQ(kExpectedLength, FPDFJavaScriptAction_GetName(js.get(), buf.data(),
+                                                          kExpectedLength));
+  EXPECT_EQ(L"startDelay", GetPlatformWString(buf.data()));
+}
+
+TEST_F(FPDFJavaScriptEmbedderTest, GetJSScript) {
+  ASSERT_TRUE(OpenDocument("bug_679649.pdf"));
+  ScopedFPDFJavaScriptAction js(FPDFDoc_GetJavaScriptAction(document(), 0));
+  ASSERT_TRUE(js);
+
+  {
+    FPDF_WCHAR buf[10];
+    EXPECT_EQ(0u, FPDFJavaScriptAction_GetScript(nullptr, nullptr, 0));
+    EXPECT_EQ(0u, FPDFJavaScriptAction_GetScript(nullptr, buf, 0));
+    EXPECT_EQ(0u, FPDFJavaScriptAction_GetScript(nullptr, buf, sizeof(buf)));
+  }
+
+  constexpr size_t kExpectedLength = 218;
+  ASSERT_EQ(kExpectedLength,
+            FPDFJavaScriptAction_GetScript(js.get(), nullptr, 0));
+
+  // Check that the string value of an AP is not returned if the buffer is too
+  // small. The result buffer should be overwritten with an empty string.
+  std::vector<FPDF_WCHAR> buf = GetFPDFWideStringBuffer(kExpectedLength);
+  // Write in the buffer to verify it's not overwritten.
+  memcpy(buf.data(), "abcdefgh", 8);
+  EXPECT_EQ(kExpectedLength, FPDFJavaScriptAction_GetScript(
+                                 js.get(), buf.data(), kExpectedLength - 1));
+  EXPECT_EQ(0, memcmp(buf.data(), "abcdefgh", 8));
+
+  static const wchar_t kExpectedScript[] =
+      L"function ping() {\n  app.alert(\"ping\");\n}\n"
+      L"var timer = app.setTimeOut(\"ping()\", 100);\napp.clearTimeOut(timer);";
+  EXPECT_EQ(kExpectedLength, FPDFJavaScriptAction_GetScript(
+                                 js.get(), buf.data(), kExpectedLength));
+  EXPECT_EQ(kExpectedScript, GetPlatformWString(buf.data()));
+}
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index cf1c14f..396b6d9 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -19,6 +19,7 @@
 #include "public/fpdf_flatten.h"
 #include "public/fpdf_formfill.h"
 #include "public/fpdf_fwlevent.h"
+#include "public/fpdf_javascript.h"
 #include "public/fpdf_ppo.h"
 #include "public/fpdf_progressive.h"
 #include "public/fpdf_save.h"
@@ -271,6 +272,13 @@
     CHK(FPDF_SetFormFieldHighlightAlpha);
     CHK(FPDF_SetFormFieldHighlightColor);
 
+    // fpdf_javascript.h
+    CHK(FPDFDoc_CloseJavaScriptAction);
+    CHK(FPDFDoc_GetJavaScriptAction);
+    CHK(FPDFDoc_GetJavaScriptActionCount);
+    CHK(FPDFJavaScriptAction_GetName);
+    CHK(FPDFJavaScriptAction_GetScript);
+
     // fpdf_ppo.h
     CHK(FPDF_CopyViewerPreferences);
     CHK(FPDF_ImportNPagesToOne);
diff --git a/public/cpp/fpdf_deleters.h b/public/cpp/fpdf_deleters.h
index 9a700e3..633ddf5 100644
--- a/public/cpp/fpdf_deleters.h
+++ b/public/cpp/fpdf_deleters.h
@@ -9,6 +9,7 @@
 #include "public/fpdf_dataavail.h"
 #include "public/fpdf_edit.h"
 #include "public/fpdf_formfill.h"
+#include "public/fpdf_javascript.h"
 #include "public/fpdf_structtree.h"
 #include "public/fpdf_text.h"
 #include "public/fpdf_transformpage.h"
@@ -48,6 +49,12 @@
   }
 };
 
+struct FPDFJavaScriptActionDeleter {
+  inline void operator()(FPDF_JAVASCRIPT_ACTION javascript) {
+    FPDFDoc_CloseJavaScriptAction(javascript);
+  }
+};
+
 struct FPDFPageDeleter {
   inline void operator()(FPDF_PAGE page) { FPDF_ClosePage(page); }
 };
diff --git a/public/cpp/fpdf_scopers.h b/public/cpp/fpdf_scopers.h
index ebae107..ff57c1b 100644
--- a/public/cpp/fpdf_scopers.h
+++ b/public/cpp/fpdf_scopers.h
@@ -9,13 +9,6 @@
 #include <type_traits>
 
 #include "public/cpp/fpdf_deleters.h"
-#include "public/fpdf_annot.h"
-#include "public/fpdf_dataavail.h"
-#include "public/fpdf_edit.h"
-#include "public/fpdf_formfill.h"
-#include "public/fpdf_structtree.h"
-#include "public/fpdf_text.h"
-#include "public/fpdfview.h"
 
 // Versions of FPDF types that clean up the object at scope exit.
 
@@ -44,6 +37,10 @@
     std::unique_ptr<std::remove_pointer<FPDF_FORMHANDLE>::type,
                     FPDFFormHandleDeleter>;
 
+using ScopedFPDFJavaScriptAction =
+    std::unique_ptr<std::remove_pointer<FPDF_JAVASCRIPT_ACTION>::type,
+                    FPDFJavaScriptActionDeleter>;
+
 using ScopedFPDFPage =
     std::unique_ptr<std::remove_pointer<FPDF_PAGE>::type, FPDFPageDeleter>;
 
diff --git a/public/fpdf_javascript.h b/public/fpdf_javascript.h
new file mode 100644
index 0000000..19f3810
--- /dev/null
+++ b/public/fpdf_javascript.h
@@ -0,0 +1,77 @@
+// Copyright 2019 PDFium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PUBLIC_FPDF_JAVASCRIPT_H_
+#define PUBLIC_FPDF_JAVASCRIPT_H_
+
+// NOLINTNEXTLINE(build/include)
+#include "fpdfview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+// Experimental API.
+// Get the number of JavaScript actions in |document|.
+//
+//   document - handle to a document.
+//
+// Returns the number of JavaScript actions in |document| or -1 on error.
+FPDF_EXPORT int FPDF_CALLCONV
+FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document);
+
+// Experimental API.
+// Get the JavaScript action at |index| in |document|.
+//
+//   document - handle to a document.
+//   index    - the index of the requested JavaScript action.
+//
+// Returns the handle to the JavaScript action, or NULL on failure.
+// Caller owns the returned handle and must close it with
+// FPDFDoc_CloseJavaScriptAction().
+FPDF_EXPORT FPDF_JAVASCRIPT_ACTION FPDF_CALLCONV
+FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index);
+
+// Experimental API.
+// Close a loaded FPDF_JAVASCRIPT_ACTION object.
+
+//   javascript - Handle to a JavaScript action.
+FPDF_EXPORT void FPDF_CALLCONV
+FPDFDoc_CloseJavaScriptAction(FPDF_JAVASCRIPT_ACTION javascript);
+
+// Experimental API.
+// Get the name from the |javascript| handle. |buffer| is only modified if
+// |buflen| is longer than the length of the name. On errors, |buffer| is
+// unmodified and the returned length is 0.
+//
+//   javascript - handle to an JavaScript action.
+//   buffer     - buffer for holding the name, encoded in UTF-16LE.
+//   buflen     - length of the buffer in bytes.
+//
+// Returns the length of the JavaScript action name in bytes.
+FPDF_EXPORT unsigned long FPDF_CALLCONV
+FPDFJavaScriptAction_GetName(FPDF_JAVASCRIPT_ACTION javascript,
+                             FPDF_WCHAR* buffer,
+                             unsigned long buflen);
+
+// Experimental API.
+// Get the script from the |javascript| handle. |buffer| is only modified if
+// |buflen| is longer than the length of the script. On errors, |buffer| is
+// unmodified and the returned length is 0.
+//
+//   javascript - handle to an JavaScript action.
+//   buffer     - buffer for holding the name, encoded in UTF-16LE.
+//   buflen     - length of the buffer in bytes.
+//
+// Returns the length of the JavaScript action name in bytes.
+FPDF_EXPORT unsigned long FPDF_CALLCONV
+FPDFJavaScriptAction_GetScript(FPDF_JAVASCRIPT_ACTION javascript,
+                               FPDF_WCHAR* buffer,
+                               unsigned long buflen);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // PUBLIC_FPDF_JAVASCRIPT_H_
diff --git a/public/fpdfview.h b/public/fpdfview.h
index 6bbc2bf..55ab33c 100644
--- a/public/fpdfview.h
+++ b/public/fpdfview.h
@@ -45,6 +45,7 @@
 typedef struct fpdf_document_t__* FPDF_DOCUMENT;
 typedef struct fpdf_font_t__* FPDF_FONT;
 typedef struct fpdf_form_handle_t__* FPDF_FORMHANDLE;
+typedef struct fpdf_javascript_action_t* FPDF_JAVASCRIPT_ACTION;
 typedef struct fpdf_link_t__* FPDF_LINK;
 typedef struct fpdf_page_t__* FPDF_PAGE;
 typedef struct fpdf_pagelink_t__* FPDF_PAGELINK;
diff --git a/testing/resources/js.in b/testing/resources/js.in
new file mode 100644
index 0000000..7d35139
--- /dev/null
+++ b/testing/resources/js.in
@@ -0,0 +1,83 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Catalog
+  /Pages 2 0 R
+  /Names <</JavaScript 4 0 R>>
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Pages
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 2 0 R
+  /MediaBox [0 0 612 792]
+  /CropBox [0 0 612 792]
+>>
+endobj
+{{object 4 0}} <<
+  /Names [
+    (normal) 5 0 R
+    (encoded_subtype) 6 0 R
+    (no_type) 7 0 R
+    (wrongtype) 8 0 R
+    (wrongsubtype) 9 0 R
+    (nojs) 10 0 R
+  ]
+>>
+endobj
+{{object 5 0}} <<
+  /Type /Action
+  /S /JavaScript
+  /JS 11 0 R
+>>
+endobj
+{{object 6 0}} <<
+  /Type /Action
+  /S /J#61v#61Script
+  /JS 11 0 R
+>>
+endobj
+{{object 7 0}} <<
+  /S /JavaScript
+  /JS 12 0 R
+>>
+endobj
+{{object 8 0}} <<
+  /Type /action
+  /S /JavaScript
+  /JS 12 0 R
+>>
+endobj
+{{object 9 0}} <<
+  /Type /Action
+  /S /Javascript
+  /JS 12 0 R
+>>
+endobj
+{{object 10 0}} <<
+  /Type /Action
+  /S /JavaScript
+>>
+endobj
+{{object 11 0}} <<
+>>
+stream
+app.alert("ping");
+endstream
+endobj
+{{object 12 0}} <<
+>>
+stream
+app.alert("pong");
+endstream
+endobj
+{{xref}}
+trailer <<
+  /Root 1 0 R
+>>
+{{startxref}}
+%%EOF
diff --git a/testing/resources/js.pdf b/testing/resources/js.pdf
new file mode 100644
index 0000000..07e9b75
--- /dev/null
+++ b/testing/resources/js.pdf
@@ -0,0 +1,99 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+  /Type /Catalog
+  /Pages 2 0 R
+  /Names <</JavaScript 4 0 R>>
+>>
+endobj
+2 0 obj <<
+  /Type /Pages
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+  /Type /Page
+  /Parent 2 0 R
+  /MediaBox [0 0 612 792]
+  /CropBox [0 0 612 792]
+>>
+endobj
+4 0 obj <<
+  /Names [
+    (normal) 5 0 R
+    (encoded_subtype) 6 0 R
+    (no_type) 7 0 R
+    (wrongtype) 8 0 R
+    (wrongsubtype) 9 0 R
+    (nojs) 10 0 R
+  ]
+>>
+endobj
+5 0 obj <<
+  /Type /Action
+  /S /JavaScript
+  /JS 11 0 R
+>>
+endobj
+6 0 obj <<
+  /Type /Action
+  /S /J#61v#61Script
+  /JS 11 0 R
+>>
+endobj
+7 0 obj <<
+  /S /JavaScript
+  /JS 12 0 R
+>>
+endobj
+8 0 obj <<
+  /Type /action
+  /S /JavaScript
+  /JS 12 0 R
+>>
+endobj
+9 0 obj <<
+  /Type /Action
+  /S /Javascript
+  /JS 12 0 R
+>>
+endobj
+10 0 obj <<
+  /Type /Action
+  /S /JavaScript
+>>
+endobj
+11 0 obj <<
+>>
+stream
+app.alert("ping");
+endstream
+endobj
+12 0 obj <<
+>>
+stream
+app.alert("pong");
+endstream
+endobj
+xref
+0 13
+0000000000 65535 f 
+0000000015 00000 n 
+0000000099 00000 n 
+0000000162 00000 n 
+0000000264 00000 n 
+0000000432 00000 n 
+0000000499 00000 n 
+0000000570 00000 n 
+0000000621 00000 n 
+0000000688 00000 n 
+0000000755 00000 n 
+0000000810 00000 n 
+0000000868 00000 n 
+trailer <<
+  /Root 1 0 R
+>>
+startxref
+926
+%%EOF