1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2015 - 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 * Copyright (C) 2017 Jan Bajer aka bajasoft <jbajer@gmail.com>
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 **************************************************************************/
20
21 #include "HistoryModel.h"
22 #include "Console.h"
23 #include "JsonSettings.h"
24 #include "SessionsManager.h"
25 #include "ThemesManager.h"
26 #include "Utils.h"
27
28 #include <QtCore/QFile>
29 #include <QtCore/QJsonArray>
30 #include <QtCore/QJsonObject>
31
32 namespace Otter
33 {
34
Entry()35 HistoryModel::Entry::Entry() : QStandardItem()
36 {
37 }
38
setData(const QVariant & value,int role)39 void HistoryModel::Entry::setData(const QVariant &value, int role)
40 {
41 if (model() && qobject_cast<HistoryModel*>(model()))
42 {
43 model()->setData(index(), value, role);
44 }
45 else
46 {
47 QStandardItem::setData(value, role);
48 }
49 }
50
setItemData(const QVariant & value,int role)51 void HistoryModel::Entry::setItemData(const QVariant &value, int role)
52 {
53 QStandardItem::setData(value, role);
54 }
55
getTitle() const56 QString HistoryModel::Entry::getTitle() const
57 {
58 return (data(TitleRole).isNull() ? QCoreApplication::translate("Otter::HistoryEntryItem", "(Untitled)") : data(TitleRole).toString());
59 }
60
getUrl() const61 QUrl HistoryModel::Entry::getUrl() const
62 {
63 return data(UrlRole).toUrl();
64 }
65
getTimeVisited() const66 QDateTime HistoryModel::Entry::getTimeVisited() const
67 {
68 return data(TimeVisitedRole).toDateTime();
69 }
70
getIcon() const71 QIcon HistoryModel::Entry::getIcon() const
72 {
73 const QVariant iconData(data(Qt::DecorationRole));
74
75 return (iconData.isNull() ? ThemesManager::createIcon(QLatin1String("text-html")) : iconData.value<QIcon>());
76 }
77
getIdentifier() const78 quint64 HistoryModel::Entry::getIdentifier() const
79 {
80 return data(IdentifierRole).toULongLong();
81 }
82
HistoryModel(const QString & path,HistoryType type,QObject * parent)83 HistoryModel::HistoryModel(const QString &path, HistoryType type, QObject *parent) : QStandardItemModel(parent),
84 m_type(type)
85 {
86 QFile file(path);
87
88 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
89 {
90 Console::addMessage(tr("Failed to open history file: %1").arg(file.errorString()), Console::OtherCategory, Console::ErrorLevel, path);
91
92 return;
93 }
94
95 const QJsonArray historyArray(QJsonDocument::fromJson(file.readAll()).array());
96
97 file.close();
98
99 for (int i = 0; i < historyArray.count(); ++i)
100 {
101 const QJsonObject entryObject(historyArray.at(i).toObject());
102 QDateTime dateTime(QDateTime::fromString(entryObject.value(QLatin1String("time")).toString(), Qt::ISODate));
103 dateTime.setTimeSpec(Qt::UTC);
104
105 addEntry(QUrl(entryObject.value(QLatin1String("url")).toString()), entryObject.value(QLatin1String("title")).toString(), {}, dateTime);
106 }
107
108 setSortRole(TimeVisitedRole);
109 sort(0, Qt::DescendingOrder);
110 }
111
clearExcessEntries(int limit)112 void HistoryModel::clearExcessEntries(int limit)
113 {
114 if (limit > 0 && rowCount() > limit)
115 {
116 for (int i = (rowCount() - 1); i >= limit; --i)
117 {
118 removeEntry(index(i, 0).data(IdentifierRole).toULongLong());
119 }
120 }
121 }
122
clearRecentEntries(uint period)123 void HistoryModel::clearRecentEntries(uint period)
124 {
125 if (period == 0)
126 {
127 clear();
128
129 m_urls.clear();
130 m_identifiers.clear();
131
132 emit cleared();
133
134 return;
135 }
136
137 for (int i = (rowCount() - 1); i >= 0; --i)
138 {
139 if (index(i, 0).data(TimeVisitedRole).toDateTime().secsTo(QDateTime::currentDateTimeUtc()) < (period * 3600))
140 {
141 removeEntry(index(i, 0).data(IdentifierRole).toULongLong());
142 }
143 }
144 }
145
clearOldestEntries(int period)146 void HistoryModel::clearOldestEntries(int period)
147 {
148 if (period < 0)
149 {
150 return;
151 }
152
153 const QDateTime currentDateTime(QDateTime::currentDateTimeUtc());
154
155 for (int i = (rowCount() - 1); i >= 0; --i)
156 {
157 if (index(i, 0).data(TimeVisitedRole).toDateTime().daysTo(currentDateTime) > period)
158 {
159 removeEntry(index(i, 0).data(IdentifierRole).toULongLong());
160 }
161 }
162 }
163
removeEntry(quint64 identifier)164 void HistoryModel::removeEntry(quint64 identifier)
165 {
166 Entry *entry(getEntry(identifier));
167
168 if (!entry)
169 {
170 return;
171 }
172
173 const QUrl url(Utils::normalizeUrl(entry->getUrl()));
174
175 if (m_urls.contains(url))
176 {
177 m_urls[url].removeAll(entry);
178
179 if (m_urls[url].isEmpty())
180 {
181 m_urls.remove(url);
182 }
183 }
184
185 if (identifier > 0 && m_identifiers.contains(identifier))
186 {
187 m_identifiers.remove(identifier);
188 }
189
190 emit entryRemoved(entry);
191
192 removeRow(entry->row());
193
194 emit modelModified();
195 }
196
addEntry(const QUrl & url,const QString & title,const QIcon & icon,const QDateTime & date,quint64 identifier)197 HistoryModel::Entry* HistoryModel::addEntry(const QUrl &url, const QString &title, const QIcon &icon, const QDateTime &date, quint64 identifier)
198 {
199 blockSignals(true);
200
201 if (m_type == TypedHistory)
202 {
203 const QUrl normalizedUrl(Utils::normalizeUrl(url));
204
205 if (hasEntry(normalizedUrl))
206 {
207 for (int i = 0; i < m_urls[normalizedUrl].count(); ++i)
208 {
209 removeEntry(m_urls[normalizedUrl].at(i)->getIdentifier());
210 }
211 }
212 }
213
214 Entry *entry(new Entry());
215 entry->setIcon(icon);
216
217 insertRow(0, entry);
218 setData(entry->index(), url, UrlRole);
219 setData(entry->index(), title, TitleRole);
220 setData(entry->index(), date, TimeVisitedRole);
221
222 if (identifier == 0 || m_identifiers.contains(identifier))
223 {
224 identifier = (m_identifiers.isEmpty() ? 1 : (m_identifiers.keys().last() + 1));
225 }
226
227 setData(entry->index(), identifier, IdentifierRole);
228
229 m_identifiers[identifier] = entry;
230
231 blockSignals(false);
232
233 emit entryAdded(entry);
234
235 return entry;
236 }
237
getEntry(quint64 identifier) const238 HistoryModel::Entry* HistoryModel::getEntry(quint64 identifier) const
239 {
240 if (m_identifiers.contains(identifier))
241 {
242 return m_identifiers[identifier];
243 }
244
245 return nullptr;
246 }
247
findEntries(const QString & prefix,bool markAsTypedIn) const248 QVector<HistoryModel::HistoryEntryMatch> HistoryModel::findEntries(const QString &prefix, bool markAsTypedIn) const
249 {
250 QVector<Entry*> matchedEntries;
251 QVector<HistoryEntryMatch> allMatches;
252 QVector<HistoryEntryMatch> currentMatches;
253 QMultiMap<QDateTime, HistoryEntryMatch> matchesMap;
254 QHash<QUrl, QVector<Entry*> >::const_iterator urlsIterator;
255
256 for (urlsIterator = m_urls.constBegin(); urlsIterator != m_urls.constEnd(); ++urlsIterator)
257 {
258 if (urlsIterator.value().isEmpty() || matchedEntries.contains(urlsIterator.value().first()))
259 {
260 continue;
261 }
262
263 const QString result(Utils::matchUrl(urlsIterator.key(), prefix));
264
265 if (!result.isEmpty())
266 {
267 HistoryEntryMatch match;
268 match.entry = urlsIterator.value().first();
269 match.match = result;
270
271 if (markAsTypedIn)
272 {
273 match.isTypedIn = true;
274 }
275
276 matchesMap.insert(match.entry->data(TimeVisitedRole).toDateTime(), match);
277
278 matchedEntries.append(match.entry);
279 }
280 }
281
282 currentMatches = matchesMap.values().toVector();
283
284 matchesMap.clear();
285
286 for (int i = (currentMatches.count() - 1); i >= 0; --i)
287 {
288 allMatches.append(currentMatches.at(i));
289 }
290
291 return allMatches;
292 }
293
getType() const294 HistoryModel::HistoryType HistoryModel::getType() const
295 {
296 return m_type;
297 }
298
save(const QString & path) const299 bool HistoryModel::save(const QString &path) const
300 {
301 if (SessionsManager::isReadOnly())
302 {
303 return false;
304 }
305
306 QJsonArray historyArray;
307
308 for (int i = 0; i < rowCount(); ++i)
309 {
310 const QModelIndex index(this->index(i, 0));
311
312 if (index.isValid())
313 {
314 historyArray.prepend(QJsonObject({{QLatin1String("url"), index.data(UrlRole).toUrl().toString()}, {QLatin1String("title"), index.data(TitleRole).toString()}, {QLatin1String("time"), index.data(TimeVisitedRole).toDateTime().toString(Qt::ISODate)}}));
315 }
316 }
317
318 JsonSettings settings;
319 settings.setArray(historyArray);
320
321 return settings.save(path);
322 }
323
setData(const QModelIndex & index,const QVariant & value,int role)324 bool HistoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
325 {
326 Entry *entry(static_cast<Entry*>(itemFromIndex(index)));
327
328 if (!entry)
329 {
330 return QStandardItemModel::setData(index, value, role);
331 }
332
333 if (role == UrlRole && value.toUrl() != index.data(UrlRole).toUrl())
334 {
335 const QUrl oldUrl(Utils::normalizeUrl(index.data(UrlRole).toUrl()));
336 const QUrl newUrl(Utils::normalizeUrl(value.toUrl()));
337
338 if (!oldUrl.isEmpty() && m_urls.contains(oldUrl))
339 {
340 m_urls[oldUrl].removeAll(entry);
341
342 if (m_urls[oldUrl].isEmpty())
343 {
344 m_urls.remove(oldUrl);
345 }
346 }
347
348 if (!newUrl.isEmpty())
349 {
350 if (!m_urls.contains(newUrl))
351 {
352 m_urls[newUrl] = QVector<Entry*>();
353 }
354
355 m_urls[newUrl].append(entry);
356 }
357 }
358
359 entry->setItemData(value, role);
360
361 switch (role)
362 {
363 case TitleRole:
364 case UrlRole:
365 case IdentifierRole:
366 case TimeVisitedRole:
367 emit entryModified(entry);
368 emit modelModified();
369
370 break;
371 default:
372 break;
373 }
374
375 return true;
376 }
377
hasEntry(const QUrl & url) const378 bool HistoryModel::hasEntry(const QUrl &url) const
379 {
380 return m_urls.contains(url);
381 }
382
383 }
384