paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 1 | // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "base/command_line.h" |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 6 | #include "base/files/file_util.h" |
| 7 | #include "base/path_service.h" |
Jan Wilken Dörrie | f05bb10 | 2020-08-18 19:35:56 | [diff] [blame] | 8 | #include "base/strings/string_util.h" |
Lei Zhang | e02299a | 2021-04-26 23:12:24 | [diff] [blame] | 9 | #include "base/strings/stringprintf.h" |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 10 | #include "base/strings/utf_string_conversions.h" |
K. Moon | 565fc1c8e | 2020-09-10 15:53:14 | [diff] [blame] | 11 | #include "base/test/scoped_feature_list.h" |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 12 | #include "base/threading/thread_restrictions.h" |
Takumi Fujimoto | a509410 | 2019-12-03 23:45:13 | [diff] [blame] | 13 | #include "build/build_config.h" |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 14 | #include "chrome/browser/pdf/pdf_extension_test_util.h" |
| 15 | #include "chrome/browser/ui/browser.h" |
| 16 | #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| 17 | #include "chrome/test/base/in_process_browser_test.h" |
| 18 | #include "chrome/test/base/ui_test_utils.h" |
paulmeyer | feafc2d | 2017-04-25 21:46:40 | [diff] [blame] | 19 | #include "content/public/browser/browser_context.h" |
| 20 | #include "content/public/browser/browser_plugin_guest_manager.h" |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 21 | #include "content/public/browser/navigation_controller.h" |
| 22 | #include "content/public/browser/web_contents.h" |
| 23 | #include "content/public/common/content_paths.h" |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 24 | #include "content/public/common/content_switches.h" |
Peter Kasting | 919ce65 | 2020-05-07 10:22:36 | [diff] [blame] | 25 | #include "content/public/test/browser_test.h" |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 26 | #include "content/public/test/browser_test_utils.h" |
| 27 | #include "content/public/test/find_test_utils.h" |
| 28 | #include "content/public/test/test_navigation_observer.h" |
| 29 | #include "net/dns/mock_host_resolver.h" |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 30 | #include "net/test/embedded_test_server/controllable_http_response.h" |
| 31 | #include "pdf/document_loader_impl.h" |
K. Moon | 565fc1c8e | 2020-09-10 15:53:14 | [diff] [blame] | 32 | #include "pdf/pdf_features.h" |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 33 | #include "third_party/blink/public/mojom/frame/find_in_page.mojom.h" |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 34 | |
| 35 | namespace content { |
| 36 | |
| 37 | class ChromeFindRequestManagerTest : public InProcessBrowserTest { |
| 38 | public: |
| 39 | ChromeFindRequestManagerTest() |
| 40 | : normal_delegate_(nullptr), |
| 41 | last_request_id_(0) {} |
| 42 | ~ChromeFindRequestManagerTest() override {} |
| 43 | |
| 44 | void SetUpOnMainThread() override { |
| 45 | host_resolver()->AddRule("*", "127.0.0.1"); |
| 46 | embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 47 | |
| 48 | // Swap the WebContents's delegate for our test delegate. |
| 49 | normal_delegate_ = contents()->GetDelegate(); |
| 50 | contents()->SetDelegate(&test_delegate_); |
| 51 | } |
| 52 | |
| 53 | void TearDownOnMainThread() override { |
| 54 | // Swap the WebContents's delegate back to its usual delegate. |
| 55 | contents()->SetDelegate(normal_delegate_); |
| 56 | } |
| 57 | |
| 58 | protected: |
| 59 | // Navigates to |url| and waits for it to finish loading. |
| 60 | void LoadAndWait(const std::string& url) { |
| 61 | TestNavigationObserver navigation_observer(contents()); |
| 62 | ui_test_utils::NavigateToURLBlockUntilNavigationsComplete( |
| 63 | browser(), embedded_test_server()->GetURL("a.com", url), 1); |
| 64 | ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| 65 | } |
| 66 | |
| 67 | void Find(const std::string& search_text, |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 68 | blink::mojom::FindOptionsPtr options) { |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 69 | delegate()->UpdateLastRequest(++last_request_id_); |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 70 | contents()->Find(last_request_id_, base::UTF8ToUTF16(search_text), |
| 71 | std::move(options)); |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 72 | } |
| 73 | |
| 74 | WebContents* contents() const { |
| 75 | return browser()->tab_strip_model()->GetActiveWebContents(); |
| 76 | } |
| 77 | |
| 78 | FindTestWebContentsDelegate* delegate() const { |
| 79 | return static_cast<FindTestWebContentsDelegate*>(contents()->GetDelegate()); |
| 80 | } |
| 81 | |
| 82 | int last_request_id() const { |
| 83 | return last_request_id_; |
| 84 | } |
| 85 | |
| 86 | private: |
| 87 | FindTestWebContentsDelegate test_delegate_; |
| 88 | WebContentsDelegate* normal_delegate_; |
| 89 | |
| 90 | // The ID of the last find request requested. |
| 91 | int last_request_id_; |
| 92 | |
| 93 | DISALLOW_COPY_AND_ASSIGN(ChromeFindRequestManagerTest); |
| 94 | }; |
| 95 | |
K. Moon | 565fc1c8e | 2020-09-10 15:53:14 | [diff] [blame] | 96 | class ChromeFindRequestManagerTestWithPdfPartialLoading |
| 97 | : public ChromeFindRequestManagerTest { |
| 98 | public: |
| 99 | ChromeFindRequestManagerTestWithPdfPartialLoading() { |
| 100 | feature_list_.InitWithFeatures( |
| 101 | {chrome_pdf::features::kPdfIncrementalLoading, |
| 102 | chrome_pdf::features::kPdfPartialLoading}, |
| 103 | {}); |
| 104 | } |
| 105 | |
| 106 | private: |
| 107 | base::test::ScopedFeatureList feature_list_; |
| 108 | }; |
| 109 | |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 110 | // Tests searching in a full-page PDF. |
Takumi Fujimoto | a509410 | 2019-12-03 23:45:13 | [diff] [blame] | 111 | // Flaky on Windows ASAN: crbug.com/1030368. |
| 112 | #if defined(OS_WIN) && defined(ADDRESS_SANITIZER) |
| 113 | #define MAYBE_FindInPDF DISABLED_FindInPDF |
| 114 | #else |
| 115 | #define MAYBE_FindInPDF FindInPDF |
| 116 | #endif |
| 117 | IN_PROC_BROWSER_TEST_F(ChromeFindRequestManagerTest, MAYBE_FindInPDF) { |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 118 | ASSERT_TRUE(embedded_test_server()->Start()); |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 119 | LoadAndWait("/find_in_pdf_page.pdf"); |
paulmeyer | feafc2d | 2017-04-25 21:46:40 | [diff] [blame] | 120 | ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(contents())); |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 121 | |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 122 | auto options = blink::mojom::FindOptions::New(); |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 123 | Find("result", options.Clone()); |
W. James MacLean | 031de92 | 2020-03-26 18:39:31 | [diff] [blame] | 124 | delegate()->MarkNextReply(); |
| 125 | delegate()->WaitForNextReply(); |
| 126 | |
Russell Davis | 8a36226c | 2020-06-05 17:09:50 | [diff] [blame] | 127 | options->new_session = false; |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 128 | Find("result", options.Clone()); |
W. James MacLean | 031de92 | 2020-03-26 18:39:31 | [diff] [blame] | 129 | delegate()->MarkNextReply(); |
| 130 | delegate()->WaitForNextReply(); |
| 131 | |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 132 | Find("result", options.Clone()); |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 133 | delegate()->WaitForFinalReply(); |
| 134 | |
| 135 | FindResults results = delegate()->GetFindResults(); |
| 136 | EXPECT_EQ(last_request_id(), results.request_id); |
| 137 | EXPECT_EQ(5, results.number_of_matches); |
| 138 | EXPECT_EQ(3, results.active_match_ordinal); |
| 139 | } |
| 140 | |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 141 | void SendRangeResponse(net::test_server::ControllableHttpResponse* response, |
| 142 | const std::string& pdf_contents) { |
| 143 | int range_start = -1; |
| 144 | int range_end = -1; |
| 145 | { |
| 146 | auto it = response->http_request()->headers.find("Range"); |
| 147 | ASSERT_NE(response->http_request()->headers.end(), it); |
| 148 | base::StringPiece range_header = it->second; |
| 149 | base::StringPiece kBytesPrefix = "bytes="; |
Jan Wilken Dörrie | f05bb10 | 2020-08-18 19:35:56 | [diff] [blame] | 150 | ASSERT_TRUE(base::StartsWith(range_header, kBytesPrefix)); |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 151 | range_header.remove_prefix(kBytesPrefix.size()); |
| 152 | auto dash_pos = range_header.find('-'); |
| 153 | ASSERT_NE(std::string::npos, dash_pos); |
| 154 | ASSERT_LT(0u, dash_pos); |
| 155 | ASSERT_LT(dash_pos, range_header.size() - 1); |
| 156 | ASSERT_TRUE( |
| 157 | base::StringToInt(range_header.substr(0, dash_pos), &range_start)); |
| 158 | ASSERT_TRUE( |
| 159 | base::StringToInt(range_header.substr(dash_pos + 1), &range_end)); |
| 160 | } |
| 161 | ASSERT_LT(0, range_start); |
| 162 | ASSERT_LT(range_start, range_end); |
| 163 | ASSERT_LT(static_cast<size_t>(range_end), pdf_contents.size()); |
| 164 | int range_length = range_end - range_start + 1; |
| 165 | response->Send("HTTP/1.1 206 Partial Content\r\n"); |
| 166 | response->Send(base::StringPrintf("Content-Range: bytes %d-%d/%zu\r\n", |
| 167 | range_start, range_end, |
| 168 | pdf_contents.size())); |
| 169 | response->Send(base::StringPrintf("Content-Length: %d\r\n", range_length)); |
| 170 | response->Send("\r\n"); |
| 171 | response->Send(pdf_contents.substr(range_start, range_length)); |
| 172 | response->Done(); |
| 173 | } |
| 174 | |
| 175 | // Tests searching in a PDF received in chunks via range-requests. See also |
| 176 | // https://crbug.com/1027173. |
K. Moon | 565fc1c8e | 2020-09-10 15:53:14 | [diff] [blame] | 177 | IN_PROC_BROWSER_TEST_F(ChromeFindRequestManagerTestWithPdfPartialLoading, |
| 178 | FindInChunkedPDF) { |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 179 | constexpr uint32_t kStalledResponseSize = |
| 180 | chrome_pdf::DocumentLoaderImpl::kDefaultRequestSize + 123; |
| 181 | |
| 182 | // Load contents of a big, linearized pdf test file. |
| 183 | // See also //content/test/data/linearized.pdf.README file. |
| 184 | std::string pdf_contents; |
| 185 | { |
| 186 | base::ScopedAllowBlockingForTesting allow_blocking_io; |
| 187 | base::FilePath content_test_dir; |
| 188 | ASSERT_TRUE( |
| 189 | base::PathService::Get(content::DIR_TEST_DATA, &content_test_dir)); |
| 190 | base::FilePath real_pdf_path = |
| 191 | content_test_dir.AppendASCII("linearized.pdf"); |
| 192 | ASSERT_TRUE(base::ReadFileToString(real_pdf_path, &pdf_contents)); |
| 193 | } |
| 194 | DCHECK_GT(pdf_contents.size(), kStalledResponseSize); |
| 195 | |
| 196 | // Set up handling of HTTP responses from within the test. |
| 197 | const char kSimulatedPdfPath[] = "/simulated/chunked.pdf"; |
| 198 | net::test_server::ControllableHttpResponse nav_response( |
| 199 | embedded_test_server(), kSimulatedPdfPath); |
| 200 | net::test_server::ControllableHttpResponse range_response1( |
| 201 | embedded_test_server(), kSimulatedPdfPath); |
| 202 | net::test_server::ControllableHttpResponse range_response2( |
| 203 | embedded_test_server(), kSimulatedPdfPath); |
| 204 | ASSERT_TRUE(embedded_test_server()->Start()); |
| 205 | GURL pdf_url = embedded_test_server()->GetURL("a.com", kSimulatedPdfPath); |
| 206 | |
| 207 | // Kick-off browser-initiated navigation to a PDF file. |
| 208 | content::WebContents* web_contents = |
| 209 | browser()->tab_strip_model()->GetActiveWebContents(); |
| 210 | TestNavigationObserver navigation_observer(web_contents); |
| 211 | content::NavigationController::LoadURLParams params(pdf_url); |
| 212 | params.transition_type = ui::PageTransitionFromInt( |
| 213 | ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| 214 | web_contents->GetController().LoadURLWithParams(params); |
| 215 | |
| 216 | // Have the test HTTP server handle the 1st request (navigation). This |
| 217 | // request is handler in the test, rather than by embedded_test_server, to |
| 218 | // stall the request after it delivers the first kStalledResponseSize bytes of |
| 219 | // data (the stalling ensures that the range request will be processed in the |
| 220 | // next test steps). |
| 221 | nav_response.WaitForRequest(); |
| 222 | nav_response.Send("HTTP/1.1 200 OK\r\n"); |
| 223 | nav_response.Send("Accept-Ranges: bytes\r\n"); |
| 224 | nav_response.Send( |
| 225 | base::StringPrintf("Content-Length: %zu\r\n", pdf_contents.size())); |
| 226 | nav_response.Send("Content-Type: application/pdf\r\n"); |
| 227 | nav_response.Send("Pragma: no-cache\r\n"); |
| 228 | nav_response.Send("Cache-Control: no-cache, no-store, must-revalidate\r\n"); |
| 229 | nav_response.Send("\r\n"); |
| 230 | nav_response.Send(pdf_contents.substr(0, kStalledResponseSize)); |
| 231 | |
| 232 | // At this point the navigation should be considered successful (even though |
| 233 | // we haven't loaded all the bytes of the PDF yet). |
| 234 | navigation_observer.Wait(); |
| 235 | ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| 236 | |
| 237 | // Have the test handle the 2 range requests (subresource requests initiated |
| 238 | // by the PDF plugin and proxied through a renderer process for |
| 239 | // MimeHandlerView extension). These requests are handled in the test, rather |
| 240 | // than by embedded_test_server, to verify that we are indeed getting range |
| 241 | // requests (i.e. this is a sanity check that the test still tests the right |
| 242 | // thing). |
| 243 | range_response1.WaitForRequest(); |
| 244 | SendRangeResponse(&range_response1, pdf_contents); |
| 245 | range_response2.WaitForRequest(); |
| 246 | SendRangeResponse(&range_response2, pdf_contents); |
| 247 | |
| 248 | // Finish the first HTTP response and verify that the PDF has loaded |
| 249 | // successfully. |
| 250 | nav_response.Done(); |
| 251 | ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(contents())); |
| 252 | |
| 253 | // Verify that find-in-page works fine. |
| 254 | auto options = blink::mojom::FindOptions::New(); |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 255 | Find("FXCMAP_CMap", options.Clone()); |
Russell Davis | 8a36226c | 2020-06-05 17:09:50 | [diff] [blame] | 256 | options->new_session = false; |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 257 | Find("FXCMAP_CMap", options.Clone()); |
| 258 | Find("FXCMAP_CMap", options.Clone()); |
| 259 | delegate()->WaitForFinalReply(); |
| 260 | |
| 261 | FindResults results = delegate()->GetFindResults(); |
| 262 | EXPECT_EQ(last_request_id(), results.request_id); |
| 263 | EXPECT_EQ(15, results.number_of_matches); |
| 264 | EXPECT_EQ(3, results.active_match_ordinal); |
| 265 | } |
| 266 | |
paulmeyer | feafc2d | 2017-04-25 21:46:40 | [diff] [blame] | 267 | // Tests searching in a page with embedded PDFs. Note that this test, the |
| 268 | // FindInPDF test, and the find tests in web_view_browsertest.cc ensure that |
| 269 | // find-in-page works across GuestViews. |
| 270 | // |
| 271 | // TODO(paulmeyer): Note that this is left disabled for now since |
| 272 | // EnsurePDFHasLoaded() currently does not work for embedded PDFs. This will be |
| 273 | // fixed and enabled in a subsequent patch. |
| 274 | IN_PROC_BROWSER_TEST_F(ChromeFindRequestManagerTest, |
| 275 | DISABLED_FindInEmbeddedPDFs) { |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 276 | ASSERT_TRUE(embedded_test_server()->Start()); |
paulmeyer | feafc2d | 2017-04-25 21:46:40 | [diff] [blame] | 277 | LoadAndWait("/find_in_embedded_pdf_page.html"); |
| 278 | ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(contents())); |
| 279 | |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 280 | auto options = blink::mojom::FindOptions::New(); |
Russell Davis | 8a36226c | 2020-06-05 17:09:50 | [diff] [blame] | 281 | options->new_session = false; |
Rakina Zata Amni | 3f77dff | 2018-09-08 16:19:43 | [diff] [blame] | 282 | Find("result", options.Clone()); |
| 283 | options->forward = false; |
| 284 | Find("result", options.Clone()); |
| 285 | Find("result", options.Clone()); |
| 286 | Find("result", options.Clone()); |
paulmeyer | feafc2d | 2017-04-25 21:46:40 | [diff] [blame] | 287 | delegate()->WaitForFinalReply(); |
| 288 | |
| 289 | FindResults results = delegate()->GetFindResults(); |
| 290 | EXPECT_EQ(last_request_id(), results.request_id); |
| 291 | EXPECT_EQ(13, results.number_of_matches); |
| 292 | EXPECT_EQ(11, results.active_match_ordinal); |
| 293 | } |
| 294 | |
Manoj Biswas | 7a1f1fb | 2019-07-18 18:17:41 | [diff] [blame] | 295 | IN_PROC_BROWSER_TEST_F(ChromeFindRequestManagerTest, FindMissingStringInPDF) { |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 296 | ASSERT_TRUE(embedded_test_server()->Start()); |
Manoj Biswas | 7a1f1fb | 2019-07-18 18:17:41 | [diff] [blame] | 297 | LoadAndWait("/find_in_pdf_page.pdf"); |
| 298 | ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(contents())); |
| 299 | |
| 300 | auto options = blink::mojom::FindOptions::New(); |
Manoj Biswas | 7a1f1fb | 2019-07-18 18:17:41 | [diff] [blame] | 301 | Find("missing", options.Clone()); |
| 302 | delegate()->WaitForFinalReply(); |
| 303 | |
| 304 | FindResults results = delegate()->GetFindResults(); |
| 305 | EXPECT_EQ(last_request_id(), results.request_id); |
| 306 | EXPECT_EQ(0, results.number_of_matches); |
| 307 | EXPECT_EQ(0, results.active_match_ordinal); |
| 308 | } |
| 309 | |
| 310 | // Tests searching for a word character-by-character, as would typically be |
| 311 | // done by a user typing into the find bar. |
| 312 | IN_PROC_BROWSER_TEST_F(ChromeFindRequestManagerTest, |
| 313 | CharacterByCharacterFindInPDF) { |
Lukasz Anforowicz | ebee674e | 2020-01-08 05:26:31 | [diff] [blame] | 314 | ASSERT_TRUE(embedded_test_server()->Start()); |
Manoj Biswas | 7a1f1fb | 2019-07-18 18:17:41 | [diff] [blame] | 315 | LoadAndWait("/find_in_pdf_page.pdf"); |
| 316 | ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(contents())); |
| 317 | |
| 318 | auto options = blink::mojom::FindOptions::New(); |
Manoj Biswas | 7a1f1fb | 2019-07-18 18:17:41 | [diff] [blame] | 319 | Find("r", options.Clone()); |
| 320 | delegate()->MarkNextReply(); |
| 321 | delegate()->WaitForNextReply(); |
| 322 | Find("re", options.Clone()); |
| 323 | delegate()->MarkNextReply(); |
| 324 | delegate()->WaitForNextReply(); |
| 325 | Find("res", options.Clone()); |
| 326 | delegate()->MarkNextReply(); |
| 327 | delegate()->WaitForNextReply(); |
| 328 | Find("resu", options.Clone()); |
| 329 | delegate()->MarkNextReply(); |
| 330 | delegate()->WaitForNextReply(); |
| 331 | Find("resul", options.Clone()); |
| 332 | delegate()->MarkNextReply(); |
| 333 | delegate()->WaitForNextReply(); |
| 334 | Find("result", options.Clone()); |
| 335 | delegate()->WaitForFinalReply(); |
| 336 | |
| 337 | FindResults results = delegate()->GetFindResults(); |
| 338 | EXPECT_EQ(last_request_id(), results.request_id); |
| 339 | EXPECT_EQ(5, results.number_of_matches); |
| 340 | EXPECT_EQ(1, results.active_match_ordinal); |
| 341 | } |
| 342 | |
paulmeyer | da992f5 | 2017-01-27 17:11:28 | [diff] [blame] | 343 | } // namespace content |