1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "collection.h"
19 
20 #include <QHash>
21 #include <QQueue>
22 #include <QVector>
23 #include <QNetworkReply>
24 #include <QXmlStreamReader>
25 #include <QTimer>
26 
27 #include <KLocalizedString>
28 
29 #include "api.h"
30 #include "internalnetworkaccessmanager.h"
31 #include "logging_networking.h"
32 
33 using namespace Zotero;
34 
35 class Zotero::Collection::Private
36 {
37 private:
38     Zotero::Collection *p;
39 
40 public:
41     QSharedPointer<Zotero::API> api;
42 
43     static const QString top;
44 
Private(QSharedPointer<Zotero::API> a,Zotero::Collection * parent)45     Private(QSharedPointer<Zotero::API> a, Zotero::Collection *parent)
46             : p(parent), api(a) {
47         initialized = false;
48         busy = false;
49     }
50 
51     bool initialized, busy;
52 
53     QQueue<QString> downloadQueue;
54 
55     QHash<QString, QString> collectionToLabel;
56     QHash<QString, QString> collectionToParent;
57     QHash<QString, QVector<QString> > collectionToChildren;
58 
requestZoteroUrl(const QUrl & url)59     QNetworkReply *requestZoteroUrl(const QUrl &url) {
60         busy = true;
61         QUrl internalUrl = url;
62         api->addLimitToUrl(internalUrl);
63         QNetworkRequest request = api->request(internalUrl);
64         QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request);
65         connect(reply, &QNetworkReply::finished, p, &Zotero::Collection::finishedFetchingCollection);
66         return reply;
67     }
68 
runNextInDownloadQueue()69     void runNextInDownloadQueue() {
70         if (!downloadQueue.isEmpty()) {
71             const QString head = downloadQueue.dequeue();
72             QUrl url = api->baseUrl();
73             url = url.adjusted(QUrl::StripTrailingSlash);
74             url.setPath(url.path() + QString(QStringLiteral("/collections/%1/collections")).arg(head));
75             if (api->inBackoffMode())
76                 /// If Zotero asked to 'back off', wait until this period is over before issuing the next request
77                 QTimer::singleShot((api->backoffSecondsLeft() + 1) * 1000, p, [ = ]() {
78                     requestZoteroUrl(url);
79                 });
80             else
81                 requestZoteroUrl(url);
82         } else {
83             initialized = true;
84             p->emitFinishedLoading();
85         }
86     }
87 };
88 
89 const QString Zotero::Collection::Private::top = QStringLiteral("top");
90 
Collection(QSharedPointer<Zotero::API> api,QObject * parent)91 Collection::Collection(QSharedPointer<Zotero::API> api, QObject *parent)
92         : QObject(parent), d(new Zotero::Collection::Private(api, this))
93 {
94     d->collectionToLabel[Private::top] = i18n("Library");
95 
96     QUrl url = api->baseUrl();
97     url = url.adjusted(QUrl::StripTrailingSlash);
98     url.setPath(url.path() + QStringLiteral("/collections/top"));
99     if (api->inBackoffMode())
100         /// If Zotero asked to 'back off', wait until this period is over before issuing the next request
101         QTimer::singleShot((api->backoffSecondsLeft() + 1) * 1000, this, [ = ]() {
102         d->requestZoteroUrl(url);
103     });
104     else
105         d->requestZoteroUrl(url);
106 }
107 
~Collection()108 Collection::~Collection()
109 {
110     delete d;
111 }
112 
initialized() const113 bool Collection::initialized() const
114 {
115     return d->initialized;
116 }
117 
busy() const118 bool Collection::busy() const
119 {
120     return d->busy;
121 }
122 
collectionLabel(const QString & collectionId) const123 QString Collection::collectionLabel(const QString &collectionId) const
124 {
125     if (!d->initialized) return QString();
126 
127     return d->collectionToLabel[collectionId];
128 }
129 
collectionParent(const QString & collectionId) const130 QString Collection::collectionParent(const QString &collectionId) const
131 {
132     if (!d->initialized) return QString();
133 
134     return d->collectionToParent[collectionId];
135 }
136 
collectionChildren(const QString & collectionId) const137 QVector<QString> Collection::collectionChildren(const QString &collectionId) const
138 {
139     if (!d->initialized) return QVector<QString>();
140 
141     return QVector<QString>(d->collectionToChildren[collectionId]);
142 }
143 
collectionNumericId(const QString & collectionId) const144 uint Collection::collectionNumericId(const QString &collectionId) const
145 {
146     if (!d->initialized) return 0;
147 
148     if (collectionId == Private::top) /// root node
149         return 0;
150 
151     return qHash(collectionId);
152 }
153 
collectionFromNumericId(uint numericId) const154 QString Collection::collectionFromNumericId(uint numericId) const
155 {
156     if (numericId == 0) /// root node
157         return Private::top;
158 
159     // TODO make those resolutions more efficient
160     const QList<QString> keys = d->collectionToLabel.keys();
161     for (const QString &key : keys) {
162         if (numericId == qHash(key))
163             return key;
164     }
165     return QString();
166 }
167 
finishedFetchingCollection()168 void Collection::finishedFetchingCollection()
169 {
170     QNetworkReply *reply = static_cast<QNetworkReply *>(sender());
171     QString parentId = Private::top;
172 
173     if (reply->hasRawHeader("Backoff")) {
174         bool ok = false;
175         int time = QString::fromLatin1(reply->rawHeader("Backoff").constData()).toInt(&ok);
176         if (!ok) time = 10; ///< parsing argument of raw header 'Backoff' failed? 10 seconds is fallback
177         d->api->startBackoff(time);
178     } else if (reply->hasRawHeader("Retry-After")) {
179         bool ok = false;
180         int time = QString::fromLatin1(reply->rawHeader("Retry-After").constData()).toInt(&ok);
181         if (!ok) time = 10; ///< parsing argument of raw header 'Retry-After' failed? 10 seconds is fallback
182         d->api->startBackoff(time);
183     }
184 
185     if (reply->error() == QNetworkReply::NoError) {
186         QString nextPage;
187         QXmlStreamReader xmlReader(reply);
188         while (!xmlReader.atEnd() && !xmlReader.hasError()) {
189             const QXmlStreamReader::TokenType tt = xmlReader.readNext();
190             if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("title")) {
191                 /// Not perfect: guess author name from collection's title
192                 const QStringList titleFragments = xmlReader.readElementText(QXmlStreamReader::IncludeChildElements).split(QStringLiteral(" / "));
193                 if (titleFragments.count() == 3)
194                     d->collectionToLabel[Private::top] = i18n("%1's Library", titleFragments[1]);
195             } else if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("entry")) {
196                 QString title, key;
197                 while (!xmlReader.atEnd() && !xmlReader.hasError()) {
198                     const QXmlStreamReader::TokenType tt = xmlReader.readNext();
199                     if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("title"))
200                         title = xmlReader.readElementText(QXmlStreamReader::IncludeChildElements);
201                     else if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("key"))
202                         key = xmlReader.readElementText(QXmlStreamReader::IncludeChildElements);
203                     else if (tt == QXmlStreamReader::EndElement && xmlReader.name() == QStringLiteral("entry"))
204                         break;
205                 }
206 
207                 if (!key.isEmpty() && !title.isEmpty()) {
208                     d->downloadQueue.enqueue(key);
209                     d->collectionToLabel.insert(key, title);
210                     d->collectionToParent.insert(key, parentId);
211                     QVector<QString> vec = d->collectionToChildren[parentId];
212                     vec.append(key);
213                     d->collectionToChildren[parentId] = vec;
214                 }
215             } else if (tt == QXmlStreamReader::StartElement && xmlReader.name() == QStringLiteral("link")) {
216                 const QXmlStreamAttributes attrs = xmlReader.attributes();
217                 if (attrs.hasAttribute(QStringLiteral("rel")) && attrs.hasAttribute(QStringLiteral("href")) && attrs.value(QStringLiteral("rel")) == QStringLiteral("next"))
218                     nextPage = attrs.value(QStringLiteral("href")).toString();
219                 else if (attrs.hasAttribute(QStringLiteral("rel")) && attrs.hasAttribute(QStringLiteral("href")) && attrs.value(QStringLiteral("rel")) == QStringLiteral("self")) {
220                     const QString text = attrs.value(QStringLiteral("href")).toString();
221                     const int p1 = text.indexOf(QStringLiteral("/collections/"));
222                     const int p2 = text.indexOf(QStringLiteral("/"), p1 + 14);
223                     if (p1 > 0 && p2 > p1 + 14)
224                         parentId = text.mid(p1 + 13, p2 - p1 - 13);
225                 }
226             } else if (tt == QXmlStreamReader::EndElement && xmlReader.name() == QStringLiteral("feed"))
227                 break;
228         }
229 
230         if (!nextPage.isEmpty()) {
231             if (d->api->inBackoffMode())
232                 /// If Zotero asked to 'back off', wait until this period is over before issuing the next request
233                 QTimer::singleShot((d->api->backoffSecondsLeft() + 1) * 1000, this, [ = ]() {
234                     d->requestZoteroUrl(nextPage);
235                 });
236             else
237                 d->requestZoteroUrl(nextPage);
238         } else
239             d->runNextInDownloadQueue();
240     } else {
241         qCWarning(LOG_KBIBTEX_NETWORKING) << reply->errorString(); ///< something went wrong
242         d->initialized = false;
243         emitFinishedLoading();
244     }
245 }
246 
emitFinishedLoading()247 void Collection::emitFinishedLoading()
248 {
249     d->busy = false;
250     emit finishedLoading();
251 }
252