okular/core/bookmarkmanager.cpp
Albert Astals Cid caa351c723 Bookmarks: Resolve symlinks before using an url
If a symlink points to another file, we should use that file as
canonical source for bookmarks, it doesn't make sense to have two
different sets of bookmarks for two paths that are essentially the same
file

BUGS: 424340
2020-08-07 22:34:30 +00:00

729 lines
25 KiB
C++

/***************************************************************************
* Copyright (C) 2006 by Pino Toscano <pino@kde.org> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
***************************************************************************/
#include "bookmarkmanager.h"
// qt/kde includes
#include <KBookmarkAction>
#include <KBookmarkManager>
#include <KBookmarkMenu>
#include <QDebug>
#include <QFileInfo>
#include <QGuiApplication>
#include <QHash>
#include <QSet>
#include <QStandardPaths>
#include <QUrl>
// local includes
#include "document_p.h"
#include "observer.h"
using namespace Okular;
#define foreachObserver(cmd) \
{ \
QSet<DocumentObserver *>::const_iterator it = d->document->m_observers.constBegin(), end = d->document->m_observers.constEnd(); \
for (; it != end; ++it) { \
(*it)->cmd; \
} \
}
#define foreachObserverD(cmd) \
{ \
QSet<DocumentObserver *>::const_iterator it = document->m_observers.constBegin(), end = document->m_observers.constEnd(); \
for (; it != end; ++it) { \
(*it)->cmd; \
} \
}
class OkularBookmarkAction : public KBookmarkAction
{
Q_OBJECT
public:
OkularBookmarkAction(const Okular::DocumentViewport &vp, const KBookmark &bk, KBookmarkOwner *owner, QObject *parent)
: KBookmarkAction(bk, owner, parent)
{
if (vp.isValid())
setText(QString::number(vp.pageNumber + 1) + QStringLiteral(" - ") + text());
setProperty("pageNumber", vp.pageNumber + 1);
setProperty("htmlRef", bk.url().fragment(QUrl::FullyDecoded));
}
inline int pageNumber() const
{
return property("pageNumber").toInt();
}
inline QString htmlRef() const
{
return property("htmlRef").toString();
}
};
static inline bool documentViewportFuzzyCompare(const DocumentViewport &vp1, const DocumentViewport &vp2)
{
bool equal = vp1.isValid() && vp2.isValid() && (vp1.pageNumber == vp2.pageNumber) && (vp1.rePos.pos == vp2.rePos.pos);
if (!equal)
return false;
if (qAbs(vp1.rePos.normalizedX - vp2.rePos.normalizedX) >= 0.000001)
return false;
if (qAbs(vp1.rePos.normalizedY - vp2.rePos.normalizedY) >= 0.000001)
return false;
return true;
}
static inline bool bookmarkLessThan(const KBookmark &b1, const KBookmark &b2)
{
DocumentViewport vp1(b1.url().fragment(QUrl::FullyDecoded));
DocumentViewport vp2(b2.url().fragment(QUrl::FullyDecoded));
return vp1 < vp2;
}
static inline bool okularBookmarkActionLessThan(QAction *a1, QAction *a2)
{
DocumentViewport vp1(static_cast<OkularBookmarkAction *>(a1)->htmlRef());
DocumentViewport vp2(static_cast<OkularBookmarkAction *>(a2)->htmlRef());
return vp1 < vp2;
}
static QUrl mostCanonicalUrl(const QUrl &url)
{
if (!url.isLocalFile())
return url;
const QFileInfo fi(url.toLocalFile());
return QUrl::fromLocalFile(fi.canonicalFilePath());
}
class BookmarkManager::Private : public KBookmarkOwner
{
public:
Private(BookmarkManager *qq)
: KBookmarkOwner()
, q(qq)
, document(nullptr)
, manager(nullptr)
{
}
~Private() override
{
knownFiles.clear();
// no need to delete the manager, it's automatically done by KBookmarkManager
// delete manager;
}
Private(const Private &) = delete;
Private &operator=(const Private &) = delete;
QUrl currentUrl() const override;
QString currentTitle() const override;
bool enableOption(BookmarkOption option) const override;
void openBookmark(const KBookmark &bm, Qt::MouseButtons, Qt::KeyboardModifiers) override;
QHash<QUrl, QString>::iterator bookmarkFind(const QUrl &url, bool doCreate, KBookmarkGroup *result = nullptr);
// slots
void _o_changed(const QString &groupAddress, const QString &caller);
BookmarkManager *q;
QUrl url;
QHash<int, int> urlBookmarks;
DocumentPrivate *document;
QString file;
KBookmarkManager *manager;
QHash<QUrl, QString> knownFiles;
};
static inline QUrl urlForGroup(const KBookmark &group)
{
if (group.url().isValid())
return group.url();
else
return QUrl::fromUserInput(group.fullText());
}
BookmarkManager::BookmarkManager(DocumentPrivate *document)
: QObject(document->m_parent)
, d(new Private(this))
{
setObjectName(QStringLiteral("Okular::BookmarkManager"));
d->document = document;
d->file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/bookmarks.xml");
d->manager = KBookmarkManager::managerForFile(d->file, QStringLiteral("okular"));
d->manager->setEditorOptions(QGuiApplication::applicationDisplayName(), false);
d->manager->setUpdate(true);
connect(d->manager, &KBookmarkManager::changed, this, [this](const QString &groupAddress, const QString &caller) { d->_o_changed(groupAddress, caller); });
}
BookmarkManager::~BookmarkManager()
{
delete d;
}
// BEGIN Reimplementations from KBookmarkOwner
QUrl BookmarkManager::Private::currentUrl() const
{
return url;
}
QString BookmarkManager::Private::currentTitle() const
{
return url.toDisplayString();
}
bool BookmarkManager::Private::enableOption(BookmarkOption option) const
{
Q_UNUSED(option)
return false;
}
void BookmarkManager::Private::openBookmark(const KBookmark &bm, Qt::MouseButtons, Qt::KeyboardModifiers)
{
emit q->openUrl(bm.url());
}
// END Reimplementations from KBookmarkOwner
void BookmarkManager::Private::_o_changed(const QString &groupAddress, const QString &caller)
{
Q_UNUSED(caller);
if (groupAddress.isEmpty())
return;
QUrl referurl;
// first, try to find the bookmark group whom change notification was just received
QHash<QUrl, QString>::iterator it = knownFiles.begin(), itEnd = knownFiles.end();
for (; it != itEnd; ++it) {
if (it.value() == groupAddress) {
referurl = it.key();
knownFiles.erase(it);
break;
}
}
if (!referurl.isValid()) {
const KBookmark bm = manager->findByAddress(groupAddress);
// better be safe than sorry
if (bm.isNull())
return;
Q_ASSERT(bm.isGroup());
referurl = urlForGroup(bm);
}
Q_ASSERT(referurl.isValid());
emit q->bookmarksChanged(referurl);
// case for the url representing the current document
// (this might happen if the same document is open in another place;
// in such case, make really sure to be in sync)
if (referurl == url) {
// save the old bookmarks for the current url
const QHash<int, int> oldUrlBookmarks = urlBookmarks;
// set the same url again, so we reload the information we have about it
q->setUrl(referurl);
// then notify the observers about the changes in the bookmarks
for (int i = 0; i < qMax(oldUrlBookmarks.size(), urlBookmarks.size()); i++) {
bool oldContains = oldUrlBookmarks.contains(i) && oldUrlBookmarks[i] > 0;
bool curContains = urlBookmarks.contains(i) && urlBookmarks[i] > 0;
if (oldContains != curContains) {
foreachObserverD(notifyPageChanged(i, DocumentObserver::Bookmark));
} else if (oldContains && oldUrlBookmarks[i] != urlBookmarks[i]) {
foreachObserverD(notifyPageChanged(i, DocumentObserver::Bookmark));
}
}
}
emit q->saved();
}
QList<QUrl> BookmarkManager::files() const
{
QList<QUrl> ret;
KBookmarkGroup group = d->manager->root();
for (KBookmark bm = group.first(); !bm.isNull(); bm = group.next(bm)) {
if (bm.isSeparator() || !bm.isGroup())
continue;
ret.append(urlForGroup(bm));
}
return ret;
}
KBookmark::List BookmarkManager::bookmarks(const QUrl &documentUrl) const
{
const QUrl url = mostCanonicalUrl(documentUrl);
KBookmark::List ret;
KBookmarkGroup group = d->manager->root();
for (KBookmark bm = group.first(); !bm.isNull(); bm = group.next(bm)) {
if (!bm.isGroup() || urlForGroup(bm) != url)
continue;
KBookmarkGroup group = bm.toGroup();
for (KBookmark b = group.first(); !b.isNull(); b = group.next(b)) {
if (b.isSeparator() || b.isGroup())
continue;
ret.append(b);
}
break;
}
return ret;
}
KBookmark::List BookmarkManager::bookmarks() const
{
return bookmarks(d->url);
}
KBookmark::List BookmarkManager::bookmarks(int page) const
{
const KBookmark::List bmarks = bookmarks();
KBookmark::List ret;
for (const KBookmark &bm : bmarks) {
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (vp.isValid() && vp.pageNumber == page) {
ret.append(bm);
}
}
return ret;
}
KBookmark BookmarkManager::bookmark(int page) const
{
const KBookmark::List bmarks = bookmarks();
for (const KBookmark &bm : bmarks) {
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (vp.isValid() && vp.pageNumber == page) {
return bm;
}
}
return KBookmark();
}
KBookmark BookmarkManager::bookmark(const DocumentViewport &viewport) const
{
if (!viewport.isValid() || !isBookmarked(viewport.pageNumber))
return KBookmark();
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(d->url, false, &thebg);
if (it == d->knownFiles.end())
return KBookmark();
for (KBookmark bm = thebg.first(); !bm.isNull(); bm = thebg.next(bm)) {
if (bm.isSeparator() || bm.isGroup())
continue;
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (documentViewportFuzzyCompare(vp, viewport)) {
return bm;
}
}
return KBookmark();
}
void BookmarkManager::save() const
{
d->manager->emitChanged();
emit const_cast<BookmarkManager *>(this)->saved();
}
QHash<QUrl, QString>::iterator BookmarkManager::Private::bookmarkFind(const QUrl &url, bool doCreate, KBookmarkGroup *result)
{
QHash<QUrl, QString>::iterator it = knownFiles.find(url);
if (it == knownFiles.end()) {
// if the url we want to add a new entry for is not in the hash of the
// known files, then first try to find the file among the top-level
// "folder" names
bool found = false;
KBookmarkGroup root = manager->root();
for (KBookmark bm = root.first(); !found && !bm.isNull(); bm = root.next(bm)) {
if (bm.isSeparator() || !bm.isGroup())
continue;
QUrl tmpurl(urlForGroup(bm));
if (tmpurl == url) {
// got it! place it the hash of known files
KBookmarkGroup bg = bm.toGroup();
it = knownFiles.insert(url, bg.address());
found = true;
if (result)
*result = bg;
break;
}
}
if (!found && doCreate) {
// folder not found :(
// then, in a single step create a new folder and add it in our cache :)
QString purl = url.isLocalFile() ? url.toLocalFile() : url.toDisplayString();
KBookmarkGroup newbg = root.createNewFolder(purl);
newbg.setUrl(url);
it = knownFiles.insert(url, newbg.address());
if (result)
*result = newbg;
}
} else if (result) {
const KBookmark bm = manager->findByAddress(it.value());
Q_ASSERT(bm.isGroup());
*result = bm.toGroup();
}
return it;
}
void BookmarkManager::addBookmark(int page)
{
if (page >= 0 && page < (int)d->document->m_pagesVector.count()) {
if (setPageBookmark(page))
foreachObserver(notifyPageChanged(page, DocumentObserver::Bookmark));
}
}
void BookmarkManager::addBookmark(const DocumentViewport &vp)
{
addBookmark(d->url, vp);
}
bool BookmarkManager::addBookmark(const QUrl &documentUrl, const Okular::DocumentViewport &vp, const QString &title)
{
if (!documentUrl.isValid() || !vp.isValid())
return false;
if (vp.pageNumber < 0 || vp.pageNumber >= d->document->m_pagesVector.count())
return false;
const QUrl referurl = mostCanonicalUrl(documentUrl);
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(referurl, true, &thebg);
Q_ASSERT(it != d->knownFiles.end());
int count = 0; // Number of bookmarks in the current page
bool found = false;
// Check if the bookmark already exists
for (KBookmark bm = thebg.first(); !found && !bm.isNull(); bm = thebg.next(bm)) {
if (bm.isSeparator() || bm.isGroup())
continue;
DocumentViewport bmViewport(bm.url().fragment(QUrl::FullyDecoded));
if (bmViewport.isValid() && bmViewport.pageNumber == vp.pageNumber) {
++count;
if (documentViewportFuzzyCompare(bmViewport, vp))
found = true;
}
}
if (found)
return false;
QString newtitle;
if (title.isEmpty()) {
// if we have no title specified for the new bookmark, then give it the
// name '#p' where p is the page number where the bookmark is located.
// if there's more than one bookmark per page, give the name '#p-n'
// where n is the index of this bookmark among the ones of its page.
if (count > 0)
newtitle = QStringLiteral("#%1-%2").arg(vp.pageNumber + 1).arg(count);
else
newtitle = QStringLiteral("#%1").arg(vp.pageNumber + 1);
} else
newtitle = title;
QUrl newurl = referurl;
newurl.setFragment(vp.toString(), QUrl::DecodedMode);
thebg.addBookmark(newtitle, newurl, QString());
if (referurl == d->document->m_url) {
d->urlBookmarks[vp.pageNumber]++;
foreachObserver(notifyPageChanged(vp.pageNumber, DocumentObserver::Bookmark));
}
d->manager->emitChanged(thebg);
return true;
}
void BookmarkManager::removeBookmark(int page)
{
if (page >= 0 && page < (int)d->document->m_pagesVector.count()) {
if (removePageBookmark(page))
foreachObserver(notifyPageChanged(page, DocumentObserver::Bookmark));
}
}
void BookmarkManager::removeBookmark(const DocumentViewport &vp)
{
int page = vp.pageNumber;
if (page >= 0 && page < d->document->m_pagesVector.count()) {
removeBookmark(d->url, bookmark(vp));
}
}
void BookmarkManager::renameBookmark(KBookmark *bm, const QString &newName)
{
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(d->url, false, &thebg);
Q_ASSERT(it != d->knownFiles.end());
if (it == d->knownFiles.end())
return;
bm->setFullText(newName);
d->manager->emitChanged(thebg);
}
void BookmarkManager::renameBookmark(const QUrl &documentUrl, const QString &newName)
{
if (!documentUrl.isValid())
return;
const QUrl referurl = mostCanonicalUrl(documentUrl);
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(referurl, false, &thebg);
Q_ASSERT(it != d->knownFiles.end());
if (it == d->knownFiles.end())
return;
thebg.setFullText(newName);
d->manager->emitChanged(thebg);
}
QString BookmarkManager::titleForUrl(const QUrl &documentUrl) const
{
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(mostCanonicalUrl(documentUrl), false, &thebg);
Q_ASSERT(it != d->knownFiles.end());
return thebg.fullText();
}
int BookmarkManager::removeBookmark(const QUrl &documentUrl, const KBookmark &bm)
{
if (!documentUrl.isValid() || bm.isNull() || bm.isGroup() || bm.isSeparator())
return -1;
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (!vp.isValid())
return -1;
const QUrl referurl = mostCanonicalUrl(documentUrl);
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(referurl, false, &thebg);
if (it == d->knownFiles.end())
return -1;
thebg.deleteBookmark(bm);
if (referurl == d->document->m_url) {
d->urlBookmarks[vp.pageNumber]--;
foreachObserver(notifyPageChanged(vp.pageNumber, DocumentObserver::Bookmark));
}
d->manager->emitChanged(thebg);
return vp.pageNumber;
}
void BookmarkManager::removeBookmarks(const QUrl &documentUrl, const KBookmark::List &list)
{
if (!documentUrl.isValid() || list.isEmpty())
return;
const QUrl referurl = mostCanonicalUrl(documentUrl);
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(referurl, false, &thebg);
if (it == d->knownFiles.end())
return;
const QHash<int, int> oldUrlBookmarks = d->urlBookmarks;
bool deletedAny = false;
for (const KBookmark &bm : list) {
if (bm.parentGroup() == thebg) {
thebg.deleteBookmark(bm);
deletedAny = true;
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (referurl == d->document->m_url) {
d->urlBookmarks[vp.pageNumber]--;
}
}
}
if (referurl == d->document->m_url) {
for (int i = 0; i < qMax(oldUrlBookmarks.size(), d->urlBookmarks.size()); i++) {
bool oldContains = oldUrlBookmarks.contains(i) && oldUrlBookmarks[i] > 0;
bool curContains = d->urlBookmarks.contains(i) && d->urlBookmarks[i] > 0;
if (oldContains != curContains) {
foreachObserver(notifyPageChanged(i, DocumentObserver::Bookmark));
} else if (oldContains && oldUrlBookmarks[i] != d->urlBookmarks[i]) {
foreachObserver(notifyPageChanged(i, DocumentObserver::Bookmark));
}
}
}
if (deletedAny)
d->manager->emitChanged(thebg);
}
QList<QAction *> BookmarkManager::actionsForUrl(const QUrl &documentUrl) const
{
const QUrl url = mostCanonicalUrl(documentUrl);
QList<QAction *> ret;
KBookmarkGroup group = d->manager->root();
for (KBookmark bm = group.first(); !bm.isNull(); bm = group.next(bm)) {
if (!bm.isGroup() || urlForGroup(bm) != url)
continue;
KBookmarkGroup group = bm.toGroup();
for (KBookmark b = group.first(); !b.isNull(); b = group.next(b)) {
if (b.isSeparator() || b.isGroup())
continue;
ret.append(new OkularBookmarkAction(DocumentViewport(b.url().fragment(QUrl::FullyDecoded)), b, d, nullptr));
}
break;
}
std::sort(ret.begin(), ret.end(), okularBookmarkActionLessThan);
return ret;
}
void BookmarkManager::setUrl(const QUrl &url)
{
d->url = mostCanonicalUrl(url);
d->urlBookmarks.clear();
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(d->url, false, &thebg);
if (it != d->knownFiles.end()) {
for (KBookmark bm = thebg.first(); !bm.isNull(); bm = thebg.next(bm)) {
if (bm.isSeparator() || bm.isGroup())
continue;
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (!vp.isValid())
continue;
d->urlBookmarks[vp.pageNumber]++;
}
}
}
bool BookmarkManager::setPageBookmark(int page)
{
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(d->url, true, &thebg);
Q_ASSERT(it != d->knownFiles.end());
bool found = false;
bool added = false;
for (KBookmark bm = thebg.first(); !found && !bm.isNull(); bm = thebg.next(bm)) {
if (bm.isSeparator() || bm.isGroup())
continue;
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (vp.isValid() && vp.pageNumber == page)
found = true;
}
if (!found) {
d->urlBookmarks[page]++;
DocumentViewport vp;
vp.pageNumber = page;
QUrl newurl = d->url;
newurl.setFragment(vp.toString(), QUrl::DecodedMode);
thebg.addBookmark(QLatin1String("#") + QString::number(vp.pageNumber + 1), newurl, QString());
added = true;
d->manager->emitChanged(thebg);
}
return added;
}
bool BookmarkManager::removePageBookmark(int page)
{
KBookmarkGroup thebg;
QHash<QUrl, QString>::iterator it = d->bookmarkFind(d->url, false, &thebg);
if (it == d->knownFiles.end())
return false;
bool found = false;
for (KBookmark bm = thebg.first(); !found && !bm.isNull(); bm = thebg.next(bm)) {
if (bm.isSeparator() || bm.isGroup())
continue;
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (vp.isValid() && vp.pageNumber == page) {
found = true;
thebg.deleteBookmark(bm);
d->urlBookmarks[page]--;
d->manager->emitChanged(thebg);
}
}
return found;
}
bool BookmarkManager::isBookmarked(int page) const
{
return d->urlBookmarks.contains(page) && d->urlBookmarks[page] > 0;
}
bool BookmarkManager::isBookmarked(const DocumentViewport &viewport) const
{
KBookmark bm = bookmark(viewport);
return !bm.isNull();
}
KBookmark BookmarkManager::nextBookmark(const DocumentViewport &viewport) const
{
KBookmark::List bmarks = bookmarks();
std::sort(bmarks.begin(), bmarks.end(), bookmarkLessThan);
KBookmark bookmark;
for (const KBookmark &bm : qAsConst(bmarks)) {
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (viewport < vp) {
bookmark = bm;
break;
}
}
return bookmark;
}
KBookmark BookmarkManager::previousBookmark(const DocumentViewport &viewport) const
{
KBookmark::List bmarks = bookmarks();
std::sort(bmarks.begin(), bmarks.end(), bookmarkLessThan);
KBookmark bookmark;
for (KBookmark::List::const_iterator it = bmarks.constEnd(); it != bmarks.constBegin(); --it) {
KBookmark bm = *(it - 1);
DocumentViewport vp(bm.url().fragment(QUrl::FullyDecoded));
if (vp < viewport) {
bookmark = bm;
break;
}
}
return bookmark;
}
#undef foreachObserver
#undef foreachObserverD
#include "bookmarkmanager.moc"
/* kate: replace-tabs on; indent-width 4; */