Merge pull request #7610 from glassez/webapi2

Redesign Web API
This commit is contained in:
Vladimir Golovnev 2018-02-16 10:09:35 +03:00 committed by GitHub
commit d07ece53e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 5007 additions and 3125 deletions

View file

@ -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
@ -51,6 +52,7 @@ utils/random.h
utils/string.h
utils/version.h
asyncfilestorage.h
exceptions.h
filesystemwatcher.h
global.h
iconprovider.h
@ -84,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
@ -113,6 +116,7 @@ utils/net.cpp
utils/random.cpp
utils/string.cpp
asyncfilestorage.cpp
exceptions.cpp
filesystemwatcher.cpp
iconprovider.cpp
logger.cpp

View file

@ -17,9 +17,11 @@ HEADERS += \
$$PWD/bittorrent/torrentinfo.h \
$$PWD/bittorrent/tracker.h \
$$PWD/bittorrent/trackerentry.h \
$$PWD/exceptions.h \
$$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 \
@ -82,8 +84,10 @@ SOURCES += \
$$PWD/bittorrent/torrentinfo.cpp \
$$PWD/bittorrent/tracker.cpp \
$$PWD/bittorrent/trackerentry.cpp \
$$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 \

View file

@ -60,23 +60,32 @@ TorrentInfo &TorrentInfo::operator=(const TorrentInfo &other)
return *this;
}
TorrentInfo TorrentInfo::loadFromFile(const QString &path, QString &error)
TorrentInfo TorrentInfo::load(const QByteArray &data, QString *error) noexcept
{
error.clear();
libt::error_code ec;
TorrentInfo info(NativePtr(new libt::torrent_info(Utils::Fs::toNativePath(path).toStdString(), ec)));
if (ec) {
error = QString::fromUtf8(ec.message().c_str());
qDebug("Cannot load .torrent file: %s", qUtf8Printable(error));
TorrentInfo info(NativePtr(new libt::torrent_info(data.constData(), data.size(), ec)));
if (error) {
if (ec)
*error = QString::fromStdString(ec.message());
else
error->clear();
}
return info;
}
TorrentInfo TorrentInfo::loadFromFile(const QString &path)
TorrentInfo TorrentInfo::loadFromFile(const QString &path, QString *error) noexcept
{
QString error;
return loadFromFile(path, error);
libt::error_code ec;
TorrentInfo info(NativePtr(new libt::torrent_info(Utils::Fs::toNativePath(path).toStdString(), ec)));
if (error) {
if (ec)
*error = QString::fromStdString(ec.message());
else
error->clear();
}
return info;
}
bool TorrentInfo::isValid() const

View file

@ -63,8 +63,8 @@ namespace BitTorrent
explicit TorrentInfo(NativeConstPtr nativeInfo = NativeConstPtr());
TorrentInfo(const TorrentInfo &other);
static TorrentInfo loadFromFile(const QString &path, QString &error);
static TorrentInfo loadFromFile(const QString &path);
static TorrentInfo load(const QByteArray &data, QString *error = nullptr) noexcept;
static TorrentInfo loadFromFile(const QString &path, QString *error = nullptr) noexcept;
TorrentInfo &operator=(const TorrentInfo &other);

View file

@ -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))
{
}

View file

@ -31,6 +31,7 @@
#define BITTORRENT_TRACKER_H
#include <QHash>
#include <QObject>
#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)

40
src/base/exceptions.cpp Normal file
View file

@ -0,0 +1,40 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 "exceptions.h"
RuntimeError::RuntimeError(const QString &message)
: std::runtime_error {message.toUtf8().data()}
, m_message {message}
{
}
QString RuntimeError::message() const
{
return m_message;
}

View file

@ -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 <glassez@yandex.ru>
*
* 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,19 @@
* 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 <stdexcept>
#include <QString>
class prefjson
class RuntimeError : public std::runtime_error
{
private:
prefjson();
public:
static QByteArray getPreferences();
static void setPreferences(const QString& json);
explicit RuntimeError(const QString &message = "");
QString message() const;
private:
const QString m_message;
};
#endif // PREFJSON_H

View file

@ -0,0 +1,81 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
*
* 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)
{
}

86
src/base/http/httperror.h Normal file
View file

@ -0,0 +1,86 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 = "");
};

View file

@ -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);

View file

@ -29,17 +29,13 @@
#ifndef HTTP_RESPONSEBUILDER_H
#define HTTP_RESPONSEBUILDER_H
#include <QObject>
#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);

View file

@ -37,6 +37,9 @@
namespace Http
{
const char METHOD_GET[] = "GET";
const char METHOD_POST[] = "POST";
const char HEADER_CACHE_CONTROL[] = "cache-control";
const char HEADER_CONTENT_ENCODING[] = "content-encoding";
const char HEADER_CONTENT_LENGTH[] = "content-length";
@ -52,13 +55,14 @@ namespace Http
const char HEADER_X_FRAME_OPTIONS[] = "x-frame-options";
const char HEADER_X_XSS_PROTECTION[] = "x-xss-protection";
const char CONTENT_TYPE_CSS[] = "text/css; charset=UTF-8";
const char CONTENT_TYPE_GIF[] = "image/gif";
const char CONTENT_TYPE_HTML[] = "text/html; charset=UTF-8";
const char CONTENT_TYPE_JS[] = "application/javascript; charset=UTF-8";
const char CONTENT_TYPE_HTML[] = "text/html";
const char CONTENT_TYPE_JS[] = "application/javascript";
const char CONTENT_TYPE_JSON[] = "application/json";
const char CONTENT_TYPE_BMP[] = "image/bmp";
const char CONTENT_TYPE_GIF[] = "image/gif";
const char CONTENT_TYPE_JPEG[] = "image/jpeg";
const char CONTENT_TYPE_PNG[] = "image/png";
const char CONTENT_TYPE_TXT[] = "text/plain; charset=UTF-8";
const char CONTENT_TYPE_TXT[] = "text/plain";
const char CONTENT_TYPE_SVG[] = "image/svg+xml";
// portability: "\r\n" doesn't guarantee mapping to the correct value

View file

@ -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();

View file

@ -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;

View file

@ -30,6 +30,10 @@
#include "fs.h"
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <QDebug>
#include <QDir>
#include <QFile>
@ -47,6 +51,7 @@
#include <kernel/fs_info.h>
#else
#include <sys/vfs.h>
#include <unistd.h>
#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;
}

View file

@ -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);

View file

@ -40,6 +40,8 @@
#include <QThreadStorage>
#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;
}

View file

@ -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);
}
}

View file

@ -286,7 +286,7 @@ bool AddNewTorrentDialog::loadTorrent(const QString &torrentPath)
m_hasMetadata = true;
QString error;
m_torrentInfo = BitTorrent::TorrentInfo::loadFromFile(m_filePath, error);
m_torrentInfo = BitTorrent::TorrentInfo::loadFromFile(m_filePath, &error);
if (!m_torrentInfo.isValid()) {
MessageBoxRaised::critical(this, tr("Invalid torrent"), tr("Failed to load the torrent: %1.\nError: %2", "Don't remove the '\n' characters. They insert a newline.").arg(Utils::Fs::toNativePath(m_filePath)).arg(error));
return false;

View file

@ -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
}

View file

@ -3014,6 +3014,31 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupAltWebUI">
<property name="title">
<string>Use alternative Web UI</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QLabel" name="labelWebUIRootFolder">
<property name="text">
<string>Files location:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="FileSystemPathLineEdit" name="textWebUIRootFolder" native="true"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="checkDynDNS">
<property name="title">

View file

@ -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
)

View file

@ -1,525 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <algorithm>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QNetworkCookie>
#include <QTemporaryFile>
#include <QTimer>
#include <QUrl>
#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<UnbanTimer*>(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);
if (path.endsWith("about.html") || path.endsWith("index.html") || path.endsWith("client.js"))
dataStr.replace("${VERSION}", QBT_VERSION);
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<const char *>(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("'", "&#39;");
translation.replace("\"", "&#34;");
data.replace(i, regex.matchedLength(), translation);
i += translation.length();
}
else {
found = false; // no more translatable strings
}
}
}
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<QStringRef> 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;
}

View file

@ -1,116 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QHash>
#include <QMap>
#include <QObject>
#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<QString, WebSession *> sessions_;
QHash<QHostAddress, int> clientFailedAttempts_;
QMap<QString, QByteArray> 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

View file

@ -0,0 +1,91 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QJsonDocument>
#include <QMetaObject>
#include "apierror.h"
APIController::APIController(ISessionManager *sessionManager, QObject *parent)
: QObject {parent}
, m_sessionManager {sessionManager}
{
}
QVariant APIController::run(const QString &action, const StringMap &params, 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<QString> &requiredParams) const
{
const QSet<QString> 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);
}

View file

@ -0,0 +1,72 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QMap>
#include <QObject>
#include <QSet>
#include <QString>
#include <QVariant>
struct ISessionManager;
using StringMap = QMap<QString, QString>;
using DataMap = QMap<QString, QByteArray>;
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 &params, const DataMap &data = {});
ISessionManager *sessionManager() const;
protected:
const StringMap &params() const;
const DataMap &data() const;
void checkParams(const QSet<QString> &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;
};

View file

@ -0,0 +1,40 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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;
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QVariant>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#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;
};

View file

@ -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 <glassez@yandex.ru>
* Copyright (C) 2006-2012 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2006-2012 Ishan Arora <ishan@qbittorrent.org>
*
* 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 <QCoreApplication>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QStringList>
#include <QTimer>
#include <QTranslator>
#ifndef QT_NO_OPENSSL
#include <QSslCertificate>
#include <QSslKey>
#endif
#include <QStringList>
#include <QTranslator>
#include <QRegularExpression>
#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<QString>(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());
}

View file

@ -0,0 +1,50 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006-2012 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2006-2012 Ishan Arora <ishan@qbittorrent.org>
*
* 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();
};

View file

@ -0,0 +1,108 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QCryptographicHash>
#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();
}
}

View file

@ -0,0 +1,59 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QHash>
#include <QString>
#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<QString, FailedLogin> m_clientFailedLogins;
};

View file

@ -0,0 +1,49 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QString>
#include <QVariant>
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;
};

View file

@ -0,0 +1,124 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QJsonArray>
#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));
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QVariant>
#include "apicontroller.h"
struct WebSessionData
class LogController : public APIController
{
QVariantMap syncMainDataLastResponse;
QVariantMap syncMainDataLastAcceptedResponse;
QVariantMap syncTorrentPeersLastResponse;
QVariantMap syncTorrentPeersLastAcceptedResponse;
Q_OBJECT
Q_DISABLE_COPY(LogController)
public:
using APIController::APIController;
private slots:
void mainAction();
void peersAction();
};
#endif // WEBSESSIONDATA

View file

@ -0,0 +1,131 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#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<RSS::AutoDownloadRule> rules {RSS::AutoDownloader::instance()->rules()};
QJsonObject jsonObj;
for (const auto &rule : rules)
jsonObj.insert(rule.name(), rule.toJsonObject());
setResult(jsonObj);
}

View file

@ -0,0 +1,51 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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();
};

View file

@ -0,0 +1,140 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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;
}

View file

@ -0,0 +1,79 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QVariantMap>
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);

View file

@ -0,0 +1,473 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QJsonObject>
#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<qreal>(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<QMetaType::Type>(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<QMetaType::Type>(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<int>(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<BitTorrent::PeerInfo> 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);
}

View file

@ -0,0 +1,44 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 SyncController : public APIController
{
Q_OBJECT
Q_DISABLE_COPY(SyncController)
public:
using APIController::APIController;
private slots:
void maindataAction();
void torrentPeersAction();
};

View file

@ -0,0 +1,805 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <functional>
#include <QBitArray>
#include <QDir>
#include <QJsonArray>
#include <QJsonObject>
#include <QList>
#include <QNetworkCookie>
#include <QRegularExpression>
#include <QUrl>
#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<void (BitTorrent::TorrentHandle *torrent)> &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<int>(torrent->lastSeenComplete().toTime_t()) : -1;
dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? static_cast<int>(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<QString, BitTorrent::TrackerInfo> 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<int> priorities = torrent->filePriorities();
const QVector<qreal> fp = torrent->filesProgress();
const QVector<qreal> 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<QByteArray> 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<int>(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<QNetworkCookie> 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<BitTorrent::TrackerEntry> 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);
}

View file

@ -0,0 +1,74 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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();
};

View file

@ -0,0 +1,113 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QJsonObject>
#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<qint64>(sessionStatus.payloadDownloadRate);
dict[KEY_TRANSFER_DLDATA] = static_cast<qint64>(sessionStatus.totalPayloadDownload);
dict[KEY_TRANSFER_UPSPEED] = static_cast<qint64>(sessionStatus.payloadUploadRate);
dict[KEY_TRANSFER_UPDATA] = static_cast<qint64>(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<qint64>(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()));
}

View file

@ -0,0 +1,49 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
*
* 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();
};

File diff suppressed because it is too large Load diff

View file

@ -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 <QCoreApplication>
#include <QString>
#include <QVariant>
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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014, 2017 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 <QStringList>
#include "abstractwebapplication.h"
#include <QDateTime>
#include <QHash>
#include <QMap>
#include <QObject>
#include <QRegularExpression>
#include <QSet>
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<int, 3, 2> 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<QString, QMap<QString, Action> > initializeActions();
static QMap<QString, QMap<QString, Action> > actions_;
// Persistent data
QMap<QString, WebSession *> m_sessions;
// Current data
WebSession *m_currentSession = nullptr;
Http::Request m_request;
Http::Environment m_env;
const QRegularExpression m_apiPathPattern {(QLatin1String("^/api/v2/(?<scope>[A-Za-z_][A-Za-z_0-9]*)/(?<action>[A-Za-z_][A-Za-z_0-9]*)$"))};
const QRegularExpression m_apiLegacyPathPattern {QLatin1String("^/(?<action>((sync|control|query)/[A-Za-z_][A-Za-z_0-9]*|login|logout))(/(?<hash>[^/]+))?$")};
QHash<QString, APIController *> m_apiControllers;
QSet<QString> m_publicAPIs;
bool m_isAltUIUsed = false;
QString m_rootFolder;
QStringList m_domainList;
struct TranslatedFile
{
QByteArray data;
QDateTime lastModified;
};
QMap<QString, TranslatedFile> m_translatedFiles;
};
#endif // WEBAPPLICATION_H

View file

@ -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<Http::Server> m_httpServer;
QPointer<Net::DNSUpdater> m_dnsUpdater;
QPointer<AbstractWebApplication> m_webapp;
QPointer<WebApplication> m_webapp;
quint16 m_port;
};

View file

@ -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

View file

@ -1,47 +1,49 @@
<RCC>
<qresource prefix="/">
<file>www/private/css/Core.css</file>
<file>www/private/css/dynamicTable.css</file>
<file>www/private/css/Layout.css</file>
<file>www/private/css/style.css</file>
<file>www/private/css/Tabs.css</file>
<file>www/private/css/Window.css</file>
<file>www/private/scripts/client.js</file>
<file>www/private/scripts/clipboard.min.js</file>
<file>www/private/scripts/contextmenu.js</file>
<file>www/private/scripts/download.js</file>
<file>www/private/scripts/dynamicTable.js</file>
<file>www/private/scripts/excanvas-compressed.js</file>
<file>www/private/scripts/misc.js</file>
<file>www/private/scripts/mocha.js</file>
<file>www/private/scripts/mocha-init.js</file>
<file>www/private/scripts/mocha-yc.js</file>
<file>www/private/scripts/mootools-1.2-core-yc.js</file>
<file>www/private/scripts/mootools-1.2-more.js</file>
<file>www/private/scripts/parametrics.js</file>
<file>www/private/scripts/progressbar.js</file>
<file>www/private/scripts/prop-files.js</file>
<file>www/private/scripts/prop-general.js</file>
<file>www/private/scripts/prop-trackers.js</file>
<file>www/private/scripts/prop-webseeds.js</file>
<file>www/private/about.html</file>
<file>www/private/addtrackers.html</file>
<file>www/private/confirmdeletion.html</file>
<file>www/private/download.html</file>
<file>www/private/downloadlimit.html</file>
<file>www/private/filters.html</file>
<file>www/private/index.html</file>
<file>www/private/login.html</file>
<file>www/public/about.html</file>
<file>www/public/addtrackers.html</file>
<file>www/public/confirmdeletion.html</file>
<file>www/public/css/Core.css</file>
<file>www/public/css/dynamicTable.css</file>
<file>www/public/css/Layout.css</file>
<file>www/private/newcategory.html</file>
<file>www/private/preferences.html</file>
<file>www/private/preferences_content.html</file>
<file>www/private/properties.html</file>
<file>www/private/properties_content.html</file>
<file>www/private/rename.html</file>
<file>www/private/setlocation.html</file>
<file>www/private/statistics.html</file>
<file>www/private/transferlist.html</file>
<file>www/private/upload.html</file>
<file>www/private/uploadlimit.html</file>
<file>www/public/css/style.css</file>
<file>www/public/css/Tabs.css</file>
<file>www/public/css/Window.css</file>
<file>www/public/download.html</file>
<file>www/public/downloadlimit.html</file>
<file>www/public/filters.html</file>
<file>www/public/newcategory.html</file>
<file>www/public/preferences.html</file>
<file>www/public/preferences_content.html</file>
<file>www/public/properties.html</file>
<file>www/public/properties_content.html</file>
<file>www/public/rename.html</file>
<file>www/public/scripts/client.js</file>
<file>www/public/scripts/clipboard.min.js</file>
<file>www/public/scripts/contextmenu.js</file>
<file>www/public/scripts/download.js</file>
<file>www/public/scripts/dynamicTable.js</file>
<file>www/public/scripts/excanvas-compressed.js</file>
<file>www/public/scripts/misc.js</file>
<file>www/public/scripts/mocha-init.js</file>
<file>www/public/scripts/mocha-yc.js</file>
<file>www/public/scripts/mocha.js</file>
<file>www/public/scripts/mootools-1.2-core-yc.js</file>
<file>www/public/scripts/mootools-1.2-more.js</file>
<file>www/public/scripts/parametrics.js</file>
<file>www/public/scripts/progressbar.js</file>
<file>www/public/scripts/prop-files.js</file>
<file>www/public/scripts/prop-general.js</file>
<file>www/public/scripts/prop-trackers.js</file>
<file>www/public/scripts/prop-webseeds.js</file>
<file>www/public/setlocation.html</file>
<file>www/public/statistics.html</file>
<file>www/public/transferlist.html</file>
<file>www/public/upload.html</file>
<file>www/public/uploadlimit.html</file>
<file>www/public/login.html</file>
</qresource>
</RCC>

View file

@ -1,4 +1,4 @@
<img src="images/skin/mascot.png" align="right"></img>
<img src="images/skin/mascot.png" style="float: right;" alt="qBittorrent Mascot" />
<h3>qBittorrent ${VERSION} QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]</h3>
<p>QBT_TR(An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.)QBT_TR[CONTEXT=about]</p>
<p>Copyright (c) 2011-2018 The qBittorrent project</p>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Trackers addition dialog)QBT_TR[CONTEXT=TrackersAdditionDlg]</title>
@ -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();
}
@ -25,12 +28,12 @@
</script>
</head>
<body>
<center>
<div style="text-align: center;">
<br/>
<h2 class="vcenter">QBT_TR(List of trackers to add (one per line):)QBT_TR[CONTEXT=TrackersAdditionDlg]</h2>
<textarea name="list" id="trackersUrls" rows="10" cols="1"></textarea>
<br/>
<input type="button" value="QBT_TR(Add)QBT_TR[CONTEXT=HttpServer]" id="addTrackersButton"/>
</center>
</div>
</body>
</html>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Deletion confirmation - qBittorrent)QBT_TR[CONTEXT=confirmDeletionDlg]</title>
@ -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();

View file

@ -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;
}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Add Torrent Links)QBT_TR[CONTEXT=downloadFromURL]</title>
@ -10,8 +10,8 @@
</head>
<body>
<iframe id="download_frame" name="download_frame" class="invisible" src="javascript:false;"></iframe>
<form action="command/download" enctype="multipart/form-data" method="post" id="downloadForm" style="text-align: center;" target="download_frame">
<center>
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="downloadForm" style="text-align: center;" target="download_frame">
<div style="text-align: center;">
<br/>
<h2 class="vcenter">QBT_TR(Download Torrents from their URLs or Magnet links)QBT_TR[CONTEXT=HttpServer]</h2>
<textarea id="urls" rows="10" name="urls"></textarea>
@ -27,7 +27,7 @@
</div>
<div class="formRow">
<label for="rename" class="leftLabelLarge">QBT_TR(Rename torrent)QBT_TR[CONTEXT=HttpServer]</label>
<input type="text" name="rename" style="width: 16em;"/>
<input type="text" id="rename" name="rename" style="width: 16em;"/>
</div>
<div class="formRow">
<label for="category" class="leftLabelLarge">QBT_TR(Category:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
@ -40,33 +40,33 @@
</div>
<div class="formRow">
<label for="skip_checking" class="leftLabelLarge">QBT_TR(Skip hash check)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
<input type="checkbox" name="skip_checking" value="true"/>
<input type="checkbox" id="skip_checking" name="skip_checking" value="true"/>
</div>
<div class="formRow">
<label for="root_folder" class="leftLabelLarge">QBT_TR(Create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
<input type="checkbox" name="root_folder" value="true" checked="checked"/>
</div>
<input type="checkbox" id="root_folder" name="root_folder" value="true" checked="checked"/>
</div>
<div class="formRow">
<label for="sequentialDownload" class="leftLabelLarge">QBT_TR(Download in sequential order)QBT_TR[CONTEXT=TransferListWidget]</label>
<input type="checkbox" name="sequentialDownload" value="true"/>
<input type="checkbox" id="sequentialDownload" name="sequentialDownload" value="true"/>
</div>
<div class="formRow">
<label for="firstLastPiecePrio" class="leftLabelLarge">QBT_TR(Download first and last pieces first)QBT_TR[CONTEXT=TransferListWidget]</label>
<input type="checkbox" name="firstLastPiecePrio" value="true"/>
<input type="checkbox" id="firstLastPiecePrio" name="firstLastPiecePrio" value="true"/>
</div>
<div class="formRow">
<label for="dlLimit" class="leftLabelLarge">QBT_TR(Limit download rate)QBT_TR[CONTEXT=HttpServer]</label>
<input type="text" name="dlLimit" style="width: 16em;" placeholder="Bytes/s"/>
<input type="text" id="dlLimit" name="dlLimit" style="width: 16em;" placeholder="Bytes/s"/>
</div>
<div class="formRow">
<label for="upLimit" class="leftLabelLarge">QBT_TR(Limit upload rate)QBT_TR[CONTEXT=HttpServer]</label>
<input type="text" name="upLimit" style="width: 16em;" placeholder="Bytes/s"/>
<input type="text" id="upLimit" name="upLimit" style="width: 16em;" placeholder="Bytes/s"/>
</div>
<div id="submitbutton" style="margin-top: 12px; text-align: center;">
<button type="submit" id="submitButton">QBT_TR(Download)QBT_TR[CONTEXT=downloadFromURL]</button>
</div>
</center>
</fieldset>
</div>
</form>
<script type="text/javascript">

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]</title>
@ -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('|'),

View file

@ -1,14 +1,14 @@
<span class="filterTitle">QBT_TR(Status)QBT_TR[CONTEXT=TransferListFiltersWidget]</span>
<ul class="filterList">
<li id="all_filter"><a href="#" onclick="setFilter('all');return false;"><img src="images/skin/filterall.png"/>QBT_TR(All (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="downloading_filter"><a href="#" onclick="setFilter('downloading');return false;"><img src="images/skin/downloading.png"/>QBT_TR(Downloading (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="seeding_filter"><a href="#" onclick="setFilter('seeding');return false;"><img src="images/skin/uploading.png"/>QBT_TR(Seeding (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="completed_filter"><a href="#" onclick="setFilter('completed');return false;"><img src="images/skin/completed.png"/>QBT_TR(Completed (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="resumed_filter"><a href="#" onclick="setFilter('resumed');return false;"><img src="images/skin/resumed.png"/>QBT_TR(Resumed (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="paused_filter"><a href="#" onclick="setFilter('paused');return false;"><img src="images/skin/paused.png"/>QBT_TR(Paused (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="active_filter"><a href="#" onclick="setFilter('active');return false;"><img src="images/skin/filteractive.png"/>QBT_TR(Active (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="inactive_filter"><a href="#" onclick="setFilter('inactive');return false;"><img src="images/skin/filterinactive.png"/>QBT_TR(Inactive (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="errored_filter"><a href="#" onclick="setFilter('errored');return false;"><img src="images/skin/error.png"/>QBT_TR(Errored (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="all_filter"><a href="#" onclick="setFilter('all');return false;"><img src="images/skin/filterall.png" alt="All" />QBT_TR(All (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="downloading_filter"><a href="#" onclick="setFilter('downloading');return false;"><img src="images/skin/downloading.png" alt="Downloading" />QBT_TR(Downloading (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="seeding_filter"><a href="#" onclick="setFilter('seeding');return false;"><img src="images/skin/uploading.png" alt="Seeding" />QBT_TR(Seeding (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="completed_filter"><a href="#" onclick="setFilter('completed');return false;"><img src="images/skin/completed.png" alt="Completed" />QBT_TR(Completed (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="resumed_filter"><a href="#" onclick="setFilter('resumed');return false;"><img src="images/skin/resumed.png" alt="Resumed" />QBT_TR(Resumed (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="paused_filter"><a href="#" onclick="setFilter('paused');return false;"><img src="images/skin/paused.png" alt="Paused" />QBT_TR(Paused (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="active_filter"><a href="#" onclick="setFilter('active');return false;"><img src="images/skin/filteractive.png" alt="Active" />QBT_TR(Active (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="inactive_filter"><a href="#" onclick="setFilter('inactive');return false;"><img src="images/skin/filterinactive.png" alt="Inactive" />QBT_TR(Inactive (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
<li id="errored_filter"><a href="#" onclick="setFilter('errored');return false;"><img src="images/skin/error.png" alt="Errored" />QBT_TR(Errored (0))QBT_TR[CONTEXT=StatusFiltersWidget]</a></li>
</ul>
<br/>
<span class="filterTitle">QBT_TR(Categories)QBT_TR[CONTEXT=TransferListFiltersWidget]</span>

View file

@ -1,9 +1,10 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=10; IE=9; IE=8;" />
<meta http-equiv="X-UA-Compatible" content="IE=10" />
<title>qBittorrent ${VERSION} QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]</title>
<link rel="icon" type="image/png" href="images/skin/qbittorrent16.png" />
<link rel="stylesheet" href="css/dynamicTable.css" type="text/css" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<!--<link rel="stylesheet" type="text/css" href="css/Content.css" />-->
@ -50,12 +51,10 @@
<li class="divider"><a id="resumeLink"><img class="MyMenuIcon" alt="QBT_TR(&Resume)QBT_TR[CONTEXT=MainWindow]" src="theme/media-playback-start" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(&Resume)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="pauseLink"><img class="MyMenuIcon" src="theme/media-playback-pause" alt="QBT_TR(&Pause)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(&Pause)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="deleteLink"><img class="MyMenuIcon" src="theme/list-remove" alt="QBT_TR(&Delete)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(&Delete)QBT_TR[CONTEXT=MainWindow]</a></li>
<span id="queueingLinks">
<li class="divider"><a id="topPrioLink"><img class="MyMenuIcon" src="theme/go-top" alt="QBT_TR(Top Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Top Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="increasePrioLink"><img class="MyMenuIcon" src="theme/go-up" alt="QBT_TR(Increase Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Increase Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="decreasePrioLink"><img class="MyMenuIcon" src="theme/go-down" alt="QBT_TR(Decrease Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Decrease Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li><a id="bottomPrioLink"><img class="MyMenuIcon" src="theme/go-bottom" alt="QBT_TR(Minimum Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Minimum Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
</span>
<li id="topPrioItem" class="divider"><a id="topPrioLink"><img class="MyMenuIcon" src="theme/go-top" alt="QBT_TR(Top Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Top Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="increasePrioItem"><a id="increasePrioLink"><img class="MyMenuIcon" src="theme/go-up" alt="QBT_TR(Increase Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Increase Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="decreasePrioItem"><a id="decreasePrioLink"><img class="MyMenuIcon" src="theme/go-down" alt="QBT_TR(Decrease Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Decrease Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li id="bottomPrioItem"><a id="bottomPrioLink"><img class="MyMenuIcon" src="theme/go-bottom" alt="QBT_TR(Minimum Priority)QBT_TR[CONTEXT=MainWindow]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Minimum Priority)QBT_TR[CONTEXT=MainWindow]</a></li>
<li class="divider"><a id="recheckLink"><img class="MyMenuIcon" src="theme/document-edit-verify" alt="QBT_TR(Force Recheck)QBT_TR[CONTEXT=TransferListWidget]" width="16" height="16" onload="fixPNG(this)"/>QBT_TR(Force recheck)QBT_TR[CONTEXT=TransferListWidget]</a></li>
</ul>
</li>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]</title>
@ -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,

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]</title>

View file

@ -17,7 +17,7 @@
<label for="appendext_checkbox">QBT_TR(Append .!qB extension to incomplete files)QBT_TR[CONTEXT=OptionsDialog]</label>
</span><br/><br/>
QBT_TR(Automatically add torrents from:)QBT_TR[CONTEXT=OptionsDialog]<br/>
<table border="1" id="watched_folders_tab">
<table id="watched_folders_tab" style="border: 1px solid black;">
<thead><tr><th>QBT_TR(Monitored Folder)QBT_TR[CONTEXT=ScanFoldersModel]</th><th>QBT_TR(Override Save Location)QBT_TR[CONTEXT=ScanFoldersModel]</th></tr></thead>
<tbody></tbody>
<tfoot><tr>
@ -70,7 +70,7 @@
<legend><input type="checkbox" id="autorun_checkbox" onclick="updateAutoRun();"/>
<label for="autorun_checkbox">QBT_TR(Run external program on torrent completion)QBT_TR[CONTEXT=OptionsDialog]</label></legend>
<input type="text" id="autorunProg_txt" style="width: 400px;"/><br/>
<i>QBT_TR(Supported parameters (case sensitive):)QBT_TR[CONTEXT=OptionsDialog]
<div style="font-style: italic;">QBT_TR(Supported parameters (case sensitive):)QBT_TR[CONTEXT=OptionsDialog]
<ul>
<li>QBT_TR(%N: Torrent name)QBT_TR[CONTEXT=OptionsDialog]</li>
<li>QBT_TR(%L: Category)QBT_TR[CONTEXT=OptionsDialog]</li>
@ -83,7 +83,7 @@
<li>QBT_TR(%I: Info hash)QBT_TR[CONTEXT=OptionsDialog]</li>
</ul>
QBT_TR(Tip: Encapsulate parameter with quotation marks to avoid text being cut off at whitespace (e.g., "%N"))QBT_TR[CONTEXT=OptionsDialog]
</i>
</div>
</fieldset>
</div>
@ -256,7 +256,7 @@
<option value="8">QBT_TR(Saturday)QBT_TR[CONTEXT=HttpServer]</option>
<option value="9">QBT_TR(Sunday)QBT_TR[CONTEXT=HttpServer]</option>
</select>
</br/>
<br/>
</fieldset>
</fieldset>
@ -472,7 +472,7 @@
</div>
<br/>
<center><input type="button" value="QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]" onclick="applyPreferences();"/></center>
<div style="text-align: center;"><input type="button" value="QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]" onclick="applyPreferences();"/></div>
<script type="text/javascript">
// Downloads tab
@ -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();
};

View file

@ -52,7 +52,7 @@
<table class="dynamicTable" style="width: 100%">
<thead>
<tr>
<th style="width: 30%;">QBT_TR(URL)QBT_TR[CONTEXT=TrackerList] <img src="theme/list-add" id="addTrackersPlus"/></th>
<th style="width: 30%;">QBT_TR(URL)QBT_TR[CONTEXT=TrackerList] <img src="theme/list-add" id="addTrackersPlus" alt="Add Trackers" /></th>
<th style="width: 10%;">QBT_TR(Status)QBT_TR[CONTEXT=TrackerList]</th>
<th style="width: 10%;">QBT_TR(Peers)QBT_TR[CONTEXT=TrackerList]</th>
<th style="width: 50%;">QBT_TR(Message)QBT_TR[CONTEXT=TrackerList]</th>
@ -64,7 +64,7 @@
</div>
<div id="prop_peers" class="invisible">
<div id="peers">
<div>
<div id="torrentPeersTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable" style="position:relative;">
<thead>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Rename)QBT_TR[CONTEXT=TransferListWidget]</title>
@ -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,

View file

@ -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,
@ -407,12 +407,18 @@ window.addEvent('load', function () {
torrentsTable.columns['priority'].force_hide = !queueing_enabled;
torrentsTable.updateColumn('priority');
if (queueing_enabled) {
$('queueingLinks').removeClass('invisible');
$('topPrioItem').removeClass('invisible');
$('increasePrioItem').removeClass('invisible');
$('decreasePrioItem').removeClass('invisible');
$('bottomPrioItem').removeClass('invisible');
$('queueingButtons').removeClass('invisible');
$('queueingMenuItems').removeClass('invisible');
}
else {
$('queueingLinks').addClass('invisible');
$('topPrioItem').addClass('invisible');
$('increasePrioItem').addClass('invisible');
$('decreasePrioItem').addClass('invisible');
$('bottomPrioItem').addClass('invisible');
$('queueingButtons').addClass('invisible');
$('queueingMenuItems').addClass('invisible');
}
@ -439,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;
@ -659,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({

View file

@ -23,7 +23,7 @@
getSavePath = function() {
var req = new Request({
url: 'command/getSavePath',
url: 'api/v2/app/defaultSavePath',
method: 'get',
noCache: true,
onFailure: function() {

View file

@ -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("<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>QBT_TR(qBittorrent has been shutdown.)QBT_TR[CONTEXT=HttpServer]</title><style type=\"text/css\">body { text-align: center; }</style></head><body><h1>QBT_TR(qBittorrent has been shutdown.)QBT_TR[CONTEXT=HttpServer]</h1></body></html>");
stop();

View file

@ -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<y.length;z++){w[y[z]]=i.call(this,y[z]);}}else{w=i.call(this,x);}return w;};};f.prototype.extend=function(i,v){this[i]=v;
}.overloadSetter();f.prototype.implement=function(i,v){this.prototype[i]=v;}.overloadSetter();var o=Array.prototype.slice;f.from=function(i){return(e(i)=="function")?i:function(){return i;
};};Array.from=function(i){if(i==null){return[];}return(k.isEnumerable(i)&&typeof i!="string")?(e(i)=="array")?i:o.call(i):[i];};Number.from=function(v){var i=parseFloat(v);
return isFinite(i)?i:null;};String.from=function(i){return i+"";};f.implement({hide:function(){this.$hidden=true;return this;},protect:function(){this.$protected=true;
return this;}});var k=this.Type=function(x,w){if(x){var v=x.toLowerCase();var i=function(y){return(e(y)==v);};k["is"+x]=i;if(w!=null){w.prototype.$family=(function(){return v;
}).hide();w.type=i;}}if(w==null){return null;}w.extend(this);w.$constructor=k;w.prototype.$constructor=w;return w;};var p=Object.prototype.toString;k.isEnumerable=function(i){return(i!=null&&typeof i.length=="number"&&p.call(i)!="[object Function]");
};var b={};var d=function(i){var v=e(i.prototype);return b[v]||(b[v]=[]);};var h=function(w,A){if(A&&A.$hidden){return;}var v=d(this);for(var x=0;x<v.length;
x++){var z=v[x];if(e(z)=="type"){h.call(z,w,A);}else{z.call(this,w,A);}}var y=this.prototype[w];if(y==null||!y.$protected){this.prototype[w]=A;}if(this[w]==null&&e(A)=="function"){t.call(this,w,function(i){return A.apply(i,o.call(arguments,1));
});}};var t=function(i,w){if(w&&w.$hidden){return;}var v=this[i];if(v==null||!v.$protected){this[i]=w;}};k.implement({implement:h.overloadSetter(),extend:t.overloadSetter(),alias:function(i,v){h.call(this,i,this.prototype[v]);
}.overloadSetter(),mirror:function(i){d(this).push(i);return this;}});new k("Type",k);var c=function(v,A,y){var x=(A!=Object),E=A.prototype;if(x){A=new k(v,A);
}for(var B=0,z=y.length;B<z;B++){var F=y[B],D=A[F],C=E[F];if(D){D.protect();}if(x&&C){A.implement(F,C.protect());}}if(x){var w=E.propertyIsEnumerable(y[0]);
A.forEachMethod=function(J){if(!w){for(var I=0,G=y.length;I<G;I++){J.call(E,E[y[I]],y[I]);}}for(var H in E){J.call(E,E[H],H);}};}return c;};c("String",String,["charAt","charCodeAt","concat","indexOf","lastIndexOf","match","quote","replace","search","slice","split","substr","substring","trim","toLowerCase","toUpperCase"])("Array",Array,["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice","indexOf","lastIndexOf","filter","forEach","every","map","some","reduce","reduceRight"])("Number",Number,["toExponential","toFixed","toLocaleString","toPrecision"])("Function",f,["apply","call","bind"])("RegExp",RegExp,["exec","test"])("Object",Object,["create","defineProperty","defineProperties","keys","getPrototypeOf","getOwnPropertyDescriptor","getOwnPropertyNames","preventExtensions","isExtensible","seal","isSealed","freeze","isFrozen"])("Date",Date,["now"]);
Object.extend=t.overloadSetter();Date.extend("now",function(){return +(new Date);});new k("Boolean",Boolean);Number.prototype.$family=function(){return isFinite(this)?"number":"null";
}.hide();Number.extend("random",function(v,i){return Math.floor(Math.random()*(i-v+1)+v);});var l=Object.prototype.hasOwnProperty;Object.extend("forEach",function(i,w,x){for(var v in i){if(l.call(i,v)){w.call(x,i[v],v,i);
}}});Object.each=Object.forEach;Array.implement({forEach:function(x,y){for(var w=0,v=this.length;w<v;w++){if(w in this){x.call(y,this[w],w,this);}}},each:function(i,v){Array.forEach(this,i,v);
return this;}});var s=function(i){switch(e(i)){case"array":return i.clone();case"object":return Object.clone(i);default:return i;}};Array.implement("clone",function(){var v=this.length,w=new Array(v);
while(v--){w[v]=s(this[v]);}return w;});var a=function(v,i,w){switch(e(w)){case"object":if(e(v[i])=="object"){Object.merge(v[i],w);}else{v[i]=Object.clone(w);
}break;case"array":v[i]=w.clone();break;default:v[i]=w;}return v;};Object.extend({merge:function(C,y,x){if(e(y)=="string"){return a(C,y,x);}for(var B=1,w=arguments.length;
B<w;B++){var z=arguments[B];for(var A in z){a(C,A,z[A]);}}return C;},clone:function(i){var w={};for(var v in i){w[v]=s(i[v]);}return w;},append:function(z){for(var y=1,w=arguments.length;
y<w;y++){var v=arguments[y]||{};for(var x in v){z[x]=v[x];}}return z;}});["Object","WhiteSpace","TextNode","Collection","Arguments"].each(function(i){new k(i);
});var j=Date.now();String.extend("uniqueID",function(){return(j++).toString(36);});var g=this.Hash=new k("Hash",function(i){if(e(i)=="hash"){i=Object.clone(i.getClean());
}for(var v in i){this[v]=i[v];}return this;});g.implement({forEach:function(i,v){Object.forEach(this,i,v);},getClean:function(){var v={};for(var i in this){if(this.hasOwnProperty(i)){v[i]=this[i];
}}return v;},getLength:function(){var v=0;for(var i in this){if(this.hasOwnProperty(i)){v++;}}return v;}});g.alias("each","forEach");Object.type=k.isObject;
var n=this.Native=function(i){return new k(i.name,i.initialize);};n.type=k.type;n.implement=function(x,v){for(var w=0;w<x.length;w++){x[w].implement(v);
}return n;};var m=Array.type;Array.type=function(i){return u(i,Array)||m(i);};this.$A=function(i){return Array.from(i).slice();};this.$arguments=function(v){return function(){return arguments[v];
};};this.$chk=function(i){return !!(i||i===0);};this.$clear=function(i){clearTimeout(i);clearInterval(i);return null;};this.$defined=function(i){return(i!=null);
};this.$each=function(w,v,x){var i=e(w);((i=="arguments"||i=="collection"||i=="array"||i=="elements")?Array:Object).each(w,v,x);};this.$empty=function(){};
this.$extend=function(v,i){return Object.append(v,i);};this.$H=function(i){return new g(i);};this.$merge=function(){var i=Array.slice(arguments);i.unshift({});
return Object.merge.apply(null,i);};this.$lambda=f.from;this.$mixin=Object.merge;this.$random=Number.random;this.$splat=Array.from;this.$time=Date.now;
this.$type=function(i){var v=e(i);if(v=="elements"){return"array";}return(v=="null")?false:v;};this.$unlink=function(i){switch(e(i)){case"object":return Object.clone(i);
case"array":return Array.clone(i);case"hash":return new g(i);default:return i;}};})();Array.implement({every:function(c,d){for(var b=0,a=this.length>>>0;
b<a;b++){if((b in this)&&!c.call(d,this[b],b,this)){return false;}}return true;},filter:function(d,f){var c=[];for(var e,b=0,a=this.length>>>0;b<a;b++){if(b in this){e=this[b];
if(d.call(f,e,b,this)){c.push(e);}}}return c;},indexOf:function(c,d){var b=this.length>>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a<b;a++){if(this[a]===c){return a;
}}return -1;},map:function(c,e){var d=this.length>>>0,b=Array(d);for(var a=0;a<d;a++){if(a in this){b[a]=c.call(e,this[a],a,this);}}return b;},some:function(c,d){for(var b=0,a=this.length>>>0;
b<a;b++){if((b in this)&&c.call(d,this[b],b,this)){return true;}}return false;},clean:function(){return this.filter(function(a){return a!=null;});},invoke:function(a){var b=Array.slice(arguments,1);
return this.map(function(c){return c[a].apply(c,b);});},associate:function(c){var d={},b=Math.min(this.length,c.length);for(var a=0;a<b;a++){d[c[a]]=this[a];
}return d;},link:function(c){var a={};for(var e=0,b=this.length;e<b;e++){for(var d in c){if(c[d](this[e])){a[d]=this[e];delete c[d];break;}}}return a;},contains:function(a,b){return this.indexOf(a,b)!=-1;
},append:function(a){this.push.apply(this,a);return this;},getLast:function(){return(this.length)?this[this.length-1]:null;},getRandom:function(){return(this.length)?this[Number.random(0,this.length-1)]:null;
},include:function(a){if(!this.contains(a)){this.push(a);}return this;},combine:function(c){for(var b=0,a=c.length;b<a;b++){this.include(c[b]);}return this;
},erase:function(b){for(var a=this.length;a--;){if(this[a]===b){this.splice(a,1);}}return this;},empty:function(){this.length=0;return this;},flatten:function(){var d=[];
for(var b=0,a=this.length;b<a;b++){var c=typeOf(this[b]);if(c=="null"){continue;}d=d.concat((c=="array"||c=="collection"||c=="arguments"||instanceOf(this[b],Array))?Array.flatten(this[b]):this[b]);
}return d;},pick:function(){for(var b=0,a=this.length;b<a;b++){if(this[b]!=null){return this[b];}}return null;},hexToRgb:function(b){if(this.length!=3){return null;
}var a=this.map(function(c){if(c.length==1){c+=c;}return c.toInt(16);});return(b)?a:"rgb("+a+")";},rgbToHex:function(d){if(this.length<3){return null;}if(this.length==4&&this[3]==0&&!d){return"transparent";
}var b=[];for(var a=0;a<3;a++){var c=(this[a]-0).toString(16);b.push((c.length==1)?"0"+c:c);}return(d)?b:"#"+b.join("");}});Array.alias("extend","append");
var $pick=function(){return Array.from(arguments).pick();};String.implement({test:function(a,b){return((typeOf(a)=="regexp")?a:new RegExp(""+a,b)).test(this);
},contains:function(a,b){return(b)?(b+this+b).indexOf(b+a+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;a<this;a++){b.call(c,a,this);}},toFloat:function(){return parseFloat(this);},toInt:function(a){return parseInt(this,a||10);
}});Number.alias("each","times");(function(b){var a={};b.each(function(c){if(!Number[c]){a[c]=function(){return Math[c].apply(null,[this].concat(Array.from(arguments)));
};}});Number.implement(a);})(["abs","acos","asin","atan","atan2","ceil","cos","exp","floor","log","max","min","pow","sin","sqrt","tan"]);Function.extend({attempt:function(){for(var b=0,a=arguments.length;
b<a;b++){try{return arguments[b]();}catch(c){}}return null;}});Function.implement({attempt:function(a,c){try{return this.apply(c,Array.from(a));}catch(b){}return null;
},bind:function(e){var a=this,b=arguments.length>1?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<b;e++){var c=g[e];if(c in d){f[c]=d[c];}}return f;},map:function(b,e,f){var d={};for(var c in b){if(a.call(b,c)){d[c]=e.call(f,b[c],c,b);
}}return d;},filter:function(b,e,g){var d={};for(var c in b){var f=b[c];if(a.call(b,c)&&e.call(g,f,c,b)){d[c]=f;}}return d;},every:function(b,d,e){for(var c in b){if(a.call(b,c)&&!d.call(e,b[c],c)){return false;
}}return true;},some:function(b,d,e){for(var c in b){if(a.call(b,c)&&d.call(e,b[c],c)){return true;}}return false;},keys:function(b){var d=[];for(var c in b){if(a.call(b,c)){d.push(c);
}}return d;},values:function(c){var b=[];for(var d in c){if(a.call(c,d)){b.push(c[d]);}}return b;},getLength:function(b){return Object.keys(b).length;},keyOf:function(b,d){for(var c in b){if(a.call(b,c)&&b[c]===d){return c;
}}return null;},contains:function(b,c){return Object.keyOf(b,c)!=null;},toQueryString:function(b,c){var d=[];Object.each(b,function(h,g){if(c){g=c+"["+g+"]";
}var f;switch(typeOf(h)){case"object":f=Object.toQueryString(h,g);break;case"array":var e={};h.each(function(k,j){e[j]=k;});f=Object.toQueryString(e,g);
break;default:f=g+"="+encodeURIComponent(h);}if(h!=null){d.push(f);}});return d.join("&");}});})();Hash.implement({has:Object.prototype.hasOwnProperty,keyOf:function(a){return Object.keyOf(this,a);
},hasValue:function(a){return Object.contains(this,a);},extend:function(a){Hash.each(a||{},function(c,b){Hash.set(this,b,c);},this);return this;},combine:function(a){Hash.each(a||{},function(c,b){Hash.include(this,b,c);
},this);return this;},erase:function(a){if(this.hasOwnProperty(a)){delete this[a];}return this;},get:function(a){return(this.hasOwnProperty(a))?this[a]:null;
},set:function(a,b){if(!this[a]||this.hasOwnProperty(a)){this[a]=b;}return this;},empty:function(){Hash.each(this,function(b,a){delete this[a];},this);
return this;},include:function(a,b){if(this[a]==null){this[a]=b;}return this;},map:function(a,b){return new Hash(Object.map(this,a,b));},filter:function(a,b){return new Hash(Object.filter(this,a,b));
},every:function(a,b){return Object.every(this,a,b);},some:function(a,b){return Object.some(this,a,b);},getKeys:function(){return Object.keys(this);},getValues:function(){return Object.values(this);
},toQueryString:function(a){return Object.toQueryString(this,a);}});Hash.extend=Object.append;Hash.alias({indexOf:"keyOf",contains:"hasValue"});(function(){var k=this.document;
var h=k.window=this;var a=navigator.userAgent.toLowerCase(),b=navigator.platform.toLowerCase(),i=a.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0],f=i[1]=="ie"&&k.documentMode;
var o=this.Browser={extend:Function.prototype.extend,name:(i[1]=="version")?i[3]:i[1],version:f||parseFloat((i[1]=="opera"&&i[4])?i[4]:i[2]),Platform:{name:a.match(/ip(?:ad|od|hone)/)?"ios":(a.match(/(?:webos|android)/)||b.match(/mac|win|linux/)||["other"])[0]},Features:{xpath:!!(k.evaluate),air:!!(h.runtime),query:!!(k.querySelector),json:!!(h.JSON)},Plugins:{}};
o[o.name]=true;o[o.name+parseInt(o.version,10)]=true;o.Platform[o.Platform.name]=true;o.Request=(function(){var q=function(){return new XMLHttpRequest();
};var p=function(){return new ActiveXObject("MSXML2.XMLHTTP");};var e=function(){return new ActiveXObject("Microsoft.XMLHTTP");};return Function.attempt(function(){q();
return q;},function(){p();return p;},function(){e();return e;});})();o.Features.xhr=!!(o.Request);var j=(Function.attempt(function(){return navigator.plugins["Shockwave Flash"].description;
},function(){return new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version");})||"0 r0").match(/\d+/g);o.Plugins.Flash={version:Number(j[0]||"0."+j[1])||0,build:Number(j[2])||0};
o.exec=function(p){if(!p){return p;}if(h.execScript){h.execScript(p);}else{var e=k.createElement("script");e.setAttribute("type","text/javascript");e.text=p;
k.head.appendChild(e);k.head.removeChild(e);}return p;};String.implement("stripScripts",function(p){var e="";var q=this.replace(/<script[^>]*>([\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<r.length;p++){var t=r[p];var q={parts:[],tag:"*",combinator:i(t[0].combinator)};for(var o=0;o<t.length;
o++){var s=t[o];if(!s.reverseCombinator){s.reverseCombinator=" ";}s.combinator=s.reverseCombinator;delete s.reverseCombinator;}t.reverse().push(q);}return u;
};var f=function(o){return o.replace(/[-[\]{}()*+?.\\^$|,#\s]/g,function(p){return"\\"+p;});};var j=new RegExp("^(?:\\s*(,)\\s*|\\s*(<combinator>+)\\s*|(\\s+)|(<unicode>+|\\*)|\\#(<unicode>+)|\\.(<unicode>+)|\\[\\s*(<unicode1>+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(<unicode>+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(/<combinator>/,"["+f(">+~`!@$%^&={}\\;</")+"]").replace(/<unicode>/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(/<unicode1>/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='<a id="'+v+'"></a>';
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</foo>";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML='<a name="'+v+'"></a><b id="'+v+'"></b>';
s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='<a class="f"></a><a class="b"></a>';c.getElementsByClassName("b").length;
c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='<a class="a"></a><a class="f b a"></a>';y=(c.getElementsByClassName("a").length!=2);
}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo</foo>";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");
}catch(C){}try{c.innerHTML='<a class="MiX"></a>';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML='<select><option selected="selected">a</option></select>';
s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='<a class=""></a>';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0);
}catch(C){}}try{c.innerHTML='<form action="s"><input id="action"/></form>';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;L<K;L++){this[B](N[L],J,D,q,G,W,c);if(p.length){break search;}}}else{for(L=0,K=N.length;L<K;L++){this[B](N[L],J,D,q,G,W,c);
}}}N=this.found;}}if(I||(F.expressions.length>1)){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<x){return false;}}else{if(x<w){return false;}}return((w-x)%y)==0;};};k.pushArray=function(p,c,r,o,n,q){if(this.matchSelector(p,c,r,o,n,q)){this.found.push(p);
}};k.pushUID=function(q,c,s,p,n,r){var o=this.getUID(q);if(!this.uniques[o]&&this.matchSelector(q,c,s,p,n,r)){this.uniques[o]=true;this.found.push(q);}};
k.matchNode=function(n,o){if(this.isHTMLDocument&&this.nativeMatchesSelector){try{return this.nativeMatchesSelector.call(n,o.replace(/\[([^=]+)=\s*([^'"\]]+?)\s*\]/g,'[$1="$2"]'));
}catch(u){}}var t=this.Slick.parse(o);if(!t){return true;}var r=t.expressions,s=0,q;for(q=0;(currentExpression=r[q]);q++){if(currentExpression.length==1){var p=currentExpression[0];
if(this.matchSelector(n,(this.isXMLDocument)?p.tag:p.tag.toUpperCase(),p.id,p.classes,p.attributes,p.pseudos)){return true;}s++;}}if(s==t.length){return false;
}var c=this.search(this.document,t),v;for(q=0;v=c[q++];){if(v===n){return true;}}return false;};k.matchPseudo=function(q,c,p){var n="pseudo:"+c;if(this[n]){return this[n](q,p);
}var o=this.getAttribute(q,c);return(p)?p==o:!!o;};k.matchSelector=function(o,v,c,p,q,s){if(v){var t=(this.isXMLDocument)?o.nodeName:o.nodeName.toUpperCase();
if(v=="*"){if(t<"@"){return false;}}else{if(t!=v){return false;}}}if(c&&o.getAttribute("id")!=c){return false;}var r,n,u;if(p){for(r=p.length;r--;){u=this.getAttribute(o,"class");
if(!(u&&p[r].regexp.test(u))){return false;}}}if(q){for(r=q.length;r--;){n=q[r];if(n.operator?!n.test(this.getAttribute(o,n.key)):!this.hasAttribute(o,n.key)){return false;
}}}if(s){for(r=s.length;r--;){n=s[r];if(!this.matchPseudo(o,n.key,n.value)){return false;}}}return true;};var j={" ":function(q,w,n,r,s,u,p){var t,v,o;
if(this.isHTMLDocument){getById:if(n){v=this.document.getElementById(n);if((!v&&q.all)||(this.idGetsName&&v&&v.getAttributeNode("id").nodeValue!=n)){o=q.all[n];
if(!o){return;}if(!o[0]){o=[o];}for(t=0;v=o[t++];){var c=v.getAttributeNode("id");if(c&&c.nodeValue==n){this.push(v,w,null,r,s,u);break;}}return;}if(!v){if(this.contains(this.root,q)){return;
}else{break getById;}}else{if(this.document!==q&&!this.contains(q,v)){return;}}this.push(v,w,null,r,s,u);return;}getByClass:if(r&&q.getElementsByClassName&&!this.brokenGEBCN){o=q.getElementsByClassName(p.join(" "));
if(!(o&&o.length)){break getByClass;}for(t=0;v=o[t++];){this.push(v,w,n,null,s,u);}return;}}getByTag:{o=q.getElementsByTagName(w);if(!(o&&o.length)){break getByTag;
}if(!this.brokenStarGEBTN){w=null;}for(t=0;v=o[t++];){this.push(v,w,n,r,s,u);}}},">":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<c;f++){a=d[f];if(g[a.key]!=null){continue;
}if(a.value!=null&&a.operator=="="){g[a.key]=a.value;}else{if(!a.value&&!a.operator){g[a.key]=true;}}}}if(e.classList&&g["class"]==null){g["class"]=e.classList.join(" ");
}}return document.newElement(b,g);};if(Browser.Element){Element.prototype=Browser.Element.prototype;Element.prototype._fireEvent=(function(a){return function(b,c){return a.call(this,b,c);
};})(Element.prototype.fireEvent);}new Type("Element",Element).mirror(function(a){if(Array.prototype[a]){return;}var b={};b[a]=function(){var h=[],e=arguments,j=true;
for(var g=0,d=this.length;g<d;g++){var f=this[g],c=h[g]=f[a].apply(f,e);j=(j&&typeOf(c)=="element");}return(j)?new Elements(h):h;};Elements.implement(b);
});if(!Browser.Element){Element.parent=Object;Element.Prototype={"$constructor":Element,"$family":Function.from("element").hide()};Element.mirror(function(a,b){Element.Prototype[a]=b;
});}Element.Constructors={};Element.Constructors=new Hash;var IFrame=new Type("IFrame",function(){var e=Array.link(arguments,{properties:Type.isObject,iframe:function(f){return(f!=null);
}});var c=e.properties||{},b;if(e.iframe){b=document.id(e.iframe);}var d=c.onload||function(){};delete c.onload;c.id=c.name=[c.id,c.name,b?(b.id||b.name):"IFrame_"+String.uniqueID()].pick();
b=new Element(b||"iframe",c);var a=function(){d.call(b.contentWindow);};if(window.frames[c.id]){a();}else{b.addListener("load",a);}return b;});var Elements=this.Elements=function(a){if(a&&a.length){var e={},d;
for(var c=0;d=a[c++];){var b=Slick.uidOf(d);if(!e[b]){e[b]=true;this.push(d);}}}};Elements.prototype={length:0};Elements.parent=Array;new Type("Elements",Elements).implement({filter:function(a,b){if(!a){return this;
}return new Elements(Array.filter(this,(typeOf(a)=="string")?function(c){return c.match(a);}:a,b));}.protect(),push:function(){var d=this.length;for(var b=0,a=arguments.length;
b<a;b++){var c=document.id(arguments[b]);if(c){this[d++]=c;}}return(this.length=d);}.protect(),unshift:function(){var b=[];for(var c=0,a=arguments.length;
c<a;c++){var d=document.id(arguments[c]);if(d){b.push(d);}}return Array.prototype.unshift.apply(this,b);}.protect(),concat:function(){var b=new Elements(this);
for(var c=0,a=arguments.length;c<a;c++){var d=arguments[c];if(Type.isEnumerable(d)){b.append(d);}else{b.push(d);}}return b;}.protect(),append:function(c){for(var b=0,a=c.length;
b<a;b++){this.push(c[b]);}return this;}.protect(),empty:function(){while(this.length){delete this[--this.length];}return this;}.protect()});Elements.alias("extend","append");
(function(){var f=Array.prototype.splice,a={"0":0,"1":1,length:2};f.call(a,1,1);if(a[1]==1){Elements.implement("splice",function(){var g=this.length;var e=f.apply(this,arguments);
while(g>=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("<input name=x>").name=="x");}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&amp;").replace(/"/g,"&quot;");};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<D;F++){var G=E[F];switch(typeOf(G)){case"element":H.push(G);break;case"string":Slick.search(this.document,G,H);
}}return H;});}if(window.$$==null){Window.implement("$$",function(e){if(arguments.length==1){if(typeof e=="string"){return Slick.search(this.document,e,new Elements);
}else{if(Type.isEnumerable(e)){return new Elements(e);}}}return new Elements(arguments);});}var w={before:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e);
}},after:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e.nextSibling);}},bottom:function(l,e){e.appendChild(l);},top:function(l,e){e.insertBefore(l,e.firstChild);
}};w.inside=w.bottom;Object.each(w,function(l,D){D=D.capitalize();var e={};e["inject"+D]=function(E){l(this,document.id(E,true));return this;};e["grab"+D]=function(E){l(document.id(E,true),this);
return this;};Element.implement(e);});var j={},d={};var k={};Array.forEach(["type","value","defaultValue","accessKey","cellPadding","cellSpacing","colSpan","frameBorder","rowSpan","tabIndex","useMap"],function(e){k[e.toLowerCase()]=e;
});k.html="innerHTML";k.text=(document.createElement("div").textContent==null)?"innerText":"textContent";Object.forEach(k,function(l,e){d[e]=function(D,E){D[l]=E;
};j[e]=function(D){return D[l];};});var x=["compact","nowrap","ismap","declare","noshade","checked","disabled","readOnly","multiple","selected","noresize","defer","defaultChecked","autofocus","controls","autoplay","loop"];
var h={};Array.forEach(x,function(e){var l=e.toLowerCase();h[l]=e;d[l]=function(D,E){D[e]=!!E;};j[l]=function(D){return !!D[e];};});Object.append(d,{"class":function(e,l){("className" in e)?e.className=(l||""):e.setAttribute("class",l);
},"for":function(e,l){("htmlFor" in e)?e.htmlFor=l:e.setAttribute("for",l);},style:function(e,l){(e.style)?e.style.cssText=l:e.setAttribute("style",l);
},value:function(e,l){e.value=(l!=null)?l:"";}});j["class"]=function(e){return("className" in e)?e.className||null:e.getAttribute("class");};var f=document.createElement("button");
try{f.type="button";}catch(z){}if(f.type!="button"){d.type=function(e,l){e.setAttribute("type",l);};}f=null;var p=document.createElement("input");p.value="t";
p.type="submit";if(p.value!="t"){d.type=function(l,e){var D=l.value;l.type=e;l.value=D;};}p=null;var q=(function(e){e.random="attribute";return(e.getAttribute("random")=="attribute");
})(document.createElement("div"));Element.implement({setProperty:function(l,D){var E=d[l.toLowerCase()];if(E){E(this,D);}else{if(q){var e=this.retrieve("$attributeWhiteList",{});
}if(D==null){this.removeAttribute(l);if(q){delete e[l];}}else{this.setAttribute(l,""+D);if(q){e[l]=true;}}}return this;},setProperties:function(e){for(var l in e){this.setProperty(l,e[l]);
}return this;},getProperty:function(F){var D=j[F.toLowerCase()];if(D){return D(this);}if(q){var l=this.getAttributeNode(F),E=this.retrieve("$attributeWhiteList",{});
if(!l){return null;}if(l.expando&&!E[F]){var G=this.outerHTML;if(G.substr(0,G.search(/\/?['"]?>(?![^<]*<['"])/)).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<F;D++){var l=document.id(G[D],true);if(l){E.appendChild(l);}}if(e){this.appendChild(e);}return this;
},appendText:function(l,e){return this.grab(this.getDocument().newTextNode(l),e);},grab:function(l,e){w[e||"bottom"](document.id(l,true),this);return this;
},inject:function(l,e){w[e||"bottom"](this,document.id(l,true));return this;},replaces:function(e){e=document.id(e,true);e.parentNode.replaceChild(this,e);
return this;},wraps:function(l,e){l=document.id(l,true);return this.replaces(l).grab(l,e);},getSelected:function(){this.selectedIndex;return new Elements(Array.from(this.options).filter(function(e){return e.selected;
}));},toQueryString:function(){var e=[];this.getElements("input, select, textarea").each(function(D){var l=D.type;if(!D.name||D.disabled||l=="submit"||l=="reset"||l=="file"||l=="image"){return;
}var E=(D.get("tag")=="select")?D.getSelected().map(function(F){return document.id(F).get("value");}):((l=="radio"||l=="checkbox")&&!D.checked)?null:D.get("value");
Array.from(E).each(function(F){if(typeof F!="undefined"){e.push(encodeURIComponent(D.name)+"="+encodeURIComponent(F));}});});return e.join("&");}});var i={},A={};
var B=function(e){return(A[e]||(A[e]={}));};var v=function(l){var e=l.uniqueNumber;if(l.removeEvents){l.removeEvents();}if(l.clearAttributes){l.clearAttributes();
}if(e!=null){delete i[e];delete A[e];}return l;};var C={input:"checked",option:"selected",textarea:"value"};Element.implement({destroy:function(){var e=v(this).getElementsByTagName("*");
Array.each(e,v);Element.dispose(this);return null;},empty:function(){Array.from(this.childNodes).each(Element.dispose);return this;},dispose:function(){return(this.parentNode)?this.parentNode.removeChild(this):this;
},clone:function(G,E){G=G!==false;var L=this.cloneNode(G),D=[L],F=[this],J;if(G){D.append(Array.from(L.getElementsByTagName("*")));F.append(Array.from(this.getElementsByTagName("*")));
}for(J=D.length;J--;){var H=D[J],K=F[J];if(!E){H.removeAttribute("id");}if(H.clearAttributes){H.clearAttributes();H.mergeAttributes(K);H.removeAttribute("uniqueNumber");
if(H.options){var O=H.options,e=K.options;for(var I=O.length;I--;){O[I].selected=e[I].selected;}}}var l=C[K.tagName.toLowerCase()];if(l&&K[l]){H[l]=K[l];
}}if(Browser.ie){var M=L.getElementsByTagName("object"),N=this.getElementsByTagName("object");for(J=M.length;J--;){M[J].outerHTML=N[J].outerHTML;}}return document.id(L);
}});[Element,Window,Document].invoke("implement",{addListener:function(E,D){if(E=="unload"){var e=D,l=this;D=function(){l.removeListener("unload",D);e();
};}else{i[Slick.uidOf(this)]=this;}if(this.addEventListener){this.addEventListener(E,D,!!arguments[2]);}else{this.attachEvent("on"+E,D);}return this;},removeListener:function(l,e){if(this.removeEventListener){this.removeEventListener(l,e,!!arguments[2]);
}else{this.detachEvent("on"+l,e);}return this;},retrieve:function(l,e){var E=B(Slick.uidOf(this)),D=E[l];if(e!=null&&D==null){D=E[l]=e;}return D!=null?D:null;
},store:function(l,e){var D=B(Slick.uidOf(this));D[l]=e;return this;},eliminate:function(e){var l=B(Slick.uidOf(this));delete l[e];return this;}});if(window.attachEvent&&!window.addEventListener){window.addListener("unload",function(){Object.each(i,v);
if(window.CollectGarbage){CollectGarbage();}});}Element.Properties={};Element.Properties=new Hash;Element.Properties.style={set:function(e){this.style.cssText=e;
},get:function(){return this.style.cssText;},erase:function(){this.style.cssText="";}};Element.Properties.tag={get:function(){return this.tagName.toLowerCase();
}};Element.Properties.html={set:function(e){if(e==null){e="";}else{if(typeOf(e)=="array"){e=e.join("");}}this.innerHTML=e;},erase:function(){this.innerHTML="";
}};var t=document.createElement("div");t.innerHTML="<nav></nav>";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="<tr><td></td></tr>";return true;
});var c=document.createElement("tr"),o="<td></td>";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"<table>","</table>"],select:[1,"<select>","</select>"],tbody:[2,"<table><tbody>","</tbody></table>"],tr:[3,"<table><tbody><tr>","</tr></tbody></table>"]};
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="<select><option>s</option></select>";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;E<D.length;E++){var F=D[E],e=F.getAttributeNode("value"),H=(e&&e.specified)?F.value:F.get("text");
if(H==G){return F.selected=true;}}},get:function(){var D=this,l=D.get("tag");if(l!="select"&&l!="option"){return this.getProperty("value");}if(l=="select"&&!(D=D.getSelected()[0])){return"";
}var e=D.getAttributeNode("value");return(e&&e.specified)?D.value:D.get("text");}};}n=null;if(document.createElement("div").getAttributeNode("id")){Element.Properties.id={set:function(e){this.id=this.getAttributeNode("id").value=e;
},get:function(){return this.id||null;},erase:function(){this.id=this.getAttributeNode("id").value="";}};}})();(function(){var i=document.html;var d=document.createElement("div");
d.style.color="red";d.style.color=null;var c=d.style.color=="red";d=null;Element.Properties.styles={set:function(k){this.setStyles(k);}};var h=(i.style.opacity!=null),e=(i.style.filter!=null),j=/alpha\(opacity=([\d.]+)\)/i;
var a=function(l,k){l.store("$opacity",k);l.style.visibility=k>0||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<this.frames){var j=this.transition(this.frame/this.frames);this.set(this.compute(this.from,this.to,j));
}else{this.frame=this.frames;this.set(this.compute(this.from,this.to,1));this.stop();}},set:function(g){return g;},compute:function(i,h,g){return f.compute(i,h,g);
},check:function(){if(!this.isRunning()){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;},start:function(k,j){if(!this.check(k,j)){return this;}this.from=k;this.to=j;this.frame=(this.options.frameSkip)?0:-1;this.time=null;
this.transition=this.getTransition();var i=this.options.frames,h=this.options.fps,g=this.options.duration;this.duration=f.Durations[g]||g.toInt();this.frameInterval=1000/h;
this.frames=i||Math.round(this.duration/this.frameInterval);this.fireEvent("start",this.subject);b.call(this,h);return this;},stop:function(){if(this.isRunning()){this.time=null;
d.call(this,this.options.fps);if(this.frames==this.frame){this.fireEvent("complete",this.subject);if(!this.callChain()){this.fireEvent("chainComplete",this.subject);
}}else{this.fireEvent("stop",this.subject);}}return this;},cancel:function(){if(this.isRunning()){this.time=null;d.call(this,this.options.fps);this.frame=this.frames;
this.fireEvent("cancel",this.subject).clearChain();}return this;},pause:function(){if(this.isRunning()){this.time=null;d.call(this,this.options.fps);}return this;
},resume:function(){if((this.frame<this.frames)&&!this.isRunning()){b.call(this,this.options.fps);}return this;},isRunning:function(){var g=e[this.options.fps];
return g&&g.contains(this);}});f.compute=function(i,h,g){return(h-i)*g+i;};f.Durations={"short":250,normal:500,"long":1000};var e={},c={};var a=function(){var h=Date.now();
for(var j=this.length;j--;){var g=this[j];if(g){g.step(h);}}};var b=function(h){var g=e[h]||(e[h]=[]);g.push(this);if(!c[h]){c[h]=a.periodical(Math.round(1000/h),g);
}};var d=function(h){var g=e[h];if(g){g.erase(this);if(!g.length&&c[h]){delete e[h];c[h]=clearInterval(c[h]);}}};})();Fx.CSS=new Class({Extends:Fx,prepare:function(b,e,a){a=Array.from(a);
var h=a[0],g=a[1];if(g==null){g=h;h=b.getStyle(e);var c=this.options.unit;if(c&&h.slice(-c.length)!=c&&parseFloat(h)!=0){b.setStyle(e,g+c);var d=b.getComputedStyle(e);
if(!(/px$/.test(d))){d=b.style[("pixel-"+e).camelCase()];if(d==null){var f=b.style.left;b.style.left=g+c;d=b.style.pixelLeft;b.style.left=f;}}h=(g||1)/(parseFloat(d)||1)*(parseFloat(h)||0);
b.setStyle(e,h+c);}}return{from:this.parse(h),to:this.parse(g)};},parse:function(a){a=Function.from(a)();a=(typeof a=="string")?a.split(" "):Array.from(a);
return a.map(function(c){c=String(c);var b=false;Object.each(Fx.CSS.Parsers,function(f,e){if(b){return;}var d=f.parse(c);if(d||d===0){b={value:d,parser:f};
}});b=b||{value:c,parser:Fx.CSS.Parsers.String};return b;});},compute:function(d,c,b){var a=[];(Math.min(d.length,c.length)).times(function(e){a.push({value:d[e].parser.compute(d[e].value,c[e].value,b),parser:d[e].parser});
});a.$family=Function.from("fx:css:value");return a;},serve:function(c,b){if(typeOf(c)!="fx:css:value"){c=this.parse(c);}var a=[];c.each(function(d){a=a.concat(d.parser.serve(d.value,b));
});return a;},render:function(a,d,c,b){a.setStyle(d,this.serve(c,b));},search:function(a){if(Fx.CSS.Cache[a]){return Fx.CSS.Cache[a];}var c={},b=new RegExp("^"+a.escapeRegExp()+"$");
Array.each(document.styleSheets,function(f,e){var d=f.href;if(d&&d.contains("://")&&!d.contains(document.domain)){return;}var g=f.rules||f.cssRules;Array.each(g,function(k,h){if(!k.style){return;
}var j=(k.selectorText)?k.selectorText.replace(/^\w+/,function(i){return i.toLowerCase();}):null;if(!j||!b.test(j)){return;}Object.each(Element.Styles,function(l,i){if(!k.style[i]||Element.ShortStyles[i]){return;
}l=String(k.style[i]);c[i]=((/^rgb/).test(l))?l.rgbToHex():l;});});});return Fx.CSS.Cache[a]=c;}});Fx.CSS.Cache={};Fx.CSS.Parsers={Color:{parse:function(a){if(a.match(/^#[0-9a-f]{3,6}$/i)){return a.hexToRgb(true);
}return((a=a.match(/(\d+),\s*(\d+),\s*(\d+)/)))?[a[1],a[2],a[3]]:false;},compute:function(c,b,a){return c.map(function(e,d){return Math.round(Fx.compute(c[d],b[d],a));
});},serve:function(a){return a.map(Number);}},Number:{parse:parseFloat,compute:Fx.compute,serve:function(b,a){return(a)?b+a:b;}},String:{parse:Function.from(false),compute:function(b,a){return a;
},serve:function(a){return a;}}};Fx.CSS.Parsers=new Hash(Fx.CSS.Parsers);Fx.Tween=new Class({Extends:Fx.CSS,initialize:function(b,a){this.element=this.subject=document.id(b);
this.parent(a);},set:function(b,a){if(arguments.length==1){a=b;b=this.property||this.options.property;}this.render(this.element,b,a,this.options.unit);
return this;},start:function(c,e,d){if(!this.check(c,e,d)){return this;}var b=Array.flatten(arguments);this.property=this.options.property||b.shift();var a=this.prepare(this.element,this.property,b);
return this.parent(a.from,a.to);}});Element.Properties.tween={set:function(a){this.get("tween").cancel().setOptions(a);return this;},get:function(){var a=this.retrieve("tween");
if(!a){a=new Fx.Tween(this,{link:"cancel"});this.store("tween",a);}return a;}};Element.implement({tween:function(a,c,b){this.get("tween").start(a,c,b);
return this;},fade:function(d){var e=this.get("tween"),g,c=["opacity"].append(arguments),a;if(c[1]==null){c[1]="toggle";}switch(c[1]){case"in":g="start";
c[1]=1;break;case"out":g="start";c[1]=0;break;case"show":g="set";c[1]=1;break;case"hide":g="set";c[1]=0;break;case"toggle":var b=this.retrieve("fade:flag",this.getStyle("opacity")==1);
g="start";c[1]=b?0:1;this.store("fade:flag",!b);a=true;break;default:g="start";}if(!a){this.eliminate("fade:flag");}e[g].apply(e,c);var f=c[c.length-1];
if(g=="set"||f!=0){this.setStyle("visibility",f==0?"hidden":"visible");}else{e.chain(function(){this.element.setStyle("visibility","hidden");this.callChain();
});}return this;},highlight:function(c,a){if(!a){a=this.retrieve("highlight:original",this.getStyle("background-color"));a=(a=="transparent")?"#fff":a;
}var b=this.get("tween");b.start("background-color",c||"#ffff88",a).chain(function(){this.setStyle("background-color",this.retrieve("highlight:original"));
b.callChain();}.bind(this));return this;}});Fx.Morph=new Class({Extends:Fx.CSS,initialize:function(b,a){this.element=this.subject=document.id(b);this.parent(a);
},set:function(a){if(typeof a=="string"){a=this.search(a);}for(var b in a){this.render(this.element,b,a[b],this.options.unit);}return this;},compute:function(e,d,c){var a={};
for(var b in e){a[b]=this.parent(e[b],d[b],c);}return a;},start:function(b){if(!this.check(b)){return this;}if(typeof b=="string"){b=this.search(b);}var e={},d={};
for(var c in b){var a=this.prepare(this.element,c,b[c]);e[c]=a.from;d[c]=a.to;}return this.parent(e,d);}});Element.Properties.morph={set:function(a){this.get("morph").cancel().setOptions(a);
return this;},get:function(){var a=this.retrieve("morph");if(!a){a=new Fx.Morph(this,{link:"cancel"});this.store("morph",a);}return a;}};Element.implement({morph:function(a){this.get("morph").start(a);
return this;}});Fx.implement({getTransition:function(){var a=this.options.transition||Fx.Transitions.Sine.easeInOut;if(typeof a=="string"){var b=a.split(":");
a=Fx.Transitions;a=a[b[0]]||a[b[0].capitalize()];if(b[1]){a=a["ease"+b[1].capitalize()+(b[2]?b[2].capitalize():"")];}}return a;}});Fx.Transition=function(c,b){b=Array.from(b);
var a=function(d){return c(d,b);};return Object.append(a,{easeIn:a,easeOut:function(d){return 1-c(1-d,b);},easeInOut:function(d){return(d<=0.5?c(2*d,b):(2-c(2*(1-d),b)))/2;
}});};Fx.Transitions={linear:function(a){return a;}};Fx.Transitions=new Hash(Fx.Transitions);Fx.Transitions.extend=function(a){for(var b in a){Fx.Transitions[b]=new Fx.Transition(a[b]);
}};Fx.Transitions.extend({Pow:function(b,a){return Math.pow(b,a&&a[0]||6);},Expo:function(a){return Math.pow(2,8*(a-1));},Circ:function(a){return 1-Math.sin(Math.acos(a));
},Sine:function(a){return 1-Math.cos(a*Math.PI/2);},Back:function(b,a){a=a&&a[0]||1.618;return Math.pow(b,2)*((a+1)*b-a);},Bounce:function(f){var e;for(var d=0,c=1;
1;d+=c,c/=2){if(f>=(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(/<body[^>]*>([\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='<object id="'+id+'"';for(var property in properties){build+=" "+property+'="'+properties[property]+'"';
}build+=">";for(var param in params){if(params[param]){build+='<param name="'+param+'" value="'+params[param]+'" />';}}build+="</object>";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('<invoke name="'+fn+'" returntype="javascript">'+__flash__argumentsToXML(arguments,2)+"</invoke>");
return eval(rs);};})();

View file

@ -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: {

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Set location)QBT_TR[CONTEXT=HttpServer]</title>
@ -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,

View file

@ -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) {

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]</title>
@ -10,12 +10,10 @@
</head>
<body>
<iframe id="upload_frame" name="upload_frame" class="invisible" src="javascript:false;"></iframe>
<form action="command/upload" enctype="multipart/form-data" method="post" id="uploadForm" style="text-align: center;" target="upload_frame">
<p>
<div style="margin-top: 25px; display: inline-block; border: 1px solid lightgrey; border-radius: 4px;">
<input type="file" id="fileselect" name="fileselect[]" multiple="multiple"/>
</div>
</p>
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="uploadForm" style="text-align: center;" target="upload_frame">
<div style="margin-top: 25px; display: inline-block; border: 1px solid lightgrey; border-radius: 4px;">
<input type="file" id="fileselect" name="fileselect[]" multiple="multiple"/>
</div>
<fieldset class="settings" style="border: 0; text-align: left;">
<div class="formRow" style="margin-top: 12px;">
<label for="savepath" class="leftLabelLarge">QBT_TR(Save files to location:)QBT_TR[CONTEXT=HttpServer]</label>
@ -23,7 +21,7 @@
</div>
<div class="formRow">
<label for="rename" class="leftLabelLarge">QBT_TR(Rename torrent)QBT_TR[CONTEXT=HttpServer]</label>
<input type="text" name="rename" style="width: 16em;"/>
<input type="text" id="rename" name="rename" style="width: 16em;"/>
</div>
<div class="formRow">
<label for="category" class="leftLabelLarge">QBT_TR(Category:)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
@ -36,27 +34,27 @@
</div>
<div class="formRow">
<label for="skip_checking" class="leftLabelLarge">QBT_TR(Skip hash check)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
<input type="checkbox" name="skip_checking" value="true"/>
<input type="checkbox" id="skip_checking" name="skip_checking" value="true"/>
</div>
<div class="formRow">
<label for="root_folder" class="leftLabelLarge">QBT_TR(Create subfolder)QBT_TR[CONTEXT=AddNewTorrentDialog]</label>
<input type="checkbox" name="root_folder" value="true" checked="checked"/>
<input type="checkbox" id="root_folder" name="root_folder" value="true" checked="checked"/>
</div>
<div class="formRow">
<label for="sequentialDownload" class="leftLabelLarge">QBT_TR(Download in sequential order)QBT_TR[CONTEXT=TransferListWidget]</label>
<input type="checkbox" name="sequentialDownload" value="true"/>
<input type="checkbox" id="sequentialDownload" name="sequentialDownload" value="true"/>
</div>
<div class="formRow">
<label for="firstLastPiecePrio" class="leftLabelLarge">QBT_TR(Download first and last pieces first)QBT_TR[CONTEXT=TransferListWidget]</label>
<input type="checkbox" name="firstLastPiecePrio" value="true"/>
<input type="checkbox" id="firstLastPiecePrio" name="firstLastPiecePrio" value="true"/>
</div>
<div class="formRow">
<label for="dlLimit" class="leftLabelLarge">QBT_TR(Limit download rate)QBT_TR[CONTEXT=HttpServer]</label>
<input type="text" name="dlLimit" style="width: 16em;" placeholder="Bytes/s"/>
<input type="text" id="dlLimit" name="dlLimit" style="width: 16em;" placeholder="Bytes/s"/>
</div>
<div class="formRow">
<label for="upLimit" class="leftLabelLarge">QBT_TR(Limit upload rate)QBT_TR[CONTEXT=HttpServer]</label>
<input type="text" name="upLimit" style="width: 16em;" placeholder="Bytes/s"/>
<input type="text" id="upLimit" name="upLimit" style="width: 16em;" placeholder="Bytes/s"/>
</div>
<div id="submitbutton" style="margin-top: 30px; text-align: center;">
<button type="submit" style="font-size: 1em;">QBT_TR(Upload Torrents)QBT_TR[CONTEXT=HttpServer]</button>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>QBT_TR(Torrent Upload Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]</title>
@ -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('|'),

View file

@ -1,8 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" dir="ltr">
<!DOCTYPE html>
<html lang="${LANG}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>qBittorrent QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]</title>
<link rel="icon" type="image/png" href="images/skin/qbittorrent16.png" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<script type="text/javascript" src="scripts/mootools-1.2-core-yc.js" charset="utf-8"></script>
<script type="text/javascript">
@ -20,7 +21,7 @@
function submitLoginForm() {
new Request({
url: 'login',
url: 'api/v2/auth/login',
method: 'post',
data: $('loginform').toQueryString(),
onComplete: function() {
@ -74,7 +75,7 @@
<img src="images/skin/qbittorrent-tray.svg" alt="qBittorrent logo" style="height: 11em;"/>
</div>
<div id="formplace" class="col">
<form id="loginform" action="">
<form id="loginform">
<div class="row"><label for="username">QBT_TR(Username)QBT_TR[CONTEXT=HttpServer]</label><br /><input type="text" id="username" name="username" /></div>
<div class="row"><label for="password">QBT_TR(Password)QBT_TR[CONTEXT=HttpServer]</label><br /><input type="password" id="password" name="password" /></div>
<div class="row"><input type="submit" id="login" value="QBT_TR(Login)QBT_TR[CONTEXT=HttpServer]" /></div>