From 27d8dbf13b89192622b517f5133da0b1622027f3 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Sat, 14 Oct 2017 16:27:21 +0300 Subject: [PATCH] Redesign Web API Normalize Web API method names. Allow to use alternative Web UI. Switch Web API version to standard form (i.e. "2.0"). Improve Web UI translation code. Retranslate changed files. Add Web API for RSS subsystem. --- src/base/CMakeLists.txt | 2 + src/base/base.pri | 2 + src/base/bittorrent/tracker.cpp | 2 +- src/base/bittorrent/tracker.h | 3 +- src/base/http/httperror.cpp | 81 + src/base/http/httperror.h | 86 + src/base/http/responsebuilder.cpp | 5 - src/base/http/responsebuilder.h | 6 +- src/base/preferences.cpp | 20 + src/base/preferences.h | 4 + src/base/utils/fs.cpp | 19 + src/base/utils/fs.h | 1 + src/base/utils/string.cpp | 18 + src/base/utils/string.h | 4 + src/gui/optionsdlg.cpp | 11 + src/gui/optionsdlg.ui | 25 + src/webui/CMakeLists.txt | 29 +- src/webui/abstractwebapplication.cpp | 525 ------ src/webui/abstractwebapplication.h | 116 -- src/webui/api/apicontroller.cpp | 91 + src/webui/api/apicontroller.h | 72 + src/webui/api/apierror.cpp | 40 + src/webui/{jsonutils.h => api/apierror.h} | 38 +- .../{prefjson.cpp => api/appcontroller.cpp} | 73 +- src/webui/api/appcontroller.h | 50 + src/webui/api/authcontroller.cpp | 108 ++ src/webui/api/authcontroller.h | 59 + src/webui/api/isessionmanager.h | 49 + src/webui/api/logcontroller.cpp | 124 ++ src/webui/{prefjson.h => api/logcontroller.h} | 25 +- src/webui/api/rsscontroller.cpp | 131 ++ src/webui/api/rsscontroller.h | 51 + src/webui/api/serialize/serialize_torrent.cpp | 140 ++ src/webui/api/serialize/serialize_torrent.h | 79 + src/webui/api/synccontroller.cpp | 473 +++++ .../synccontroller.h} | 25 +- src/webui/api/torrentscontroller.cpp | 805 +++++++++ src/webui/api/torrentscontroller.h | 74 + src/webui/api/transfercontroller.cpp | 113 ++ src/webui/api/transfercontroller.h | 49 + src/webui/btjson.cpp | 1134 ------------ src/webui/btjson.h | 62 - src/webui/webapplication.cpp | 1519 +++++++---------- src/webui/webapplication.h | 179 +- src/webui/webui.h | 4 +- src/webui/webui.pri | 29 +- src/webui/webui.qrc | 82 +- src/webui/www/{public => private}/about.html | 0 .../www/{public => private}/addtrackers.html | 7 +- .../{public => private}/confirmdeletion.html | 8 +- .../www/{public => private}/css/Core.css | 0 .../www/{public => private}/css/Layout.css | 0 .../www/{public => private}/css/Tabs.css | 0 .../www/{public => private}/css/Window.css | 0 .../{public => private}/css/dynamicTable.css | 0 src/webui/www/private/css/style.css | 481 ++++++ .../www/{public => private}/download.html | 2 +- .../{public => private}/downloadlimit.html | 4 +- .../www/{public => private}/filters.html | 0 src/webui/www/private/index.html | 1 + .../www/{public => private}/newcategory.html | 4 +- .../www/{public => private}/preferences.html | 0 .../preferences_content.html | 23 +- .../www/{public => private}/properties.html | 0 .../properties_content.html | 0 src/webui/www/{public => private}/rename.html | 2 +- .../www/{public => private}/scripts/client.js | 6 +- .../scripts/clipboard.min.js | 0 .../scripts/contextmenu.js | 0 .../{public => private}/scripts/download.js | 2 +- .../scripts/dynamicTable.js | 0 .../scripts/excanvas-compressed.js | 0 .../www/{public => private}/scripts/misc.js | 0 .../{public => private}/scripts/mocha-init.js | 106 +- .../{public => private}/scripts/mocha-yc.js | 0 .../www/{public => private}/scripts/mocha.js | 0 .../private/scripts/mootools-1.2-core-yc.js | 527 ++++++ .../scripts/mootools-1.2-more.js | 0 .../scripts/parametrics.js | 8 +- .../scripts/progressbar.js | 0 .../{public => private}/scripts/prop-files.js | 4 +- .../scripts/prop-general.js | 2 +- .../scripts/prop-trackers.js | 2 +- .../scripts/prop-webseeds.js | 2 +- .../www/{public => private}/setlocation.html | 2 +- .../www/{public => private}/statistics.html | 0 .../www/{public => private}/transferlist.html | 8 +- src/webui/www/{public => private}/upload.html | 10 +- .../www/{public => private}/uploadlimit.html | 4 +- src/webui/www/{private => public}/login.html | 3 +- 90 files changed, 4817 insertions(+), 3038 deletions(-) create mode 100644 src/base/http/httperror.cpp create mode 100644 src/base/http/httperror.h delete mode 100644 src/webui/abstractwebapplication.cpp delete mode 100644 src/webui/abstractwebapplication.h create mode 100644 src/webui/api/apicontroller.cpp create mode 100644 src/webui/api/apicontroller.h create mode 100644 src/webui/api/apierror.cpp rename src/webui/{jsonutils.h => api/apierror.h} (73%) rename src/webui/{prefjson.cpp => api/appcontroller.cpp} (90%) create mode 100644 src/webui/api/appcontroller.h create mode 100644 src/webui/api/authcontroller.cpp create mode 100644 src/webui/api/authcontroller.h create mode 100644 src/webui/api/isessionmanager.h create mode 100644 src/webui/api/logcontroller.cpp rename src/webui/{prefjson.h => api/logcontroller.h} (79%) create mode 100644 src/webui/api/rsscontroller.cpp create mode 100644 src/webui/api/rsscontroller.h create mode 100644 src/webui/api/serialize/serialize_torrent.cpp create mode 100644 src/webui/api/serialize/serialize_torrent.h create mode 100644 src/webui/api/synccontroller.cpp rename src/webui/{websessiondata.h => api/synccontroller.h} (79%) create mode 100644 src/webui/api/torrentscontroller.cpp create mode 100644 src/webui/api/torrentscontroller.h create mode 100644 src/webui/api/transfercontroller.cpp create mode 100644 src/webui/api/transfercontroller.h delete mode 100644 src/webui/btjson.cpp delete mode 100644 src/webui/btjson.h rename src/webui/www/{public => private}/about.html (100%) rename src/webui/www/{public => private}/addtrackers.html (87%) rename src/webui/www/{public => private}/confirmdeletion.html (89%) rename src/webui/www/{public => private}/css/Core.css (100%) rename src/webui/www/{public => private}/css/Layout.css (100%) rename src/webui/www/{public => private}/css/Tabs.css (100%) rename src/webui/www/{public => private}/css/Window.css (100%) rename src/webui/www/{public => private}/css/dynamicTable.css (100%) create mode 100644 src/webui/www/private/css/style.css rename src/webui/www/{public => private}/download.html (96%) rename src/webui/www/{public => private}/downloadlimit.html (95%) rename src/webui/www/{public => private}/filters.html (100%) rename src/webui/www/{public => private}/newcategory.html (96%) rename src/webui/www/{public => private}/preferences.html (100%) rename src/webui/www/{public => private}/preferences_content.html (99%) rename src/webui/www/{public => private}/properties.html (100%) rename src/webui/www/{public => private}/properties_content.html (100%) rename src/webui/www/{public => private}/rename.html (97%) rename src/webui/www/{public => private}/scripts/client.js (99%) rename src/webui/www/{public => private}/scripts/clipboard.min.js (100%) rename src/webui/www/{public => private}/scripts/contextmenu.js (100%) rename src/webui/www/{public => private}/scripts/download.js (97%) rename src/webui/www/{public => private}/scripts/dynamicTable.js (100%) rename src/webui/www/{public => private}/scripts/excanvas-compressed.js (100%) rename src/webui/www/{public => private}/scripts/misc.js (100%) rename src/webui/www/{public => private}/scripts/mocha-init.js (90%) rename src/webui/www/{public => private}/scripts/mocha-yc.js (100%) rename src/webui/www/{public => private}/scripts/mocha.js (100%) create mode 100644 src/webui/www/private/scripts/mootools-1.2-core-yc.js rename src/webui/www/{public => private}/scripts/mootools-1.2-more.js (100%) rename src/webui/www/{public => private}/scripts/parametrics.js (97%) rename src/webui/www/{public => private}/scripts/progressbar.js (100%) rename src/webui/www/{public => private}/scripts/prop-files.js (99%) rename src/webui/www/{public => private}/scripts/prop-general.js (98%) rename src/webui/www/{public => private}/scripts/prop-trackers.js (98%) rename src/webui/www/{public => private}/scripts/prop-webseeds.js (97%) rename src/webui/www/{public => private}/setlocation.html (97%) rename src/webui/www/{public => private}/statistics.html (100%) rename src/webui/www/{public => private}/transferlist.html (93%) rename src/webui/www/{public => private}/upload.html (91%) rename src/webui/www/{public => private}/uploadlimit.html (95%) rename src/webui/www/{private => public}/login.html (96%) 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" -prefjson::prefjson() +void AppController::webapiVersionAction() { + setResult(static_cast(API_VERSION)); } -QByteArray prefjson::getPreferences() +void AppController::versionAction() { - const Preferences* const pref = Preferences::instance(); + setResult(QBT_VERSION); +} + +void AppController::shutdownAction() +{ + 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) + +public: + using APIController::APIController; + +private slots: + void maindataAction(); + void torrentPeersAction(); }; - -#endif // WEBSESSIONDATA - 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" +#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" -static const int API_VERSION = 17; -static const int API_VERSION_MIN = 15; +constexpr int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024; -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; -} +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; - } -} + 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; -void WebApplication::action_public_index() -{ - QString path; + const QString locale = Preferences::instance()->getLocale(); + bool isTranslationNeeded = !locale.startsWith("en") || locale.startsWith("en_AU") || locale.startsWith("en_GB"); - if (!args_.isEmpty()) { - if (args_.back() == "favicon.ico") - path = ":/icons/skin/qbittorrent16.png"; - else - path = WWW_FOLDER + args_.join("/"); - } + 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(); - printFile(path); -} + 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, ""); -void WebApplication::action_public_webui() -{ - if (!sessionActive()) - printFile(PRIVATE_FOLDER + "login.html"); - else - printFile(PRIVATE_FOLDER + "index.html"); -} + // Use HTML code for quotes to prevent issues with JS + translation.replace("'", "'"); + translation.replace("\"", """); -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()); - - 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); - } -} - -void WebApplication::action_public_logout() -{ - CHECK_URI(0); - sessionEnd(); -} - -void WebApplication::action_public_theme() -{ - if (args_.size() != 1) { - status(404, "Not Found"); - return; - } - - 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() -{ - CHECK_URI(1); - print(btjson::getPropertiesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesTrackers() -{ - CHECK_URI(1); - print(btjson::getTrackersForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesWebSeeds() -{ - CHECK_URI(1); - print(btjson::getWebSeedsForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesFiles() -{ - CHECK_URI(1); - print(btjson::getFilesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -// 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() -{ - CHECK_URI(0); - - 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); - - bool ok = false; - int lastKnownId = request().gets["last_known_id"].toInt(&ok); - if (!ok) - lastKnownId = -1; - - print(btjson::getLog(isNormal, isInfo, isWarning, isCritical, lastKnownId), Http::CONTENT_TYPE_JSON); -} - -// 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; - - lastKnownId = request().gets["last_known_id"].toInt(&ok); - if (!ok) - lastKnownId = -1; - - print(btjson::getPeerLog(lastKnownId), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_getPieceHashes() -{ - CHECK_URI(1); - print(btjson::getPieceHashesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_getPieceStates() -{ - CHECK_URI(1); - print(btjson::getPieceStatesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -// 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); -} - -// 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); -} - - -void WebApplication::action_version_api() -{ - CHECK_URI(0); - print(QString::number(API_VERSION), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_version_api_min() -{ - CHECK_URI(0); - print(QString::number(API_VERSION_MIN), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_version_qbittorrent() -{ - CHECK_URI(0); - print(QString(QBT_VERSION), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_shutdown() -{ - qDebug() << "Shutdown request from Web UI"; - CHECK_URI(0); - - // Special case handling for shutdown, we - // need to reply to the Web UI before - // actually shutting down. - QTimer::singleShot(100, qApp, SLOT(quit())); -} - -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); + 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); } } - 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); - } + inline QUrl urlFromHostHeader(const QString &hostHeader) + { + if (!hostHeader.contains(QLatin1String("://"))) + return QUrl(QLatin1String("http://") + hostHeader); + return hostHeader; } - - if (partialSuccess) - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - else - print(QByteArray("Fails."), Http::CONTENT_TYPE_TXT); } -void WebApplication::action_command_upload() +WebApplication::WebApplication(QObject *parent) + : QObject(parent) { - CHECK_URI(0); + 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)); - 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(); + declarePublicAPI(QLatin1String("auth/login")); - 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"; + configure(); + connect(Preferences::instance(), &Preferences::changed, this, &WebApplication::configure); +} + +WebApplication::~WebApplication() +{ + // cleanup sessions data + qDeleteAll(m_sessions); +} + +void WebApplication::sendWebUIFile() +{ + const QStringList pathItems {request().path.split('/', QString::SkipEmptyParts)}; + if (pathItems.contains(".") || pathItems.contains("..")) + throw InternalServerErrorHTTPError(); + + 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; + } + + 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; + } + } + + const QString path { + (request().path != QLatin1String("/") + ? request().path + : (session() + ? QLatin1String("/index.html") + : QLatin1String("/login.html"))) + }; + + QString localPath { + m_rootFolder + + (session() ? PRIVATE_FOLDER : PUBLIC_FOLDER) + + path + }; + + QFileInfo fileInfo {localPath}; + + if (!fileInfo.exists() && session()) { + // try to send public file if there is no private one + localPath = m_rootFolder + PUBLIC_FOLDER + path; + fileInfo.setFile(localPath); + } + + if (m_isAltUIUsed) { +#ifdef Q_OS_UNIX + if (!Utils::Fs::isRegularFile(localPath)) { status(500, "Internal Server Error"); - print(QObject::tr("I/O Error: Could not create temporary file."), Http::CONTENT_TYPE_TXT); - continue; + print(tr("Unacceptable file type, only regular file is allowed."), Http::CONTENT_TYPE_TXT); + return; } +#endif - 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); - } - 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; + while (fileInfo.filePath() != m_rootFolder) { + if (fileInfo.isSymLink()) + throw InternalServerErrorHTTPError(tr("Symlinks inside alternative UI folder are forbidden.")); - 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); - } - } - // Clean up - Utils::Fs::forceRemove(filePath); - } -} - -void WebApplication::action_command_addTrackers() -{ - 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); - } -} - -void WebApplication::action_command_resumeAll() -{ - CHECK_URI(0); - - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - torrent->resume(); -} - -void WebApplication::action_command_pauseAll() -{ - CHECK_URI(0); - - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - torrent->pause(); -} - -void WebApplication::action_command_resume() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->resume(); -} - -void WebApplication::action_command_pause() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->pause(); -} - -void WebApplication::action_command_setPreferences() -{ - CHECK_URI(0); - CHECK_PARAMETERS("json"); - prefjson::setPreferences(request().posts["json"]); -} - -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); -} - -void WebApplication::action_command_getGlobalUpLimit() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->uploadSpeedLimit()), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_getGlobalDlLimit() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->downloadSpeedLimit()), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_setGlobalUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - BitTorrent::Session::instance()->setUploadSpeedLimit(limit); -} - -void WebApplication::action_command_setGlobalDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - BitTorrent::Session::instance()->setDownloadSpeedLimit(limit); -} - -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); -} - -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); -} - -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); - } -} - -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); - } -} - -void WebApplication::action_command_toggleAlternativeSpeedLimits() -{ - CHECK_URI(0); - BitTorrent::Session *const session = BitTorrent::Session::instance(); - session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled()); -} - -void WebApplication::action_command_alternativeSpeedLimitsEnabled() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled()) - , Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_toggleSequentialDownload() -{ - 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(); - } -} - -void WebApplication::action_command_toggleFirstLastPiecePrio() -{ - 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(); - } -} - -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); - } -} - -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); - } -} - -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); -} - -void WebApplication::action_command_deletePerm() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) - BitTorrent::Session::instance()->deleteTorrent(hash, true); -} - -void WebApplication::action_command_increasePrio() -{ - 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()->increaseTorrentsPriority(hashes); -} - -void WebApplication::action_command_decreasePrio() -{ - 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); -} - -void WebApplication::action_command_topPrio() -{ - 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); -} - -void WebApplication::action_command_bottomPrio() -{ - 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()->bottomTorrentsPriority(hashes); -} - -void WebApplication::action_command_setLocation() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "location"); - - QStringList hashes = request().posts["hashes"].split("|"); - QString newLocation = request().posts["location"].trimmed(); - - // check if the location exists - if (newLocation.isEmpty() || !QDir(newLocation).exists()) - return; - - 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)); - - torrent->move(Utils::Fs::expandPathAbs(newLocation)); + fileInfo.setFile(fileInfo.path()); } } + + sendFile(localPath); } -void WebApplication::action_command_rename() +WebSession *WebApplication::session() { - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "name"); - - QString hash = request().posts["hash"]; - QString name = request().posts["name"].trimmed(); - - 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"); - } + return m_currentSession; } -void WebApplication::action_command_setAutoTMM() +const Http::Request &WebApplication::request() const { - 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); - } + return m_request; } -void WebApplication::action_command_recheck() +const Http::Environment &WebApplication::env() const { - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->forceRecheck(); -} - -void WebApplication::action_command_setCategory() -{ - 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; - } - } - } -} - -void WebApplication::action_command_addCategory() -{ - CHECK_URI(0); - CHECK_PARAMETERS("category"); - - QString category = request().posts["category"].trimmed(); - - if (!BitTorrent::Session::isValidCategoryName(category) && !category.isEmpty()) { - status(400, tr("Incorrect category name")); - return; - } - - BitTorrent::Session::instance()->addCategory(category); -} - -void WebApplication::action_command_removeCategories() -{ - CHECK_URI(0); - CHECK_PARAMETERS("categories"); - - QStringList categories = request().posts["categories"].split('\n'); - foreach (const QString &category, categories) - BitTorrent::Session::instance()->removeCategory(category); -} - -void WebApplication::action_command_getSavePath() -{ - CHECK_URI(0); - print(BitTorrent::Session::instance()->defaultSavePath()); -} - -bool WebApplication::isPublicScope() -{ - return (scope_ == DEFAULT_SCOPE || scope_ == VERSION_INFO); + return m_env; } void WebApplication::doProcessRequest() { - scope_ = DEFAULT_SCOPE; - action_ = DEFAULT_ACTION; + QStringMap *params = &((request().method == QLatin1String("GET")) + ? m_request.gets : m_request.posts); + QString scope, action; - parsePath(); + const auto findAPICall = [&]() -> bool + { + QRegularExpressionMatch match = m_apiPathPattern.match(request().path); + if (!match.hasMatch()) return false; - if (args_.contains(".") || args_.contains("..")) { - qDebug() << Q_FUNC_INFO << "Invalid path:" << request().path; - status(404, "Not Found"); - return; - } + action = match.captured(QLatin1String("action")); + scope = match.captured(QLatin1String("scope")); + return true; + }; - if (!isPublicScope() && !sessionActive()) { - status(403, "Forbidden"); - return; - } + const auto findLegacyAPICall = [&]() -> bool + { + QRegularExpressionMatch match = m_apiLegacyPathPattern.match(request().path); + if (!match.hasMatch()) return false; - if (actions_.value(scope_).value(action_) != 0) { - (this->*(actions_[scope_][action_]))(); + 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 (request().path == QLatin1String("/version/api_min")) { + print(QString(COMPAT_API_VERSION_MIN), Http::CONTENT_TYPE_TXT); + return; + } + + sendWebUIFile(); } else { - status(404, "Not Found"); - qDebug() << Q_FUNC_INFO << "Resource not found:" << request().path; + 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; + } + } + 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); + } + } } } -void WebApplication::parsePath() +void WebApplication::configure() { - if (request().path == "/") action_ = WEBUI_ACTION; + const auto pref = Preferences::instance(); - // 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(); + m_domainList = Preferences::instance()->getServerDomains().split(';', QString::SkipEmptyParts); + std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); }); + + const QString rootFolder = Utils::Fs::expandPathAbs( + !pref->isAltWebUiEnabled() ? WWW_FOLDER : pref->getWebUiRootFolder()); + if (rootFolder != m_rootFolder) { + m_translatedFiles.clear(); + m_rootFolder = rootFolder; } - - if (!pathItems.empty() && actions_[scope_].contains(pathItems.front())) { - action_ = pathItems.front(); - pathItems.pop_front(); - } - - args_ = pathItems; } -WebApplication::WebApplication(QObject *parent) - : AbstractWebApplication(parent) +void WebApplication::registerAPIController(const QString &scope, APIController *controller) { + Q_ASSERT(controller); + Q_ASSERT(!m_apiControllers.value(scope)); + + m_apiControllers[scope] = controller; } -QMap > WebApplication::actions_ = WebApplication::initializeActions(); +void WebApplication::declarePublicAPI(const QString &apiPath) +{ + m_publicAPIs << apiPath; +} + +void WebApplication::sendFile(const QString &path) +{ + const QDateTime lastModified {QFileInfo(path).lastModified()}; + + // 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; + } + + 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))); + } + + QByteArray data {file.readAll()}; + file.close(); + + const QMimeType type {QMimeDatabase().mimeTypeForFileNameAndData(path, data)}; + const bool isTranslatable {type.inherits(QLatin1String("text/plain"))}; + + // Translate the file + if (isTranslatable) { + QString dataStr {data}; + translateDocument(dataStr); + data = dataStr.toUtf8(); + + m_translatedFiles[path] = {data, lastModified}; // caching translated file + } + + print(data, type.name()); +} + +Http::Response WebApplication::processRequest(const Http::Request &request, const Http::Environment &env) +{ + m_currentSession = nullptr; + m_request = request; + m_env = env; + + // clear response + clear(); + + try { + // block cross-site requests + if (isCrossSiteRequest(m_request) || !validateHostHeader(m_domainList)) + throw UnauthorizedHTTPError(); + + sessionInitialize(); + doProcessRequest(); + } + catch (const HTTPError &error) { + status(error.statusCode(), error.statusText()); + if (!error.message().isEmpty()) + print(error.message(), Http::CONTENT_TYPE_TXT); + } + + // 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';"); + + return response(); +} + +QString WebApplication::clientId() const +{ + return env().clientAddress.toString(); +} + +void WebApplication::sessionInitialize() +{ + Q_ASSERT(!m_currentSession); + + const QString sessionId {parseCookie(m_request.headers.value(QLatin1String("cookie"))).value(C_SID)}; + + // 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!"; + } + } + + if (!m_currentSession && !isAuthNeeded()) + sessionStart(); +} + +QString WebApplication::generateSid() const +{ + 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 (m_sessions.contains(sid)); + + return sid; +} + +bool WebApplication::isAuthNeeded() +{ + 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; +} + +bool WebApplication::isPublicAPI(const QString &scope, const QString &action) const +{ + return m_publicAPIs.contains(QString::fromLatin1("%1/%2").arg(scope).arg(action)); +} + +void WebApplication::sessionStart() +{ + Q_ASSERT(!m_currentSession); + + // 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()); + } + + 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::sessionEnd() +{ + Q_ASSERT(m_currentSession); + + QNetworkCookie cookie(C_SID); + cookie.setPath(QLatin1String("/")); + cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1)); + + delete m_sessions.take(m_currentSession->id()); + m_currentSession = nullptr; + + header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); +} + +bool WebApplication::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) + 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; + } + + 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; + } + + return true; +} + +bool WebApplication::validateHostHeader(const QStringList &domains) const +{ + 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; + } + + // 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 + + 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; + } + + 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; +} + +// WebSession + +WebSession::WebSession(const QString &sid) + : m_sid {sid} +{ + updateTimestamp(); +} + +QString WebSession::id() const +{ + return m_sid; +} + +uint WebSession::timestamp() const +{ + return m_timestamp; +} + +QVariant WebSession::getData(const QString &id) const +{ + return m_data.value(id); +} + +void WebSession::setData(const QString &id, const QVariant &data) +{ + m_data[id] = data; +} + +void WebSession::updateTimestamp() +{ + m_timestamp = QDateTime::currentDateTime().toTime_t(); +} 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(); + void doProcessRequest(); + void configure(); - typedef void (WebApplication::*Action)(); + void registerAPIController(const QString &scope, APIController *controller); + void declarePublicAPI(const QString &apiPath); - QString scope_; - QString action_; - QStringList args_; + void sendFile(const QString &path); + void sendWebUIFile(); - void doProcessRequest() override; + // Session management + QString generateSid() const; + void sessionInitialize(); + bool isAuthNeeded(); + bool isPublicAPI(const QString &scope, const QString &action) const; - bool isPublicScope(); - void parsePath(); + bool isCrossSiteRequest(const Http::Request &request) const; + bool validateHostHeader(const QStringList &domains) const; - static QMap > initializeActions(); - static QMap > actions_; + // 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; }; - -#endif // WEBAPPLICATION_H 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] +