1// Copyright (c) 2012 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 <string>
6
7#include "base/files/file_path.h"
8#include "base/files/scoped_temp_dir.h"
9#include "base/path_service.h"
10#include "base/strings/string_util.h"
11#include "base/strings/utf_string_conversions.h"
12#include "content/browser/download/save_package.h"
13#include "content/public/common/url_constants.h"
14#include "content/test/test_render_view_host.h"
15#include "content/test/test_web_contents.h"
16#include "net/test/url_request/url_request_mock_http_job.h"
17#include "testing/gtest/include/gtest/gtest.h"
18#include "url/gurl.h"
19
20namespace content {
21
22#define FPL FILE_PATH_LITERAL
23#define HTML_EXTENSION ".html"
24#if defined(OS_WIN)
25#define FPL_HTML_EXTENSION L".html"
26#else
27#define FPL_HTML_EXTENSION ".html"
28#endif
29
30namespace {
31
32// This constant copied from save_package.cc.
33#if defined(OS_WIN)
34const uint32 kMaxFilePathLength = MAX_PATH - 1;
35const uint32 kMaxFileNameLength = MAX_PATH - 1;
36#elif defined(OS_POSIX)
37const uint32 kMaxFilePathLength = PATH_MAX - 1;
38const uint32 kMaxFileNameLength = NAME_MAX;
39#endif
40
41// Used to make long filenames.
42std::string long_file_name(
43    "EFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz01234567"
44    "89ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz012345"
45    "6789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123"
46    "456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789a");
47
48bool HasOrdinalNumber(const base::FilePath::StringType& filename) {
49  base::FilePath::StringType::size_type r_paren_index =
50      filename.rfind(FPL(')'));
51  base::FilePath::StringType::size_type l_paren_index =
52      filename.rfind(FPL('('));
53  if (l_paren_index >= r_paren_index)
54    return false;
55
56  for (base::FilePath::StringType::size_type i = l_paren_index + 1;
57       i != r_paren_index; ++i) {
58    if (!IsAsciiDigit(filename[i]))
59      return false;
60  }
61
62  return true;
63}
64
65}  // namespace
66
67class SavePackageTest : public RenderViewHostImplTestHarness {
68 public:
69  bool GetGeneratedFilename(bool need_success_generate_filename,
70                            const std::string& disposition,
71                            const std::string& url,
72                            bool need_htm_ext,
73                            base::FilePath::StringType* generated_name) {
74    SavePackage* save_package;
75    if (need_success_generate_filename)
76      save_package = save_package_success_.get();
77    else
78      save_package = save_package_fail_.get();
79    return save_package->GenerateFileName(disposition, GURL(url), need_htm_ext,
80                                          generated_name);
81  }
82
83  base::FilePath EnsureHtmlExtension(const base::FilePath& name) {
84    return SavePackage::EnsureHtmlExtension(name);
85  }
86
87  base::FilePath EnsureMimeExtension(const base::FilePath& name,
88                               const std::string& content_mime_type) {
89    return SavePackage::EnsureMimeExtension(name, content_mime_type);
90  }
91
92  GURL GetUrlToBeSaved() {
93    return save_package_success_->GetUrlToBeSaved();
94  }
95
96 protected:
97  virtual void SetUp() {
98    RenderViewHostImplTestHarness::SetUp();
99
100    // Do the initialization in SetUp so contents() is initialized by
101    // RenderViewHostImplTestHarness::SetUp.
102    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
103
104    save_package_success_ = new SavePackage(contents(),
105        temp_dir_.path().AppendASCII("testfile" HTML_EXTENSION),
106        temp_dir_.path().AppendASCII("testfile_files"));
107
108    // We need to construct a path that is *almost* kMaxFilePathLength long
109    long_file_name.reserve(kMaxFilePathLength + long_file_name.length());
110    while (long_file_name.length() < kMaxFilePathLength)
111      long_file_name += long_file_name;
112    long_file_name.resize(
113        kMaxFilePathLength - 9 - temp_dir_.path().value().length());
114
115    save_package_fail_ = new SavePackage(contents(),
116        temp_dir_.path().AppendASCII(long_file_name + HTML_EXTENSION),
117        temp_dir_.path().AppendASCII(long_file_name + "_files"));
118  }
119
120 private:
121  // SavePackage for successfully generating file name.
122  scoped_refptr<SavePackage> save_package_success_;
123  // SavePackage for failed generating file name.
124  scoped_refptr<SavePackage> save_package_fail_;
125
126  base::ScopedTempDir temp_dir_;
127};
128
129static const struct {
130  const char* disposition;
131  const char* url;
132  const base::FilePath::CharType* expected_name;
133  bool need_htm_ext;
134} kGeneratedFiles[] = {
135  // We mainly focus on testing duplicated names here, since retrieving file
136  // name from disposition and url has been tested in DownloadManagerTest.
137
138  // No useful information in disposition or URL, use default.
139  {"1.html", "http://www.savepage.com/",
140    FPL("saved_resource") FPL_HTML_EXTENSION, true},
141
142  // No duplicate occurs.
143  {"filename=1.css", "http://www.savepage.com", FPL("1.css"), false},
144
145  // No duplicate occurs.
146  {"filename=1.js", "http://www.savepage.com", FPL("1.js"), false},
147
148  // Append numbers for duplicated names.
149  {"filename=1.css", "http://www.savepage.com", FPL("1(1).css"), false},
150
151  // No duplicate occurs.
152  {"filename=1(1).js", "http://www.savepage.com", FPL("1(1).js"), false},
153
154  // Append numbers for duplicated names.
155  {"filename=1.css", "http://www.savepage.com", FPL("1(2).css"), false},
156
157  // Change number for duplicated names.
158  {"filename=1(1).css", "http://www.savepage.com", FPL("1(3).css"), false},
159
160  // No duplicate occurs.
161  {"filename=1(11).css", "http://www.savepage.com", FPL("1(11).css"), false},
162
163  // Test for case-insensitive file names.
164  {"filename=readme.txt", "http://www.savepage.com",
165                          FPL("readme.txt"), false},
166
167  {"filename=readme.TXT", "http://www.savepage.com",
168                          FPL("readme(1).TXT"), false},
169
170  {"filename=READme.txt", "http://www.savepage.com",
171                          FPL("readme(2).txt"), false},
172
173  {"filename=Readme(1).txt", "http://www.savepage.com",
174                          FPL("readme(3).txt"), false},
175};
176
177TEST_F(SavePackageTest, TestSuccessfullyGenerateSavePackageFilename) {
178  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kGeneratedFiles); ++i) {
179    base::FilePath::StringType file_name;
180    bool ok = GetGeneratedFilename(true,
181                                   kGeneratedFiles[i].disposition,
182                                   kGeneratedFiles[i].url,
183                                   kGeneratedFiles[i].need_htm_ext,
184                                   &file_name);
185    ASSERT_TRUE(ok);
186    EXPECT_EQ(kGeneratedFiles[i].expected_name, file_name);
187  }
188}
189
190TEST_F(SavePackageTest, TestUnSuccessfullyGenerateSavePackageFilename) {
191  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kGeneratedFiles); ++i) {
192    base::FilePath::StringType file_name;
193    bool ok = GetGeneratedFilename(false,
194                                   kGeneratedFiles[i].disposition,
195                                   kGeneratedFiles[i].url,
196                                   kGeneratedFiles[i].need_htm_ext,
197                                   &file_name);
198    ASSERT_FALSE(ok);
199  }
200}
201
202// Crashing on Windows, see http://crbug.com/79365
203#if defined(OS_WIN)
204#define MAYBE_TestLongSavePackageFilename DISABLED_TestLongSavePackageFilename
205#else
206#define MAYBE_TestLongSavePackageFilename TestLongSavePackageFilename
207#endif
208TEST_F(SavePackageTest, MAYBE_TestLongSavePackageFilename) {
209  const std::string base_url("http://www.google.com/");
210  const std::string long_file = long_file_name + ".css";
211  const std::string url = base_url + long_file;
212
213  base::FilePath::StringType filename;
214  // Test that the filename is successfully shortened to fit.
215  ASSERT_TRUE(GetGeneratedFilename(true, std::string(), url, false, &filename));
216  EXPECT_TRUE(filename.length() < long_file.length());
217  EXPECT_FALSE(HasOrdinalNumber(filename));
218
219  // Test that the filename is successfully shortened to fit, and gets an
220  // an ordinal appended.
221  ASSERT_TRUE(GetGeneratedFilename(true, std::string(), url, false, &filename));
222  EXPECT_TRUE(filename.length() < long_file.length());
223  EXPECT_TRUE(HasOrdinalNumber(filename));
224
225  // Test that the filename is successfully shortened to fit, and gets a
226  // different ordinal appended.
227  base::FilePath::StringType filename2;
228  ASSERT_TRUE(
229      GetGeneratedFilename(true, std::string(), url, false, &filename2));
230  EXPECT_TRUE(filename2.length() < long_file.length());
231  EXPECT_TRUE(HasOrdinalNumber(filename2));
232  EXPECT_NE(filename, filename2);
233}
234
235// Crashing on Windows, see http://crbug.com/79365
236#if defined(OS_WIN)
237#define MAYBE_TestLongSafePureFilename DISABLED_TestLongSafePureFilename
238#else
239#define MAYBE_TestLongSafePureFilename TestLongSafePureFilename
240#endif
241TEST_F(SavePackageTest, MAYBE_TestLongSafePureFilename) {
242  const base::FilePath save_dir(FPL("test_dir"));
243  const base::FilePath::StringType ext(FPL_HTML_EXTENSION);
244  base::FilePath::StringType filename =
245#if defined(OS_WIN)
246      base::ASCIIToWide(long_file_name);
247#else
248      long_file_name;
249#endif
250
251  // Test that the filename + extension doesn't exceed kMaxFileNameLength
252  uint32 max_path = SavePackage::GetMaxPathLengthForDirectory(save_dir);
253  ASSERT_TRUE(SavePackage::GetSafePureFileName(save_dir, ext, max_path,
254                                               &filename));
255  EXPECT_TRUE(filename.length() <= kMaxFileNameLength-ext.length());
256}
257
258static const struct {
259  const base::FilePath::CharType* page_title;
260  const base::FilePath::CharType* expected_name;
261} kExtensionTestCases[] = {
262  // Extension is preserved if it is already proper for HTML.
263  {FPL("filename.html"), FPL("filename.html")},
264  {FPL("filename.HTML"), FPL("filename.HTML")},
265  {FPL("filename.XHTML"), FPL("filename.XHTML")},
266  {FPL("filename.xhtml"), FPL("filename.xhtml")},
267  {FPL("filename.htm"), FPL("filename.htm")},
268  // ".htm" is added if the extension is improper for HTML.
269  {FPL("hello.world"), FPL("hello.world") FPL_HTML_EXTENSION},
270  {FPL("hello.txt"), FPL("hello.txt") FPL_HTML_EXTENSION},
271  {FPL("is.html.good"), FPL("is.html.good") FPL_HTML_EXTENSION},
272  // ".htm" is added if the name doesn't have an extension.
273  {FPL("helloworld"), FPL("helloworld") FPL_HTML_EXTENSION},
274  {FPL("helloworld."), FPL("helloworld.") FPL_HTML_EXTENSION},
275};
276
277// Crashing on Windows, see http://crbug.com/79365
278#if defined(OS_WIN)
279#define MAYBE_TestEnsureHtmlExtension DISABLED_TestEnsureHtmlExtension
280#else
281#define MAYBE_TestEnsureHtmlExtension TestEnsureHtmlExtension
282#endif
283TEST_F(SavePackageTest, MAYBE_TestEnsureHtmlExtension) {
284  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kExtensionTestCases); ++i) {
285    base::FilePath original = base::FilePath(kExtensionTestCases[i].page_title);
286    base::FilePath expected =
287        base::FilePath(kExtensionTestCases[i].expected_name);
288    base::FilePath actual = EnsureHtmlExtension(original);
289    EXPECT_EQ(expected.value(), actual.value()) << "Failed for page title: " <<
290        kExtensionTestCases[i].page_title;
291  }
292}
293
294// Crashing on Windows, see http://crbug.com/79365
295#if defined(OS_WIN)
296#define MAYBE_TestEnsureMimeExtension DISABLED_TestEnsureMimeExtension
297#else
298#define MAYBE_TestEnsureMimeExtension TestEnsureMimeExtension
299#endif
300TEST_F(SavePackageTest, MAYBE_TestEnsureMimeExtension) {
301  static const struct {
302    const base::FilePath::CharType* page_title;
303    const base::FilePath::CharType* expected_name;
304    const char* contents_mime_type;
305  } kExtensionTests[] = {
306    { FPL("filename.html"), FPL("filename.html"), "text/html" },
307    { FPL("filename.htm"), FPL("filename.htm"), "text/html" },
308    { FPL("filename.xhtml"), FPL("filename.xhtml"), "text/html" },
309#if defined(OS_WIN)
310    { FPL("filename"), FPL("filename.htm"), "text/html" },
311#else  // defined(OS_WIN)
312    { FPL("filename"), FPL("filename.html"), "text/html" },
313#endif  // defined(OS_WIN)
314    { FPL("filename.html"), FPL("filename.html"), "text/xml" },
315    { FPL("filename.xml"), FPL("filename.xml"), "text/xml" },
316    { FPL("filename"), FPL("filename.xml"), "text/xml" },
317    { FPL("filename.xhtml"), FPL("filename.xhtml"),
318      "application/xhtml+xml" },
319    { FPL("filename.html"), FPL("filename.html"),
320      "application/xhtml+xml" },
321    { FPL("filename"), FPL("filename.xhtml"), "application/xhtml+xml" },
322    { FPL("filename.txt"), FPL("filename.txt"), "text/plain" },
323    { FPL("filename"), FPL("filename.txt"), "text/plain" },
324    { FPL("filename.css"), FPL("filename.css"), "text/css" },
325    { FPL("filename"), FPL("filename.css"), "text/css" },
326    { FPL("filename.abc"), FPL("filename.abc"), "unknown/unknown" },
327    { FPL("filename"), FPL("filename"), "unknown/unknown" },
328  };
329  for (uint32 i = 0; i < ARRAYSIZE_UNSAFE(kExtensionTests); ++i) {
330    base::FilePath original = base::FilePath(kExtensionTests[i].page_title);
331    base::FilePath expected = base::FilePath(kExtensionTests[i].expected_name);
332    std::string mime_type(kExtensionTests[i].contents_mime_type);
333    base::FilePath actual = EnsureMimeExtension(original, mime_type);
334    EXPECT_EQ(expected.value(), actual.value()) << "Failed for page title: " <<
335        kExtensionTests[i].page_title << " MIME:" << mime_type;
336  }
337}
338
339// Test that the suggested names generated by SavePackage are reasonable:
340// If the name is a URL, retrieve only the path component since the path name
341// generation code will turn the entire URL into the file name leading to bad
342// extension names. For example, a page with no title and a URL:
343// http://www.foo.com/a/path/name.txt will turn into file:
344// "http www.foo.com a path name.txt", when we want to save it as "name.txt".
345
346static const struct SuggestedSaveNameTestCase {
347  const char* page_url;
348  const base::string16 page_title;
349  const base::FilePath::CharType* expected_name;
350  bool ensure_html_extension;
351} kSuggestedSaveNames[] = {
352  // Title overrides the URL.
353  { "http://foo.com",
354    base::ASCIIToUTF16("A page title"),
355    FPL("A page title") FPL_HTML_EXTENSION,
356    true
357  },
358  // Extension is preserved.
359  { "http://foo.com",
360    base::ASCIIToUTF16("A page title with.ext"),
361    FPL("A page title with.ext"),
362    false
363  },
364  // If the title matches the URL, use the last component of the URL.
365  { "http://foo.com/bar",
366    base::ASCIIToUTF16("foo.com/bar"),
367    FPL("bar"),
368    false
369  },
370  // If the title matches the URL, but there is no "filename" component,
371  // use the domain.
372  { "http://foo.com",
373    base::ASCIIToUTF16("foo.com"),
374    FPL("foo.com"),
375    false
376  },
377  // Make sure fuzzy matching works.
378  { "http://foo.com/bar",
379    base::ASCIIToUTF16("foo.com/bar"),
380    FPL("bar"),
381    false
382  },
383  // A URL-like title that does not match the title is respected in full.
384  { "http://foo.com",
385    base::ASCIIToUTF16("http://www.foo.com/path/title.txt"),
386    FPL("http   www.foo.com path title.txt"),
387    false
388  },
389};
390
391// Crashing on Windows, see http://crbug.com/79365
392#if defined(OS_WIN)
393#define MAYBE_TestSuggestedSaveNames DISABLED_TestSuggestedSaveNames
394#else
395#define MAYBE_TestSuggestedSaveNames TestSuggestedSaveNames
396#endif
397TEST_F(SavePackageTest, MAYBE_TestSuggestedSaveNames) {
398  for (size_t i = 0; i < arraysize(kSuggestedSaveNames); ++i) {
399    scoped_refptr<SavePackage> save_package(
400        new SavePackage(contents(), base::FilePath(), base::FilePath()));
401    save_package->page_url_ = GURL(kSuggestedSaveNames[i].page_url);
402    save_package->title_ = kSuggestedSaveNames[i].page_title;
403
404    base::FilePath save_name = save_package->GetSuggestedNameForSaveAs(
405        kSuggestedSaveNames[i].ensure_html_extension,
406        std::string(), std::string());
407    EXPECT_EQ(kSuggestedSaveNames[i].expected_name, save_name.value()) <<
408        "Test case " << i;
409  }
410}
411
412static const base::FilePath::CharType* kTestDir =
413    FILE_PATH_LITERAL("save_page");
414
415// GetUrlToBeSaved method should return correct url to be saved.
416TEST_F(SavePackageTest, TestGetUrlToBeSaved) {
417  base::FilePath file_name(FILE_PATH_LITERAL("a.htm"));
418  GURL url = net::URLRequestMockHTTPJob::GetMockUrl(
419      base::FilePath(kTestDir).Append(file_name));
420  NavigateAndCommit(url);
421  EXPECT_EQ(url, GetUrlToBeSaved());
422}
423
424// GetUrlToBeSaved method sould return actual url to be saved,
425// instead of the displayed url used to view source of a page.
426// Ex:GetUrlToBeSaved method should return http://www.google.com
427// when user types view-source:http://www.google.com
428TEST_F(SavePackageTest, TestGetUrlToBeSavedViewSource) {
429  base::FilePath file_name(FILE_PATH_LITERAL("a.htm"));
430  GURL mock_url = net::URLRequestMockHTTPJob::GetMockUrl(
431      base::FilePath(kTestDir).Append(file_name));
432  GURL view_source_url =
433      GURL(kViewSourceScheme + std::string(":") + mock_url.spec());
434  GURL actual_url = net::URLRequestMockHTTPJob::GetMockUrl(
435      base::FilePath(kTestDir).Append(file_name));
436  NavigateAndCommit(view_source_url);
437  EXPECT_EQ(actual_url, GetUrlToBeSaved());
438  EXPECT_EQ(view_source_url, contents()->GetLastCommittedURL());
439}
440
441}  // namespace content
442