diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index a8ba36ee4..a81155322 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -19,6 +19,7 @@ bittorrent/torrentinfo.h bittorrent/tracker.h bittorrent/trackerentry.h http/connection.h +http/httperror.h http/irequesthandler.h http/requestparser.h http/responsebuilder.h @@ -85,6 +86,7 @@ bittorrent/torrentinfo.cpp bittorrent/tracker.cpp bittorrent/trackerentry.cpp http/connection.cpp +http/httperror.cpp http/requestparser.cpp http/responsebuilder.cpp http/responsegenerator.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 426254794..0afca4c40 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -21,6 +21,7 @@ HEADERS += \ $$PWD/filesystemwatcher.h \ $$PWD/global.h \ $$PWD/http/connection.h \ + $$PWD/http/httperror.h \ $$PWD/http/irequesthandler.h \ $$PWD/http/requestparser.h \ $$PWD/http/responsebuilder.h \ @@ -86,6 +87,7 @@ SOURCES += \ $$PWD/exceptions.cpp \ $$PWD/filesystemwatcher.cpp \ $$PWD/http/connection.cpp \ + $$PWD/http/httperror.cpp \ $$PWD/http/requestparser.cpp \ $$PWD/http/responsebuilder.cpp \ $$PWD/http/responsegenerator.cpp \ diff --git a/src/base/bittorrent/tracker.cpp b/src/base/bittorrent/tracker.cpp index f914aa8aa..6429016e6 100644 --- a/src/base/bittorrent/tracker.cpp +++ b/src/base/bittorrent/tracker.cpp @@ -74,7 +74,7 @@ libtorrent::entry Peer::toEntry(bool noPeerId) const // Tracker Tracker::Tracker(QObject *parent) - : Http::ResponseBuilder(parent) + : QObject(parent) , m_server(new Http::Server(this, this)) { } diff --git a/src/base/bittorrent/tracker.h b/src/base/bittorrent/tracker.h index 967433445..0f03fb5b2 100644 --- a/src/base/bittorrent/tracker.h +++ b/src/base/bittorrent/tracker.h @@ -31,6 +31,7 @@ #define BITTORRENT_TRACKER_H #include +#include #include "base/http/irequesthandler.h" #include "base/http/responsebuilder.h" @@ -75,7 +76,7 @@ namespace BitTorrent /* Basic Bittorrent tracker implementation in Qt */ /* Following http://wiki.theory.org/BitTorrent_Tracker_Protocol */ - class Tracker : public Http::ResponseBuilder, public Http::IRequestHandler + class Tracker : public QObject, public Http::IRequestHandler, private Http::ResponseBuilder { Q_OBJECT Q_DISABLE_COPY(Tracker) diff --git a/src/base/http/httperror.cpp b/src/base/http/httperror.cpp new file mode 100644 index 000000000..d75d4f79a --- /dev/null +++ b/src/base/http/httperror.cpp @@ -0,0 +1,81 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "httperror.h" + +HTTPError::HTTPError(int statusCode, const QString &statusText, const QString &message) + : RuntimeError {message} + , m_statusCode {statusCode} + , m_statusText {statusText} +{ +} + +int HTTPError::statusCode() const +{ + return m_statusCode; +} + +QString HTTPError::statusText() const +{ + return m_statusText; +} + +BadRequestHTTPError::BadRequestHTTPError(const QString &message) + : HTTPError(400, QLatin1String("Bad Request"), message) +{ +} + +ConflictHTTPError::ConflictHTTPError(const QString &message) + : HTTPError(409, QLatin1String("Conflict"), message) +{ +} + +ForbiddenHTTPError::ForbiddenHTTPError(const QString &message) + : HTTPError(403, QLatin1String("Forbidden"), message) +{ +} + +NotFoundHTTPError::NotFoundHTTPError(const QString &message) + : HTTPError(404, QLatin1String("Not Found"), message) +{ +} + +UnsupportedMediaTypeHTTPError::UnsupportedMediaTypeHTTPError(const QString &message) + : HTTPError(415, QLatin1String("Unsupported Media Type"), message) +{ +} + +UnauthorizedHTTPError::UnauthorizedHTTPError(const QString &message) + : HTTPError(401, QLatin1String("Unauthorized"), message) +{ +} + +InternalServerErrorHTTPError::InternalServerErrorHTTPError(const QString &message) + : HTTPError(500, QLatin1String("Internal Server Error"), message) +{ +} diff --git a/src/base/http/httperror.h b/src/base/http/httperror.h new file mode 100644 index 000000000..88fd716d1 --- /dev/null +++ b/src/base/http/httperror.h @@ -0,0 +1,86 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "base/exceptions.h" + +class HTTPError : public RuntimeError +{ +public: + HTTPError(int statusCode, const QString &statusText, const QString &message = ""); + + int statusCode() const; + QString statusText() const; + +private: + const int m_statusCode; + const QString m_statusText; +}; + +class BadRequestHTTPError : public HTTPError +{ +public: + explicit BadRequestHTTPError(const QString &message = ""); +}; + +class ForbiddenHTTPError : public HTTPError +{ +public: + explicit ForbiddenHTTPError(const QString &message = ""); +}; + +class NotFoundHTTPError : public HTTPError +{ +public: + explicit NotFoundHTTPError(const QString &message = ""); +}; + +class ConflictHTTPError : public HTTPError +{ +public: + explicit ConflictHTTPError(const QString &message = ""); +}; + +class UnsupportedMediaTypeHTTPError : public HTTPError +{ +public: + explicit UnsupportedMediaTypeHTTPError(const QString &message = ""); +}; + +class UnauthorizedHTTPError : public HTTPError +{ +public: + explicit UnauthorizedHTTPError(const QString &message = ""); +}; + +class InternalServerErrorHTTPError : public HTTPError +{ +public: + explicit InternalServerErrorHTTPError(const QString &message = ""); +}; diff --git a/src/base/http/responsebuilder.cpp b/src/base/http/responsebuilder.cpp index 954ed05b5..916391a4c 100644 --- a/src/base/http/responsebuilder.cpp +++ b/src/base/http/responsebuilder.cpp @@ -30,11 +30,6 @@ using namespace Http; -ResponseBuilder::ResponseBuilder(QObject *parent) - : QObject(parent) -{ -} - void ResponseBuilder::status(uint code, const QString &text) { m_response.status = ResponseStatus(code, text); diff --git a/src/base/http/responsebuilder.h b/src/base/http/responsebuilder.h index 4332c06be..f1053cc81 100644 --- a/src/base/http/responsebuilder.h +++ b/src/base/http/responsebuilder.h @@ -29,17 +29,13 @@ #ifndef HTTP_RESPONSEBUILDER_H #define HTTP_RESPONSEBUILDER_H -#include #include "types.h" namespace Http { - class ResponseBuilder : public QObject + class ResponseBuilder { public: - explicit ResponseBuilder(QObject *parent = nullptr); - - protected: void status(uint code = 200, const QString &text = QLatin1String("OK")); void header(const QString &name, const QString &value); void print(const QString &text, const QString &type = CONTENT_TYPE_HTML); diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 23f888c3d..03b12e6bb 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -609,6 +609,26 @@ void Preferences::setWebUiHttpsKey(const QByteArray &data) setValue("Preferences/WebUI/HTTPS/Key", data); } +bool Preferences::isAltWebUiEnabled() const +{ + return value("Preferences/WebUI/AlternativeUIEnabled", false).toBool(); +} + +void Preferences::setAltWebUiEnabled(bool enabled) +{ + setValue("Preferences/WebUI/AlternativeUIEnabled", enabled); +} + +QString Preferences::getWebUiRootFolder() const +{ + return value("Preferences/WebUI/RootFolder").toString(); +} + +void Preferences::setWebUiRootFolder(const QString &path) +{ + setValue("Preferences/WebUI/RootFolder", path); +} + bool Preferences::isDynDNSEnabled() const { return value("Preferences/DynDNS/Enabled", false).toBool(); diff --git a/src/base/preferences.h b/src/base/preferences.h index 39e06483a..e01127d7f 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -204,6 +204,10 @@ public: void setWebUiHttpsCertificate(const QByteArray &data); QByteArray getWebUiHttpsKey() const; void setWebUiHttpsKey(const QByteArray &data); + bool isAltWebUiEnabled() const; + void setAltWebUiEnabled(bool enabled); + QString getWebUiRootFolder() const; + void setWebUiRootFolder(const QString &path); // Dynamic DNS bool isDynDNSEnabled() const; diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index 1cdccd087..63a997b49 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -30,6 +30,10 @@ #include "fs.h" +#include +#include +#include + #include #include #include @@ -47,6 +51,7 @@ #include #else #include +#include #endif #include "base/bittorrent/torrenthandle.h" @@ -281,3 +286,17 @@ QString Utils::Fs::tempPath() QDir().mkdir(path); return path; } + +bool Utils::Fs::isRegularFile(const QString &path) +{ + struct ::stat st; + if (::stat(path.toUtf8().constData(), &st) != 0) { + // analyse erno and log the error + const auto err = errno; + qDebug("Could not get file stats for path '%s'. Error: %s" + , qUtf8Printable(path), qUtf8Printable(strerror(err))); + return false; + } + + return (st.st_mode & S_IFMT) == S_IFREG; +} diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 769b66a88..14265fc13 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -56,6 +56,7 @@ namespace Utils bool sameFileNames(const QString &first, const QString &second); QString expandPath(const QString &path); QString expandPathAbs(const QString &path); + bool isRegularFile(const QString &path); bool smartRemoveEmptyFolderTree(const QString &path); bool forceRemove(const QString &filePath); diff --git a/src/base/utils/string.cpp b/src/base/utils/string.cpp index c2a592b9c..65430309b 100644 --- a/src/base/utils/string.cpp +++ b/src/base/utils/string.cpp @@ -40,6 +40,8 @@ #include #endif +#include "../tristatebool.h" + namespace { class NaturalCompare @@ -184,3 +186,19 @@ QString Utils::String::wildcardToRegex(const QString &pattern) { return qt_regexp_toCanonical(pattern, QRegExp::Wildcard); } + +bool Utils::String::parseBool(const QString &string, const bool defaultValue) +{ + if (defaultValue) + return (string.compare("false", Qt::CaseInsensitive) == 0) ? false : true; + return (string.compare("true", Qt::CaseInsensitive) == 0) ? true : false; +} + +TriStateBool Utils::String::parseTriStateBool(const QString &string) +{ + if (string.compare("true", Qt::CaseInsensitive) == 0) + return TriStateBool::True; + if (string.compare("false", Qt::CaseInsensitive) == 0) + return TriStateBool::False; + return TriStateBool::Undefined; +} diff --git a/src/base/utils/string.h b/src/base/utils/string.h index 5a8041955..69eb7c020 100644 --- a/src/base/utils/string.h +++ b/src/base/utils/string.h @@ -34,6 +34,7 @@ class QByteArray; class QLatin1String; +class TriStateBool; namespace Utils { @@ -66,6 +67,9 @@ namespace Utils return str; } + + bool parseBool(const QString &string, const bool defaultValue); + TriStateBool parseTriStateBool(const QString &string); } } diff --git a/src/gui/optionsdlg.cpp b/src/gui/optionsdlg.cpp index a5e0b15a8..004aeaec3 100644 --- a/src/gui/optionsdlg.cpp +++ b/src/gui/optionsdlg.cpp @@ -183,6 +183,9 @@ OptionsDialog::OptionsDialog(QWidget *parent) m_ui->groupFileAssociation->setVisible(false); #endif + m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen); + m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location")); + // Connect signals / slots // Shortcuts for frequently used signals that have more than one overload. They would require // type casts and that is why we declare required member pointer here instead. @@ -367,6 +370,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->domainNameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->DNSUsernameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->DNSPasswordTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); + connect(m_ui->groupAltWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->textWebUIRootFolder, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton); #endif // RSS tab @@ -677,6 +682,9 @@ void OptionsDialog::saveOptions() pref->setDynDomainName(m_ui->domainNameTxt->text()); pref->setDynDNSUsername(m_ui->DNSUsernameTxt->text()); pref->setDynDNSPassword(m_ui->DNSPasswordTxt->text()); + // Alternative UI + pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked()); + pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath()); } // End Web UI // End preferences @@ -1069,6 +1077,9 @@ void OptionsDialog::loadOptions() m_ui->domainNameTxt->setText(pref->getDynDomainName()); m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername()); m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword()); + + m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled()); + m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder()); // End Web UI preferences } diff --git a/src/gui/optionsdlg.ui b/src/gui/optionsdlg.ui index 9118a2fc4..d12393784 100644 --- a/src/gui/optionsdlg.ui +++ b/src/gui/optionsdlg.ui @@ -3014,6 +3014,31 @@ Use ';' to split multiple entries. Can use wildcard '*'. + + + + Use alternative Web UI + + + true + + + false + + + + + + Files location: + + + + + + + + + diff --git a/src/webui/CMakeLists.txt b/src/webui/CMakeLists.txt index b959d5abd..59b34a2ad 100644 --- a/src/webui/CMakeLists.txt +++ b/src/webui/CMakeLists.txt @@ -1,18 +1,31 @@ set(QBT_WEBUI_HEADERS -abstractwebapplication.h -btjson.h +api/apicontroller.h +api/apierror.h +api/appcontroller.h +api/isessionmanager.h +api/authcontroller.h +api/logcontroller.h +api/rsscontroller.h +api/synccontroller.h +api/torrentscontroller.h +api/transfercontroller.h +api/serialize/serialize_torrent.h extra_translations.h -jsonutils.h -prefjson.h webapplication.h -websessiondata.h webui.h ) set(QBT_WEBUI_SOURCES -abstractwebapplication.cpp -btjson.cpp -prefjson.cpp +api/apicontroller.cpp +api/apierror.cpp +api/appcontroller.cpp +api/authcontroller.cpp +api/logcontroller.cpp +api/rsscontroller.cpp +api/synccontroller.cpp +api/torrentscontroller.cpp +api/transfercontroller.cpp +api/serialize/serialize_torrent.cpp webapplication.cpp webui.cpp ) diff --git a/src/webui/abstractwebapplication.cpp b/src/webui/abstractwebapplication.cpp deleted file mode 100644 index 1be2e73ad..000000000 --- a/src/webui/abstractwebapplication.cpp +++ /dev/null @@ -1,525 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev - * - * 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. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -#include "abstractwebapplication.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "base/logger.h" -#include "base/preferences.h" -#include "base/utils/fs.h" -#include "base/utils/net.h" -#include "base/utils/random.h" -#include "base/utils/string.h" -#include "websessiondata.h" - -// UnbanTimer - -class UnbanTimer: public QTimer -{ -public: - UnbanTimer(const QHostAddress& peer_ip, QObject *parent) - : QTimer(parent), m_peerIp(peer_ip) - { - setSingleShot(true); - setInterval(BAN_TIME); - } - - inline const QHostAddress& peerIp() const { return m_peerIp; } - -private: - QHostAddress m_peerIp; -}; - -// WebSession - -struct WebSession -{ - const QString id; - uint timestamp; - WebSessionData data; - - WebSession(const QString& id) - : id(id) - { - updateTimestamp(); - } - - void updateTimestamp() - { - timestamp = QDateTime::currentDateTime().toTime_t(); - } -}; - -namespace -{ - inline QUrl urlFromHostHeader(const QString &hostHeader) - { - if (!hostHeader.contains(QLatin1String("://"))) - return QUrl(QLatin1String("http://") + hostHeader); - return hostHeader; - } -} - -// AbstractWebApplication - -AbstractWebApplication::AbstractWebApplication(QObject *parent) - : Http::ResponseBuilder(parent) - , session_(0) -{ - QTimer *timer = new QTimer(this); - connect(timer, &QTimer::timeout, this, &AbstractWebApplication::removeInactiveSessions); - timer->start(60 * 1000); // 1 min. - - reloadDomainList(); - connect(Preferences::instance(), &Preferences::changed, this, &AbstractWebApplication::reloadDomainList); -} - -AbstractWebApplication::~AbstractWebApplication() -{ - // cleanup sessions data - qDeleteAll(sessions_); -} - -Http::Response AbstractWebApplication::processRequest(const Http::Request &request, const Http::Environment &env) -{ - session_ = 0; - request_ = request; - env_ = env; - - // clear response - clear(); - - // avoid clickjacking attacks - header(Http::HEADER_X_FRAME_OPTIONS, "SAMEORIGIN"); - header(Http::HEADER_X_XSS_PROTECTION, "1; mode=block"); - header(Http::HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff"); - header(Http::HEADER_CONTENT_SECURITY_POLICY, "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none';"); - - // block cross-site requests - if (isCrossSiteRequest(request_) || !validateHostHeader(domainList)) { - status(401, "Unauthorized"); - return response(); - } - - sessionInitialize(); - if (!sessionActive() && !isAuthNeeded()) - sessionStart(); - - if (isBanned()) { - status(403, "Forbidden"); - print(QObject::tr("Your IP address has been banned after too many failed authentication attempts."), Http::CONTENT_TYPE_TXT); - } - else { - doProcessRequest(); - } - - return response(); -} - -void AbstractWebApplication::UnbanTimerEvent() -{ - UnbanTimer* ubantimer = static_cast(sender()); - qDebug("Ban period has expired for %s", qUtf8Printable(ubantimer->peerIp().toString())); - clientFailedAttempts_.remove(ubantimer->peerIp()); - ubantimer->deleteLater(); -} - -void AbstractWebApplication::removeInactiveSessions() -{ - const uint now = QDateTime::currentDateTime().toTime_t(); - - foreach (const QString &id, sessions_.keys()) { - if ((now - sessions_[id]->timestamp) > INACTIVE_TIME) - delete sessions_.take(id); - } -} - -void AbstractWebApplication::reloadDomainList() -{ - domainList = Preferences::instance()->getServerDomains().split(';', QString::SkipEmptyParts); - std::for_each(domainList.begin(), domainList.end(), [](QString &entry){ entry = entry.trimmed(); }); -} - -bool AbstractWebApplication::sessionInitialize() -{ - if (session_ == 0) - { - const QString sessionId = parseCookie(request_).value(C_SID); - - // TODO: Additional session check - - if (!sessionId.isEmpty()) { - if (sessions_.contains(sessionId)) { - session_ = sessions_[sessionId]; - session_->updateTimestamp(); - return true; - } - else { - qDebug() << Q_FUNC_INFO << "session does not exist!"; - } - } - } - - return false; -} - -bool AbstractWebApplication::readFile(const QString& path, QByteArray &data, QString &type) -{ - QString ext = ""; - int index = path.lastIndexOf('.') + 1; - if (index > 0) - ext = path.mid(index); - - // find translated file in cache - if (translatedFiles_.contains(path)) { - data = translatedFiles_[path]; - } - else { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qDebug("File %s was not found!", qUtf8Printable(path)); - return false; - } - - data = file.readAll(); - file.close(); - - // Translate the file - if ((ext == "html") || ((ext == "js") && !path.endsWith("excanvas-compressed.js"))) { - QString dataStr = QString::fromUtf8(data.constData()); - translateDocument(dataStr); - - data = dataStr.toUtf8(); - translatedFiles_[path] = data; // cashing translated file - } - } - - type = CONTENT_TYPE_BY_EXT[ext]; - return true; -} - -WebSessionData *AbstractWebApplication::session() -{ - Q_ASSERT(session_ != 0); - return &session_->data; -} - - -QString AbstractWebApplication::generateSid() -{ - QString sid; - - do { - const size_t size = 6; - quint32 tmp[size]; - - for (size_t i = 0; i < size; ++i) - tmp[i] = Utils::Random::rand(); - - sid = QByteArray::fromRawData(reinterpret_cast(tmp), sizeof(quint32) * size).toBase64(); - } - while (sessions_.contains(sid)); - - return sid; -} - -void AbstractWebApplication::translateDocument(QString& data) -{ - const QRegExp regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR(\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\])"); - const QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?"); - int i = 0; - bool found = true; - - const QString locale = Preferences::instance()->getLocale(); - bool isTranslationNeeded = !locale.startsWith("en") || locale.startsWith("en_AU") || locale.startsWith("en_GB"); - - while(i < data.size() && found) { - i = regex.indexIn(data, i); - if (i >= 0) { - //qDebug("Found translatable string: %s", regex.cap(1).toUtf8().data()); - QByteArray word = regex.cap(1).toUtf8(); - - QString translation = word; - if (isTranslationNeeded) { - QString context = regex.cap(4); - translation = qApp->translate(context.toUtf8().constData(), word.constData(), 0, 1); - } - // Remove keyboard shortcuts - translation.replace(mnemonic, ""); - - // Use HTML code for quotes to prevent issues with JS - translation.replace("'", "'"); - translation.replace("\"", """); - - data.replace(i, regex.matchedLength(), translation); - i += translation.length(); - } - else { - found = false; // no more translatable strings - } - - data.replace(QLatin1String("${LANG}"), locale.left(2)); - data.replace(QLatin1String("${VERSION}"), QBT_VERSION); - } -} - -bool AbstractWebApplication::isBanned() const -{ - return clientFailedAttempts_.value(env_.clientAddress, 0) >= MAX_AUTH_FAILED_ATTEMPTS; -} - -int AbstractWebApplication::failedAttempts() const -{ - return clientFailedAttempts_.value(env_.clientAddress, 0); -} - -void AbstractWebApplication::resetFailedAttempts() -{ - clientFailedAttempts_.remove(env_.clientAddress); -} - -void AbstractWebApplication::increaseFailedAttempts() -{ - const int nb_fail = clientFailedAttempts_.value(env_.clientAddress, 0) + 1; - - clientFailedAttempts_[env_.clientAddress] = nb_fail; - if (nb_fail == MAX_AUTH_FAILED_ATTEMPTS) { - // Max number of failed attempts reached - // Start ban period - UnbanTimer* ubantimer = new UnbanTimer(env_.clientAddress, this); - connect(ubantimer, SIGNAL(timeout()), SLOT(UnbanTimerEvent())); - ubantimer->start(); - } -} - -bool AbstractWebApplication::isAuthNeeded() -{ - qDebug("Checking auth rules against client address %s", qPrintable(env().clientAddress.toString())); - const Preferences *pref = Preferences::instance(); - if (!pref->isWebUiLocalAuthEnabled() && Utils::Net::isLoopbackAddress(env().clientAddress)) - return false; - if (pref->isWebUiAuthSubnetWhitelistEnabled() && Utils::Net::isIPInRange(env().clientAddress, pref->getWebUiAuthSubnetWhitelist())) - return false; - return true; -} - -void AbstractWebApplication::printFile(const QString& path) -{ - QByteArray data; - QString type; - - if (!readFile(path, data, type)) { - status(404, "Not Found"); - return; - } - - print(data, type); -} - -bool AbstractWebApplication::sessionStart() -{ - if (session_ == 0) { - session_ = new WebSession(generateSid()); - sessions_[session_->id] = session_; - - QNetworkCookie cookie(C_SID, session_->id.toUtf8()); - cookie.setHttpOnly(true); - cookie.setPath(QLatin1String("/")); - header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); - - return true; - } - - return false; -} - -bool AbstractWebApplication::sessionEnd() -{ - if ((session_ != 0) && (sessions_.contains(session_->id))) { - QNetworkCookie cookie(C_SID); - cookie.setPath(QLatin1String("/")); - cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1)); - - sessions_.remove(session_->id); - delete session_; - session_ = 0; - - header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); - return true; - } - - return false; -} - -QString AbstractWebApplication::saveTmpFile(const QByteArray &data) -{ - QTemporaryFile tmpfile(Utils::Fs::tempPath() + "XXXXXX.torrent"); - tmpfile.setAutoRemove(false); - if (tmpfile.open()) { - tmpfile.write(data); - tmpfile.close(); - return tmpfile.fileName(); - } - - qWarning() << "I/O Error: Could not create temporary file"; - return QString(); -} - -bool AbstractWebApplication::isCrossSiteRequest(const Http::Request &request) const -{ - // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers - - const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool - { - // [rfc6454] 5. Comparing Origins - return ((left.port() == right.port()) - // && (left.scheme() == right.scheme()) // not present in this context - && (left.host() == right.host())); - }; - - const QString targetOrigin = request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST)); - const QString originValue = request.headers.value(Http::HEADER_ORIGIN); - const QString refererValue = request.headers.value(Http::HEADER_REFERER); - - if (originValue.isEmpty() && refererValue.isEmpty()) { - // owasp.org recommends to block this request, but doing so will inevitably lead Web API users to spoof headers - // so lets be permissive here - return false; - } - - // sent with CORS requests, as well as with POST requests - if (!originValue.isEmpty()) { - const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue); - if (isInvalid) - Logger::instance()->addMessage(tr("WebUI: Origin header & Target origin mismatch!") + "\n" - + tr("Source IP: '%1'. Origin header: '%2'. Target origin: '%3'") - .arg(env_.clientAddress.toString()).arg(originValue).arg(targetOrigin) - , Log::WARNING); - return isInvalid; - } - - if (!refererValue.isEmpty()) { - const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue); - if (isInvalid) - Logger::instance()->addMessage(tr("WebUI: Referer header & Target origin mismatch!") + "\n" - + tr("Source IP: '%1'. Referer header: '%2'. Target origin: '%3'") - .arg(env_.clientAddress.toString()).arg(refererValue).arg(targetOrigin) - , Log::WARNING); - return isInvalid; - } - - return true; -} - -bool AbstractWebApplication::validateHostHeader(const QStringList &domains) const -{ - const QUrl hostHeader = urlFromHostHeader(request().headers[Http::HEADER_HOST]); - const QString requestHost = hostHeader.host(); - - // (if present) try matching host header's port with local port - const int requestPort = hostHeader.port(); - if ((requestPort != -1) && (env().localPort != requestPort)) { - Logger::instance()->addMessage(tr("WebUI: Invalid Host header, port mismatch.") + "\n" - + tr("Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'") - .arg(env().clientAddress.toString()).arg(env().localPort) - .arg(request().headers[Http::HEADER_HOST]) - , Log::WARNING); - return false; - } - - // try matching host header with local address -#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) - const bool sameAddr = env().localAddress.isEqual(QHostAddress(requestHost)); -#else - const auto equal = [](const Q_IPV6ADDR &l, const Q_IPV6ADDR &r) -> bool { - for (int i = 0; i < 16; ++i) { - if (l[i] != r[i]) - return false; - } - return true; - }; - const bool sameAddr = equal(env().localAddress.toIPv6Address(), QHostAddress(requestHost).toIPv6Address()); -#endif - - if (sameAddr) - return true; - - // try matching host header with domain list - for (const auto &domain : domains) { - QRegExp domainRegex(domain, Qt::CaseInsensitive, QRegExp::Wildcard); - if (requestHost.contains(domainRegex)) - return true; - } - - Logger::instance()->addMessage(tr("WebUI: Invalid Host header.") + "\n" - + tr("Request source IP: '%1'. Received Host header: '%2'") - .arg(env().clientAddress.toString()).arg(request().headers[Http::HEADER_HOST]) - , Log::WARNING); - return false; -} - -const QStringMap AbstractWebApplication::CONTENT_TYPE_BY_EXT = { - { "htm", Http::CONTENT_TYPE_HTML }, - { "html", Http::CONTENT_TYPE_HTML }, - { "css", Http::CONTENT_TYPE_CSS }, - { "gif", Http::CONTENT_TYPE_GIF }, - { "png", Http::CONTENT_TYPE_PNG }, - { "js", Http::CONTENT_TYPE_JS }, - { "svg", Http::CONTENT_TYPE_SVG } -}; - -QStringMap AbstractWebApplication::parseCookie(const Http::Request &request) const -{ - // [rfc6265] 4.2.1. Syntax - QStringMap ret; - const QString cookieStr = request.headers.value(QLatin1String("cookie")); - const QVector cookies = cookieStr.splitRef(';', QString::SkipEmptyParts); - - for (const auto &cookie : cookies) { - const int idx = cookie.indexOf('='); - if (idx < 0) - continue; - - const QString name = cookie.left(idx).trimmed().toString(); - const QString value = Utils::String::unquote(cookie.mid(idx + 1).trimmed()) - .toString(); - ret.insert(name, value); - } - return ret; -} diff --git a/src/webui/abstractwebapplication.h b/src/webui/abstractwebapplication.h deleted file mode 100644 index 0cf62562c..000000000 --- a/src/webui/abstractwebapplication.h +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev - * - * 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. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -#ifndef ABSTRACTWEBAPPLICATION_H -#define ABSTRACTWEBAPPLICATION_H - -#include -#include -#include - -#include "base/http/irequesthandler.h" -#include "base/http/responsebuilder.h" -#include "base/http/types.h" - -struct WebSession; -struct WebSessionData; - -const char C_SID[] = "SID"; // name of session id cookie -const int BAN_TIME = 3600000; // 1 hour -const int INACTIVE_TIME = 900; // Session inactive time (in secs = 15 min.) -const int MAX_AUTH_FAILED_ATTEMPTS = 5; - -class AbstractWebApplication : public Http::ResponseBuilder, public Http::IRequestHandler -{ - Q_OBJECT - Q_DISABLE_COPY(AbstractWebApplication) - -public: - explicit AbstractWebApplication(QObject *parent = 0); - virtual ~AbstractWebApplication(); - - Http::Response processRequest(const Http::Request &request, const Http::Environment &env) final; - -protected: - virtual void doProcessRequest() = 0; - - bool isBanned() const; - int failedAttempts() const; - void resetFailedAttempts(); - void increaseFailedAttempts(); - - void printFile(const QString &path); - - // Session management - bool sessionActive() const { return session_ != 0; } - bool sessionStart(); - bool sessionEnd(); - - bool isAuthNeeded(); - - bool readFile(const QString &path, QByteArray &data, QString &type); - - // save data to temporary file on disk and return its name (or empty string if fails) - static QString saveTmpFile(const QByteArray &data); - - WebSessionData *session(); - const Http::Request &request() const { return request_; } - const Http::Environment &env() const { return env_; } - -private slots: - void UnbanTimerEvent(); - void removeInactiveSessions(); - - void reloadDomainList(); - -private: - // Persistent data - QMap sessions_; - QHash clientFailedAttempts_; - QMap translatedFiles_; - - // Current data - WebSession *session_; - Http::Request request_; - Http::Environment env_; - - QStringList domainList; - - QString generateSid(); - bool sessionInitialize(); - - QStringMap parseCookie(const Http::Request &request) const; - bool isCrossSiteRequest(const Http::Request &request) const; - bool validateHostHeader(const QStringList &domains) const; - - static void translateDocument(QString &data); - - static const QStringMap CONTENT_TYPE_BY_EXT; -}; - -#endif // ABSTRACTWEBAPPLICATION_H diff --git a/src/webui/api/apicontroller.cpp b/src/webui/api/apicontroller.cpp new file mode 100644 index 000000000..c306a351b --- /dev/null +++ b/src/webui/api/apicontroller.cpp @@ -0,0 +1,91 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "apicontroller.h" + +#include +#include + +#include "apierror.h" + +APIController::APIController(ISessionManager *sessionManager, QObject *parent) + : QObject {parent} + , m_sessionManager {sessionManager} +{ +} + +QVariant APIController::run(const QString &action, const StringMap ¶ms, const DataMap &data) +{ + m_result.clear(); // clear result + m_params = params; + m_data = data; + + const QString methodName {action + QLatin1String("Action")}; + if (!QMetaObject::invokeMethod(this, methodName.toLatin1().constData())) + throw APIError(APIErrorType::NotFound); + + return m_result; +} + +ISessionManager *APIController::sessionManager() const +{ + return m_sessionManager; +} + +const StringMap &APIController::params() const +{ + return m_params; +} + +const DataMap &APIController::data() const +{ + return m_data; +} + +void APIController::checkParams(const QSet &requiredParams) const +{ + const QSet params {this->params().keys().toSet()}; + + if (!params.contains(requiredParams)) + throw APIError(APIErrorType::BadParams); +} + +void APIController::setResult(const QString &result) +{ + m_result = result; +} + +void APIController::setResult(const QJsonArray &result) +{ + m_result = QJsonDocument(result); +} + +void APIController::setResult(const QJsonObject &result) +{ + m_result = QJsonDocument(result); +} diff --git a/src/webui/api/apicontroller.h b/src/webui/api/apicontroller.h new file mode 100644 index 000000000..419d6c390 --- /dev/null +++ b/src/webui/api/apicontroller.h @@ -0,0 +1,72 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include +#include +#include +#include + +struct ISessionManager; +using StringMap = QMap; +using DataMap = QMap; + +class APIController : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(APIController) + +#ifndef Q_MOC_RUN +#define WEBAPI_PUBLIC +#define WEBAPI_PRIVATE +#endif + +public: + explicit APIController(ISessionManager *sessionManager, QObject *parent = nullptr); + + QVariant run(const QString &action, const StringMap ¶ms, const DataMap &data = {}); + + ISessionManager *sessionManager() const; + +protected: + const StringMap ¶ms() const; + const DataMap &data() const; + void checkParams(const QSet &requiredParams) const; + + void setResult(const QString &result); + void setResult(const QJsonArray &result); + void setResult(const QJsonObject &result); + +private: + ISessionManager *m_sessionManager; + StringMap m_params; + DataMap m_data; + QVariant m_result; +}; diff --git a/src/webui/api/apierror.cpp b/src/webui/api/apierror.cpp new file mode 100644 index 000000000..37afb80f7 --- /dev/null +++ b/src/webui/api/apierror.cpp @@ -0,0 +1,40 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "apierror.h" + +APIError::APIError(APIErrorType type, const QString &message) + : RuntimeError {message} + , m_type {type} +{ +} + +APIErrorType APIError::type() const +{ + return m_type; +} diff --git a/src/webui/jsonutils.h b/src/webui/api/apierror.h similarity index 73% rename from src/webui/jsonutils.h rename to src/webui/api/apierror.h index dcd3327da..2ce43a3ae 100644 --- a/src/webui/jsonutils.h +++ b/src/webui/api/apierror.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2018 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,26 +26,26 @@ * exception statement from your version. */ -#ifndef JSONUTILS_H -#define JSONUTILS_H +#pragma once -#include -#include -#include -#include +#include "base/exceptions.h" -namespace json { +enum class APIErrorType +{ + BadParams, + BadData, + NotFound, + AccessDenied, + Conflict +}; - inline QByteArray toJson(const QVariant& var) - { - return QJsonDocument::fromVariant(var).toJson(QJsonDocument::Compact); - } +class APIError : public RuntimeError +{ +public: + explicit APIError(APIErrorType type, const QString &message = ""); - inline QVariant fromJson(const QString& json) - { - return QJsonDocument::fromJson(json.toUtf8()).toVariant(); - } + APIErrorType type() const; -} - -#endif // JSONUTILS_H +private: + const APIErrorType m_type; +}; diff --git a/src/webui/prefjson.cpp b/src/webui/api/appcontroller.cpp similarity index 90% rename from src/webui/prefjson.cpp rename to src/webui/api/appcontroller.cpp index e1be04572..2b3035c61 100644 --- a/src/webui/prefjson.cpp +++ b/src/webui/api/appcontroller.cpp @@ -1,6 +1,8 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006-2012 Ishan Arora and Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2006-2012 Christophe Dumez + * Copyright (C) 2006-2012 Ishan Arora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,37 +26,58 @@ * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. - * - * Contact : chris@qbittorrent.org */ -#include "prefjson.h" +#include "appcontroller.h" #include +#include +#include +#include +#include +#include +#include +#include + #ifndef QT_NO_OPENSSL #include #include #endif -#include -#include -#include #include "base/bittorrent/session.h" #include "base/net/portforwarder.h" #include "base/net/proxyconfigurationmanager.h" #include "base/preferences.h" +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_session.h" #include "base/scanfoldersmodel.h" #include "base/utils/fs.h" #include "base/utils/net.h" -#include "jsonutils.h" +#include "../webapplication.h" + +void AppController::webapiVersionAction() +{ + setResult(static_cast(API_VERSION)); +} -prefjson::prefjson() +void AppController::versionAction() { + setResult(QBT_VERSION); } -QByteArray prefjson::getPreferences() +void AppController::shutdownAction() { - const Preferences* const pref = Preferences::instance(); + qDebug() << "Shutdown request from Web UI"; + + // Special case handling for shutdown, we + // need to reply to the Web UI before + // actually shutting down. + QTimer::singleShot(100, qApp, &QCoreApplication::quit); +} + +void AppController::preferencesAction() +{ + const Preferences *const pref = Preferences::instance(); auto session = BitTorrent::Session::instance(); QVariantMap data; @@ -187,14 +210,22 @@ QByteArray prefjson::getPreferences() data["dyndns_password"] = pref->getDynDNSPassword(); data["dyndns_domain"] = pref->getDynDomainName(); - return json::toJson(data); + // RSS settings + data["RSSRefreshInterval"] = RSS::Session::instance()->refreshInterval(); + data["RSSMaxArticlesPerFeed"] = RSS::Session::instance()->maxArticlesPerFeed(); + data["RSSProcessingEnabled"] = RSS::Session::instance()->isProcessingEnabled(); + data["RSSAutoDownloadingEnabled"] = RSS::AutoDownloader::instance()->isProcessingEnabled(); + + setResult(QJsonObject::fromVariantMap(data)); } -void prefjson::setPreferences(const QString& json) +void AppController::setPreferencesAction() { - Preferences* const pref = Preferences::instance(); + checkParams({"json"}); + + Preferences *const pref = Preferences::instance(); auto session = BitTorrent::Session::instance(); - const QVariantMap m = json::fromJson(json).toMap(); + const QVariantMap m = QJsonDocument::fromJson(params()["json"].toUtf8()).toVariant().toMap(); // Downloads // Hard Disk @@ -454,4 +485,14 @@ void prefjson::setPreferences(const QString& json) // Save preferences pref->apply(); + + RSS::Session::instance()->setRefreshInterval(m["RSSRefreshInterval"].toUInt()); + RSS::Session::instance()->setMaxArticlesPerFeed(m["RSSMaxArticlesPerFeed"].toInt()); + RSS::Session::instance()->setProcessingEnabled(m["RSSProcessingEnabled"].toBool()); + RSS::AutoDownloader::instance()->setProcessingEnabled(m["RSSAutoDownloadingEnabled"].toBool()); +} + +void AppController::defaultSavePathAction() +{ + setResult(BitTorrent::Session::instance()->defaultSavePath()); } diff --git a/src/webui/api/appcontroller.h b/src/webui/api/appcontroller.h new file mode 100644 index 000000000..e1b240075 --- /dev/null +++ b/src/webui/api/appcontroller.h @@ -0,0 +1,50 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2006-2012 Christophe Dumez + * Copyright (C) 2006-2012 Ishan Arora + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "apicontroller.h" + +class AppController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(AppController) + +public: + using APIController::APIController; + +private slots: + void webapiVersionAction(); + void versionAction(); + void shutdownAction(); + void preferencesAction(); + void setPreferencesAction(); + void defaultSavePathAction(); +}; diff --git a/src/webui/api/authcontroller.cpp b/src/webui/api/authcontroller.cpp new file mode 100644 index 000000000..71d190dc6 --- /dev/null +++ b/src/webui/api/authcontroller.cpp @@ -0,0 +1,108 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "authcontroller.h" + +#include + +#include "base/preferences.h" +#include "base/utils/string.h" +#include "apierror.h" +#include "isessionmanager.h" + +constexpr int BAN_TIME = 3600000; // 1 hour +constexpr int MAX_AUTH_FAILED_ATTEMPTS = 5; + +void AuthController::loginAction() +{ + if (sessionManager()->session()) { + setResult(QLatin1String("Ok.")); + return; + } + + if (isBanned()) + throw APIError(APIErrorType::AccessDenied + , tr("Your IP address has been banned after too many failed authentication attempts.")); + + QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(params()["password"].toLocal8Bit()); + QString pass = md5.result().toHex(); + + const QString username {Preferences::instance()->getWebUiUsername()}; + const QString password {Preferences::instance()->getWebUiPassword()}; + + const bool equalUser = Utils::String::slowEquals(params()["username"].toUtf8(), username.toUtf8()); + const bool equalPass = Utils::String::slowEquals(pass.toUtf8(), password.toUtf8()); + + if (equalUser && equalPass) { + sessionManager()->sessionStart(); + setResult(QLatin1String("Ok.")); + } + else { + QString addr = sessionManager()->clientId(); + increaseFailedAttempts(); + qDebug("client IP: %s (%d failed attempts)", qUtf8Printable(addr), failedAttemptsCount()); + setResult(QLatin1String("Fails.")); + } +} + +void AuthController::logoutAction() +{ + sessionManager()->sessionEnd(); +} + +bool AuthController::isBanned() const +{ + const uint now = QDateTime::currentDateTime().toTime_t(); + const FailedLogin failedLogin = m_clientFailedLogins.value(sessionManager()->clientId()); + + bool isBanned = (failedLogin.bannedAt > 0); + if (isBanned && ((now - failedLogin.bannedAt) > BAN_TIME)) { + m_clientFailedLogins.remove(sessionManager()->clientId()); + isBanned = false; + } + + return isBanned; +} + +int AuthController::failedAttemptsCount() const +{ + return m_clientFailedLogins.value(sessionManager()->clientId()).failedAttemptsCount; +} + +void AuthController::increaseFailedAttempts() +{ + FailedLogin &failedLogin = m_clientFailedLogins[sessionManager()->clientId()]; + ++failedLogin.failedAttemptsCount; + + if (failedLogin.failedAttemptsCount == MAX_AUTH_FAILED_ATTEMPTS) { + // Max number of failed attempts reached + // Start ban period + failedLogin.bannedAt = QDateTime::currentDateTime().toTime_t(); + } +} diff --git a/src/webui/api/authcontroller.h b/src/webui/api/authcontroller.h new file mode 100644 index 000000000..88d8cf861 --- /dev/null +++ b/src/webui/api/authcontroller.h @@ -0,0 +1,59 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +#include "apicontroller.h" + +class AuthController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(AuthController) + +public: + using APIController::APIController; + +private slots: + void loginAction(); + void logoutAction(); + +private: + bool isBanned() const; + int failedAttemptsCount() const; + void increaseFailedAttempts(); + + struct FailedLogin + { + int failedAttemptsCount = 0; + uint bannedAt = 0; + }; + mutable QHash m_clientFailedLogins; +}; diff --git a/src/webui/api/isessionmanager.h b/src/webui/api/isessionmanager.h new file mode 100644 index 000000000..980b9952b --- /dev/null +++ b/src/webui/api/isessionmanager.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +struct ISession +{ + virtual ~ISession() = default; + virtual QString id() const = 0; + virtual QVariant getData(const QString &id) const = 0; + virtual void setData(const QString &id, const QVariant &data) = 0; +}; + +struct ISessionManager +{ + virtual ~ISessionManager() = default; + virtual QString clientId() const = 0; + virtual ISession *session() = 0; + virtual void sessionStart() = 0; + virtual void sessionEnd() = 0; +}; diff --git a/src/webui/api/logcontroller.cpp b/src/webui/api/logcontroller.cpp new file mode 100644 index 000000000..0ca60c5dd --- /dev/null +++ b/src/webui/api/logcontroller.cpp @@ -0,0 +1,124 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "logcontroller.h" + +#include + +#include "base/logger.h" +#include "base/utils/string.h" + +const char KEY_LOG_ID[] = "id"; +const char KEY_LOG_TIMESTAMP[] = "timestamp"; +const char KEY_LOG_MSG_TYPE[] = "type"; +const char KEY_LOG_MSG_MESSAGE[] = "message"; +const char KEY_LOG_PEER_IP[] = "ip"; +const char KEY_LOG_PEER_BLOCKED[] = "blocked"; +const char KEY_LOG_PEER_REASON[] = "reason"; + +// Returns the log in JSON format. +// The return value is an array of dictionaries. +// The dictionary keys are: +// - "id": id of the message +// - "timestamp": milliseconds since epoch +// - "type": type of the message (int, see MsgType) +// - "message": text of the message +// GET params: +// - normal (bool): include normal messages (default true) +// - info (bool): include info messages (default true) +// - warning (bool): include warning messages (default true) +// - critical (bool): include critical messages (default true) +// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) +void LogController::mainAction() +{ + using Utils::String::parseBool; + + const bool isNormal = parseBool(params()["normal"], true); + const bool isInfo = parseBool(params()["info"], true); + const bool isWarning = parseBool(params()["warning"], true); + const bool isCritical = parseBool(params()["critical"], true); + + bool ok = false; + int lastKnownId = params()["last_known_id"].toInt(&ok); + if (!ok) + lastKnownId = -1; + + Logger *const logger = Logger::instance(); + QVariantList msgList; + + foreach (const Log::Msg &msg, logger->getMessages(lastKnownId)) { + if (!((msg.type == Log::NORMAL && isNormal) + || (msg.type == Log::INFO && isInfo) + || (msg.type == Log::WARNING && isWarning) + || (msg.type == Log::CRITICAL && isCritical))) + continue; + QVariantMap map; + map[KEY_LOG_ID] = msg.id; + map[KEY_LOG_TIMESTAMP] = msg.timestamp; + map[KEY_LOG_MSG_TYPE] = msg.type; + map[KEY_LOG_MSG_MESSAGE] = msg.message; + msgList.append(map); + } + + setResult(QJsonArray::fromVariantList(msgList)); +} + +// Returns the peer log in JSON format. +// The return value is an array of dictionaries. +// The dictionary keys are: +// - "id": id of the message +// - "timestamp": milliseconds since epoch +// - "ip": IP of the peer +// - "blocked": whether or not the peer was blocked +// - "reason": reason of the block +// GET params: +// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) +void LogController::peersAction() +{ + int lastKnownId; + bool ok; + + lastKnownId = params()["last_known_id"].toInt(&ok); + if (!ok) + lastKnownId = -1; + + Logger *const logger = Logger::instance(); + QVariantList peerList; + + foreach (const Log::Peer &peer, logger->getPeers(lastKnownId)) { + QVariantMap map; + map[KEY_LOG_ID] = peer.id; + map[KEY_LOG_TIMESTAMP] = peer.timestamp; + map[KEY_LOG_PEER_IP] = peer.ip; + map[KEY_LOG_PEER_BLOCKED] = peer.blocked; + map[KEY_LOG_PEER_REASON] = peer.reason; + peerList.append(map); + } + + setResult(QJsonArray::fromVariantList(peerList)); +} diff --git a/src/webui/prefjson.h b/src/webui/api/logcontroller.h similarity index 79% rename from src/webui/prefjson.h rename to src/webui/api/logcontroller.h index fe64e8dbc..281d2e220 100644 --- a/src/webui/prefjson.h +++ b/src/webui/api/logcontroller.h @@ -1,6 +1,6 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006-2012 Ishan Arora and Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,24 +24,21 @@ * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. - * - * Contact : chris@qbittorrent.org */ -#ifndef PREFJSON_H -#define PREFJSON_H +#pragma once -#include +#include "apicontroller.h" -class prefjson +class LogController : public APIController { -private: - prefjson(); + Q_OBJECT + Q_DISABLE_COPY(LogController) public: - static QByteArray getPreferences(); - static void setPreferences(const QString& json); + using APIController::APIController; +private slots: + void mainAction(); + void peersAction(); }; - -#endif // PREFJSON_H diff --git a/src/webui/api/rsscontroller.cpp b/src/webui/api/rsscontroller.cpp new file mode 100644 index 000000000..5c541f72c --- /dev/null +++ b/src/webui/api/rsscontroller.cpp @@ -0,0 +1,131 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "rsscontroller.h" + +#include +#include +#include + +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_autodownloadrule.h" +#include "base/rss/rss_folder.h" +#include "base/rss/rss_session.h" +#include "base/utils/string.h" +#include "apierror.h" + +using Utils::String::parseBool; + +void RSSController::addFolderAction() +{ + checkParams({"path"}); + + const QString path = params()["path"].trimmed(); + QString error; + if (!RSS::Session::instance()->addFolder(path, &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::addFeedAction() +{ + checkParams({"url", "path"}); + + const QString url = params()["url"].trimmed(); + const QString path = params()["path"].trimmed(); + QString error; + if (!RSS::Session::instance()->addFeed(url, (path.isEmpty() ? url : path), &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::removeItemAction() +{ + checkParams({"path"}); + + const QString path = params()["path"].trimmed(); + QString error; + if (!RSS::Session::instance()->removeItem(path, &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::moveItemAction() +{ + checkParams({"itemPath", "destPath"}); + + const QString itemPath = params()["itemPath"].trimmed(); + const QString destPath = params()["destPath"].trimmed(); + QString error; + if (!RSS::Session::instance()->moveItem(itemPath, destPath, &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::itemsAction() +{ + const bool withData {parseBool(params()["withData"], false)}; + + const auto jsonVal = RSS::Session::instance()->rootFolder()->toJsonValue(withData); + setResult(jsonVal.toObject()); +} + +void RSSController::setRuleAction() +{ + checkParams({"ruleName", "ruleDef"}); + + const QString ruleName {params()["ruleName"].trimmed()}; + const QByteArray ruleDef {params()["ruleDef"].trimmed().toUtf8()}; + + const auto jsonObj = QJsonDocument::fromJson(ruleDef).object(); + RSS::AutoDownloader::instance()->insertRule(RSS::AutoDownloadRule::fromJsonObject(jsonObj, ruleName)); +} + +void RSSController::renameRuleAction() +{ + checkParams({"ruleName", "newRuleName"}); + + const QString ruleName {params()["ruleName"].trimmed()}; + const QString newRuleName {params()["newRuleName"].trimmed()}; + + RSS::AutoDownloader::instance()->renameRule(ruleName, newRuleName); +} + +void RSSController::removeRuleAction() +{ + checkParams({"ruleName"}); + + const QString ruleName {params()["ruleName"].trimmed()}; + RSS::AutoDownloader::instance()->removeRule(ruleName); +} + +void RSSController::rulesAction() +{ + const QList rules {RSS::AutoDownloader::instance()->rules()}; + QJsonObject jsonObj; + for (const auto &rule : rules) + jsonObj.insert(rule.name(), rule.toJsonObject()); + + setResult(jsonObj); +} diff --git a/src/webui/api/rsscontroller.h b/src/webui/api/rsscontroller.h new file mode 100644 index 000000000..4c72a2e09 --- /dev/null +++ b/src/webui/api/rsscontroller.h @@ -0,0 +1,51 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "apicontroller.h" + +class RSSController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(RSSController) + +public: + using APIController::APIController; + +private slots: + void addFolderAction(); + void addFeedAction(); + void removeItemAction(); + void moveItemAction(); + void itemsAction(); + void setRuleAction(); + void renameRuleAction(); + void removeRuleAction(); + void rulesAction(); +}; diff --git a/src/webui/api/serialize/serialize_torrent.cpp b/src/webui/api/serialize/serialize_torrent.cpp new file mode 100644 index 000000000..d28b20a55 --- /dev/null +++ b/src/webui/api/serialize/serialize_torrent.cpp @@ -0,0 +1,140 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "serialize_torrent.h" + +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrenthandle.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" + +namespace +{ + QString torrentStateToString(const BitTorrent::TorrentState state) + { + switch (state) { + case BitTorrent::TorrentState::Error: + return QLatin1String("error"); + case BitTorrent::TorrentState::MissingFiles: + return QLatin1String("missingFiles"); + case BitTorrent::TorrentState::Uploading: + return QLatin1String("uploading"); + case BitTorrent::TorrentState::PausedUploading: + return QLatin1String("pausedUP"); + case BitTorrent::TorrentState::QueuedUploading: + return QLatin1String("queuedUP"); + case BitTorrent::TorrentState::StalledUploading: + return QLatin1String("stalledUP"); + case BitTorrent::TorrentState::CheckingUploading: + return QLatin1String("checkingUP"); + case BitTorrent::TorrentState::ForcedUploading: + return QLatin1String("forcedUP"); + case BitTorrent::TorrentState::Allocating: + return QLatin1String("allocating"); + case BitTorrent::TorrentState::Downloading: + return QLatin1String("downloading"); + case BitTorrent::TorrentState::DownloadingMetadata: + return QLatin1String("metaDL"); + case BitTorrent::TorrentState::PausedDownloading: + return QLatin1String("pausedDL"); + case BitTorrent::TorrentState::QueuedDownloading: + return QLatin1String("queuedDL"); + case BitTorrent::TorrentState::StalledDownloading: + return QLatin1String("stalledDL"); + case BitTorrent::TorrentState::CheckingDownloading: + return QLatin1String("checkingDL"); + case BitTorrent::TorrentState::ForcedDownloading: + return QLatin1String("forcedDL"); +#if LIBTORRENT_VERSION_NUM < 10100 + case BitTorrent::TorrentState::QueuedForChecking: + return QLatin1String("queuedForChecking"); +#endif + case BitTorrent::TorrentState::CheckingResumeData: + return QLatin1String("checkingResumeData"); + default: + return QLatin1String("unknown"); + } + } +} + +QVariantMap serialize(const BitTorrent::TorrentHandle &torrent) +{ + QVariantMap ret; + ret[KEY_TORRENT_HASH] = QString(torrent.hash()); + ret[KEY_TORRENT_NAME] = torrent.name(); + ret[KEY_TORRENT_MAGNET_URI] = torrent.toMagnetUri(); + ret[KEY_TORRENT_SIZE] = torrent.wantedSize(); + ret[KEY_TORRENT_PROGRESS] = torrent.progress(); + ret[KEY_TORRENT_DLSPEED] = torrent.downloadPayloadRate(); + ret[KEY_TORRENT_UPSPEED] = torrent.uploadPayloadRate(); + ret[KEY_TORRENT_PRIORITY] = torrent.queuePosition(); + ret[KEY_TORRENT_SEEDS] = torrent.seedsCount(); + ret[KEY_TORRENT_NUM_COMPLETE] = torrent.totalSeedsCount(); + ret[KEY_TORRENT_LEECHS] = torrent.leechsCount(); + ret[KEY_TORRENT_NUM_INCOMPLETE] = torrent.totalLeechersCount(); + const qreal ratio = torrent.realRatio(); + ret[KEY_TORRENT_RATIO] = (ratio > BitTorrent::TorrentHandle::MAX_RATIO) ? -1 : ratio; + ret[KEY_TORRENT_STATE] = torrentStateToString(torrent.state()); + ret[KEY_TORRENT_ETA] = torrent.eta(); + ret[KEY_TORRENT_SEQUENTIAL_DOWNLOAD] = torrent.isSequentialDownload(); + if (torrent.hasMetadata()) + ret[KEY_TORRENT_FIRST_LAST_PIECE_PRIO] = torrent.hasFirstLastPiecePriority(); + ret[KEY_TORRENT_CATEGORY] = torrent.category(); + ret[KEY_TORRENT_TAGS] = torrent.tags().toList().join(", "); + ret[KEY_TORRENT_SUPER_SEEDING] = torrent.superSeeding(); + ret[KEY_TORRENT_FORCE_START] = torrent.isForced(); + ret[KEY_TORRENT_SAVE_PATH] = Utils::Fs::toNativePath(torrent.savePath()); + ret[KEY_TORRENT_ADDED_ON] = torrent.addedTime().toTime_t(); + ret[KEY_TORRENT_COMPLETION_ON] = torrent.completedTime().toTime_t(); + ret[KEY_TORRENT_TRACKER] = torrent.currentTracker(); + ret[KEY_TORRENT_DL_LIMIT] = torrent.downloadLimit(); + ret[KEY_TORRENT_UP_LIMIT] = torrent.uploadLimit(); + ret[KEY_TORRENT_AMOUNT_DOWNLOADED] = torrent.totalDownload(); + ret[KEY_TORRENT_AMOUNT_UPLOADED] = torrent.totalUpload(); + ret[KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION] = torrent.totalPayloadDownload(); + ret[KEY_TORRENT_AMOUNT_UPLOADED_SESSION] = torrent.totalPayloadUpload(); + ret[KEY_TORRENT_AMOUNT_LEFT] = torrent.incompletedSize(); + ret[KEY_TORRENT_AMOUNT_COMPLETED] = torrent.completedSize(); + ret[KEY_TORRENT_RATIO_LIMIT] = torrent.maxRatio(); + ret[KEY_TORRENT_LAST_SEEN_COMPLETE_TIME] = torrent.lastSeenComplete().toTime_t(); + ret[KEY_TORRENT_AUTO_TORRENT_MANAGEMENT] = torrent.isAutoTMMEnabled(); + ret[KEY_TORRENT_TIME_ACTIVE] = torrent.activeTime(); + + if (torrent.isPaused() || torrent.isChecking()) { + ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = 0; + } + else { + QDateTime dt = QDateTime::currentDateTime(); + dt = dt.addSecs(-torrent.timeSinceActivity()); + ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = dt.toTime_t(); + } + + ret[KEY_TORRENT_TOTAL_SIZE] = torrent.totalSize(); + + return ret; +} diff --git a/src/webui/api/serialize/serialize_torrent.h b/src/webui/api/serialize/serialize_torrent.h new file mode 100644 index 000000000..8f861637a --- /dev/null +++ b/src/webui/api/serialize/serialize_torrent.h @@ -0,0 +1,79 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +namespace BitTorrent +{ + class TorrentHandle; +} + +// Torrent keys +const char KEY_TORRENT_HASH[] = "hash"; +const char KEY_TORRENT_NAME[] = "name"; +const char KEY_TORRENT_MAGNET_URI[] = "magnet_uri"; +const char KEY_TORRENT_SIZE[] = "size"; +const char KEY_TORRENT_PROGRESS[] = "progress"; +const char KEY_TORRENT_DLSPEED[] = "dlspeed"; +const char KEY_TORRENT_UPSPEED[] = "upspeed"; +const char KEY_TORRENT_PRIORITY[] = "priority"; +const char KEY_TORRENT_SEEDS[] = "num_seeds"; +const char KEY_TORRENT_NUM_COMPLETE[] = "num_complete"; +const char KEY_TORRENT_LEECHS[] = "num_leechs"; +const char KEY_TORRENT_NUM_INCOMPLETE[] = "num_incomplete"; +const char KEY_TORRENT_RATIO[] = "ratio"; +const char KEY_TORRENT_ETA[] = "eta"; +const char KEY_TORRENT_STATE[] = "state"; +const char KEY_TORRENT_SEQUENTIAL_DOWNLOAD[] = "seq_dl"; +const char KEY_TORRENT_FIRST_LAST_PIECE_PRIO[] = "f_l_piece_prio"; +const char KEY_TORRENT_CATEGORY[] = "category"; +const char KEY_TORRENT_TAGS[] = "tags"; +const char KEY_TORRENT_SUPER_SEEDING[] = "super_seeding"; +const char KEY_TORRENT_FORCE_START[] = "force_start"; +const char KEY_TORRENT_SAVE_PATH[] = "save_path"; +const char KEY_TORRENT_ADDED_ON[] = "added_on"; +const char KEY_TORRENT_COMPLETION_ON[] = "completion_on"; +const char KEY_TORRENT_TRACKER[] = "tracker"; +const char KEY_TORRENT_DL_LIMIT[] = "dl_limit"; +const char KEY_TORRENT_UP_LIMIT[] = "up_limit"; +const char KEY_TORRENT_AMOUNT_DOWNLOADED[] = "downloaded"; +const char KEY_TORRENT_AMOUNT_UPLOADED[] = "uploaded"; +const char KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION[] = "downloaded_session"; +const char KEY_TORRENT_AMOUNT_UPLOADED_SESSION[] = "uploaded_session"; +const char KEY_TORRENT_AMOUNT_LEFT[] = "amount_left"; +const char KEY_TORRENT_AMOUNT_COMPLETED[] = "completed"; +const char KEY_TORRENT_RATIO_LIMIT[] = "ratio_limit"; +const char KEY_TORRENT_LAST_SEEN_COMPLETE_TIME[] = "seen_complete"; +const char KEY_TORRENT_LAST_ACTIVITY_TIME[] = "last_activity"; +const char KEY_TORRENT_TOTAL_SIZE[] = "total_size"; +const char KEY_TORRENT_AUTO_TORRENT_MANAGEMENT[] = "auto_tmm"; +const char KEY_TORRENT_TIME_ACTIVE[] = "time_active"; + +QVariantMap serialize(const BitTorrent::TorrentHandle &torrent); diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp new file mode 100644 index 000000000..35704c713 --- /dev/null +++ b/src/webui/api/synccontroller.cpp @@ -0,0 +1,473 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "synccontroller.h" + +#include + +#include "base/bittorrent/peerinfo.h" +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrenthandle.h" +#include "base/net/geoipmanager.h" +#include "base/preferences.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" +#include "apierror.h" +#include "isessionmanager.h" +#include "serialize/serialize_torrent.h" + +// Sync main data keys +const char KEY_SYNC_MAINDATA_QUEUEING[] = "queueing"; +const char KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS[] = "use_alt_speed_limits"; +const char KEY_SYNC_MAINDATA_REFRESH_INTERVAL[] = "refresh_interval"; + +// Sync torrent peers keys +const char KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS[] = "show_flags"; + +// Peer keys +const char KEY_PEER_IP[] = "ip"; +const char KEY_PEER_PORT[] = "port"; +const char KEY_PEER_COUNTRY_CODE[] = "country_code"; +const char KEY_PEER_COUNTRY[] = "country"; +const char KEY_PEER_CLIENT[] = "client"; +const char KEY_PEER_PROGRESS[] = "progress"; +const char KEY_PEER_DOWN_SPEED[] = "dl_speed"; +const char KEY_PEER_UP_SPEED[] = "up_speed"; +const char KEY_PEER_TOT_DOWN[] = "downloaded"; +const char KEY_PEER_TOT_UP[] = "uploaded"; +const char KEY_PEER_CONNECTION_TYPE[] = "connection"; +const char KEY_PEER_FLAGS[] = "flags"; +const char KEY_PEER_FLAGS_DESCRIPTION[] = "flags_desc"; +const char KEY_PEER_RELEVANCE[] = "relevance"; +const char KEY_PEER_FILES[] = "files"; + +// TransferInfo keys +const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; +const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; +const char KEY_TRANSFER_DLRATELIMIT[] = "dl_rate_limit"; +const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; +const char KEY_TRANSFER_UPDATA[] = "up_info_data"; +const char KEY_TRANSFER_UPRATELIMIT[] = "up_rate_limit"; +const char KEY_TRANSFER_DHT_NODES[] = "dht_nodes"; +const char KEY_TRANSFER_CONNECTION_STATUS[] = "connection_status"; + +// Statistics keys +const char KEY_TRANSFER_ALLTIME_DL[] = "alltime_dl"; +const char KEY_TRANSFER_ALLTIME_UL[] = "alltime_ul"; +const char KEY_TRANSFER_TOTAL_WASTE_SESSION[] = "total_wasted_session"; +const char KEY_TRANSFER_GLOBAL_RATIO[] = "global_ratio"; +const char KEY_TRANSFER_TOTAL_PEER_CONNECTIONS[] = "total_peer_connections"; +const char KEY_TRANSFER_READ_CACHE_HITS[] = "read_cache_hits"; +const char KEY_TRANSFER_TOTAL_BUFFERS_SIZE[] = "total_buffers_size"; +const char KEY_TRANSFER_WRITE_CACHE_OVERLOAD[] = "write_cache_overload"; +const char KEY_TRANSFER_READ_CACHE_OVERLOAD[] = "read_cache_overload"; +const char KEY_TRANSFER_QUEUED_IO_JOBS[] = "queued_io_jobs"; +const char KEY_TRANSFER_AVERAGE_TIME_QUEUE[] = "average_time_queue"; +const char KEY_TRANSFER_TOTAL_QUEUED_SIZE[] = "total_queued_size"; + +const char KEY_FULL_UPDATE[] = "full_update"; +const char KEY_RESPONSE_ID[] = "rid"; +const char KEY_SUFFIX_REMOVED[] = "_removed"; + +namespace +{ + void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData); + void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems); + void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems); + QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData); + + QVariantMap getTranserInfo() + { + QVariantMap map; + const BitTorrent::SessionStatus &sessionStatus = BitTorrent::Session::instance()->status(); + const BitTorrent::CacheStatus &cacheStatus = BitTorrent::Session::instance()->cacheStatus(); + map[KEY_TRANSFER_DLSPEED] = sessionStatus.payloadDownloadRate; + map[KEY_TRANSFER_DLDATA] = sessionStatus.totalPayloadDownload; + map[KEY_TRANSFER_UPSPEED] = sessionStatus.payloadUploadRate; + map[KEY_TRANSFER_UPDATA] = sessionStatus.totalPayloadUpload; + map[KEY_TRANSFER_DLRATELIMIT] = BitTorrent::Session::instance()->downloadSpeedLimit(); + map[KEY_TRANSFER_UPRATELIMIT] = BitTorrent::Session::instance()->uploadSpeedLimit(); + + quint64 atd = BitTorrent::Session::instance()->getAlltimeDL(); + quint64 atu = BitTorrent::Session::instance()->getAlltimeUL(); + map[KEY_TRANSFER_ALLTIME_DL] = atd; + map[KEY_TRANSFER_ALLTIME_UL] = atu; + map[KEY_TRANSFER_TOTAL_WASTE_SESSION] = sessionStatus.totalWasted; + map[KEY_TRANSFER_GLOBAL_RATIO] = ((atd > 0) && (atu > 0)) ? Utils::String::fromDouble(static_cast(atu) / atd, 2) : "-"; + map[KEY_TRANSFER_TOTAL_PEER_CONNECTIONS] = sessionStatus.peersCount; + + qreal readRatio = cacheStatus.readRatio; + map[KEY_TRANSFER_READ_CACHE_HITS] = (readRatio >= 0) ? Utils::String::fromDouble(100 * readRatio, 2) : "-"; + map[KEY_TRANSFER_TOTAL_BUFFERS_SIZE] = cacheStatus.totalUsedBuffers * 16 * 1024; + + // num_peers is not reliable (adds up peers, which didn't even overcome tcp handshake) + quint32 peers = 0; + foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) + peers += torrent->peersCount(); + map[KEY_TRANSFER_WRITE_CACHE_OVERLOAD] = ((sessionStatus.diskWriteQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskWriteQueue) / peers, 2) : "0"; + map[KEY_TRANSFER_READ_CACHE_OVERLOAD] = ((sessionStatus.diskReadQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskReadQueue) / peers, 2) : "0"; + + map[KEY_TRANSFER_QUEUED_IO_JOBS] = cacheStatus.jobQueueLength; + map[KEY_TRANSFER_AVERAGE_TIME_QUEUE] = cacheStatus.averageJobTime; + map[KEY_TRANSFER_TOTAL_QUEUED_SIZE] = cacheStatus.queuedBytes; + + map[KEY_TRANSFER_DHT_NODES] = sessionStatus.dhtNodes; + if (!BitTorrent::Session::instance()->isListening()) + map[KEY_TRANSFER_CONNECTION_STATUS] = "disconnected"; + else + map[KEY_TRANSFER_CONNECTION_STATUS] = sessionStatus.hasIncomingConnections ? "connected" : "firewalled"; + return map; + } + + // Compare two structures (prevData, data) and calculate difference (syncData). + // Structures encoded as map. + void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData) + { + // initialize output variable + syncData.clear(); + + QVariantList removedItems; + foreach (QString key, data.keys()) { + removedItems.clear(); + + switch (static_cast(data[key].type())) { + case QMetaType::QVariantMap: { + QVariantMap map; + processMap(prevData[key].toMap(), data[key].toMap(), map); + if (!map.isEmpty()) + syncData[key] = map; + } + break; + case QMetaType::QVariantHash: { + QVariantMap map; + processHash(prevData[key].toHash(), data[key].toHash(), map, removedItems); + if (!map.isEmpty()) + syncData[key] = map; + if (!removedItems.isEmpty()) + syncData[key + KEY_SUFFIX_REMOVED] = removedItems; + } + break; + case QMetaType::QVariantList: { + QVariantList list; + processList(prevData[key].toList(), data[key].toList(), list, removedItems); + if (!list.isEmpty()) + syncData[key] = list; + if (!removedItems.isEmpty()) + syncData[key + KEY_SUFFIX_REMOVED] = removedItems; + } + break; + case QMetaType::QString: + case QMetaType::LongLong: + case QMetaType::Float: + case QMetaType::Int: + case QMetaType::Bool: + case QMetaType::Double: + case QMetaType::ULongLong: + case QMetaType::UInt: + case QMetaType::QDateTime: + if (prevData[key] != data[key]) + syncData[key] = data[key]; + break; + default: + Q_ASSERT_X(false, "processMap" + , QString("Unexpected type: %1") + .arg(QMetaType::typeName(static_cast(data[key].type()))) + .toUtf8().constData()); + } + } + } + + // Compare two lists of structures (prevData, data) and calculate difference (syncData, removedItems). + // Structures encoded as map. + // Lists are encoded as hash table (indexed by structure key value) to improve ease of searching for removed items. + void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems) + { + // initialize output variables + syncData.clear(); + removedItems.clear(); + + if (prevData.isEmpty()) { + // If list was empty before, then difference is a whole new list. + foreach (QString key, data.keys()) + syncData[key] = data[key]; + } + else { + foreach (QString key, data.keys()) { + switch (data[key].type()) { + case QVariant::Map: + if (!prevData.contains(key)) { + // new list item found - append it to syncData + syncData[key] = data[key]; + } + else { + QVariantMap map; + processMap(prevData[key].toMap(), data[key].toMap(), map); + // existing list item found - remove it from prevData + prevData.remove(key); + if (!map.isEmpty()) + // changed list item found - append its changes to syncData + syncData[key] = map; + } + break; + default: + Q_ASSERT(0); + } + } + + if (!prevData.isEmpty()) { + // prevData contains only items that are missing now - + // put them in removedItems + foreach (QString s, prevData.keys()) + removedItems << s; + } + } + } + + // Compare two lists of simple value (prevData, data) and calculate difference (syncData, removedItems). + void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems) + { + // initialize output variables + syncData.clear(); + removedItems.clear(); + + if (prevData.isEmpty()) { + // If list was empty before, then difference is a whole new list. + syncData = data; + } + else { + foreach (QVariant item, data) { + if (!prevData.contains(item)) + // new list item found - append it to syncData + syncData.append(item); + else + // unchanged list item found - remove it from prevData + prevData.removeOne(item); + } + + if (!prevData.isEmpty()) + // prevData contains only items that are missing now - + // put them in removedItems + removedItems = prevData; + } + } + + QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData) + { + QVariantMap syncData; + bool fullUpdate = true; + int lastResponseId = 0; + if (acceptedResponseId > 0) { + lastResponseId = lastData[KEY_RESPONSE_ID].toInt(); + + if (lastResponseId == acceptedResponseId) + lastAcceptedData = lastData; + + int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt(); + + if (lastAcceptedResponseId == acceptedResponseId) { + processMap(lastAcceptedData, data, syncData); + fullUpdate = false; + } + } + + if (fullUpdate) { + lastAcceptedData.clear(); + syncData = data; + syncData[KEY_FULL_UPDATE] = true; + } + + lastResponseId = lastResponseId % 1000000 + 1; // cycle between 1 and 1000000 + lastData = data; + lastData[KEY_RESPONSE_ID] = lastResponseId; + syncData[KEY_RESPONSE_ID] = lastResponseId; + + return syncData; + } +} + +// The function returns the changed data from the server to synchronize with the web client. +// Return value is map in JSON format. +// Map contain the key: +// - "Rid": ID response +// Map can contain the keys: +// - "full_update": full data update flag +// - "torrents": dictionary contains information about torrents. +// - "torrents_removed": a list of hashes of removed torrents +// - "categories": list of categories +// - "categories_removed": list of removed categories +// - "server_state": map contains information about the state of the server +// The keys of the 'torrents' dictionary are hashes of torrents. +// Each value of the 'torrents' dictionary contains map. The map can contain following keys: +// - "name": Torrent name +// - "size": Torrent size +// - "progress: Torrent progress +// - "dlspeed": Torrent download speed +// - "upspeed": Torrent upload speed +// - "priority": Torrent priority (-1 if queuing is disabled) +// - "num_seeds": Torrent seeds connected to +// - "num_complete": Torrent seeds in the swarm +// - "num_leechs": Torrent leechers connected to +// - "num_incomplete": Torrent leechers in the swarm +// - "ratio": Torrent share ratio +// - "eta": Torrent ETA +// - "state": Torrent state +// - "seq_dl": Torrent sequential download state +// - "f_l_piece_prio": Torrent first last piece priority state +// - "completion_on": Torrent copletion time +// - "tracker": Torrent tracker +// - "dl_limit": Torrent download limit +// - "up_limit": Torrent upload limit +// - "downloaded": Amount of data downloaded +// - "uploaded": Amount of data uploaded +// - "downloaded_session": Amount of data downloaded since program open +// - "uploaded_session": Amount of data uploaded since program open +// - "amount_left": Amount of data left to download +// - "save_path": Torrent save path +// - "completed": Amount of data completed +// - "ratio_limit": Upload share ratio limit +// - "seen_complete": Indicates the time when the torrent was last seen complete/whole +// - "last_activity": Last time when a chunk was downloaded/uploaded +// - "total_size": Size including unwanted data +// Server state map may contain the following keys: +// - "connection_status": connection status +// - "dht_nodes": DHT nodes count +// - "dl_info_data": bytes downloaded +// - "dl_info_speed": download speed +// - "dl_rate_limit: download rate limit +// - "up_info_data: bytes uploaded +// - "up_info_speed: upload speed +// - "up_rate_limit: upload speed limit +// - "queueing": priority system usage flag +// - "refresh_interval": torrents table refresh interval +// GET param: +// - rid (int): last response id +void SyncController::maindataAction() +{ + auto lastResponse = sessionManager()->session()->getData(QLatin1String("syncMainDataLastResponse")).toMap(); + auto lastAcceptedResponse = sessionManager()->session()->getData(QLatin1String("syncMainDataLastAcceptedResponse")).toMap(); + + QVariantMap data; + QVariantHash torrents; + + BitTorrent::Session *const session = BitTorrent::Session::instance(); + + foreach (BitTorrent::TorrentHandle *const torrent, session->torrents()) { + QVariantMap map = serialize(*torrent); + map.remove(KEY_TORRENT_HASH); + + // Calculated last activity time can differ from actual value by up to 10 seconds (this is a libtorrent issue). + // So we don't need unnecessary updates of last activity time in response. + if (lastResponse.contains("torrents") && lastResponse["torrents"].toHash().contains(torrent->hash()) && + lastResponse["torrents"].toHash()[torrent->hash()].toMap().contains(KEY_TORRENT_LAST_ACTIVITY_TIME)) { + uint lastValue = lastResponse["torrents"].toHash()[torrent->hash()].toMap()[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt(); + if (qAbs(static_cast(lastValue - map[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt())) < 15) + map[KEY_TORRENT_LAST_ACTIVITY_TIME] = lastValue; + } + + torrents[torrent->hash()] = map; + } + + data["torrents"] = torrents; + + QVariantList categories; + foreach (const QString &category, session->categories().keys()) + categories << category; + + data["categories"] = categories; + + QVariantMap serverState = getTranserInfo(); + serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); + serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); + serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); + data["server_state"] = serverState; + + const int acceptedResponseId {params()["rid"].toInt()}; + setResult(QJsonObject::fromVariantMap(generateSyncData(acceptedResponseId, data, lastAcceptedResponse, lastResponse))); + + sessionManager()->session()->setData(QLatin1String("syncMainDataLastResponse"), lastResponse); + sessionManager()->session()->setData(QLatin1String("syncMainDataLastAcceptedResponse"), lastAcceptedResponse); +} + +// GET param: +// - hash (string): torrent hash +// - rid (int): last response id +void SyncController::torrentPeersAction() +{ + auto lastResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastResponse")).toMap(); + auto lastAcceptedResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastAcceptedResponse")).toMap(); + + const QString hash {params()["hash"]}; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QVariantMap data; + QVariantHash peers; + QList peersList = torrent->peers(); +#ifndef DISABLE_COUNTRIES_RESOLUTION + bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries(); +#else + bool resolvePeerCountries = false; +#endif + + data[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS] = resolvePeerCountries; + + foreach (const BitTorrent::PeerInfo &pi, peersList) { + if (pi.address().ip.isNull()) continue; + QVariantMap peer; +#ifndef DISABLE_COUNTRIES_RESOLUTION + if (resolvePeerCountries) { + peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower(); + peer[KEY_PEER_COUNTRY] = Net::GeoIPManager::CountryName(pi.country()); + } +#endif + peer[KEY_PEER_IP] = pi.address().ip.toString(); + peer[KEY_PEER_PORT] = pi.address().port; + peer[KEY_PEER_CLIENT] = pi.client(); + peer[KEY_PEER_PROGRESS] = pi.progress(); + peer[KEY_PEER_DOWN_SPEED] = pi.payloadDownSpeed(); + peer[KEY_PEER_UP_SPEED] = pi.payloadUpSpeed(); + peer[KEY_PEER_TOT_DOWN] = pi.totalDownload(); + peer[KEY_PEER_TOT_UP] = pi.totalUpload(); + peer[KEY_PEER_CONNECTION_TYPE] = pi.connectionType(); + peer[KEY_PEER_FLAGS] = pi.flags(); + peer[KEY_PEER_FLAGS_DESCRIPTION] = pi.flagsDescription(); + peer[KEY_PEER_RELEVANCE] = pi.relevance(); + peer[KEY_PEER_FILES] = torrent->info().filesForPiece(pi.downloadingPieceIndex()).join(QLatin1String("\n")); + + peers[pi.address().ip.toString() + ":" + QString::number(pi.address().port)] = peer; + } + + data["peers"] = peers; + + const int acceptedResponseId {params()["rid"].toInt()}; + setResult(QJsonObject::fromVariantMap(generateSyncData(acceptedResponseId, data, lastAcceptedResponse, lastResponse))); + + sessionManager()->session()->setData(QLatin1String("syncTorrentPeersLastResponse"), lastResponse); + sessionManager()->session()->setData(QLatin1String("syncTorrentPeersLastAcceptedResponse"), lastAcceptedResponse); +} diff --git a/src/webui/websessiondata.h b/src/webui/api/synccontroller.h similarity index 79% rename from src/webui/websessiondata.h rename to src/webui/api/synccontroller.h index 01dd6ab57..0e5e79e45 100644 --- a/src/webui/websessiondata.h +++ b/src/webui/api/synccontroller.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2018 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,18 +26,19 @@ * exception statement from your version. */ -#ifndef WEBSESSIONDATA -#define WEBSESSIONDATA +#pragma once -#include +#include "apicontroller.h" -struct WebSessionData +class SyncController : public APIController { - QVariantMap syncMainDataLastResponse; - QVariantMap syncMainDataLastAcceptedResponse; - QVariantMap syncTorrentPeersLastResponse; - QVariantMap syncTorrentPeersLastAcceptedResponse; -}; + Q_OBJECT + Q_DISABLE_COPY(SyncController) -#endif // WEBSESSIONDATA +public: + using APIController::APIController; +private slots: + void maindataAction(); + void torrentPeersAction(); +}; diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp new file mode 100644 index 000000000..2667f39a7 --- /dev/null +++ b/src/webui/api/torrentscontroller.cpp @@ -0,0 +1,805 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "torrentscontroller.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrenthandle.h" +#include "base/bittorrent/torrentinfo.h" +#include "base/bittorrent/trackerentry.h" +#include "base/logger.h" +#include "base/net/downloadmanager.h" +#include "base/torrentfilter.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" +#include "serialize/serialize_torrent.h" +#include "apierror.h" + +// Tracker keys +const char KEY_TRACKER_URL[] = "url"; +const char KEY_TRACKER_STATUS[] = "status"; +const char KEY_TRACKER_MSG[] = "msg"; +const char KEY_TRACKER_PEERS[] = "num_peers"; + +// Web seed keys +const char KEY_WEBSEED_URL[] = "url"; + +// Torrent keys (Properties) +const char KEY_PROP_TIME_ELAPSED[] = "time_elapsed"; +const char KEY_PROP_SEEDING_TIME[] = "seeding_time"; +const char KEY_PROP_ETA[] = "eta"; +const char KEY_PROP_CONNECT_COUNT[] = "nb_connections"; +const char KEY_PROP_CONNECT_COUNT_LIMIT[] = "nb_connections_limit"; +const char KEY_PROP_DOWNLOADED[] = "total_downloaded"; +const char KEY_PROP_DOWNLOADED_SESSION[] = "total_downloaded_session"; +const char KEY_PROP_UPLOADED[] = "total_uploaded"; +const char KEY_PROP_UPLOADED_SESSION[] = "total_uploaded_session"; +const char KEY_PROP_DL_SPEED[] = "dl_speed"; +const char KEY_PROP_DL_SPEED_AVG[] = "dl_speed_avg"; +const char KEY_PROP_UP_SPEED[] = "up_speed"; +const char KEY_PROP_UP_SPEED_AVG[] = "up_speed_avg"; +const char KEY_PROP_DL_LIMIT[] = "dl_limit"; +const char KEY_PROP_UP_LIMIT[] = "up_limit"; +const char KEY_PROP_WASTED[] = "total_wasted"; +const char KEY_PROP_SEEDS[] = "seeds"; +const char KEY_PROP_SEEDS_TOTAL[] = "seeds_total"; +const char KEY_PROP_PEERS[] = "peers"; +const char KEY_PROP_PEERS_TOTAL[] = "peers_total"; +const char KEY_PROP_RATIO[] = "share_ratio"; +const char KEY_PROP_REANNOUNCE[] = "reannounce"; +const char KEY_PROP_TOTAL_SIZE[] = "total_size"; +const char KEY_PROP_PIECES_NUM[] = "pieces_num"; +const char KEY_PROP_PIECE_SIZE[] = "piece_size"; +const char KEY_PROP_PIECES_HAVE[] = "pieces_have"; +const char KEY_PROP_CREATED_BY[] = "created_by"; +const char KEY_PROP_LAST_SEEN[] = "last_seen"; +const char KEY_PROP_ADDITION_DATE[] = "addition_date"; +const char KEY_PROP_COMPLETION_DATE[] = "completion_date"; +const char KEY_PROP_CREATION_DATE[] = "creation_date"; +const char KEY_PROP_SAVE_PATH[] = "save_path"; +const char KEY_PROP_COMMENT[] = "comment"; + +// File keys +const char KEY_FILE_NAME[] = "name"; +const char KEY_FILE_SIZE[] = "size"; +const char KEY_FILE_PROGRESS[] = "progress"; +const char KEY_FILE_PRIORITY[] = "priority"; +const char KEY_FILE_IS_SEED[] = "is_seed"; +const char KEY_FILE_PIECE_RANGE[] = "piece_range"; +const char KEY_FILE_AVAILABILITY[] = "availability"; + +namespace +{ + using Utils::String::parseBool; + using Utils::String::parseTriStateBool; + + void applyToTorrents(const QStringList &hashes, const std::function &func) + { + if ((hashes.size() == 1) && (hashes[0] == QLatin1String("all"))) { + foreach (BitTorrent::TorrentHandle *torrent, BitTorrent::Session::instance()->torrents()) + func(torrent); + } + else { + for (const QString &hash : hashes) { + BitTorrent::TorrentHandle *torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) + func(torrent); + } + } + } +} + +// Returns all the torrents in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "hash": Torrent hash +// - "name": Torrent name +// - "size": Torrent size +// - "progress: Torrent progress +// - "dlspeed": Torrent download speed +// - "upspeed": Torrent upload speed +// - "priority": Torrent priority (-1 if queuing is disabled) +// - "num_seeds": Torrent seeds connected to +// - "num_complete": Torrent seeds in the swarm +// - "num_leechs": Torrent leechers connected to +// - "num_incomplete": Torrent leechers in the swarm +// - "ratio": Torrent share ratio +// - "eta": Torrent ETA +// - "state": Torrent state +// - "seq_dl": Torrent sequential download state +// - "f_l_piece_prio": Torrent first last piece priority state +// - "force_start": Torrent force start state +// - "category": Torrent category +// GET params: +// - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive +// - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") +// - sort (string): name of column for sorting by its value +// - reverse (bool): enable reverse sorting +// - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) +// - offset (int): set offset (if less than 0 - offset from end) +void TorrentsController::infoAction() +{ + const QString filter {params()["filter"]}; + const QString category {params()["category"]}; + const QString sortedColumn {params()["sort"]}; + const bool reverse {parseBool(params()["reverse"], false)}; + int limit {params()["limit"].toInt()}; + int offset {params()["offset"].toInt()}; + + QVariantList torrentList; + TorrentFilter torrentFilter(filter, TorrentFilter::AnyHash, category); + foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) { + if (torrentFilter.match(torrent)) + torrentList.append(serialize(*torrent)); + } + + std::sort(torrentList.begin(), torrentList.end() + , [sortedColumn, reverse](const QVariant &torrent1, const QVariant &torrent2) + { + return reverse + ? (torrent1.toMap().value(sortedColumn) > torrent2.toMap().value(sortedColumn)) + : (torrent1.toMap().value(sortedColumn) < torrent2.toMap().value(sortedColumn)); + }); + + const int size = torrentList.size(); + // normalize offset + if (offset < 0) + offset = size + offset; + if ((offset >= size) || (offset < 0)) + offset = 0; + // normalize limit + if (limit <= 0) + limit = -1; // unlimited + + if ((limit > 0) || (offset > 0)) + torrentList = torrentList.mid(offset, limit); + + setResult(QJsonArray::fromVariantList(torrentList)); +} + +// Returns the properties for a torrent in JSON format. +// The return value is a JSON-formatted dictionary. +// The dictionary keys are: +// - "time_elapsed": Torrent elapsed time +// - "seeding_time": Torrent elapsed time while complete +// - "eta": Torrent ETA +// - "nb_connections": Torrent connection count +// - "nb_connections_limit": Torrent connection count limit +// - "total_downloaded": Total data uploaded for torrent +// - "total_downloaded_session": Total data downloaded this session +// - "total_uploaded": Total data uploaded for torrent +// - "total_uploaded_session": Total data uploaded this session +// - "dl_speed": Torrent download speed +// - "dl_speed_avg": Torrent average download speed +// - "up_speed": Torrent upload speed +// - "up_speed_avg": Torrent average upload speed +// - "dl_limit": Torrent download limit +// - "up_limit": Torrent upload limit +// - "total_wasted": Total data wasted for torrent +// - "seeds": Torrent connected seeds +// - "seeds_total": Torrent total number of seeds +// - "peers": Torrent connected peers +// - "peers_total": Torrent total number of peers +// - "share_ratio": Torrent share ratio +// - "reannounce": Torrent next reannounce time +// - "total_size": Torrent total size +// - "pieces_num": Torrent pieces count +// - "piece_size": Torrent piece size +// - "pieces_have": Torrent pieces have +// - "created_by": Torrent creator +// - "last_seen": Torrent last seen complete +// - "addition_date": Torrent addition date +// - "completion_date": Torrent completion date +// - "creation_date": Torrent creation date +// - "save_path": Torrent save path +// - "comment": Torrent comment +void TorrentsController::propertiesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantMap dataDict; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime(); + dataDict[KEY_PROP_SEEDING_TIME] = torrent->seedingTime(); + dataDict[KEY_PROP_ETA] = torrent->eta(); + dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount(); + dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit(); + dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload(); + dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); + dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload(); + dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload(); + dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate(); + dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / (1 + torrent->activeTime() - torrent->finishedTime()); + dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate(); + dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / (1 + torrent->activeTime()); + dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit(); + dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit(); + dataDict[KEY_PROP_WASTED] = torrent->wastedSize(); + dataDict[KEY_PROP_SEEDS] = torrent->seedsCount(); + dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount(); + dataDict[KEY_PROP_PEERS] = torrent->leechsCount(); + dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount(); + const qreal ratio = torrent->realRatio(); + dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::TorrentHandle::MAX_RATIO ? -1 : ratio; + dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce(); + dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize(); + dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount(); + dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength(); + dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave(); + dataDict[KEY_PROP_CREATED_BY] = torrent->creator(); + dataDict[KEY_PROP_ADDITION_DATE] = torrent->addedTime().toTime_t(); + if (torrent->hasMetadata()) { + dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? static_cast(torrent->lastSeenComplete().toTime_t()) : -1; + dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? static_cast(torrent->completedTime().toTime_t()) : -1; + dataDict[KEY_PROP_CREATION_DATE] = torrent->creationDate().toTime_t(); + } + else { + dataDict[KEY_PROP_LAST_SEEN] = -1; + dataDict[KEY_PROP_COMPLETION_DATE] = -1; + dataDict[KEY_PROP_CREATION_DATE] = -1; + } + dataDict[KEY_PROP_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); + dataDict[KEY_PROP_COMMENT] = torrent->comment(); + + setResult(QJsonObject::fromVariantMap(dataDict)); +} + +// Returns the trackers for a torrent in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "url": Tracker URL +// - "status": Tracker status +// - "num_peers": Tracker peer count +// - "msg": Tracker message (last) +void TorrentsController::trackersAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList trackerList; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QHash trackersData = torrent->trackerInfos(); + foreach (const BitTorrent::TrackerEntry &tracker, torrent->trackers()) { + QVariantMap trackerDict; + trackerDict[KEY_TRACKER_URL] = tracker.url(); + const BitTorrent::TrackerInfo data = trackersData.value(tracker.url()); + QString status; + switch (tracker.status()) { + case BitTorrent::TrackerEntry::NotContacted: + status = tr("Not contacted yet"); break; + case BitTorrent::TrackerEntry::Updating: + status = tr("Updating..."); break; + case BitTorrent::TrackerEntry::Working: + status = tr("Working"); break; + case BitTorrent::TrackerEntry::NotWorking: + status = tr("Not working"); break; + } + trackerDict[KEY_TRACKER_STATUS] = status; + trackerDict[KEY_TRACKER_PEERS] = data.numPeers; + trackerDict[KEY_TRACKER_MSG] = data.lastMessage.trimmed(); + + trackerList.append(trackerDict); + } + + setResult(QJsonArray::fromVariantList(trackerList)); +} + +// Returns the web seeds for a torrent in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "url": Web seed URL +void TorrentsController::webseedsAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList webSeedList; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + foreach (const QUrl &webseed, torrent->urlSeeds()) { + QVariantMap webSeedDict; + webSeedDict[KEY_WEBSEED_URL] = webseed.toString(); + webSeedList.append(webSeedDict); + } + + setResult(QJsonArray::fromVariantList(webSeedList)); +} + +// Returns the files in a torrent in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "name": File name +// - "size": File size +// - "progress": File progress +// - "priority": File priority +// - "is_seed": Flag indicating if torrent is seeding/complete +// - "piece_range": Piece index range, the first number is the starting piece index +// and the second number is the ending piece index (inclusive) +void TorrentsController::filesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList fileList; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + if (torrent->hasMetadata()) { + const QVector priorities = torrent->filePriorities(); + const QVector fp = torrent->filesProgress(); + const QVector fileAvailability = torrent->availableFileFractions(); + const BitTorrent::TorrentInfo info = torrent->info(); + for (int i = 0; i < torrent->filesCount(); ++i) { + QVariantMap fileDict; + fileDict[KEY_FILE_PROGRESS] = fp[i]; + fileDict[KEY_FILE_PRIORITY] = priorities[i]; + fileDict[KEY_FILE_SIZE] = torrent->fileSize(i); + fileDict[KEY_FILE_AVAILABILITY] = fileAvailability[i]; + + QString fileName = torrent->filePath(i); + if (fileName.endsWith(QB_EXT, Qt::CaseInsensitive)) + fileName.chop(QB_EXT.size()); + fileDict[KEY_FILE_NAME] = Utils::Fs::toNativePath(fileName); + + const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(i); + fileDict[KEY_FILE_PIECE_RANGE] = QVariantList {idx.first(), idx.last()}; + + if (i == 0) + fileDict[KEY_FILE_IS_SEED] = torrent->isSeed(); + + fileList.append(fileDict); + } + } + + setResult(QJsonArray::fromVariantList(fileList)); +} + +// Returns an array of hashes (of each pieces respectively) for a torrent in JSON format. +// The return value is a JSON-formatted array of strings (hex strings). +void TorrentsController::pieceHashesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList pieceHashes; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + const QVector hashes = torrent->info().pieceHashes(); + pieceHashes.reserve(hashes.size()); + foreach (const QByteArray &hash, hashes) + pieceHashes.append(hash.toHex()); + + setResult(QJsonArray::fromVariantList(pieceHashes)); +} + +// Returns an array of states (of each pieces respectively) for a torrent in JSON format. +// The return value is a JSON-formatted array of ints. +// 0: piece not downloaded +// 1: piece requested or downloading +// 2: piece already downloaded +void TorrentsController::pieceStatesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList pieceStates; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + const QBitArray states = torrent->pieces(); + pieceStates.reserve(states.size()); + for (int i = 0; i < states.size(); ++i) + pieceStates.append(static_cast(states[i]) * 2); + + const QBitArray dlstates = torrent->downloadingPieces(); + for (int i = 0; i < states.size(); ++i) { + if (dlstates[i]) + pieceStates[i] = 1; + } + + setResult(QJsonArray::fromVariantList(pieceStates)); +} + +void TorrentsController::addAction() +{ + const QString urls = params()["urls"]; + + const bool skipChecking = parseBool(params()["skip_checking"], false); + const bool seqDownload = parseBool(params()["sequentialDownload"], false); + const bool firstLastPiece = parseBool(params()["firstLastPiecePrio"], false); + const TriStateBool addPaused = parseTriStateBool(params()["paused"]); + const TriStateBool rootFolder = parseTriStateBool(params()["root_folder"]); + const QString savepath = params()["savepath"].trimmed(); + const QString category = params()["category"].trimmed(); + const QString cookie = params()["cookie"]; + const QString torrentName = params()["rename"].trimmed(); + const int upLimit = params()["upLimit"].toInt(); + const int dlLimit = params()["dlLimit"].toInt(); + + QList cookies; + if (!cookie.isEmpty()) { + const QStringList cookiesStr = cookie.split("; "); + for (QString cookieStr : cookiesStr) { + cookieStr = cookieStr.trimmed(); + int index = cookieStr.indexOf('='); + if (index > 1) { + QByteArray name = cookieStr.left(index).toLatin1(); + QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1(); + cookies += QNetworkCookie(name, value); + } + } + } + + BitTorrent::AddTorrentParams params; + // TODO: Check if destination actually exists + params.skipChecking = skipChecking; + params.sequential = seqDownload; + params.firstLastPiecePriority = firstLastPiece; + params.addPaused = addPaused; + params.createSubfolder = rootFolder; + params.savePath = savepath; + params.category = category; + params.name = torrentName; + params.uploadLimit = (upLimit > 0) ? upLimit : -1; + params.downloadLimit = (dlLimit > 0) ? dlLimit : -1; + + bool partialSuccess = false; + for (QString url : urls.split('\n')) { + url = url.trimmed(); + if (!url.isEmpty()) { + Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8())); + partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, params); + } + } + + for (auto it = data().constBegin(); it != data().constEnd(); ++it) { + const BitTorrent::TorrentInfo torrentInfo = BitTorrent::TorrentInfo::load(it.value()); + if (!torrentInfo.isValid()) { + throw APIError(APIErrorType::BadData + , tr("Error: '%1' is not a valid torrent file.").arg(it.key())); + } + + partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentInfo, params); + } + + if (partialSuccess) + setResult("Ok."); + else + setResult("Fails."); +} + +void TorrentsController::addTrackersAction() +{ + checkParams({"hash", "urls"}); + + const QString hash = params()["hash"]; + + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) { + QList trackers; + foreach (QString url, params()["urls"].split('\n')) { + url = url.trimmed(); + if (!url.isEmpty()) + trackers << url; + } + torrent->addTrackers(trackers); + } +} + +void TorrentsController::pauseAction() +{ + checkParams({"hashes"}); + + const QStringList hashes = params()["hashes"].split('|'); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->pause(); }); +} + +void TorrentsController::resumeAction() +{ + checkParams({"hashes"}); + + const QStringList hashes = params()["hashes"].split('|'); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->resume(); }); +} + +void TorrentsController::filePrioAction() +{ + checkParams({"hash", "id", "priority"}); + + const QString hash = params()["hash"]; + int fileID = params()["id"].toInt(); + int priority = params()["priority"].toInt(); + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + + if (torrent && torrent->hasMetadata()) + torrent->setFilePriority(fileID, priority); +} + +void TorrentsController::uploadLimitAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + QVariantMap map; + foreach (const QString &hash, hashes) { + int limit = -1; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) + limit = torrent->uploadLimit(); + map[hash] = limit; + } + + setResult(QJsonObject::fromVariantMap(map)); +} + +void TorrentsController::downloadLimitAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + QVariantMap map; + foreach (const QString &hash, hashes) { + int limit = -1; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) + limit = torrent->downloadLimit(); + map[hash] = limit; + } + + setResult(QJsonObject::fromVariantMap(map)); +} + +void TorrentsController::setUploadLimitAction() +{ + checkParams({"hashes", "limit"}); + + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) + limit = -1; + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *torrent) { torrent->setUploadLimit(limit); }); +} + +void TorrentsController::setDownloadLimitAction() +{ + checkParams({"hashes", "limit"}); + + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) + limit = -1; + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *torrent) { torrent->setDownloadLimit(limit); }); +} + +void TorrentsController::toggleSequentialDownloadAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->toggleSequentialDownload(); }); +} + +void TorrentsController::toggleFirstLastPiecePrioAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->toggleFirstLastPiecePriority(); }); +} + +void TorrentsController::setSuperSeedingAction() +{ + checkParams({"hashes", "value"}); + + const bool value {parseBool(params()["value"], false)}; + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *torrent) { torrent->setSuperSeeding(value); }); +} + +void TorrentsController::setForceStartAction() +{ + checkParams({"hashes", "value"}); + + const bool value {parseBool(params()["value"], false)}; + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *torrent) { torrent->resume(value); }); +} + +void TorrentsController::deleteAction() +{ + checkParams({"hashes", "delete_files"}); + + const QStringList hashes {params()["hashes"].split('|')}; + const bool deleteFiles {parseBool(params()["delete_files"], false)}; + applyToTorrents(hashes, [deleteFiles](BitTorrent::TorrentHandle *torrent) + { + BitTorrent::Session::instance()->deleteTorrent(torrent->hash(), deleteFiles); + }); +} + +void TorrentsController::increasePrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->increaseTorrentsPriority(hashes); +} + +void TorrentsController::decreasePrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->decreaseTorrentsPriority(hashes); +} + +void TorrentsController::topPrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->topTorrentsPriority(hashes); +} + +void TorrentsController::bottomPrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->bottomTorrentsPriority(hashes); +} + +void TorrentsController::setLocationAction() +{ + checkParams({"hashes", "location"}); + + const QStringList hashes {params()["hashes"].split("|")}; + const QString newLocation {params()["location"].trimmed()}; + + // check if the location exists + if (newLocation.isEmpty() || !QDir(newLocation).exists()) + return; + + applyToTorrents(hashes, [newLocation](BitTorrent::TorrentHandle *torrent) + { + LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"") + .arg(torrent->name()).arg(torrent->savePath()).arg(newLocation)); + torrent->move(Utils::Fs::expandPathAbs(newLocation)); + }); +} + +void TorrentsController::renameAction() +{ + checkParams({"hash", "name"}); + + const QString hash = params()["hash"]; + QString name = params()["name"].trimmed(); + + if (name.isEmpty()) + throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name")); + + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + name.replace(QRegularExpression("\r?\n|\r"), " "); + qDebug() << "Renaming" << torrent->name() << "to" << name; + torrent->setName(name); +} + +void TorrentsController::setAutoManagementAction() +{ + checkParams({"hashes", "enable"}); + + const QStringList hashes {params()["hashes"].split('|')}; + const bool isEnabled {parseBool(params()["enable"], false)}; + + applyToTorrents(hashes, [isEnabled](BitTorrent::TorrentHandle *torrent) + { + torrent->setAutoTMMEnabled(isEnabled); + }); +} + +void TorrentsController::recheckAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->forceRecheck(); }); +} + +void TorrentsController::setCategoryAction() +{ + checkParams({"hashes", "category"}); + + const QStringList hashes {params()["hashes"].split('|')}; + const QString category {params()["category"].trimmed()}; + applyToTorrents(hashes, [category](BitTorrent::TorrentHandle *torrent) + { + if (!torrent->setCategory(category)) + throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); + }); +} + +void TorrentsController::createCategoryAction() +{ + checkParams({"category"}); + + const QString category {params()["category"].trimmed()}; + if (!BitTorrent::Session::isValidCategoryName(category) && !category.isEmpty()) + throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); + + BitTorrent::Session::instance()->addCategory(category); +} + +void TorrentsController::removeCategoriesAction() +{ + checkParams({"categories"}); + + const QStringList categories {params()["categories"].split('\n')}; + for (const QString &category : categories) + BitTorrent::Session::instance()->removeCategory(category); +} diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h new file mode 100644 index 000000000..0b1814ef2 --- /dev/null +++ b/src/webui/api/torrentscontroller.h @@ -0,0 +1,74 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "apicontroller.h" + +class TorrentsController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(TorrentsController) + +public: + using APIController::APIController; + +private slots: + void infoAction(); + void propertiesAction(); + void trackersAction(); + void webseedsAction(); + void filesAction(); + void pieceHashesAction(); + void pieceStatesAction(); + void resumeAction(); + void pauseAction(); + void recheckAction(); + void renameAction(); + void setCategoryAction(); + void createCategoryAction(); + void removeCategoriesAction(); + void addAction(); + void deleteAction(); + void addTrackersAction(); + void filePrioAction(); + void uploadLimitAction(); + void downloadLimitAction(); + void setUploadLimitAction(); + void setDownloadLimitAction(); + void increasePrioAction(); + void decreasePrioAction(); + void topPrioAction(); + void bottomPrioAction(); + void setLocationAction(); + void setAutoManagementAction(); + void setSuperSeedingAction(); + void setForceStartAction(); + void toggleSequentialDownloadAction(); + void toggleFirstLastPiecePrioAction(); +}; diff --git a/src/webui/api/transfercontroller.cpp b/src/webui/api/transfercontroller.cpp new file mode 100644 index 000000000..d623ad79b --- /dev/null +++ b/src/webui/api/transfercontroller.cpp @@ -0,0 +1,113 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "transfercontroller.h" + +#include + +#include "base/bittorrent/session.h" + +const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; +const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; +const char KEY_TRANSFER_DLRATELIMIT[] = "dl_rate_limit"; +const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; +const char KEY_TRANSFER_UPDATA[] = "up_info_data"; +const char KEY_TRANSFER_UPRATELIMIT[] = "up_rate_limit"; +const char KEY_TRANSFER_DHT_NODES[] = "dht_nodes"; +const char KEY_TRANSFER_CONNECTION_STATUS[] = "connection_status"; + +// Returns the global transfer information in JSON format. +// The return value is a JSON-formatted dictionary. +// The dictionary keys are: +// - "dl_info_speed": Global download rate +// - "dl_info_data": Data downloaded this session +// - "up_info_speed": Global upload rate +// - "up_info_data": Data uploaded this session +// - "dl_rate_limit": Download rate limit +// - "up_rate_limit": Upload rate limit +// - "dht_nodes": DHT nodes connected to +// - "connection_status": Connection status +void TransferController::infoAction() +{ + const BitTorrent::SessionStatus &sessionStatus = BitTorrent::Session::instance()->status(); + + QJsonObject dict; + + dict[KEY_TRANSFER_DLSPEED] = static_cast(sessionStatus.payloadDownloadRate); + dict[KEY_TRANSFER_DLDATA] = static_cast(sessionStatus.totalPayloadDownload); + dict[KEY_TRANSFER_UPSPEED] = static_cast(sessionStatus.payloadUploadRate); + dict[KEY_TRANSFER_UPDATA] = static_cast(sessionStatus.totalPayloadUpload); + dict[KEY_TRANSFER_DLRATELIMIT] = BitTorrent::Session::instance()->downloadSpeedLimit(); + dict[KEY_TRANSFER_UPRATELIMIT] = BitTorrent::Session::instance()->uploadSpeedLimit(); + dict[KEY_TRANSFER_DHT_NODES] = static_cast(sessionStatus.dhtNodes); + if (!BitTorrent::Session::instance()->isListening()) + dict[KEY_TRANSFER_CONNECTION_STATUS] = QLatin1String("disconnected"); + else + dict[KEY_TRANSFER_CONNECTION_STATUS] = QLatin1String(sessionStatus.hasIncomingConnections ? "connected" : "firewalled"); + + setResult(dict); +} + +void TransferController::uploadLimitAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->uploadSpeedLimit())); +} + +void TransferController::downloadLimitAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->downloadSpeedLimit())); +} + +void TransferController::setUploadLimitAction() +{ + checkParams({"limit"}); + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) limit = -1; + + BitTorrent::Session::instance()->setUploadSpeedLimit(limit); +} + +void TransferController::setDownloadLimitAction() +{ + checkParams({"limit"}); + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) limit = -1; + + BitTorrent::Session::instance()->setDownloadSpeedLimit(limit); +} + +void TransferController::toggleSpeedLimitsModeAction() +{ + BitTorrent::Session *const session = BitTorrent::Session::instance(); + session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled()); +} + +void TransferController::speedLimitsModeAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled())); +} diff --git a/src/webui/api/transfercontroller.h b/src/webui/api/transfercontroller.h new file mode 100644 index 000000000..c6ffe713e --- /dev/null +++ b/src/webui/api/transfercontroller.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "apicontroller.h" + +class TransferController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(TransferController) + +public: + using APIController::APIController; + +private slots: + void infoAction(); + void speedLimitsModeAction(); + void toggleSpeedLimitsModeAction(); + void uploadLimitAction(); + void downloadLimitAction(); + void setUploadLimitAction(); + void setDownloadLimitAction(); +}; diff --git a/src/webui/btjson.cpp b/src/webui/btjson.cpp deleted file mode 100644 index c6beb383e..000000000 --- a/src/webui/btjson.cpp +++ /dev/null @@ -1,1134 +0,0 @@ -/* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2012, Christophe Dumez - * - * 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. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * Contact : chris@qbittorrent.org - */ - -#include "btjson.h" - -#include -#include -#include - -#include - -#include "base/bittorrent/cachestatus.h" -#include "base/bittorrent/session.h" -#include "base/bittorrent/sessionstatus.h" -#include "base/bittorrent/peerinfo.h" -#include "base/bittorrent/torrenthandle.h" -#include "base/bittorrent/trackerentry.h" -#include "base/logger.h" -#include "base/net/geoipmanager.h" -#include "base/preferences.h" -#include "base/torrentfilter.h" -#include "base/utils/fs.h" -#include "base/utils/misc.h" -#include "base/utils/string.h" -#include "jsonutils.h" - -#define CACHED_VARIABLE(VARTYPE, VAR, DUR) \ - static VARTYPE VAR; \ - static QElapsedTimer cacheTimer; \ - static bool initialized = false; \ - if (initialized && !cacheTimer.hasExpired(DUR)) \ - return json::toJson(VAR); \ - initialized = true; \ - cacheTimer.start(); \ - VAR = VARTYPE() - -#define CACHED_VARIABLE_FOR_HASH(VARTYPE, VAR, DUR, HASH) \ - static VARTYPE VAR; \ - static QString prev_hash; \ - static QElapsedTimer cacheTimer; \ - if (prev_hash == HASH && !cacheTimer.hasExpired(DUR)) \ - return json::toJson(VAR); \ - prev_hash = HASH; \ - cacheTimer.start(); \ - VAR = VARTYPE() - - -// Numerical constants -static const int CACHE_DURATION_MS = 1500; // 1500ms - -// Torrent keys -static const char KEY_TORRENT_HASH[] = "hash"; -static const char KEY_TORRENT_NAME[] = "name"; -static const char KEY_TORRENT_MAGNET_URI[] = "magnet_uri"; -static const char KEY_TORRENT_SIZE[] = "size"; -static const char KEY_TORRENT_PROGRESS[] = "progress"; -static const char KEY_TORRENT_DLSPEED[] = "dlspeed"; -static const char KEY_TORRENT_UPSPEED[] = "upspeed"; -static const char KEY_TORRENT_PRIORITY[] = "priority"; -static const char KEY_TORRENT_SEEDS[] = "num_seeds"; -static const char KEY_TORRENT_NUM_COMPLETE[] = "num_complete"; -static const char KEY_TORRENT_LEECHS[] = "num_leechs"; -static const char KEY_TORRENT_NUM_INCOMPLETE[] = "num_incomplete"; -static const char KEY_TORRENT_RATIO[] = "ratio"; -static const char KEY_TORRENT_ETA[] = "eta"; -static const char KEY_TORRENT_STATE[] = "state"; -static const char KEY_TORRENT_SEQUENTIAL_DOWNLOAD[] = "seq_dl"; -static const char KEY_TORRENT_FIRST_LAST_PIECE_PRIO[] = "f_l_piece_prio"; -static const char KEY_TORRENT_CATEGORY[] = "category"; -static const char KEY_TORRENT_TAGS[] = "tags"; -static const char KEY_TORRENT_SUPER_SEEDING[] = "super_seeding"; -static const char KEY_TORRENT_FORCE_START[] = "force_start"; -static const char KEY_TORRENT_SAVE_PATH[] = "save_path"; -static const char KEY_TORRENT_ADDED_ON[] = "added_on"; -static const char KEY_TORRENT_COMPLETION_ON[] = "completion_on"; -static const char KEY_TORRENT_TRACKER[] = "tracker"; -static const char KEY_TORRENT_DL_LIMIT[] = "dl_limit"; -static const char KEY_TORRENT_UP_LIMIT[] = "up_limit"; -static const char KEY_TORRENT_AMOUNT_DOWNLOADED[] = "downloaded"; -static const char KEY_TORRENT_AMOUNT_UPLOADED[] = "uploaded"; -static const char KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION[] = "downloaded_session"; -static const char KEY_TORRENT_AMOUNT_UPLOADED_SESSION[] = "uploaded_session"; -static const char KEY_TORRENT_AMOUNT_LEFT[] = "amount_left"; -static const char KEY_TORRENT_AMOUNT_COMPLETED[] = "completed"; -static const char KEY_TORRENT_RATIO_LIMIT[] = "ratio_limit"; -static const char KEY_TORRENT_LAST_SEEN_COMPLETE_TIME[] = "seen_complete"; -static const char KEY_TORRENT_LAST_ACTIVITY_TIME[] = "last_activity"; -static const char KEY_TORRENT_TOTAL_SIZE[] = "total_size"; -static const char KEY_TORRENT_AUTO_TORRENT_MANAGEMENT[] = "auto_tmm"; -static const char KEY_TORRENT_TIME_ACTIVE[] = "time_active"; - -// Peer keys -static const char KEY_PEER_IP[] = "ip"; -static const char KEY_PEER_PORT[] = "port"; -static const char KEY_PEER_COUNTRY_CODE[] = "country_code"; -static const char KEY_PEER_COUNTRY[] = "country"; -static const char KEY_PEER_CLIENT[] = "client"; -static const char KEY_PEER_PROGRESS[] = "progress"; -static const char KEY_PEER_DOWN_SPEED[] = "dl_speed"; -static const char KEY_PEER_UP_SPEED[] = "up_speed"; -static const char KEY_PEER_TOT_DOWN[] = "downloaded"; -static const char KEY_PEER_TOT_UP[] = "uploaded"; -static const char KEY_PEER_CONNECTION_TYPE[] = "connection"; -static const char KEY_PEER_FLAGS[] = "flags"; -static const char KEY_PEER_FLAGS_DESCRIPTION[] = "flags_desc"; -static const char KEY_PEER_RELEVANCE[] = "relevance"; -static const char KEY_PEER_FILES[] = "files"; - -// Tracker keys -static const char KEY_TRACKER_URL[] = "url"; -static const char KEY_TRACKER_STATUS[] = "status"; -static const char KEY_TRACKER_MSG[] = "msg"; -static const char KEY_TRACKER_PEERS[] = "num_peers"; - -// Web seed keys -static const char KEY_WEBSEED_URL[] = "url"; - -// Torrent keys (Properties) -static const char KEY_PROP_TIME_ELAPSED[] = "time_elapsed"; -static const char KEY_PROP_SEEDING_TIME[] = "seeding_time"; -static const char KEY_PROP_ETA[] = "eta"; -static const char KEY_PROP_CONNECT_COUNT[] = "nb_connections"; -static const char KEY_PROP_CONNECT_COUNT_LIMIT[] = "nb_connections_limit"; -static const char KEY_PROP_DOWNLOADED[] = "total_downloaded"; -static const char KEY_PROP_DOWNLOADED_SESSION[] = "total_downloaded_session"; -static const char KEY_PROP_UPLOADED[] = "total_uploaded"; -static const char KEY_PROP_UPLOADED_SESSION[] = "total_uploaded_session"; -static const char KEY_PROP_DL_SPEED[] = "dl_speed"; -static const char KEY_PROP_DL_SPEED_AVG[] = "dl_speed_avg"; -static const char KEY_PROP_UP_SPEED[] = "up_speed"; -static const char KEY_PROP_UP_SPEED_AVG[] = "up_speed_avg"; -static const char KEY_PROP_DL_LIMIT[] = "dl_limit"; -static const char KEY_PROP_UP_LIMIT[] = "up_limit"; -static const char KEY_PROP_WASTED[] = "total_wasted"; -static const char KEY_PROP_SEEDS[] = "seeds"; -static const char KEY_PROP_SEEDS_TOTAL[] = "seeds_total"; -static const char KEY_PROP_PEERS[] = "peers"; -static const char KEY_PROP_PEERS_TOTAL[] = "peers_total"; -static const char KEY_PROP_RATIO[] = "share_ratio"; -static const char KEY_PROP_REANNOUNCE[] = "reannounce"; -static const char KEY_PROP_TOTAL_SIZE[] = "total_size"; -static const char KEY_PROP_PIECES_NUM[] = "pieces_num"; -static const char KEY_PROP_PIECE_SIZE[] = "piece_size"; -static const char KEY_PROP_PIECES_HAVE[] = "pieces_have"; -static const char KEY_PROP_CREATED_BY[] = "created_by"; -static const char KEY_PROP_LAST_SEEN[] = "last_seen"; -static const char KEY_PROP_ADDITION_DATE[] = "addition_date"; -static const char KEY_PROP_COMPLETION_DATE[] = "completion_date"; -static const char KEY_PROP_CREATION_DATE[] = "creation_date"; -static const char KEY_PROP_SAVE_PATH[] = "save_path"; -static const char KEY_PROP_COMMENT[] = "comment"; - -// File keys -static const char KEY_FILE_NAME[] = "name"; -static const char KEY_FILE_SIZE[] = "size"; -static const char KEY_FILE_PROGRESS[] = "progress"; -static const char KEY_FILE_PRIORITY[] = "priority"; -static const char KEY_FILE_IS_SEED[] = "is_seed"; -static const char KEY_FILE_PIECE_RANGE[] = "piece_range"; -static const char KEY_FILE_AVAILABILITY[] = "availability"; - -// TransferInfo keys -static const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; -static const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; -static const char KEY_TRANSFER_DLRATELIMIT[] = "dl_rate_limit"; -static const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; -static const char KEY_TRANSFER_UPDATA[] = "up_info_data"; -static const char KEY_TRANSFER_UPRATELIMIT[] = "up_rate_limit"; -static const char KEY_TRANSFER_DHT_NODES[] = "dht_nodes"; -static const char KEY_TRANSFER_CONNECTION_STATUS[] = "connection_status"; - -// Statistics keys -static const char KEY_TRANSFER_ALLTIME_DL[] = "alltime_dl"; -static const char KEY_TRANSFER_ALLTIME_UL[] = "alltime_ul"; -static const char KEY_TRANSFER_TOTAL_WASTE_SESSION[] = "total_wasted_session"; -static const char KEY_TRANSFER_GLOBAL_RATIO[] = "global_ratio"; -static const char KEY_TRANSFER_TOTAL_PEER_CONNECTIONS[] = "total_peer_connections"; -static const char KEY_TRANSFER_READ_CACHE_HITS[] = "read_cache_hits"; -static const char KEY_TRANSFER_TOTAL_BUFFERS_SIZE[] = "total_buffers_size"; -static const char KEY_TRANSFER_WRITE_CACHE_OVERLOAD[] = "write_cache_overload"; -static const char KEY_TRANSFER_READ_CACHE_OVERLOAD[] = "read_cache_overload"; -static const char KEY_TRANSFER_QUEUED_IO_JOBS[] = "queued_io_jobs"; -static const char KEY_TRANSFER_AVERAGE_TIME_QUEUE[] = "average_time_queue"; -static const char KEY_TRANSFER_TOTAL_QUEUED_SIZE[] = "total_queued_size"; - -// Sync main data keys -static const char KEY_SYNC_MAINDATA_QUEUEING[] = "queueing"; -static const char KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS[] = "use_alt_speed_limits"; -static const char KEY_SYNC_MAINDATA_REFRESH_INTERVAL[] = "refresh_interval"; - -// Sync torrent peers keys -static const char KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS[] = "show_flags"; - -static const char KEY_FULL_UPDATE[] = "full_update"; -static const char KEY_RESPONSE_ID[] = "rid"; -static const char KEY_SUFFIX_REMOVED[] = "_removed"; - -// Log keys -static const char KEY_LOG_ID[] = "id"; -static const char KEY_LOG_TIMESTAMP[] = "timestamp"; -static const char KEY_LOG_MSG_TYPE[] = "type"; -static const char KEY_LOG_MSG_MESSAGE[] = "message"; -static const char KEY_LOG_PEER_IP[] = "ip"; -static const char KEY_LOG_PEER_BLOCKED[] = "blocked"; -static const char KEY_LOG_PEER_REASON[] = "reason"; - -namespace -{ - QString torrentStateToString(const BitTorrent::TorrentState state) - { - switch (state) { - case BitTorrent::TorrentState::Error: - return QLatin1String("error"); - case BitTorrent::TorrentState::MissingFiles: - return QLatin1String("missingFiles"); - case BitTorrent::TorrentState::Uploading: - return QLatin1String("uploading"); - case BitTorrent::TorrentState::PausedUploading: - return QLatin1String("pausedUP"); - case BitTorrent::TorrentState::QueuedUploading: - return QLatin1String("queuedUP"); - case BitTorrent::TorrentState::StalledUploading: - return QLatin1String("stalledUP"); - case BitTorrent::TorrentState::CheckingUploading: - return QLatin1String("checkingUP"); - case BitTorrent::TorrentState::ForcedUploading: - return QLatin1String("forcedUP"); - case BitTorrent::TorrentState::Allocating: - return QLatin1String("allocating"); - case BitTorrent::TorrentState::Downloading: - return QLatin1String("downloading"); - case BitTorrent::TorrentState::DownloadingMetadata: - return QLatin1String("metaDL"); - case BitTorrent::TorrentState::PausedDownloading: - return QLatin1String("pausedDL"); - case BitTorrent::TorrentState::QueuedDownloading: - return QLatin1String("queuedDL"); - case BitTorrent::TorrentState::StalledDownloading: - return QLatin1String("stalledDL"); - case BitTorrent::TorrentState::CheckingDownloading: - return QLatin1String("checkingDL"); - case BitTorrent::TorrentState::ForcedDownloading: - return QLatin1String("forcedDL"); -#if LIBTORRENT_VERSION_NUM < 10100 - case BitTorrent::TorrentState::QueuedForChecking: - return QLatin1String("queuedForChecking"); -#endif - case BitTorrent::TorrentState::CheckingResumeData: - return QLatin1String("checkingResumeData"); - default: - return QLatin1String("unknown"); - } - } - - class QTorrentCompare - { - public: - QTorrentCompare(const QString &key, bool greaterThan = false) - : m_key(key) - , m_greaterThan(greaterThan) - { - } - - bool operator()(const QVariant &torrent1, const QVariant &torrent2) - { - return m_greaterThan - ? (torrent1.toMap().value(m_key) > torrent2.toMap().value(m_key)) - : (torrent1.toMap().value(m_key) < torrent2.toMap().value(m_key)); - } - - private: - const QString m_key; - const bool m_greaterThan; - }; - - QVariantMap getTranserInfoMap(); - QVariantMap toMap(BitTorrent::TorrentHandle *const torrent); - void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData); - void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems); - void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems); - QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData); - - QVariantMap getTranserInfoMap() - { - QVariantMap map; - const BitTorrent::SessionStatus &sessionStatus = BitTorrent::Session::instance()->status(); - const BitTorrent::CacheStatus &cacheStatus = BitTorrent::Session::instance()->cacheStatus(); - map[KEY_TRANSFER_DLSPEED] = sessionStatus.payloadDownloadRate; - map[KEY_TRANSFER_DLDATA] = sessionStatus.totalPayloadDownload; - map[KEY_TRANSFER_UPSPEED] = sessionStatus.payloadUploadRate; - map[KEY_TRANSFER_UPDATA] = sessionStatus.totalPayloadUpload; - map[KEY_TRANSFER_DLRATELIMIT] = BitTorrent::Session::instance()->downloadSpeedLimit(); - map[KEY_TRANSFER_UPRATELIMIT] = BitTorrent::Session::instance()->uploadSpeedLimit(); - - quint64 atd = BitTorrent::Session::instance()->getAlltimeDL(); - quint64 atu = BitTorrent::Session::instance()->getAlltimeUL(); - map[KEY_TRANSFER_ALLTIME_DL] = atd; - map[KEY_TRANSFER_ALLTIME_UL] = atu; - map[KEY_TRANSFER_TOTAL_WASTE_SESSION] = sessionStatus.totalWasted; - map[KEY_TRANSFER_GLOBAL_RATIO] = ( atd > 0 && atu > 0 ) ? Utils::String::fromDouble(static_cast(atu) / atd, 2) : "-"; - map[KEY_TRANSFER_TOTAL_PEER_CONNECTIONS] = sessionStatus.peersCount; - - qreal readRatio = cacheStatus.readRatio; - map[KEY_TRANSFER_READ_CACHE_HITS] = (readRatio >= 0) ? Utils::String::fromDouble(100 * readRatio, 2) : "-"; - map[KEY_TRANSFER_TOTAL_BUFFERS_SIZE] = cacheStatus.totalUsedBuffers * 16 * 1024; - - // num_peers is not reliable (adds up peers, which didn't even overcome tcp handshake) - quint32 peers = 0; - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - peers += torrent->peersCount(); - map[KEY_TRANSFER_WRITE_CACHE_OVERLOAD] = ((sessionStatus.diskWriteQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskWriteQueue) / peers, 2) : "0"; - map[KEY_TRANSFER_READ_CACHE_OVERLOAD] = ((sessionStatus.diskReadQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskReadQueue) / peers, 2) : "0"; - - map[KEY_TRANSFER_QUEUED_IO_JOBS] = cacheStatus.jobQueueLength; - map[KEY_TRANSFER_AVERAGE_TIME_QUEUE] = cacheStatus.averageJobTime; - map[KEY_TRANSFER_TOTAL_QUEUED_SIZE] = cacheStatus.queuedBytes; - - map[KEY_TRANSFER_DHT_NODES] = sessionStatus.dhtNodes; - if (!BitTorrent::Session::instance()->isListening()) - map[KEY_TRANSFER_CONNECTION_STATUS] = "disconnected"; - else - map[KEY_TRANSFER_CONNECTION_STATUS] = sessionStatus.hasIncomingConnections ? "connected" : "firewalled"; - return map; - } - - QVariantMap toMap(BitTorrent::TorrentHandle *const torrent) - { - QVariantMap ret; - ret[KEY_TORRENT_HASH] = QString(torrent->hash()); - ret[KEY_TORRENT_NAME] = torrent->name(); - ret[KEY_TORRENT_MAGNET_URI] = torrent->toMagnetUri(); - ret[KEY_TORRENT_SIZE] = torrent->wantedSize(); - ret[KEY_TORRENT_PROGRESS] = torrent->progress(); - ret[KEY_TORRENT_DLSPEED] = torrent->downloadPayloadRate(); - ret[KEY_TORRENT_UPSPEED] = torrent->uploadPayloadRate(); - ret[KEY_TORRENT_PRIORITY] = torrent->queuePosition(); - ret[KEY_TORRENT_SEEDS] = torrent->seedsCount(); - ret[KEY_TORRENT_NUM_COMPLETE] = torrent->totalSeedsCount(); - ret[KEY_TORRENT_LEECHS] = torrent->leechsCount(); - ret[KEY_TORRENT_NUM_INCOMPLETE] = torrent->totalLeechersCount(); - const qreal ratio = torrent->realRatio(); - ret[KEY_TORRENT_RATIO] = (ratio > BitTorrent::TorrentHandle::MAX_RATIO) ? -1 : ratio; - ret[KEY_TORRENT_STATE] = torrentStateToString(torrent->state()); - ret[KEY_TORRENT_ETA] = torrent->eta(); - ret[KEY_TORRENT_SEQUENTIAL_DOWNLOAD] = torrent->isSequentialDownload(); - if (torrent->hasMetadata()) - ret[KEY_TORRENT_FIRST_LAST_PIECE_PRIO] = torrent->hasFirstLastPiecePriority(); - ret[KEY_TORRENT_CATEGORY] = torrent->category(); - ret[KEY_TORRENT_TAGS] = torrent->tags().toList().join(", "); - ret[KEY_TORRENT_SUPER_SEEDING] = torrent->superSeeding(); - ret[KEY_TORRENT_FORCE_START] = torrent->isForced(); - ret[KEY_TORRENT_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); - ret[KEY_TORRENT_ADDED_ON] = torrent->addedTime().toTime_t(); - ret[KEY_TORRENT_COMPLETION_ON] = torrent->completedTime().toTime_t(); - ret[KEY_TORRENT_TRACKER] = torrent->currentTracker(); - ret[KEY_TORRENT_DL_LIMIT] = torrent->downloadLimit(); - ret[KEY_TORRENT_UP_LIMIT] = torrent->uploadLimit(); - ret[KEY_TORRENT_AMOUNT_DOWNLOADED] = torrent->totalDownload(); - ret[KEY_TORRENT_AMOUNT_UPLOADED] = torrent->totalUpload(); - ret[KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); - ret[KEY_TORRENT_AMOUNT_UPLOADED_SESSION] = torrent->totalPayloadUpload(); - ret[KEY_TORRENT_AMOUNT_LEFT] = torrent->incompletedSize(); - ret[KEY_TORRENT_AMOUNT_COMPLETED] = torrent->completedSize(); - ret[KEY_TORRENT_RATIO_LIMIT] = torrent->maxRatio(); - ret[KEY_TORRENT_LAST_SEEN_COMPLETE_TIME] = torrent->lastSeenComplete().toTime_t(); - ret[KEY_TORRENT_AUTO_TORRENT_MANAGEMENT] = torrent->isAutoTMMEnabled(); - ret[KEY_TORRENT_TIME_ACTIVE] = torrent->activeTime(); - - if (torrent->isPaused() || torrent->isChecking()) - ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = 0; - else { - QDateTime dt = QDateTime::currentDateTime(); - dt = dt.addSecs(-torrent->timeSinceActivity()); - ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = dt.toTime_t(); - } - - ret[KEY_TORRENT_TOTAL_SIZE] = torrent->totalSize(); - - return ret; - } - - // Compare two structures (prevData, data) and calculate difference (syncData). - // Structures encoded as map. - void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData) - { - // initialize output variable - syncData.clear(); - - QVariantList removedItems; - foreach (QString key, data.keys()) { - removedItems.clear(); - - switch (static_cast(data[key].type())) { - case QMetaType::QVariantMap: { - QVariantMap map; - processMap(prevData[key].toMap(), data[key].toMap(), map); - if (!map.isEmpty()) - syncData[key] = map; - } - break; - case QMetaType::QVariantHash: { - QVariantMap map; - processHash(prevData[key].toHash(), data[key].toHash(), map, removedItems); - if (!map.isEmpty()) - syncData[key] = map; - if (!removedItems.isEmpty()) - syncData[key + KEY_SUFFIX_REMOVED] = removedItems; - } - break; - case QMetaType::QVariantList: { - QVariantList list; - processList(prevData[key].toList(), data[key].toList(), list, removedItems); - if (!list.isEmpty()) - syncData[key] = list; - if (!removedItems.isEmpty()) - syncData[key + KEY_SUFFIX_REMOVED] = removedItems; - } - break; - case QMetaType::QString: - case QMetaType::LongLong: - case QMetaType::Float: - case QMetaType::Int: - case QMetaType::Bool: - case QMetaType::Double: - case QMetaType::ULongLong: - case QMetaType::UInt: - case QMetaType::QDateTime: - if (prevData[key] != data[key]) - syncData[key] = data[key]; - break; - default: - Q_ASSERT_X(false, "processMap" - , QString("Unexpected type: %1") - .arg(QMetaType::typeName(static_cast(data[key].type()))) - .toUtf8().constData()); - } - } - } - - // Compare two lists of structures (prevData, data) and calculate difference (syncData, removedItems). - // Structures encoded as map. - // Lists are encoded as hash table (indexed by structure key value) to improve ease of searching for removed items. - void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems) - { - // initialize output variables - syncData.clear(); - removedItems.clear(); - - if (prevData.isEmpty()) { - // If list was empty before, then difference is a whole new list. - foreach (QString key, data.keys()) - syncData[key] = data[key]; - } - else { - foreach (QString key, data.keys()) { - switch (data[key].type()) { - case QVariant::Map: - if (!prevData.contains(key)) { - // new list item found - append it to syncData - syncData[key] = data[key]; - } - else { - QVariantMap map; - processMap(prevData[key].toMap(), data[key].toMap(), map); - // existing list item found - remove it from prevData - prevData.remove(key); - if (!map.isEmpty()) - // changed list item found - append its changes to syncData - syncData[key] = map; - } - break; - default: - Q_ASSERT(0); - } - } - - if (!prevData.isEmpty()) { - // prevData contains only items that are missing now - - // put them in removedItems - foreach (QString s, prevData.keys()) - removedItems << s; - } - } - } - - // Compare two lists of simple value (prevData, data) and calculate difference (syncData, removedItems). - void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems) - { - // initialize output variables - syncData.clear(); - removedItems.clear(); - - if (prevData.isEmpty()) { - // If list was empty before, then difference is a whole new list. - syncData = data; - } - else { - foreach (QVariant item, data) { - if (!prevData.contains(item)) - // new list item found - append it to syncData - syncData.append(item); - else - // unchanged list item found - remove it from prevData - prevData.removeOne(item); - } - - if (!prevData.isEmpty()) - // prevData contains only items that are missing now - - // put them in removedItems - removedItems = prevData; - } - } - - QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData) - { - QVariantMap syncData; - bool fullUpdate = true; - int lastResponseId = 0; - if (acceptedResponseId > 0) { - lastResponseId = lastData[KEY_RESPONSE_ID].toInt(); - - if (lastResponseId == acceptedResponseId) - lastAcceptedData = lastData; - - int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt(); - - if (lastAcceptedResponseId == acceptedResponseId) { - processMap(lastAcceptedData, data, syncData); - fullUpdate = false; - } - } - - if (fullUpdate) { - lastAcceptedData.clear(); - syncData = data; - syncData[KEY_FULL_UPDATE] = true; - } - - lastResponseId = lastResponseId % 1000000 + 1; // cycle between 1 and 1000000 - lastData = data; - lastData[KEY_RESPONSE_ID] = lastResponseId; - syncData[KEY_RESPONSE_ID] = lastResponseId; - - return syncData; - } -} - -/** - * Returns all the torrents in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "hash": Torrent hash - * - "name": Torrent name - * - "size": Torrent size - * - "progress: Torrent progress - * - "dlspeed": Torrent download speed - * - "upspeed": Torrent upload speed - * - "priority": Torrent priority (-1 if queuing is disabled) - * - "num_seeds": Torrent seeds connected to - * - "num_complete": Torrent seeds in the swarm - * - "num_leechs": Torrent leechers connected to - * - "num_incomplete": Torrent leechers in the swarm - * - "ratio": Torrent share ratio - * - "eta": Torrent ETA - * - "state": Torrent state - * - "seq_dl": Torrent sequential download state - * - "f_l_piece_prio": Torrent first last piece priority state - * - "force_start": Torrent force start state - * - "category": Torrent category - */ -QByteArray btjson::getTorrents(QString filter, QString category, - QString sortedColumn, bool reverse, int limit, int offset) -{ - QVariantList torrentList; - TorrentFilter torrentFilter(filter, TorrentFilter::AnyHash, category); - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) { - if (torrentFilter.match(torrent)) - torrentList.append(toMap(torrent)); - } - - std::sort(torrentList.begin(), torrentList.end(), QTorrentCompare(sortedColumn, reverse)); - int size = torrentList.size(); - // normalize offset - if (offset < 0) - offset = size + offset; - if ((offset >= size) || (offset < 0)) - offset = 0; - // normalize limit - if (limit <= 0) - limit = -1; // unlimited - - if ((limit > 0) || (offset > 0)) - return json::toJson(torrentList.mid(offset, limit)); - else - return json::toJson(torrentList); -} - -/** - * The function returns the changed data from the server to synchronize with the web client. - * Return value is map in JSON format. - * Map contain the key: - * - "Rid": ID response - * Map can contain the keys: - * - "full_update": full data update flag - * - "torrents": dictionary contains information about torrents. - * - "torrents_removed": a list of hashes of removed torrents - * - "categories": list of categories - * - "categories_removed": list of removed categories - * - "server_state": map contains information about the state of the server - * The keys of the 'torrents' dictionary are hashes of torrents. - * Each value of the 'torrents' dictionary contains map. The map can contain following keys: - * - "name": Torrent name - * - "size": Torrent size - * - "progress: Torrent progress - * - "dlspeed": Torrent download speed - * - "upspeed": Torrent upload speed - * - "priority": Torrent priority (-1 if queuing is disabled) - * - "num_seeds": Torrent seeds connected to - * - "num_complete": Torrent seeds in the swarm - * - "num_leechs": Torrent leechers connected to - * - "num_incomplete": Torrent leechers in the swarm - * - "ratio": Torrent share ratio - * - "eta": Torrent ETA - * - "state": Torrent state - * - "seq_dl": Torrent sequential download state - * - "f_l_piece_prio": Torrent first last piece priority state - * - "completion_on": Torrent copletion time - * - "tracker": Torrent tracker - * - "dl_limit": Torrent download limit - * - "up_limit": Torrent upload limit - * - "downloaded": Amount of data downloaded - * - "uploaded": Amount of data uploaded - * - "downloaded_session": Amount of data downloaded since program open - * - "uploaded_session": Amount of data uploaded since program open - * - "amount_left": Amount of data left to download - * - "save_path": Torrent save path - * - "completed": Amount of data completed - * - "ratio_limit": Upload share ratio limit - * - "seen_complete": Indicates the time when the torrent was last seen complete/whole - * - "last_activity": Last time when a chunk was downloaded/uploaded - * - "total_size": Size including unwanted data - * Server state map may contain the following keys: - * - "connection_status": connection status - * - "dht_nodes": DHT nodes count - * - "dl_info_data": bytes downloaded - * - "dl_info_speed": download speed - * - "dl_rate_limit: download rate limit - * - "up_info_data: bytes uploaded - * - "up_info_speed: upload speed - * - "up_rate_limit: upload speed limit - * - "queueing": priority system usage flag - * - "refresh_interval": torrents table refresh interval - */ -QByteArray btjson::getSyncMainData(int acceptedResponseId, QVariantMap &lastData, QVariantMap &lastAcceptedData) -{ - QVariantMap data; - QVariantHash torrents; - - BitTorrent::Session *const session = BitTorrent::Session::instance(); - - foreach (BitTorrent::TorrentHandle *const torrent, session->torrents()) { - QVariantMap map = toMap(torrent); - map.remove(KEY_TORRENT_HASH); - - // Calculated last activity time can differ from actual value by up to 10 seconds (this is a libtorrent issue). - // So we don't need unnecessary updates of last activity time in response. - if (lastData.contains("torrents") && lastData["torrents"].toHash().contains(torrent->hash()) && - lastData["torrents"].toHash()[torrent->hash()].toMap().contains(KEY_TORRENT_LAST_ACTIVITY_TIME)) { - uint lastValue = lastData["torrents"].toHash()[torrent->hash()].toMap()[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt(); - if (qAbs(static_cast(lastValue - map[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt())) < 15) - map[KEY_TORRENT_LAST_ACTIVITY_TIME] = lastValue; - } - - torrents[torrent->hash()] = map; - } - - data["torrents"] = torrents; - - QVariantList categories; - foreach (const QString &category, session->categories().keys()) - categories << category; - - data["categories"] = categories; - - QVariantMap serverState = getTranserInfoMap(); - serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); - serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); - serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); - data["server_state"] = serverState; - - return json::toJson(generateSyncData(acceptedResponseId, data, lastAcceptedData, lastData)); -} - -QByteArray btjson::getSyncTorrentPeersData(int acceptedResponseId, QString hash, QVariantMap &lastData, QVariantMap &lastAcceptedData) -{ - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - QVariantMap data; - QVariantHash peers; - QList peersList = torrent->peers(); -#ifndef DISABLE_COUNTRIES_RESOLUTION - bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries(); -#else - bool resolvePeerCountries = false; -#endif - - data[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS] = resolvePeerCountries; - - foreach (const BitTorrent::PeerInfo &pi, peersList) { - if (pi.address().ip.isNull()) continue; - QVariantMap peer; -#ifndef DISABLE_COUNTRIES_RESOLUTION - if (resolvePeerCountries) { - peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower(); - peer[KEY_PEER_COUNTRY] = Net::GeoIPManager::CountryName(pi.country()); - } -#endif - peer[KEY_PEER_IP] = pi.address().ip.toString(); - peer[KEY_PEER_PORT] = pi.address().port; - peer[KEY_PEER_CLIENT] = pi.client(); - peer[KEY_PEER_PROGRESS] = pi.progress(); - peer[KEY_PEER_DOWN_SPEED] = pi.payloadDownSpeed(); - peer[KEY_PEER_UP_SPEED] = pi.payloadUpSpeed(); - peer[KEY_PEER_TOT_DOWN] = pi.totalDownload(); - peer[KEY_PEER_TOT_UP] = pi.totalUpload(); - peer[KEY_PEER_CONNECTION_TYPE] = pi.connectionType(); - peer[KEY_PEER_FLAGS] = pi.flags(); - peer[KEY_PEER_FLAGS_DESCRIPTION] = pi.flagsDescription(); - peer[KEY_PEER_RELEVANCE] = pi.relevance(); - peer[KEY_PEER_FILES] = torrent->info().filesForPiece(pi.downloadingPieceIndex()).join(QLatin1String("\n")); - - peers[pi.address().ip.toString() + ":" + QString::number(pi.address().port)] = peer; - } - - data["peers"] = peers; - - return json::toJson(generateSyncData(acceptedResponseId, data, lastAcceptedData, lastData)); -} - -/** - * Returns the trackers for a torrent in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "url": Tracker URL - * - "status": Tracker status - * - "num_peers": Tracker peer count - * - "msg": Tracker message (last) - */ -QByteArray btjson::getTrackersForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, trackerList, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - QHash trackers_data = torrent->trackerInfos(); - foreach (const BitTorrent::TrackerEntry &tracker, torrent->trackers()) { - QVariantMap trackerDict; - trackerDict[KEY_TRACKER_URL] = tracker.url(); - const BitTorrent::TrackerInfo data = trackers_data.value(tracker.url()); - QString status; - switch (tracker.status()) { - case BitTorrent::TrackerEntry::NotContacted: - status = tr("Not contacted yet"); break; - case BitTorrent::TrackerEntry::Updating: - status = tr("Updating..."); break; - case BitTorrent::TrackerEntry::Working: - status = tr("Working"); break; - case BitTorrent::TrackerEntry::NotWorking: - status = tr("Not working"); break; - } - trackerDict[KEY_TRACKER_STATUS] = status; - trackerDict[KEY_TRACKER_PEERS] = data.numPeers; - trackerDict[KEY_TRACKER_MSG] = data.lastMessage.trimmed(); - - trackerList.append(trackerDict); - } - - return json::toJson(trackerList); -} - -/** - * Returns the web seeds for a torrent in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "url": Web seed URL - */ -QByteArray btjson::getWebSeedsForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, webSeedList, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - foreach (const QUrl &webseed, torrent->urlSeeds()) { - QVariantMap webSeedDict; - webSeedDict[KEY_WEBSEED_URL] = webseed.toString(); - webSeedList.append(webSeedDict); - } - - return json::toJson(webSeedList); -} - -/** - * Returns the properties for a torrent in JSON format. - * - * The return value is a JSON-formatted dictionary. - * The dictionary keys are: - * - "time_elapsed": Torrent elapsed time - * - "seeding_time": Torrent elapsed time while complete - * - "eta": Torrent ETA - * - "nb_connections": Torrent connection count - * - "nb_connections_limit": Torrent connection count limit - * - "total_downloaded": Total data uploaded for torrent - * - "total_downloaded_session": Total data downloaded this session - * - "total_uploaded": Total data uploaded for torrent - * - "total_uploaded_session": Total data uploaded this session - * - "dl_speed": Torrent download speed - * - "dl_speed_avg": Torrent average download speed - * - "up_speed": Torrent upload speed - * - "up_speed_avg": Torrent average upload speed - * - "dl_limit": Torrent download limit - * - "up_limit": Torrent upload limit - * - "total_wasted": Total data wasted for torrent - * - "seeds": Torrent connected seeds - * - "seeds_total": Torrent total number of seeds - * - "peers": Torrent connected peers - * - "peers_total": Torrent total number of peers - * - "share_ratio": Torrent share ratio - * - "reannounce": Torrent next reannounce time - * - "total_size": Torrent total size - * - "pieces_num": Torrent pieces count - * - "piece_size": Torrent piece size - * - "pieces_have": Torrent pieces have - * - "created_by": Torrent creator - * - "last_seen": Torrent last seen complete - * - "addition_date": Torrent addition date - * - "completion_date": Torrent completion date - * - "creation_date": Torrent creation date - * - "save_path": Torrent save path - * - "comment": Torrent comment - */ -QByteArray btjson::getPropertiesForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantMap, dataDict, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime(); - dataDict[KEY_PROP_SEEDING_TIME] = torrent->seedingTime(); - dataDict[KEY_PROP_ETA] = torrent->eta(); - dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount(); - dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit(); - dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload(); - dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); - dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload(); - dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload(); - dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate(); - dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / (1 + torrent->activeTime() - torrent->finishedTime()); - dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate(); - dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / (1 + torrent->activeTime()); - dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit(); - dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit(); - dataDict[KEY_PROP_WASTED] = torrent->wastedSize(); - dataDict[KEY_PROP_SEEDS] = torrent->seedsCount(); - dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount(); - dataDict[KEY_PROP_PEERS] = torrent->leechsCount(); - dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount(); - const qreal ratio = torrent->realRatio(); - dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::TorrentHandle::MAX_RATIO ? -1 : ratio; - dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce(); - dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize(); - dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount(); - dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength(); - dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave(); - dataDict[KEY_PROP_CREATED_BY] = torrent->creator(); - dataDict[KEY_PROP_ADDITION_DATE] = torrent->addedTime().toTime_t(); - if (torrent->hasMetadata()) { - dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? static_cast(torrent->lastSeenComplete().toTime_t()) : -1; - dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? static_cast(torrent->completedTime().toTime_t()) : -1; - dataDict[KEY_PROP_CREATION_DATE] = torrent->creationDate().toTime_t(); - } - else { - dataDict[KEY_PROP_LAST_SEEN] = -1; - dataDict[KEY_PROP_COMPLETION_DATE] = -1; - dataDict[KEY_PROP_CREATION_DATE] = -1; - } - dataDict[KEY_PROP_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); - dataDict[KEY_PROP_COMMENT] = torrent->comment(); - - return json::toJson(dataDict); -} - -/** - * Returns the files in a torrent in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "name": File name - * - "size": File size - * - "progress": File progress - * - "priority": File priority - * - "is_seed": Flag indicating if torrent is seeding/complete - * - "piece_range": Piece index range, the first number is the starting piece index - * and the second number is the ending piece index (inclusive) - */ -QByteArray btjson::getFilesForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, fileList, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - if (!torrent->hasMetadata()) - return json::toJson(fileList); - - const QVector priorities = torrent->filePriorities(); - const QVector fp = torrent->filesProgress(); - const QVector fileAvailability = torrent->availableFileFractions(); - const BitTorrent::TorrentInfo info = torrent->info(); - for (int i = 0; i < torrent->filesCount(); ++i) { - QVariantMap fileDict; - fileDict[KEY_FILE_PROGRESS] = fp[i]; - fileDict[KEY_FILE_PRIORITY] = priorities[i]; - fileDict[KEY_FILE_SIZE] = torrent->fileSize(i); - fileDict[KEY_FILE_AVAILABILITY] = fileAvailability[i]; - - QString fileName = torrent->filePath(i); - if (fileName.endsWith(QB_EXT, Qt::CaseInsensitive)) - fileName.chop(QB_EXT.size()); - fileDict[KEY_FILE_NAME] = Utils::Fs::toNativePath(fileName); - - const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(i); - fileDict[KEY_FILE_PIECE_RANGE] = QVariantList {idx.first(), idx.last()}; - - if (i == 0) - fileDict[KEY_FILE_IS_SEED] = torrent->isSeed(); - - fileList.append(fileDict); - } - - return json::toJson(fileList); -} - -/** - * Returns an array of hashes (of each pieces respectively) for a torrent in JSON format. - * - * The return value is a JSON-formatted array of strings (hex strings). - */ -QByteArray btjson::getPieceHashesForTorrent(const QString &hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, pieceHashes, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - const QVector hashes = torrent->info().pieceHashes(); - pieceHashes.reserve(hashes.size()); - foreach (const QByteArray &hash, hashes) - pieceHashes.append(hash.toHex()); - - return json::toJson(pieceHashes); -} - -/** - * Returns an array of states (of each pieces respectively) for a torrent in JSON format. - * - * The return value is a JSON-formatted array of ints. - * 0: piece not downloaded - * 1: piece requested or downloading - * 2: piece already downloaded - */ -QByteArray btjson::getPieceStatesForTorrent(const QString &hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, pieceStates, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - const QBitArray states = torrent->pieces(); - pieceStates.reserve(states.size()); - for (int i = 0; i < states.size(); ++i) - pieceStates.append(static_cast(states[i]) * 2); - - const QBitArray dlstates = torrent->downloadingPieces(); - for (int i = 0; i < states.size(); ++i) { - if (dlstates[i]) - pieceStates[i] = 1; - } - - return json::toJson(pieceStates); -} - -/** - * Returns the global transfer information in JSON format. - * - * The return value is a JSON-formatted dictionary. - * The dictionary keys are: - * - "dl_info_speed": Global download rate - * - "dl_info_data": Data downloaded this session - * - "up_info_speed": Global upload rate - * - "up_info_data": Data uploaded this session - * - "dl_rate_limit": Download rate limit - * - "up_rate_limit": Upload rate limit - * - "dht_nodes": DHT nodes connected to - * - "connection_status": Connection status - */ -QByteArray btjson::getTransferInfo() -{ - return json::toJson(getTranserInfoMap()); -} - -QByteArray btjson::getTorrentsRatesLimits(QStringList &hashes, bool downloadLimits) -{ - QVariantMap map; - - foreach (const QString &hash, hashes) { - int limit = -1; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - limit = downloadLimits ? torrent->downloadLimit() : torrent->uploadLimit(); - map[hash] = limit; - } - - return json::toJson(map); -} - -/** - * Returns the log in JSON format. - * - * The return value is an array of dictionaries. - * The dictionary keys are: - * - "id": id of the message - * - "timestamp": milliseconds since epoch - * - "type": type of the message (int, see MsgType) - * - "message": text of the message - */ -QByteArray btjson::getLog(bool normal, bool info, bool warning, bool critical, int lastKnownId) -{ - Logger* const logger = Logger::instance(); - QVariantList msgList; - - foreach (const Log::Msg& msg, logger->getMessages(lastKnownId)) { - if (!((msg.type == Log::NORMAL && normal) - || (msg.type == Log::INFO && info) - || (msg.type == Log::WARNING && warning) - || (msg.type == Log::CRITICAL && critical))) - continue; - QVariantMap map; - map[KEY_LOG_ID] = msg.id; - map[KEY_LOG_TIMESTAMP] = msg.timestamp; - map[KEY_LOG_MSG_TYPE] = msg.type; - map[KEY_LOG_MSG_MESSAGE] = msg.message; - msgList.append(map); - } - - return json::toJson(msgList); -} - -/** - * Returns the peer log in JSON format. - * - * The return value is an array of dictionaries. - * The dictionary keys are: - * - "id": id of the message - * - "timestamp": milliseconds since epoch - * - "ip": IP of the peer - * - "blocked": whether or not the peer was blocked - * - "reason": reason of the block - */ -QByteArray btjson::getPeerLog(int lastKnownId) -{ - Logger* const logger = Logger::instance(); - QVariantList peerList; - - foreach (const Log::Peer& peer, logger->getPeers(lastKnownId)) { - QVariantMap map; - map[KEY_LOG_ID] = peer.id; - map[KEY_LOG_TIMESTAMP] = peer.timestamp; - map[KEY_LOG_PEER_IP] = peer.ip; - map[KEY_LOG_PEER_BLOCKED] = peer.blocked; - map[KEY_LOG_PEER_REASON] = peer.reason; - peerList.append(map); - } - - return json::toJson(peerList); -} diff --git a/src/webui/btjson.h b/src/webui/btjson.h deleted file mode 100644 index 914fb1aca..000000000 --- a/src/webui/btjson.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2012, Christophe Dumez - * - * 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. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * Contact : chris@qbittorrent.org - */ - -#ifndef BTJSON_H -#define BTJSON_H - -#include -#include -#include - -class btjson -{ - Q_DECLARE_TR_FUNCTIONS(misc) - -private: - btjson() {} - -public: - static QByteArray getTorrents(QString filter = "all", QString category = QString(), - QString sortedColumn = "name", bool reverse = false, int limit = 0, int offset = 0); - static QByteArray getSyncMainData(int acceptedResponseId, QVariantMap &lastData, QVariantMap &lastAcceptedData); - static QByteArray getSyncTorrentPeersData(int acceptedResponseId, QString hash, QVariantMap &lastData, QVariantMap &lastAcceptedData); - static QByteArray getTrackersForTorrent(const QString& hash); - static QByteArray getWebSeedsForTorrent(const QString& hash); - static QByteArray getPropertiesForTorrent(const QString& hash); - static QByteArray getFilesForTorrent(const QString& hash); - static QByteArray getPieceHashesForTorrent(const QString &hash); - static QByteArray getPieceStatesForTorrent(const QString &hash); - static QByteArray getTransferInfo(); - static QByteArray getTorrentsRatesLimits(QStringList& hashes, bool downloadLimits); - static QByteArray getLog(bool normal, bool info, bool warning, bool critical, int lastKnownId); - static QByteArray getPeerLog(int lastKnownId); -}; // class btjson - -#endif // BTJSON_H diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index c888bc910..d55464858 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -28,963 +28,708 @@ #include "webapplication.h" +#include +#include #include +#include #include #include -#include +#include #include -#include +#include +#include +#include +#include +#include +#include #include +#include -#include "base/bittorrent/session.h" -#include "base/bittorrent/torrenthandle.h" -#include "base/bittorrent/torrentinfo.h" -#include "base/bittorrent/trackerentry.h" +#include "base/http/httperror.h" #include "base/iconprovider.h" #include "base/logger.h" -#include "base/net/downloadmanager.h" #include "base/preferences.h" -#include "base/tristatebool.h" #include "base/utils/fs.h" #include "base/utils/misc.h" +#include "base/utils/net.h" +#include "base/utils/random.h" #include "base/utils/string.h" -#include "btjson.h" -#include "jsonutils.h" -#include "prefjson.h" -#include "websessiondata.h" - -static const int API_VERSION = 17; -static const int API_VERSION_MIN = 15; - -const QString WWW_FOLDER = ":/www/public/"; -const QString PRIVATE_FOLDER = ":/www/private/"; -const QString DEFAULT_SCOPE = "public"; -const QString SCOPE_IMAGES = "images"; -const QString SCOPE_THEME = "theme"; -const QString DEFAULT_ACTION = "index"; -const QString WEBUI_ACTION = "webui"; -const QString VERSION_INFO = "version"; -const QString MAX_AGE_MONTH = "public, max-age=2592000"; - -#define ADD_ACTION(scope, action) actions[#scope][#action] = &WebApplication::action_##scope##_##action - -QMap> WebApplication::initializeActions() -{ - QMap> actions; - - ADD_ACTION(public, webui); - ADD_ACTION(public, index); - ADD_ACTION(public, login); - ADD_ACTION(public, logout); - ADD_ACTION(public, theme); - ADD_ACTION(public, images); - ADD_ACTION(query, torrents); - ADD_ACTION(query, preferences); - ADD_ACTION(query, transferInfo); - ADD_ACTION(query, propertiesGeneral); - ADD_ACTION(query, propertiesTrackers); - ADD_ACTION(query, propertiesWebSeeds); - ADD_ACTION(query, propertiesFiles); - ADD_ACTION(query, getLog); - ADD_ACTION(query, getPeerLog); - ADD_ACTION(query, getPieceHashes); - ADD_ACTION(query, getPieceStates); - ADD_ACTION(sync, maindata); - ADD_ACTION(sync, torrent_peers); - ADD_ACTION(command, shutdown); - ADD_ACTION(command, download); - ADD_ACTION(command, upload); - ADD_ACTION(command, addTrackers); - ADD_ACTION(command, resumeAll); - ADD_ACTION(command, pauseAll); - ADD_ACTION(command, resume); - ADD_ACTION(command, pause); - ADD_ACTION(command, setPreferences); - ADD_ACTION(command, setFilePrio); - ADD_ACTION(command, getGlobalUpLimit); - ADD_ACTION(command, getGlobalDlLimit); - ADD_ACTION(command, setGlobalUpLimit); - ADD_ACTION(command, setGlobalDlLimit); - ADD_ACTION(command, getTorrentsUpLimit); - ADD_ACTION(command, getTorrentsDlLimit); - ADD_ACTION(command, setTorrentsUpLimit); - ADD_ACTION(command, setTorrentsDlLimit); - ADD_ACTION(command, alternativeSpeedLimitsEnabled); - ADD_ACTION(command, toggleAlternativeSpeedLimits); - ADD_ACTION(command, toggleSequentialDownload); - ADD_ACTION(command, toggleFirstLastPiecePrio); - ADD_ACTION(command, setSuperSeeding); - ADD_ACTION(command, setForceStart); - ADD_ACTION(command, delete); - ADD_ACTION(command, deletePerm); - ADD_ACTION(command, increasePrio); - ADD_ACTION(command, decreasePrio); - ADD_ACTION(command, topPrio); - ADD_ACTION(command, bottomPrio); - ADD_ACTION(command, setLocation); - ADD_ACTION(command, rename); - ADD_ACTION(command, setAutoTMM); - ADD_ACTION(command, recheck); - ADD_ACTION(command, setCategory); - ADD_ACTION(command, addCategory); - ADD_ACTION(command, removeCategories); - ADD_ACTION(command, getSavePath); - ADD_ACTION(version, api); - ADD_ACTION(version, api_min); - ADD_ACTION(version, qbittorrent); - - return actions; -} +#include "api/apierror.h" +#include "api/appcontroller.h" +#include "api/authcontroller.h" +#include "api/logcontroller.h" +#include "api/rsscontroller.h" +#include "api/synccontroller.h" +#include "api/torrentscontroller.h" +#include "api/transfercontroller.h" + +constexpr int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024; + +const QString PATH_PREFIX_IMAGES {"/images/"}; +const QString PATH_PREFIX_THEME {"/theme/"}; +const QString WWW_FOLDER {":/www"}; +const QString PUBLIC_FOLDER {"/public"}; +const QString PRIVATE_FOLDER {"/private"}; +const QString MAX_AGE_MONTH {"public, max-age=2592000"}; namespace { -#define CHECK_URI(ARGS_NUM) \ - if (args_.size() != ARGS_NUM) { \ - status(404, "Not Found"); \ - return; \ - } -#define CHECK_PARAMETERS(PARAMETERS) \ - QStringList parameters; \ - parameters << PARAMETERS; \ - if (parameters.size() != request().posts.size()) { \ - status(400, "Bad Request"); \ - return; \ - } \ - foreach (QString key, request().posts.keys()) { \ - if (!parameters.contains(key, Qt::CaseInsensitive)) { \ - status(400, "Bad Request"); \ - return; \ - } \ - } - - bool parseBool(const QString &string, const bool defaultValue) + QStringMap parseCookie(const QString &cookieStr) { - if (defaultValue) - return (string.compare("false", Qt::CaseInsensitive) == 0) ? false : true; - return (string.compare("true", Qt::CaseInsensitive) == 0) ? true : false; + // [rfc6265] 4.2.1. Syntax + QStringMap ret; + const QVector cookies = cookieStr.splitRef(';', QString::SkipEmptyParts); + + for (const auto &cookie : cookies) { + const int idx = cookie.indexOf('='); + if (idx < 0) + continue; + + const QString name = cookie.left(idx).trimmed().toString(); + const QString value = Utils::String::unquote(cookie.mid(idx + 1).trimmed()).toString(); + ret.insert(name, value); + } + return ret; } - TriStateBool parseTristatebool(const QString &string) + void translateDocument(QString &data) { - if (string.compare("true", Qt::CaseInsensitive) == 0) - return TriStateBool::True; - if (string.compare("false", Qt::CaseInsensitive) == 0) - return TriStateBool::False; - return TriStateBool::Undefined; - } -} - -void WebApplication::action_public_index() -{ - QString path; - - if (!args_.isEmpty()) { - if (args_.back() == "favicon.ico") - path = ":/icons/skin/qbittorrent16.png"; - else - path = WWW_FOLDER + args_.join("/"); - } - - printFile(path); -} - -void WebApplication::action_public_webui() -{ - if (!sessionActive()) - printFile(PRIVATE_FOLDER + "login.html"); - else - printFile(PRIVATE_FOLDER + "index.html"); -} - -void WebApplication::action_public_login() -{ - if (sessionActive()) { - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - return; - } - - const Preferences *const pref = Preferences::instance(); - QCryptographicHash md5(QCryptographicHash::Md5); - - md5.addData(request().posts["password"].toLocal8Bit()); - QString pass = md5.result().toHex(); - - bool equalUser = Utils::String::slowEquals(request().posts["username"].toUtf8(), pref->getWebUiUsername().toUtf8()); - bool equalPass = Utils::String::slowEquals(pass.toUtf8(), pref->getWebUiPassword().toUtf8()); + const QRegExp regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR(\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\])"); + const QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?"); + int i = 0; + bool found = true; + + const QString locale = Preferences::instance()->getLocale(); + bool isTranslationNeeded = !locale.startsWith("en") || locale.startsWith("en_AU") || locale.startsWith("en_GB"); + + while (i < data.size() && found) { + i = regex.indexIn(data, i); + if (i >= 0) { + //qDebug("Found translatable string: %s", regex.cap(1).toUtf8().data()); + QByteArray word = regex.cap(1).toUtf8(); + + QString translation = word; + if (isTranslationNeeded) { + QString context = regex.cap(4); + translation = qApp->translate(context.toUtf8().constData(), word.constData(), 0, 1); + } + // Remove keyboard shortcuts + translation.replace(mnemonic, ""); + + // Use HTML code for quotes to prevent issues with JS + translation.replace("'", "'"); + translation.replace("\"", """); + + data.replace(i, regex.matchedLength(), translation); + i += translation.length(); + } + else { + found = false; // no more translatable strings + } - if (equalUser && equalPass) { - sessionStart(); - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - } - else { - QString addr = env().clientAddress.toString(); - increaseFailedAttempts(); - qDebug("client IP: %s (%d failed attempts)", qUtf8Printable(addr), failedAttempts()); - print(QByteArray("Fails."), Http::CONTENT_TYPE_TXT); + data.replace(QLatin1String("${LANG}"), locale.left(2)); + data.replace(QLatin1String("${VERSION}"), QBT_VERSION); + } } -} -void WebApplication::action_public_logout() -{ - CHECK_URI(0); - sessionEnd(); -} - -void WebApplication::action_public_theme() -{ - if (args_.size() != 1) { - status(404, "Not Found"); - return; + inline QUrl urlFromHostHeader(const QString &hostHeader) + { + if (!hostHeader.contains(QLatin1String("://"))) + return QUrl(QLatin1String("http://") + hostHeader); + return hostHeader; } - - QString url = IconProvider::instance()->getIconPath(args_.front()); - qDebug() << Q_FUNC_INFO << "There icon:" << url; - - printFile(url); - header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); -} - -void WebApplication::action_public_images() -{ - const QString path = ":/icons/" + args_.join("/"); - printFile(path); - header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); -} - -// GET params: -// - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive -// - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") -// - sort (string): name of column for sorting by its value -// - reverse (bool): enable reverse sorting -// - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) -// - offset (int): set offset (if less than 0 - offset from end) -void WebApplication::action_query_torrents() -{ - CHECK_URI(0); - - const QStringMap &gets = request().gets; - print(btjson::getTorrents( - gets["filter"], gets["category"], gets["sort"], parseBool(gets["reverse"], false), - gets["limit"].toInt(), gets["offset"].toInt()) - , Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_preferences() -{ - CHECK_URI(0); - print(prefjson::getPreferences(), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_transferInfo() -{ - CHECK_URI(0); - print(btjson::getTransferInfo(), Http::CONTENT_TYPE_JSON); } -void WebApplication::action_query_propertiesGeneral() +WebApplication::WebApplication(QObject *parent) + : QObject(parent) { - CHECK_URI(1); - print(btjson::getPropertiesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} + registerAPIController(QLatin1String("app"), new AppController(this, this)); + registerAPIController(QLatin1String("auth"), new AuthController(this, this)); + registerAPIController(QLatin1String("log"), new LogController(this, this)); + registerAPIController(QLatin1String("rss"), new RSSController(this, this)); + registerAPIController(QLatin1String("sync"), new SyncController(this, this)); + registerAPIController(QLatin1String("torrents"), new TorrentsController(this, this)); + registerAPIController(QLatin1String("transfer"), new TransferController(this, this)); -void WebApplication::action_query_propertiesTrackers() -{ - CHECK_URI(1); - print(btjson::getTrackersForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} + declarePublicAPI(QLatin1String("auth/login")); -void WebApplication::action_query_propertiesWebSeeds() -{ - CHECK_URI(1); - print(btjson::getWebSeedsForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); + configure(); + connect(Preferences::instance(), &Preferences::changed, this, &WebApplication::configure); } -void WebApplication::action_query_propertiesFiles() +WebApplication::~WebApplication() { - CHECK_URI(1); - print(btjson::getFilesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); + // cleanup sessions data + qDeleteAll(m_sessions); } -// GET params: -// - normal (bool): include normal messages (default true) -// - info (bool): include info messages (default true) -// - warning (bool): include warning messages (default true) -// - critical (bool): include critical messages (default true) -// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) -void WebApplication::action_query_getLog() +void WebApplication::sendWebUIFile() { - CHECK_URI(0); + const QStringList pathItems {request().path.split('/', QString::SkipEmptyParts)}; + if (pathItems.contains(".") || pathItems.contains("..")) + throw InternalServerErrorHTTPError(); - const bool isNormal = parseBool(request().gets["normal"], true); - const bool isInfo = parseBool(request().gets["info"], true); - const bool isWarning = parseBool(request().gets["warning"], true); - const bool isCritical = parseBool(request().gets["critical"], true); + if (!m_isAltUIUsed) { + if (request().path.startsWith(PATH_PREFIX_IMAGES)) { + const QString imageFilename {request().path.mid(PATH_PREFIX_IMAGES.size())}; + sendFile(QLatin1String(":/icons/") + imageFilename); + header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); + return; + } - bool ok = false; - int lastKnownId = request().gets["last_known_id"].toInt(&ok); - if (!ok) - lastKnownId = -1; + if (request().path.startsWith(PATH_PREFIX_THEME)) { + const QString iconId {request().path.mid(PATH_PREFIX_THEME.size())}; + sendFile(IconProvider::instance()->getIconPath(iconId)); + header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); + return; + } + } - print(btjson::getLog(isNormal, isInfo, isWarning, isCritical, lastKnownId), Http::CONTENT_TYPE_JSON); -} + const QString path { + (request().path != QLatin1String("/") + ? request().path + : (session() + ? QLatin1String("/index.html") + : QLatin1String("/login.html"))) + }; -// GET params: -// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) -void WebApplication::action_query_getPeerLog() -{ - CHECK_URI(0); - int lastKnownId; - bool ok; + QString localPath { + m_rootFolder + + (session() ? PRIVATE_FOLDER : PUBLIC_FOLDER) + + path + }; - lastKnownId = request().gets["last_known_id"].toInt(&ok); - if (!ok) - lastKnownId = -1; + QFileInfo fileInfo {localPath}; - print(btjson::getPeerLog(lastKnownId), Http::CONTENT_TYPE_JSON); -} + if (!fileInfo.exists() && session()) { + // try to send public file if there is no private one + localPath = m_rootFolder + PUBLIC_FOLDER + path; + fileInfo.setFile(localPath); + } -void WebApplication::action_query_getPieceHashes() -{ - CHECK_URI(1); - print(btjson::getPieceHashesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} + if (m_isAltUIUsed) { +#ifdef Q_OS_UNIX + if (!Utils::Fs::isRegularFile(localPath)) { + status(500, "Internal Server Error"); + print(tr("Unacceptable file type, only regular file is allowed."), Http::CONTENT_TYPE_TXT); + return; + } +#endif -void WebApplication::action_query_getPieceStates() -{ - CHECK_URI(1); - print(btjson::getPieceStatesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} + while (fileInfo.filePath() != m_rootFolder) { + if (fileInfo.isSymLink()) + throw InternalServerErrorHTTPError(tr("Symlinks inside alternative UI folder are forbidden.")); -// GET param: -// - rid (int): last response id -void WebApplication::action_sync_maindata() -{ - CHECK_URI(0); - print(btjson::getSyncMainData(request().gets["rid"].toInt(), - session()->syncMainDataLastResponse, - session()->syncMainDataLastAcceptedResponse), Http::CONTENT_TYPE_JSON); -} + fileInfo.setFile(fileInfo.path()); + } + } -// GET param: -// - hash (string): torrent hash -// - rid (int): last response id -void WebApplication::action_sync_torrent_peers() -{ - CHECK_URI(0); - print(btjson::getSyncTorrentPeersData(request().gets["rid"].toInt(), - request().gets["hash"], - session()->syncTorrentPeersLastResponse, - session()->syncTorrentPeersLastAcceptedResponse), Http::CONTENT_TYPE_JSON); + sendFile(localPath); } - -void WebApplication::action_version_api() +WebSession *WebApplication::session() { - CHECK_URI(0); - print(QString::number(API_VERSION), Http::CONTENT_TYPE_TXT); + return m_currentSession; } -void WebApplication::action_version_api_min() +const Http::Request &WebApplication::request() const { - CHECK_URI(0); - print(QString::number(API_VERSION_MIN), Http::CONTENT_TYPE_TXT); + return m_request; } -void WebApplication::action_version_qbittorrent() +const Http::Environment &WebApplication::env() const { - CHECK_URI(0); - print(QString(QBT_VERSION), Http::CONTENT_TYPE_TXT); + return m_env; } -void WebApplication::action_command_shutdown() +void WebApplication::doProcessRequest() { - qDebug() << "Shutdown request from Web UI"; - CHECK_URI(0); + QStringMap *params = &((request().method == QLatin1String("GET")) + ? m_request.gets : m_request.posts); + QString scope, action; - // Special case handling for shutdown, we - // need to reply to the Web UI before - // actually shutting down. - QTimer::singleShot(100, qApp, SLOT(quit())); -} + const auto findAPICall = [&]() -> bool + { + QRegularExpressionMatch match = m_apiPathPattern.match(request().path); + if (!match.hasMatch()) return false; -void WebApplication::action_command_download() -{ - CHECK_URI(0); - - const QString urls = request().posts.value("urls"); - const bool skipChecking = parseBool(request().posts.value("skip_checking"), false); - const bool seqDownload = parseBool(request().posts.value("sequentialDownload"), false); - const bool firstLastPiece = parseBool(request().posts.value("firstLastPiecePrio"), false); - const TriStateBool addPaused = parseTristatebool(request().posts.value("paused")); - const TriStateBool rootFolder = parseTristatebool(request().posts.value("root_folder")); - const QString savepath = request().posts.value("savepath").trimmed(); - const QString category = request().posts.value("category").trimmed(); - const QString cookie = request().posts.value("cookie"); - const QString torrentName = request().posts.value("rename").trimmed(); - const int upLimit = request().posts.value("upLimit").toInt(); - const int dlLimit = request().posts.value("dlLimit").toInt(); - - QList cookies; - if (!cookie.isEmpty()) { - const QStringList cookiesStr = cookie.split("; "); - for (QString cookieStr : cookiesStr) { - cookieStr = cookieStr.trimmed(); - int index = cookieStr.indexOf('='); - if (index > 1) { - QByteArray name = cookieStr.left(index).toLatin1(); - QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1(); - cookies += QNetworkCookie(name, value); - } - } - } + action = match.captured(QLatin1String("action")); + scope = match.captured(QLatin1String("scope")); + return true; + }; - BitTorrent::AddTorrentParams params; - // TODO: Check if destination actually exists - params.skipChecking = skipChecking; - params.sequential = seqDownload; - params.firstLastPiecePriority = firstLastPiece; - params.addPaused = addPaused; - params.createSubfolder = rootFolder; - params.savePath = savepath; - params.category = category; - params.name = torrentName; - params.uploadLimit = (upLimit > 0) ? upLimit : -1; - params.downloadLimit = (dlLimit > 0) ? dlLimit : -1; - - bool partialSuccess = false; - for (QString url : urls.split('\n')) { - url = url.trimmed(); - if (!url.isEmpty()) { - Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8())); - partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, params); + const auto findLegacyAPICall = [&]() -> bool + { + QRegularExpressionMatch match = m_apiLegacyPathPattern.match(request().path); + if (!match.hasMatch()) return false; + + struct APICompatInfo + { + QString scope; + QString action; + }; + const QMap APICompatMapping { + {"sync/maindata", {"sync", "maindata"}}, + {"sync/torrent_peers", {"sync", "torrentPeers"}}, + + {"login", {"auth", "login"}}, + {"logout", {"auth", "logout"}}, + + {"command/shutdown", {"app", "shutdown"}}, + {"query/preferences", {"app", "preferences"}}, + {"command/setPreferences", {"app", "setPreferences"}}, + {"command/getSavePath", {"app", "defaultSavePath"}}, + {"version/qbittorrent", {"app", "version"}}, + + {"query/getLog", {"log", "main"}}, + {"query/getPeerLog", {"log", "peers"}}, + + {"query/torrents", {"torrents", "info"}}, + {"query/propertiesGeneral", {"torrents", "properties"}}, + {"query/propertiesTrackers", {"torrents", "trackers"}}, + {"query/propertiesWebSeeds", {"torrents", "webseeds"}}, + {"query/propertiesFiles", {"torrents", "files"}}, + {"query/getPieceHashes", {"torrents", "pieceHashes"}}, + {"query/getPieceStates", {"torrents", "pieceStates"}}, + {"command/resume", {"torrents", "resume"}}, + {"command/pause", {"torrents", "pause"}}, + {"command/recheck", {"torrents", "recheck"}}, + {"command/resumeAll", {"torrents", "resume"}}, + {"command/pauseAll", {"torrents", "pause"}}, + {"command/rename", {"torrents", "rename"}}, + {"command/download", {"torrents", "add"}}, + {"command/upload", {"torrents", "add"}}, + {"command/delete", {"torrents", "delete"}}, + {"command/deletePerm", {"torrents", "delete"}}, + {"command/addTrackers", {"torrents", "addTrackers"}}, + {"command/setFilePrio", {"torrents", "filePrio"}}, + {"command/setCategory", {"torrents", "setCategory"}}, + {"command/addCategory", {"torrents", "createCategory"}}, + {"command/removeCategories", {"torrents", "removeCategories"}}, + {"command/getTorrentsUpLimit", {"torrents", "uploadLimit"}}, + {"command/getTorrentsDlLimit", {"torrents", "downloadLimit"}}, + {"command/setTorrentsUpLimit", {"torrents", "setUploadLimit"}}, + {"command/setTorrentsDlLimit", {"torrents", "setDownloadLimit"}}, + {"command/increasePrio", {"torrents", "increasePrio"}}, + {"command/decreasePrio", {"torrents", "decreasePrio"}}, + {"command/topPrio", {"torrents", "topPrio"}}, + {"command/bottomPrio", {"torrents", "bottomPrio"}}, + {"command/setLocation", {"torrents", "setLocation"}}, + {"command/setAutoTMM", {"torrents", "setAutoManagement"}}, + {"command/setSuperSeeding", {"torrents", "setSuperSeeding"}}, + {"command/setForceStart", {"torrents", "setForceStart"}}, + {"command/toggleSequentialDownload", {"torrents", "toggleSequentialDownload"}}, + {"command/toggleFirstLastPiecePrio", {"torrents", "toggleFirstLastPiecePrio"}}, + + {"query/transferInfo", {"transfer", "info"}}, + {"command/alternativeSpeedLimitsEnabled", {"transfer", "speedLimitsMode"}}, + {"command/toggleAlternativeSpeedLimits", {"transfer", "toggleSpeedLimitsMode"}}, + {"command/getGlobalUpLimit", {"transfer", "uploadLimit"}}, + {"command/getGlobalDlLimit", {"transfer", "downloadLimit"}}, + {"command/setGlobalUpLimit", {"transfer", "setUploadLimit"}}, + {"command/setGlobalDlLimit", {"transfer", "setDownloadLimit"}} + }; + + const QString legacyAction {match.captured(QLatin1String("action"))}; + const APICompatInfo compatInfo = APICompatMapping.value(legacyAction); + + scope = compatInfo.scope; + action = compatInfo.action; + + if (legacyAction == QLatin1String("command/delete")) + (*params)["deleteFiles"] = "false"; + else if (legacyAction == QLatin1String("command/deletePerm")) + (*params)["deleteFiles"] = "true"; + + const QString hash {match.captured(QLatin1String("hash"))}; + (*params)[QLatin1String("hash")] = hash; + + return true; + }; + + if (!findAPICall()) + findLegacyAPICall(); + + APIController *controller = m_apiControllers.value(scope); + if (!controller) { + if (request().path == QLatin1String("/version/api")) { + print(QString(COMPAT_API_VERSION), Http::CONTENT_TYPE_TXT); + return; } - } - if (partialSuccess) - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - else - print(QByteArray("Fails."), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_upload() -{ - CHECK_URI(0); - - const bool skipChecking = parseBool(request().posts.value("skip_checking"), false); - const bool seqDownload = parseBool(request().posts.value("sequentialDownload"), false); - const bool firstLastPiece = parseBool(request().posts.value("firstLastPiecePrio"), false); - const TriStateBool addPaused = parseTristatebool(request().posts.value("paused")); - const TriStateBool rootFolder = parseTristatebool(request().posts.value("root_folder")); - const QString savepath = request().posts.value("savepath").trimmed(); - const QString category = request().posts.value("category").trimmed(); - const QString torrentName = request().posts.value("rename").trimmed(); - const int upLimit = request().posts.value("upLimit").toInt(); - const int dlLimit = request().posts.value("dlLimit").toInt(); - - for (const Http::UploadedFile &torrent : request().files) { - const QString filePath = saveTmpFile(torrent.data); - if (filePath.isEmpty()) { - qWarning() << "I/O Error: Could not create temporary file"; - status(500, "Internal Server Error"); - print(QObject::tr("I/O Error: Could not create temporary file."), Http::CONTENT_TYPE_TXT); - continue; + if (request().path == QLatin1String("/version/api_min")) { + print(QString(COMPAT_API_VERSION_MIN), Http::CONTENT_TYPE_TXT); + return; } - const BitTorrent::TorrentInfo torrentInfo = BitTorrent::TorrentInfo::loadFromFile(filePath); - if (!torrentInfo.isValid()) { - status(415, "Unsupported Media Type"); - print(QObject::tr("Error: '%1' is not a valid torrent file.\n").arg(torrent.filename), Http::CONTENT_TYPE_TXT); + sendWebUIFile(); + } + else { + if (!session() && !isPublicAPI(scope, action)) + throw ForbiddenHTTPError(); + + DataMap data; + for (const Http::UploadedFile &torrent : request().files) + data[torrent.filename] = torrent.data; + + try { + const QVariant result = controller->run(action, *params, data); + switch (result.userType()) { + case QMetaType::QString: + print(result.toString(), Http::CONTENT_TYPE_TXT); + break; + case QMetaType::QJsonDocument: + print(result.toJsonDocument().toJson(QJsonDocument::Compact), Http::CONTENT_TYPE_JSON); + break; + default: + print(result.toString(), Http::CONTENT_TYPE_TXT); + break; + } } - else { - BitTorrent::AddTorrentParams params; - // TODO: Check if destination actually exists - params.skipChecking = skipChecking; - params.sequential = seqDownload; - params.firstLastPiecePriority = firstLastPiece; - params.addPaused = addPaused; - params.createSubfolder = rootFolder; - params.savePath = savepath; - params.category = category; - params.name = torrentName; - params.uploadLimit = (upLimit > 0) ? upLimit : -1; - params.downloadLimit = (dlLimit > 0) ? dlLimit : -1; - - if (!BitTorrent::Session::instance()->addTorrent(torrentInfo, params)) { - status(500, "Internal Server Error"); - print(QObject::tr("Error: Could not add torrent to session."), Http::CONTENT_TYPE_TXT); + catch (const APIError &error) { + // re-throw as HTTPError + switch (error.type()) { + case APIErrorType::AccessDenied: + throw ForbiddenHTTPError(error.message()); + case APIErrorType::BadData: + throw UnsupportedMediaTypeHTTPError(error.message()); + case APIErrorType::BadParams: + throw BadRequestHTTPError(error.message()); + case APIErrorType::Conflict: + throw ConflictHTTPError(error.message()); + case APIErrorType::NotFound: + throw NotFoundHTTPError(error.message()); + default: + Q_ASSERT(false); } } - // Clean up - Utils::Fs::forceRemove(filePath); } } -void WebApplication::action_command_addTrackers() +void WebApplication::configure() { - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "urls"); - QString hash = request().posts["hash"]; - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - QList trackers; - foreach (QString url, request().posts["urls"].split('\n')) { - url = url.trimmed(); - if (!url.isEmpty()) - trackers << url; - } - torrent->addTrackers(trackers); - } -} + const auto pref = Preferences::instance(); -void WebApplication::action_command_resumeAll() -{ - CHECK_URI(0); + m_domainList = Preferences::instance()->getServerDomains().split(';', QString::SkipEmptyParts); + std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); }); - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - torrent->resume(); + const QString rootFolder = Utils::Fs::expandPathAbs( + !pref->isAltWebUiEnabled() ? WWW_FOLDER : pref->getWebUiRootFolder()); + if (rootFolder != m_rootFolder) { + m_translatedFiles.clear(); + m_rootFolder = rootFolder; + } } -void WebApplication::action_command_pauseAll() +void WebApplication::registerAPIController(const QString &scope, APIController *controller) { - CHECK_URI(0); + Q_ASSERT(controller); + Q_ASSERT(!m_apiControllers.value(scope)); - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - torrent->pause(); + m_apiControllers[scope] = controller; } -void WebApplication::action_command_resume() +void WebApplication::declarePublicAPI(const QString &apiPath) { - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->resume(); + m_publicAPIs << apiPath; } -void WebApplication::action_command_pause() +void WebApplication::sendFile(const QString &path) { - CHECK_URI(0); - CHECK_PARAMETERS("hash"); + const QDateTime lastModified {QFileInfo(path).lastModified()}; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->pause(); -} + // find translated file in cache + auto it = m_translatedFiles.constFind(path); + if ((it != m_translatedFiles.constEnd()) && (lastModified <= (*it).lastModified)) { + print((*it).data, QMimeDatabase().mimeTypeForFileNameAndData(path, (*it).data).name()); + return; + } -void WebApplication::action_command_setPreferences() -{ - CHECK_URI(0); - CHECK_PARAMETERS("json"); - prefjson::setPreferences(request().posts["json"]); -} + QFile file {path}; + if (!file.open(QIODevice::ReadOnly)) { + qDebug("File %s was not found!", qUtf8Printable(path)); + throw NotFoundHTTPError(); + } + + if (file.size() > MAX_ALLOWED_FILESIZE) { + qWarning("%s: exceeded the maximum allowed file size!", qUtf8Printable(path)); + throw InternalServerErrorHTTPError(tr("Exceeded the maximum allowed file size (%1)!") + .arg(Utils::Misc::friendlyUnit(MAX_ALLOWED_FILESIZE))); + } -void WebApplication::action_command_setFilePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "id" << "priority"); - QString hash = request().posts["hash"]; - int fileID = request().posts["id"].toInt(); - int priority = request().posts["priority"].toInt(); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - - if (torrent && torrent->hasMetadata()) - torrent->setFilePriority(fileID, priority); -} + QByteArray data {file.readAll()}; + file.close(); -void WebApplication::action_command_getGlobalUpLimit() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->uploadSpeedLimit()), Http::CONTENT_TYPE_TXT); -} + const QMimeType type {QMimeDatabase().mimeTypeForFileNameAndData(path, data)}; + const bool isTranslatable {type.inherits(QLatin1String("text/plain"))}; -void WebApplication::action_command_getGlobalDlLimit() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->downloadSpeedLimit()), Http::CONTENT_TYPE_TXT); -} + // Translate the file + if (isTranslatable) { + QString dataStr {data}; + translateDocument(dataStr); + data = dataStr.toUtf8(); -void WebApplication::action_command_setGlobalUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; + m_translatedFiles[path] = {data, lastModified}; // caching translated file + } - BitTorrent::Session::instance()->setUploadSpeedLimit(limit); + print(data, type.name()); } -void WebApplication::action_command_setGlobalDlLimit() +Http::Response WebApplication::processRequest(const Http::Request &request, const Http::Environment &env) { - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - BitTorrent::Session::instance()->setDownloadSpeedLimit(limit); -} + m_currentSession = nullptr; + m_request = request; + m_env = env; -void WebApplication::action_command_getTorrentsUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - print(btjson::getTorrentsRatesLimits(hashes, false), Http::CONTENT_TYPE_JSON); -} + // clear response + clear(); -void WebApplication::action_command_getTorrentsDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - print(btjson::getTorrentsRatesLimits(hashes, true), Http::CONTENT_TYPE_JSON); -} + try { + // block cross-site requests + if (isCrossSiteRequest(m_request) || !validateHostHeader(m_domainList)) + throw UnauthorizedHTTPError(); -void WebApplication::action_command_setTorrentsUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "limit"); - - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) - limit = -1; - - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setUploadLimit(limit); + sessionInitialize(); + doProcessRequest(); } -} - -void WebApplication::action_command_setTorrentsDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "limit"); - - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) - limit = -1; - - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setDownloadLimit(limit); + catch (const HTTPError &error) { + status(error.statusCode(), error.statusText()); + if (!error.message().isEmpty()) + print(error.message(), Http::CONTENT_TYPE_TXT); } -} -void WebApplication::action_command_toggleAlternativeSpeedLimits() -{ - CHECK_URI(0); - BitTorrent::Session *const session = BitTorrent::Session::instance(); - session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled()); -} + // avoid clickjacking attacks + header(Http::HEADER_X_FRAME_OPTIONS, "SAMEORIGIN"); + header(Http::HEADER_X_XSS_PROTECTION, "1; mode=block"); + header(Http::HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff"); + header(Http::HEADER_CONTENT_SECURITY_POLICY, "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none';"); -void WebApplication::action_command_alternativeSpeedLimitsEnabled() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled()) - , Http::CONTENT_TYPE_TXT); + return response(); } -void WebApplication::action_command_toggleSequentialDownload() +QString WebApplication::clientId() const { - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->toggleSequentialDownload(); - } + return env().clientAddress.toString(); } -void WebApplication::action_command_toggleFirstLastPiecePrio() +void WebApplication::sessionInitialize() { - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->toggleFirstLastPiecePriority(); - } -} + Q_ASSERT(!m_currentSession); -void WebApplication::action_command_setSuperSeeding() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "value"); - - const bool value = parseBool(request().posts["value"], false); - const QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setSuperSeeding(value); - } -} + const QString sessionId {parseCookie(m_request.headers.value(QLatin1String("cookie"))).value(C_SID)}; -void WebApplication::action_command_setForceStart() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "value"); - - const bool value = parseBool(request().posts["value"], false); - const QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->resume(value); + // TODO: Additional session check + + if (!sessionId.isEmpty()) { + m_currentSession = m_sessions.value(sessionId); + if (m_currentSession) { + const uint now = QDateTime::currentDateTime().toTime_t(); + if ((now - m_currentSession->m_timestamp) > INACTIVE_TIME) { + // session is outdated - removing it + delete m_sessions.take(sessionId); + m_currentSession = nullptr; + } + else { + m_currentSession->updateTimestamp(); + } + } + else { + qDebug() << Q_FUNC_INFO << "session does not exist!"; + } } -} -void WebApplication::action_command_delete() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) - BitTorrent::Session::instance()->deleteTorrent(hash, false); + if (!m_currentSession && !isAuthNeeded()) + sessionStart(); } -void WebApplication::action_command_deletePerm() +QString WebApplication::generateSid() const { - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) - BitTorrent::Session::instance()->deleteTorrent(hash, true); -} + QString sid; -void WebApplication::action_command_increasePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); + do { + const size_t size = 6; + quint32 tmp[size]; - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; + for (size_t i = 0; i < size; ++i) + tmp[i] = Utils::Random::rand(); + + sid = QByteArray::fromRawData(reinterpret_cast(tmp), sizeof(quint32) * size).toBase64(); } + while (m_sessions.contains(sid)); - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->increaseTorrentsPriority(hashes); + return sid; } -void WebApplication::action_command_decreasePrio() +bool WebApplication::isAuthNeeded() { - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; - } - - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->decreaseTorrentsPriority(hashes); + qDebug("Checking auth rules against client address %s", qPrintable(m_env.clientAddress.toString())); + const Preferences *pref = Preferences::instance(); + if (!pref->isWebUiLocalAuthEnabled() && Utils::Net::isLoopbackAddress(m_env.clientAddress)) + return false; + if (pref->isWebUiAuthSubnetWhitelistEnabled() && Utils::Net::isIPInRange(m_env.clientAddress, pref->getWebUiAuthSubnetWhitelist())) + return false; + return true; } -void WebApplication::action_command_topPrio() +bool WebApplication::isPublicAPI(const QString &scope, const QString &action) const { - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; - } - - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->topTorrentsPriority(hashes); + return m_publicAPIs.contains(QString::fromLatin1("%1/%2").arg(scope).arg(action)); } -void WebApplication::action_command_bottomPrio() +void WebApplication::sessionStart() { - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); + Q_ASSERT(!m_currentSession); - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; + // remove outdated sessions + const uint now = QDateTime::currentDateTime().toTime_t(); + foreach (const auto session, m_sessions) { + if ((now - session->timestamp()) > INACTIVE_TIME) + delete m_sessions.take(session->id()); } - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->bottomTorrentsPriority(hashes); + m_currentSession = new WebSession(generateSid()); + m_sessions[m_currentSession->id()] = m_currentSession; + + QNetworkCookie cookie(C_SID, m_currentSession->id().toUtf8()); + cookie.setHttpOnly(true); + cookie.setPath(QLatin1String("/")); + header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); } -void WebApplication::action_command_setLocation() +void WebApplication::sessionEnd() { - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "location"); - - QStringList hashes = request().posts["hashes"].split("|"); - QString newLocation = request().posts["location"].trimmed(); + Q_ASSERT(m_currentSession); - // check if the location exists - if (newLocation.isEmpty() || !QDir(newLocation).exists()) - return; + QNetworkCookie cookie(C_SID); + cookie.setPath(QLatin1String("/")); + cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1)); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - Logger::instance()->addMessage(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"").arg(torrent->name()).arg(torrent->savePath()).arg(newLocation)); + delete m_sessions.take(m_currentSession->id()); + m_currentSession = nullptr; - torrent->move(Utils::Fs::expandPathAbs(newLocation)); - } - } + header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); } -void WebApplication::action_command_rename() +bool WebApplication::isCrossSiteRequest(const Http::Request &request) const { - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "name"); + // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers - QString hash = request().posts["hash"]; - QString name = request().posts["name"].trimmed(); + const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool + { + // [rfc6454] 5. Comparing Origins + return ((left.port() == right.port()) + // && (left.scheme() == right.scheme()) // not present in this context + && (left.host() == right.host())); + }; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent && !name.isEmpty()) { - name.replace(QRegularExpression("\r?\n|\r"), " "); - qDebug() << "Renaming" << torrent->name() << "to" << name; - torrent->setName(name); - } - else { - status(400, "Incorrect torrent hash or name"); + const QString targetOrigin = request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST)); + const QString originValue = request.headers.value(Http::HEADER_ORIGIN); + const QString refererValue = request.headers.value(Http::HEADER_REFERER); + + if (originValue.isEmpty() && refererValue.isEmpty()) { + // owasp.org recommends to block this request, but doing so will inevitably lead Web API users to spoof headers + // so lets be permissive here + return false; } -} -void WebApplication::action_command_setAutoTMM() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "enable"); - - const QStringList hashes = request().posts["hashes"].split('|'); - const bool isEnabled = parseBool(request().posts["enable"], false); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setAutoTMMEnabled(isEnabled); + // sent with CORS requests, as well as with POST requests + if (!originValue.isEmpty()) { + const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue); + if (isInvalid) + LogMsg(tr("WebUI: Origin header & Target origin mismatch! Source IP: '%1'. Origin header: '%2'. Target origin: '%3'") + .arg(m_env.clientAddress.toString()).arg(originValue).arg(targetOrigin) + , Log::WARNING); + return isInvalid; } -} -void WebApplication::action_command_recheck() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); + if (!refererValue.isEmpty()) { + const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue); + if (isInvalid) + LogMsg(tr("WebUI: Referer header & Target origin mismatch! Source IP: '%1'. Referer header: '%2'. Target origin: '%3'") + .arg(m_env.clientAddress.toString()).arg(refererValue).arg(targetOrigin) + , Log::WARNING); + return isInvalid; + } - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->forceRecheck(); + return true; } -void WebApplication::action_command_setCategory() +bool WebApplication::validateHostHeader(const QStringList &domains) const { - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "category"); - - QStringList hashes = request().posts["hashes"].split('|'); - QString category = request().posts["category"].trimmed(); - - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - if (!torrent->setCategory(category)) { - status(400, "Incorrect category name"); - return; - } - } + const QUrl hostHeader = urlFromHostHeader(m_request.headers[Http::HEADER_HOST]); + const QString requestHost = hostHeader.host(); + + // (if present) try matching host header's port with local port + const int requestPort = hostHeader.port(); + if ((requestPort != -1) && (m_env.localPort != requestPort)) { + LogMsg(tr("WebUI: Invalid Host header, port mismatch. Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'") + .arg(m_env.clientAddress.toString()).arg(m_env.localPort) + .arg(m_request.headers[Http::HEADER_HOST]) + , Log::WARNING); + return false; } -} -void WebApplication::action_command_addCategory() -{ - CHECK_URI(0); - CHECK_PARAMETERS("category"); + // try matching host header with local address +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + const bool sameAddr = m_env.localAddress.isEqual(QHostAddress(requestHost)); +#else + const auto equal = [](const Q_IPV6ADDR &l, const Q_IPV6ADDR &r) -> bool + { + for (int i = 0; i < 16; ++i) { + if (l[i] != r[i]) + return false; + } + return true; + }; + const bool sameAddr = equal(m_env.localAddress.toIPv6Address(), QHostAddress(requestHost).toIPv6Address()); +#endif - QString category = request().posts["category"].trimmed(); + if (sameAddr) + return true; - if (!BitTorrent::Session::isValidCategoryName(category) && !category.isEmpty()) { - status(400, tr("Incorrect category name")); - return; + // try matching host header with domain list + for (const auto &domain : domains) { + QRegExp domainRegex(domain, Qt::CaseInsensitive, QRegExp::Wildcard); + if (requestHost.contains(domainRegex)) + return true; } - BitTorrent::Session::instance()->addCategory(category); + LogMsg(tr("WebUI: Invalid Host header. Request source IP: '%1'. Received Host header: '%2'") + .arg(m_env.clientAddress.toString()).arg(m_request.headers[Http::HEADER_HOST]) + , Log::WARNING); + return false; } -void WebApplication::action_command_removeCategories() -{ - CHECK_URI(0); - CHECK_PARAMETERS("categories"); +// WebSession - QStringList categories = request().posts["categories"].split('\n'); - foreach (const QString &category, categories) - BitTorrent::Session::instance()->removeCategory(category); +WebSession::WebSession(const QString &sid) + : m_sid {sid} +{ + updateTimestamp(); } -void WebApplication::action_command_getSavePath() +QString WebSession::id() const { - CHECK_URI(0); - print(BitTorrent::Session::instance()->defaultSavePath()); + return m_sid; } -bool WebApplication::isPublicScope() +uint WebSession::timestamp() const { - return (scope_ == DEFAULT_SCOPE || scope_ == VERSION_INFO); + return m_timestamp; } -void WebApplication::doProcessRequest() +QVariant WebSession::getData(const QString &id) const { - scope_ = DEFAULT_SCOPE; - action_ = DEFAULT_ACTION; - - parsePath(); - - if (args_.contains(".") || args_.contains("..")) { - qDebug() << Q_FUNC_INFO << "Invalid path:" << request().path; - status(404, "Not Found"); - return; - } - - if (!isPublicScope() && !sessionActive()) { - status(403, "Forbidden"); - return; - } - - if (actions_.value(scope_).value(action_) != 0) { - (this->*(actions_[scope_][action_]))(); - } - else { - status(404, "Not Found"); - qDebug() << Q_FUNC_INFO << "Resource not found:" << request().path; - } + return m_data.value(id); } -void WebApplication::parsePath() +void WebSession::setData(const QString &id, const QVariant &data) { - if (request().path == "/") action_ = WEBUI_ACTION; - - // check action for requested path - QStringList pathItems = request().path.split('/', QString::SkipEmptyParts); - if (!pathItems.empty() && actions_.contains(pathItems.front())) { - scope_ = pathItems.front(); - pathItems.pop_front(); - } - - if (!pathItems.empty() && actions_[scope_].contains(pathItems.front())) { - action_ = pathItems.front(); - pathItems.pop_front(); - } - - args_ = pathItems; + m_data[id] = data; } -WebApplication::WebApplication(QObject *parent) - : AbstractWebApplication(parent) +void WebSession::updateTimestamp() { + m_timestamp = QDateTime::currentDateTime().toTime_t(); } - -QMap > WebApplication::actions_ = WebApplication::initializeActions(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 318e1ae7d..2d098f6cb 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2014, 2017 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,95 +26,118 @@ * exception statement from your version. */ -#ifndef WEBAPPLICATION_H -#define WEBAPPLICATION_H +#pragma once -#include -#include "abstractwebapplication.h" +#include +#include +#include +#include +#include +#include -class WebApplication : public AbstractWebApplication +#include "api/isessionmanager.h" +#include "base/http/irequesthandler.h" +#include "base/http/responsebuilder.h" +#include "base/http/types.h" +#include "base/utils/version.h" + +constexpr Utils::Version API_VERSION {2, 0, 0}; +constexpr int COMPAT_API_VERSION = 18; +constexpr int COMPAT_API_VERSION_MIN = 18; + +class APIController; +class WebApplication; + +constexpr char C_SID[] = "SID"; // name of session id cookie +constexpr int INACTIVE_TIME = 900; // Session inactive time (in secs = 15 min.) + +class WebSession : public ISession +{ + friend class WebApplication; + +public: + explicit WebSession(const QString &sid); + + QString id() const override; + uint timestamp() const; + + QVariant getData(const QString &id) const override; + void setData(const QString &id, const QVariant &data) override; + +private: + void updateTimestamp(); + + const QString m_sid; + uint m_timestamp; + QVariantHash m_data; +}; + +class WebApplication + : public QObject, public Http::IRequestHandler, public ISessionManager + , private Http::ResponseBuilder { + Q_OBJECT Q_DISABLE_COPY(WebApplication) +#ifndef Q_MOC_RUN +#define WEBAPI_PUBLIC +#define WEBAPI_PRIVATE +#endif + public: explicit WebApplication(QObject *parent = nullptr); + ~WebApplication() override; + + Http::Response processRequest(const Http::Request &request, const Http::Environment &env); + + QString clientId() const override; + WebSession *session() override; + void sessionStart() override; + void sessionEnd() override; + + const Http::Request &request() const; + const Http::Environment &env() const; private: - // Actions - void action_public_webui(); - void action_public_index(); - void action_public_login(); - void action_public_logout(); - void action_public_theme(); - void action_public_images(); - void action_query_torrents(); - void action_query_preferences(); - void action_query_transferInfo(); - void action_query_propertiesGeneral(); - void action_query_propertiesTrackers(); - void action_query_propertiesWebSeeds(); - void action_query_propertiesFiles(); - void action_query_getLog(); - void action_query_getPeerLog(); - void action_query_getPieceHashes(); - void action_query_getPieceStates(); - void action_sync_maindata(); - void action_sync_torrent_peers(); - void action_command_shutdown(); - void action_command_download(); - void action_command_upload(); - void action_command_addTrackers(); - void action_command_resumeAll(); - void action_command_pauseAll(); - void action_command_resume(); - void action_command_pause(); - void action_command_setPreferences(); - void action_command_setFilePrio(); - void action_command_getGlobalUpLimit(); - void action_command_getGlobalDlLimit(); - void action_command_setGlobalUpLimit(); - void action_command_setGlobalDlLimit(); - void action_command_getTorrentsUpLimit(); - void action_command_getTorrentsDlLimit(); - void action_command_setTorrentsUpLimit(); - void action_command_setTorrentsDlLimit(); - void action_command_alternativeSpeedLimitsEnabled(); - void action_command_toggleAlternativeSpeedLimits(); - void action_command_toggleSequentialDownload(); - void action_command_toggleFirstLastPiecePrio(); - void action_command_setSuperSeeding(); - void action_command_setForceStart(); - void action_command_delete(); - void action_command_deletePerm(); - void action_command_increasePrio(); - void action_command_decreasePrio(); - void action_command_topPrio(); - void action_command_bottomPrio(); - void action_command_setLocation(); - void action_command_rename(); - void action_command_setAutoTMM(); - void action_command_recheck(); - void action_command_setCategory(); - void action_command_addCategory(); - void action_command_removeCategories(); - void action_command_getSavePath(); - void action_version_api(); - void action_version_api_min(); - void action_version_qbittorrent(); - - typedef void (WebApplication::*Action)(); - - QString scope_; - QString action_; - QStringList args_; - - void doProcessRequest() override; - - bool isPublicScope(); - void parsePath(); - - static QMap > initializeActions(); - static QMap > actions_; -}; + void doProcessRequest(); + void configure(); + + void registerAPIController(const QString &scope, APIController *controller); + void declarePublicAPI(const QString &apiPath); + + void sendFile(const QString &path); + void sendWebUIFile(); -#endif // WEBAPPLICATION_H + // Session management + QString generateSid() const; + void sessionInitialize(); + bool isAuthNeeded(); + bool isPublicAPI(const QString &scope, const QString &action) const; + + bool isCrossSiteRequest(const Http::Request &request) const; + bool validateHostHeader(const QStringList &domains) const; + + // Persistent data + QMap m_sessions; + + // Current data + WebSession *m_currentSession = nullptr; + Http::Request m_request; + Http::Environment m_env; + + const QRegularExpression m_apiPathPattern {(QLatin1String("^/api/v2/(?[A-Za-z_][A-Za-z_0-9]*)/(?[A-Za-z_][A-Za-z_0-9]*)$"))}; + const QRegularExpression m_apiLegacyPathPattern {QLatin1String("^/(?((sync|control|query)/[A-Za-z_][A-Za-z_0-9]*|login|logout))(/(?[^/]+))?$")}; + + QHash m_apiControllers; + QSet m_publicAPIs; + bool m_isAltUIUsed = false; + QString m_rootFolder; + QStringList m_domainList; + + struct TranslatedFile + { + QByteArray data; + QDateTime lastModified; + }; + QMap m_translatedFiles; +}; diff --git a/src/webui/webui.h b/src/webui/webui.h index c552405e5..55ab815e2 100644 --- a/src/webui/webui.h +++ b/src/webui/webui.h @@ -42,7 +42,7 @@ namespace Net class DNSUpdater; } -class AbstractWebApplication; +class WebApplication; class WebUI : public QObject { @@ -64,7 +64,7 @@ private: bool m_isErrored; QPointer m_httpServer; QPointer m_dnsUpdater; - QPointer m_webapp; + QPointer m_webapp; quint16 m_port; }; diff --git a/src/webui/webui.pri b/src/webui/webui.pri index e07add1fd..8d27a8d48 100644 --- a/src/webui/webui.pri +++ b/src/webui/webui.pri @@ -1,17 +1,30 @@ HEADERS += \ - $$PWD/abstractwebapplication.h \ - $$PWD/btjson.h \ + $$PWD/api/apicontroller.h \ + $$PWD/api/apierror.h \ + $$PWD/api/appcontroller.h \ + $$PWD/api/authcontroller.h \ + $$PWD/api/isessionmanager.h \ + $$PWD/api/logcontroller.h \ + $$PWD/api/rsscontroller.h \ + $$PWD/api/synccontroller.h \ + $$PWD/api/torrentscontroller.h \ + $$PWD/api/transfercontroller.h \ + $$PWD/api/serialize/serialize_torrent.h \ $$PWD/extra_translations.h \ - $$PWD/jsonutils.h \ - $$PWD/prefjson.h \ $$PWD/webapplication.h \ - $$PWD/websessiondata.h \ $$PWD/webui.h SOURCES += \ - $$PWD/abstractwebapplication.cpp \ - $$PWD/btjson.cpp \ - $$PWD/prefjson.cpp \ + $$PWD/api/apicontroller.cpp \ + $$PWD/api/apierror.cpp \ + $$PWD/api/appcontroller.cpp \ + $$PWD/api/authcontroller.cpp \ + $$PWD/api/logcontroller.cpp \ + $$PWD/api/rsscontroller.cpp \ + $$PWD/api/synccontroller.cpp \ + $$PWD/api/torrentscontroller.cpp \ + $$PWD/api/transfercontroller.cpp \ + $$PWD/api/serialize/serialize_torrent.cpp \ $$PWD/webapplication.cpp \ $$PWD/webui.cpp diff --git a/src/webui/webui.qrc b/src/webui/webui.qrc index c1e481195..048ff7909 100644 --- a/src/webui/webui.qrc +++ b/src/webui/webui.qrc @@ -1,47 +1,49 @@ + www/private/css/Core.css + www/private/css/dynamicTable.css + www/private/css/Layout.css + www/private/css/style.css + www/private/css/Tabs.css + www/private/css/Window.css + www/private/scripts/client.js + www/private/scripts/clipboard.min.js + www/private/scripts/contextmenu.js + www/private/scripts/download.js + www/private/scripts/dynamicTable.js + www/private/scripts/excanvas-compressed.js + www/private/scripts/misc.js + www/private/scripts/mocha.js + www/private/scripts/mocha-init.js + www/private/scripts/mocha-yc.js + www/private/scripts/mootools-1.2-core-yc.js + www/private/scripts/mootools-1.2-more.js + www/private/scripts/parametrics.js + www/private/scripts/progressbar.js + www/private/scripts/prop-files.js + www/private/scripts/prop-general.js + www/private/scripts/prop-trackers.js + www/private/scripts/prop-webseeds.js + www/private/about.html + www/private/addtrackers.html + www/private/confirmdeletion.html + www/private/download.html + www/private/downloadlimit.html + www/private/filters.html www/private/index.html - www/private/login.html - www/public/about.html - www/public/addtrackers.html - www/public/confirmdeletion.html - www/public/css/Core.css - www/public/css/dynamicTable.css - www/public/css/Layout.css + www/private/newcategory.html + www/private/preferences.html + www/private/preferences_content.html + www/private/properties.html + www/private/properties_content.html + www/private/rename.html + www/private/setlocation.html + www/private/statistics.html + www/private/transferlist.html + www/private/upload.html + www/private/uploadlimit.html www/public/css/style.css - www/public/css/Tabs.css - www/public/css/Window.css - www/public/download.html - www/public/downloadlimit.html - www/public/filters.html - www/public/newcategory.html - www/public/preferences.html - www/public/preferences_content.html - www/public/properties.html - www/public/properties_content.html - www/public/rename.html - www/public/scripts/client.js - www/public/scripts/clipboard.min.js - www/public/scripts/contextmenu.js - www/public/scripts/download.js - www/public/scripts/dynamicTable.js - www/public/scripts/excanvas-compressed.js - www/public/scripts/misc.js - www/public/scripts/mocha-init.js - www/public/scripts/mocha-yc.js - www/public/scripts/mocha.js www/public/scripts/mootools-1.2-core-yc.js - www/public/scripts/mootools-1.2-more.js - www/public/scripts/parametrics.js - www/public/scripts/progressbar.js - www/public/scripts/prop-files.js - www/public/scripts/prop-general.js - www/public/scripts/prop-trackers.js - www/public/scripts/prop-webseeds.js - www/public/setlocation.html - www/public/statistics.html - www/public/transferlist.html - www/public/upload.html - www/public/uploadlimit.html + www/public/login.html diff --git a/src/webui/www/public/about.html b/src/webui/www/private/about.html similarity index 100% rename from src/webui/www/public/about.html rename to src/webui/www/private/about.html diff --git a/src/webui/www/public/addtrackers.html b/src/webui/www/private/addtrackers.html similarity index 87% rename from src/webui/www/public/addtrackers.html rename to src/webui/www/private/addtrackers.html index 0d9d958db..a12141ff2 100644 --- a/src/webui/www/public/addtrackers.html +++ b/src/webui/www/private/addtrackers.html @@ -13,9 +13,12 @@ new Event(e).stop(); var hash = new URI().getData('hash'); new Request({ - url: 'command/addTrackers', + url: 'api/v2/torrents/addTrackers', method: 'post', - data: {hash: hash, urls: $('trackersUrls').value}, + data: { + hash: hash, + urls: $('trackersUrls').value + }, onComplete: function() { window.parent.closeWindows(); } diff --git a/src/webui/www/public/confirmdeletion.html b/src/webui/www/private/confirmdeletion.html similarity index 89% rename from src/webui/www/public/confirmdeletion.html rename to src/webui/www/private/confirmdeletion.html index 420cf6855..2f5e9faea 100644 --- a/src/webui/www/public/confirmdeletion.html +++ b/src/webui/www/private/confirmdeletion.html @@ -17,14 +17,14 @@ $('confirmBtn').addEvent('click', function(e){ parent.torrentsTable.deselectAll(); new Event(e).stop(); - var cmd = 'command/delete'; - if($('deleteFromDiskCB').get('checked')) - cmd = 'command/deletePerm'; + var cmd = 'api/v2/torrents/delete'; + var deleteFiles = $('deleteFromDiskCB').get('checked'); new Request({ url: cmd, method: 'post', data: { - 'hashes': hashes.join('|') + 'hashes': hashes.join('|'), + 'deleteFiles': deleteFiles }, onComplete: function() { window.parent.closeWindows(); diff --git a/src/webui/www/public/css/Core.css b/src/webui/www/private/css/Core.css similarity index 100% rename from src/webui/www/public/css/Core.css rename to src/webui/www/private/css/Core.css diff --git a/src/webui/www/public/css/Layout.css b/src/webui/www/private/css/Layout.css similarity index 100% rename from src/webui/www/public/css/Layout.css rename to src/webui/www/private/css/Layout.css diff --git a/src/webui/www/public/css/Tabs.css b/src/webui/www/private/css/Tabs.css similarity index 100% rename from src/webui/www/public/css/Tabs.css rename to src/webui/www/private/css/Tabs.css diff --git a/src/webui/www/public/css/Window.css b/src/webui/www/private/css/Window.css similarity index 100% rename from src/webui/www/public/css/Window.css rename to src/webui/www/private/css/Window.css diff --git a/src/webui/www/public/css/dynamicTable.css b/src/webui/www/private/css/dynamicTable.css similarity index 100% rename from src/webui/www/public/css/dynamicTable.css rename to src/webui/www/private/css/dynamicTable.css diff --git a/src/webui/www/private/css/style.css b/src/webui/www/private/css/style.css new file mode 100644 index 000000000..f8c19ac85 --- /dev/null +++ b/src/webui/www/private/css/style.css @@ -0,0 +1,481 @@ +/* Reset */ + +/*ul,ol,dl,li,dt,dd,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,fieldset,input,object,iframe { margin: 0; padding: 0; }*/ +a img,:link img,:visited img { border: none; } +/*table { border-collapse: collapse; border-spacing: 0; }*/ +:focus { outline: none; } + +/* Structure */ + +body { + margin: 0; + text-align: left; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 18px; + color: #555; +} + +.aside { + width: 300px; +} + +.invisible { + display: none; +} + +/* Typography */ + +h2, h3, h4 { + margin: 0; + padding: 0 0 5px 0; + font-size: 12px; + font-weight: bold; + color: #333; +} + +h2 { + font-size: 14px; + color: #555; + font-weight: bold; +} + +#mochaPage h3 { + display: block; + font-size: 12px; + padding: 6px 0 6px 0; + margin: 0 0 8px 0; + border-bottom: 1px solid #bbb; +} + +#error_div { + color: #f00; + font-weight: bold; +} + +h4 { + font-size: 11px; +} + +a { + color: #e60; + text-decoration: none; + cursor: pointer; +} + +a:hover { + text-decoration: none; +} + +p { + margin: 0; + padding: 0 0 9px 0; +} + +/* List Elements */ + +ul { + list-style: outside; + margin: 0 0 9px 16px; +} + +dt { + font-weight: bold; +} + +dd { + padding: 0 0 9px 0; +} + +/* Code */ + +pre { + background-color: #f6f6f6; + color: #006600; + display: block; + font-family: 'Courier New', Courier, monospace; + font-size: 11px; + max-height: 250px; + overflow: auto; + margin: 0 0 10px 0; + padding: 10px; + border: 1px solid #d1d7dc; + } + +/* Dividers */ + +hr { + background-color: #ddd; + color: #ccc; + height: 1px; + border: 0px; +} + +.vcenter { + vertical-align: middle; +} + +#urls { + width:90%; + height:100%; +} + +#trackersUrls { + width:90%; + height:100%; +} + +#Filters ul { + list-style-type: none; +} + +#Filters ul li { + margin-left: -16px; +} + +#Filters ul img { + padding: 2px 4px; + vertical-align: middle; + width: 16px; + height: 16px; +} + +.selectedFilter { + background-color: #415A8D; + color: #FFFFFF; +} + +.selectedFilter a { + color: #FFFFFF; +} + +#properties { + background-color: #e5e5e5; +} + +a.propButton { + border: 1px solid rgb(85, 81, 91); + /*border-radius: 3px;*/ + padding: 2px; + margin-left: 3px; + margin-right: 3px; +} + +a.propButton img { + margin-bottom: -4px; +} + +.scrollableMenu { + overflow-y: auto; + overflow-x: hidden; +} + +/* context menu specific */ + +.contextMenu { border:1px solid #999; padding:0; background:#eee; list-style-type:none; display:none;} +.contextMenu .separator { border-top:1px solid #999; } +.contextMenu li { margin:0; padding:0;} +.contextMenu li a { + display: block; + padding: 5px 20px 5px 5px; + font-size: 12px; + text-decoration: none; + font-family: tahoma,arial,sans-serif; + color: #000; + white-space: nowrap; +} +.contextMenu li a:hover { background-color:#ddd; } +.contextMenu li a.disabled { color:#ccc; font-style:italic; } +.contextMenu li a.disabled:hover { background-color:#eee; } +.contextMenu li ul { + padding: 0; + border:1px solid #999; padding:0; background:#eee; + list-style-type:none; + position: absolute; + left: -999em; + z-index: 8000; + margin: -29px 0 0 100%; + width: 164px; +} +.contextMenu li ul li a { + position: relative; +} +.contextMenu li a.arrow-right, .contextMenu li a:hover.arrow-right { + background-image: url(../images/skin/arrow-right.gif); + background-repeat: no-repeat; + background-position: right center; +} +.contextMenu li:hover ul, +.contextMenu li.ieHover ul, +.contextMenu li li.ieHover ul, +.contextMenu li li li.ieHover ul, +.contextMenu li li:hover ul, +.contextMenu li li li:hover ul { /* lists nested under hovered list items */ + left: auto; +} + +.contextMenu li img { + width: 16px; + height: 16px; + margin-bottom: -4px; + -ms-interpolation-mode : bicubic; +} + +/* Sliders */ + +.slider { + clear: both; + position: relative; + font-size: 12px; + font-weight: bold; + width: 400px; + margin-bottom: 15px; +} + +.sliderWrapper { + position: relative; + font-size: 1px; + line-height: 1px; + height: 9px; + width: 422px; +} + +.sliderarea { + position: absolute; + top: 0; + left: 0; + height: 7px; + width: 420px; + font-size: 1px; + line-height: 1px; + background: #f2f2f2 url(../images/skin/slider-area.gif) repeat-x; + border: 1px solid #a3a3a3; + border-bottom: 1px solid #ccc; + border-left: 1px solid #ccc; + margin: 0; + padding: 0; + overflow: hidden; +} + +.sliderknob { + position: absolute; + top: 0; + left: 0; + height: 9px; + width: 19px; + font-size: 1px; + line-height: 1px; + background: url(../images/skin/knob.gif) no-repeat; + cursor: pointer; + overflow: hidden; + z-index: 2; +} + +.update { + padding-bottom: 5px; +} + +.mochaToolButton { + margin-right: 10px; +} + +/* Mocha Customization */ +#mochaToolbar { + margin-top: 5px; +} + +#mochaToolbar .divider { + background-image: url(../images/skin/toolbox-divider.gif); + background-repeat: no-repeat; + background-position: left center; + padding-left: 14px; + padding-top: 15px; +} + +.MyMenuIcon { + margin-left: -18px; + margin-bottom: -3px; + padding-right: 5px; +} + +/* Tri-state checkbox */ + +label.tristate { + background: url(../images/3-state-checkbox.gif) 0 0 no-repeat; + display: block; + float: left; + height: 13px; + margin: .15em 8px 5px 0px; + overflow: hidden; + text-indent: -999em; + width: 13px; +} + +label.checked { + background-position: 0 -13px; +} + +label.partial { + background-position: 0 -26px; +} + +fieldset.settings { + border: solid 1px black; + border-radius: 8px; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + padding: 4px 4px 4px 10px; +} + +fieldset.settings legend { + margin-left: 8px; + padding: 4px; + font-weight: bold; +} + +fieldset.settings label { + padding: 2px; +} + +fieldset.settings .leftLabelSmall { + width: 5em; + float: left; + text-align: right; + margin-right: 0.5em; + display: block; +} + +fieldset.settings .leftLabelLarge { + width: 14em; + float: left; + text-align: right; + margin-right: 0.5em; + display: block; +} + +div.formRow { + clear: left; + display: block; +} + +.filterTitle { + font-weight: bold; + text-transform: uppercase; + padding-left: 5px; +} + +ul.filterList { + margin: 0 0 0 16px; + padding-left: 0; +} + +ul.filterList a { + display: block; +} + +ul.filterList li:hover { + background-color: #e60; +} + +ul.filterList li:hover a { + color: white; +} + +td.generalLabel { + white-space: nowrap; + text-align: right; + width: 1px; + vertical-align: top; +} + +#filesTable { + line-height: 20px; +} + +#trackersTable, #webseedsTable { + line-height: 25px; +} + +#addTrackersPlus { + width: 16px; + cursor: pointer; + margin-bottom: -3px; +} + +.unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +#prop_general { + padding: 2px; +} + +#watched_folders_tab { + border-collapse: collapse; +} + +#watched_folders_tab td, #watched_folders_tab th { + padding: 2px 4px; + border: 1px solid black; +} + +.select-watched-folder-editable { + position:relative; + background-color: white; + border: solid grey 1px; + width: 160px; + height: 20px; +} + +.select-watched-folder-editable select { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + border: none; + width: 160px; + margin: 0; +} + +.select-watched-folder-editable input { + position: absolute; + top: 0px; + left: 0px; + width: 140px; + padding: 1px; + border: none; +} + +.select-watched-folder-editable select:focus, .select-editable input:focus { + outline: none; +} + +/* + * Workaround to prevent the transfer list from + * disappearing when zooming in the browser. + */ +#filtersColumn_handle { + margin-left: -1px; +} + +#error_div { + float: left; + font-size: 14px; +} + +.combo_priority { + font-size: 1em; +} + +td.statusBarSeparator { + width: 22px; + background-image: url('../images/skin/toolbox-divider.gif'); + background-repeat: no-repeat; + background-position: center 1px; + background-size: 2px 18px; +} diff --git a/src/webui/www/public/download.html b/src/webui/www/private/download.html similarity index 96% rename from src/webui/www/public/download.html rename to src/webui/www/private/download.html index 09c0b10bc..c42cb1f46 100644 --- a/src/webui/www/public/download.html +++ b/src/webui/www/private/download.html @@ -10,7 +10,7 @@ -
+

QBT_TR(Download Torrents from their URLs or Magnet links)QBT_TR[CONTEXT=HttpServer]

diff --git a/src/webui/www/public/downloadlimit.html b/src/webui/www/private/downloadlimit.html similarity index 95% rename from src/webui/www/public/downloadlimit.html rename to src/webui/www/private/downloadlimit.html index ddad8d6c0..0f8baa3e4 100644 --- a/src/webui/www/public/downloadlimit.html +++ b/src/webui/www/private/downloadlimit.html @@ -25,7 +25,7 @@ var limit = $("dllimitUpdatevalue").value.toInt() * 1024; if (hashes[0] == "global") { new Request({ - url: 'command/setGlobalDlLimit', + url: 'api/v2/transfer/setDownloadLimit', method: 'post', data: { 'limit': limit @@ -38,7 +38,7 @@ } else { new Request({ - url: 'command/setTorrentsDlLimit', + url: 'api/v2/torrents/setDownloadLimit', method: 'post', data: { 'hashes': hashes.join('|'), diff --git a/src/webui/www/public/filters.html b/src/webui/www/private/filters.html similarity index 100% rename from src/webui/www/public/filters.html rename to src/webui/www/private/filters.html diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index 9c8d85f32..d3b90928a 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -4,6 +4,7 @@ qBittorrent ${VERSION} QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog] + diff --git a/src/webui/www/public/newcategory.html b/src/webui/www/private/newcategory.html similarity index 96% rename from src/webui/www/public/newcategory.html rename to src/webui/www/private/newcategory.html index 4f88be8ec..242821149 100644 --- a/src/webui/www/public/newcategory.html +++ b/src/webui/www/private/newcategory.html @@ -33,7 +33,7 @@ var hashesList = new URI().getData('hashes'); if (!hashesList) { new Request({ - url: 'command/addCategory', + url: 'api/v2/torrents/createCategory', method: 'post', data: { category: categoryName @@ -46,7 +46,7 @@ else { new Request({ - url: 'command/setCategory', + url: 'api/v2/torrents/setCategory', method: 'post', data: { hashes: hashesList, diff --git a/src/webui/www/public/preferences.html b/src/webui/www/private/preferences.html similarity index 100% rename from src/webui/www/public/preferences.html rename to src/webui/www/private/preferences.html diff --git a/src/webui/www/public/preferences_content.html b/src/webui/www/private/preferences_content.html similarity index 99% rename from src/webui/www/public/preferences_content.html rename to src/webui/www/private/preferences_content.html index 14a7394f1..7290bb5bf 100644 --- a/src/webui/www/public/preferences_content.html +++ b/src/webui/www/private/preferences_content.html @@ -826,7 +826,7 @@ time_padding = function(val) { } loadPreferences = function() { - var url = 'query/preferences'; + var url = 'api/v2/app/preferences'; var request = new Request.JSON({ url: url, method: 'get', @@ -1374,18 +1374,19 @@ applyPreferences = function() { // Send it to qBT var json_str = JSON.encode(settings); - new Request({url: 'command/setPreferences', + new Request({url: 'api/v2/app/setPreferences', method: 'post', - data: {'json': json_str, - }, - onFailure: function() { - alert("QBT_TR(Unable to save program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]"); - window.parent.closeWindows(); - }, + data: { + 'json': json_str, + }, + onFailure: function() { + alert("QBT_TR(Unable to save program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]"); + window.parent.closeWindows(); + }, onSuccess: function() { - // Close window - window.parent.location.reload(); - window.parent.closeWindows(); + // Close window + window.parent.location.reload(); + window.parent.closeWindows(); } }).send(); }; diff --git a/src/webui/www/public/properties.html b/src/webui/www/private/properties.html similarity index 100% rename from src/webui/www/public/properties.html rename to src/webui/www/private/properties.html diff --git a/src/webui/www/public/properties_content.html b/src/webui/www/private/properties_content.html similarity index 100% rename from src/webui/www/public/properties_content.html rename to src/webui/www/private/properties_content.html diff --git a/src/webui/www/public/rename.html b/src/webui/www/private/rename.html similarity index 97% rename from src/webui/www/public/rename.html rename to src/webui/www/private/rename.html index b6d02068f..b6f3ee3e9 100644 --- a/src/webui/www/public/rename.html +++ b/src/webui/www/private/rename.html @@ -36,7 +36,7 @@ var hash = new URI().getData('hash'); if (hash) { new Request({ - url: 'command/rename', + url: 'api/v2/torrents/rename', method: 'post', data: { hash: hash, diff --git a/src/webui/www/public/scripts/client.js b/src/webui/www/private/scripts/client.js similarity index 99% rename from src/webui/www/public/scripts/client.js rename to src/webui/www/private/scripts/client.js index 42d1bd2d4..edd0db80a 100644 --- a/src/webui/www/public/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -273,7 +273,7 @@ window.addEvent('load', function () { var syncMainDataTimer; var syncMainData = function () { - var url = new URI('sync/maindata'); + var url = new URI('api/v2/sync/maindata'); url.setData('rid', syncMainDataLastResponseId); var request = new Request.JSON({ url : url, @@ -445,7 +445,7 @@ window.addEvent('load', function () { // Change icon immediately to give some feedback updateAltSpeedIcon(!alternativeSpeedLimits); - new Request({url: 'command/toggleAlternativeSpeedLimits', + new Request({url: 'api/v2/transfer/toggleSpeedLimitsMode', method: 'post', onComplete: function() { alternativeSpeedLimits = !alternativeSpeedLimits; @@ -665,7 +665,7 @@ var loadTorrentPeersData = function(){ loadTorrentPeersTimer = loadTorrentPeersData.delay(syncMainDataTimerPeriod); return; } - var url = new URI('sync/torrent_peers'); + var url = new URI('api/v2/sync/torrentPeers'); url.setData('rid', syncTorrentPeersLastResponseId); url.setData('hash', current_hash); var request = new Request.JSON({ diff --git a/src/webui/www/public/scripts/clipboard.min.js b/src/webui/www/private/scripts/clipboard.min.js similarity index 100% rename from src/webui/www/public/scripts/clipboard.min.js rename to src/webui/www/private/scripts/clipboard.min.js diff --git a/src/webui/www/public/scripts/contextmenu.js b/src/webui/www/private/scripts/contextmenu.js similarity index 100% rename from src/webui/www/public/scripts/contextmenu.js rename to src/webui/www/private/scripts/contextmenu.js diff --git a/src/webui/www/public/scripts/download.js b/src/webui/www/private/scripts/download.js similarity index 97% rename from src/webui/www/public/scripts/download.js rename to src/webui/www/private/scripts/download.js index 6ca0ce1c8..92e9ef742 100644 --- a/src/webui/www/public/scripts/download.js +++ b/src/webui/www/private/scripts/download.js @@ -23,7 +23,7 @@ getSavePath = function() { var req = new Request({ - url: 'command/getSavePath', + url: 'api/v2/app/defaultSavePath', method: 'get', noCache: true, onFailure: function() { diff --git a/src/webui/www/public/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js similarity index 100% rename from src/webui/www/public/scripts/dynamicTable.js rename to src/webui/www/private/scripts/dynamicTable.js diff --git a/src/webui/www/public/scripts/excanvas-compressed.js b/src/webui/www/private/scripts/excanvas-compressed.js similarity index 100% rename from src/webui/www/public/scripts/excanvas-compressed.js rename to src/webui/www/private/scripts/excanvas-compressed.js diff --git a/src/webui/www/public/scripts/misc.js b/src/webui/www/private/scripts/misc.js similarity index 100% rename from src/webui/www/public/scripts/misc.js rename to src/webui/www/private/scripts/misc.js diff --git a/src/webui/www/public/scripts/mocha-init.js b/src/webui/www/private/scripts/mocha-init.js similarity index 90% rename from src/webui/www/public/scripts/mocha-init.js rename to src/webui/www/private/scripts/mocha-init.js index 9788189f5..aec14933b 100644 --- a/src/webui/www/public/scripts/mocha-init.js +++ b/src/webui/www/private/scripts/mocha-init.js @@ -142,7 +142,7 @@ initializeWindows = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { new Request({ - url: 'command/toggleSequentialDownload', + url: 'api/v2/toggleSequentialDownload', method: 'post', data: { hashes: hashes.join("|") @@ -156,7 +156,7 @@ initializeWindows = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { new Request({ - url: 'command/toggleFirstLastPiecePrio', + url: 'api/v2/toggleFirstLastPiecePrio', method: 'post', data: { hashes: hashes.join("|") @@ -170,7 +170,7 @@ initializeWindows = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { new Request({ - url: 'command/setSuperSeeding', + url: 'api/v2/torrents/setSuperSeeding', method: 'post', data: { value: val, @@ -185,7 +185,7 @@ initializeWindows = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { new Request({ - url: 'command/setForceStart', + url: 'api/v2/torrents/setForceStart', method: 'post', data: { value: 'true', @@ -274,15 +274,13 @@ initializeWindows = function() { pauseFN = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { - hashes.each(function(hash, index) { - new Request({ - url: 'command/pause', - method: 'post', - data: { - hash: hash - } - }).send(); - }); + new Request({ + url: 'api/v2/torrents/pause', + method: 'post', + data: { + hashes: hashes.join("|") + } + }).send(); updateMainData(); } }; @@ -290,15 +288,13 @@ initializeWindows = function() { startFN = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { - hashes.each(function(hash, index) { - new Request({ - url: 'command/resume', - method: 'post', - data: { - hash: hash - } - }).send(); - }); + new Request({ + url: 'api/v2/torrents/resume', + method: 'post', + data: { + hashes: hashes.join("|") + } + }).send(); updateMainData(); } }; @@ -313,7 +309,7 @@ initializeWindows = function() { enable = true; }); new Request({ - url: 'command/setAutoTMM', + url: 'api/v2/torrents/setAutoManagement', method: 'post', data: { hashes: hashes.join("|"), @@ -329,10 +325,10 @@ initializeWindows = function() { if (hashes.length) { hashes.each(function(hash, index) { new Request({ - url: 'command/recheck', + url: 'api/v2/torrents/recheck', method: 'post', data: { - hash: hash + hashes: hash } }).send(); }); @@ -409,7 +405,7 @@ initializeWindows = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { new Request({ - url: 'command/setCategory', + url: 'api/v2/torrents/setCategory', method: 'post', data: { hashes: hashes.join("|"), @@ -439,7 +435,7 @@ initializeWindows = function() { removeCategoryFN = function (categoryHash) { var categoryName = category_list[categoryHash].name; new Request({ - url: 'command/removeCategories', + url: 'api/v2/torrents/removeCategories', method: 'post', data: { categories: categoryName @@ -455,7 +451,7 @@ initializeWindows = function() { categories.push(category_list[hash].name); } new Request({ - url: 'command/removeCategories', + url: 'api/v2/torrents/removeCategories', method: 'post', data: { categories: categories.join('\n') @@ -467,15 +463,13 @@ initializeWindows = function() { startTorrentsByCategoryFN = function (categoryHash) { var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash); if (hashes.length) { - hashes.each(function (hash, index) { - new Request({ - url: 'command/resume', - method: 'post', - data: { - hash: hash - } - }).send(); - }); + new Request({ + url: 'api/v2/torrents/resume', + method: 'post', + data: { + hashes: hashes.join("|") + } + }).send(); updateMainData(); } }; @@ -483,15 +477,13 @@ initializeWindows = function() { pauseTorrentsByCategoryFN = function (categoryHash) { var hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash); if (hashes.length) { - hashes.each(function (hash, index) { - new Request({ - url: 'command/pause', - method: 'post', - data: { - hash: hash - } - }).send(); - }); + new Request({ + url: 'api/v2/torrents/pause', + method: 'post', + data: { + hashes: hashes.join("|") + } + }).send(); updateMainData(); } }; @@ -545,11 +537,15 @@ initializeWindows = function() { return torrentsTable.selectedRowsIds().join("\n"); }; - ['pauseAll', 'resumeAll'].each(function(item) { - addClickEvent(item, function(e) { + ['pause', 'resume'].each(function(item) { + addClickEvent(item + 'All', function(e) { new Event(e).stop(); new Request({ - url: 'command/' + item + url: 'api/v2/torrents/' + item, + method: 'post', + data: { + hashes: "all" + } }).send(); updateMainData(); }); @@ -562,10 +558,10 @@ initializeWindows = function() { if (hashes.length) { hashes.each(function(hash, index) { new Request({ - url: 'command/' + item, + url: 'api/v2/torrents/' + item, method: 'post', data: { - hash: hash + hashes: hash } }).send(); }); @@ -574,7 +570,7 @@ initializeWindows = function() { }); }); - ['decreasePrio', 'increasePrio', 'topPrio', 'bottomPrio'].each(function(item) { + ['decrease_prio', 'increase_prio', 'top_prio', 'bottom_prio'].each(function(item) { addClickEvent(item, function(e) { new Event(e).stop(); setPriorityFN(item); @@ -585,7 +581,7 @@ initializeWindows = function() { var hashes = torrentsTable.selectedRowsIds(); if (hashes.length) { new Request({ - url: 'command/' + cmd, + url: 'api/v2/torrents/' + cmd, method: 'post', data: { hashes: hashes.join("|") @@ -611,7 +607,7 @@ initializeWindows = function() { addClickEvent('logout', function(e) { new Event(e).stop(); new Request({ - url: 'logout', + url: 'api/v2/auth/logout', method: 'post', onSuccess: function() { window.location.reload(); @@ -623,7 +619,7 @@ initializeWindows = function() { new Event(e).stop(); if (confirm('QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]')) { new Request({ - url: 'command/shutdown', + url: 'api/v2/app/shutdown', onSuccess: function() { document.write("QBT_TR(qBittorrent has been shutdown.)QBT_TR[CONTEXT=HttpServer]

QBT_TR(qBittorrent has been shutdown.)QBT_TR[CONTEXT=HttpServer]

"); stop(); diff --git a/src/webui/www/public/scripts/mocha-yc.js b/src/webui/www/private/scripts/mocha-yc.js similarity index 100% rename from src/webui/www/public/scripts/mocha-yc.js rename to src/webui/www/private/scripts/mocha-yc.js diff --git a/src/webui/www/public/scripts/mocha.js b/src/webui/www/private/scripts/mocha.js similarity index 100% rename from src/webui/www/public/scripts/mocha.js rename to src/webui/www/private/scripts/mocha.js diff --git a/src/webui/www/private/scripts/mootools-1.2-core-yc.js b/src/webui/www/private/scripts/mootools-1.2-core-yc.js new file mode 100644 index 000000000..288f2a8d4 --- /dev/null +++ b/src/webui/www/private/scripts/mootools-1.2-core-yc.js @@ -0,0 +1,527 @@ +/* +--- +MooTools: the javascript framework + +web build: + - http://mootools.net/core/76bf47062d6c1983d66ce47ad66aa0e0 + +packager build: + - packager build Core/Core Core/Array Core/String Core/Number Core/Function Core/Object Core/Event Core/Browser Core/Class Core/Class.Extras Core/Slick.Parser Core/Slick.Finder Core/Element Core/Element.Style Core/Element.Event Core/Element.Delegation Core/Element.Dimensions Core/Fx Core/Fx.CSS Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request Core/Request.HTML Core/Request.JSON Core/Cookie Core/JSON Core/DOMReady Core/Swiff + +copyrights: + - [MooTools](http://mootools.net) + +licenses: + - [MIT License](http://mootools.net/license.txt) +... +*/ + +(function(){this.MooTools={version:"1.4.5",build:"ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0"};var e=this.typeOf=function(i){if(i==null){return"null";}if(i.$family!=null){return i.$family(); +}if(i.nodeName){if(i.nodeType==1){return"element";}if(i.nodeType==3){return(/\S/).test(i.nodeValue)?"textnode":"whitespace";}}else{if(typeof i.length=="number"){if(i.callee){return"arguments"; +}if("item" in i){return"collection";}}}return typeof i;};var u=this.instanceOf=function(w,i){if(w==null){return false;}var v=w.$constructor||w.constructor; +while(v){if(v===i){return true;}v=v.parent;}if(!w.hasOwnProperty){return false;}return w instanceof i;};var f=this.Function;var r=true;for(var q in {toString:1}){r=null; +}if(r){r=["hasOwnProperty","valueOf","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","constructor"];}f.prototype.overloadSetter=function(v){var i=this; +return function(x,w){if(x==null){return this;}if(v||typeof x!="string"){for(var y in x){i.call(this,y,x[y]);}if(r){for(var z=r.length;z--;){y=r[z];if(x.hasOwnProperty(y)){i.call(this,y,x[y]); +}}}}else{i.call(this,x,w);}return this;};};f.prototype.overloadGetter=function(v){var i=this;return function(x){var y,w;if(typeof x!="string"){y=x;}else{if(arguments.length>1){y=arguments; +}else{if(v){y=[x];}}}if(y){w={};for(var z=0;z>>0; +b>>0;b>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a>>0,b=Array(d);for(var a=0;a>>0; +b-1:String(this).indexOf(a)>-1;},trim:function(){return String(this).replace(/^\s+|\s+$/g,""); +},clean:function(){return String(this).replace(/\s+/g," ").trim();},camelCase:function(){return String(this).replace(/-\D/g,function(a){return a.charAt(1).toUpperCase(); +});},hyphenate:function(){return String(this).replace(/[A-Z]/g,function(a){return("-"+a.charAt(0).toLowerCase());});},capitalize:function(){return String(this).replace(/\b[a-z]/g,function(a){return a.toUpperCase(); +});},escapeRegExp:function(){return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1");},toInt:function(a){return parseInt(this,a||10);},toFloat:function(){return parseFloat(this); +},hexToRgb:function(b){var a=String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);return(a)?a.slice(1).hexToRgb(b):null;},rgbToHex:function(b){var a=String(this).match(/\d{1,3}/g); +return(a)?a.rgbToHex(b):null;},substitute:function(a,b){return String(this).replace(b||(/\\?\{([^{}]+)\}/g),function(d,c){if(d.charAt(0)=="\\"){return d.slice(1); +}return(a[c]!=null)?a[c]:"";});}});Number.implement({limit:function(b,a){return Math.min(a,Math.max(b,this));},round:function(a){a=Math.pow(10,a||0).toFixed(a<0?-a:0); +return Math.round(this*a)/a;},times:function(b,c){for(var a=0;a1?Array.slice(arguments,1):null,d=function(){};var c=function(){var g=e,h=arguments.length;if(this instanceof c){d.prototype=a.prototype; +g=new d;}var f=(!b&&!h)?a.call(g):a.apply(g,b&&h?b.concat(Array.slice(arguments)):b||arguments);return g==e?f:g;};return c;},pass:function(b,c){var a=this; +if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},delay:function(b,c,a){return setTimeout(this.pass((a==null?[]:a),c),b); +},periodical:function(c,b,a){return setInterval(this.pass((a==null?[]:a),b),c);}});delete Function.prototype.bind;Function.implement({create:function(b){var a=this; +b=b||{};return function(d){var c=b.arguments;c=(c!=null)?Array.from(c):Array.slice(arguments,(b.event)?1:0);if(b.event){c=[d||window.event].extend(c);}var e=function(){return a.apply(b.bind||null,c); +};if(b.delay){return setTimeout(e,b.delay);}if(b.periodical){return setInterval(e,b.periodical);}if(b.attempt){return Function.attempt(e);}return e();}; +},bind:function(c,b){var a=this;if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},bindWithEvent:function(c,b){var a=this; +if(b!=null){b=Array.from(b);}return function(d){return a.apply(c,(b==null)?arguments:[d].concat(b));};},run:function(a,b){return this.apply(b,Array.from(a)); +}});if(Object.create==Function.prototype.create){Object.create=null;}var $try=Function.attempt;(function(){var a=Object.prototype.hasOwnProperty;Object.extend({subset:function(d,g){var f={}; +for(var e=0,b=g.length;e]*>([\s\S]*?)<\/script>/gi,function(r,s){e+=s+"\n"; +return"";});if(p===true){o.exec(e);}else{if(typeOf(p)=="function"){p(e,q);}}return q;});o.extend({Document:this.Document,Window:this.Window,Element:this.Element,Event:this.Event}); +this.Window=this.$constructor=new Type("Window",function(){});this.$family=Function.from("window").hide();Window.mirror(function(e,p){h[e]=p;});this.Document=k.$constructor=new Type("Document",function(){}); +k.$family=Function.from("document").hide();Document.mirror(function(e,p){k[e]=p;});k.html=k.documentElement;if(!k.head){k.head=k.getElementsByTagName("head")[0]; +}if(k.execCommand){try{k.execCommand("BackgroundImageCache",false,true);}catch(g){}}if(this.attachEvent&&!this.addEventListener){var c=function(){this.detachEvent("onunload",c); +k.head=k.html=k.window=null;};this.attachEvent("onunload",c);}var m=Array.from;try{m(k.html.childNodes);}catch(g){Array.from=function(p){if(typeof p!="string"&&Type.isEnumerable(p)&&typeOf(p)!="array"){var e=p.length,q=new Array(e); +while(e--){q[e]=p[e];}return q;}return m(p);};var l=Array.prototype,n=l.slice;["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice"].each(function(e){var p=l[e]; +Array[e]=function(q){return p.apply(Array.from(q),n.call(arguments,1));};});}if(o.Platform.ios){o.Platform.ipod=true;}o.Engine={};var d=function(p,e){o.Engine.name=p; +o.Engine[p+e]=true;o.Engine.version=e;};if(o.ie){o.Engine.trident=true;switch(o.version){case 6:d("trident",4);break;case 7:d("trident",5);break;case 8:d("trident",6); +}}if(o.firefox){o.Engine.gecko=true;if(o.version>=3){d("gecko",19);}else{d("gecko",18);}}if(o.safari||o.chrome){o.Engine.webkit=true;switch(o.version){case 2:d("webkit",419); +break;case 3:d("webkit",420);break;case 4:d("webkit",525);}}if(o.opera){o.Engine.presto=true;if(o.version>=9.6){d("presto",960);}else{if(o.version>=9.5){d("presto",950); +}else{d("presto",925);}}}if(o.name=="unknown"){switch((a.match(/(?:webkit|khtml|gecko)/)||[])[0]){case"webkit":case"khtml":o.Engine.webkit=true;break;case"gecko":o.Engine.gecko=true; +}}this.$exec=o.exec;})();(function(){var b={};var a=this.DOMEvent=new Type("DOMEvent",function(c,g){if(!g){g=window;}c=c||g.event;if(c.$extended){return c; +}this.event=c;this.$extended=true;this.shift=c.shiftKey;this.control=c.ctrlKey;this.alt=c.altKey;this.meta=c.metaKey;var i=this.type=c.type;var h=c.target||c.srcElement; +while(h&&h.nodeType==3){h=h.parentNode;}this.target=document.id(h);if(i.indexOf("key")==0){var d=this.code=(c.which||c.keyCode);this.key=b[d]||Object.keyOf(Event.Keys,d); +if(i=="keydown"){if(d>111&&d<124){this.key="f"+(d-111);}else{if(d>95&&d<106){this.key=d-96;}}}if(this.key==null){this.key=String.fromCharCode(d).toLowerCase(); +}}else{if(i=="click"||i=="dblclick"||i=="contextmenu"||i=="DOMMouseScroll"||i.indexOf("mouse")==0){var j=g.document;j=(!j.compatMode||j.compatMode=="CSS1Compat")?j.html:j.body; +this.page={x:(c.pageX!=null)?c.pageX:c.clientX+j.scrollLeft,y:(c.pageY!=null)?c.pageY:c.clientY+j.scrollTop};this.client={x:(c.pageX!=null)?c.pageX-g.pageXOffset:c.clientX,y:(c.pageY!=null)?c.pageY-g.pageYOffset:c.clientY}; +if(i=="DOMMouseScroll"||i=="mousewheel"){this.wheel=(c.wheelDelta)?c.wheelDelta/120:-(c.detail||0)/3;}this.rightClick=(c.which==3||c.button==2);if(i=="mouseover"||i=="mouseout"){var k=c.relatedTarget||c[(i=="mouseover"?"from":"to")+"Element"]; +while(k&&k.nodeType==3){k=k.parentNode;}this.relatedTarget=document.id(k);}}else{if(i.indexOf("touch")==0||i.indexOf("gesture")==0){this.rotation=c.rotation; +this.scale=c.scale;this.targetTouches=c.targetTouches;this.changedTouches=c.changedTouches;var f=this.touches=c.touches;if(f&&f[0]){var e=f[0];this.page={x:e.pageX,y:e.pageY}; +this.client={x:e.clientX,y:e.clientY};}}}}if(!this.client){this.client={};}if(!this.page){this.page={};}});a.implement({stop:function(){return this.preventDefault().stopPropagation(); +},stopPropagation:function(){if(this.event.stopPropagation){this.event.stopPropagation();}else{this.event.cancelBubble=true;}return this;},preventDefault:function(){if(this.event.preventDefault){this.event.preventDefault(); +}else{this.event.returnValue=false;}return this;}});a.defineKey=function(d,c){b[d]=c;return this;};a.defineKeys=a.defineKey.overloadSetter(true);a.defineKeys({"38":"up","40":"down","37":"left","39":"right","27":"esc","32":"space","8":"backspace","9":"tab","46":"delete","13":"enter"}); +})();var Event=DOMEvent;Event.Keys={};Event.Keys=new Hash(Event.Keys);(function(){var a=this.Class=new Type("Class",function(h){if(instanceOf(h,Function)){h={initialize:h}; +}var g=function(){e(this);if(g.$prototyping){return this;}this.$caller=null;var i=(this.initialize)?this.initialize.apply(this,arguments):this;this.$caller=this.caller=null; +return i;}.extend(this).implement(h);g.$constructor=a;g.prototype.$constructor=g;g.prototype.parent=c;return g;});var c=function(){if(!this.$caller){throw new Error('The method "parent" cannot be called.'); +}var g=this.$caller.$name,h=this.$caller.$owner.parent,i=(h)?h.prototype[g]:null;if(!i){throw new Error('The method "'+g+'" has no parent.');}return i.apply(this,arguments); +};var e=function(g){for(var h in g){var j=g[h];switch(typeOf(j)){case"object":var i=function(){};i.prototype=j;g[h]=e(new i);break;case"array":g[h]=j.clone(); +break;}}return g;};var b=function(g,h,j){if(j.$origin){j=j.$origin;}var i=function(){if(j.$protected&&this.$caller==null){throw new Error('The method "'+h+'" cannot be called.'); +}var l=this.caller,m=this.$caller;this.caller=m;this.$caller=i;var k=j.apply(this,arguments);this.$caller=m;this.caller=l;return k;}.extend({$owner:g,$origin:j,$name:h}); +return i;};var f=function(h,i,g){if(a.Mutators.hasOwnProperty(h)){i=a.Mutators[h].call(this,i);if(i==null){return this;}}if(typeOf(i)=="function"){if(i.$hidden){return this; +}this.prototype[h]=(g)?i:b(this,h,i);}else{Object.merge(this.prototype,h,i);}return this;};var d=function(g){g.$prototyping=true;var h=new g;delete g.$prototyping; +return h;};a.implement("implement",f.overloadSetter());a.Mutators={Extends:function(g){this.parent=g;this.prototype=d(g);},Implements:function(g){Array.from(g).each(function(j){var h=new j; +for(var i in h){f.call(this,i,h[i],true);}},this);}};})();(function(){this.Chain=new Class({$chain:[],chain:function(){this.$chain.append(Array.flatten(arguments)); +return this;},callChain:function(){return(this.$chain.length)?this.$chain.shift().apply(this,arguments):false;},clearChain:function(){this.$chain.empty(); +return this;}});var a=function(b){return b.replace(/^on([A-Z])/,function(c,d){return d.toLowerCase();});};this.Events=new Class({$events:{},addEvent:function(d,c,b){d=a(d); +if(c==$empty){return this;}this.$events[d]=(this.$events[d]||[]).include(c);if(b){c.internal=true;}return this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]); +}return this;},fireEvent:function(e,c,b){e=a(e);var d=this.$events[e];if(!d){return this;}c=Array.from(c);d.each(function(f){if(b){f.delay(b,this,c);}else{f.apply(this,c); +}},this);return this;},removeEvent:function(e,d){e=a(e);var c=this.$events[e];if(c&&!d.internal){var b=c.indexOf(d);if(b!=-1){delete c[b];}}return this; +},removeEvents:function(d){var e;if(typeOf(d)=="object"){for(e in d){this.removeEvent(e,d[e]);}return this;}if(d){d=a(d);}for(e in this.$events){if(d&&d!=e){continue; +}var c=this.$events[e];for(var b=c.length;b--;){if(b in c){this.removeEvent(e,c[b]);}}}return this;}});this.Options=new Class({setOptions:function(){var b=this.options=Object.merge.apply(null,[{},this.options].append(arguments)); +if(this.addEvent){for(var c in b){if(typeOf(b[c])!="function"||!(/^on[A-Z]/).test(c)){continue;}this.addEvent(c,b[c]);delete b[c];}}return this;}});})(); +(function(){var k,n,l,g,a={},c={},m=/\\/g;var e=function(q,p){if(q==null){return null;}if(q.Slick===true){return q;}q=(""+q).replace(/^\s+|\s+$/g,"");g=!!p; +var o=(g)?c:a;if(o[q]){return o[q];}k={Slick:true,expressions:[],raw:q,reverse:function(){return e(this.raw,true);}};n=-1;while(q!=(q=q.replace(j,b))){}k.length=k.expressions.length; +return o[k.raw]=(g)?h(k):k;};var i=function(o){if(o==="!"){return" ";}else{if(o===" "){return"!";}else{if((/^!/).test(o)){return o.replace(/^!/,"");}else{return"!"+o; +}}}};var h=function(u){var r=u.expressions;for(var p=0;p+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(//,"["+f(">+~`!@$%^&={}\\;/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(//g,"(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])")); +function b(x,s,D,z,r,C,q,B,A,y,u,F,G,v,p,w){if(s||n===-1){k.expressions[++n]=[];l=-1;if(s){return"";}}if(D||z||l===-1){D=D||" ";var t=k.expressions[n]; +if(g&&t[l]){t[l].reverseCombinator=i(D);}t[++l]={combinator:D,tag:"*"};}var o=k.expressions[n][l];if(r){o.tag=r.replace(m,"");}else{if(C){o.id=C.replace(m,""); +}else{if(q){q=q.replace(m,"");if(!o.classList){o.classList=[];}if(!o.classes){o.classes=[];}o.classList.push(q);o.classes.push({value:q,regexp:new RegExp("(^|\\s)"+f(q)+"(\\s|$)")}); +}else{if(G){w=w||p;w=w?w.replace(m,""):null;if(!o.pseudos){o.pseudos=[];}o.pseudos.push({key:G.replace(m,""),value:w,type:F.length==1?"class":"element"}); +}else{if(B){B=B.replace(m,"");u=(u||"").replace(m,"");var E,H;switch(A){case"^=":H=new RegExp("^"+f(u));break;case"$=":H=new RegExp(f(u)+"$");break;case"~=":H=new RegExp("(^|\\s)"+f(u)+"(\\s|$)"); +break;case"|=":H=new RegExp("^"+f(u)+"(-|$)");break;case"=":E=function(I){return u==I;};break;case"*=":E=function(I){return I&&I.indexOf(u)>-1;};break; +case"!=":E=function(I){return u!=I;};break;default:E=function(I){return !!I;};}if(u==""&&(/^[*$^]=$/).test(A)){E=function(){return false;};}if(!E){E=function(I){return I&&H.test(I); +};}if(!o.attributes){o.attributes=[];}o.attributes.push({key:B,operator:A,value:u,test:E});}}}}}return"";}var d=(this.Slick||{});d.parse=function(o){return e(o); +};d.escapeRegExp=f;if(!this.Slick){this.Slick=d;}}).apply((typeof exports!="undefined")?exports:this);(function(){var k={},m={},d=Object.prototype.toString; +k.isNativeCode=function(c){return(/\{\s*\[native code\]\s*\}/).test(""+c);};k.isXML=function(c){return(!!c.xmlVersion)||(!!c.xml)||(d.call(c)=="[object XMLDocument]")||(c.nodeType==9&&c.documentElement.nodeName!="HTML"); +};k.setDocument=function(w){var p=w.nodeType;if(p==9){}else{if(p){w=w.ownerDocument;}else{if(w.navigator){w=w.document;}else{return;}}}if(this.document===w){return; +}this.document=w;var A=w.documentElement,o=this.getUIDXML(A),s=m[o],r;if(s){for(r in s){this[r]=s[r];}return;}s=m[o]={};s.root=A;s.isXMLDocument=this.isXML(w); +s.brokenStarGEBTN=s.starSelectsClosedQSA=s.idGetsName=s.brokenMixedCaseQSA=s.brokenGEBCN=s.brokenCheckedQSA=s.brokenEmptyAttributeQSA=s.isHTMLDocument=s.nativeMatchesSelector=false; +var q,u,y,z,t;var x,v="slick_uniqueid";var c=w.createElement("div");var n=w.body||w.getElementsByTagName("body")[0]||A;n.appendChild(c);try{c.innerHTML=''; +s.isHTMLDocument=!!w.getElementById(v);}catch(C){}if(s.isHTMLDocument){c.style.display="none";c.appendChild(w.createComment(""));u=(c.getElementsByTagName("*").length>1); +try{c.innerHTML="foo";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML=''; +s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='';c.getElementsByClassName("b").length; +c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='';y=(c.getElementsByClassName("a").length!=2); +}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/"); +}catch(C){}try{c.innerHTML='';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML=''; +s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0); +}catch(C){}}try{c.innerHTML='';t=(c.firstChild.getAttribute("action")!="s");}catch(C){}s.nativeMatchesSelector=A.matchesSelector||A.mozMatchesSelector||A.webkitMatchesSelector; +if(s.nativeMatchesSelector){try{s.nativeMatchesSelector.call(A,":slick");s.nativeMatchesSelector=null;}catch(C){}}}try{A.slick_expando=1;delete A.slick_expando; +s.getUID=this.getUIDHTML;}catch(C){s.getUID=this.getUIDXML;}n.removeChild(c);c=x=n=null;s.getAttribute=(s.isHTMLDocument&&t)?function(G,E){var H=this.attributeGetters[E]; +if(H){return H.call(G);}var F=G.getAttributeNode(E);return(F)?F.nodeValue:null;}:function(F,E){var G=this.attributeGetters[E];return(G)?G.call(F):F.getAttribute(E); +};s.hasAttribute=(A&&this.isNativeCode(A.hasAttribute))?function(F,E){return F.hasAttribute(E);}:function(F,E){F=F.getAttributeNode(E);return !!(F&&(F.specified||F.nodeValue)); +};var D=A&&this.isNativeCode(A.contains),B=w&&this.isNativeCode(w.contains);s.contains=(D&&B)?function(E,F){return E.contains(F);}:(D&&!B)?function(E,F){return E===F||((E===w)?w.documentElement:E).contains(F); +}:(A&&A.compareDocumentPosition)?function(E,F){return E===F||!!(E.compareDocumentPosition(F)&16);}:function(E,F){if(F){do{if(F===E){return true;}}while((F=F.parentNode)); +}return false;};s.documentSorter=(A.compareDocumentPosition)?function(F,E){if(!F.compareDocumentPosition||!E.compareDocumentPosition){return 0;}return F.compareDocumentPosition(E)&4?-1:F===E?0:1; +}:("sourceIndex" in A)?function(F,E){if(!F.sourceIndex||!E.sourceIndex){return 0;}return F.sourceIndex-E.sourceIndex;}:(w.createRange)?function(H,F){if(!H.ownerDocument||!F.ownerDocument){return 0; +}var G=H.ownerDocument.createRange(),E=F.ownerDocument.createRange();G.setStart(H,0);G.setEnd(H,0);E.setStart(F,0);E.setEnd(F,0);return G.compareBoundaryPoints(Range.START_TO_END,E); +}:null;A=null;for(r in s){this[r]=s[r];}};var f=/^([#.]?)((?:[\w-]+|\*))$/,h=/\[.+[*$^]=(?:""|'')?\]/,g={};k.search=function(U,z,H,s){var p=this.found=(s)?null:(H||[]); +if(!U){return p;}else{if(U.navigator){U=U.document;}else{if(!U.nodeType){return p;}}}var F,O,V=this.uniques={},I=!!(H&&H.length),y=(U.nodeType==9);if(this.document!==(y?U:U.ownerDocument)){this.setDocument(U); +}if(I){for(O=p.length;O--;){V[this.getUID(p[O])]=true;}}if(typeof z=="string"){var r=z.match(f);simpleSelectors:if(r){var u=r[1],v=r[2],A,E;if(!u){if(v=="*"&&this.brokenStarGEBTN){break simpleSelectors; +}E=U.getElementsByTagName(v);if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{if(u=="#"){if(!this.isHTMLDocument||!y){break simpleSelectors; +}A=U.getElementById(v);if(!A){return p;}if(this.idGetsName&&A.getAttributeNode("id").nodeValue!=v){break simpleSelectors;}if(s){return A||null;}if(!(I&&V[this.getUID(A)])){p.push(A); +}}else{if(u=="."){if(!this.isHTMLDocument||((!U.getElementsByClassName||this.brokenGEBCN)&&U.querySelectorAll)){break simpleSelectors;}if(U.getElementsByClassName&&!this.brokenGEBCN){E=U.getElementsByClassName(v); +if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{var T=new RegExp("(^|\\s)"+e.escapeRegExp(v)+"(\\s|$)");E=U.getElementsByTagName("*"); +for(O=0;A=E[O++];){className=A.className;if(!(className&&T.test(className))){continue;}if(s){return A;}if(!(I&&V[this.getUID(A)])){p.push(A);}}}}}}if(I){this.sort(p); +}return(s)?null:p;}querySelector:if(U.querySelectorAll){if(!this.isHTMLDocument||g[z]||this.brokenMixedCaseQSA||(this.brokenCheckedQSA&&z.indexOf(":checked")>-1)||(this.brokenEmptyAttributeQSA&&h.test(z))||(!y&&z.indexOf(",")>-1)||e.disableQSA){break querySelector; +}var S=z,x=U;if(!y){var C=x.getAttribute("id"),t="slickid__";x.setAttribute("id",t);S="#"+t+" "+S;U=x.parentNode;}try{if(s){return U.querySelector(S)||null; +}else{E=U.querySelectorAll(S);}}catch(Q){g[z]=1;break querySelector;}finally{if(!y){if(C){x.setAttribute("id",C);}else{x.removeAttribute("id");}U=x;}}if(this.starSelectsClosedQSA){for(O=0; +A=E[O++];){if(A.nodeName>"@"&&!(I&&V[this.getUID(A)])){p.push(A);}}}else{for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}if(I){this.sort(p); +}return p;}F=this.Slick.parse(z);if(!F.length){return p;}}else{if(z==null){return p;}else{if(z.Slick){F=z;}else{if(this.contains(U.documentElement||U,z)){(p)?p.push(z):p=z; +return p;}else{return p;}}}}this.posNTH={};this.posNTHLast={};this.posNTHType={};this.posNTHTypeLast={};this.push=(!I&&(s||(F.length==1&&F.expressions[0].length==1)))?this.pushArray:this.pushUID; +if(p==null){p=[];}var M,L,K;var B,J,D,c,q,G,W;var N,P,o,w,R=F.expressions;search:for(O=0;(P=R[O]);O++){for(M=0;(o=P[M]);M++){B="combinator:"+o.combinator; +if(!this[B]){continue search;}J=(this.isXMLDocument)?o.tag:o.tag.toUpperCase();D=o.id;c=o.classList;q=o.classes;G=o.attributes;W=o.pseudos;w=(M===(P.length-1)); +this.bitUniques={};if(w){this.uniques=V;this.found=p;}else{this.uniques={};this.found=[];}if(M===0){this[B](U,J,D,q,G,W,c);if(s&&w&&p.length){break search; +}}else{if(s&&w){for(L=0,K=N.length;L1)){this.sort(p);}return(s)?(p[0]||null):p;};k.uidx=1;k.uidk="slick-uniqueid";k.getUIDXML=function(n){var c=n.getAttribute(this.uidk); +if(!c){c=this.uidx++;n.setAttribute(this.uidk,c);}return c;};k.getUIDHTML=function(c){return c.uniqueNumber||(c.uniqueNumber=this.uidx++);};k.sort=function(c){if(!this.documentSorter){return c; +}c.sort(this.documentSorter);return c;};k.cacheNTH={};k.matchNTH=/^([+-]?\d*)?([a-z]+)?([+-]\d+)?$/;k.parseNTHArgument=function(q){var o=q.match(this.matchNTH); +if(!o){return false;}var p=o[2]||false;var n=o[1]||1;if(n=="-"){n=-1;}var c=+o[3]||0;o=(p=="n")?{a:n,b:c}:(p=="odd")?{a:2,b:1}:(p=="even")?{a:2,b:0}:{a:0,b:n}; +return(this.cacheNTH[q]=o);};k.createNTHPseudo=function(p,n,c,o){return function(s,q){var u=this.getUID(s);if(!this[c][u]){var A=s.parentNode;if(!A){return false; +}var r=A[p],t=1;if(o){var z=s.nodeName;do{if(r.nodeName!=z){continue;}this[c][this.getUID(r)]=t++;}while((r=r[n]));}else{do{if(r.nodeType!=1){continue; +}this[c][this.getUID(r)]=t++;}while((r=r[n]));}}q=q||"n";var v=this.cacheNTH[q]||this.parseNTHArgument(q);if(!v){return false;}var y=v.a,x=v.b,w=this[c][u]; +if(y==0){return x==w;}if(y>0){if(w":function(p,c,r,o,n,q){if((p=p.firstChild)){do{if(p.nodeType==1){this.push(p,c,r,o,n,q); +}}while((p=p.nextSibling));}},"+":function(p,c,r,o,n,q){while((p=p.nextSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q);break;}}},"^":function(p,c,r,o,n,q){p=p.firstChild; +if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:+"](p,c,r,o,n,q);}}},"~":function(q,c,s,p,n,r){while((q=q.nextSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}},"++":function(p,c,r,o,n,q){this["combinator:+"](p,c,r,o,n,q); +this["combinator:!+"](p,c,r,o,n,q);},"~~":function(p,c,r,o,n,q){this["combinator:~"](p,c,r,o,n,q);this["combinator:!~"](p,c,r,o,n,q);},"!":function(p,c,r,o,n,q){while((p=p.parentNode)){if(p!==this.document){this.push(p,c,r,o,n,q); +}}},"!>":function(p,c,r,o,n,q){p=p.parentNode;if(p!==this.document){this.push(p,c,r,o,n,q);}},"!+":function(p,c,r,o,n,q){while((p=p.previousSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q); +break;}}},"!^":function(p,c,r,o,n,q){p=p.lastChild;if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:!+"](p,c,r,o,n,q);}}},"!~":function(q,c,s,p,n,r){while((q=q.previousSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}}};for(var i in j){k["combinator:"+i]=j[i];}var l={empty:function(c){var n=c.firstChild; +return !(n&&n.nodeType==1)&&!(c.innerText||c.textContent||"").length;},not:function(c,n){return !this.matchNode(c,n);},contains:function(c,n){return(c.innerText||c.textContent||"").indexOf(n)>-1; +},"first-child":function(c){while((c=c.previousSibling)){if(c.nodeType==1){return false;}}return true;},"last-child":function(c){while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"only-child":function(o){var n=o;while((n=n.previousSibling)){if(n.nodeType==1){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"nth-child":k.createNTHPseudo("firstChild","nextSibling","posNTH"),"nth-last-child":k.createNTHPseudo("lastChild","previousSibling","posNTHLast"),"nth-of-type":k.createNTHPseudo("firstChild","nextSibling","posNTHType",true),"nth-last-of-type":k.createNTHPseudo("lastChild","previousSibling","posNTHTypeLast",true),index:function(n,c){return this["pseudo:nth-child"](n,""+(c+1)); +},even:function(c){return this["pseudo:nth-child"](c,"2n");},odd:function(c){return this["pseudo:nth-child"](c,"2n+1");},"first-of-type":function(c){var n=c.nodeName; +while((c=c.previousSibling)){if(c.nodeName==n){return false;}}return true;},"last-of-type":function(c){var n=c.nodeName;while((c=c.nextSibling)){if(c.nodeName==n){return false; +}}return true;},"only-of-type":function(o){var n=o,p=o.nodeName;while((n=n.previousSibling)){if(n.nodeName==p){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeName==p){return false; +}}return true;},enabled:function(c){return !c.disabled;},disabled:function(c){return c.disabled;},checked:function(c){return c.checked||c.selected;},focus:function(c){return this.isHTMLDocument&&this.document.activeElement===c&&(c.href||c.type||this.hasAttribute(c,"tabindex")); +},root:function(c){return(c===this.root);},selected:function(c){return c.selected;}};for(var b in l){k["pseudo:"+b]=l[b];}var a=k.attributeGetters={"for":function(){return("htmlFor" in this)?this.htmlFor:this.getAttribute("for"); +},href:function(){return("href" in this)?this.getAttribute("href",2):this.getAttribute("href");},style:function(){return(this.style)?this.style.cssText:this.getAttribute("style"); +},tabindex:function(){var c=this.getAttributeNode("tabindex");return(c&&c.specified)?c.nodeValue:null;},type:function(){return this.getAttribute("type"); +},maxlength:function(){var c=this.getAttributeNode("maxLength");return(c&&c.specified)?c.nodeValue:null;}};a.MAXLENGTH=a.maxLength=a.maxlength;var e=k.Slick=(this.Slick||{}); +e.version="1.1.7";e.search=function(n,o,c){return k.search(n,o,c);};e.find=function(c,n){return k.search(c,n,null,true);};e.contains=function(c,n){k.setDocument(c); +return k.contains(c,n);};e.getAttribute=function(n,c){k.setDocument(n);return k.getAttribute(n,c);};e.hasAttribute=function(n,c){k.setDocument(n);return k.hasAttribute(n,c); +};e.match=function(n,c){if(!(n&&c)){return false;}if(!c||c===n){return true;}k.setDocument(n);return k.matchNode(n,c);};e.defineAttributeGetter=function(c,n){k.attributeGetters[c]=n; +return this;};e.lookupAttributeGetter=function(c){return k.attributeGetters[c];};e.definePseudo=function(c,n){k["pseudo:"+c]=function(p,o){return n.call(p,o); +};return this;};e.lookupPseudo=function(c){var n=k["pseudo:"+c];if(n){return function(o){return n.call(this,o);};}return null;};e.override=function(n,c){k.override(n,c); +return this;};e.isXML=k.isXML;e.uidOf=function(c){return k.getUIDHTML(c);};if(!this.Slick){this.Slick=e;}}).apply((typeof exports!="undefined")?exports:this); +var Element=function(b,g){var h=Element.Constructors[b];if(h){return h(g);}if(typeof b!="string"){return document.id(b).set(g);}if(!g){g={};}if(!(/^[\w-]+$/).test(b)){var e=Slick.parse(b).expressions[0][0]; +b=(e.tag=="*")?"div":e.tag;if(e.id&&g.id==null){g.id=e.id;}var d=e.attributes;if(d){for(var a,f=0,c=d.length;f=this.length){delete this[g--];}return e;}.protect());}Array.forEachMethod(function(g,e){Elements.implement(e,g);});Array.mirror(Elements);var d; +try{d=(document.createElement("").name=="x");}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&").replace(/"/g,""");};Document.implement({newElement:function(e,g){if(g&&g.checked!=null){g.defaultChecked=g.checked; +}if(d&&g){e="<"+e;if(g.name){e+=' name="'+c(g.name)+'"';}if(g.type){e+=' type="'+c(g.type)+'"';}e+=">";delete g.name;delete g.type;}return this.id(this.createElement(e)).set(g); +}});})();(function(){Slick.uidOf(window);Slick.uidOf(document);Document.implement({newTextNode:function(e){return this.createTextNode(e);},getDocument:function(){return this; +},getWindow:function(){return this.window;},id:(function(){var e={string:function(E,D,l){E=Slick.find(l,"#"+E.replace(/(\W)/g,"\\$1"));return(E)?e.element(E,D):null; +},element:function(D,E){Slick.uidOf(D);if(!E&&!D.$family&&!(/^(?:object|embed)$/i).test(D.tagName)){var l=D.fireEvent;D._fireEvent=function(F,G){return l(F,G); +};Object.append(D,Element.Prototype);}return D;},object:function(D,E,l){if(D.toElement){return e.element(D.toElement(l),E);}return null;}};e.textnode=e.whitespace=e.window=e.document=function(l){return l; +};return function(D,F,E){if(D&&D.$family&&D.uniqueNumber){return D;}var l=typeOf(D);return(e[l])?e[l](D,F,E||document):null;};})()});if(window.$==null){Window.implement("$",function(e,l){return document.id(e,l,this.document); +});}Window.implement({getDocument:function(){return this.document;},getWindow:function(){return this;}});[Document,Element].invoke("implement",{getElements:function(e){return Slick.search(this,e,new Elements); +},getElement:function(e){return document.id(Slick.find(this,e));}});var m={contains:function(e){return Slick.contains(this,e);}};if(!document.contains){Document.implement(m); +}if(!document.createElement("div").contains){Element.implement(m);}Element.implement("hasChild",function(e){return this!==e&&this.contains(e);});(function(l,E,e){this.Selectors={}; +var F=this.Selectors.Pseudo=new Hash();var D=function(){for(var G in F){if(F.hasOwnProperty(G)){Slick.definePseudo(G,F[G]);delete F[G];}}};Slick.search=function(H,I,G){D(); +return l.call(this,H,I,G);};Slick.find=function(G,H){D();return E.call(this,G,H);};Slick.match=function(H,G){D();return e.call(this,H,G);};})(Slick.search,Slick.find,Slick.match); +var r=function(E,D){if(!E){return D;}E=Object.clone(Slick.parse(E));var l=E.expressions;for(var e=l.length;e--;){l[e][0].combinator=D;}return E;};Object.forEach({getNext:"~",getPrevious:"!~",getParent:"!"},function(e,l){Element.implement(l,function(D){return this.getElement(r(D,e)); +});});Object.forEach({getAllNext:"~",getAllPrevious:"!~",getSiblings:"~~",getChildren:">",getParents:"!"},function(e,l){Element.implement(l,function(D){return this.getElements(r(D,e)); +});});Element.implement({getFirst:function(e){return document.id(Slick.search(this,r(e,">"))[0]);},getLast:function(e){return document.id(Slick.search(this,r(e,">")).getLast()); +},getWindow:function(){return this.ownerDocument.window;},getDocument:function(){return this.ownerDocument;},getElementById:function(e){return document.id(Slick.find(this,"#"+(""+e).replace(/(\W)/g,"\\$1"))); +},match:function(e){return !e||Slick.match(this,e);}});if(window.$$==null){Window.implement("$$",function(e){var H=new Elements;if(arguments.length==1&&typeof e=="string"){return Slick.search(this.document,e,H); +}var E=Array.flatten(arguments);for(var F=0,D=E.length;F(?![^<]*<['"])/)).indexOf(F)<0){return null;}E[F]=true;}}var e=Slick.getAttribute(this,F); +return(!e&&!Slick.hasAttribute(this,F))?null:e;},getProperties:function(){var e=Array.from(arguments);return e.map(this.getProperty,this).associate(e); +},removeProperty:function(e){return this.setProperty(e,null);},removeProperties:function(){Array.each(arguments,this.removeProperty,this);return this;},set:function(D,l){var e=Element.Properties[D]; +(e&&e.set)?e.set.call(this,l):this.setProperty(D,l);}.overloadSetter(),get:function(l){var e=Element.Properties[l];return(e&&e.get)?e.get.apply(this):this.getProperty(l); +}.overloadGetter(),erase:function(l){var e=Element.Properties[l];(e&&e.erase)?e.erase.apply(this):this.removeProperty(l);return this;},hasClass:function(e){return this.className.clean().contains(e," "); +},addClass:function(e){if(!this.hasClass(e)){this.className=(this.className+" "+e).clean();}return this;},removeClass:function(e){this.className=this.className.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)"),"$1"); +return this;},toggleClass:function(e,l){if(l==null){l=!this.hasClass(e);}return(l)?this.addClass(e):this.removeClass(e);},adopt:function(){var E=this,e,G=Array.flatten(arguments),F=G.length; +if(F>1){E=e=document.createDocumentFragment();}for(var D=0;D";var a=(t.childNodes.length==1);if(!a){var s="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),b=document.createDocumentFragment(),u=s.length; +while(u--){b.createElement(s[u]);}}t=null;var g=Function.attempt(function(){var e=document.createElement("table");e.innerHTML="";return true; +});var c=document.createElement("tr"),o="";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"","
"],select:[1,""],tbody:[2,"","
"],tr:[3,"","
"]}; +e.thead=e.tfoot=e.tbody;return function(D){var E=e[this.get("tag")];if(!E&&!a){E=[0,"",""];}if(!E){return l.call(this,D);}var H=E[0],G=document.createElement("div"),F=G; +if(!a){b.appendChild(G);}G.innerHTML=[E[1],D,E[2]].flatten().join("");while(H--){F=F.firstChild;}this.empty().adopt(F.childNodes);if(!a){b.removeChild(G); +}G=null;};})(Element.Properties.html.set);}var n=document.createElement("form");n.innerHTML="";if(n.firstChild.value!="s"){Element.Properties.value={set:function(G){var l=this.get("tag"); +if(l!="select"){return this.setProperty("value",G);}var D=this.getElements("option");for(var E=0;E0||k==null?"visible":"hidden";};var f=(h?function(l,k){l.style.opacity=k;}:(e?function(l,k){var n=l.style; +if(!l.currentStyle||!l.currentStyle.hasLayout){n.zoom=1;}if(k==null||k==1){k="";}else{k="alpha(opacity="+(k*100).limit(0,100).round()+")";}var m=n.filter||l.getComputedStyle("filter")||""; +n.filter=j.test(m)?m.replace(j,k):m+k;if(!n.filter){n.removeAttribute("filter");}}:a));var g=(h?function(l){var k=l.style.opacity||l.getComputedStyle("opacity"); +return(k=="")?1:k.toFloat();}:(e?function(l){var m=(l.style.filter||l.getComputedStyle("filter")),k;if(m){k=m.match(j);}return(k==null||m==null)?1:(k[1]/100); +}:function(l){var k=l.retrieve("$opacity");if(k==null){k=(l.style.visibility=="hidden"?0:1);}return k;}));var b=(i.style.cssFloat==null)?"styleFloat":"cssFloat"; +Element.implement({getComputedStyle:function(m){if(this.currentStyle){return this.currentStyle[m.camelCase()];}var l=Element.getDocument(this).defaultView,k=l?l.getComputedStyle(this,null):null; +return(k)?k.getPropertyValue((m==b)?"float":m.hyphenate()):null;},setStyle:function(l,k){if(l=="opacity"){if(k!=null){k=parseFloat(k);}f(this,k);return this; +}l=(l=="float"?b:l).camelCase();if(typeOf(k)!="string"){var m=(Element.Styles[l]||"@").split(" ");k=Array.from(k).map(function(o,n){if(!m[n]){return""; +}return(typeOf(o)=="number")?m[n].replace("@",Math.round(o)):o;}).join(" ");}else{if(k==String(Number(k))){k=Math.round(k);}}this.style[l]=k;if((k==""||k==null)&&c&&this.style.removeAttribute){this.style.removeAttribute(l); +}return this;},getStyle:function(q){if(q=="opacity"){return g(this);}q=(q=="float"?b:q).camelCase();var k=this.style[q];if(!k||q=="zIndex"){k=[];for(var p in Element.ShortStyles){if(q!=p){continue; +}for(var o in Element.ShortStyles[p]){k.push(this.getStyle(o));}return k.join(" ");}k=this.getComputedStyle(q);}if(k){k=String(k);var m=k.match(/rgba?\([\d\s,]+\)/); +if(m){k=k.replace(m[0],m[0].rgbToHex());}}if(Browser.opera||Browser.ie){if((/^(height|width)$/).test(q)&&!(/px$/.test(k))){var l=(q=="width")?["left","right"]:["top","bottom"],n=0; +l.each(function(r){n+=this.getStyle("border-"+r+"-width").toInt()+this.getStyle("padding-"+r).toInt();},this);return this["offset"+q.capitalize()]-n+"px"; +}if(Browser.ie&&(/^border(.+)Width|margin|padding/).test(q)&&isNaN(parseFloat(k))){return"0px";}}return k;},setStyles:function(l){for(var k in l){this.setStyle(k,l[k]); +}return this;},getStyles:function(){var k={};Array.flatten(arguments).each(function(l){k[l]=this.getStyle(l);},this);return k;}});Element.Styles={left:"@px",top:"@px",bottom:"@px",right:"@px",width:"@px",height:"@px",maxWidth:"@px",maxHeight:"@px",minWidth:"@px",minHeight:"@px",backgroundColor:"rgb(@, @, @)",backgroundPosition:"@px @px",color:"rgb(@, @, @)",fontSize:"@px",letterSpacing:"@px",lineHeight:"@px",clip:"rect(@px @px @px @px)",margin:"@px @px @px @px",padding:"@px @px @px @px",border:"@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)",borderWidth:"@px @px @px @px",borderStyle:"@ @ @ @",borderColor:"rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)",zIndex:"@",zoom:"@",fontWeight:"@",textIndent:"@px",opacity:"@"}; +Element.implement({setOpacity:function(k){f(this,k);return this;},getOpacity:function(){return g(this);}});Element.Properties.opacity={set:function(k){f(this,k); +a(this,k);},get:function(){return g(this);}};Element.Styles=new Hash(Element.Styles);Element.ShortStyles={margin:{},padding:{},border:{},borderWidth:{},borderStyle:{},borderColor:{}}; +["Top","Right","Bottom","Left"].each(function(q){var p=Element.ShortStyles;var l=Element.Styles;["margin","padding"].each(function(r){var s=r+q;p[r][s]=l[s]="@px"; +});var o="border"+q;p.border[o]=l[o]="@px @ rgb(@, @, @)";var n=o+"Width",k=o+"Style",m=o+"Color";p[o]={};p.borderWidth[n]=p[o][n]=l[n]="@px";p.borderStyle[k]=p[o][k]=l[k]="@"; +p.borderColor[m]=p[o][m]=l[m]="rgb(@, @, @)";});})();(function(){Element.Properties.events={set:function(b){this.addEvents(b);}};[Element,Window,Document].invoke("implement",{addEvent:function(f,h){var i=this.retrieve("events",{}); +if(!i[f]){i[f]={keys:[],values:[]};}if(i[f].keys.contains(h)){return this;}i[f].keys.push(h);var g=f,b=Element.Events[f],d=h,j=this;if(b){if(b.onAdd){b.onAdd.call(this,h,f); +}if(b.condition){d=function(k){if(b.condition.call(this,k,f)){return h.call(this,k);}return true;};}if(b.base){g=Function.from(b.base).call(this,f);}}var e=function(){return h.call(j); +};var c=Element.NativeEvents[g];if(c){if(c==2){e=function(k){k=new DOMEvent(k,j.getWindow());if(d.call(j,k)===false){k.stop();}};}this.addListener(g,e,arguments[2]); +}i[f].values.push(e);return this;},removeEvent:function(e,d){var c=this.retrieve("events");if(!c||!c[e]){return this;}var h=c[e];var b=h.keys.indexOf(d); +if(b==-1){return this;}var g=h.values[b];delete h.keys[b];delete h.values[b];var f=Element.Events[e];if(f){if(f.onRemove){f.onRemove.call(this,d,e);}if(f.base){e=Function.from(f.base).call(this,e); +}}return(Element.NativeEvents[e])?this.removeListener(e,g,arguments[2]):this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this; +},removeEvents:function(b){var d;if(typeOf(b)=="object"){for(d in b){this.removeEvent(d,b[d]);}return this;}var c=this.retrieve("events");if(!c){return this; +}if(!b){for(d in c){this.removeEvents(d);}this.eliminate("events");}else{if(c[b]){c[b].keys.each(function(e){this.removeEvent(b,e);},this);delete c[b]; +}}return this;},fireEvent:function(e,c,b){var d=this.retrieve("events");if(!d||!d[e]){return this;}c=Array.from(c);d[e].keys.each(function(f){if(b){f.delay(b,this,c); +}else{f.apply(this,c);}},this);return this;},cloneEvents:function(e,d){e=document.id(e);var c=e.retrieve("events");if(!c){return this;}if(!d){for(var b in c){this.cloneEvents(e,b); +}}else{if(c[d]){c[d].keys.each(function(f){this.addEvent(d,f);},this);}}return this;}});Element.NativeEvents={click:2,dblclick:2,mouseup:2,mousedown:2,contextmenu:2,mousewheel:2,DOMMouseScroll:2,mouseover:2,mouseout:2,mousemove:2,selectstart:2,selectend:2,keydown:2,keypress:2,keyup:2,orientationchange:2,touchstart:2,touchmove:2,touchend:2,touchcancel:2,gesturestart:2,gesturechange:2,gestureend:2,focus:2,blur:2,change:2,reset:2,select:2,submit:2,paste:2,input:2,load:2,unload:1,beforeunload:2,resize:1,move:1,DOMContentLoaded:1,readystatechange:1,error:1,abort:1,scroll:1}; +Element.Events={mousewheel:{base:(Browser.firefox)?"DOMMouseScroll":"mousewheel"}};if("onmouseenter" in document.documentElement){Element.NativeEvents.mouseenter=Element.NativeEvents.mouseleave=2; +}else{var a=function(b){var c=b.relatedTarget;if(c==null){return true;}if(!c){return false;}return(c!=this&&c.prefix!="xul"&&typeOf(this)!="document"&&!this.contains(c)); +};Element.Events.mouseenter={base:"mouseover",condition:a};Element.Events.mouseleave={base:"mouseout",condition:a};}if(!window.addEventListener){Element.NativeEvents.propertychange=2; +Element.Events.change={base:function(){var b=this.type;return(this.get("tag")=="input"&&(b=="radio"||b=="checkbox"))?"propertychange":"change";},condition:function(b){return this.type!="radio"||(b.event.propertyName=="checked"&&this.checked); +}};}Element.Events=new Hash(Element.Events);})();(function(){var c=!!window.addEventListener;Element.NativeEvents.focusin=Element.NativeEvents.focusout=2; +var k=function(l,m,n,o,p){while(p&&p!=l){if(m(p,o)){return n.call(p,o,p);}p=document.id(p.parentNode);}};var a={mouseenter:{base:"mouseover"},mouseleave:{base:"mouseout"},focus:{base:"focus"+(c?"":"in"),capture:true},blur:{base:c?"blur":"focusout",capture:true}}; +var b="$delegation:";var i=function(l){return{base:"focusin",remove:function(m,o){var p=m.retrieve(b+l+"listeners",{})[o];if(p&&p.forms){for(var n=p.forms.length; +n--;){p.forms[n].removeEvent(l,p.fns[n]);}}},listen:function(x,r,v,n,t,s){var o=(t.get("tag")=="form")?t:n.target.getParent("form");if(!o){return;}var u=x.retrieve(b+l+"listeners",{}),p=u[s]||{forms:[],fns:[]},m=p.forms,w=p.fns; +if(m.indexOf(o)!=-1){return;}m.push(o);var q=function(y){k(x,r,v,y,t);};o.addEvent(l,q);w.push(q);u[s]=p;x.store(b+l+"listeners",u);}};};var d=function(l){return{base:"focusin",listen:function(m,n,p,q,r){var o={blur:function(){this.removeEvents(o); +}};o[l]=function(s){k(m,n,p,s,r);};q.target.addEvents(o);}};};if(!c){Object.append(a,{submit:i("submit"),reset:i("reset"),change:d("change"),select:d("select")}); +}var h=Element.prototype,f=h.addEvent,j=h.removeEvent;var e=function(l,m){return function(r,q,n){if(r.indexOf(":relay")==-1){return l.call(this,r,q,n); +}var o=Slick.parse(r).expressions[0][0];if(o.pseudos[0].key!="relay"){return l.call(this,r,q,n);}var p=o.tag;o.pseudos.slice(1).each(function(s){p+=":"+s.key+(s.value?"("+s.value+")":""); +});l.call(this,r,q);return m.call(this,p,o.pseudos[0].value,q);};};var g={addEvent:function(v,q,x){var t=this.retrieve("$delegates",{}),r=t[v];if(r){for(var y in r){if(r[y].fn==x&&r[y].match==q){return this; +}}}var p=v,u=q,o=x,n=a[v]||{};v=n.base||p;q=function(B){return Slick.match(B,u);};var w=Element.Events[p];if(w&&w.condition){var l=q,m=w.condition;q=function(C,B){return l(C,B)&&m.call(C,B,v); +};}var z=this,s=String.uniqueID();var A=n.listen?function(B,C){if(!C&&B&&B.target){C=B.target;}if(C){n.listen(z,q,x,B,C,s);}}:function(B,C){if(!C&&B&&B.target){C=B.target; +}if(C){k(z,q,x,B,C);}};if(!r){r={};}r[s]={match:u,fn:o,delegator:A};t[p]=r;return f.call(this,v,A,n.capture);},removeEvent:function(r,n,t,u){var q=this.retrieve("$delegates",{}),p=q[r]; +if(!p){return this;}if(u){var m=r,w=p[u].delegator,l=a[r]||{};r=l.base||m;if(l.remove){l.remove(this,u);}delete p[u];q[m]=p;return j.call(this,r,w);}var o,v; +if(t){for(o in p){v=p[o];if(v.match==n&&v.fn==t){return g.removeEvent.call(this,r,n,t,o);}}}else{for(o in p){v=p[o];if(v.match==n){g.removeEvent.call(this,r,n,v.fn,o); +}}}return this;}};[Element,Window,Document].invoke("implement",{addEvent:e(f,g.addEvent),removeEvent:e(j,g.removeEvent)});})();(function(){var h=document.createElement("div"),e=document.createElement("div"); +h.style.height="0";h.appendChild(e);var d=(e.offsetParent===h);h=e=null;var l=function(m){return k(m,"position")!="static"||a(m);};var i=function(m){return l(m)||(/^(?:table|td|th)$/i).test(m.tagName); +};Element.implement({scrollTo:function(m,n){if(a(this)){this.getWindow().scrollTo(m,n);}else{this.scrollLeft=m;this.scrollTop=n;}return this;},getSize:function(){if(a(this)){return this.getWindow().getSize(); +}return{x:this.offsetWidth,y:this.offsetHeight};},getScrollSize:function(){if(a(this)){return this.getWindow().getScrollSize();}return{x:this.scrollWidth,y:this.scrollHeight}; +},getScroll:function(){if(a(this)){return this.getWindow().getScroll();}return{x:this.scrollLeft,y:this.scrollTop};},getScrolls:function(){var n=this.parentNode,m={x:0,y:0}; +while(n&&!a(n)){m.x+=n.scrollLeft;m.y+=n.scrollTop;n=n.parentNode;}return m;},getOffsetParent:d?function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}var n=(k(m,"position")=="static")?i:l;while((m=m.parentNode)){if(n(m)){return m;}}return null;}:function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}try{return m.offsetParent;}catch(n){}return null;},getOffsets:function(){if(this.getBoundingClientRect&&!Browser.Platform.ios){var r=this.getBoundingClientRect(),o=document.id(this.getDocument().documentElement),q=o.getScroll(),t=this.getScrolls(),s=(k(this,"position")=="fixed"); +return{x:r.left.toInt()+t.x+((s)?0:q.x)-o.clientLeft,y:r.top.toInt()+t.y+((s)?0:q.y)-o.clientTop};}var n=this,m={x:0,y:0};if(a(this)){return m;}while(n&&!a(n)){m.x+=n.offsetLeft; +m.y+=n.offsetTop;if(Browser.firefox){if(!c(n)){m.x+=b(n);m.y+=g(n);}var p=n.parentNode;if(p&&k(p,"overflow")!="visible"){m.x+=b(p);m.y+=g(p);}}else{if(n!=this&&Browser.safari){m.x+=b(n); +m.y+=g(n);}}n=n.offsetParent;}if(Browser.firefox&&!c(this)){m.x-=b(this);m.y-=g(this);}return m;},getPosition:function(p){var q=this.getOffsets(),n=this.getScrolls(); +var m={x:q.x-n.x,y:q.y-n.y};if(p&&(p=document.id(p))){var o=p.getPosition();return{x:m.x-o.x-b(p),y:m.y-o.y-g(p)};}return m;},getCoordinates:function(o){if(a(this)){return this.getWindow().getCoordinates(); +}var m=this.getPosition(o),n=this.getSize();var p={left:m.x,top:m.y,width:n.x,height:n.y};p.right=p.left+p.width;p.bottom=p.top+p.height;return p;},computePosition:function(m){return{left:m.x-j(this,"margin-left"),top:m.y-j(this,"margin-top")}; +},setPosition:function(m){return this.setStyles(this.computePosition(m));}});[Document,Window].invoke("implement",{getSize:function(){var m=f(this);return{x:m.clientWidth,y:m.clientHeight}; +},getScroll:function(){var n=this.getWindow(),m=f(this);return{x:n.pageXOffset||m.scrollLeft,y:n.pageYOffset||m.scrollTop};},getScrollSize:function(){var o=f(this),n=this.getSize(),m=this.getDocument().body; +return{x:Math.max(o.scrollWidth,m.scrollWidth,n.x),y:Math.max(o.scrollHeight,m.scrollHeight,n.y)};},getPosition:function(){return{x:0,y:0};},getCoordinates:function(){var m=this.getSize(); +return{top:0,left:0,bottom:m.y,right:m.x,height:m.y,width:m.x};}});var k=Element.getComputedStyle;function j(m,n){return k(m,n).toInt()||0;}function c(m){return k(m,"-moz-box-sizing")=="border-box"; +}function g(m){return j(m,"border-top-width");}function b(m){return j(m,"border-left-width");}function a(m){return(/^(?:body|html)$/i).test(m.tagName); +}function f(m){var n=m.getDocument();return(!n.compatMode||n.compatMode=="CSS1Compat")?n.html:n.body;}})();Element.alias({position:"setPosition"});[Window,Document,Element].invoke("implement",{getHeight:function(){return this.getSize().y; +},getWidth:function(){return this.getSize().x;},getScrollTop:function(){return this.getScroll().y;},getScrollLeft:function(){return this.getScroll().x; +},getScrollHeight:function(){return this.getScrollSize().y;},getScrollWidth:function(){return this.getScrollSize().x;},getTop:function(){return this.getPosition().y; +},getLeft:function(){return this.getPosition().x;}});(function(){var f=this.Fx=new Class({Implements:[Chain,Events,Options],options:{fps:60,unit:false,duration:500,frames:null,frameSkip:true,link:"ignore"},initialize:function(g){this.subject=this.subject||this; +this.setOptions(g);},getTransition:function(){return function(g){return -(Math.cos(Math.PI*g)-1)/2;};},step:function(g){if(this.options.frameSkip){var h=(this.time!=null)?(g-this.time):0,i=h/this.frameInterval; +this.time=g;this.frame+=i;}else{this.frame++;}if(this.frame=(7-4*d)/11){e=c*c-Math.pow((11-6*d-11*f)/4,2);break;}}return e;},Elastic:function(b,a){return Math.pow(2,10*--b)*Math.cos(20*b*Math.PI*(a&&a[0]||1)/3); +}});["Quad","Cubic","Quart","Quint"].each(function(b,a){Fx.Transitions[b]=new Fx.Transition(function(c){return Math.pow(c,a+2);});});(function(){var d=function(){},a=("onprogress" in new Browser.Request); +var c=this.Request=new Class({Implements:[Chain,Events,Options],options:{url:"",data:"",headers:{"X-Requested-With":"XMLHttpRequest",Accept:"text/javascript, text/html, application/xml, text/xml, */*"},async:true,format:false,method:"post",link:"ignore",isSuccess:null,emulation:true,urlEncoded:true,encoding:"utf-8",evalScripts:false,evalResponse:false,timeout:0,noCache:false},initialize:function(e){this.xhr=new Browser.Request(); +this.setOptions(e);this.headers=this.options.headers;},onStateChange:function(){var e=this.xhr;if(e.readyState!=4||!this.running){return;}this.running=false; +this.status=0;Function.attempt(function(){var f=e.status;this.status=(f==1223)?204:f;}.bind(this));e.onreadystatechange=d;if(a){e.onprogress=e.onloadstart=d; +}clearTimeout(this.timer);this.response={text:this.xhr.responseText||"",xml:this.xhr.responseXML};if(this.options.isSuccess.call(this,this.status)){this.success(this.response.text,this.response.xml); +}else{this.failure();}},isSuccess:function(){var e=this.status;return(e>=200&&e<300);},isRunning:function(){return !!this.running;},processScripts:function(e){if(this.options.evalResponse||(/(ecma|java)script/).test(this.getHeader("Content-type"))){return Browser.exec(e); +}return e.stripScripts(this.options.evalScripts);},success:function(f,e){this.onSuccess(this.processScripts(f),e);},onSuccess:function(){this.fireEvent("complete",arguments).fireEvent("success",arguments).callChain(); +},failure:function(){this.onFailure();},onFailure:function(){this.fireEvent("complete").fireEvent("failure",this.xhr);},loadstart:function(e){this.fireEvent("loadstart",[e,this.xhr]); +},progress:function(e){this.fireEvent("progress",[e,this.xhr]);},timeout:function(){this.fireEvent("timeout",this.xhr);},setHeader:function(e,f){this.headers[e]=f; +return this;},getHeader:function(e){return Function.attempt(function(){return this.xhr.getResponseHeader(e);}.bind(this));},check:function(){if(!this.running){return true; +}switch(this.options.link){case"cancel":this.cancel();return true;case"chain":this.chain(this.caller.pass(arguments,this));return false;}return false;},send:function(o){if(!this.check(o)){return this; +}this.options.isSuccess=this.options.isSuccess||this.isSuccess;this.running=true;var l=typeOf(o);if(l=="string"||l=="element"){o={data:o};}var h=this.options; +o=Object.append({data:h.data,url:h.url,method:h.method},o);var j=o.data,f=String(o.url),e=o.method.toLowerCase();switch(typeOf(j)){case"element":j=document.id(j).toQueryString(); +break;case"object":case"hash":j=Object.toQueryString(j);}if(this.options.format){var m="format="+this.options.format;j=(j)?m+"&"+j:m;}if(this.options.emulation&&!["get","post"].contains(e)){var k="_method="+e; +j=(j)?k+"&"+j:k;e="post";}if(this.options.urlEncoded&&["post","put"].contains(e)){var g=(this.options.encoding)?"; charset="+this.options.encoding:"";this.headers["Content-type"]="application/x-www-form-urlencoded"+g; +}if(!f){f=document.location.pathname;}var i=f.lastIndexOf("/");if(i>-1&&(i=f.indexOf("#"))>-1){f=f.substr(0,i);}if(this.options.noCache){f+=(f.contains("?")?"&":"?")+String.uniqueID(); +}if(j&&e=="get"){f+=(f.contains("?")?"&":"?")+j;j=null;}var n=this.xhr;if(a){n.onloadstart=this.loadstart.bind(this);n.onprogress=this.progress.bind(this); +}n.open(e.toUpperCase(),f,this.options.async,this.options.user,this.options.password);if(this.options.user&&"withCredentials" in n){n.withCredentials=true; +}n.onreadystatechange=this.onStateChange.bind(this);Object.each(this.headers,function(q,p){try{n.setRequestHeader(p,q);}catch(r){this.fireEvent("exception",[p,q]); +}},this);this.fireEvent("request");n.send(j);if(!this.options.async){this.onStateChange();}else{if(this.options.timeout){this.timer=this.timeout.delay(this.options.timeout,this); +}}return this;},cancel:function(){if(!this.running){return this;}this.running=false;var e=this.xhr;e.abort();clearTimeout(this.timer);e.onreadystatechange=d; +if(a){e.onprogress=e.onloadstart=d;}this.xhr=new Browser.Request();this.fireEvent("cancel");return this;}});var b={};["get","post","put","delete","GET","POST","PUT","DELETE"].each(function(e){b[e]=function(g){var f={method:e}; +if(g!=null){f.data=g;}return this.send(f);};});c.implement(b);Element.Properties.send={set:function(e){var f=this.get("send").cancel();f.setOptions(e); +return this;},get:function(){var e=this.retrieve("send");if(!e){e=new c({data:this,link:"cancel",method:this.get("method")||"post",url:this.get("action")}); +this.store("send",e);}return e;}};Element.implement({send:function(e){var f=this.get("send");f.send({data:this,url:e||f.options.url});return this;}});})(); +Request.HTML=new Class({Extends:Request,options:{update:false,append:false,evalScripts:true,filter:false,headers:{Accept:"text/html, application/xml, text/xml, */*"}},success:function(f){var e=this.options,c=this.response; +c.html=f.stripScripts(function(h){c.javascript=h;});var d=c.html.match(/]*>([\s\S]*?)<\/body>/i);if(d){c.html=d[1];}var b=new Element("div").set("html",c.html); +c.tree=b.childNodes;c.elements=b.getElements(e.filter||"*");if(e.filter){c.tree=c.elements;}if(e.update){var g=document.id(e.update).empty();if(e.filter){g.adopt(c.elements); +}else{g.set("html",c.html);}}else{if(e.append){var a=document.id(e.append);if(e.filter){c.elements.reverse().inject(a);}else{a.adopt(b.getChildren());}}}if(e.evalScripts){Browser.exec(c.javascript); +}this.onSuccess(c.tree,c.elements,c.html,c.javascript);}});Element.Properties.load={set:function(a){var b=this.get("load").cancel();b.setOptions(a);return this; +},get:function(){var a=this.retrieve("load");if(!a){a=new Request.HTML({data:this,link:"cancel",update:this,method:"get"});this.store("load",a);}return a; +}};Element.implement({load:function(){this.get("load").send(Array.link(arguments,{data:Type.isObject,url:Type.isString}));return this;}});if(typeof JSON=="undefined"){this.JSON={}; +}JSON=new Hash({stringify:JSON.stringify,parse:JSON.parse});(function(){var special={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"}; +var escape=function(chr){return special[chr]||"\\u"+("0000"+chr.charCodeAt(0).toString(16)).slice(-4);};JSON.validate=function(string){string=string.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""); +return(/^[\],:{}\s]*$/).test(string);};JSON.encode=JSON.stringify?function(obj){return JSON.stringify(obj);}:function(obj){if(obj&&obj.toJSON){obj=obj.toJSON(); +}switch(typeOf(obj)){case"string":return'"'+obj.replace(/[\x00-\x1f\\"]/g,escape)+'"';case"array":return"["+obj.map(JSON.encode).clean()+"]";case"object":case"hash":var string=[]; +Object.each(obj,function(value,key){var json=JSON.encode(value);if(json){string.push(JSON.encode(key)+":"+json);}});return"{"+string+"}";case"number":case"boolean":return""+obj; +case"null":return"null";}return null;};JSON.decode=function(string,secure){if(!string||typeOf(string)!="string"){return null;}if(secure||JSON.secure){if(JSON.parse){return JSON.parse(string); +}if(!JSON.validate(string)){throw new Error("JSON could not decode the input; security is enabled and the value is not secure.");}}return eval("("+string+")"); +};})();Request.JSON=new Class({Extends:Request,options:{secure:true},initialize:function(a){this.parent(a);Object.append(this.headers,{Accept:"application/json","X-Request":"JSON"}); +},success:function(c){var b;try{b=this.response.json=JSON.decode(c,this.options.secure);}catch(a){this.fireEvent("error",[c,a]);return;}if(b==null){this.onFailure(); +}else{this.onSuccess(b,c);}}});var Cookie=new Class({Implements:Options,options:{path:"/",domain:false,duration:false,secure:false,document:document,encode:true},initialize:function(b,a){this.key=b; +this.setOptions(a);},write:function(b){if(this.options.encode){b=encodeURIComponent(b);}if(this.options.domain){b+="; domain="+this.options.domain;}if(this.options.path){b+="; path="+this.options.path; +}if(this.options.duration){var a=new Date();a.setTime(a.getTime()+this.options.duration*24*60*60*1000);b+="; expires="+a.toGMTString();}if(this.options.secure){b+="; secure"; +}this.options.document.cookie=this.key+"="+b;return this;},read:function(){var a=this.options.document.cookie.match("(?:^|;)\\s*"+this.key.escapeRegExp()+"=([^;]*)"); +return(a)?decodeURIComponent(a[1]):null;},dispose:function(){new Cookie(this.key,Object.merge({},this.options,{duration:-1})).write("");return this;}}); +Cookie.write=function(b,c,a){return new Cookie(b,a).write(c);};Cookie.read=function(a){return new Cookie(a).read();};Cookie.dispose=function(b,a){return new Cookie(b,a).dispose(); +};(function(i,k){var l,f,e=[],c,b,d=k.createElement("div");var g=function(){clearTimeout(b);if(l){return;}Browser.loaded=l=true;k.removeListener("DOMContentLoaded",g).removeListener("readystatechange",a); +k.fireEvent("domready");i.fireEvent("domready");};var a=function(){for(var m=e.length;m--;){if(e[m]()){g();return true;}}return false;};var j=function(){clearTimeout(b); +if(!a()){b=setTimeout(j,10);}};k.addListener("DOMContentLoaded",g);var h=function(){try{d.doScroll();return true;}catch(m){}return false;};if(d.doScroll&&!h()){e.push(h); +c=true;}if(k.readyState){e.push(function(){var m=k.readyState;return(m=="loaded"||m=="complete");});}if("onreadystatechange" in k){k.addListener("readystatechange",a); +}else{c=true;}if(c){j();}Element.Events.domready={onAdd:function(m){if(l){m.call(this);}}};Element.Events.load={base:"load",onAdd:function(m){if(f&&this==i){m.call(this); +}},condition:function(){if(this==i){g();delete Element.Events.load;}return true;}};i.addEvent("load",function(){f=true;});})(window,document);(function(){var Swiff=this.Swiff=new Class({Implements:Options,options:{id:null,height:1,width:1,container:null,properties:{},params:{quality:"high",allowScriptAccess:"always",wMode:"window",swLiveConnect:true},callBacks:{},vars:{}},toElement:function(){return this.object; +},initialize:function(path,options){this.instance="Swiff_"+String.uniqueID();this.setOptions(options);options=this.options;var id=this.id=options.id||this.instance; +var container=document.id(options.container);Swiff.CallBacks[this.instance]={};var params=options.params,vars=options.vars,callBacks=options.callBacks; +var properties=Object.append({height:options.height,width:options.width},options.properties);var self=this;for(var callBack in callBacks){Swiff.CallBacks[this.instance][callBack]=(function(option){return function(){return option.apply(self.object,arguments); +};})(callBacks[callBack]);vars[callBack]="Swiff.CallBacks."+this.instance+"."+callBack;}params.flashVars=Object.toQueryString(vars);if(Browser.ie){properties.classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; +params.movie=path;}else{properties.type="application/x-shockwave-flash";}properties.data=path;var build='';}}build+="";this.object=((container)?container.empty():new Element("div")).set("html",build).firstChild; +},replaces:function(element){element=document.id(element,true);element.parentNode.replaceChild(this.toElement(),element);return this;},inject:function(element){document.id(element,true).appendChild(this.toElement()); +return this;},remote:function(){return Swiff.remote.apply(Swiff,[this.toElement()].append(arguments));}});Swiff.CallBacks={};Swiff.remote=function(obj,fn){var rs=obj.CallFunction(''+__flash__argumentsToXML(arguments,2)+""); +return eval(rs);};})(); \ No newline at end of file diff --git a/src/webui/www/public/scripts/mootools-1.2-more.js b/src/webui/www/private/scripts/mootools-1.2-more.js similarity index 100% rename from src/webui/www/public/scripts/mootools-1.2-more.js rename to src/webui/www/private/scripts/mootools-1.2-more.js diff --git a/src/webui/www/public/scripts/parametrics.js b/src/webui/www/private/scripts/parametrics.js similarity index 97% rename from src/webui/www/public/scripts/parametrics.js rename to src/webui/www/private/scripts/parametrics.js index f95dc7680..651dc6d1c 100644 --- a/src/webui/www/public/scripts/parametrics.js +++ b/src/webui/www/private/scripts/parametrics.js @@ -21,7 +21,7 @@ MochaUI.extend({ // Get global upload limit var maximum = 500; var req = new Request({ - url: 'command/getGlobalUpLimit', + url: 'api/v2/transfer/uploadLimit', method: 'post', data: {}, onSuccess: function(data) { @@ -70,7 +70,7 @@ MochaUI.extend({ } else { var req = new Request.JSON({ - url: 'command/getTorrentsUpLimit', + url: 'api/v2/torrents/uploadLimit', noCache : true, method: 'post', data: { @@ -125,7 +125,7 @@ MochaUI.extend({ // Get global upload limit var maximum = 500; var req = new Request({ - url: 'command/getGlobalDlLimit', + url: 'api/v2/transfer/downloadLimit', method: 'post', data: {}, onSuccess: function(data) { @@ -174,7 +174,7 @@ MochaUI.extend({ } else { var req = new Request.JSON({ - url: 'command/getTorrentsDlLimit', + url: 'api/v2/torrents/downloadLimit', noCache : true, method: 'post', data: { diff --git a/src/webui/www/public/scripts/progressbar.js b/src/webui/www/private/scripts/progressbar.js similarity index 100% rename from src/webui/www/public/scripts/progressbar.js rename to src/webui/www/private/scripts/progressbar.js diff --git a/src/webui/www/public/scripts/prop-files.js b/src/webui/www/private/scripts/prop-files.js similarity index 99% rename from src/webui/www/public/scripts/prop-files.js rename to src/webui/www/private/scripts/prop-files.js index 41ec8d66e..b09b77eb3 100644 --- a/src/webui/www/public/scripts/prop-files.js +++ b/src/webui/www/private/scripts/prop-files.js @@ -94,7 +94,7 @@ var allCBUnchecked = function() { var setFilePriority = function(id, priority) { if (current_hash === "") return; new Request({ - url: 'command/setFilePrio', + url: 'api/v2/torrents/filePrio', method: 'post', data: { 'hash': current_hash, @@ -289,7 +289,7 @@ var loadTorrentFilesData = function() { fTable.removeAllRows(); current_hash = new_hash; } - var url = 'query/propertiesFiles/' + current_hash; + var url = new URI('api/v2/torrents/files?hash=' + current_hash); var request = new Request.JSON({ url: url, noCache: true, diff --git a/src/webui/www/public/scripts/prop-general.js b/src/webui/www/private/scripts/prop-general.js similarity index 98% rename from src/webui/www/public/scripts/prop-general.js rename to src/webui/www/private/scripts/prop-general.js index 346404c7d..339371f8b 100644 --- a/src/webui/www/public/scripts/prop-general.js +++ b/src/webui/www/private/scripts/prop-general.js @@ -41,7 +41,7 @@ var loadTorrentData = function() { } // Display hash $('torrent_hash').set('html', current_hash); - var url = 'query/propertiesGeneral/' + current_hash; + var url = new URI('api/v2/torrents/properties?hash=' + current_hash); var request = new Request.JSON({ url: url, noCache: true, diff --git a/src/webui/www/public/scripts/prop-trackers.js b/src/webui/www/private/scripts/prop-trackers.js similarity index 98% rename from src/webui/www/public/scripts/prop-trackers.js rename to src/webui/www/private/scripts/prop-trackers.js index 32a61b056..6770de5ad 100644 --- a/src/webui/www/public/scripts/prop-trackers.js +++ b/src/webui/www/private/scripts/prop-trackers.js @@ -70,7 +70,7 @@ var loadTrackersData = function() { tTable.removeAllRows(); current_hash = new_hash; } - var url = 'query/propertiesTrackers/' + current_hash; + var url = new URI('api/v2/torrents/trackers?hash=' + current_hash); var request = new Request.JSON({ url: url, noCache: true, diff --git a/src/webui/www/public/scripts/prop-webseeds.js b/src/webui/www/private/scripts/prop-webseeds.js similarity index 97% rename from src/webui/www/public/scripts/prop-webseeds.js rename to src/webui/www/private/scripts/prop-webseeds.js index 12f366ca9..5cde2b30d 100644 --- a/src/webui/www/public/scripts/prop-webseeds.js +++ b/src/webui/www/private/scripts/prop-webseeds.js @@ -70,7 +70,7 @@ var loadWebSeedsData = function() { wsTable.removeAllRows(); current_hash = new_hash; } - var url = 'query/propertiesWebSeeds/' + current_hash; + var url = new URI('api/v2/torrents/webseeds?hash=' + current_hash); var request = new Request.JSON({ url: url, noCache: true, diff --git a/src/webui/www/public/setlocation.html b/src/webui/www/private/setlocation.html similarity index 97% rename from src/webui/www/public/setlocation.html rename to src/webui/www/private/setlocation.html index 6824b754b..ef769d80a 100644 --- a/src/webui/www/public/setlocation.html +++ b/src/webui/www/private/setlocation.html @@ -29,7 +29,7 @@ var hashesList = new URI().getData('hashes'); new Request({ - url: 'command/setLocation', + url: 'api/v2/torrents/setLocation', method: 'post', data: { hashes: hashesList, diff --git a/src/webui/www/public/statistics.html b/src/webui/www/private/statistics.html similarity index 100% rename from src/webui/www/public/statistics.html rename to src/webui/www/private/statistics.html diff --git a/src/webui/www/public/transferlist.html b/src/webui/www/private/transferlist.html similarity index 93% rename from src/webui/www/public/transferlist.html rename to src/webui/www/private/transferlist.html index 0d9b239d5..59e98a0b1 100644 --- a/src/webui/www/public/transferlist.html +++ b/src/webui/www/private/transferlist.html @@ -44,16 +44,16 @@ renameFN(); }, prioTop : function (element, ref) { - setPriorityFN('topPrio'); + setPriorityFN('top_prio'); }, prioUp : function (element, ref) { - setPriorityFN('increasePrio'); + setPriorityFN('increase_prio'); }, prioDown : function (element, ref) { - setPriorityFN('decreasePrio'); + setPriorityFN('decrease_prio'); }, prioBottom : function (element, ref) { - setPriorityFN('bottomPrio'); + setPriorityFN('bottom_prio'); }, DownloadLimit : function (element, ref) { diff --git a/src/webui/www/public/upload.html b/src/webui/www/private/upload.html similarity index 91% rename from src/webui/www/public/upload.html rename to src/webui/www/private/upload.html index 165083cec..2c1519916 100644 --- a/src/webui/www/public/upload.html +++ b/src/webui/www/private/upload.html @@ -10,12 +10,10 @@ -
-

-

- -
-

+ +
+ +
diff --git a/src/webui/www/public/uploadlimit.html b/src/webui/www/private/uploadlimit.html similarity index 95% rename from src/webui/www/public/uploadlimit.html rename to src/webui/www/private/uploadlimit.html index df98fdb29..43c808c53 100644 --- a/src/webui/www/public/uploadlimit.html +++ b/src/webui/www/private/uploadlimit.html @@ -25,7 +25,7 @@ var limit = $("uplimitUpdatevalue").value.toInt() * 1024; if (hashes[0] == "global") { new Request({ - url: 'command/setGlobalUpLimit', + url: 'api/v2/transfer/setUploadLimit', method: 'post', data: { 'limit': limit @@ -38,7 +38,7 @@ } else { new Request({ - url: 'command/setTorrentsUpLimit', + url: 'api/v2/torrents/set_upload_limit', method: 'post', data: { 'hashes': hashes.join('|'), diff --git a/src/webui/www/private/login.html b/src/webui/www/public/login.html similarity index 96% rename from src/webui/www/private/login.html rename to src/webui/www/public/login.html index 5eb115b5a..de195401a 100644 --- a/src/webui/www/private/login.html +++ b/src/webui/www/public/login.html @@ -3,6 +3,7 @@ qBittorrent QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog] +