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 "chrome/browser/external_protocol/external_protocol_handler.h"
6
7#include <set>
8
9#include "base/bind.h"
10#include "base/logging.h"
11#include "base/message_loop/message_loop.h"
12#include "base/prefs/pref_registry_simple.h"
13#include "base/prefs/pref_service.h"
14#include "base/prefs/scoped_user_pref_update.h"
15#include "base/strings/string_util.h"
16#include "base/threading/thread.h"
17#include "build/build_config.h"
18#include "chrome/browser/browser_process.h"
19#include "chrome/browser/platform_util.h"
20#include "chrome/browser/profiles/profile.h"
21#include "chrome/browser/tab_contents/tab_util.h"
22#include "chrome/common/pref_names.h"
23#include "content/public/browser/browser_thread.h"
24#include "content/public/browser/web_contents.h"
25#include "net/base/escape.h"
26#include "url/gurl.h"
27
28using content::BrowserThread;
29
30// Whether we accept requests for launching external protocols. This is set to
31// false every time an external protocol is requested, and set back to true on
32// each user gesture. This variable should only be accessed from the UI thread.
33static bool g_accept_requests = true;
34
35namespace {
36
37// Functions enabling unit testing. Using a NULL delegate will use the default
38// behavior; if a delegate is provided it will be used instead.
39ShellIntegration::DefaultProtocolClientWorker* CreateShellWorker(
40    ShellIntegration::DefaultWebClientObserver* observer,
41    const std::string& protocol,
42    ExternalProtocolHandler::Delegate* delegate) {
43  if (!delegate)
44    return new ShellIntegration::DefaultProtocolClientWorker(observer,
45                                                             protocol);
46
47  return delegate->CreateShellWorker(observer, protocol);
48}
49
50ExternalProtocolHandler::BlockState GetBlockStateWithDelegate(
51    const std::string& scheme,
52    ExternalProtocolHandler::Delegate* delegate) {
53  if (!delegate)
54    return ExternalProtocolHandler::GetBlockState(scheme);
55
56  return delegate->GetBlockState(scheme);
57}
58
59void RunExternalProtocolDialogWithDelegate(
60    const GURL& url,
61    int render_process_host_id,
62    int routing_id,
63    ExternalProtocolHandler::Delegate* delegate) {
64  if (!delegate) {
65    ExternalProtocolHandler::RunExternalProtocolDialog(url,
66                                                       render_process_host_id,
67                                                       routing_id);
68  } else {
69    delegate->RunExternalProtocolDialog(url, render_process_host_id,
70                                        routing_id);
71  }
72}
73
74void LaunchUrlWithoutSecurityCheckWithDelegate(
75    const GURL& url,
76    int render_process_host_id,
77    int tab_contents_id,
78    ExternalProtocolHandler::Delegate* delegate) {
79  if (!delegate) {
80    ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(
81        url, render_process_host_id, tab_contents_id);
82  } else {
83    delegate->LaunchUrlWithoutSecurityCheck(url);
84  }
85}
86
87// When we are about to launch a URL with the default OS level application,
88// we check if that external application will be us. If it is we just ignore
89// the request.
90class ExternalDefaultProtocolObserver
91    : public ShellIntegration::DefaultWebClientObserver {
92 public:
93  ExternalDefaultProtocolObserver(const GURL& escaped_url,
94                                  int render_process_host_id,
95                                  int tab_contents_id,
96                                  bool prompt_user,
97                                  ExternalProtocolHandler::Delegate* delegate)
98      : delegate_(delegate),
99        escaped_url_(escaped_url),
100        render_process_host_id_(render_process_host_id),
101        tab_contents_id_(tab_contents_id),
102        prompt_user_(prompt_user) {}
103
104  virtual void SetDefaultWebClientUIState(
105      ShellIntegration::DefaultWebClientUIState state) OVERRIDE {
106    DCHECK(base::MessageLoopForUI::IsCurrent());
107
108    // If we are still working out if we're the default, or we've found
109    // out we definately are the default, we end here.
110    if (state == ShellIntegration::STATE_PROCESSING) {
111      return;
112    }
113
114    if (delegate_)
115      delegate_->FinishedProcessingCheck();
116
117    if (state == ShellIntegration::STATE_IS_DEFAULT) {
118      if (delegate_)
119        delegate_->BlockRequest();
120      return;
121    }
122
123    // If we get here, either we are not the default or we cannot work out
124    // what the default is, so we proceed.
125    if (prompt_user_) {
126      // Ask the user if they want to allow the protocol. This will call
127      // LaunchUrlWithoutSecurityCheck if the user decides to accept the
128      // protocol.
129      RunExternalProtocolDialogWithDelegate(escaped_url_,
130          render_process_host_id_, tab_contents_id_, delegate_);
131      return;
132    }
133
134    LaunchUrlWithoutSecurityCheckWithDelegate(
135        escaped_url_, render_process_host_id_, tab_contents_id_, delegate_);
136  }
137
138  virtual bool IsOwnedByWorker() OVERRIDE { return true; }
139
140 private:
141  ExternalProtocolHandler::Delegate* delegate_;
142  GURL escaped_url_;
143  int render_process_host_id_;
144  int tab_contents_id_;
145  bool prompt_user_;
146};
147
148}  // namespace
149
150// static
151void ExternalProtocolHandler::PrepopulateDictionary(
152    base::DictionaryValue* win_pref) {
153  static bool is_warm = false;
154  if (is_warm)
155    return;
156  is_warm = true;
157
158  static const char* const denied_schemes[] = {
159    "afp",
160    "data",
161    "disk",
162    "disks",
163    // ShellExecuting file:///C:/WINDOWS/system32/notepad.exe will simply
164    // execute the file specified!  Hopefully we won't see any "file" schemes
165    // because we think of file:// URLs as handled URLs, but better to be safe
166    // than to let an attacker format the user's hard drive.
167    "file",
168    "hcp",
169    "javascript",
170    "ms-help",
171    "nntp",
172    "shell",
173    "vbscript",
174    // view-source is a special case in chrome. When it comes through an
175    // iframe or a redirect, it looks like an external protocol, but we don't
176    // want to shellexecute it.
177    "view-source",
178    "vnd.ms.radio",
179  };
180
181  static const char* const allowed_schemes[] = {
182    "mailto",
183    "news",
184    "snews",
185#if defined(OS_WIN)
186    "ms-windows-store",
187#endif
188  };
189
190  bool should_block;
191  for (size_t i = 0; i < arraysize(denied_schemes); ++i) {
192    if (!win_pref->GetBoolean(denied_schemes[i], &should_block)) {
193      win_pref->SetBoolean(denied_schemes[i], true);
194    }
195  }
196
197  for (size_t i = 0; i < arraysize(allowed_schemes); ++i) {
198    if (!win_pref->GetBoolean(allowed_schemes[i], &should_block)) {
199      win_pref->SetBoolean(allowed_schemes[i], false);
200    }
201  }
202}
203
204// static
205ExternalProtocolHandler::BlockState ExternalProtocolHandler::GetBlockState(
206    const std::string& scheme) {
207  // If we are being carpet bombed, block the request.
208  if (!g_accept_requests)
209    return BLOCK;
210
211  if (scheme.length() == 1) {
212    // We have a URL that looks something like:
213    //   C:/WINDOWS/system32/notepad.exe
214    // ShellExecuting this URL will cause the specified program to be executed.
215    return BLOCK;
216  }
217
218  // Check the stored prefs.
219  // TODO(pkasting): http://b/1119651 This kind of thing should go in the
220  // preferences on the profile, not in the local state.
221  PrefService* pref = g_browser_process->local_state();
222  if (pref) {  // May be NULL during testing.
223    DictionaryPrefUpdate update_excluded_schemas(pref, prefs::kExcludedSchemes);
224
225    // Warm up the dictionary if needed.
226    PrepopulateDictionary(update_excluded_schemas.Get());
227
228    bool should_block;
229    if (update_excluded_schemas->GetBoolean(scheme, &should_block))
230      return should_block ? BLOCK : DONT_BLOCK;
231  }
232
233  return UNKNOWN;
234}
235
236// static
237void ExternalProtocolHandler::SetBlockState(const std::string& scheme,
238                                            BlockState state) {
239  // Set in the stored prefs.
240  // TODO(pkasting): http://b/1119651 This kind of thing should go in the
241  // preferences on the profile, not in the local state.
242  PrefService* pref = g_browser_process->local_state();
243  if (pref) {  // May be NULL during testing.
244    DictionaryPrefUpdate update_excluded_schemas(pref, prefs::kExcludedSchemes);
245
246    if (state == UNKNOWN) {
247      update_excluded_schemas->Remove(scheme, NULL);
248    } else {
249      update_excluded_schemas->SetBoolean(scheme, (state == BLOCK));
250    }
251  }
252}
253
254// static
255void ExternalProtocolHandler::LaunchUrlWithDelegate(const GURL& url,
256                                                    int render_process_host_id,
257                                                    int tab_contents_id,
258                                                    Delegate* delegate) {
259  DCHECK(base::MessageLoopForUI::IsCurrent());
260
261  // Escape the input scheme to be sure that the command does not
262  // have parameters unexpected by the external program.
263  std::string escaped_url_string = net::EscapeExternalHandlerValue(url.spec());
264  GURL escaped_url(escaped_url_string);
265  BlockState block_state =
266      GetBlockStateWithDelegate(escaped_url.scheme(), delegate);
267  if (block_state == BLOCK) {
268    if (delegate)
269      delegate->BlockRequest();
270    return;
271  }
272
273  g_accept_requests = false;
274
275  // The worker creates tasks with references to itself and puts them into
276  // message loops. When no tasks are left it will delete the observer and
277  // eventually be deleted itself.
278  ShellIntegration::DefaultWebClientObserver* observer =
279      new ExternalDefaultProtocolObserver(url,
280                                          render_process_host_id,
281                                          tab_contents_id,
282                                          block_state == UNKNOWN,
283                                          delegate);
284  scoped_refptr<ShellIntegration::DefaultProtocolClientWorker> worker =
285      CreateShellWorker(observer, escaped_url.scheme(), delegate);
286
287  // Start the check process running. This will send tasks to the FILE thread
288  // and when the answer is known will send the result back to the observer on
289  // the UI thread.
290  worker->StartCheckIsDefault();
291}
292
293// static
294void ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(
295    const GURL& url,
296    int render_process_host_id,
297    int tab_contents_id) {
298  content::WebContents* web_contents = tab_util::GetWebContentsByID(
299      render_process_host_id, tab_contents_id);
300  if (!web_contents)
301    return;
302
303  platform_util::OpenExternal(
304      Profile::FromBrowserContext(web_contents->GetBrowserContext()), url);
305}
306
307// static
308void ExternalProtocolHandler::RegisterPrefs(PrefRegistrySimple* registry) {
309  registry->RegisterDictionaryPref(prefs::kExcludedSchemes);
310}
311
312// static
313void ExternalProtocolHandler::PermitLaunchUrl() {
314  DCHECK(base::MessageLoopForUI::IsCurrent());
315  g_accept_requests = true;
316}
317