diff options
author | phajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-08-12 21:34:49 +0000 |
---|---|---|
committer | phajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-08-12 21:34:49 +0000 |
commit | b96aa938b7f86b67f7d4c167e713f5c23fae01d6 (patch) | |
tree | 92c58342b82e416a5ffd29a7c283fdcfe9c0c82b | |
parent | 4778bf3be094a4faac5e622a5f18be308908e53c (diff) | |
download | chromium_src-b96aa938b7f86b67f7d4c167e713f5c23fae01d6.zip chromium_src-b96aa938b7f86b67f7d4c167e713f5c23fae01d6.tar.gz chromium_src-b96aa938b7f86b67f7d4c167e713f5c23fae01d6.tar.bz2 |
First step to create application shortcuts on Linux.
Create a working desktop shortcut. For now it displays no UI, but the backend works.
TEST=none
http://crbug.com/17251
Review URL: http://codereview.chromium.org/164280
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@23226 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/browser.cc | 2 | ||||
-rw-r--r-- | chrome/browser/gtk/standard_menus.cc | 8 | ||||
-rw-r--r-- | chrome/browser/shell_integration.cc | 2 | ||||
-rw-r--r-- | chrome/browser/shell_integration.h | 20 | ||||
-rw-r--r-- | chrome/browser/shell_integration_linux.cc | 136 | ||||
-rw-r--r-- | chrome/browser/shell_integration_unittest.cc | 94 | ||||
-rw-r--r-- | chrome/browser/tab_contents/tab_contents.cc | 6 | ||||
-rw-r--r-- | chrome/chrome.gyp | 1 |
8 files changed, 261 insertions, 8 deletions
diff --git a/chrome/browser/browser.cc b/chrome/browser/browser.cc index 562f3b0..160be20 100644 --- a/chrome/browser/browser.cc +++ b/chrome/browser/browser.cc @@ -1114,7 +1114,7 @@ void Browser::OpenFile() { void Browser::OpenCreateShortcutsDialog() { UserMetrics::RecordAction(L"CreateShortcut", profile_); -#if defined(OS_WIN) +#if defined(OS_WIN) || defined(OS_LINUX) GetSelectedTabContents()->CreateShortcut(); #else NOTIMPLEMENTED(); diff --git a/chrome/browser/gtk/standard_menus.cc b/chrome/browser/gtk/standard_menus.cc index 72a66ac..bf1ef7db 100644 --- a/chrome/browser/gtk/standard_menus.cc +++ b/chrome/browser/gtk/standard_menus.cc @@ -37,12 +37,8 @@ struct MenuCreateMaterial developer_menu_materials[] = { }; struct MenuCreateMaterial standard_page_menu_materials[] = { - // The Create App Shortcuts menu item hasn't been implemented yet. - // Remove it until it is. - // http://code.google.com/p/chromium/issues/detail?id=17251 - // { MENU_NORMAL, IDC_CREATE_SHORTCUTS, IDS_CREATE_SHORTCUTS }, - // { MENU_SEPARATOR }, - + { MENU_NORMAL, IDC_CREATE_SHORTCUTS, IDS_CREATE_SHORTCUTS }, + { MENU_SEPARATOR }, { MENU_NORMAL, IDC_CUT, IDS_CUT, 0, NULL, GDK_x, GDK_CONTROL_MASK, true }, { MENU_NORMAL, IDC_COPY, IDS_COPY, 0, NULL, GDK_c, GDK_CONTROL_MASK, true }, { MENU_NORMAL, IDC_PASTE, IDS_PASTE, 0, NULL, GDK_v, GDK_CONTROL_MASK, true }, diff --git a/chrome/browser/shell_integration.cc b/chrome/browser/shell_integration.cc index e982ab8..219ce77 100644 --- a/chrome/browser/shell_integration.cc +++ b/chrome/browser/shell_integration.cc @@ -5,8 +5,10 @@ #include "chrome/browser/shell_integration.h" #include "base/message_loop.h" +#include "base/path_service.h" #include "base/thread.h" #include "chrome/browser/browser_process.h" +#include "chrome/common/chrome_paths.h" /////////////////////////////////////////////////////////////////////////////// // ShellIntegration::DefaultBrowserWorker diff --git a/chrome/browser/shell_integration.h b/chrome/browser/shell_integration.h index 4059266..9abfadd 100644 --- a/chrome/browser/shell_integration.h +++ b/chrome/browser/shell_integration.h @@ -7,8 +7,12 @@ #include <string> +#include "base/basictypes.h" #include "base/ref_counted.h" +#include "base/string16.h" +class FilePath; +class GURL; class MessageLoop; class ShellIntegration { @@ -27,6 +31,22 @@ class ShellIntegration { // user. This method is very fast so it can be invoked in the UI thread. static bool IsFirefoxDefaultBrowser(); +#if defined(OS_LINUX) + // Returns filename for .desktop file based on |url|, sanitized for security. + static FilePath GetDesktopShortcutFilename(const GURL& url); + + // Returns contents for .desktop file based on |template_contents|, |url| + // and |title|. The |template_contents| should be contents of .desktop file + // used to launch Chrome. + static std::string GetDesktopFileContents( + const std::string& template_contents, const GURL& url, + const string16& title); + + // Creates a desktop shortcut for |url| with |title|. It is not guaranteed + // to exist immediately after returning from this function, because actual + // file operation is done on the file thread. + static void CreateDesktopShortcut(const GURL& url, const string16& title); +#endif // defined(OS_LINUX) // The current default browser UI state enum DefaultBrowserUIState { diff --git a/chrome/browser/shell_integration_linux.cc b/chrome/browser/shell_integration_linux.cc index fc5e053..48abd49 100644 --- a/chrome/browser/shell_integration_linux.cc +++ b/chrome/browser/shell_integration_linux.cc @@ -12,9 +12,22 @@ #include <vector> +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/message_loop.h" +#include "base/path_service.h" #include "base/process_util.h" +#include "base/string_tokenizer.h" +#include "base/string_util.h" +#include "base/task.h" +#include "base/thread.h" +#include "chrome/browser/browser_process.h" +#include "chrome/common/chrome_paths.h" +#include "googleurl/src/gurl.h" -static const char* GetDesktopName() { +namespace { + +const char* GetDesktopName() { #if defined(GOOGLE_CHROME_BUILD) return "google-chrome.desktop"; #else // CHROMIUM_BUILD @@ -31,6 +44,72 @@ static const char* GetDesktopName() { #endif } +bool GetDesktopShortcutTemplate(std::string* output) { + std::vector<std::string> search_paths; + + const char* xdg_data_home = getenv("XDG_DATA_HOME"); + if (xdg_data_home) + search_paths.push_back(xdg_data_home); + + const char* xdg_data_dirs = getenv("XDG_DATA_DIRS"); + if (xdg_data_dirs) { + StringTokenizer tokenizer(xdg_data_dirs, ":"); + while (tokenizer.GetNext()) { + search_paths.push_back(tokenizer.token()); + } + } + + std::string template_filename(GetDesktopName()); + for (std::vector<std::string>::const_iterator i = search_paths.begin(); + i != search_paths.end(); ++i) { + FilePath path = FilePath(*i).Append(template_filename); + if (file_util::PathExists(path)) + return file_util::ReadFileToString(path, output); + } + + return false; +} + +class CreateDesktopShortcutTask : public Task { + public: + CreateDesktopShortcutTask(const GURL& url, const string16& title) + : url_(url), + title_(title) { + } + + virtual void Run() { + // TODO(phajdan.jr): Report errors from this function, possibly as infobars. + FilePath desktop_path; + if (!PathService::Get(chrome::DIR_USER_DESKTOP, &desktop_path)) + return; + desktop_path = + desktop_path.Append(ShellIntegration::GetDesktopShortcutFilename(url_)); + + if (file_util::PathExists(desktop_path)) + return; + + std::string template_contents; + if (!GetDesktopShortcutTemplate(&template_contents)) + return; + + std::string contents = ShellIntegration::GetDesktopFileContents( + template_contents, url_, title_); + int bytes_written = file_util::WriteFile(desktop_path, contents.data(), + contents.length()); + if (bytes_written != static_cast<int>(contents.length())) { + file_util::Delete(desktop_path, false); + } + } + + private: + const GURL url_; // URL of the web application. + const string16 title_; // Title displayed to the user. + + DISALLOW_COPY_AND_ASSIGN(CreateDesktopShortcutTask); +}; + +} // namespace + // We delegate the difficult of setting the default browser in Linux desktop // environments to a new xdg utility, xdg-settings. We'll have to include a copy // of it for this to work, obviously, but that's actually the suggested approach @@ -98,3 +177,58 @@ bool ShellIntegration::IsFirefoxDefaultBrowser() { base::GetAppOutput(CommandLine(argv), &browser); return browser.find("irefox") != std::string::npos; } + +FilePath ShellIntegration::GetDesktopShortcutFilename(const GURL& url) { + std::wstring filename = UTF8ToWide(url.spec()) + L".desktop"; + file_util::ReplaceIllegalCharacters(&filename, '_'); + + // Return BaseName to be absolutely sure we're not vulnerable to a directory + // traversal attack. + return FilePath::FromWStringHack(filename).BaseName(); +} + +std::string ShellIntegration::GetDesktopFileContents( + const std::string& template_contents, const GURL& url, + const string16& title) { + // See http://standards.freedesktop.org/desktop-entry-spec/latest/ + std::string output_buffer; + StringTokenizer tokenizer(template_contents, "\n"); + while (tokenizer.GetNext()) { + // TODO(phajdan.jr): Add the icon. + + if (tokenizer.token().substr(0, 5) == "Exec=") { + std::string exec_path = tokenizer.token().substr(5); + StringTokenizer exec_tokenizer(exec_path, " "); + std::string final_path; + while (exec_tokenizer.GetNext()) { + if (exec_tokenizer.token() != "%U") + final_path += exec_tokenizer.token() + " "; + } + std::string app_switch(StringPrintf("\"--app=%s\"", + url.spec().c_str())); + ReplaceSubstringsAfterOffset(&app_switch, 0, "%", "%%"); + output_buffer += std::string("Exec=") + final_path + app_switch + "\n"; + } else if (tokenizer.token().substr(0, 5) == "Name=") { + std::string final_title = UTF16ToUTF8(title); + // Make sure no endline characters can slip in and possibly introduce + // additional lines (like Exec, which makes it a security risk). Also + // use the URL as a default when the title is empty. + if (final_title.empty() || + final_title.find("\n") != std::string::npos || + final_title.find("\r") != std::string::npos) + final_title = url.spec(); + output_buffer += StringPrintf("Name=%s\n", final_title.c_str()); + } else if (tokenizer.token().substr(0, 8) == "Comment=") { + // Skip the line. + } else { + output_buffer += tokenizer.token() + "\n"; + } + } + return output_buffer; +} + +void ShellIntegration::CreateDesktopShortcut(const GURL& url, + const string16& title) { + g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE, + new CreateDesktopShortcutTask(url, title)); +} diff --git a/chrome/browser/shell_integration_unittest.cc b/chrome/browser/shell_integration_unittest.cc new file mode 100644 index 0000000..fe01fc1 --- /dev/null +++ b/chrome/browser/shell_integration_unittest.cc @@ -0,0 +1,94 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/shell_integration.h" + +#include "base/file_path.h" +#include "base/string_util.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" + +#define FPL FILE_PATH_LITERAL + +#if defined(OS_LINUX) +TEST(ShellIntegrationTest, GetDesktopShortcutFilename) { + const struct { + const FilePath::CharType* path; + const char* url; + } test_cases[] = { + { FPL("http___foo_.desktop"), "http://foo" }, + { FPL("http___foo_bar_.desktop"), "http://foo/bar/" }, + { FPL("http___foo_bar_a=b&c=d.desktop"), "http://foo/bar?a=b&c=d" }, + + // Now we're starting to be more evil... + { FPL("http___foo_.desktop"), "http://foo/bar/baz/../../../../../" }, + { FPL("http___foo_.desktop"), "http://foo/bar/././../baz/././../" }, + { FPL("http___.._.desktop"), "http://../../../../" }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); i++) { + EXPECT_EQ(test_cases[i].path, ShellIntegration::GetDesktopShortcutFilename( + GURL(test_cases[i].url)).value()) << " while testing " << + test_cases[i].url; + } +} + +TEST(ShellIntegrationTest, GetDesktopFileContents) { + const struct { + const char* url; + const char* title; + const char* template_contents; + const char* expected_output; + } test_cases[] = { + // Dumb case. + { "ignored", "ignored", "", "" }, + + // Real-world case. + { "http://gmail.com", + "GMail", + + "[Desktop Entry]\n" + "Version=1.0\n" + "Encoding=UTF-8\n" + "Name=Google Chrome\n" + "Comment=The web browser from Google\n" + "Exec=/opt/google/chrome/google-chrome %U\n" + "Terminal=false\n" + "Icon=/opt/google/chrome/product_logo_48.png\n" + "Type=Application\n" + "Categories=Application;Network;WebBrowser;\n" + "MimeType=text/html;text/xml;application/xhtml_xml;\n", + + "[Desktop Entry]\n" + "Version=1.0\n" + "Encoding=UTF-8\n" + "Name=GMail\n" + "Exec=/opt/google/chrome/google-chrome \"--app=http://gmail.com/\"\n" + "Terminal=false\n" + "Icon=/opt/google/chrome/product_logo_48.png\n" + "Type=Application\n" + "Categories=Application;Network;WebBrowser;\n" + "MimeType=text/html;text/xml;application/xhtml_xml;\n" + }, + + // Now we're starting to be more evil... + { "http://evil.com/evil --join-the-b0tnet", + "Ownz0red\nExec=rm -rf /", + + "Name=Google Chrome\n" + "Exec=/opt/google/chrome/google-chrome %U\n", + + "Name=http://evil.com/evil%20--join-the-b0tnet\n" + "Exec=/opt/google/chrome/google-chrome " + "\"--app=http://evil.com/evil%%20--join-the-b0tnet\"\n" + }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); i++) { + EXPECT_EQ(test_cases[i].expected_output, + ShellIntegration::GetDesktopFileContents( + test_cases[i].template_contents, + GURL(test_cases[i].url), + ASCIIToUTF16(test_cases[i].title))); + } +} +#endif // defined(OS_LINUX) diff --git a/chrome/browser/tab_contents/tab_contents.cc b/chrome/browser/tab_contents/tab_contents.cc index f6e6687..36a5d7e 100644 --- a/chrome/browser/tab_contents/tab_contents.cc +++ b/chrome/browser/tab_contents/tab_contents.cc @@ -45,6 +45,7 @@ #include "chrome/browser/thumbnail_store.h" #include "chrome/browser/search_engines/template_url_fetcher.h" #include "chrome/browser/search_engines/template_url_model.h" +#include "chrome/browser/shell_integration.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/notification_service.h" #include "chrome/common/page_action.h" @@ -743,6 +744,10 @@ void TabContents::CreateShortcut() { if (!entry) return; +#if defined(OS_LINUX) + // TODO(phajdan.jr): Finish creating shortcuts (UI etc). + ShellIntegration::CreateDesktopShortcut(GetURL(), GetTitle()); +#else // We only allow one pending install request. By resetting the page id we // effectively cancel the pending install request. pending_install_.page_id = entry->page_id(); @@ -760,6 +765,7 @@ void TabContents::CreateShortcut() { // Request the application info. When done OnDidGetApplicationInfo is invoked // and we'll create the shortcut. render_view_host()->GetApplicationInfo(pending_install_.page_id); +#endif } void TabContents::ShowPageInfo(const GURL& url, diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 4411daef..053792a 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -3916,6 +3916,7 @@ 'browser/sessions/session_service_test_helper.h', 'browser/sessions/session_service_unittest.cc', 'browser/sessions/tab_restore_service_unittest.cc', + 'browser/shell_integration_unittest.cc', 'browser/spellcheck_unittest.cc', 'browser/ssl/ssl_host_state_unittest.cc', 'browser/sync/glue/bookmark_model_worker_unittest.cc', |