1// Copyright (c) 2011 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 "base/metrics/histogram.h"
8#include "base/threading/thread.h"
9#include "base/utf_string_conversions.h"
10#include "chrome/browser/content_settings/content_settings_provider.h"
11#include "chrome/browser/extensions/extension_service.h"
12#include "chrome/browser/notifications/desktop_notification_service_factory.h"
13#include "chrome/browser/notifications/notification.h"
14#include "chrome/browser/notifications/notification_object_proxy.h"
15#include "chrome/browser/notifications/notification_ui_manager.h"
16#include "chrome/browser/notifications/notifications_prefs_cache.h"
17#include "chrome/browser/prefs/pref_service.h"
18#include "chrome/browser/prefs/scoped_user_pref_update.h"
19#include "chrome/browser/profiles/profile.h"
20#include "chrome/browser/tab_contents/confirm_infobar_delegate.h"
21#include "chrome/browser/ui/browser_list.h"
22#include "chrome/common/pref_names.h"
23#include "chrome/common/url_constants.h"
24#include "content/browser/browser_child_process_host.h"
25#include "content/browser/browser_thread.h"
26#include "content/browser/renderer_host/render_process_host.h"
27#include "content/browser/renderer_host/render_view_host.h"
28#include "content/browser/site_instance.h"
29#include "content/browser/tab_contents/tab_contents.h"
30#include "content/browser/worker_host/worker_process_host.h"
31#include "content/common/desktop_notification_messages.h"
32#include "content/common/notification_service.h"
33#include "content/common/notification_type.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/Source/WebKit/chromium/public/WebNotificationPresenter.h"
40#include "ui/base/l10n/l10n_util.h"
41#include "ui/base/resource/resource_bundle.h"
42
43using WebKit::WebNotificationPresenter;
44using WebKit::WebTextDirection;
45
46const ContentSetting kDefaultSetting = CONTENT_SETTING_ASK;
47
48namespace {
49
50typedef content_settings::ProviderInterface::Rules Rules;
51
52void GetOriginsWithSettingFromContentSettingsRules(
53    const Rules& content_setting_rules,
54    ContentSetting setting,
55    std::vector<GURL>* origins) {
56  origins->clear();
57
58  for (Rules::const_iterator rule = content_setting_rules.begin();
59       rule != content_setting_rules.end();
60       ++rule) {
61    if (setting == rule->content_setting) {
62      std::string url_str = rule->requesting_url_pattern.AsString();
63      if (!rule->requesting_url_pattern.IsValid()) {
64        // TODO(markusheintz): This will be removed in one of the next
65        // refactoring steps as this entire function will disapear.
66        LOG(DFATAL) << "Ignoring invalid content settings pattern: "
67                    << url_str;
68      } else if (url_str.find(ContentSettingsPattern::kDomainWildcard) == 0) {
69        // TODO(markusheintz): This must be changed once the UI code is
70        // refactored and content_settings patterns are fully supported for
71        // notifications settings.
72        LOG(DFATAL) << "Ignoring unsupported content settings pattern: "
73                    << url_str << ". Content settings patterns other than "
74                    << "hostnames (e.g. wildcard patterns) are not supported "
75                    << "for notification content settings yet.";
76      } else {
77        origins->push_back(
78            content_settings::NotificationProvider::ToGURL(
79                rule->requesting_url_pattern));
80      }
81    }
82  }
83}
84
85}  // namespace
86
87// NotificationPermissionInfoBarDelegate --------------------------------------
88
89// The delegate for the infobar shown when an origin requests notification
90// permissions.
91class NotificationPermissionInfoBarDelegate : public ConfirmInfoBarDelegate {
92 public:
93  NotificationPermissionInfoBarDelegate(TabContents* contents,
94                                        const GURL& origin,
95                                        const string16& display_name,
96                                        int process_id,
97                                        int route_id,
98                                        int callback_context);
99
100 private:
101  virtual ~NotificationPermissionInfoBarDelegate();
102
103  // ConfirmInfoBarDelegate:
104  virtual void InfoBarClosed();
105  virtual SkBitmap* GetIcon() const;
106  virtual Type GetInfoBarType() const;
107  virtual string16 GetMessageText() const;
108  virtual string16 GetButtonLabel(InfoBarButton button) const;
109  virtual bool Accept();
110  virtual bool Cancel();
111
112  // The origin we are asking for permissions on.
113  GURL origin_;
114
115  // The display name for the origin to be displayed.  Will be different from
116  // origin_ for extensions.
117  string16 display_name_;
118
119  // The Profile that we restore sessions from.
120  Profile* profile_;
121
122  // The callback information that tells us how to respond to javascript via
123  // the correct RenderView.
124  int process_id_;
125  int route_id_;
126  int callback_context_;
127
128  // Whether the user clicked one of the buttons.
129  bool action_taken_;
130
131  DISALLOW_COPY_AND_ASSIGN(NotificationPermissionInfoBarDelegate);
132};
133
134NotificationPermissionInfoBarDelegate::NotificationPermissionInfoBarDelegate(
135    TabContents* contents,
136    const GURL& origin,
137    const string16& display_name,
138    int process_id,
139    int route_id,
140    int callback_context)
141    : ConfirmInfoBarDelegate(contents),
142      origin_(origin),
143      display_name_(display_name),
144      profile_(contents->profile()),
145      process_id_(process_id),
146      route_id_(route_id),
147      callback_context_(callback_context),
148      action_taken_(false) {
149}
150
151NotificationPermissionInfoBarDelegate::
152    ~NotificationPermissionInfoBarDelegate() {
153}
154
155void NotificationPermissionInfoBarDelegate::InfoBarClosed() {
156  if (!action_taken_)
157    UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Ignored", 1);
158
159  RenderViewHost* host = RenderViewHost::FromID(process_id_, route_id_);
160  if (host) {
161    host->Send(new DesktopNotificationMsg_PermissionRequestDone(
162        route_id_, callback_context_));
163  }
164
165  delete this;
166}
167
168SkBitmap* NotificationPermissionInfoBarDelegate::GetIcon() const {
169  return ResourceBundle::GetSharedInstance().GetBitmapNamed(
170     IDR_PRODUCT_ICON_32);
171}
172
173InfoBarDelegate::Type
174    NotificationPermissionInfoBarDelegate::GetInfoBarType() const {
175  return PAGE_ACTION_TYPE;
176}
177
178string16 NotificationPermissionInfoBarDelegate::GetMessageText() const {
179  return l10n_util::GetStringFUTF16(IDS_NOTIFICATION_PERMISSIONS,
180                                    display_name_);
181}
182
183string16 NotificationPermissionInfoBarDelegate::GetButtonLabel(
184    InfoBarButton button) const {
185  return l10n_util::GetStringUTF16((button == BUTTON_OK) ?
186      IDS_NOTIFICATION_PERMISSION_YES : IDS_NOTIFICATION_PERMISSION_NO);
187}
188
189bool NotificationPermissionInfoBarDelegate::Accept() {
190  UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Allowed", 1);
191  DesktopNotificationServiceFactory::GetForProfile(profile_)->
192      GrantPermission(origin_);
193  action_taken_ = true;
194  return true;
195}
196
197bool NotificationPermissionInfoBarDelegate::Cancel() {
198  UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Denied", 1);
199  DesktopNotificationServiceFactory::GetForProfile(profile_)->
200      DenyPermission(origin_);
201  action_taken_ = true;
202  return true;
203}
204
205
206// DesktopNotificationService -------------------------------------------------
207
208// static
209string16 DesktopNotificationService::CreateDataUrl(
210    const GURL& icon_url, const string16& title, const string16& body,
211    WebTextDirection dir) {
212  int resource;
213  std::vector<std::string> subst;
214  if (icon_url.is_valid()) {
215    resource = IDR_NOTIFICATION_ICON_HTML;
216    subst.push_back(icon_url.spec());
217    subst.push_back(EscapeForHTML(UTF16ToUTF8(title)));
218    subst.push_back(EscapeForHTML(UTF16ToUTF8(body)));
219    // icon float position
220    subst.push_back(dir == WebKit::WebTextDirectionRightToLeft ?
221                    "right" : "left");
222  } else if (title.empty() || body.empty()) {
223    resource = IDR_NOTIFICATION_1LINE_HTML;
224    string16 line = title.empty() ? body : title;
225    // Strings are div names in the template file.
226    string16 line_name = title.empty() ? ASCIIToUTF16("description")
227                                       : ASCIIToUTF16("title");
228    subst.push_back(EscapeForHTML(UTF16ToUTF8(line_name)));
229    subst.push_back(EscapeForHTML(UTF16ToUTF8(line)));
230  } else {
231    resource = IDR_NOTIFICATION_2LINE_HTML;
232    subst.push_back(EscapeForHTML(UTF16ToUTF8(title)));
233    subst.push_back(EscapeForHTML(UTF16ToUTF8(body)));
234  }
235  // body text direction
236  subst.push_back(dir == WebKit::WebTextDirectionRightToLeft ?
237                  "rtl" : "ltr");
238
239  return CreateDataUrl(resource, subst);
240}
241
242// static
243string16 DesktopNotificationService::CreateDataUrl(
244    int resource, const std::vector<std::string>& subst) {
245  const base::StringPiece template_html(
246      ResourceBundle::GetSharedInstance().GetRawDataResource(
247          resource));
248
249  if (template_html.empty()) {
250    NOTREACHED() << "unable to load template. ID: " << resource;
251    return string16();
252  }
253
254  std::string data = ReplaceStringPlaceholders(template_html, subst, NULL);
255  return UTF8ToUTF16("data:text/html;charset=utf-8," +
256                      EscapeQueryParamValue(data, false));
257}
258
259DesktopNotificationService::DesktopNotificationService(Profile* profile,
260    NotificationUIManager* ui_manager)
261    : profile_(profile),
262      ui_manager_(ui_manager) {
263  prefs_registrar_.Init(profile_->GetPrefs());
264  InitPrefs();
265  StartObserving();
266}
267
268DesktopNotificationService::~DesktopNotificationService() {
269  StopObserving();
270}
271
272void DesktopNotificationService::RegisterUserPrefs(PrefService* user_prefs) {
273  content_settings::NotificationProvider::RegisterUserPrefs(user_prefs);
274}
275
276// Initialize the cache with the allowed and denied origins, or
277// create the preferences if they don't exist yet.
278void DesktopNotificationService::InitPrefs() {
279  provider_.reset(new content_settings::NotificationProvider(profile_));
280
281  std::vector<GURL> allowed_origins;
282  std::vector<GURL> denied_origins;
283  ContentSetting default_content_setting = CONTENT_SETTING_DEFAULT;
284
285  if (!profile_->IsOffTheRecord()) {
286    default_content_setting =
287        profile_->GetHostContentSettingsMap()->GetDefaultContentSetting(
288            CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
289    allowed_origins = GetAllowedOrigins();
290    denied_origins = GetBlockedOrigins();
291  }
292
293  prefs_cache_ = new NotificationsPrefsCache();
294  prefs_cache_->SetCacheDefaultContentSetting(default_content_setting);
295  prefs_cache_->SetCacheAllowedOrigins(allowed_origins);
296  prefs_cache_->SetCacheDeniedOrigins(denied_origins);
297  prefs_cache_->set_is_initialized(true);
298}
299
300void DesktopNotificationService::StartObserving() {
301  if (!profile_->IsOffTheRecord()) {
302    prefs_registrar_.Add(prefs::kDesktopNotificationAllowedOrigins, this);
303    prefs_registrar_.Add(prefs::kDesktopNotificationDeniedOrigins, this);
304    notification_registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
305                                NotificationService::AllSources());
306    notification_registrar_.Add(
307        this,
308        NotificationType::CONTENT_SETTINGS_CHANGED,
309        // TODO(markusheintz): Remember to change to HostContentSettingsMap.
310        NotificationService::AllSources());
311  }
312  notification_registrar_.Add(this, NotificationType::PROFILE_DESTROYED,
313                              Source<Profile>(profile_));
314}
315
316void DesktopNotificationService::StopObserving() {
317  if (!profile_->IsOffTheRecord()) {
318    prefs_registrar_.RemoveAll();
319  }
320  notification_registrar_.RemoveAll();
321}
322
323void DesktopNotificationService::GrantPermission(const GURL& origin) {
324  ContentSettingsPattern pattern =
325      content_settings::NotificationProvider::ToContentSettingsPattern(origin);
326  provider_->SetContentSetting(
327      pattern,
328      pattern,
329      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
330      NO_RESOURCE_IDENTIFIER,
331      CONTENT_SETTING_ALLOW);
332
333  // Schedule a cache update on the IO thread.
334  BrowserThread::PostTask(
335      BrowserThread::IO, FROM_HERE,
336      NewRunnableMethod(
337          prefs_cache_.get(),
338          &NotificationsPrefsCache::CacheAllowedOrigin,
339          origin));
340}
341
342void DesktopNotificationService::DenyPermission(const GURL& origin) {
343  // Update content settings
344  ContentSettingsPattern pattern =
345      content_settings::NotificationProvider::ToContentSettingsPattern(origin);
346  provider_->SetContentSetting(
347      pattern,
348      pattern,
349      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
350      NO_RESOURCE_IDENTIFIER,
351      CONTENT_SETTING_BLOCK);
352
353  // Schedule a cache update on the IO thread.
354  BrowserThread::PostTask(
355      BrowserThread::IO, FROM_HERE,
356      NewRunnableMethod(
357          prefs_cache_.get(),
358          &NotificationsPrefsCache::CacheDeniedOrigin,
359          origin));
360}
361
362void DesktopNotificationService::Observe(NotificationType type,
363                                         const NotificationSource& source,
364                                         const NotificationDetails& details) {
365  if (NotificationType::PREF_CHANGED == type) {
366    const std::string& name = *Details<std::string>(details).ptr();
367    OnPrefsChanged(name);
368  } else if (NotificationType::CONTENT_SETTINGS_CHANGED == type) {
369    // TODO(markusheintz): Check if content settings type default was changed;
370    const ContentSetting default_content_setting =
371        profile_->GetHostContentSettingsMap()->GetDefaultContentSetting(
372            CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
373    // Schedule a cache update on the IO thread.
374    BrowserThread::PostTask(
375        BrowserThread::IO, FROM_HERE,
376        NewRunnableMethod(
377            prefs_cache_.get(),
378            &NotificationsPrefsCache::SetCacheDefaultContentSetting,
379            default_content_setting));
380  } else if (NotificationType::EXTENSION_UNLOADED == type) {
381    // Remove all notifications currently shown or queued by the extension
382    // which was unloaded.
383    const Extension* extension =
384        Details<UnloadedExtensionInfo>(details)->extension;
385    if (extension)
386      ui_manager_->CancelAllBySourceOrigin(extension->url());
387  } else if (NotificationType::PROFILE_DESTROYED == type) {
388    StopObserving();
389  }
390}
391
392void DesktopNotificationService::OnPrefsChanged(const std::string& pref_name) {
393  if (pref_name == prefs::kDesktopNotificationAllowedOrigins) {
394    // Schedule a cache update on the IO thread.
395    std::vector<GURL> allowed_origins(GetAllowedOrigins());
396    BrowserThread::PostTask(
397        BrowserThread::IO, FROM_HERE,
398        NewRunnableMethod(
399            prefs_cache_.get(),
400            &NotificationsPrefsCache::SetCacheAllowedOrigins,
401            allowed_origins));
402  } else if (pref_name == prefs::kDesktopNotificationDeniedOrigins) {
403    // Schedule a cache update on the IO thread.
404    std::vector<GURL> denied_origins(GetBlockedOrigins());
405    BrowserThread::PostTask(
406        BrowserThread::IO, FROM_HERE,
407        NewRunnableMethod(
408            prefs_cache_.get(),
409            &NotificationsPrefsCache::SetCacheDeniedOrigins,
410            denied_origins));
411  } else {
412    NOTREACHED();
413  }
414}
415
416ContentSetting DesktopNotificationService::GetDefaultContentSetting() {
417  return profile_->GetHostContentSettingsMap()->GetDefaultContentSetting(
418      CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
419}
420
421void DesktopNotificationService::SetDefaultContentSetting(
422    ContentSetting setting) {
423  profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
424      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, setting);
425}
426
427bool DesktopNotificationService::IsDefaultContentSettingManaged() const {
428  return profile_->GetHostContentSettingsMap()->IsDefaultContentSettingManaged(
429      CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
430}
431
432void DesktopNotificationService::ResetToDefaultContentSetting() {
433  profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
434      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, CONTENT_SETTING_DEFAULT);
435}
436
437std::vector<GURL> DesktopNotificationService::GetAllowedOrigins() {
438  content_settings::ProviderInterface::Rules content_setting_rules;
439  provider_->GetAllContentSettingsRules(
440      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
441      NO_RESOURCE_IDENTIFIER,
442      &content_setting_rules);
443  std::vector<GURL> allowed_origins;
444
445  GetOriginsWithSettingFromContentSettingsRules(
446      content_setting_rules, CONTENT_SETTING_ALLOW, &allowed_origins);
447
448  return allowed_origins;
449}
450
451std::vector<GURL> DesktopNotificationService::GetBlockedOrigins() {
452  content_settings::ProviderInterface::Rules content_settings_rules;
453  provider_->GetAllContentSettingsRules(
454      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
455      NO_RESOURCE_IDENTIFIER,
456      &content_settings_rules);
457  std::vector<GURL> denied_origins;
458
459  GetOriginsWithSettingFromContentSettingsRules(
460      content_settings_rules, CONTENT_SETTING_BLOCK, &denied_origins);
461
462  return denied_origins;
463}
464
465void DesktopNotificationService::ResetAllowedOrigin(const GURL& origin) {
466  ContentSettingsPattern pattern =
467      ContentSettingsPattern::FromURLNoWildcard(origin);
468  provider_->SetContentSetting(
469      pattern,
470      pattern,
471      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
472      NO_RESOURCE_IDENTIFIER,
473      CONTENT_SETTING_DEFAULT);
474}
475
476void DesktopNotificationService::ResetBlockedOrigin(const GURL& origin) {
477  ContentSettingsPattern pattern =
478      ContentSettingsPattern::FromURLNoWildcard(origin);
479  provider_->SetContentSetting(
480      pattern,
481      pattern,
482      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
483      NO_RESOURCE_IDENTIFIER,
484      CONTENT_SETTING_DEFAULT);
485}
486
487void DesktopNotificationService::ResetAllOrigins() {
488  provider_->ClearAllContentSettingsRules(CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
489}
490
491ContentSetting DesktopNotificationService::GetContentSetting(
492    const GURL& origin) {
493  ContentSetting provided_setting = provider_->GetContentSetting(
494      origin,
495      origin,
496      CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
497      NO_RESOURCE_IDENTIFIER);
498  if (CONTENT_SETTING_DEFAULT == provided_setting)
499    return GetDefaultContentSetting();
500  return provided_setting;
501}
502
503void DesktopNotificationService::RequestPermission(
504    const GURL& origin, int process_id, int route_id, int callback_context,
505    TabContents* tab) {
506  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
507  if (!tab) {
508    Browser* browser = BrowserList::GetLastActive();
509    if (browser)
510      tab = browser->GetSelectedTabContents();
511  }
512
513  if (!tab)
514    return;
515
516  // If |origin| hasn't been seen before and the default content setting for
517  // notifications is "ask", show an infobar.
518  // The cache can only answer queries on the IO thread once it's initialized,
519  // so don't ask the cache.
520  ContentSetting setting = GetContentSetting(origin);
521  if (setting == CONTENT_SETTING_ASK) {
522    // Show an info bar requesting permission.
523    tab->AddInfoBar(new NotificationPermissionInfoBarDelegate(
524                        tab, origin, DisplayNameForOrigin(origin), process_id,
525                        route_id, callback_context));
526  } else {
527    // Notify renderer immediately.
528    RenderViewHost* host = RenderViewHost::FromID(process_id, route_id);
529    if (host) {
530      host->Send(new DesktopNotificationMsg_PermissionRequestDone(
531          route_id, callback_context));
532    }
533  }
534}
535
536void DesktopNotificationService::ShowNotification(
537    const Notification& notification) {
538  ui_manager_->Add(notification, profile_);
539}
540
541bool DesktopNotificationService::CancelDesktopNotification(
542    int process_id, int route_id, int notification_id) {
543  scoped_refptr<NotificationObjectProxy> proxy(
544      new NotificationObjectProxy(process_id, route_id, notification_id,
545                                  false));
546  return ui_manager_->CancelById(proxy->id());
547}
548
549
550bool DesktopNotificationService::ShowDesktopNotification(
551    const DesktopNotificationHostMsg_Show_Params& params,
552    int process_id, int route_id, DesktopNotificationSource source) {
553  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
554  const GURL& origin = params.origin;
555  NotificationObjectProxy* proxy =
556      new NotificationObjectProxy(process_id, route_id,
557                                  params.notification_id,
558                                  source == WorkerNotification);
559  GURL contents;
560  if (params.is_html) {
561    contents = params.contents_url;
562  } else {
563    // "upconvert" the string parameters to a data: URL.
564    contents = GURL(
565        CreateDataUrl(params.icon_url, params.title, params.body,
566                      params.direction));
567  }
568  Notification notification(
569      origin, contents, DisplayNameForOrigin(origin),
570      params.replace_id, proxy);
571  ShowNotification(notification);
572  return true;
573}
574
575string16 DesktopNotificationService::DisplayNameForOrigin(
576    const GURL& origin) {
577  // If the source is an extension, lookup the display name.
578  if (origin.SchemeIs(chrome::kExtensionScheme)) {
579    ExtensionService* ext_service = profile_->GetExtensionService();
580    if (ext_service) {
581      const Extension* extension = ext_service->GetExtensionByURL(origin);
582      if (extension)
583        return UTF8ToUTF16(extension->name());
584    }
585  }
586  return UTF8ToUTF16(origin.host());
587}
588
589void DesktopNotificationService::NotifySettingsChange() {
590  NotificationService::current()->Notify(
591      NotificationType::DESKTOP_NOTIFICATION_SETTINGS_CHANGED,
592      Source<DesktopNotificationService>(this),
593      NotificationService::NoDetails());
594}
595