Added URI Action handling public API and its test cases

Support for opening a link in new tab/window exists using mouse. But
after setting focus over link by pressing Tab key, we do not have the
provision to open the link in new tab/window using keyboard shortcut.
This new API carries keyboard modifier value whenever URI action
needs to be performed.

Embedder tests are added to validate the added APIs.

Bug: chromium:994500
Change-Id: Id1bb8d6dd5c607bc5194d300250d8b796c7ff03c
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/66830
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Badhri Ravikumar <bravi@microsoft.com>
diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp
index dfee33c..a12e3b0 100644
--- a/fpdfsdk/fpdf_formfill_embeddertest.cpp
+++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp
@@ -21,7 +21,9 @@
 #include "testing/gtest/include/gtest/gtest.h"
 
 using testing::_;
+using testing::InSequence;
 using testing::NiceMock;
+using testing::StrEq;
 
 using FPDFFormFillEmbedderTest = EmbedderTest;
 
@@ -590,6 +592,7 @@
   EXPECT_CALL(mock, KillTimer(_)).Times(0);
   EXPECT_CALL(mock, OnFocusChange(_, _, _)).Times(0);
   EXPECT_CALL(mock, DoURIAction(_)).Times(0);
+  EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(_, _, _)).Times(0);
   SetDelegate(&mock);
 
   EXPECT_TRUE(OpenDocument("hello_world.pdf"));
@@ -2976,6 +2979,13 @@
     ASSERT_TRUE(OpenDocument("annots_action_handling.pdf"));
     page_ = LoadPage(0);
     ASSERT_TRUE(page_);
+
+    // Set Widget and Link as supported tabbable annots.
+    constexpr FPDF_ANNOTATION_SUBTYPE kFocusableSubtypes[] = {FPDF_ANNOT_WIDGET,
+                                                              FPDF_ANNOT_LINK};
+    constexpr size_t kSubtypeCount = FX_ArraySize(kFocusableSubtypes);
+    ASSERT_TRUE(FPDFAnnot_SetFocusableSubtypes(
+        form_handle(), kFocusableSubtypes, kSubtypeCount));
   }
 
   void TearDown() override {
@@ -2983,10 +2993,14 @@
     EmbedderTest::TearDown();
   }
 
-  void SetFocusOnFirstForm() {
+  void SetFocusOnNthAnnot(size_t n) {
+    DCHECK_NE(n, 0);
+    // Setting focus on first annot.
     FORM_OnMouseMove(form_handle(), page(), /*modifier=*/0, 100, 680);
     FORM_OnLButtonDown(form_handle(), page(), /*modifier=*/0, 100, 680);
     FORM_OnLButtonUp(form_handle(), page(), /*modifier=*/0, 100, 680);
+    for (size_t i = 1; i < n; i++)
+      ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Tab, 0));
   }
 
   FPDF_PAGE page() { return page_; }
@@ -3001,7 +3015,7 @@
   EXPECT_CALL(mock, DoURIAction(_)).Times(0);
   SetDelegate(&mock);
 
-  SetFocusOnFirstForm();
+  SetFocusOnNthAnnot(1);
 
   // Tab once from first form to go to button widget.
   ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Tab, 0));
@@ -3010,3 +3024,62 @@
   // handling key press implementation on buttons.
   ASSERT_FALSE(FORM_OnChar(form_handle(), page(), FWL_VKEY_Return, 0));
 }
+
+TEST_F(FPDFFormFillActionUriTest, LinkActionInvokeTest) {
+  NiceMock<EmbedderTestMockDelegate> mock;
+  {
+    InSequence sequence;
+    // TODO(crbug.com/994500) : DoURIAction number of expected calls to be
+    // updated to 4.
+    const char kExpectedUri[] = "https://www.cs.chromium.org/";
+    EXPECT_CALL(mock, DoURIAction(StrEq(kExpectedUri))).Times(0);
+    EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(_, _, _)).Times(0);
+  }
+  SetDelegate(&mock);
+  SetFocusOnNthAnnot(3);
+  int modifier = 0;
+  // TODO(crbug.com/994500): Following asserts to be changed to ASSERT_TRUE.
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  modifier = FWL_EVENTFLAG_ControlKey;
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  modifier = FWL_EVENTFLAG_ShiftKey;
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  modifier |= FWL_EVENTFLAG_ControlKey;
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+}
+
+class FPDFFormFillActionUriTestVersion2 : public FPDFFormFillActionUriTest {
+  void SetUp() override {
+    SetFormFillInfoVersion(2);
+    FPDFFormFillActionUriTest::SetUp();
+  }
+};
+
+TEST_F(FPDFFormFillActionUriTestVersion2, LinkActionInvokeTest) {
+  NiceMock<EmbedderTestMockDelegate> mock;
+  {
+    InSequence sequence;
+    EXPECT_CALL(mock, DoURIAction(_)).Times(0);
+    // TODO(crbug.com/994500): Following comments has to be removed.
+    // const char kExpectedUri[] = "https://www.cs.chromium.org/";
+    // EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(
+    //                      _, StrEq(kExpectedUri), 0));
+    // EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(
+    //                       _, StrEq(kExpectedUri), FWL_EVENTFLAG_ControlKey));
+    // EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(
+    //                      _, StrEq(kExpectedUri), FWL_EVENTFLAG_ShiftKey));
+    // EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(
+    //                      _, StrEq(kExpectedUri), 3));
+  }
+  SetDelegate(&mock);
+  SetFocusOnNthAnnot(3);
+  int modifier = 0;
+  // TODO(crbug.com/994500): Following asserts to be changed to ASSERT_TRUE.
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  modifier = FWL_EVENTFLAG_ControlKey;
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  modifier = FWL_EVENTFLAG_ShiftKey;
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  modifier |= FWL_EVENTFLAG_ControlKey;
+  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+}
\ No newline at end of file
diff --git a/public/fpdf_formfill.h b/public/fpdf_formfill.h
index 1f656b0..4ec345f 100644
--- a/public/fpdf_formfill.h
+++ b/public/fpdf_formfill.h
@@ -680,6 +680,10 @@
    * Return value:
    *       None.
    * Comments:
+   *       If the embedder is version 2 or higher and have implementation for
+   *       FFI_DoURIActionWithKeyboardModifier, then
+   *       FFI_DoURIActionWithKeyboardModifier takes precedence over
+   *       FFI_DoURIAction.
    *       See the URI actions description of <<PDF Reference, version 1.7>>
    *       for more details.
    */
@@ -1102,6 +1106,32 @@
   void (*FFI_OnFocusChange)(struct _FPDF_FORMFILLINFO* param,
                             FPDF_ANNOTATION annot,
                             int page_index);
+
+  /**
+   * Method: FFI_DoURIActionWithKeyboardModifier
+   *       Ask the implementation to navigate to a uniform resource identifier
+   *       with the specified modifiers.
+   * Interface Version:
+   *       Ignored if |version| < 2.
+   * Implementation Required:
+   *       No
+   * Parameters:
+   *       param           -   Pointer to the interface structure itself.
+   *       uri             -   A byte string which indicates the uniform
+   *                           resource identifier, terminated by 0.
+   *       modifiers       -   Keyboard modifier that indicates which of
+   *                           the virtual keys are down, if any.
+   * Return value:
+   *       None.
+   * Comments:
+   *       If the embedder who is version 2 and does not implement this API,
+   *       then a call will be redirected to FFI_DoURIAction.
+   *       See the URI actions description of <<PDF Reference, version 1.7>>
+   *       for more details.
+   */
+  void(*FFI_DoURIActionWithKeyboardModifier)(struct _FPDF_FORMFILLINFO* param,
+      FPDF_BYTESTRING uri,
+      int modifiers);
 } FPDF_FORMFILLINFO;
 
 /*
diff --git a/testing/embedder_test.cpp b/testing/embedder_test.cpp
index 291b8d7..caba758 100644
--- a/testing/embedder_test.cpp
+++ b/testing/embedder_test.cpp
@@ -253,6 +253,8 @@
   formfillinfo->FFI_GetPage = GetPageTrampoline;
   formfillinfo->FFI_DoURIAction = DoURIActionTrampoline;
   formfillinfo->FFI_OnFocusChange = OnFocusChangeTrampoline;
+  formfillinfo->FFI_DoURIActionWithKeyboardModifier =
+      DoURIActionWithKeyboardModifierTrampoline;
 
   if (javascript_option == JavaScriptOption::kEnableJavaScript)
     formfillinfo->m_pJsPlatform = platform;
@@ -614,6 +616,15 @@
 }
 
 // static
+void EmbedderTest::DoURIActionWithKeyboardModifierTrampoline(
+    FPDF_FORMFILLINFO* info,
+    FPDF_BYTESTRING uri,
+    int modifiers) {
+  EmbedderTest* test = static_cast<EmbedderTest*>(info);
+  return test->delegate_->DoURIActionWithKeyboardModifier(info, uri, modifiers);
+}
+
+// static
 std::string EmbedderTest::HashBitmap(FPDF_BITMAP bitmap) {
   int stride = FPDFBitmap_GetStride(bitmap);
   int usable_bytes_per_row =
diff --git a/testing/embedder_test.h b/testing/embedder_test.h
index 5d42442..e9e69fb 100644
--- a/testing/embedder_test.h
+++ b/testing/embedder_test.h
@@ -69,6 +69,11 @@
     virtual void OnFocusChange(FPDF_FORMFILLINFO* info,
                                FPDF_ANNOTATION annot,
                                int page_index) {}
+
+    // Equivalent to FPDF_FORMFILLINFO::FFI_DoURIActionWithKeyboardModifier().
+    virtual void DoURIActionWithKeyboardModifier(FPDF_FORMFILLINFO* info,
+                                                 FPDF_BYTESTRING uri,
+                                                 int modifiers) {}
   };
 
   EmbedderTest();
@@ -302,6 +307,9 @@
   static void OnFocusChangeTrampoline(FPDF_FORMFILLINFO* info,
                                       FPDF_ANNOTATION annot,
                                       int page_index);
+  static void DoURIActionWithKeyboardModifierTrampoline(FPDF_FORMFILLINFO* info,
+                                                        FPDF_BYTESTRING uri,
+                                                        int modifiers);
   static int WriteBlockCallback(FPDF_FILEWRITE* pFileWrite,
                                 const void* data,
                                 unsigned long size);
diff --git a/testing/embedder_test_mock_delegate.h b/testing/embedder_test_mock_delegate.h
index 06f144f..f2cbb84 100644
--- a/testing/embedder_test_mock_delegate.h
+++ b/testing/embedder_test_mock_delegate.h
@@ -21,6 +21,10 @@
                void(FPDF_FORMFILLINFO* info,
                     FPDF_ANNOTATION annot,
                     int page_index));
+  MOCK_METHOD3(DoURIActionWithKeyboardModifier,
+               void(FPDF_FORMFILLINFO* info,
+                    FPDF_BYTESTRING uri,
+                    int modifiers));
 };
 
 #endif  // TESTING_EMBEDDER_TEST_MOCK_DELEGATE_H_
diff --git a/testing/resources/annots_action_handling.in b/testing/resources/annots_action_handling.in
index bbf1a48..51becc7 100644
--- a/testing/resources/annots_action_handling.in
+++ b/testing/resources/annots_action_handling.in
@@ -14,20 +14,32 @@
 {{object 3 0}} <<
   /Type /Page
   /Parent 2 0 R
-  /Annots [4 0 R 5 0 R]
+  /Annots [5 0 R 6 0 R 7 0 R]
+  /Contents 4 0 R
   /Tabs /R
 >>
 endobj
 {{object 4 0}} <<
+  {{streamlen}}
+>>
+stream
+BT
+70 340 Td
+14 Tf
+(External Link ) Tj
+ET
+endstream
+endobj
+{{object 5 0}} <<
   /Type /Annot
   /Subtype /Widget
   /FT /Tx
   /Parent 3 0 R
-  /T (Sub_LeftBottom)
+  /T (TextField)
   /Rect [69 670 220 690]
 >>
 endobj
-{{object 5 0}} <<
+{{object 6 0}} <<
   /Type /Annot
   /Subtype /Widget
   /FT /Btn
@@ -41,6 +53,18 @@
   /Ff 65536
 >>
 endobj
+{{object 7 0}} <<
+  /Type /Annot
+  /Subtype /Link
+  /Rect [69 338 180 358]
+  /A <<
+    /Type /Action
+    /S /URI
+    /URI (https://www.cs.chromium.org/)
+  >>
+  /F 4
+>>
+endobj
 {{xref}}
 {{trailer}}
 {{startxref}}
diff --git a/testing/resources/annots_action_handling.pdf b/testing/resources/annots_action_handling.pdf
index 70e62f7..49ea926 100644
--- a/testing/resources/annots_action_handling.pdf
+++ b/testing/resources/annots_action_handling.pdf
@@ -15,20 +15,32 @@
 3 0 obj <<
   /Type /Page
   /Parent 2 0 R
-  /Annots [4 0 R 5 0 R]
+  /Annots [5 0 R 6 0 R 7 0 R]
+  /Contents 4 0 R
   /Tabs /R
 >>
 endobj
 4 0 obj <<
+  /Length 42
+>>
+stream
+BT
+70 340 Td
+14 Tf
+(External Link ) Tj
+ET
+endstream
+endobj
+5 0 obj <<
   /Type /Annot
   /Subtype /Widget
   /FT /Tx
   /Parent 3 0 R
-  /T (Sub_LeftBottom)
+  /T (TextField)
   /Rect [69 670 220 690]
 >>
 endobj
-5 0 obj <<
+6 0 obj <<
   /Type /Annot
   /Subtype /Widget
   /FT /Btn
@@ -42,18 +54,32 @@
   /Ff 65536
 >>
 endobj
+7 0 obj <<
+  /Type /Annot
+  /Subtype /Link
+  /Rect [69 338 180 358]
+  /A <<
+    /Type /Action
+    /S /URI
+    /URI (https://www.cs.chromium.org/)
+  >>
+  /F 4
+>>
+endobj
 xref
-0 6
+0 8
 0000000000 65535 f 
 0000000015 00000 n 
 0000000094 00000 n 
 0000000157 00000 n 
-0000000243 00000 n 
-0000000371 00000 n 
+0000000267 00000 n 
+0000000360 00000 n 
+0000000483 00000 n 
+0000000666 00000 n 
 trailer <<
   /Root 1 0 R
-  /Size 6
+  /Size 8
 >>
 startxref
-554
+834
 %%EOF
\ No newline at end of file