Implemented URI Action Handling for links

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 shortcuts.
This implementation uses FFI_DoURIActionWithKeyboardModifier API
which handles opening the link in new tab/window.

CL for tests : https://pdfium-review.googlesource.com/c/pdfium/+/66830

Bug: chromium:994500
Change-Id: I65500983d1a07e9b31743d0a0df88a1bde4eb142
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/68930
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Badhri Ravikumar <bravi@microsoft.com>
diff --git a/core/fpdfdoc/cpdf_aaction.cpp b/core/fpdfdoc/cpdf_aaction.cpp
index 300e94a..46fe173 100644
--- a/core/fpdfdoc/cpdf_aaction.cpp
+++ b/core/fpdfdoc/cpdf_aaction.cpp
@@ -57,10 +57,11 @@
 }
 
 // static
-bool CPDF_AAction::IsUserClick(AActionType eType) {
-  switch (eType) {
+bool CPDF_AAction::IsUserInput(AActionType type) {
+  switch (type) {
     case kButtonUp:
     case kButtonDown:
+    case kKeyStroke:
       return true;
     default:
       return false;
diff --git a/core/fpdfdoc/cpdf_aaction.h b/core/fpdfdoc/cpdf_aaction.h
index c3c65e8..0076a9c 100644
--- a/core/fpdfdoc/cpdf_aaction.h
+++ b/core/fpdfdoc/cpdf_aaction.h
@@ -48,7 +48,7 @@
   CPDF_Action GetAction(AActionType eType) const;
   const CPDF_Dictionary* GetDict() const { return m_pDict.Get(); }
 
-  static bool IsUserClick(AActionType eType);
+  static bool IsUserInput(AActionType type);
 
  private:
   RetainPtr<const CPDF_Dictionary> const m_pDict;
diff --git a/fpdfsdk/cpdfsdk_actionhandler.cpp b/fpdfsdk/cpdfsdk_actionhandler.cpp
index 6b2b4b9..5e3e83b 100644
--- a/fpdfsdk/cpdfsdk_actionhandler.cpp
+++ b/fpdfsdk/cpdfsdk_actionhandler.cpp
@@ -59,6 +59,22 @@
   return false;
 }
 
+bool CPDFSDK_ActionHandler::DoAction_Link(
+    const CPDF_Action& action,
+    CPDF_AAction::AActionType type,
+    CPDFSDK_FormFillEnvironment* form_fill_env,
+    int modifiers) {
+  ASSERT(form_fill_env);
+  if (action.GetType() != CPDF_Action::URI)
+    return false;
+
+  if (!CPDF_AAction::IsUserInput(type))
+    return false;
+
+  DoAction_URI(form_fill_env, action, modifiers);
+  return true;
+}
+
 bool CPDFSDK_ActionHandler::DoAction_Page(
     const CPDF_Action& action,
     enum CPDF_AAction::AActionType eType,
@@ -105,7 +121,7 @@
     }
   } else {
     DoAction_NoJs(action, CPDF_AAction::AActionType::kDocumentOpen,
-                  pFormFillEnv);
+                  pFormFillEnv, /*modifiers=*/0);
   }
 
   for (int32_t i = 0, sz = action.GetSubActionsCount(); i < sz; i++) {
@@ -136,7 +152,7 @@
         RunDocumentPageJavaScript(pFormFillEnv, type, swJS);
     }
   } else {
-    DoAction_NoJs(action, type, pFormFillEnv);
+    DoAction_NoJs(action, type, pFormFillEnv, /*modifiers=*/0);
   }
 
   ASSERT(pFormFillEnv);
@@ -184,7 +200,7 @@
       }
     }
   } else {
-    DoAction_NoJs(action, type, pFormFillEnv);
+    DoAction_NoJs(action, type, pFormFillEnv, /*modifiers=*/0);
   }
 
   for (int32_t i = 0, sz = action.GetSubActionsCount(); i < sz; i++) {
@@ -200,7 +216,8 @@
 void CPDFSDK_ActionHandler::DoAction_NoJs(
     const CPDF_Action& action,
     CPDF_AAction::AActionType type,
-    CPDFSDK_FormFillEnvironment* pFormFillEnv) {
+    CPDFSDK_FormFillEnvironment* pFormFillEnv,
+    int modifiers) {
   ASSERT(pFormFillEnv);
 
   switch (action.GetType()) {
@@ -208,8 +225,8 @@
       DoAction_GoTo(pFormFillEnv, action);
       break;
     case CPDF_Action::URI:
-      if (CPDF_AAction::IsUserClick(type))
-        DoAction_URI(pFormFillEnv, action);
+      if (CPDF_AAction::IsUserInput(type))
+        DoAction_URI(pFormFillEnv, action, modifiers);
       break;
     case CPDF_Action::Hide:
       DoAction_Hide(action, pFormFillEnv);
@@ -218,7 +235,7 @@
       DoAction_Named(pFormFillEnv, action);
       break;
     case CPDF_Action::SubmitForm:
-      if (CPDF_AAction::IsUserClick(type))
+      if (CPDF_AAction::IsUserInput(type))
         DoAction_SubmitForm(action, pFormFillEnv);
       break;
     case CPDF_Action::ResetForm:
@@ -268,11 +285,12 @@
 
 void CPDFSDK_ActionHandler::DoAction_URI(
     CPDFSDK_FormFillEnvironment* pFormFillEnv,
-    const CPDF_Action& action) {
+    const CPDF_Action& action,
+    int modifiers) {
   ASSERT(action.GetDict());
 
   ByteString sURI = action.GetURI(pFormFillEnv->GetPDFDocument());
-  pFormFillEnv->DoURIAction(sURI.c_str());
+  pFormFillEnv->DoURIAction(sURI.c_str(), modifiers);
 }
 
 void CPDFSDK_ActionHandler::DoAction_Named(
diff --git a/fpdfsdk/cpdfsdk_actionhandler.h b/fpdfsdk/cpdfsdk_actionhandler.h
index a8bd9cf..7880eba 100644
--- a/fpdfsdk/cpdfsdk_actionhandler.h
+++ b/fpdfsdk/cpdfsdk_actionhandler.h
@@ -44,6 +44,10 @@
                                 CPDFSDK_FormFillEnvironment* pFormFillEnv,
                                 CPDF_FormField* pFormField,
                                 CPDFSDK_FieldAction* data);
+  bool DoAction_Link(const CPDF_Action& action,
+                     CPDF_AAction::AActionType type,
+                     CPDFSDK_FormFillEnvironment* form_fill_env,
+                     int modifiers);
 
  private:
   using RunScriptCallback = std::function<void(IJS_EventContext* context)>;
@@ -68,7 +72,8 @@
 
   void DoAction_NoJs(const CPDF_Action& action,
                      CPDF_AAction::AActionType type,
-                     CPDFSDK_FormFillEnvironment* pFormFillEnv);
+                     CPDFSDK_FormFillEnvironment* pFormFillEnv,
+                     int modifiers);
   void RunDocumentPageJavaScript(CPDFSDK_FormFillEnvironment* pFormFillEnv,
                                  CPDF_AAction::AActionType type,
                                  const WideString& script);
@@ -89,7 +94,8 @@
   void DoAction_Launch(CPDFSDK_FormFillEnvironment* pFormFillEnv,
                        const CPDF_Action& action);
   void DoAction_URI(CPDFSDK_FormFillEnvironment* pFormFillEnv,
-                    const CPDF_Action& action);
+                    const CPDF_Action& action,
+                    int modifiers);
   void DoAction_Named(CPDFSDK_FormFillEnvironment* pFormFillEnv,
                       const CPDF_Action& action);
 
diff --git a/fpdfsdk/cpdfsdk_baannot.cpp b/fpdfsdk/cpdfsdk_baannot.cpp
index 87866b5..968e8b5 100644
--- a/fpdfsdk/cpdfsdk_baannot.cpp
+++ b/fpdfsdk/cpdfsdk_baannot.cpp
@@ -212,7 +212,7 @@
   if (AAction.ActionExist(eAAT))
     return AAction.GetAction(eAAT);
 
-  if (eAAT == CPDF_AAction::kButtonUp)
+  if (eAAT == CPDF_AAction::kButtonUp || eAAT == CPDF_AAction::kKeyStroke)
     return GetAction();
 
   return CPDF_Action(nullptr);
diff --git a/fpdfsdk/cpdfsdk_baannothandler.cpp b/fpdfsdk/cpdfsdk_baannothandler.cpp
index 10bde5c..ef90835 100644
--- a/fpdfsdk/cpdfsdk_baannothandler.cpp
+++ b/fpdfsdk/cpdfsdk_baannothandler.cpp
@@ -12,11 +12,13 @@
 #include "core/fpdfapi/page/cpdf_page.h"
 #include "core/fpdfdoc/cpdf_interactiveform.h"
 #include "core/fxge/cfx_drawutils.h"
+#include "fpdfsdk/cpdfsdk_actionhandler.h"
 #include "fpdfsdk/cpdfsdk_annot.h"
 #include "fpdfsdk/cpdfsdk_baannot.h"
 #include "fpdfsdk/cpdfsdk_formfillenvironment.h"
 #include "fpdfsdk/cpdfsdk_pageview.h"
 #include "fpdfsdk/formfiller/cffl_formfiller.h"
+#include "public/fpdf_fwlevent.h"
 #include "third_party/base/stl_util.h"
 
 namespace {
@@ -173,7 +175,24 @@
 bool CPDFSDK_BAAnnotHandler::OnKeyDown(CPDFSDK_Annot* pAnnot,
                                        int nKeyCode,
                                        int nFlag) {
-  return false;
+  ASSERT(pAnnot);
+
+  // OnKeyDown() is implemented only for link annotations for now. As
+  // OnKeyDown() is implemented for other subtypes, following check should be
+  // modified.
+  if (nKeyCode != FWL_VKEY_Return ||
+      pAnnot->GetAnnotSubtype() != CPDF_Annot::Subtype::LINK) {
+    return false;
+  }
+
+  CPDFSDK_BAAnnot* ba_annot = pAnnot->AsBAAnnot();
+  CPDF_Action action = ba_annot->GetAAction(CPDF_AAction::kKeyStroke);
+  if (!action.GetDict() || action.GetType() != CPDF_Action::URI) {
+    return false;
+  }
+
+  return form_fill_environment_->GetActionHandler()->DoAction_Link(
+      action, CPDF_AAction::kKeyStroke, form_fill_environment_.Get(), nFlag);
 }
 
 bool CPDFSDK_BAAnnotHandler::OnKeyUp(CPDFSDK_Annot* pAnnot,
diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.cpp b/fpdfsdk/cpdfsdk_formfillenvironment.cpp
index fc479e6..5748dcd 100644
--- a/fpdfsdk/cpdfsdk_formfillenvironment.cpp
+++ b/fpdfsdk/cpdfsdk_formfillenvironment.cpp
@@ -384,8 +384,17 @@
     m_pInfo->FFI_SetTextFieldFocus(m_pInfo, focusText, nTextLen, bFocus);
 }
 
-void CPDFSDK_FormFillEnvironment::DoURIAction(const char* bsURI) {
-  if (m_pInfo && m_pInfo->FFI_DoURIAction)
+void CPDFSDK_FormFillEnvironment::DoURIAction(const char* bsURI,
+                                              uint32_t modifiers) {
+  if (!m_pInfo)
+    return;
+
+  if (m_pInfo->version >= 2 && m_pInfo->FFI_DoURIActionWithKeyboardModifier) {
+    m_pInfo->FFI_DoURIActionWithKeyboardModifier(m_pInfo, bsURI, modifiers);
+    return;
+  }
+
+  if (m_pInfo->FFI_DoURIAction)
     m_pInfo->FFI_DoURIAction(m_pInfo, bsURI);
 }
 
diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.h b/fpdfsdk/cpdfsdk_formfillenvironment.h
index c047f22..96e0405 100644
--- a/fpdfsdk/cpdfsdk_formfillenvironment.h
+++ b/fpdfsdk/cpdfsdk_formfillenvironment.h
@@ -95,7 +95,7 @@
   void OnSetFieldInputFocus(FPDF_WIDESTRING focusText,
                             FPDF_DWORD nTextLen,
                             bool bFocus);
-  void DoURIAction(const char* bsURI);
+  void DoURIAction(const char* bsURI, uint32_t modifiers);
   void DoGoToAction(int nPageIndex,
                     int zoomMode,
                     float* fPosArray,
diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp
index aef5c61..5a589c6 100644
--- a/fpdfsdk/fpdf_formfill_embeddertest.cpp
+++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp
@@ -3028,23 +3028,36 @@
   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);
+    const char kExpectedUri[] = "https://cs.chromium.org/";
+#ifdef PDF_ENABLE_XFA
+    EXPECT_CALL(mock,
+                DoURIActionWithKeyboardModifier(_, StrEq(kExpectedUri), _))
+        .Times(4);
+#else   // PDF_ENABLE_XFA
+    EXPECT_CALL(mock, DoURIAction(StrEq(kExpectedUri))).Times(4);
     EXPECT_CALL(mock, DoURIActionWithKeyboardModifier(_, _, _)).Times(0);
+#endif  // PDF_ENABLE_XFA
   }
   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));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
   modifier = FWL_EVENTFLAG_ControlKey;
-  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
   modifier = FWL_EVENTFLAG_ShiftKey;
-  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
   modifier |= FWL_EVENTFLAG_ControlKey;
-  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
+
+  ASSERT_FALSE(FORM_OnKeyDown(nullptr, page(), FWL_VKEY_Return, modifier));
+  ASSERT_FALSE(
+      FORM_OnKeyDown(form_handle(), nullptr, FWL_VKEY_Return, modifier));
+  // Following checks should be changed to ASSERT_TRUE if FORM_OnKeyDown starts
+  // handling for Shift/Space/Control.
+  ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Shift, modifier));
+  ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Space, modifier));
+  ASSERT_FALSE(
+      FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Control, modifier));
 }
 
 class FPDFFormFillActionUriTestVersion2 : public FPDFFormFillActionUriTest {
@@ -3059,26 +3072,34 @@
   {
     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));
+    const char kExpectedUri[] = "https://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));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
   modifier = FWL_EVENTFLAG_ControlKey;
-  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
   modifier = FWL_EVENTFLAG_ShiftKey;
-  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
   modifier |= FWL_EVENTFLAG_ControlKey;
-  ASSERT_FALSE(FORM_OnKeyUp(form_handle(), page(), FWL_VKEY_Return, modifier));
+  ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Return, modifier));
+
+  ASSERT_FALSE(FORM_OnKeyDown(nullptr, page(), FWL_VKEY_Return, modifier));
+  ASSERT_FALSE(
+      FORM_OnKeyDown(form_handle(), nullptr, FWL_VKEY_Return, modifier));
+  // Following checks should be changed to ASSERT_TRUE if FORM_OnKeyDown starts
+  // handling for Shift/Space/Control.
+  ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Shift, modifier));
+  ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Space, modifier));
+  ASSERT_FALSE(
+      FORM_OnKeyDown(form_handle(), page(), FWL_VKEY_Control, modifier));
 }
diff --git a/testing/resources/annots_action_handling.in b/testing/resources/annots_action_handling.in
index 51becc7..be46954 100644
--- a/testing/resources/annots_action_handling.in
+++ b/testing/resources/annots_action_handling.in
@@ -60,7 +60,7 @@
   /A <<
     /Type /Action
     /S /URI
-    /URI (https://www.cs.chromium.org/)
+    /URI (https://cs.chromium.org/)
   >>
   /F 4
 >>
diff --git a/testing/resources/annots_action_handling.pdf b/testing/resources/annots_action_handling.pdf
index 49ea926..92dc830 100644
--- a/testing/resources/annots_action_handling.pdf
+++ b/testing/resources/annots_action_handling.pdf
@@ -61,7 +61,7 @@
   /A <<
     /Type /Action
     /S /URI
-    /URI (https://www.cs.chromium.org/)
+    /URI (https://cs.chromium.org/)
   >>
   /F 4
 >>
@@ -81,5 +81,5 @@
   /Size 8
 >>
 startxref
-834
+830
 %%EOF
\ No newline at end of file