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