1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2013 - 2020 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 "BookmarksModel.h"
22 #include "Console.h"
23 #include "FeedsManager.h"
24 #include "HistoryManager.h"
25 #include "SessionsManager.h"
26 #include "ThemesManager.h"
27 #include "Utils.h"
28 
29 #include <QtCore/QCoreApplication>
30 #include <QtCore/QFile>
31 #include <QtCore/QMimeData>
32 #include <QtCore/QSaveFile>
33 #include <QtWidgets/QMessageBox>
34 
35 namespace Otter
36 {
37 
Bookmark()38 BookmarksModel::Bookmark::Bookmark() : QStandardItem()
39 {
40 }
41 
remove()42 void BookmarksModel::Bookmark::remove()
43 {
44 	BookmarksModel *model(qobject_cast<BookmarksModel*>(this->model()));
45 
46 	if (model)
47 	{
48 		model->removeBookmark(this);
49 	}
50 	else
51 	{
52 		delete this;
53 	}
54 }
55 
setData(const QVariant & value,int role)56 void BookmarksModel::Bookmark::setData(const QVariant &value, int role)
57 {
58 	if (model() && qobject_cast<BookmarksModel*>(model()))
59 	{
60 		model()->setData(index(), value, role);
61 	}
62 	else
63 	{
64 		QStandardItem::setData(value, role);
65 	}
66 }
67 
setItemData(const QVariant & value,int role)68 void BookmarksModel::Bookmark::setItemData(const QVariant &value, int role)
69 {
70 	QStandardItem::setData(value, role);
71 }
72 
clone() const73 QStandardItem* BookmarksModel::Bookmark::clone() const
74 {
75 	Bookmark *bookmark(new Bookmark());
76 	bookmark->setData(data(TypeRole), TypeRole);
77 	bookmark->setData(data(UrlRole), UrlRole);
78 	bookmark->setData(data(TitleRole), TitleRole);
79 	bookmark->setData(data(DescriptionRole), DescriptionRole);
80 	bookmark->setData(data(KeywordRole), KeywordRole);
81 	bookmark->setData(data(TimeAddedRole), TimeAddedRole);
82 	bookmark->setData(data(TimeModifiedRole), TimeModifiedRole);
83 	bookmark->setData(data(TimeVisitedRole), TimeVisitedRole);
84 	bookmark->setData(data(VisitsRole), VisitsRole);
85 
86 	return bookmark;
87 }
88 
getChild(int index) const89 BookmarksModel::Bookmark* BookmarksModel::Bookmark::getChild(int index) const
90 {
91 	BookmarksModel *model(qobject_cast<BookmarksModel*>(this->model()));
92 
93 	if (model)
94 	{
95 		return model->getBookmark(model->index(index, 0, this->index()));
96 	}
97 
98 	return nullptr;
99 }
100 
getTitle() const101 QString BookmarksModel::Bookmark::getTitle() const
102 {
103 	return data(TitleRole).toString();
104 }
105 
getDescription() const106 QString BookmarksModel::Bookmark::getDescription() const
107 {
108 	return QStandardItem::data(DescriptionRole).toString();
109 }
110 
getKeyword() const111 QString BookmarksModel::Bookmark::getKeyword() const
112 {
113 	return QStandardItem::data(KeywordRole).toString();
114 }
115 
getUrl() const116 QUrl BookmarksModel::Bookmark::getUrl() const
117 {
118 	return QStandardItem::data(UrlRole).toUrl();
119 }
120 
getTimeAdded() const121 QDateTime BookmarksModel::Bookmark::getTimeAdded() const
122 {
123 	return QStandardItem::data(TimeAddedRole).toDateTime();
124 }
125 
getTimeModified() const126 QDateTime BookmarksModel::Bookmark::getTimeModified() const
127 {
128 	return QStandardItem::data(TimeModifiedRole).toDateTime();
129 }
130 
getTimeVisited() const131 QDateTime BookmarksModel::Bookmark::getTimeVisited() const
132 {
133 	return QStandardItem::data(TimeVisitedRole).toDateTime();
134 }
135 
getIcon() const136 QIcon BookmarksModel::Bookmark::getIcon() const
137 {
138 	return data(Qt::DecorationRole).value<QIcon>();
139 }
140 
data(int role) const141 QVariant BookmarksModel::Bookmark::data(int role) const
142 {
143 	if (role == Qt::DisplayRole)
144 	{
145 		const BookmarkType type(getType());
146 
147 		switch (type)
148 		{
149 			case RootBookmark:
150 				{
151 					const BookmarksModel *model(qobject_cast<BookmarksModel*>(this->model()));
152 
153 					if (model && model->getFormatMode() == NotesMode)
154 					{
155 						return QCoreApplication::translate("Otter::BookmarksModel", "Notes");
156 					}
157 				}
158 
159 				return QCoreApplication::translate("Otter::BookmarksModel", "Bookmarks");
160 			case TrashBookmark:
161 				return QCoreApplication::translate("Otter::BookmarksModel", "Trash");
162 			case FeedBookmark:
163 			case FolderBookmark:
164 			case UrlBookmark:
165 				if (QStandardItem::data(role).isNull())
166 				{
167 					if (type == UrlBookmark)
168 					{
169 						const BookmarksModel *model(qobject_cast<BookmarksModel*>(this->model()));
170 
171 						if (model && model->getFormatMode() == NotesMode)
172 						{
173 							const QString text(data(DescriptionRole).toString());
174 							const int newLinePosition(text.indexOf(QLatin1Char('\n')));
175 
176 							if (newLinePosition > 0 && newLinePosition < (text.count() - 1))
177 							{
178 								return text.left(newLinePosition) + QStringLiteral("…");
179 							}
180 
181 							return text;
182 						}
183 					}
184 
185 					return QCoreApplication::translate("Otter::BookmarksModel", "(Untitled)");
186 				}
187 
188 				break;
189 			default:
190 				break;
191 		}
192 	}
193 
194 	if (role == Qt::DecorationRole)
195 	{
196 		switch (getType())
197 		{
198 			case RootBookmark:
199 			case FolderBookmark:
200 				return ThemesManager::createIcon(QLatin1String("inode-directory"));
201 			case FeedBookmark:
202 				return ThemesManager::createIcon(QLatin1String("application-rss+xml"));
203 			case TrashBookmark:
204 				return ThemesManager::createIcon(QLatin1String("user-trash"));
205 			case UrlBookmark:
206 				return HistoryManager::getIcon(data(UrlRole).toUrl());
207 			default:
208 				break;
209 		}
210 
211 		return {};
212 	}
213 
214 	if (role == Qt::AccessibleDescriptionRole && getType() == SeparatorBookmark)
215 	{
216 		return QLatin1String("separator");
217 	}
218 
219 	if (role == IsTrashedRole)
220 	{
221 		QModelIndex parent(index().parent());
222 
223 		while (parent.isValid())
224 		{
225 			const BookmarkType type(static_cast<BookmarkType>(parent.data(TypeRole).toInt()));
226 
227 			if (type == RootBookmark)
228 			{
229 				break;
230 			}
231 
232 			if (type == TrashBookmark)
233 			{
234 				return true;
235 			}
236 
237 			parent = parent.parent();
238 		}
239 
240 		return false;
241 	}
242 
243 	return QStandardItem::data(role);
244 }
245 
getRawData(int role) const246 QVariant BookmarksModel::Bookmark::getRawData(int role) const
247 {
248 	return QStandardItem::data(role);
249 }
250 
getUrls() const251 QVector<QUrl> BookmarksModel::Bookmark::getUrls() const
252 {
253 	QVector<QUrl> urls;
254 
255 	if (getType() == UrlBookmark)
256 	{
257 		urls.append(data(UrlRole).toUrl());
258 	}
259 
260 	for (int i = 0; i < rowCount(); ++i)
261 	{
262 		const Bookmark *bookmark(static_cast<Bookmark*>(child(i, 0)));
263 
264 		if (!bookmark)
265 		{
266 			continue;
267 		}
268 
269 		switch (bookmark->getType())
270 		{
271 			case FolderBookmark:
272 				urls.append(bookmark->getUrls());
273 
274 				break;
275 			case UrlBookmark:
276 				urls.append(bookmark->getUrl());
277 
278 				break;
279 			default:
280 				break;
281 		}
282 	}
283 
284 	return urls;
285 }
286 
getIdentifier() const287 quint64 BookmarksModel::Bookmark::getIdentifier() const
288 {
289 	return QStandardItem::data(IdentifierRole).toULongLong();
290 }
291 
getType() const292 BookmarksModel::BookmarkType BookmarksModel::Bookmark::getType() const
293 {
294 	return static_cast<BookmarkType>(QStandardItem::data(TypeRole).toInt());
295 }
296 
getVisits() const297 int BookmarksModel::Bookmark::getVisits() const
298 {
299 	return QStandardItem::data(VisitsRole).toInt();
300 }
301 
isAncestorOf(Bookmark * child) const302 bool BookmarksModel::Bookmark::isAncestorOf(Bookmark *child) const
303 {
304 	if (child == nullptr || child == this)
305 	{
306 		return false;
307 	}
308 
309 	QStandardItem *parent(child->parent());
310 
311 	while (parent)
312 	{
313 		if (parent == this)
314 		{
315 			return true;
316 		}
317 
318 		parent = parent->parent();
319 	}
320 
321 	return false;
322 }
323 
operator <(const QStandardItem & other) const324 bool BookmarksModel::Bookmark::operator<(const QStandardItem &other) const
325 {
326 	const BookmarkType type(getType());
327 
328 	if (type == RootBookmark || type == TrashBookmark)
329 	{
330 		return false;
331 	}
332 
333 	return QStandardItem::operator<(other);
334 }
335 
BookmarksModel(const QString & path,FormatMode mode,QObject * parent)336 BookmarksModel::BookmarksModel(const QString &path, FormatMode mode, QObject *parent) : QStandardItemModel(parent),
337 	m_rootItem(new Bookmark()),
338 	m_trashItem(new Bookmark()),
339 	m_importTargetItem(nullptr),
340 	m_mode(mode)
341 {
342 	m_rootItem->setData(RootBookmark, TypeRole);
343 	m_rootItem->setDragEnabled(false);
344 	m_trashItem->setData(TrashBookmark, TypeRole);
345 	m_trashItem->setDragEnabled(false);
346 	m_trashItem->setEnabled(false);
347 
348 	appendRow(m_rootItem);
349 	appendRow(m_trashItem);
350 	setItemPrototype(new Bookmark());
351 
352 	if (!QFile::exists(path))
353 	{
354 		return;
355 	}
356 
357 	QFile file(path);
358 
359 	if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
360 	{
361 		Console::addMessage(((mode == NotesMode) ? tr("Failed to open notes file: %1") : tr("Failed to open bookmarks file: %1")).arg(file.errorString()), Console::OtherCategory, Console::ErrorLevel, path);
362 
363 		return;
364 	}
365 
366 	QXmlStreamReader reader(&file);
367 
368 	if (reader.readNextStartElement() && reader.name() == QLatin1String("xbel") && reader.attributes().value(QLatin1String("version")).toString() == QLatin1String("1.0"))
369 	{
370 		while (reader.readNextStartElement())
371 		{
372 			if (reader.name() == QLatin1String("folder") || reader.name() == QLatin1String("bookmark") || reader.name() == QLatin1String("separator"))
373 			{
374 				readBookmark(&reader, m_rootItem);
375 			}
376 			else
377 			{
378 				reader.skipCurrentElement();
379 			}
380 
381 			if (reader.hasError())
382 			{
383 				m_rootItem->removeRows(0, m_rootItem->rowCount());
384 
385 				Console::addMessage(((m_mode == NotesMode) ? tr("Failed to load notes file: %1") : tr("Failed to load bookmarks file: %1")).arg(reader.errorString()), Console::OtherCategory, Console::ErrorLevel, path);
386 
387 				QMessageBox::warning(nullptr, tr("Error"), ((m_mode == NotesMode) ? tr("Failed to load notes file.") : tr("Failed to load bookmarks file.")), QMessageBox::Close);
388 
389 				return;
390 			}
391 		}
392 	}
393 
394 	connect(this, &BookmarksModel::itemChanged, this, &BookmarksModel::modelModified);
395 	connect(this, &BookmarksModel::rowsInserted, this, &BookmarksModel::modelModified);
396 	connect(this, &BookmarksModel::rowsInserted, this, &BookmarksModel::notifyBookmarkModified);
397 	connect(this, &BookmarksModel::rowsRemoved, this, &BookmarksModel::modelModified);
398 	connect(this, &BookmarksModel::rowsRemoved, this, &BookmarksModel::notifyBookmarkModified);
399 	connect(this, &BookmarksModel::rowsMoved, this, &BookmarksModel::modelModified);
400 }
401 
beginImport(Bookmark * target,int estimatedUrlsAmount,int estimatedKeywordsAmount)402 void BookmarksModel::beginImport(Bookmark *target, int estimatedUrlsAmount, int estimatedKeywordsAmount)
403 {
404 	m_importTargetItem = target;
405 
406 	beginResetModel();
407 	blockSignals(true);
408 
409 	if (estimatedUrlsAmount > 0)
410 	{
411 		m_urls.reserve(m_urls.count() + estimatedUrlsAmount);
412 	}
413 
414 	if (estimatedKeywordsAmount > 0)
415 	{
416 		m_keywords.reserve(m_keywords.count() + estimatedKeywordsAmount);
417 	}
418 }
419 
endImport()420 void BookmarksModel::endImport()
421 {
422 	m_urls.squeeze();
423 	m_keywords.squeeze();
424 
425 	blockSignals(false);
426 	endResetModel();
427 
428 	if (m_importTargetItem)
429 	{
430 		emit bookmarkModified(m_importTargetItem);
431 
432 		m_importTargetItem = nullptr;
433 	}
434 
435 	emit modelModified();
436 }
437 
trashBookmark(Bookmark * bookmark)438 void BookmarksModel::trashBookmark(Bookmark *bookmark)
439 {
440 	if (!bookmark)
441 	{
442 		return;
443 	}
444 
445 	const BookmarkType type(bookmark->getType());
446 
447 	if (type != RootBookmark && type != TrashBookmark)
448 	{
449 		if (type == SeparatorBookmark || bookmark->data(IsTrashedRole).toBool())
450 		{
451 			bookmark->remove();
452 		}
453 		else
454 		{
455 			Bookmark *previousParent(static_cast<Bookmark*>(bookmark->parent()));
456 
457 			m_trash[bookmark] = {bookmark->parent()->index(), bookmark->row()};
458 
459 			m_trashItem->appendRow(bookmark->parent()->takeRow(bookmark->row()));
460 			m_trashItem->setEnabled(true);
461 
462 			removeBookmarkUrl(bookmark);
463 
464 			emit bookmarkModified(bookmark);
465 			emit bookmarkTrashed(bookmark, previousParent);
466 			emit modelModified();
467 		}
468 	}
469 }
470 
restoreBookmark(Bookmark * bookmark)471 void BookmarksModel::restoreBookmark(Bookmark *bookmark)
472 {
473 	if (!bookmark)
474 	{
475 		return;
476 	}
477 
478 	Bookmark *formerParent(m_trash.contains(bookmark) ? getBookmark(m_trash[bookmark].first) : m_rootItem);
479 
480 	if (!formerParent || static_cast<BookmarkType>(formerParent->data(TypeRole).toInt()) != FolderBookmark)
481 	{
482 		formerParent = m_rootItem;
483 	}
484 
485 	if (m_trash.contains(bookmark))
486 	{
487 		formerParent->insertRow(m_trash[bookmark].second, bookmark->parent()->takeRow(bookmark->row()));
488 
489 		m_trash.remove(bookmark);
490 	}
491 	else
492 	{
493 		formerParent->appendRow(bookmark->parent()->takeRow(bookmark->row()));
494 	}
495 
496 	readdBookmarkUrl(bookmark);
497 
498 	m_trashItem->setEnabled(m_trashItem->rowCount() > 0);
499 
500 	emit bookmarkModified(bookmark);
501 	emit bookmarkRestored(bookmark);
502 	emit modelModified();
503 }
504 
removeBookmark(Bookmark * bookmark)505 void BookmarksModel::removeBookmark(Bookmark *bookmark)
506 {
507 	if (!bookmark)
508 	{
509 		return;
510 	}
511 
512 	removeBookmarkUrl(bookmark);
513 
514 	const quint64 identifier(bookmark->data(IdentifierRole).toULongLong());
515 
516 	if (identifier > 0 && m_identifiers.contains(identifier))
517 	{
518 		m_identifiers.remove(identifier);
519 	}
520 
521 	if (!bookmark->data(KeywordRole).toString().isEmpty() && m_keywords.contains(bookmark->data(KeywordRole).toString()))
522 	{
523 		m_keywords.remove(bookmark->data(KeywordRole).toString());
524 	}
525 
526 	emit bookmarkRemoved(bookmark, static_cast<Bookmark*>(bookmark->parent()));
527 
528 	bookmark->parent()->removeRow(bookmark->row());
529 
530 	emit modelModified();
531 }
532 
readBookmark(QXmlStreamReader * reader,Bookmark * parent)533 void BookmarksModel::readBookmark(QXmlStreamReader *reader, Bookmark *parent)
534 {
535 	Bookmark *bookmark(nullptr);
536 
537 	if (reader->name() == QLatin1String("folder"))
538 	{
539 		bookmark = addBookmark(FolderBookmark, {{IdentifierRole, reader->attributes().value(QLatin1String("id")).toULongLong()}, {TimeAddedRole, readDateTime(reader, QLatin1String("added"))}, {TimeModifiedRole, readDateTime(reader, QLatin1String("modified"))}}, parent);
540 
541 		while (reader->readNext())
542 		{
543 			if (reader->isStartElement())
544 			{
545 				if (reader->name() == QLatin1String("title"))
546 				{
547 					bookmark->setItemData(reader->readElementText().trimmed(), TitleRole);
548 				}
549 				else if (reader->name() == QLatin1String("desc"))
550 				{
551 					bookmark->setItemData(reader->readElementText().trimmed(), DescriptionRole);
552 				}
553 				else if (reader->name() == QLatin1String("folder") || reader->name() == QLatin1String("bookmark") || reader->name() == QLatin1String("separator"))
554 				{
555 					readBookmark(reader, bookmark);
556 				}
557 				else if (reader->name() == QLatin1String("info"))
558 				{
559 					while (reader->readNext())
560 					{
561 						if (reader->isStartElement())
562 						{
563 							if (reader->name() == QLatin1String("metadata") && reader->attributes().value(QLatin1String("owner")).toString().startsWith(QLatin1String("http://otter-browser.org/")))
564 							{
565 								while (reader->readNext())
566 								{
567 									if (reader->isStartElement())
568 									{
569 										if (reader->name() == QLatin1String("keyword"))
570 										{
571 											const QString keyword(reader->readElementText().trimmed());
572 
573 											if (!keyword.isEmpty())
574 											{
575 												bookmark->setItemData(keyword, KeywordRole);
576 
577 												handleKeywordChanged(bookmark, keyword);
578 											}
579 										}
580 										else
581 										{
582 											reader->skipCurrentElement();
583 										}
584 									}
585 									else if (reader->isEndElement() && reader->name() == QLatin1String("metadata"))
586 									{
587 										break;
588 									}
589 								}
590 							}
591 							else
592 							{
593 								reader->skipCurrentElement();
594 							}
595 						}
596 						else if (reader->isEndElement() && reader->name() == QLatin1String("info"))
597 						{
598 							break;
599 						}
600 					}
601 				}
602 				else
603 				{
604 					reader->skipCurrentElement();
605 				}
606 			}
607 			else if (reader->isEndElement() && reader->name() == QLatin1String("folder"))
608 			{
609 				break;
610 			}
611 			else if (reader->hasError())
612 			{
613 				return;
614 			}
615 		}
616 	}
617 	else if (reader->name() == QLatin1String("bookmark"))
618 	{
619 		const bool isFeed(reader->attributes().hasAttribute(QLatin1String("feed")));
620 
621 		bookmark = addBookmark((isFeed ? FeedBookmark : UrlBookmark), {{IdentifierRole, reader->attributes().value(QLatin1String("id")).toULongLong()}, {UrlRole, reader->attributes().value(QLatin1String("href")).toString()}, {TimeAddedRole, readDateTime(reader, QLatin1String("added"))}, {TimeModifiedRole, readDateTime(reader, QLatin1String("modified"))}, {TimeVisitedRole, readDateTime(reader, QLatin1String("visited"))}}, parent);
622 
623 		while (reader->readNext())
624 		{
625 			if (reader->isStartElement())
626 			{
627 				if (reader->name() == QLatin1String("title"))
628 				{
629 					bookmark->setItemData(reader->readElementText().trimmed(), TitleRole);
630 				}
631 				else if (reader->name() == QLatin1String("desc"))
632 				{
633 					bookmark->setItemData(reader->readElementText().trimmed(), DescriptionRole);
634 				}
635 				else if (reader->name() == QLatin1String("info"))
636 				{
637 					while (reader->readNext())
638 					{
639 						if (reader->isStartElement())
640 						{
641 							if (reader->name() == QLatin1String("metadata") && reader->attributes().value(QLatin1String("owner")).toString().startsWith(QLatin1String("http://otter-browser.org/")))
642 							{
643 								while (reader->readNext())
644 								{
645 									if (reader->isStartElement())
646 									{
647 										if (reader->name() == QLatin1String("keyword"))
648 										{
649 											const QString keyword(reader->readElementText().trimmed());
650 
651 											if (!keyword.isEmpty())
652 											{
653 												bookmark->setItemData(keyword, KeywordRole);
654 
655 												handleKeywordChanged(bookmark, keyword);
656 											}
657 										}
658 										else if (reader->name() == QLatin1String("visits"))
659 										{
660 											bookmark->setItemData(reader->readElementText().toInt(), VisitsRole);
661 										}
662 										else
663 										{
664 											reader->skipCurrentElement();
665 										}
666 									}
667 									else if (reader->isEndElement() && reader->name() == QLatin1String("metadata"))
668 									{
669 										break;
670 									}
671 								}
672 							}
673 							else
674 							{
675 								reader->skipCurrentElement();
676 							}
677 						}
678 						else if (reader->isEndElement() && reader->name() == QLatin1String("info"))
679 						{
680 							break;
681 						}
682 					}
683 				}
684 				else
685 				{
686 					reader->skipCurrentElement();
687 				}
688 			}
689 			else if (reader->isEndElement() && reader->name() == QLatin1String("bookmark"))
690 			{
691 				break;
692 			}
693 			else if (reader->hasError())
694 			{
695 				return;
696 			}
697 		}
698 
699 		if (isFeed)
700 		{
701 			setupFeed(bookmark);
702 		}
703 	}
704 	else if (reader->name() == QLatin1String("separator"))
705 	{
706 		addBookmark(SeparatorBookmark, {}, parent);
707 
708 		reader->readNext();
709 	}
710 }
711 
writeBookmark(QXmlStreamWriter * writer,Bookmark * bookmark) const712 void BookmarksModel::writeBookmark(QXmlStreamWriter *writer, Bookmark *bookmark) const
713 {
714 	if (!bookmark)
715 	{
716 		return;
717 	}
718 
719 	const BookmarkType type(bookmark->getType());
720 
721 	switch (type)
722 	{
723 		case FeedBookmark:
724 		case UrlBookmark:
725 			writer->writeStartElement(QLatin1String("bookmark"));
726 			writer->writeAttribute(QLatin1String("id"), QString::number(bookmark->getRawData(IdentifierRole).toULongLong()));
727 
728 			if (type == FeedBookmark)
729 			{
730 				writer->writeAttribute(QLatin1String("feed"), QLatin1String("true"));
731 			}
732 
733 			if (!bookmark->getRawData(UrlRole).toString().isEmpty())
734 			{
735 				writer->writeAttribute(QLatin1String("href"), bookmark->getRawData(UrlRole).toString());
736 			}
737 
738 			if (bookmark->getRawData(TimeAddedRole).toDateTime().isValid())
739 			{
740 				writer->writeAttribute(QLatin1String("added"), bookmark->getRawData(TimeAddedRole).toDateTime().toString(Qt::ISODate));
741 			}
742 
743 			if (bookmark->getRawData(TimeModifiedRole).toDateTime().isValid())
744 			{
745 				writer->writeAttribute(QLatin1String("modified"), bookmark->getRawData(TimeModifiedRole).toDateTime().toString(Qt::ISODate));
746 			}
747 
748 			if (m_mode != NotesMode)
749 			{
750 				if (bookmark->getRawData(TimeVisitedRole).toDateTime().isValid())
751 				{
752 					writer->writeAttribute(QLatin1String("visited"), bookmark->getRawData(TimeVisitedRole).toDateTime().toString(Qt::ISODate));
753 				}
754 
755 				writer->writeTextElement(QLatin1String("title"), bookmark->getRawData(TitleRole).toString());
756 			}
757 
758 			if (!bookmark->getRawData(DescriptionRole).toString().isEmpty())
759 			{
760 				writer->writeTextElement(QLatin1String("desc"), bookmark->getRawData(DescriptionRole).toString());
761 			}
762 
763 			if (m_mode == BookmarksMode && (!bookmark->getRawData(KeywordRole).toString().isEmpty() || bookmark->getRawData(VisitsRole).toInt() > 0))
764 			{
765 				writer->writeStartElement(QLatin1String("info"));
766 				writer->writeStartElement(QLatin1String("metadata"));
767 				writer->writeAttribute(QLatin1String("owner"), QLatin1String("http://otter-browser.org/otter-xbel-bookmark"));
768 
769 				if (!bookmark->getRawData(KeywordRole).toString().isEmpty())
770 				{
771 					writer->writeTextElement(QLatin1String("keyword"), bookmark->getRawData(KeywordRole).toString());
772 				}
773 
774 				if (bookmark->getRawData(VisitsRole).toInt() > 0)
775 				{
776 					writer->writeTextElement(QLatin1String("visits"), QString::number(bookmark->getRawData(VisitsRole).toInt()));
777 				}
778 
779 				writer->writeEndElement();
780 				writer->writeEndElement();
781 			}
782 
783 			writer->writeEndElement();
784 
785 			break;
786 		case FolderBookmark:
787 			writer->writeStartElement(QLatin1String("folder"));
788 			writer->writeAttribute(QLatin1String("id"), QString::number(bookmark->getRawData(IdentifierRole).toULongLong()));
789 
790 			if (bookmark->getRawData(TimeAddedRole).toDateTime().isValid())
791 			{
792 				writer->writeAttribute(QLatin1String("added"), bookmark->getRawData(TimeAddedRole).toDateTime().toString(Qt::ISODate));
793 			}
794 
795 			if (bookmark->getRawData(TimeModifiedRole).toDateTime().isValid())
796 			{
797 				writer->writeAttribute(QLatin1String("modified"), bookmark->getRawData(TimeModifiedRole).toDateTime().toString(Qt::ISODate));
798 			}
799 
800 			writer->writeTextElement(QLatin1String("title"), bookmark->getRawData(TitleRole).toString());
801 
802 			if (!bookmark->getRawData(DescriptionRole).toString().isEmpty())
803 			{
804 				writer->writeTextElement(QLatin1String("desc"), bookmark->getRawData(DescriptionRole).toString());
805 			}
806 
807 			if (m_mode == BookmarksMode && !bookmark->getRawData(KeywordRole).toString().isEmpty())
808 			{
809 				writer->writeStartElement(QLatin1String("info"));
810 				writer->writeStartElement(QLatin1String("metadata"));
811 				writer->writeAttribute(QLatin1String("owner"), QLatin1String("http://otter-browser.org/otter-xbel-bookmark"));
812 				writer->writeTextElement(QLatin1String("keyword"), bookmark->getRawData(KeywordRole).toString());
813 				writer->writeEndElement();
814 				writer->writeEndElement();
815 			}
816 
817 			for (int i = 0; i < bookmark->rowCount(); ++i)
818 			{
819 				writeBookmark(writer, bookmark->getChild(i));
820 			}
821 
822 			writer->writeEndElement();
823 
824 			break;
825 		default:
826 			writer->writeEmptyElement(QLatin1String("separator"));
827 
828 			break;
829 	}
830 }
831 
removeBookmarkUrl(Bookmark * bookmark)832 void BookmarksModel::removeBookmarkUrl(Bookmark *bookmark)
833 {
834 	if (!bookmark)
835 	{
836 		return;
837 	}
838 
839 	switch (bookmark->getType())
840 	{
841 		case FeedBookmark:
842 		case UrlBookmark:
843 			{
844 				const QUrl url(Utils::normalizeUrl(bookmark->data(UrlRole).toUrl()));
845 
846 				if (!url.isEmpty() && m_urls.contains(url))
847 				{
848 					m_urls[url].removeAll(bookmark);
849 
850 					if (m_urls[url].isEmpty())
851 					{
852 						m_urls.remove(url);
853 					}
854 				}
855 			}
856 
857 			break;
858 		case FolderBookmark:
859 			for (int i = 0; i < bookmark->rowCount(); ++i)
860 			{
861 				removeBookmarkUrl(bookmark->getChild(i));
862 			}
863 
864 			break;
865 		default:
866 			break;
867 	}
868 }
869 
readdBookmarkUrl(Bookmark * bookmark)870 void BookmarksModel::readdBookmarkUrl(Bookmark *bookmark)
871 {
872 	if (!bookmark)
873 	{
874 		return;
875 	}
876 
877 	switch (bookmark->getType())
878 	{
879 		case FolderBookmark:
880 			for (int i = 0; i < bookmark->rowCount(); ++i)
881 			{
882 				readdBookmarkUrl(bookmark->getChild(i));
883 			}
884 
885 			break;
886 		case UrlBookmark:
887 			{
888 				const QUrl url(Utils::normalizeUrl(bookmark->data(UrlRole).toUrl()));
889 
890 				if (!url.isEmpty())
891 				{
892 					if (!m_urls.contains(url))
893 					{
894 						m_urls[url] = QVector<Bookmark*>();
895 					}
896 
897 					m_urls[url].append(bookmark);
898 				}
899 			}
900 
901 			break;
902 		default:
903 			break;
904 	}
905 }
906 
setupFeed(BookmarksModel::Bookmark * bookmark)907 void BookmarksModel::setupFeed(BookmarksModel::Bookmark *bookmark)
908 {
909 	const QUrl normalizedUrl(Utils::normalizeUrl(bookmark->getUrl()));
910 	Feed *feed(FeedsManager::createFeed(bookmark->getUrl(), bookmark->getTitle()));
911 
912 	if (!m_feeds.contains(normalizedUrl))
913 	{
914 		m_feeds[normalizedUrl] = {};
915 
916 		connect(feed, &Feed::entriesModified, this, &BookmarksModel::handleFeedModified);
917 	}
918 
919 	m_feeds[normalizedUrl].append(bookmark);
920 
921 	if (feed)
922 	{
923 		handleFeedModified(feed);
924 	}
925 }
926 
emptyTrash()927 void BookmarksModel::emptyTrash()
928 {
929 	m_trashItem->removeRows(0, m_trashItem->rowCount());
930 	m_trashItem->setEnabled(false);
931 
932 	m_trash.clear();
933 
934 	emit modelModified();
935 }
936 
handleFeedModified(Feed * feed)937 void BookmarksModel::handleFeedModified(Feed *feed)
938 {
939 	if (!hasFeed(feed->getUrl()))
940 	{
941 		return;
942 	}
943 
944 	const QUrl normalizedUrl(Utils::normalizeUrl(feed->getUrl()));
945 	QVector<Bookmark*> bookmarks(m_feeds.value(feed->getUrl()));
946 
947 	if (feed->getUrl() != normalizedUrl)
948 	{
949 		bookmarks.append(m_feeds.value(normalizedUrl));
950 	}
951 
952 	if (bookmarks.isEmpty())
953 	{
954 		return;
955 	}
956 
957 	beginResetModel();
958 	blockSignals(true);
959 
960 	for (int i = 0; i < bookmarks.count(); ++i)
961 	{
962 		Bookmark *bookmark(bookmarks.at(i));
963 		bookmark->removeRows(0, bookmark->rowCount());
964 
965 		const QVector<Feed::Entry> entries(feed->getEntries());
966 
967 		for (int j = 0; j < entries.count(); ++j)
968 		{
969 			const Feed::Entry entry(entries.at(j));
970 
971 			if (entry.url.isValid())
972 			{
973 				addBookmark(UrlBookmark, {{UrlRole, entry.url}, {TitleRole, entry.title}, {DescriptionRole, entry.summary}}, bookmark);
974 			}
975 		}
976 	}
977 
978 	blockSignals(false);
979 	endResetModel();
980 
981 	for (int i = 0; i < bookmarks.count(); ++i)
982 	{
983 		emit bookmarkModified(bookmarks.at(i));
984 	}
985 
986 	emit modelModified();
987 }
988 
handleKeywordChanged(Bookmark * bookmark,const QString & newKeyword,const QString & oldKeyword)989 void BookmarksModel::handleKeywordChanged(Bookmark *bookmark, const QString &newKeyword, const QString &oldKeyword)
990 {
991 	if (!oldKeyword.isEmpty() && m_keywords.contains(oldKeyword))
992 	{
993 		m_keywords.remove(oldKeyword);
994 	}
995 
996 	if (!newKeyword.isEmpty())
997 	{
998 		m_keywords[newKeyword] = bookmark;
999 	}
1000 }
1001 
handleUrlChanged(Bookmark * bookmark,const QUrl & newUrl,const QUrl & oldUrl)1002 void BookmarksModel::handleUrlChanged(Bookmark *bookmark, const QUrl &newUrl, const QUrl &oldUrl)
1003 {
1004 	if (!oldUrl.isEmpty() && m_urls.contains(oldUrl))
1005 	{
1006 		m_urls[oldUrl].removeAll(bookmark);
1007 
1008 		if (m_urls[oldUrl].isEmpty())
1009 		{
1010 			m_urls.remove(oldUrl);
1011 		}
1012 	}
1013 
1014 	if (!newUrl.isEmpty())
1015 	{
1016 		if (!m_urls.contains(newUrl))
1017 		{
1018 			m_urls[newUrl] = QVector<Bookmark*>();
1019 		}
1020 
1021 		m_urls[newUrl].append(bookmark);
1022 	}
1023 }
1024 
notifyBookmarkModified(const QModelIndex & index)1025 void BookmarksModel::notifyBookmarkModified(const QModelIndex &index)
1026 {
1027 	Bookmark *bookmark(getBookmark(index));
1028 
1029 	if (bookmark)
1030 	{
1031 		emit bookmarkModified(bookmark);
1032 	}
1033 }
1034 
addBookmark(BookmarkType type,const QMap<int,QVariant> & metaData,Bookmark * parent,int index)1035 BookmarksModel::Bookmark* BookmarksModel::addBookmark(BookmarkType type, const QMap<int, QVariant> &metaData, Bookmark *parent, int index)
1036 {
1037 	Bookmark *bookmark(new Bookmark());
1038 
1039 	if (!parent)
1040 	{
1041 		parent = m_rootItem;
1042 	}
1043 
1044 	parent->insertRow(((index < 0) ? parent->rowCount() : index), bookmark);
1045 
1046 	if (type == FeedBookmark || type == UrlBookmark || type == SeparatorBookmark)
1047 	{
1048 		bookmark->setDropEnabled(false);
1049 	}
1050 
1051 	if (type == FeedBookmark || type == FolderBookmark || type == UrlBookmark)
1052 	{
1053 		quint64 identifier(metaData.value(IdentifierRole).toULongLong());
1054 
1055 		if (identifier == 0 || m_identifiers.contains(identifier))
1056 		{
1057 			identifier = (m_identifiers.isEmpty() ? 1 : (m_identifiers.keys().last() + 1));
1058 		}
1059 
1060 		m_identifiers[identifier] = bookmark;
1061 
1062 		setItemData(bookmark->index(), metaData);
1063 
1064 		bookmark->setItemData(identifier, IdentifierRole);
1065 
1066 		if (!metaData.contains(TimeAddedRole) || !metaData.contains(TimeModifiedRole))
1067 		{
1068 			const QDateTime currentDateTime(QDateTime::currentDateTimeUtc());
1069 
1070 			bookmark->setItemData(currentDateTime, TimeAddedRole);
1071 			bookmark->setItemData(currentDateTime, TimeModifiedRole);
1072 		}
1073 
1074 		if (type == FeedBookmark || type == UrlBookmark)
1075 		{
1076 			const QUrl url(metaData.value(UrlRole).toUrl());
1077 
1078 			if (!url.isEmpty())
1079 			{
1080 				handleUrlChanged(bookmark, Utils::normalizeUrl(url));
1081 			}
1082 
1083 			if (type == UrlBookmark)
1084 			{
1085 				bookmark->setFlags(bookmark->flags() | Qt::ItemNeverHasChildren);
1086 			}
1087 		}
1088 	}
1089 
1090 	if (type == FeedBookmark)
1091 	{
1092 		setupFeed(bookmark);
1093 	}
1094 
1095 	bookmark->setItemData(type, TypeRole);
1096 
1097 	emit bookmarkAdded(bookmark);
1098 	emit modelModified();
1099 
1100 	return bookmark;
1101 }
1102 
getBookmarkByKeyword(const QString & keyword) const1103 BookmarksModel::Bookmark* BookmarksModel::getBookmarkByKeyword(const QString &keyword) const
1104 {
1105 	if (m_keywords.contains(keyword))
1106 	{
1107 		return m_keywords[keyword];
1108 	}
1109 
1110 	return nullptr;
1111 }
1112 
getBookmarkByPath(const QString & path) const1113 BookmarksModel::Bookmark* BookmarksModel::getBookmarkByPath(const QString &path) const
1114 {
1115 	if (path == QLatin1String("/"))
1116 	{
1117 		return m_rootItem;
1118 	}
1119 
1120 	if (path.startsWith(QLatin1Char('#')))
1121 	{
1122 		return getBookmark(path.mid(1).toULongLong());
1123 	}
1124 
1125 	Bookmark *bookmark(m_rootItem);
1126 	const QStringList directories(path.split(QLatin1Char('/'), QString::SkipEmptyParts));
1127 
1128 	for (int i = 0; i < directories.count(); ++i)
1129 	{
1130 		bool hasMatch(false);
1131 
1132 		for (int j = 0; j < bookmark->rowCount(); ++j)
1133 		{
1134 			Bookmark *childBookmark(bookmark->getChild(j));
1135 
1136 			if (childBookmark && childBookmark->getTitle() == directories.at(i))
1137 			{
1138 				bookmark = childBookmark;
1139 
1140 				hasMatch = true;
1141 
1142 				break;
1143 			}
1144 		}
1145 
1146 		if (!hasMatch)
1147 		{
1148 			return nullptr;
1149 		}
1150 	}
1151 
1152 	return bookmark;
1153 }
1154 
getBookmark(const QModelIndex & index) const1155 BookmarksModel::Bookmark* BookmarksModel::getBookmark(const QModelIndex &index) const
1156 {
1157 	Bookmark *bookmark(static_cast<Bookmark*>(itemFromIndex(index)));
1158 
1159 	if (bookmark)
1160 	{
1161 		return bookmark;
1162 	}
1163 
1164 	const QVariant data(index.data(BookmarksModel::IdentifierRole));
1165 
1166 	return (data.isValid() ? getBookmark(data.toULongLong()) : nullptr);
1167 }
1168 
getBookmark(quint64 identifier) const1169 BookmarksModel::Bookmark* BookmarksModel::getBookmark(quint64 identifier) const
1170 {
1171 	if (identifier == 0)
1172 	{
1173 		return m_rootItem;
1174 	}
1175 
1176 	if (m_identifiers.contains(identifier))
1177 	{
1178 		return m_identifiers[identifier];
1179 	}
1180 
1181 	return nullptr;
1182 }
1183 
getRootItem() const1184 BookmarksModel::Bookmark* BookmarksModel::getRootItem() const
1185 {
1186 	return m_rootItem;
1187 }
1188 
getTrashItem() const1189 BookmarksModel::Bookmark* BookmarksModel::getTrashItem() const
1190 {
1191 	return m_trashItem;
1192 }
1193 
mimeData(const QModelIndexList & indexes) const1194 QMimeData* BookmarksModel::mimeData(const QModelIndexList &indexes) const
1195 {
1196 	QMimeData *mimeData(new QMimeData());
1197 	QStringList texts;
1198 	texts.reserve(indexes.count());
1199 
1200 	QList<QUrl> urls;
1201 	urls.reserve(indexes.count());
1202 
1203 	if (indexes.count() == 1)
1204 	{
1205 		mimeData->setProperty("x-item-index", indexes.at(0));
1206 	}
1207 
1208 	for (int i = 0; i < indexes.count(); ++i)
1209 	{
1210 		if (indexes.at(i).isValid() && static_cast<BookmarkType>(indexes.at(i).data(TypeRole).toInt()) == UrlBookmark)
1211 		{
1212 			texts.append(indexes.at(i).data(UrlRole).toString());
1213 			urls.append(indexes.at(i).data(UrlRole).toUrl());
1214 		}
1215 	}
1216 
1217 	mimeData->setText(texts.join(QLatin1String(", ")));
1218 	mimeData->setUrls(urls);
1219 
1220 	return mimeData;
1221 }
1222 
readDateTime(QXmlStreamReader * reader,const QString & attribute)1223 QDateTime BookmarksModel::readDateTime(QXmlStreamReader *reader, const QString &attribute)
1224 {
1225 	QDateTime dateTime(QDateTime::fromString(reader->attributes().value(attribute).toString(), Qt::ISODate));
1226 	dateTime.setTimeSpec(Qt::UTC);
1227 
1228 	return dateTime;
1229 }
1230 
mimeTypes() const1231 QStringList BookmarksModel::mimeTypes() const
1232 {
1233 	return {QLatin1String("text/uri-list")};
1234 }
1235 
getKeywords() const1236 QStringList BookmarksModel::getKeywords() const
1237 {
1238 	return m_keywords.keys();
1239 }
1240 
findBookmarks(const QString & prefix) const1241 QVector<BookmarksModel::BookmarkMatch> BookmarksModel::findBookmarks(const QString &prefix) const
1242 {
1243 	QVector<Bookmark*> matchedBookmarks;
1244 	QVector<BookmarkMatch> allMatches;
1245 	QVector<BookmarkMatch> currentMatches;
1246 	QMultiMap<QDateTime, BookmarkMatch> matchesMap;
1247 	QHash<QString, Bookmark*>::const_iterator keywordsIterator;
1248 
1249 	for (keywordsIterator = m_keywords.constBegin(); keywordsIterator != m_keywords.constEnd(); ++keywordsIterator)
1250 	{
1251 		if (keywordsIterator.key().startsWith(prefix, Qt::CaseInsensitive))
1252 		{
1253 			BookmarkMatch match;
1254 			match.bookmark = keywordsIterator.value();
1255 			match.match = keywordsIterator.key();
1256 
1257 			matchesMap.insert(match.bookmark->getTimeVisited(), match);
1258 
1259 			matchedBookmarks.append(match.bookmark);
1260 		}
1261 	}
1262 
1263 	currentMatches = matchesMap.values().toVector();
1264 
1265 	matchesMap.clear();
1266 
1267 	for (int i = (currentMatches.count() - 1); i >= 0; --i)
1268 	{
1269 		allMatches.append(currentMatches.at(i));
1270 	}
1271 
1272 	QHash<QUrl, QVector<Bookmark*> >::const_iterator urlsIterator;
1273 
1274 	for (urlsIterator = m_urls.constBegin(); urlsIterator != m_urls.constEnd(); ++urlsIterator)
1275 	{
1276 		if (urlsIterator.value().isEmpty() || matchedBookmarks.contains(urlsIterator.value().first()))
1277 		{
1278 			continue;
1279 		}
1280 
1281 		const QString result(Utils::matchUrl(urlsIterator.key(), prefix));
1282 
1283 		if (!result.isEmpty())
1284 		{
1285 			BookmarkMatch match;
1286 			match.bookmark = urlsIterator.value().first();
1287 			match.match = result;
1288 
1289 			matchesMap.insert(match.bookmark->getTimeVisited(), match);
1290 
1291 			matchedBookmarks.append(match.bookmark);
1292 		}
1293 	}
1294 
1295 	currentMatches = matchesMap.values().toVector();
1296 
1297 	matchesMap.clear();
1298 
1299 	for (int i = (currentMatches.count() - 1); i >= 0; --i)
1300 	{
1301 		allMatches.append(currentMatches.at(i));
1302 	}
1303 
1304 	return allMatches;
1305 }
1306 
findUrls(const QUrl & url,QStandardItem * branch) const1307 QVector<BookmarksModel::Bookmark*> BookmarksModel::findUrls(const QUrl &url, QStandardItem *branch) const
1308 {
1309 	if (!branch)
1310 	{
1311 		branch = m_rootItem;
1312 	}
1313 
1314 	const QUrl normalizedUrl(Utils::normalizeUrl(url));
1315 	QVector<Bookmark*> bookmarks;
1316 
1317 	for (int i = 0; i < branch->rowCount(); ++i)
1318 	{
1319 		Bookmark *bookmark(static_cast<Bookmark*>(branch->child(i)));
1320 
1321 		if (bookmark)
1322 		{
1323 			switch (bookmark->getType())
1324 			{
1325 				case FolderBookmark:
1326 					bookmarks.append(findUrls(url, bookmark));
1327 
1328 					break;
1329 				case UrlBookmark:
1330 					if (normalizedUrl == Utils::normalizeUrl(bookmark->getUrl()))
1331 					{
1332 						bookmarks.append(bookmark);
1333 					}
1334 
1335 					break;
1336 				default:
1337 					break;
1338 			}
1339 		}
1340 	}
1341 
1342 	return bookmarks;
1343 }
1344 
getBookmarks(const QUrl & url) const1345 QVector<BookmarksModel::Bookmark*> BookmarksModel::getBookmarks(const QUrl &url) const
1346 {
1347 	const QUrl normalizedUrl(Utils::normalizeUrl(url));
1348 	QVector<BookmarksModel::Bookmark*> bookmarks;
1349 
1350 	if (m_urls.contains(url))
1351 	{
1352 		bookmarks = m_urls[url];
1353 	}
1354 
1355 	if (url != normalizedUrl && m_urls.contains(normalizedUrl))
1356 	{
1357 		bookmarks.append(m_urls[normalizedUrl]);
1358 	}
1359 
1360 	return bookmarks;
1361 }
1362 
getFormatMode() const1363 BookmarksModel::FormatMode BookmarksModel::getFormatMode() const
1364 {
1365 	return m_mode;
1366 }
1367 
moveBookmark(Bookmark * bookmark,Bookmark * newParent,int newRow)1368 bool BookmarksModel::moveBookmark(Bookmark *bookmark, Bookmark *newParent, int newRow)
1369 {
1370 	if (!bookmark || !newParent || bookmark == newParent || bookmark->isAncestorOf(newParent))
1371 	{
1372 		return false;
1373 	}
1374 
1375 	Bookmark *previousParent(static_cast<Bookmark*>(bookmark->parent()));
1376 
1377 	if (!previousParent)
1378 	{
1379 		if (newRow < 0)
1380 		{
1381 			newParent->appendRow(bookmark);
1382 		}
1383 		else
1384 		{
1385 			newParent->insertRow(newRow, bookmark);
1386 		}
1387 
1388 		emit modelModified();
1389 
1390 		return true;
1391 	}
1392 
1393 	const int previousRow(bookmark->row());
1394 
1395 	if (newRow < 0)
1396 	{
1397 		newParent->appendRow(bookmark->parent()->takeRow(bookmark->row()));
1398 
1399 		emit bookmarkMoved(bookmark, previousParent, previousRow);
1400 		emit modelModified();
1401 
1402 		return true;
1403 	}
1404 
1405 	int targetRow(newRow);
1406 
1407 	if (bookmark->parent() == newParent && bookmark->row() < newRow)
1408 	{
1409 		--targetRow;
1410 	}
1411 
1412 	newParent->insertRow(targetRow, bookmark->parent()->takeRow(bookmark->row()));
1413 
1414 	emit bookmarkMoved(bookmark, previousParent, previousRow);
1415 	emit modelModified();
1416 
1417 	return true;
1418 }
1419 
canDropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent) const1420 bool BookmarksModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
1421 {
1422 	const QModelIndex index(data->property("x-item-index").toModelIndex());
1423 
1424 	if (index.isValid())
1425 	{
1426 		const Bookmark *bookmark(getBookmark(index));
1427 		Bookmark *newParent(getBookmark(parent));
1428 
1429 		return (bookmark && newParent && bookmark != newParent && !bookmark->isAncestorOf(newParent));
1430 	}
1431 
1432 	return QStandardItemModel::canDropMimeData(data, action, row, column, parent);
1433 }
1434 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)1435 bool BookmarksModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
1436 {
1437 	const BookmarkType type(static_cast<BookmarkType>(parent.data(TypeRole).toInt()));
1438 
1439 	if (type == FolderBookmark || type == RootBookmark || type == TrashBookmark)
1440 	{
1441 		const QModelIndex index(data->property("x-item-index").toModelIndex());
1442 
1443 		if (index.isValid())
1444 		{
1445 			return moveBookmark(getBookmark(index), getBookmark(parent), row);
1446 		}
1447 
1448 		if (data->hasUrls())
1449 		{
1450 			const QVector<QUrl> urls(Utils::extractUrls(data));
1451 
1452 			for (int i = 0; i < urls.count(); ++i)
1453 			{
1454 				addBookmark(UrlBookmark, {{UrlRole, urls.at(i)}, {TitleRole, (data->property("x-url-title").toString().isEmpty() ? urls.at(i).toString() : data->property("x-url-title").toString())}}, getBookmark(parent), row);
1455 			}
1456 
1457 			return true;
1458 		}
1459 
1460 		return QStandardItemModel::dropMimeData(data, action, row, column, parent);
1461 	}
1462 
1463 	return false;
1464 }
1465 
save(const QString & path) const1466 bool BookmarksModel::save(const QString &path) const
1467 {
1468 	if (SessionsManager::isReadOnly())
1469 	{
1470 		return false;
1471 	}
1472 
1473 	QSaveFile file(path);
1474 
1475 	if (!file.open(QIODevice::WriteOnly))
1476 	{
1477 		return false;
1478 	}
1479 
1480 	QXmlStreamWriter writer(&file);
1481 	writer.setAutoFormatting(true);
1482 	writer.setAutoFormattingIndent(-1);
1483 	writer.writeStartDocument();
1484 	writer.writeDTD(QLatin1String("<!DOCTYPE xbel>"));
1485 	writer.writeStartElement(QLatin1String("xbel"));
1486 	writer.writeAttribute(QLatin1String("version"), QLatin1String("1.0"));
1487 
1488 	for (int i = 0; i < m_rootItem->rowCount(); ++i)
1489 	{
1490 		writeBookmark(&writer, m_rootItem->getChild(i));
1491 	}
1492 
1493 	writer.writeEndDocument();
1494 
1495 	return file.commit();
1496 }
1497 
setData(const QModelIndex & index,const QVariant & value,int role)1498 bool BookmarksModel::setData(const QModelIndex &index, const QVariant &value, int role)
1499 {
1500 	Bookmark *bookmark(getBookmark(index));
1501 
1502 	if (!bookmark)
1503 	{
1504 		return QStandardItemModel::setData(index, value, role);
1505 	}
1506 
1507 	switch (role)
1508 	{
1509 		case DescriptionRole:
1510 			if (m_mode == NotesMode)
1511 			{
1512 				const QString title(value.toString().section(QLatin1Char('\n'), 0, 0).left(100));
1513 
1514 				setData(index, ((title == value.toString().trimmed()) ? title : title + QStringLiteral("…")), TitleRole);
1515 			}
1516 
1517 			break;
1518 		case KeywordRole:
1519 			if (value.toString() != index.data(KeywordRole).toString())
1520 			{
1521 				handleKeywordChanged(bookmark, value.toString(), index.data(KeywordRole).toString());
1522 			}
1523 
1524 			break;
1525 		case UrlRole:
1526 			if (value.toUrl() != index.data(UrlRole).toUrl())
1527 			{
1528 				handleUrlChanged(bookmark, Utils::normalizeUrl(value.toUrl()), Utils::normalizeUrl(index.data(UrlRole).toUrl()));
1529 			}
1530 
1531 			break;
1532 		default:
1533 			break;
1534 	}
1535 
1536 	bookmark->setItemData(value, role);
1537 
1538 	switch (role)
1539 	{
1540 		case TitleRole:
1541 		case UrlRole:
1542 		case DescriptionRole:
1543 		case IdentifierRole:
1544 		case TypeRole:
1545 		case KeywordRole:
1546 		case TimeAddedRole:
1547 		case TimeModifiedRole:
1548 		case TimeVisitedRole:
1549 		case VisitsRole:
1550 			emit bookmarkModified(bookmark);
1551 			emit modelModified();
1552 
1553 			break;
1554 		default:
1555 			break;
1556 	}
1557 
1558 	return true;
1559 }
1560 
hasBookmark(const QUrl & url) const1561 bool BookmarksModel::hasBookmark(const QUrl &url) const
1562 {
1563 	return (m_urls.contains(Utils::normalizeUrl(url)) || m_urls.contains(url));
1564 }
1565 
hasFeed(const QUrl & url) const1566 bool BookmarksModel::hasFeed(const QUrl &url) const
1567 {
1568 	return (m_feeds.contains(Utils::normalizeUrl(url)) || m_feeds.contains(url));
1569 }
1570 
hasKeyword(const QString & keyword) const1571 bool BookmarksModel::hasKeyword(const QString &keyword) const
1572 {
1573 	return m_keywords.contains(keyword);
1574 }
1575 
1576 }
1577