1// Copyright 2013 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/content_settings/permission_queue_controller.h"
6
7#include "base/prefs/pref_service.h"
8#include "chrome/browser/chrome_notification_types.h"
9#include "chrome/browser/content_settings/host_content_settings_map.h"
10#include "chrome/browser/content_settings/permission_context_uma_util.h"
11#include "chrome/browser/geolocation/geolocation_infobar_delegate.h"
12#include "chrome/browser/infobars/infobar_service.h"
13#include "chrome/browser/media/midi_permission_infobar_delegate.h"
14#include "chrome/browser/notifications/desktop_notification_infobar_delegate.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/services/gcm/push_messaging_infobar_delegate.h"
17#include "chrome/browser/tab_contents/tab_util.h"
18#include "chrome/common/pref_names.h"
19#include "components/content_settings/core/common/content_settings.h"
20#include "components/infobars/core/infobar.h"
21#include "content/public/browser/browser_thread.h"
22#include "content/public/browser/notification_details.h"
23#include "content/public/browser/notification_source.h"
24#include "content/public/browser/notification_types.h"
25#include "content/public/browser/web_contents.h"
26#include "content/public/common/url_constants.h"
27
28#if defined(OS_ANDROID)
29#include "chrome/browser/media/protected_media_identifier_infobar_delegate.h"
30#endif
31
32namespace {
33
34InfoBarService* GetInfoBarService(const PermissionRequestID& id) {
35  content::WebContents* web_contents =
36      tab_util::GetWebContentsByID(id.render_process_id(), id.render_view_id());
37  return web_contents ? InfoBarService::FromWebContents(web_contents) : NULL;
38}
39
40}
41
42
43class PermissionQueueController::PendingInfobarRequest {
44 public:
45  PendingInfobarRequest(ContentSettingsType type,
46                        const PermissionRequestID& id,
47                        const GURL& requesting_frame,
48                        const GURL& embedder,
49                        PermissionDecidedCallback callback);
50  ~PendingInfobarRequest();
51
52  bool IsForPair(const GURL& requesting_frame,
53                 const GURL& embedder) const;
54
55  const PermissionRequestID& id() const { return id_; }
56  const GURL& requesting_frame() const { return requesting_frame_; }
57  bool has_infobar() const { return !!infobar_; }
58  infobars::InfoBar* infobar() { return infobar_; }
59
60  void RunCallback(bool allowed);
61  void CreateInfoBar(PermissionQueueController* controller,
62                     const std::string& display_languages);
63
64 private:
65  ContentSettingsType type_;
66  PermissionRequestID id_;
67  GURL requesting_frame_;
68  GURL embedder_;
69  PermissionDecidedCallback callback_;
70  infobars::InfoBar* infobar_;
71
72  // Purposefully do not disable copying, as this is stored in STL containers.
73};
74
75PermissionQueueController::PendingInfobarRequest::PendingInfobarRequest(
76    ContentSettingsType type,
77    const PermissionRequestID& id,
78    const GURL& requesting_frame,
79    const GURL& embedder,
80    PermissionDecidedCallback callback)
81    : type_(type),
82      id_(id),
83      requesting_frame_(requesting_frame),
84      embedder_(embedder),
85      callback_(callback),
86      infobar_(NULL) {
87}
88
89PermissionQueueController::PendingInfobarRequest::~PendingInfobarRequest() {
90}
91
92bool PermissionQueueController::PendingInfobarRequest::IsForPair(
93    const GURL& requesting_frame,
94    const GURL& embedder) const {
95  return (requesting_frame_ == requesting_frame) && (embedder_ == embedder);
96}
97
98void PermissionQueueController::PendingInfobarRequest::RunCallback(
99    bool allowed) {
100  callback_.Run(allowed);
101}
102
103void PermissionQueueController::PendingInfobarRequest::CreateInfoBar(
104    PermissionQueueController* controller,
105    const std::string& display_languages) {
106  switch (type_) {
107    case CONTENT_SETTINGS_TYPE_GEOLOCATION:
108      infobar_ = GeolocationInfoBarDelegate::Create(
109          GetInfoBarService(id_), controller, id_, requesting_frame_,
110          display_languages);
111      break;
112#if defined(ENABLE_NOTIFICATIONS)
113    case CONTENT_SETTINGS_TYPE_NOTIFICATIONS:
114      infobar_ = DesktopNotificationInfoBarDelegate::Create(
115          GetInfoBarService(id_), controller, id_, requesting_frame_,
116          display_languages);
117      break;
118#endif  // ENABLE_NOTIFICATIONS
119    case CONTENT_SETTINGS_TYPE_MIDI_SYSEX:
120      infobar_ = MidiPermissionInfoBarDelegate::Create(
121          GetInfoBarService(id_), controller, id_, requesting_frame_,
122          display_languages, type_);
123      break;
124    case CONTENT_SETTINGS_TYPE_PUSH_MESSAGING:
125      infobar_ = gcm::PushMessagingInfoBarDelegate::Create(
126          GetInfoBarService(id_), controller, id_, requesting_frame_,
127          display_languages, type_);
128      break;
129#if defined(OS_ANDROID)
130    case CONTENT_SETTINGS_TYPE_PROTECTED_MEDIA_IDENTIFIER:
131      infobar_ = ProtectedMediaIdentifierInfoBarDelegate::Create(
132          GetInfoBarService(id_), controller, id_, requesting_frame_,
133          display_languages);
134      break;
135#endif
136    default:
137      NOTREACHED();
138      break;
139  }
140}
141
142
143PermissionQueueController::PermissionQueueController(Profile* profile,
144                                                     ContentSettingsType type)
145    : profile_(profile),
146      type_(type),
147      in_shutdown_(false) {
148}
149
150PermissionQueueController::~PermissionQueueController() {
151  // Cancel all outstanding requests.
152  in_shutdown_ = true;
153  while (!pending_infobar_requests_.empty())
154    CancelInfoBarRequest(pending_infobar_requests_.front().id());
155}
156
157void PermissionQueueController::CreateInfoBarRequest(
158    const PermissionRequestID& id,
159    const GURL& requesting_frame,
160    const GURL& embedder,
161    PermissionDecidedCallback callback) {
162  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
163
164  if (requesting_frame.SchemeIs(content::kChromeUIScheme) ||
165      embedder.SchemeIs(content::kChromeUIScheme))
166    return;
167
168  pending_infobar_requests_.push_back(PendingInfobarRequest(
169      type_, id, requesting_frame, embedder, callback));
170  if (!AlreadyShowingInfoBarForTab(id))
171    ShowQueuedInfoBarForTab(id);
172}
173
174void PermissionQueueController::CancelInfoBarRequest(
175    const PermissionRequestID& id) {
176  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
177
178  for (PendingInfobarRequests::iterator i(pending_infobar_requests_.begin());
179       i != pending_infobar_requests_.end(); ++i) {
180    if (i->id().Equals(id)) {
181      if (i->has_infobar())
182        GetInfoBarService(id)->RemoveInfoBar(i->infobar());
183      else
184        pending_infobar_requests_.erase(i);
185      return;
186    }
187  }
188}
189
190void PermissionQueueController::OnPermissionSet(
191    const PermissionRequestID& id,
192    const GURL& requesting_frame,
193    const GURL& embedder,
194    bool update_content_setting,
195    bool allowed) {
196  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
197
198  // TODO(miguelg): move the permission persistence to
199  // PermissionContextBase once all the types are moved there.
200  if (update_content_setting) {
201    UpdateContentSetting(requesting_frame, embedder, allowed);
202    if (allowed)
203      PermissionContextUmaUtil::PermissionGranted(type_);
204    else
205      PermissionContextUmaUtil::PermissionDenied(type_);
206  } else {
207    PermissionContextUmaUtil::PermissionDismissed(type_);
208  }
209
210  // Cancel this request first, then notify listeners.  TODO(pkasting): Why
211  // is this order important?
212  PendingInfobarRequests requests_to_notify;
213  PendingInfobarRequests infobars_to_remove;
214  std::vector<PendingInfobarRequests::iterator> pending_requests_to_remove;
215  for (PendingInfobarRequests::iterator i = pending_infobar_requests_.begin();
216       i != pending_infobar_requests_.end(); ++i) {
217    if (!i->IsForPair(requesting_frame, embedder))
218      continue;
219    requests_to_notify.push_back(*i);
220    if (!i->has_infobar()) {
221      // We haven't created an infobar yet, just record the pending request
222      // index and remove it later.
223      pending_requests_to_remove.push_back(i);
224      continue;
225    }
226    if (i->id().Equals(id)) {
227      // The infobar that called us is i->infobar(), and its delegate is
228      // currently in either Accept() or Cancel(). This means that
229      // RemoveInfoBar() will be called later on, and that will trigger a
230      // notification we're observing.
231      continue;
232    }
233
234    // This infobar is for the same frame/embedder pair, but in a different
235    // tab. We should remove it now that we've got an answer for it.
236    infobars_to_remove.push_back(*i);
237  }
238
239  // Remove all infobars for the same |requesting_frame| and |embedder|.
240  for (PendingInfobarRequests::iterator i = infobars_to_remove.begin();
241       i != infobars_to_remove.end(); ++i)
242    GetInfoBarService(i->id())->RemoveInfoBar(i->infobar());
243
244  // Send out the permission notifications.
245  for (PendingInfobarRequests::iterator i = requests_to_notify.begin();
246       i != requests_to_notify.end(); ++i)
247    i->RunCallback(allowed);
248
249  // Remove the pending requests in reverse order.
250  for (int i = pending_requests_to_remove.size() - 1; i >= 0; --i)
251    pending_infobar_requests_.erase(pending_requests_to_remove[i]);
252}
253
254void PermissionQueueController::Observe(
255    int type,
256    const content::NotificationSource& source,
257    const content::NotificationDetails& details) {
258  DCHECK_EQ(chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED, type);
259  // We will receive this notification for all infobar closures, so we need to
260  // check whether this is the geolocation infobar we're tracking. Note that the
261  // InfoBarContainer (if any) may have received this notification before us and
262  // caused the infobar to be deleted, so it's not safe to dereference the
263  // contents of the infobar. The address of the infobar, however, is OK to
264  // use to find the PendingInfobarRequest to remove because
265  // pending_infobar_requests_ will not have received any new entries between
266  // the NotificationService's call to InfoBarContainer::Observe and this
267  // method.
268  infobars::InfoBar* infobar =
269      content::Details<infobars::InfoBar::RemovedDetails>(details)->first;
270  for (PendingInfobarRequests::iterator i = pending_infobar_requests_.begin();
271       i != pending_infobar_requests_.end(); ++i) {
272    if (i->infobar() == infobar) {
273      PermissionRequestID id(i->id());
274      pending_infobar_requests_.erase(i);
275      ShowQueuedInfoBarForTab(id);
276      return;
277    }
278  }
279}
280
281bool PermissionQueueController::AlreadyShowingInfoBarForTab(
282    const PermissionRequestID& id) const {
283  for (PendingInfobarRequests::const_iterator i(
284           pending_infobar_requests_.begin());
285       i != pending_infobar_requests_.end(); ++i) {
286    if (i->id().IsForSameTabAs(id) && i->has_infobar())
287      return true;
288  }
289  return false;
290}
291
292void PermissionQueueController::ShowQueuedInfoBarForTab(
293    const PermissionRequestID& id) {
294  DCHECK(!AlreadyShowingInfoBarForTab(id));
295
296  // We can get here for example during tab shutdown, when the InfoBarService is
297  // removing all existing infobars, thus calling back to Observe(). In this
298  // case the service still exists, and is supplied as the source of the
299  // notification we observed, but is no longer accessible from its WebContents.
300  // In this case we should just go ahead and cancel further infobars for this
301  // tab instead of trying to access the service.
302  //
303  // Similarly, if we're being destroyed, we should also avoid showing further
304  // infobars.
305  InfoBarService* infobar_service = GetInfoBarService(id);
306  if (!infobar_service || in_shutdown_) {
307    ClearPendingInfobarRequestsForTab(id);
308    return;
309  }
310
311  for (PendingInfobarRequests::iterator i = pending_infobar_requests_.begin();
312       i != pending_infobar_requests_.end(); ++i) {
313    if (i->id().IsForSameTabAs(id) && !i->has_infobar()) {
314      RegisterForInfoBarNotifications(infobar_service);
315      i->CreateInfoBar(
316          this, profile_->GetPrefs()->GetString(prefs::kAcceptLanguages));
317      return;
318    }
319  }
320
321  UnregisterForInfoBarNotifications(infobar_service);
322}
323
324void PermissionQueueController::ClearPendingInfobarRequestsForTab(
325    const PermissionRequestID& id) {
326  for (PendingInfobarRequests::iterator i = pending_infobar_requests_.begin();
327       i != pending_infobar_requests_.end(); ) {
328    if (i->id().IsForSameTabAs(id)) {
329      DCHECK(!i->has_infobar());
330      i = pending_infobar_requests_.erase(i);
331    } else {
332      ++i;
333    }
334  }
335}
336
337void PermissionQueueController::RegisterForInfoBarNotifications(
338    InfoBarService* infobar_service) {
339  if (!registrar_.IsRegistered(
340      this, chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED,
341      content::Source<InfoBarService>(infobar_service))) {
342    registrar_.Add(this,
343                   chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED,
344                   content::Source<InfoBarService>(infobar_service));
345  }
346}
347
348void PermissionQueueController::UnregisterForInfoBarNotifications(
349    InfoBarService* infobar_service) {
350  if (registrar_.IsRegistered(
351      this, chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED,
352      content::Source<InfoBarService>(infobar_service))) {
353    registrar_.Remove(this,
354                      chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED,
355                      content::Source<InfoBarService>(infobar_service));
356  }
357}
358
359void PermissionQueueController::UpdateContentSetting(
360    const GURL& requesting_frame,
361    const GURL& embedder,
362    bool allowed) {
363  if (requesting_frame.GetOrigin().SchemeIsFile()) {
364    // Chrome can be launched with --disable-web-security which allows
365    // geolocation requests from file:// URLs. We don't want to store these
366    // in the host content settings map.
367    return;
368  }
369
370  ContentSetting content_setting =
371      allowed ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;
372
373  ContentSettingsPattern embedder_pattern =
374      (type_ == CONTENT_SETTINGS_TYPE_NOTIFICATIONS) ?
375      ContentSettingsPattern::Wildcard() :
376      ContentSettingsPattern::FromURLNoWildcard(embedder.GetOrigin());
377
378  profile_->GetHostContentSettingsMap()->SetContentSetting(
379      ContentSettingsPattern::FromURLNoWildcard(requesting_frame.GetOrigin()),
380      embedder_pattern,
381      type_,
382      std::string(),
383      content_setting);
384}
385