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 }