File indexing completed on 2025-01-12 04:35:49

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2019 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "zoterobrowser.h"
0021 
0022 #include <QTreeView>
0023 #include <QTabWidget>
0024 #include <QListView>
0025 #include <QLayout>
0026 #include <QFormLayout>
0027 #include <QAbstractItemModel>
0028 #include <QRadioButton>
0029 #include <QPushButton>
0030 #include <QPointer>
0031 #include <QLineEdit>
0032 #include <QComboBox>
0033 
0034 #include <KLocalizedString>
0035 #include <KWallet>
0036 #include <KMessageBox>
0037 
0038 #include <Element>
0039 #include "searchresults.h"
0040 #include "zotero/collectionmodel.h"
0041 #include "zotero/collection.h"
0042 #include "zotero/items.h"
0043 #include "zotero/groups.h"
0044 #include "zotero/tags.h"
0045 #include "zotero/tagmodel.h"
0046 #include "zotero/api.h"
0047 #include "zotero/oauthwizard.h"
0048 #include "logging_program.h"
0049 
0050 using KWallet::Wallet;
0051 
0052 class ZoteroBrowser::Private
0053 {
0054 private:
0055     ZoteroBrowser *p;
0056 
0057 public:
0058     Zotero::Items *items;
0059     Zotero::Groups *groups;
0060     Zotero::Tags *tags;
0061     Zotero::TagModel *tagModel;
0062     Zotero::Collection *collection;
0063     Zotero::CollectionModel *collectionModel;
0064     QSharedPointer<Zotero::API> api;
0065     bool needToApplyCredentials;
0066 
0067     SearchResults *searchResults;
0068 
0069     QTabWidget *tabWidget;
0070     QTreeView *collectionBrowser;
0071     QListView *tagBrowser;
0072     QLineEdit *lineEditNumericUserId;
0073     QLineEdit *lineEditApiKey;
0074     QRadioButton *radioPersonalLibrary;
0075     QRadioButton *radioGroupLibrary;
0076     bool comboBoxGroupListInitialized;
0077     QComboBox *comboBoxGroupList;
0078 
0079     QCursor nonBusyCursor;
0080 
0081     Wallet *wallet;
0082     static const QString walletFolderOAuth, walletEntryKBibTeXZotero, walletKeyZoteroId, walletKeyZoteroApiKey;
0083 
0084     Private(SearchResults *sr, ZoteroBrowser *parent)
0085             : p(parent), items(nullptr), groups(nullptr), tags(nullptr), tagModel(nullptr), collection(nullptr), collectionModel(nullptr), needToApplyCredentials(true), searchResults(sr), comboBoxGroupListInitialized(false), nonBusyCursor(p->cursor()), wallet(nullptr) {
0086         setupGUI();
0087     }
0088 
0089     ~Private() {
0090         if (wallet != nullptr)
0091             delete wallet;
0092         if (items != nullptr) delete items;
0093         if (groups != nullptr) delete groups;
0094         if (tags != nullptr) delete tags;
0095         if (tagModel != nullptr) delete tagModel;
0096         if (collection != nullptr) delete collection;
0097         if (collectionModel != nullptr) delete collectionModel;
0098         api.clear();
0099     }
0100 
0101     void setupGUI()
0102     {
0103         QBoxLayout *layout = new QVBoxLayout(p);
0104         tabWidget = new QTabWidget(p);
0105         layout->addWidget(tabWidget);
0106 
0107         QWidget *container = new QWidget(tabWidget);
0108         tabWidget->addTab(container, QIcon::fromTheme(QStringLiteral("preferences-web-browser-identification")), i18n("Library"));
0109         connect(tabWidget, &QTabWidget::currentChanged, p, &ZoteroBrowser::tabChanged);
0110         QBoxLayout *containerLayout = new QVBoxLayout(container);
0111 
0112         /// Personal or Group Library
0113         QGridLayout *gridLayout = new QGridLayout();
0114         containerLayout->addLayout(gridLayout);
0115         gridLayout->setContentsMargins(0, 0, 0, 0);
0116         gridLayout->setColumnMinimumWidth(0, 16); // TODO determine size of a radio button
0117         radioPersonalLibrary = new QRadioButton(i18n("Personal library"), container);
0118         gridLayout->addWidget(radioPersonalLibrary, 0, 0, 1, 2);
0119         radioGroupLibrary = new QRadioButton(i18n("Group library"), container);
0120         gridLayout->addWidget(radioGroupLibrary, 1, 0, 1, 2);
0121         comboBoxGroupList = new QComboBox(container);
0122         gridLayout->addWidget(comboBoxGroupList, 2, 1, 1, 1);
0123         QSizePolicy sizePolicy = comboBoxGroupList->sizePolicy();
0124         sizePolicy.setHorizontalPolicy(QSizePolicy::MinimumExpanding);
0125         comboBoxGroupList->setSizePolicy(sizePolicy);
0126         radioPersonalLibrary->setChecked(true);
0127         comboBoxGroupList->setEnabled(false);
0128         comboBoxGroupList->addItem(i18n("No groups available"));
0129         connect(radioGroupLibrary, &QRadioButton::toggled, p, &ZoteroBrowser::radioButtonsToggled);
0130         connect(radioPersonalLibrary, &QRadioButton::toggled, p, &ZoteroBrowser::radioButtonsToggled);
0131         connect(comboBoxGroupList, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, [this]() {
0132             needToApplyCredentials = true;
0133         });
0134 
0135         containerLayout->addStretch(10);
0136 
0137         /// Credentials
0138         QFormLayout *containerForm = new QFormLayout();
0139         containerLayout->addLayout(containerForm, 1);
0140         containerForm->setContentsMargins(0, 0, 0, 0);
0141         lineEditNumericUserId = new QLineEdit(container);
0142         lineEditNumericUserId->setSizePolicy(sizePolicy);
0143         lineEditNumericUserId->setReadOnly(true);
0144         containerForm->addRow(i18n("Numeric user id:"), lineEditNumericUserId);
0145         connect(lineEditNumericUserId, &QLineEdit::textChanged, p, &ZoteroBrowser::invalidateGroupList);
0146 
0147         lineEditApiKey = new QLineEdit(container);
0148         lineEditApiKey->setSizePolicy(sizePolicy);
0149         lineEditApiKey->setReadOnly(true);
0150         containerForm->addRow(i18n("API key:"), lineEditApiKey);
0151         connect(lineEditApiKey, &QLineEdit::textChanged, p, &ZoteroBrowser::invalidateGroupList);
0152 
0153         QBoxLayout *containerButtonLayout = new QHBoxLayout();
0154         containerLayout->addLayout(containerButtonLayout, 0);
0155         containerButtonLayout->setContentsMargins(0, 0, 0, 0);
0156         QPushButton *buttonGetOAuthCredentials = new QPushButton(QIcon::fromTheme(QStringLiteral("preferences-web-browser-identification")), i18n("Get New Credentials"), container);
0157         containerButtonLayout->addWidget(buttonGetOAuthCredentials, 0);
0158         connect(buttonGetOAuthCredentials, &QPushButton::clicked, p, &ZoteroBrowser::getOAuthCredentials);
0159         containerButtonLayout->addStretch(1);
0160 
0161         /// Collection browser
0162         collectionBrowser = new QTreeView(tabWidget);
0163         tabWidget->addTab(collectionBrowser, QIcon::fromTheme(QStringLiteral("folder-yellow")), i18n("Collections"));
0164         collectionBrowser->setHeaderHidden(true);
0165         collectionBrowser->setExpandsOnDoubleClick(false);
0166         connect(collectionBrowser, &QTreeView::doubleClicked, p, &ZoteroBrowser::collectionDoubleClicked);
0167 
0168         /// Tag browser
0169         tagBrowser = new QListView(tabWidget);
0170         tabWidget->addTab(tagBrowser, QIcon::fromTheme(QStringLiteral("mail-tagged")), i18n("Tags"));
0171         connect(tagBrowser, &QListView::doubleClicked, p, &ZoteroBrowser::tagDoubleClicked);
0172     }
0173 
0174     void queueReadOAuthCredentials() {
0175         if (wallet != nullptr && wallet->isOpen())
0176             p->readOAuthCredentials(true);
0177         else {
0178             /// Wallet is closed or not initialized
0179             if (wallet != nullptr)
0180                 /// Delete existing but closed wallet, will be replaced by new, open wallet soon
0181                 delete wallet;
0182             p->setEnabled(false);
0183             p->setCursor(Qt::WaitCursor);
0184             wallet = Wallet::openWallet(Wallet::NetworkWallet(), p->winId(), Wallet::Asynchronous);
0185             connect(wallet, &Wallet::walletOpened, p, &ZoteroBrowser::readOAuthCredentials);
0186         }
0187     }
0188 
0189     void queueWriteOAuthCredentials() {
0190         if (wallet != nullptr && wallet->isOpen())
0191             p->writeOAuthCredentials(true);
0192         else {
0193             /// Wallet is closed or not initialized
0194             if (wallet != nullptr)
0195                 /// Delete existing but closed wallet, will be replaced by new, open wallet soon
0196                 delete wallet;
0197             p->setEnabled(false);
0198             p->setCursor(Qt::WaitCursor);
0199             wallet = Wallet::openWallet(Wallet::NetworkWallet(), p->winId(), Wallet::Asynchronous);
0200             connect(wallet, &Wallet::walletOpened, p, &ZoteroBrowser::writeOAuthCredentials);
0201         }
0202     }
0203 };
0204 
0205 const QString ZoteroBrowser::Private::walletFolderOAuth = QStringLiteral("OAuth");
0206 const QString ZoteroBrowser::Private::walletEntryKBibTeXZotero = QStringLiteral("KBibTeX/Zotero");
0207 const QString ZoteroBrowser::Private::walletKeyZoteroId = QStringLiteral("UserId");
0208 const QString ZoteroBrowser::Private::walletKeyZoteroApiKey = QStringLiteral("ApiKey");
0209 
0210 ZoteroBrowser::ZoteroBrowser(SearchResults *searchResults, QWidget *parent)
0211         : QWidget(parent), d(new ZoteroBrowser::Private(searchResults, this))
0212 {
0213     /// Forece GUI update
0214     updateButtons();
0215     radioButtonsToggled();
0216 }
0217 
0218 ZoteroBrowser::~ZoteroBrowser()
0219 {
0220     delete d;
0221 }
0222 
0223 void ZoteroBrowser::visibiltyChanged(bool v) {
0224     if (v && d->lineEditApiKey->text().isEmpty())
0225         /// If Zotero dock became visible and no API key is set, check KWallet for credentials
0226         d->queueReadOAuthCredentials();
0227 }
0228 
0229 
0230 void ZoteroBrowser::modelReset()
0231 {
0232     if (!d->collection->busy() && !d->tags->busy()) {
0233         setCursor(d->nonBusyCursor);
0234         setEnabled(true);
0235     } else {
0236         setCursor(Qt::WaitCursor);
0237         setEnabled(false);
0238     }
0239 
0240     if (!d->tags->busy() && !d->collection->busy() && !(d->collection->initialized() && d->tags->initialized()))
0241         KMessageBox::information(this, i18n("KBibTeX failed to retrieve the bibliography from Zotero. Please check that the provided user id and API key are valid."), i18n("Failed to retrieve data from Zotero"));
0242 }
0243 
0244 void ZoteroBrowser::collectionDoubleClicked(const QModelIndex &index)
0245 {
0246     setCursor(Qt::WaitCursor);
0247     setEnabled(false); ///< will be re-enabled when item retrieve got finished (slot reenableWidget)
0248 
0249     const QString collectionId = index.data(Zotero::CollectionModel::CollectionIdRole).toString();
0250     d->searchResults->clear();
0251     d->items->retrieveItemsByCollection(collectionId);
0252 }
0253 
0254 void ZoteroBrowser::tagDoubleClicked(const QModelIndex &index)
0255 {
0256     setCursor(Qt::WaitCursor);
0257     setEnabled(false); ///< will be re-enabled when item retrieve got finished (slot reenableWidget)
0258 
0259     const QString tag = index.data(Zotero::TagModel::TagRole).toString();
0260     d->searchResults->clear();
0261     d->items->retrieveItemsByTag(tag);
0262 }
0263 
0264 void ZoteroBrowser::showItem(QSharedPointer<Element> e)
0265 {
0266     d->searchResults->insertElement(e);
0267     Q_EMIT itemToShow();
0268 }
0269 
0270 void ZoteroBrowser::reenableWidget()
0271 {
0272     setCursor(d->nonBusyCursor);
0273     setEnabled(true);
0274 }
0275 
0276 void ZoteroBrowser::updateButtons()
0277 {
0278     const bool validNumericIdAndApiKey = !d->lineEditNumericUserId->text().isEmpty() && !d->lineEditApiKey->text().isEmpty();
0279     d->radioGroupLibrary->setEnabled(validNumericIdAndApiKey);
0280     d->radioPersonalLibrary->setEnabled(validNumericIdAndApiKey);
0281     d->needToApplyCredentials = true;
0282 }
0283 
0284 bool ZoteroBrowser::applyCredentials()
0285 {
0286     bool ok = false;
0287     const int userId = d->lineEditNumericUserId->text().toInt(&ok);
0288     const QString apiKey = d->lineEditApiKey->text();
0289     if (ok && !apiKey.isEmpty()) {
0290         setCursor(Qt::WaitCursor);
0291         setEnabled(false);
0292 
0293         ok = false;
0294         int groupId = d->comboBoxGroupList->itemData(d->comboBoxGroupList->currentIndex()).toInt(&ok);
0295         if (!ok) groupId = -1;
0296 
0297         disconnect(d->tags, &Zotero::Tags::finishedLoading, this, &ZoteroBrowser::reenableWidget);
0298         disconnect(d->items, &Zotero::Items::stoppedSearch, this, &ZoteroBrowser::reenableWidget);
0299         disconnect(d->items, &Zotero::Items::foundElement, this, &ZoteroBrowser::showItem);
0300         disconnect(d->tagModel, &Zotero::TagModel::modelReset, this, &ZoteroBrowser::modelReset);
0301         disconnect(d->collectionModel, &Zotero::CollectionModel::modelReset, this, &ZoteroBrowser::modelReset);
0302 
0303         d->collection->deleteLater();
0304         d->items->deleteLater();
0305         d->tags->deleteLater();
0306         d->collectionModel->deleteLater();
0307         d->tagModel->deleteLater();
0308         d->api.clear();
0309 
0310         const bool makeGroupRequest = d->radioGroupLibrary->isChecked() && groupId > 0;
0311         d->api = QSharedPointer<Zotero::API>(new Zotero::API(makeGroupRequest ? Zotero::API::RequestScope::Group : Zotero::API::RequestScope::User, makeGroupRequest ? groupId : userId, d->lineEditApiKey->text(), this));
0312         d->items = new Zotero::Items(d->api, this);
0313         d->tags = new Zotero::Tags(d->api, this);
0314         d->tagModel = new Zotero::TagModel(d->tags, this);
0315         d->tagBrowser->setModel(d->tagModel);
0316         d->collection = new Zotero::Collection(d->api, this);
0317         d->collectionModel = new Zotero::CollectionModel(d->collection, this);
0318         d->collectionBrowser->setModel(d->collectionModel);
0319 
0320         connect(d->collectionModel, &Zotero::CollectionModel::modelReset, this, &ZoteroBrowser::modelReset);
0321         connect(d->tagModel, &Zotero::TagModel::modelReset, this, &ZoteroBrowser::modelReset);
0322         connect(d->items, &Zotero::Items::foundElement, this, &ZoteroBrowser::showItem);
0323         connect(d->items, &Zotero::Items::stoppedSearch, this, &ZoteroBrowser::reenableWidget);
0324         connect(d->tags, &Zotero::Tags::finishedLoading, this, &ZoteroBrowser::reenableWidget);
0325 
0326         d->needToApplyCredentials = false;
0327 
0328         return true;
0329     } else
0330         return false;
0331 }
0332 
0333 void ZoteroBrowser::radioButtonsToggled() {
0334     d->comboBoxGroupList->setEnabled(d->comboBoxGroupListInitialized && d->comboBoxGroupList->count() > 0 && d->radioGroupLibrary->isChecked());
0335     if (!d->comboBoxGroupListInitialized && d->radioGroupLibrary->isChecked())
0336         retrieveGroupList();
0337     d->needToApplyCredentials = true;
0338 }
0339 
0340 void ZoteroBrowser::groupListChanged() {
0341     d->needToApplyCredentials = true;
0342 }
0343 
0344 void ZoteroBrowser::retrieveGroupList() {
0345     bool ok = false;
0346     const int userId = d->lineEditNumericUserId->text().toInt(&ok);
0347     if (ok) {
0348         setCursor(Qt::WaitCursor);
0349         setEnabled(false);
0350         d->comboBoxGroupList->clear();
0351         d->comboBoxGroupListInitialized = false;
0352 
0353         disconnect(d->groups, &Zotero::Groups::finishedLoading, this, &ZoteroBrowser::gotGroupList);
0354         d->groups->deleteLater();
0355         d->api.clear();
0356 
0357         d->api = QSharedPointer<Zotero::API>(new Zotero::API(Zotero::API::RequestScope::User, userId, d->lineEditApiKey->text(), this));
0358         d->groups = new Zotero::Groups(d->api, this);
0359 
0360         connect(d->groups, &Zotero::Groups::finishedLoading, this, &ZoteroBrowser::gotGroupList);
0361     }
0362 }
0363 
0364 void ZoteroBrowser::invalidateGroupList() {
0365     d->comboBoxGroupList->clear();
0366     d->comboBoxGroupListInitialized = false;
0367     d->comboBoxGroupList->addItem(i18n("No groups available or no permissions"));
0368     d->comboBoxGroupList->setEnabled(false);
0369     d->radioPersonalLibrary->setChecked(true);
0370 }
0371 
0372 void ZoteroBrowser::gotGroupList() {
0373     const QMap<int, QString> groupMap = d->groups->groups();
0374     for (QMap<int, QString>::ConstIterator it = groupMap.constBegin(); it != groupMap.constEnd(); ++it) {
0375         d->comboBoxGroupList->addItem(it.value(), QVariant::fromValue<int>(it.key()));
0376     }
0377     if (groupMap.isEmpty()) {
0378         invalidateGroupList();
0379     } else {
0380         d->comboBoxGroupListInitialized = true;
0381         d->comboBoxGroupList->setEnabled(true);
0382         d->needToApplyCredentials = true;
0383     }
0384 
0385     reenableWidget();
0386 }
0387 
0388 void ZoteroBrowser::getOAuthCredentials()
0389 {
0390     QPointer<Zotero::OAuthWizard> wizard = new Zotero::OAuthWizard(this);
0391     if (wizard->exec() && !wizard->apiKey().isEmpty() && wizard->userId() >= 0) {
0392         d->lineEditApiKey->setText(wizard->apiKey());
0393         d->lineEditNumericUserId->setText(QString::number(wizard->userId()));
0394         d->queueWriteOAuthCredentials();
0395         updateButtons();
0396         retrieveGroupList();
0397     }
0398     delete wizard;
0399 }
0400 
0401 void ZoteroBrowser::readOAuthCredentials(bool ok) {
0402     /// Do not call this slot a second time
0403     disconnect(d->wallet, &Wallet::walletOpened, this, &ZoteroBrowser::readOAuthCredentials);
0404 
0405     if (ok && (d->wallet->hasFolder(ZoteroBrowser::Private::walletFolderOAuth) || d->wallet->createFolder(ZoteroBrowser::Private::walletFolderOAuth)) && d->wallet->setFolder(ZoteroBrowser::Private::walletFolderOAuth)) {
0406         if (d->wallet->hasEntry(ZoteroBrowser::Private::walletEntryKBibTeXZotero)) {
0407             QMap<QString, QString> map;
0408             if (d->wallet->readMap(ZoteroBrowser::Private::walletEntryKBibTeXZotero, map) == 0) {
0409                 if (map.contains(ZoteroBrowser::Private::walletKeyZoteroId) && map.contains(ZoteroBrowser::Private::walletKeyZoteroApiKey)) {
0410                     d->lineEditNumericUserId->setText(map.value(ZoteroBrowser::Private::walletKeyZoteroId, QString()));
0411                     d->lineEditApiKey->setText(map.value(ZoteroBrowser::Private::walletKeyZoteroApiKey, QString()));
0412                     updateButtons();
0413                     retrieveGroupList();
0414                 } else
0415                     qCWarning(LOG_KBIBTEX_PROGRAM) << "Failed to locate Zotero Id and/or API key in KWallet";
0416             } else
0417                 qCWarning(LOG_KBIBTEX_PROGRAM) << "Failed to access Zotero data in KWallet";
0418         } else
0419             qCDebug(LOG_KBIBTEX_PROGRAM) << "No Zotero credentials stored in KWallet";
0420     } else
0421         qCWarning(LOG_KBIBTEX_PROGRAM) << "Accessing KWallet to sync API key did not succeed";
0422     reenableWidget();
0423 }
0424 
0425 void ZoteroBrowser::writeOAuthCredentials(bool ok) {
0426     disconnect(d->wallet, &Wallet::walletOpened, this, &ZoteroBrowser::writeOAuthCredentials);
0427 
0428     if (ok && (d->wallet->hasFolder(ZoteroBrowser::Private::walletFolderOAuth) || d->wallet->createFolder(ZoteroBrowser::Private::walletFolderOAuth)) && d->wallet->setFolder(ZoteroBrowser::Private::walletFolderOAuth)) {
0429         QMap<QString, QString> map;
0430         map.insert(ZoteroBrowser::Private::walletKeyZoteroId, d->lineEditNumericUserId->text());
0431         map.insert(ZoteroBrowser::Private::walletKeyZoteroApiKey, d->lineEditApiKey->text());
0432         if (d->wallet->writeMap(ZoteroBrowser::Private::walletEntryKBibTeXZotero, map) != 0)
0433             qCWarning(LOG_KBIBTEX_PROGRAM) << "Writing API key to KWallet failed";
0434     } else
0435         qCWarning(LOG_KBIBTEX_PROGRAM) << "Accessing KWallet to sync API key did not succeed";
0436     reenableWidget();
0437 }
0438 
0439 void ZoteroBrowser::tabChanged(int newTabIndex) {
0440     if (newTabIndex > 0 /** tabs after credential tab*/ && d->needToApplyCredentials) {
0441         const bool success = applyCredentials();
0442         for (int i = 1; i < d->tabWidget->count(); ++i)
0443             d->tabWidget->widget(i)->setEnabled(success);
0444     }
0445 }