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/geolocation/geolocation_exceptions_table_model.h"
6
7#include "ui/base/l10n/l10n_util.h"
8#include "base/utf_string_conversions.h"
9#include "chrome/common/content_settings_helper.h"
10#include "chrome/common/url_constants.h"
11#include "grit/generated_resources.h"
12#include "ui/base/l10n/l10n_util_collator.h"
13#include "ui/base/models/table_model_observer.h"
14
15namespace {
16// Return -1, 0, or 1 depending on whether |origin1| should be sorted before,
17// equal to, or after |origin2|.
18int CompareOrigins(const GURL& origin1, const GURL& origin2) {
19  if (origin1 == origin2)
20    return 0;
21
22  // Sort alphabetically by host name.
23  std::string origin1_host(origin1.host());
24  std::string origin2_host(origin2.host());
25  if (origin1_host != origin2_host)
26    return origin1_host < origin2_host ? -1 : 1;
27
28  // We'll show non-HTTP schemes, so sort them alphabetically, but put HTTP
29  // first.
30  std::string origin1_scheme(origin1.scheme());
31  std::string origin2_scheme(origin2.scheme());
32  if (origin1_scheme != origin2_scheme) {
33    if (origin1_scheme == chrome::kHttpScheme)
34      return -1;
35    if (origin2_scheme == chrome::kHttpScheme)
36      return 1;
37    return origin1_scheme < origin2_scheme ? -1 : 1;
38  }
39
40  // Sort by port number.  This has to differ if the origins are really origins
41  // (and not longer URLs).  An unspecified port will be -1 and thus
42  // automatically come first (which is what we want).
43  int origin1_port = origin1.IntPort();
44  int origin2_port = origin2.IntPort();
45  DCHECK(origin1_port != origin2_port);
46  return origin1_port < origin2_port ? -1 : 1;
47}
48}  // namespace
49
50struct GeolocationExceptionsTableModel::Entry {
51  Entry(const GURL& in_origin,
52        const GURL& in_embedding_origin,
53        ContentSetting in_setting)
54      : origin(in_origin),
55        embedding_origin(in_embedding_origin),
56        setting(in_setting) {
57  }
58
59  GURL origin;
60  GURL embedding_origin;
61  ContentSetting setting;
62};
63
64GeolocationExceptionsTableModel::GeolocationExceptionsTableModel(
65    GeolocationContentSettingsMap* map)
66    : map_(map),
67      observer_(NULL) {
68  GeolocationContentSettingsMap::AllOriginsSettings settings(
69      map_->GetAllOriginsSettings());
70  GeolocationContentSettingsMap::AllOriginsSettings::const_iterator i;
71  for (i = settings.begin(); i != settings.end(); ++i)
72    AddEntriesForOrigin(i->first, i->second);
73}
74
75GeolocationExceptionsTableModel::~GeolocationExceptionsTableModel() {}
76
77bool GeolocationExceptionsTableModel::CanRemoveRows(
78    const Rows& rows) const {
79  for (Rows::const_iterator i(rows.begin()); i != rows.end(); ++i) {
80    const Entry& entry = entries_[*i];
81    if ((entry.origin == entry.embedding_origin) &&
82        (entry.setting == CONTENT_SETTING_DEFAULT)) {
83      for (size_t j = (*i) + 1;
84           (j < entries_.size()) && (entries_[j].origin == entry.origin); ++j) {
85        if (!rows.count(j))
86          return false;
87      }
88    }
89  }
90  return !rows.empty();
91}
92
93void GeolocationExceptionsTableModel::RemoveRows(const Rows& rows) {
94  for (Rows::const_reverse_iterator i(rows.rbegin()); i != rows.rend(); ++i) {
95    size_t row = *i;
96    Entry* entry = &entries_[row];
97    GURL entry_origin(entry->origin);  // Copy, not reference, since we'll erase
98                                       // |entry| before we're done with this.
99    bool next_has_same_origin = ((row + 1) < entries_.size()) &&
100        (entries_[row + 1].origin == entry_origin);
101    bool has_children = (entry_origin == entry->embedding_origin) &&
102        next_has_same_origin;
103    map_->SetContentSetting(entry_origin, entry->embedding_origin,
104                            CONTENT_SETTING_DEFAULT);
105    if (has_children) {
106      entry->setting = CONTENT_SETTING_DEFAULT;
107      if (observer_)
108        observer_->OnItemsChanged(row, 1);
109      continue;
110    }
111    do {
112      entries_.erase(entries_.begin() + row);  // Note: |entry| is now garbage.
113      if (observer_)
114        observer_->OnItemsRemoved(row, 1);
115      // If we remove the last non-default child of a default parent, we should
116      // remove the parent too.  We do these removals one-at-a-time because the
117      // table view will end up being called back as each row is removed, in
118      // turn calling back to CanRemoveRows(), and if we've already removed
119      // more entries than the view has, we'll have problems.
120      if ((row == 0) || rows.count(row - 1))
121        break;
122      entry = &entries_[--row];
123    } while (!next_has_same_origin && (entry->origin == entry_origin) &&
124             (entry->origin == entry->embedding_origin) &&
125             (entry->setting == CONTENT_SETTING_DEFAULT));
126  }
127}
128
129void GeolocationExceptionsTableModel::RemoveAll() {
130  int old_row_count = RowCount();
131  entries_.clear();
132  map_->ResetToDefault();
133  if (observer_)
134    observer_->OnItemsRemoved(0, old_row_count);
135}
136
137int GeolocationExceptionsTableModel::RowCount() {
138  return entries_.size();
139}
140
141string16 GeolocationExceptionsTableModel::GetText(int row,
142                                                  int column_id) {
143  const Entry& entry = entries_[row];
144  if (column_id == IDS_EXCEPTIONS_HOSTNAME_HEADER) {
145    if (entry.origin == entry.embedding_origin) {
146      return content_settings_helper::OriginToString16(entry.origin);
147    }
148    string16 indent(ASCIIToUTF16("    "));
149    if (entry.embedding_origin.is_empty()) {
150      // NOTE: As long as the user cannot add/edit entries from the exceptions
151      // dialog, it's impossible to actually have a non-default setting for some
152      // origin "embedded on any other site", so this row will never appear.  If
153      // we add the ability to add/edit exceptions, we'll need to decide when to
154      // display this and how "removing" it will function.
155      return indent + l10n_util::GetStringUTF16(
156          IDS_EXCEPTIONS_GEOLOCATION_EMBEDDED_ANY_OTHER);
157    }
158    return indent + l10n_util::GetStringFUTF16(
159        IDS_EXCEPTIONS_GEOLOCATION_EMBEDDED_ON_HOST,
160        content_settings_helper::OriginToString16(entry.embedding_origin));
161  }
162
163  if (column_id == IDS_EXCEPTIONS_ACTION_HEADER) {
164    switch (entry.setting) {
165      case CONTENT_SETTING_ALLOW:
166        return l10n_util::GetStringUTF16(IDS_EXCEPTIONS_ALLOW_BUTTON);
167      case CONTENT_SETTING_BLOCK:
168        return l10n_util::GetStringUTF16(IDS_EXCEPTIONS_BLOCK_BUTTON);
169      case CONTENT_SETTING_ASK:
170        return l10n_util::GetStringUTF16(IDS_EXCEPTIONS_ASK_BUTTON);
171      case CONTENT_SETTING_DEFAULT:
172        return l10n_util::GetStringUTF16(IDS_EXCEPTIONS_NOT_SET_BUTTON);
173      default:
174        break;
175    }
176  }
177
178  NOTREACHED();
179  return string16();
180}
181
182void GeolocationExceptionsTableModel::SetObserver(
183    ui::TableModelObserver* observer) {
184  observer_ = observer;
185}
186
187int GeolocationExceptionsTableModel::CompareValues(int row1,
188                                                   int row2,
189                                                   int column_id) {
190  DCHECK(row1 >= 0 && row1 < RowCount() &&
191         row2 >= 0 && row2 < RowCount());
192
193  const Entry& entry1 = entries_[row1];
194  const Entry& entry2 = entries_[row2];
195
196  // Sort top-level requesting origins, keeping all embedded (child) rules
197  // together.
198  int origin_comparison = CompareOrigins(entry1.origin, entry2.origin);
199  if (origin_comparison == 0) {
200    // The non-embedded rule comes before all embedded rules.
201    bool entry1_origins_same = entry1.origin == entry1.embedding_origin;
202    bool entry2_origins_same = entry2.origin == entry2.embedding_origin;
203    if (entry1_origins_same != entry2_origins_same)
204      return entry1_origins_same ? -1 : 1;
205
206    // The "default" embedded rule comes after all other embedded rules.
207    bool embedding_origin1_empty = entry1.embedding_origin.is_empty();
208    bool embedding_origin2_empty = entry2.embedding_origin.is_empty();
209    if (embedding_origin1_empty != embedding_origin2_empty)
210      return embedding_origin2_empty ? -1 : 1;
211
212    origin_comparison =
213        CompareOrigins(entry1.embedding_origin, entry2.embedding_origin);
214  } else if (column_id == IDS_EXCEPTIONS_ACTION_HEADER) {
215    // The rows are in different origins.  We need to find out how the top-level
216    // origins will compare.
217    while (entries_[row1].origin != entries_[row1].embedding_origin)
218      --row1;
219    while (entries_[row2].origin != entries_[row2].embedding_origin)
220      --row2;
221  }
222
223  // The entries are at the same "scope".  If we're sorting by action, then do
224  // that now.
225  if (column_id == IDS_EXCEPTIONS_ACTION_HEADER) {
226    int compare_text = l10n_util::CompareString16WithCollator(
227        GetCollator(), GetText(row1, column_id), GetText(row2, column_id));
228    if (compare_text != 0)
229      return compare_text;
230  }
231
232  // Sort by the relevant origin.
233  return origin_comparison;
234}
235
236void GeolocationExceptionsTableModel::AddEntriesForOrigin(
237    const GURL& origin,
238    const GeolocationContentSettingsMap::OneOriginSettings& settings) {
239  GeolocationContentSettingsMap::OneOriginSettings::const_iterator parent =
240      settings.find(origin);
241
242  // Add the "parent" entry for the non-embedded setting.
243  entries_.push_back(Entry(origin, origin,
244      (parent == settings.end()) ? CONTENT_SETTING_DEFAULT : parent->second));
245
246  // Add the "children" for any embedded settings.
247  GeolocationContentSettingsMap::OneOriginSettings::const_iterator i;
248  for (i = settings.begin(); i != settings.end(); ++i) {
249    // Skip the non-embedded setting which we already added above.
250    if (i == parent)
251      continue;
252
253    entries_.push_back(Entry(origin, i->first, i->second));
254  }
255}
256