diff --git a/pdf/accessibility_structs.cc b/pdf/accessibility_structs.cc index bfeb2f57598257..6799b2d16bb084 100644 --- a/pdf/accessibility_structs.cc +++ b/pdf/accessibility_structs.cc @@ -6,6 +6,16 @@ namespace chrome_pdf { +bool AccessibilityDocInfo::operator==(const AccessibilityDocInfo& other) const { + return page_count == other.page_count && + text_accessible == other.text_accessible && + text_copyable == other.text_copyable; +} + +bool AccessibilityDocInfo::operator!=(const AccessibilityDocInfo& other) const { + return !(*this == other); +} + AccessibilityTextStyleInfo::AccessibilityTextStyleInfo() = default; AccessibilityTextStyleInfo::AccessibilityTextStyleInfo( diff --git a/pdf/accessibility_structs.h b/pdf/accessibility_structs.h index 8fa1b17818fca9..a3aff9f3d0e6e8 100644 --- a/pdf/accessibility_structs.h +++ b/pdf/accessibility_structs.h @@ -17,6 +17,9 @@ namespace chrome_pdf { struct AccessibilityDocInfo { + bool operator==(const AccessibilityDocInfo& other) const; + bool operator!=(const AccessibilityDocInfo& other) const; + uint32_t page_count = 0; bool text_accessible = false; bool text_copyable = false; diff --git a/pdf/pdf_view_plugin_base.h b/pdf/pdf_view_plugin_base.h index a632859db8c8ac..e00cfa9a971a88 100644 --- a/pdf/pdf_view_plugin_base.h +++ b/pdf/pdf_view_plugin_base.h @@ -61,6 +61,16 @@ class PdfViewPluginBase : public PDFEngine::Client, public: using PDFEngine::Client::ScheduleTaskOnMainThread; + // Do not save files with over 100 MB. This cap should be kept in sync with + // and is also enforced in chrome/browser/resources/pdf/pdf_viewer.js. + static constexpr size_t kMaximumSavedFileSize = 100 * 1000 * 1000; + + enum class AccessibilityState { + kOff = 0, // Off. + kPending, // Enabled but waiting for doc to load. + kLoaded, // Fully loaded. + }; + enum class DocumentLoadState { kLoading = 0, kComplete, @@ -148,16 +158,6 @@ class PdfViewPluginBase : public PDFEngine::Client, } protected: - // Do not save files with over 100 MB. This cap should be kept in sync with - // and is also enforced in chrome/browser/resources/pdf/pdf_viewer.js. - static constexpr size_t kMaximumSavedFileSize = 100 * 1000 * 1000; - - enum class AccessibilityState { - kOff = 0, // Off. - kPending, // Enabled but waiting for doc to load. - kLoaded, // Fully loaded. - }; - struct BackgroundPart { gfx::Rect location; uint32_t color; diff --git a/pdf/pdf_view_plugin_base_unittest.cc b/pdf/pdf_view_plugin_base_unittest.cc index b3ee998d5f0f0e..1b6d36c932a6c1 100644 --- a/pdf/pdf_view_plugin_base_unittest.cc +++ b/pdf/pdf_view_plugin_base_unittest.cc @@ -12,11 +12,14 @@ #include "base/containers/contains.h" #include "base/containers/flat_set.h" #include "base/memory/weak_ptr.h" +#include "base/test/icu_test_util.h" #include "base/time/time.h" #include "base/values.h" #include "pdf/accessibility_structs.h" #include "pdf/buildflags.h" #include "pdf/content_restriction.h" +#include "pdf/document_attachment_info.h" +#include "pdf/document_metadata.h" #include "pdf/pdf_engine.h" #include "pdf/pdfium/pdfium_engine.h" #include "pdf/ppapi_migration/callback.h" @@ -24,6 +27,7 @@ #include "pdf/ppapi_migration/url_loader.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/gfx/geometry/size.h" @@ -56,10 +60,24 @@ class TestPDFiumEngine : public PDFiumEngine { return base::Contains(permissions_, permission); } + const std::vector& GetDocumentAttachmentInfoList() + const override { + return doc_attachment_info_list_; + } + + const DocumentMetadata& GetDocumentMetadata() const override { + return metadata_; + } + int GetNumberOfPages() const override { return static_cast(kPageNumber); } + base::Value GetBookmarks() override { + // Return an empty bookmark list. + return base::Value(base::Value::Type::LIST); + } + uint32_t GetLoadedByteSize() override { return sizeof(kSaveData); } bool ReadLoadedBytes(uint32_t length, void* buffer) override { @@ -80,16 +98,112 @@ class TestPDFiumEngine : public PDFiumEngine { permissions_.insert(permission); } + protected: + std::vector& doc_attachment_info_list() { + return doc_attachment_info_list_; + } + + DocumentMetadata& metadata() { return metadata_; } + private: + std::vector doc_attachment_info_list_; + + DocumentMetadata metadata_; + base::flat_set permissions_; }; +class TestPDFiumEngineWithDocInfo : public TestPDFiumEngine { + public: + explicit TestPDFiumEngineWithDocInfo(PDFEngine::Client* client) + : TestPDFiumEngine(client) {} + + base::Value GetBookmarks() override { + // Create `bookmark1` which navigates to an in-doc position. This bookmark + // will be in the top-level bookmark list. + base::Value bookmark1(base::Value::Type::DICTIONARY); + bookmark1.SetStringKey("title", "Bookmark 1"); + bookmark1.SetIntKey("page", 2); + bookmark1.SetIntKey("x", 10); + bookmark1.SetIntKey("y", 20); + bookmark1.SetDoubleKey("zoom", 2.0); + + // Create `bookmark2` which navigates to a web page. This bookmark will be a + // child of `bookmark1`. + base::Value bookmark2(base::Value::Type::DICTIONARY); + bookmark2.SetStringKey("title", "Bookmark 2"); + bookmark2.SetStringKey("uri", "test.com"); + + base::Value children_of_bookmark1(base::Value::Type::LIST); + children_of_bookmark1.Append(std::move(bookmark2)); + bookmark1.SetKey("children", std::move(children_of_bookmark1)); + + // Create the top-level bookmark list. + base::Value bookmarks(base::Value::Type::LIST); + bookmarks.Append(std::move(bookmark1)); + return bookmarks; + } + + absl::optional GetUniformPageSizePoints() override { + return gfx::Size(1000, 1200); + } + + // Initialize attachments, metadata for testing. + void InitializeDocument() { + InitializeDocumentAttachments(); + InitializeDocumentMetadata(); + } + + private: + void InitializeDocumentAttachments() { + doc_attachment_info_list().resize(3); + + // A regular attachment. + doc_attachment_info_list()[0].name = u"attachment1.txt"; + doc_attachment_info_list()[0].creation_date = u"D:20170712214438-07'00'"; + doc_attachment_info_list()[0].modified_date = u"D:20160115091400"; + doc_attachment_info_list()[0].is_readable = true; + doc_attachment_info_list()[0].size_bytes = 13u; + + // An unreadable attachment. + doc_attachment_info_list()[1].name = u"attachment2.pdf"; + doc_attachment_info_list()[1].is_readable = false; + + // A readable attachment that exceeds download size limit. + doc_attachment_info_list()[2].name = u"attachment3.mov"; + doc_attachment_info_list()[2].is_readable = true; + doc_attachment_info_list()[2].size_bytes = + PdfViewPluginBase::kMaximumSavedFileSize + 1; + } + + void InitializeDocumentMetadata() { + metadata().version = PdfVersion::k1_7; + metadata().size_bytes = 13u; + metadata().page_count = 13u; + metadata().linearized = true; + metadata().has_attachments = true; + metadata().tagged = true; + metadata().form_type = FormType::kAcroForm; + metadata().title = "Title"; + metadata().author = "Author"; + metadata().subject = "Subject"; + metadata().keywords = "Keywords"; + metadata().creator = "Creator"; + metadata().producer = "Producer"; + ASSERT_TRUE(base::Time::FromUTCString("2021-05-04 11:12:13", + &metadata().creation_date)); + ASSERT_TRUE( + base::Time::FromUTCString("2021-06-04 15:16:17", &metadata().mod_date)); + } +}; + // This test approach relies on PdfViewPluginBase continuing to exist. // PdfViewPluginBase and PdfViewWebPlugin are going to merge once // OutOfProcessInstance is deprecated. class FakePdfViewPluginBase : public PdfViewPluginBase { public: // Public for testing. + using PdfViewPluginBase::accessibility_state; using PdfViewPluginBase::document_load_state; using PdfViewPluginBase::edit_mode; using PdfViewPluginBase::engine; @@ -195,6 +309,93 @@ class FakePdfViewPluginBase : public PdfViewPluginBase { std::vector sent_messages_; }; +base::Value CreateExpectedFormTextFieldFocusChangeResponse() { + base::Value message(base::Value::Type::DICTIONARY); + message.SetStringKey("type", "formFocusChange"); + message.SetBoolKey("focused", false); + return message; +} + +base::Value CreateExpectedAttachmentsResponse() { + base::Value attachments(base::Value::Type::LIST); + { + base::Value attachment(base::Value::Type::DICTIONARY); + attachment.SetStringKey("name", "attachment1.txt"); + attachment.SetIntKey("size", 13); + attachment.SetBoolKey("readable", true); + attachments.Append(std::move(attachment)); + } + { + base::Value attachment(base::Value::Type::DICTIONARY); + attachment.SetStringKey("name", "attachment2.pdf"); + attachment.SetIntKey("size", 0); + attachment.SetBoolKey("readable", false); + attachments.Append(std::move(attachment)); + } + { + base::Value attachment(base::Value::Type::DICTIONARY); + attachment.SetStringKey("name", "attachment3.mov"); + attachment.SetIntKey("size", -1); + attachment.SetBoolKey("readable", true); + attachments.Append(std::move(attachment)); + } + + base::Value message(base::Value::Type::DICTIONARY); + message.SetStringKey("type", "attachments"); + message.SetKey("attachmentsData", std::move(attachments)); + return message; +} + +base::Value CreateExpectedBookmarksResponse(base::Value bookmarks) { + base::Value message(base::Value::Type::DICTIONARY); + message.SetStringKey("type", "bookmarks"); + message.SetKey("bookmarksData", std::move(bookmarks)); + return message; +} + +base::Value CreateExpectedMetadataResponse() { + base::Value metadata(base::Value::Type::DICTIONARY); + metadata.SetStringKey("version", "1.7"); + metadata.SetStringKey("fileSize", "13 B"); + metadata.SetBoolKey("linearized", true); + + metadata.SetStringKey("title", "Title"); + metadata.SetStringKey("author", "Author"); + metadata.SetStringKey("subject", "Subject"); + metadata.SetStringKey("keywords", "Keywords"); + metadata.SetStringKey("creator", "Creator"); + metadata.SetStringKey("producer", "Producer"); + metadata.SetStringKey("creationDate", "5/4/21, 4:12:13 AM"); + metadata.SetStringKey("modDate", "6/4/21, 8:16:17 AM"); + metadata.SetStringKey("pageSize", "13.89 × 16.67 in (portrait)"); + metadata.SetBoolKey("canSerializeDocument", true); + + base::Value message(base::Value::Type::DICTIONARY); + message.SetStringKey("type", "metadata"); + message.SetKey("metadataData", std::move(metadata)); + return message; +} + +base::Value CreateExpectedNoMetadataResponse() { + base::Value metadata(base::Value::Type::DICTIONARY); + metadata.SetStringKey("fileSize", "0 B"); + metadata.SetBoolKey("linearized", false); + metadata.SetStringKey("pageSize", "Varies"); + metadata.SetBoolKey("canSerializeDocument", true); + + base::Value message(base::Value::Type::DICTIONARY); + message.SetStringKey("type", "metadata"); + message.SetKey("metadataData", std::move(metadata)); + return message; +} + +base::Value CreateExpectedLoadingProgressResponse() { + base::Value message(base::Value::Type::DICTIONARY); + message.SetStringKey("type", "loadProgress"); + message.SetDoubleKey("progress", 100); + return message; +} + base::Value CreateSaveRequestMessage(PdfViewPluginBase::SaveRequestType type, const std::string& token) { base::Value message(base::Value::Type::DICTIONARY); @@ -237,6 +438,30 @@ class PdfViewPluginBaseWithEngineTest : public PdfViewPluginBaseTest { } }; +class PdfViewPluginBaseWithScopedLocaleTest + : public PdfViewPluginBaseWithEngineTest { + protected: + base::test::ScopedRestoreICUDefaultLocale scoped_locale_{"en_US"}; + base::test::ScopedRestoreDefaultTimezone la_time_{"America/Los_Angeles"}; +}; + +class PdfViewPluginBaseWithDocInfoTest + : public PdfViewPluginBaseWithScopedLocaleTest { + public: + void SetUp() override { + std::unique_ptr engine = + std::make_unique(&fake_plugin_); + fake_plugin_.InitializeEngine(std::move(engine)); + + // Initialize some arbitrary document information for the engine. + static_cast(fake_plugin_.engine()) + ->InitializeDocument(); + } +}; + +using PdfViewPluginBaseWithoutDocInfoTest = + PdfViewPluginBaseWithScopedLocaleTest; + TEST_F(PdfViewPluginBaseTest, CreateUrlLoaderInFullFrame) { fake_plugin_.set_full_frame(true); ASSERT_TRUE(fake_plugin_.full_frame()); @@ -263,6 +488,151 @@ TEST_F(PdfViewPluginBaseTest, CreateUrlLoaderWithoutFullFrame) { EXPECT_FALSE(fake_plugin_.GetDidCallStartLoadingForTesting()); } +TEST_F(PdfViewPluginBaseWithDocInfoTest, + DocumentLoadCompleteInFullFramePdfViewerWithAccessibilityEnabled) { + // Notify the render frame about document loading. + fake_plugin_.set_full_frame(true); + ASSERT_TRUE(fake_plugin_.full_frame()); + fake_plugin_.CreateUrlLoader(); + + ASSERT_FALSE(fake_plugin_.IsPrintPreview()); + ASSERT_EQ(PdfViewPluginBase::DocumentLoadState::kLoading, + fake_plugin_.document_load_state()); + + // Change the accessibility state to pending so that accessibility can be + // loaded later. + fake_plugin_.EnableAccessibility(); + EXPECT_EQ(PdfViewPluginBase::AccessibilityState::kPending, + fake_plugin_.accessibility_state()); + + EXPECT_CALL(fake_plugin_, UserMetricsRecordAction("PDF.LoadSuccess")); + EXPECT_CALL(fake_plugin_, SetFormFieldInFocus(false)); + EXPECT_CALL(fake_plugin_, PluginDidStopLoading()); + EXPECT_CALL(fake_plugin_, + SetContentRestrictions(fake_plugin_.GetContentRestrictions())); + EXPECT_CALL(fake_plugin_, + SetAccessibilityDocInfo(fake_plugin_.GetAccessibilityDocInfo())); + + fake_plugin_.DocumentLoadComplete(); + EXPECT_EQ(PdfViewPluginBase::DocumentLoadState::kComplete, + fake_plugin_.document_load_state()); + EXPECT_EQ(PdfViewPluginBase::AccessibilityState::kLoaded, + fake_plugin_.accessibility_state()); + + // Check all the sent messages. + ASSERT_EQ(5u, fake_plugin_.sent_messages().size()); + EXPECT_EQ(CreateExpectedFormTextFieldFocusChangeResponse(), + fake_plugin_.sent_messages()[0]); + EXPECT_EQ(CreateExpectedAttachmentsResponse(), + fake_plugin_.sent_messages()[1]); + EXPECT_EQ( + CreateExpectedBookmarksResponse(fake_plugin_.engine()->GetBookmarks()), + fake_plugin_.sent_messages()[2]); + EXPECT_EQ(CreateExpectedMetadataResponse(), fake_plugin_.sent_messages()[3]); + EXPECT_EQ(CreateExpectedLoadingProgressResponse(), + fake_plugin_.sent_messages()[4]); +} + +TEST_F(PdfViewPluginBaseWithDocInfoTest, + DocumentLoadCompleteInFullFramePdfViewerWithAccessibilityDisabled) { + // Notify the render frame about document loading. + fake_plugin_.set_full_frame(true); + ASSERT_TRUE(fake_plugin_.full_frame()); + fake_plugin_.CreateUrlLoader(); + + ASSERT_FALSE(fake_plugin_.IsPrintPreview()); + ASSERT_EQ(PdfViewPluginBase::DocumentLoadState::kLoading, + fake_plugin_.document_load_state()); + ASSERT_EQ(PdfViewPluginBase::AccessibilityState::kOff, + fake_plugin_.accessibility_state()); + + EXPECT_CALL(fake_plugin_, UserMetricsRecordAction("PDF.LoadSuccess")); + EXPECT_CALL(fake_plugin_, SetFormFieldInFocus(false)); + EXPECT_CALL(fake_plugin_, PluginDidStopLoading()); + EXPECT_CALL(fake_plugin_, + SetContentRestrictions(fake_plugin_.GetContentRestrictions())); + EXPECT_CALL(fake_plugin_, + SetAccessibilityDocInfo(fake_plugin_.GetAccessibilityDocInfo())) + .Times(0); + + fake_plugin_.DocumentLoadComplete(); + EXPECT_EQ(PdfViewPluginBase::DocumentLoadState::kComplete, + fake_plugin_.document_load_state()); + EXPECT_EQ(PdfViewPluginBase::AccessibilityState::kOff, + fake_plugin_.accessibility_state()); + + // Check all the sent messages. + ASSERT_EQ(5u, fake_plugin_.sent_messages().size()); + EXPECT_EQ(CreateExpectedFormTextFieldFocusChangeResponse(), + fake_plugin_.sent_messages()[0]); + EXPECT_EQ(CreateExpectedAttachmentsResponse(), + fake_plugin_.sent_messages()[1]); + EXPECT_EQ( + CreateExpectedBookmarksResponse(fake_plugin_.engine()->GetBookmarks()), + fake_plugin_.sent_messages()[2]); + EXPECT_EQ(CreateExpectedMetadataResponse(), fake_plugin_.sent_messages()[3]); + EXPECT_EQ(CreateExpectedLoadingProgressResponse(), + fake_plugin_.sent_messages()[4]); +} + +TEST_F(PdfViewPluginBaseWithDocInfoTest, + DocumentLoadCompleteInNonFullFramePdfViewer) { + ASSERT_FALSE(fake_plugin_.full_frame()); + fake_plugin_.CreateUrlLoader(); + + ASSERT_FALSE(fake_plugin_.IsPrintPreview()); + ASSERT_EQ(PdfViewPluginBase::DocumentLoadState::kLoading, + fake_plugin_.document_load_state()); + + EXPECT_CALL(fake_plugin_, UserMetricsRecordAction("PDF.LoadSuccess")); + EXPECT_CALL(fake_plugin_, SetFormFieldInFocus(false)); + EXPECT_CALL(fake_plugin_, PluginDidStopLoading()).Times(0); + EXPECT_CALL(fake_plugin_, + SetContentRestrictions(fake_plugin_.GetContentRestrictions())) + .Times(0); + + fake_plugin_.DocumentLoadComplete(); + EXPECT_EQ(PdfViewPluginBase::DocumentLoadState::kComplete, + fake_plugin_.document_load_state()); + + // Check all the sent messages. + ASSERT_EQ(5u, fake_plugin_.sent_messages().size()); + EXPECT_EQ(CreateExpectedFormTextFieldFocusChangeResponse(), + fake_plugin_.sent_messages()[0]); + EXPECT_EQ(CreateExpectedAttachmentsResponse(), + fake_plugin_.sent_messages()[1]); + EXPECT_EQ( + CreateExpectedBookmarksResponse(fake_plugin_.engine()->GetBookmarks()), + fake_plugin_.sent_messages()[2]); + EXPECT_EQ(CreateExpectedMetadataResponse(), fake_plugin_.sent_messages()[3]); + EXPECT_EQ(CreateExpectedLoadingProgressResponse(), + fake_plugin_.sent_messages()[4]); +} + +TEST_F(PdfViewPluginBaseWithoutDocInfoTest, DocumentLoadCompletePostMessages) { + fake_plugin_.CreateUrlLoader(); + + ASSERT_FALSE(fake_plugin_.IsPrintPreview()); + ASSERT_EQ(PdfViewPluginBase::DocumentLoadState::kLoading, + fake_plugin_.document_load_state()); + EXPECT_CALL(fake_plugin_, UserMetricsRecordAction("PDF.LoadSuccess")); + EXPECT_CALL(fake_plugin_, SetFormFieldInFocus(false)); + + fake_plugin_.DocumentLoadComplete(); + EXPECT_EQ(PdfViewPluginBase::DocumentLoadState::kComplete, + fake_plugin_.document_load_state()); + + // Check the sent messages when the document doesn't have any metadata, + // attachments or bookmarks. + ASSERT_EQ(3u, fake_plugin_.sent_messages().size()); + EXPECT_EQ(CreateExpectedFormTextFieldFocusChangeResponse(), + fake_plugin_.sent_messages()[0]); + EXPECT_EQ(CreateExpectedNoMetadataResponse(), + fake_plugin_.sent_messages()[1]); + EXPECT_EQ(CreateExpectedLoadingProgressResponse(), + fake_plugin_.sent_messages()[2]); +} + TEST_F(PdfViewPluginBaseTest, DocumentLoadFailedWithNotifiedRenderFrame) { // Notify the render frame about document loading. fake_plugin_.set_full_frame(true);