From c216b20abb550db36717bc937a594a9a150d78bb Mon Sep 17 00:00:00 2001 From: Carlo Baratto Date: Thu, 13 Aug 2026 09:34:39 +0200 Subject: [PATCH] Rimossa la sync server dell'avanzamento di lettura (login di sessione, postBookmark, fetchBookmark): resta solo il salvataggio locale della posizione (QSettings) con ripristino locale all'apertura. ApiClient riportato a feed OPDS + copertine --- qml/pages/ReaderPage.qml | 50 +------- src/apiclient.cpp | 246 --------------------------------------- src/apiclient.h | 29 ----- 3 files changed, 1 insertion(+), 324 deletions(-) diff --git a/qml/pages/ReaderPage.qml b/qml/pages/ReaderPage.qml index 58db1f4..1b5ecaf 100644 --- a/qml/pages/ReaderPage.qml +++ b/qml/pages/ReaderPage.qml @@ -15,7 +15,6 @@ Page { property bool ready: false property bool restoring: false property int saveCounter: 0 - property int syncCounter: 0 property string errorMessage: "" // reflow: scala il font (il testo si ri-avvolge), non lo zoom grafico property double fontScale: 1.0 @@ -36,10 +35,6 @@ Page { text: "Indice capitoli" onClicked: showChapterList() } - MenuItem { - text: "Aggiorna posizione sul server" - onClicked: syncNow() - } MenuItem { text: "Margini del testo" onClicked: { @@ -201,20 +196,11 @@ Page { if (bookId <= 0) return page.saveCounter++ - // salva locale ogni ~10 s, sync server ogni ~30 s + // salva in locale ogni ~10 s (o subito su forzatura) if (force || page.saveCounter % 5 === 0) { appSettings.setBookmarkKey(page.bookId, page.bookFormat, book.makeBookmarkKey(page.currentChapter, page.chapterFraction)) } - if (force || page.syncCounter++ % 15 === 0) - syncNow() - } - - function syncNow() { - if (bookId <= 0) - return - apiClient.postBookmark(page.bookId, page.bookFormat, - book.makeBookmarkKey(page.currentChapter, page.chapterFraction)) } function gotoChapter(index) { @@ -315,11 +301,6 @@ Page { page.currentChapter = ch page.chapterFraction = fr loadChapter() - - // ripristino anche dal server: la posizione remota vince, se esiste. - // Il login di sessione viene fatto qui (prima richiesta verso le route web) - if (page.bookId > 0) - apiClient.fetchBookmark(page.bookId, page.bookFormat) } onStatusChanged: { @@ -332,33 +313,4 @@ Page { applyMarginsJS() } } - - Connections { - target: apiClient - - // posizione letta dal server all'apertura del libro - onBookmarkReady: { - if (bookId !== page.bookId) - return - if (key.length === 0) - return // nessun segnalibro sul server: resta la posizione locale - var r = book.parseBookmarkKey(key) - if (!r || !r.hasOwnProperty("chapter")) - return - var same = (r.chapter === page.currentChapter) - && (Math.abs(r.fraction - page.chapterFraction) < 0.01) - if (same) - return - page.currentChapter = r.chapter - page.chapterFraction = r.fraction - // aggiorna anche la copia locale - appSettings.setBookmarkKey(page.bookId, page.bookFormat, key) - if (page.ready && book.chapterCount() > 0) - loadChapter() - } - // sync non disponibile (server irraggiungibile, login bloccato, ...): - // si continua in locale, senza disturbare il lettore - onBookmarkError: { } - onLoginFailed: { } - } } diff --git a/src/apiclient.cpp b/src/apiclient.cpp index 77606e9..2aebdc3 100644 --- a/src/apiclient.cpp +++ b/src/apiclient.cpp @@ -4,14 +4,12 @@ #include "settings.h" #include -#include #include #include #include #include #include #include -#include #include #include #include @@ -136,250 +134,6 @@ static bool looksLikeImage(const QByteArray &data) || data.startsWith("GIF8") || data.startsWith("RIFF"); } -// --------------------------------------------------------------------------- -// Login di sessione calibre-web (route web: richiedono flask-login, il Basic -// auth serve solo per OPDS) -// --------------------------------------------------------------------------- - -QString ApiClient::extractCsrfToken(const QByteArray &body) const -{ - // - QRegularExpression re(QStringLiteral( - "name=[\"']csrf_token[\"'][^>]*value=[\"']([^\"']+)[\"']")); - const QRegularExpressionMatch match = re.match(QString::fromUtf8(body)); - return match.hasMatch() ? match.captured(1) : QString(); -} - -void ApiClient::ensureLogin(const std::function &after) -{ - if (m_loggedIn) { - after(); - return; - } - if (m_loginInProgress) { - m_loginQueue.append(after); - return; - } - // accesso anonimo: nessun login possibile, esegui comunque (l'endpoint - // deciderà); oppure cooldown dopo un login fallito (limiter server 3/min) - if (m_settings->username().isEmpty() - || QDateTime::currentMSecsSinceEpoch() < m_loginCooldownUntil) { - after(); - return; - } - - m_loginInProgress = true; - m_loginQueue.append(after); - - // 1. GET /login per il token CSRF - const QUrl loginUrl(m_settings->baseUrl() + QStringLiteral("/login")); - QNetworkRequest request(loginUrl); - request.setRawHeader("User-Agent", "harbour-calibreweb/0.1"); - QNetworkReply *reply = m_nam.get(request); - if (m_settings->ignoreSslErrors()) { - connect(reply, &QNetworkReply::sslErrors, reply, - static_cast &)>( - &QNetworkReply::ignoreSslErrors)); - } - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - const QString token = extractCsrfToken(reply->readAll()); - if (token.isEmpty()) { - qDebug() << "login: token CSRF non trovato"; - finishLogin(false, QStringLiteral( - "Login non disponibile: pagina di login non raggiungibile")); - return; - } - - // 2. POST del form standard calibre-web - QNetworkRequest req(QUrl(m_settings->baseUrl() + QStringLiteral("/login"))); - req.setRawHeader("User-Agent", "harbour-calibreweb/0.1"); - req.setHeader(QNetworkRequest::ContentTypeHeader, - QLatin1String("application/x-www-form-urlencoded")); - applyAuth(req); - const QByteArray body = "username=" + QUrl::toPercentEncoding(m_settings->username()) - + "&password=" + QUrl::toPercentEncoding(m_settings->password()) - + "&csrf_token=" + QUrl::toPercentEncoding(token) - + "&next=/&remember_me=on&submit=Submit"; - QNetworkReply *post = m_nam.post(req, body); - if (m_settings->ignoreSslErrors()) { - connect(post, &QNetworkReply::sslErrors, post, - static_cast &)>( - &QNetworkReply::ignoreSslErrors)); - } - connect(post, &QNetworkReply::finished, this, [this, post]() { - post->deleteLater(); - const int status = post->attribute( - QNetworkRequest::HttpStatusCodeAttribute).toInt(); - const QByteArray respBody = post->readAll(); - // successo: redirect a / (QNAM segue) e il body non contiene il - // form di login; fallimento: la pagina di login viene ri-renderizzata - const bool stillForm = respBody.contains("name=\"password\"") - || respBody.contains("name='password'"); - if (status >= 400 || stillForm) { - qDebug() << "login fallito status:" << status - << "form-ancora-presente=" << stillForm; - finishLogin(false, QStringLiteral( - "Login fallito: credenziali non valide o accesso bloccato")); - } else { - qDebug() << "login OK status:" << status; - finishLogin(true, QString()); - } - }); - }); -} - -void ApiClient::finishLogin(bool ok, const QString &message) -{ - m_loginInProgress = false; - m_loggedIn = ok; - if (!ok) - m_loginCooldownUntil = QDateTime::currentMSecsSinceEpoch() + 60000; - const QList> queue = m_loginQueue; - m_loginQueue.clear(); - if (!ok && !message.isEmpty()) - emit loginFailed(message); - // esegui la coda anche in caso di fallimento: le richieste andranno a - // vuoto in modo rilevato (bookmarkError), senza bloccare il lettore - for (const auto &fn : queue) - fn(); -} - -void ApiClient::postBookmark(int bookId, const QString &format, const QString &key) -{ - ensureLogin([this, bookId, format, key]() { - sendBookmark(bookId, format, key); - }); -} - -void ApiClient::sendBookmark(int bookId, const QString &format, const QString &key) -{ - const QString url = m_settings->baseUrl() + QStringLiteral("/ajax/bookmark/") - + QString::number(bookId) + QLatin1Char('/') + format; - qDebug() << "postBookmark:" << url << "key=" << key; - - const QUrl requestUrl(url); - QNetworkRequest request(requestUrl); - request.setRawHeader("User-Agent", "harbour-calibreweb/0.1"); - request.setHeader(QNetworkRequest::ContentTypeHeader, - QLatin1String("application/x-www-form-urlencoded")); - applyAuth(request); - const QByteArray body = "bookmark=" + QUrl::toPercentEncoding(key); - - QNetworkReply *reply = m_nam.post(request, body); - if (m_settings->ignoreSslErrors()) { - connect(reply, &QNetworkReply::sslErrors, reply, - static_cast &)>( - &QNetworkReply::ignoreSslErrors)); - } - connect(reply, &QNetworkReply::finished, this, [this, reply, url, key, bookId]() { - reply->deleteLater(); - const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - const QByteArray respBody = reply->readAll(); - qDebug() << "postBookmark status:" << status; - // senza sessione il server risponde col form di login (302 seguito) - const bool authFail = respBody.contains("name=\"password\"") - || respBody.contains("name='password'"); - if (authFail) { - qDebug() << "postBookmark: sessione non valida (form di login nella risposta)"; - emit bookmarkError(bookId, QStringLiteral("Sync segnalibro: sessione non valida")); - return; - } - // 400/403 = probabile CSRF attivo: recupera il token dalla pagina di login e riprova - if ((status == 400 || status == 403) && !m_csrfRetried) { - m_csrfRetried = true; - fetchCsrfTokenAndRetry(url, key); - } else if (status >= 400) { - qDebug() << "postBookmark ERROR status:" << status; - emit bookmarkError(bookId, QStringLiteral("Sync segnalibro fallita (%1)").arg(status)); - } - }); -} - -void ApiClient::fetchCsrfTokenAndRetry(const QString &url, const QString &key) -{ - const QUrl loginUrl(m_settings->baseUrl() + QStringLiteral("/login")); - QNetworkRequest request(loginUrl); - request.setRawHeader("User-Agent", "harbour-calibreweb/0.1"); - applyAuth(request); - QNetworkReply *reply = m_nam.get(request); - if (m_settings->ignoreSslErrors()) { - connect(reply, &QNetworkReply::sslErrors, reply, - static_cast &)>( - &QNetworkReply::ignoreSslErrors)); - } - connect(reply, &QNetworkReply::finished, this, [this, reply, url, key]() { - reply->deleteLater(); - const QString csrfToken = extractCsrfToken(reply->readAll()); - if (!csrfToken.isEmpty()) { - postBookmarkWithCsrf(url, key, csrfToken); - } else { - qDebug() << "postBookmark: token CSRF non trovato nella pagina di login"; - } - m_csrfRetried = false; - }); -} - -void ApiClient::postBookmarkWithCsrf(const QString &url, const QString &key, - const QString &csrfToken) -{ - const QUrl requestUrl(url); - QNetworkRequest request(requestUrl); - request.setRawHeader("User-Agent", "harbour-calibreweb/0.1"); - request.setHeader(QNetworkRequest::ContentTypeHeader, - QLatin1String("application/x-www-form-urlencoded")); - request.setRawHeader("X-CSRFToken", csrfToken.toUtf8()); - applyAuth(request); - const QByteArray body = "bookmark=" + QUrl::toPercentEncoding(key); - QNetworkReply *reply = m_nam.post(request, body); - if (m_settings->ignoreSslErrors()) { - connect(reply, &QNetworkReply::sslErrors, reply, - static_cast &)>( - &QNetworkReply::ignoreSslErrors)); - } - connect(reply, &QNetworkReply::finished, this, [reply]() { - reply->deleteLater(); - const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qDebug() << "postBookmark (con CSRF) status:" << status; - }); -} - -void ApiClient::fetchBookmark(int bookId, const QString &format) -{ - if (bookId <= 0) - return; - ensureLogin([this, bookId, format]() { - requestBookmark(bookId, format); - }); -} - -void ApiClient::requestBookmark(int bookId, const QString &format) -{ - const QString url = m_settings->baseUrl() + QStringLiteral("/read/") - + QString::number(bookId) + QLatin1Char('/') + format; - qDebug() << "fetchBookmark:" << url; - - QNetworkReply *reply = startGet(QUrl(url)); - connect(reply, &QNetworkReply::finished, this, [this, reply, bookId, format]() { - reply->deleteLater(); - const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (reply->error() != QNetworkReply::NoError || status >= 400) { - qDebug() << "fetchBookmark ERROR status:" << status - << "err:" << reply->errorString(); - emit bookmarkError(bookId, QStringLiteral( - "Segnalibro server non disponibile (%1)").arg(status)); - return; - } - const QByteArray body = reply->readAll(); - // l'HTML del reader incorpora la posizione: bookmark: "chiave" - QRegularExpression re(QStringLiteral("bookmark:\\s*\"([^\"]*)\"")); - const QRegularExpressionMatch m = re.match(QString::fromUtf8(body)); - const QString key = m.hasMatch() ? m.captured(1).trimmed() : QString(); - qDebug() << "fetchBookmark OK book:" << bookId << "key=[" << key << "]"; - emit bookmarkReady(bookId, format, key); - }); -} - void ApiClient::fetchImage(const QString &url, const QString &id) { if (url.isEmpty()) { diff --git a/src/apiclient.h b/src/apiclient.h index 66a67c1..37ea93c 100644 --- a/src/apiclient.h +++ b/src/apiclient.h @@ -5,8 +5,6 @@ #include #include -#include - class QNetworkReply; class QNetworkRequest; class Settings; @@ -14,9 +12,6 @@ class Settings; // Cliente OPDS: scarica i feed (getFeed), li parsa e li espone come QVariantList // di entry (title, authors, summary, coverUrl, formats, isNavigation, linkUrl). // Gestisce anche il fetch delle copertine in una cache locale su disco. -// Le route web di calibre-web (login, /read, /ajax/bookmark) richiedono una -// sessione flask-login: l'app fa login col form standard (username+password+ -// csrf_token) e riusa il cookie di sessione del QNetworkAccessManager. class ApiClient : public QObject { Q_OBJECT @@ -26,44 +21,20 @@ public: Q_INVOKABLE void getFeed(const QString &url); Q_INVOKABLE void fetchImage(const QString &url, const QString &id); - // sincronizza la posizione di lettura su calibre-web (POST /ajax/bookmark) - Q_INVOKABLE void postBookmark(int bookId, const QString &format, const QString &key); - // legge il segnalibro dal server: GET /read// e estrae - // bookmark: "chiave" dall'HTML del reader - Q_INVOKABLE void fetchBookmark(int bookId, const QString &format); signals: void feedReady(const QVariantList &entries, const QString &nextUrl, const QString &feedTitle); void feedError(const QString &message); void imageReady(const QString &id, const QString &localPath); - // chiave vuota = nessun segnalibro sul server - void bookmarkReady(int bookId, const QString &format, const QString &key); - void bookmarkError(int bookId, const QString &message); - void loginFailed(const QString &message); private: QNetworkReply *startGet(const QUrl &url); void applyAuth(QNetworkRequest &request); QString resolveUrl(const QString &relative) const; - // login di sessione: esegue after() subito se già autenticati, altrimenti - // fa login e poi esegue (coda per le richieste arrivate nel frattempo) - void ensureLogin(const std::function &after); - void finishLogin(bool ok, const QString &message); - QString extractCsrfToken(const QByteArray &body) const; - void sendBookmark(int bookId, const QString &format, const QString &key); - void postBookmarkWithCsrf(const QString &url, const QString &key, - const QString &csrfToken); - void fetchCsrfTokenAndRetry(const QString &url, const QString &key); - void requestBookmark(int bookId, const QString &format); Settings *m_settings; QNetworkAccessManager m_nam; QString m_cacheDir; - bool m_csrfRetried = false; - bool m_loggedIn = false; - bool m_loginInProgress = false; - qint64 m_loginCooldownUntil = 0; // ms epoch: niente retry login per 60 s dopo un fallimento - QList> m_loginQueue; }; #endif // APICLIENT_H