desktop_notification_service.cc revision 731df977c0511bca2206b5f333555b1205ff1f43
1// Copyright (c) 2010 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/notifications/desktop_notification_service.h"
6
7#include "app/l10n_util.h"
8#include "app/resource_bundle.h"
9#include "base/metrics/histogram.h"
10#include "base/thread.h"
11#include "base/utf_string_conversions.h"
12#include "chrome/browser/browser_child_process_host.h"
13#include "chrome/browser/browser_thread.h"
14#include "chrome/browser/extensions/extensions_service.h"
15#include "chrome/browser/notifications/notification.h"
16#include "chrome/browser/notifications/notification_object_proxy.h"
17#include "chrome/browser/notifications/notification_ui_manager.h"
18#include "chrome/browser/notifications/notifications_prefs_cache.h"
19#include "chrome/browser/prefs/pref_service.h"
20#include "chrome/browser/prefs/scoped_pref_update.h"
21#include "chrome/browser/profile.h"
22#include "chrome/browser/renderer_host/render_process_host.h"
23#include "chrome/browser/renderer_host/render_view_host.h"
24#include "chrome/browser/renderer_host/site_instance.h"
25#include "chrome/browser/tab_contents/infobar_delegate.h"
26#include "chrome/browser/tab_contents/tab_contents.h"
27#include "chrome/browser/worker_host/worker_process_host.h"
28#include "chrome/common/notification_service.h"
29#include "chrome/common/notification_type.h"
30#include "chrome/common/pref_names.h"
31#include "chrome/common/render_messages.h"
32#include "chrome/common/render_messages_params.h"
33#include "chrome/common/url_constants.h"
34#include "grit/browser_resources.h"
35#include "grit/chromium_strings.h"
36#include "grit/generated_resources.h"
37#include "grit/theme_resources.h"
38#include "net/base/escape.h"
39#include "third_party/WebKit/WebKit/chromium/public/WebNotificationPresenter.h"
40
41using WebKit::WebNotificationPresenter;
42using WebKit::WebTextDirection;
43
44const ContentSetting kDefaultSetting = CONTENT_SETTING_ASK;
45
46// static
47string16 DesktopNotificationService::CreateDataUrl(
48    const GURL& icon_url, const string16& title, const string16& body,
49    WebTextDirection dir) {
50  int resource;
51  std::vector<std::string> subst;
52  if (icon_url.is_valid()) {
53    resource = IDR_NOTIFICATION_ICON_HTML;
54    subst.push_back(icon_url.spec());
55    subst.push_back(EscapeForHTML(UTF16ToUTF8(title)));
56    subst.push_back(EscapeForHTML(UTF16ToUTF8(body)));
57    // icon float position
58    subst.push_back(dir == WebKit::WebTextDirectionRightToLeft ?
59                    "right" : "left");
60  } else if (title.empty() || body.empty()) {
61    resource = IDR_NOTIFICATION_1LINE_HTML;
62    string16 line = title.empty() ? body : title;
63    // Strings are div names in the template file.
64    string16 line_name = title.empty() ? ASCIIToUTF16("description")
65                                       : ASCIIToUTF16("title");
66    subst.push_back(EscapeForHTML(UTF16ToUTF8(line_name)));
67    subst.push_back(EscapeForHTML(UTF16ToUTF8(line)));
68  } else {
69    resource = IDR_NOTIFICATION_2LINE_HTML;
70    subst.push_back(EscapeForHTML(UTF16ToUTF8(title)));
71    subst.push_back(EscapeForHTML(UTF16ToUTF8(body)));
72  }
73  // body text direction
74  subst.push_back(dir == WebKit::WebTextDirectionRightToLeft ?
75                  "rtl" : "ltr");
76
77  return CreateDataUrl(resource, subst);
78}
79
80// static
81string16 DesktopNotificationService::CreateDataUrl(
82    int resource, const std::vector<std::string>& subst) {
83  const base::StringPiece template_html(
84      ResourceBundle::GetSharedInstance().GetRawDataResource(
85          resource));
86
87  if (template_html.empty()) {
88    NOTREACHED() << "unable to load template. ID: " << resource;
89    return string16();
90  }
91
92  std::string data = ReplaceStringPlaceholders(template_html, subst, NULL);
93  return UTF8ToUTF16("data:text/html;charset=utf-8," +
94                      EscapeQueryParamValue(data, false));
95}
96
97// A task object which calls the renderer to inform the web page that the
98// permission request has completed.
99class NotificationPermissionCallbackTask : public Task {
100 public:
101  NotificationPermissionCallbackTask(int process_id, int route_id,
102      int request_id)
103      : process_id_(process_id),
104        route_id_(route_id),
105        request_id_(request_id) {
106  }
107
108  virtual void Run() {
109    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
110    RenderViewHost* host = RenderViewHost::FromID(process_id_, route_id_);
111    if (host)
112      host->Send(new ViewMsg_PermissionRequestDone(route_id_, request_id_));
113  }
114
115 private:
116  int process_id_;
117  int route_id_;
118  int request_id_;
119};
120
121// The delegate for the infobar shown when an origin requests notification
122// permissions.
123class NotificationPermissionInfoBarDelegate : public ConfirmInfoBarDelegate {
124 public:
125  NotificationPermissionInfoBarDelegate(TabContents* contents,
126                                        const GURL& origin,
127                                        const string16& display_name,
128                                        int process_id,
129                                        int route_id,
130                                        int callback_context)
131      : ConfirmInfoBarDelegate(contents),
132        origin_(origin),
133        display_name_(display_name),
134        profile_(contents->profile()),
135        process_id_(process_id),
136        route_id_(route_id),
137        callback_context_(callback_context),
138        action_taken_(false) {
139  }
140
141  // Overridden from ConfirmInfoBarDelegate:
142  virtual void InfoBarClosed() {
143    if (!action_taken_)
144      UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Ignored", 1);
145
146    BrowserThread::PostTask(
147      BrowserThread::IO, FROM_HERE,
148      new NotificationPermissionCallbackTask(
149          process_id_, route_id_, callback_context_));
150
151    delete this;
152  }
153
154  virtual string16 GetMessageText() const {
155    return l10n_util::GetStringFUTF16(IDS_NOTIFICATION_PERMISSIONS,
156                                      display_name_);
157  }
158
159  virtual SkBitmap* GetIcon() const {
160    return ResourceBundle::GetSharedInstance().GetBitmapNamed(
161       IDR_PRODUCT_ICON_32);
162  }
163
164  virtual int GetButtons() const {
165    return BUTTON_OK | BUTTON_CANCEL | BUTTON_OK_DEFAULT;
166  }
167
168  virtual string16 GetButtonLabel(InfoBarButton button) const {
169    return button == BUTTON_OK ?
170        l10n_util::GetStringUTF16(IDS_NOTIFICATION_PERMISSION_YES) :
171        l10n_util::GetStringUTF16(IDS_NOTIFICATION_PERMISSION_NO);
172  }
173
174  virtual bool Accept() {
175    UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Allowed", 1);
176    profile_->GetDesktopNotificationService()->GrantPermission(origin_);
177    action_taken_ = true;
178    return true;
179  }
180
181  virtual bool Cancel() {
182    UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Denied", 1);
183    profile_->GetDesktopNotificationService()->DenyPermission(origin_);
184    action_taken_ = true;
185    return true;
186  }
187
188  // Overridden from InfoBarDelegate:
189  virtual Type GetInfoBarType() { return PAGE_ACTION_TYPE; }
190
191 private:
192  // The origin we are asking for permissions on.
193  GURL origin_;
194
195  // The display name for the origin to be displayed.  Will be different from
196  // origin_ for extensions.
197  string16 display_name_;
198
199  // The Profile that we restore sessions from.
200  Profile* profile_;
201
202  // The callback information that tells us how to respond to javascript via
203  // the correct RenderView.
204  int process_id_;
205  int route_id_;
206  int callback_context_;
207
208  // Whether the user clicked one of the buttons.
209  bool action_taken_;
210
211  DISALLOW_COPY_AND_ASSIGN(NotificationPermissionInfoBarDelegate);
212};
213
214DesktopNotificationService::DesktopNotificationService(Profile* profile,
215    NotificationUIManager* ui_manager)
216    : profile_(profile),
217      ui_manager_(ui_manager) {
218  registrar_.Init(profile_->GetPrefs());
219  InitPrefs();
220  StartObserving();
221}
222
223DesktopNotificationService::~DesktopNotificationService() {
224  StopObserving();
225}
226
227void DesktopNotificationService::RegisterUserPrefs(PrefService* user_prefs) {
228  if (!user_prefs->FindPreference(
229      prefs::kDesktopNotificationDefaultContentSetting)) {
230    user_prefs->RegisterIntegerPref(
231        prefs::kDesktopNotificationDefaultContentSetting, kDefaultSetting);
232  }
233  if (!user_prefs->FindPreference(prefs::kDesktopNotificationAllowedOrigins))
234    user_prefs->RegisterListPref(prefs::kDesktopNotificationAllowedOrigins);
235  if (!user_prefs->FindPreference(prefs::kDesktopNotificationDeniedOrigins))
236    user_prefs->RegisterListPref(prefs::kDesktopNotificationDeniedOrigins);
237}
238
239// Initialize the cache with the allowed and denied origins, or
240// create the preferences if they don't exist yet.
241void DesktopNotificationService::InitPrefs() {
242  PrefService* prefs = profile_->GetPrefs();
243  std::vector<GURL> allowed_origins;
244  std::vector<GURL> denied_origins;
245  ContentSetting default_content_setting = CONTENT_SETTING_DEFAULT;
246
247  if (!profile_->IsOffTheRecord()) {
248    default_content_setting = IntToContentSetting(
249        prefs->GetInteger(prefs::kDesktopNotificationDefaultContentSetting));
250    allowed_origins = GetAllowedOrigins();
251    denied_origins = GetBlockedOrigins();
252  }
253
254  prefs_cache_ = new NotificationsPrefsCache();
255  prefs_cache_->SetCacheDefaultContentSetting(default_content_setting);
256  prefs_cache_->SetCacheAllowedOrigins(allowed_origins);
257  prefs_cache_->SetCacheDeniedOrigins(denied_origins);
258  prefs_cache_->set_is_initialized(true);
259}
260
261void DesktopNotificationService::StartObserving() {
262  if (!profile_->IsOffTheRecord()) {
263    registrar_.Add(prefs::kDesktopNotificationDefaultContentSetting, this);
264    registrar_.Add(prefs::kDesktopNotificationAllowedOrigins, this);
265    registrar_.Add(prefs::kDesktopNotificationDeniedOrigins, this);
266  }
267}
268
269void DesktopNotificationService::StopObserving() {
270  if (!profile_->IsOffTheRecord()) {
271    registrar_.RemoveAll();
272  }
273}
274
275void DesktopNotificationService::GrantPermission(const GURL& origin) {
276  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
277  PersistPermissionChange(origin, true);
278
279  // Schedule a cache update on the IO thread.
280  BrowserThread::PostTask(
281      BrowserThread::IO, FROM_HERE,
282      NewRunnableMethod(
283          prefs_cache_.get(), &NotificationsPrefsCache::CacheAllowedOrigin,
284          origin));
285}
286
287void DesktopNotificationService::DenyPermission(const GURL& origin) {
288  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
289  PersistPermissionChange(origin, false);
290
291  // Schedule a cache update on the IO thread.
292  BrowserThread::PostTask(
293      BrowserThread::IO, FROM_HERE,
294      NewRunnableMethod(
295          prefs_cache_.get(), &NotificationsPrefsCache::CacheDeniedOrigin,
296          origin));
297}
298
299void DesktopNotificationService::Observe(NotificationType type,
300                                         const NotificationSource& source,
301                                         const NotificationDetails& details) {
302  DCHECK(NotificationType::PREF_CHANGED == type);
303  PrefService* prefs = profile_->GetPrefs();
304  const std::string& name = *Details<std::string>(details).ptr();
305
306  if (name == prefs::kDesktopNotificationAllowedOrigins) {
307    NotificationService::current()->Notify(
308        NotificationType::DESKTOP_NOTIFICATION_SETTINGS_CHANGED,
309        Source<DesktopNotificationService>(this),
310        NotificationService::NoDetails());
311
312    std::vector<GURL> allowed_origins(GetAllowedOrigins());
313    // Schedule a cache update on the IO thread.
314    BrowserThread::PostTask(
315        BrowserThread::IO, FROM_HERE,
316        NewRunnableMethod(
317            prefs_cache_.get(),
318            &NotificationsPrefsCache::SetCacheAllowedOrigins,
319            allowed_origins));
320  } else if (name == prefs::kDesktopNotificationDeniedOrigins) {
321    NotificationService::current()->Notify(
322        NotificationType::DESKTOP_NOTIFICATION_SETTINGS_CHANGED,
323        Source<DesktopNotificationService>(this),
324        NotificationService::NoDetails());
325
326    std::vector<GURL> denied_origins(GetBlockedOrigins());
327    // Schedule a cache update on the IO thread.
328    BrowserThread::PostTask(
329        BrowserThread::IO, FROM_HERE,
330        NewRunnableMethod(
331            prefs_cache_.get(),
332            &NotificationsPrefsCache::SetCacheDeniedOrigins,
333            denied_origins));
334  } else if (name == prefs::kDesktopNotificationDefaultContentSetting) {
335    NotificationService::current()->Notify(
336        NotificationType::DESKTOP_NOTIFICATION_DEFAULT_CHANGED,
337        Source<DesktopNotificationService>(this),
338        NotificationService::NoDetails());
339
340    const ContentSetting default_content_setting = IntToContentSetting(
341        prefs->GetInteger(prefs::kDesktopNotificationDefaultContentSetting));
342
343    // Schedule a cache update on the IO thread.
344    BrowserThread::PostTask(
345        BrowserThread::IO, FROM_HERE,
346        NewRunnableMethod(
347            prefs_cache_.get(),
348            &NotificationsPrefsCache::SetCacheDefaultContentSetting,
349            default_content_setting));
350  }
351}
352
353void DesktopNotificationService::PersistPermissionChange(
354    const GURL& origin, bool is_allowed) {
355  // Don't persist changes when off the record.
356  if (profile_->IsOffTheRecord())
357    return;
358
359  PrefService* prefs = profile_->GetPrefs();
360
361  // |Observe()| updates the whole permission set in the cache, but only a
362  // single origin has changed. Hence, callers of this method manually
363  // schedule a task to update the prefs cache, and the prefs observer is
364  // disabled while the update runs.
365  StopObserving();
366
367  bool allowed_changed = false;
368  bool denied_changed = false;
369
370  ListValue* allowed_sites =
371      prefs->GetMutableList(prefs::kDesktopNotificationAllowedOrigins);
372  ListValue* denied_sites =
373      prefs->GetMutableList(prefs::kDesktopNotificationDeniedOrigins);
374  {
375    // value is passed to the preferences list, or deleted.
376    StringValue* value = new StringValue(origin.spec());
377
378    // Remove from one list and add to the other.
379    if (is_allowed) {
380      // Remove from the denied list.
381      if (denied_sites->Remove(*value) != -1)
382        denied_changed = true;
383
384      // Add to the allowed list.
385      if (allowed_sites->AppendIfNotPresent(value))
386        allowed_changed = true;
387      else
388        delete value;
389    } else {
390      // Remove from the allowed list.
391      if (allowed_sites->Remove(*value) != -1)
392        allowed_changed = true;
393
394      // Add to the denied list.
395      if (denied_sites->AppendIfNotPresent(value))
396        denied_changed = true;
397      else
398        delete value;
399    }
400  }
401
402  // Persist the pref if anthing changed, but only send updates for the
403  // list that changed.
404  if (allowed_changed || denied_changed) {
405    if (allowed_changed) {
406      ScopedPrefUpdate update_allowed(
407          prefs, prefs::kDesktopNotificationAllowedOrigins);
408    }
409    if (denied_changed) {
410      ScopedPrefUpdate updateDenied(
411          prefs, prefs::kDesktopNotificationDeniedOrigins);
412    }
413    prefs->ScheduleSavePersistentPrefs();
414  }
415  StartObserving();
416}
417
418ContentSetting DesktopNotificationService::GetDefaultContentSetting() {
419  PrefService* prefs = profile_->GetPrefs();
420  ContentSetting setting = IntToContentSetting(
421      prefs->GetInteger(prefs::kDesktopNotificationDefaultContentSetting));
422  if (setting == CONTENT_SETTING_DEFAULT)
423    setting = kDefaultSetting;
424  return setting;
425}
426
427void DesktopNotificationService::SetDefaultContentSetting(
428    ContentSetting setting) {
429  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
430  profile_->GetPrefs()->SetInteger(
431      prefs::kDesktopNotificationDefaultContentSetting,
432      setting == CONTENT_SETTING_DEFAULT ?  kDefaultSetting : setting);
433  // The cache is updated through the notification observer.
434}
435
436void DesktopNotificationService::ResetToDefaultContentSetting() {
437  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
438
439  PrefService* prefs = profile_->GetPrefs();
440  prefs->ClearPref(prefs::kDesktopNotificationDefaultContentSetting);
441}
442
443std::vector<GURL> DesktopNotificationService::GetAllowedOrigins() {
444  std::vector<GURL> allowed_origins;
445  PrefService* prefs = profile_->GetPrefs();
446  const ListValue* allowed_sites =
447      prefs->GetList(prefs::kDesktopNotificationAllowedOrigins);
448  if (allowed_sites) {
449    NotificationsPrefsCache::ListValueToGurlVector(*allowed_sites,
450                                                   &allowed_origins);
451  }
452  return allowed_origins;
453}
454
455std::vector<GURL> DesktopNotificationService::GetBlockedOrigins() {
456  std::vector<GURL> denied_origins;
457  PrefService* prefs = profile_->GetPrefs();
458  const ListValue* denied_sites =
459      prefs->GetList(prefs::kDesktopNotificationDeniedOrigins);
460  if (denied_sites) {
461    NotificationsPrefsCache::ListValueToGurlVector(*denied_sites,
462                                                   &denied_origins);
463  }
464  return denied_origins;
465}
466
467void DesktopNotificationService::ResetAllowedOrigin(const GURL& origin) {
468  if (profile_->IsOffTheRecord())
469    return;
470
471  // Since this isn't called often, let the normal observer behavior update the
472  // cache in this case.
473  PrefService* prefs = profile_->GetPrefs();
474  ListValue* allowed_sites =
475      prefs->GetMutableList(prefs::kDesktopNotificationAllowedOrigins);
476  {
477    StringValue value(origin.spec());
478    int removed_index = allowed_sites->Remove(value);
479    DCHECK_NE(-1, removed_index) << origin << " was not allowed";
480    ScopedPrefUpdate update_allowed(
481        prefs, prefs::kDesktopNotificationAllowedOrigins);
482  }
483  prefs->ScheduleSavePersistentPrefs();
484}
485
486void DesktopNotificationService::ResetBlockedOrigin(const GURL& origin) {
487  if (profile_->IsOffTheRecord())
488    return;
489
490  // Since this isn't called often, let the normal observer behavior update the
491  // cache in this case.
492  PrefService* prefs = profile_->GetPrefs();
493  ListValue* denied_sites =
494      prefs->GetMutableList(prefs::kDesktopNotificationDeniedOrigins);
495  {
496    StringValue value(origin.spec());
497    int removed_index = denied_sites->Remove(value);
498    DCHECK_NE(-1, removed_index) << origin << " was not blocked";
499    ScopedPrefUpdate update_allowed(
500        prefs, prefs::kDesktopNotificationDeniedOrigins);
501  }
502  prefs->ScheduleSavePersistentPrefs();
503}
504
505void DesktopNotificationService::ResetAllOrigins() {
506  PrefService* prefs = profile_->GetPrefs();
507  prefs->ClearPref(prefs::kDesktopNotificationAllowedOrigins);
508  prefs->ClearPref(prefs::kDesktopNotificationDeniedOrigins);
509}
510
511ContentSetting DesktopNotificationService::GetContentSetting(
512    const GURL& origin) {
513  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
514  if (profile_->IsOffTheRecord())
515    return kDefaultSetting;
516
517  std::vector<GURL> allowed_origins(GetAllowedOrigins());
518  if (std::find(allowed_origins.begin(), allowed_origins.end(), origin) !=
519      allowed_origins.end())
520    return CONTENT_SETTING_ALLOW;
521
522  std::vector<GURL> denied_origins(GetBlockedOrigins());
523  if (std::find(denied_origins.begin(), denied_origins.end(), origin) !=
524      denied_origins.end())
525    return CONTENT_SETTING_BLOCK;
526
527  return GetDefaultContentSetting();
528}
529
530void DesktopNotificationService::RequestPermission(
531    const GURL& origin, int process_id, int route_id, int callback_context,
532    TabContents* tab) {
533  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
534  if (!tab)
535    return;
536
537  // If |origin| hasn't been seen before and the default content setting for
538  // notifications is "ask", show an infobar.
539  // The cache can only answer queries on the IO thread once it's initialized,
540  // so don't ask the cache.
541  ContentSetting setting = GetContentSetting(origin);
542  if (setting == CONTENT_SETTING_ASK) {
543    // Show an info bar requesting permission.
544    tab->AddInfoBar(new NotificationPermissionInfoBarDelegate(
545                        tab, origin, DisplayNameForOrigin(origin), process_id,
546                        route_id, callback_context));
547  } else {
548    // Notify renderer immediately.
549    BrowserThread::PostTask(
550      BrowserThread::IO, FROM_HERE,
551      new NotificationPermissionCallbackTask(
552          process_id, route_id, callback_context));
553  }
554}
555
556void DesktopNotificationService::ShowNotification(
557    const Notification& notification) {
558  ui_manager_->Add(notification, profile_);
559}
560
561bool DesktopNotificationService::CancelDesktopNotification(
562    int process_id, int route_id, int notification_id) {
563  scoped_refptr<NotificationObjectProxy> proxy(
564      new NotificationObjectProxy(process_id, route_id, notification_id,
565                                  false));
566  // TODO(johnnyg): clean up this "empty" notification.
567  Notification notif(GURL(), GURL(), string16(), string16(), proxy);
568  return ui_manager_->Cancel(notif);
569}
570
571
572bool DesktopNotificationService::ShowDesktopNotification(
573    const ViewHostMsg_ShowNotification_Params& params,
574    int process_id, int route_id, DesktopNotificationSource source) {
575  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
576  const GURL& origin = params.origin;
577  NotificationObjectProxy* proxy =
578      new NotificationObjectProxy(process_id, route_id,
579                                  params.notification_id,
580                                  source == WorkerNotification);
581  GURL contents;
582  if (params.is_html) {
583    contents = params.contents_url;
584  } else {
585    // "upconvert" the string parameters to a data: URL.
586    contents = GURL(
587        CreateDataUrl(params.icon_url, params.title, params.body,
588                      params.direction));
589  }
590  Notification notification(
591      origin, contents, DisplayNameForOrigin(origin),
592      params.replace_id, proxy);
593  ShowNotification(notification);
594  return true;
595}
596
597string16 DesktopNotificationService::DisplayNameForOrigin(
598    const GURL& origin) {
599  // If the source is an extension, lookup the display name.
600  if (origin.SchemeIs(chrome::kExtensionScheme)) {
601    ExtensionsService* ext_service = profile_->GetExtensionsService();
602    if (ext_service) {
603      Extension* extension = ext_service->GetExtensionByURL(origin);
604      if (extension)
605        return UTF8ToUTF16(extension->name());
606    }
607  }
608  return UTF8ToUTF16(origin.host());
609}
610