Browse Source

Redesign Web API

Normalize Web API method names.
Allow to use alternative Web UI.
Switch Web API version to standard form (i.e. "2.0").
Improve Web UI translation code.
Retranslate changed files.
Add Web API for RSS subsystem.
adaptive-webui-19844
Vladimir Golovnev (Glassez) 7 years ago
parent
commit
27d8dbf13b
No known key found for this signature in database
GPG Key ID: 52A2C7DEE2DFA6F7
  1. 2
      src/base/CMakeLists.txt
  2. 2
      src/base/base.pri
  3. 2
      src/base/bittorrent/tracker.cpp
  4. 3
      src/base/bittorrent/tracker.h
  5. 81
      src/base/http/httperror.cpp
  6. 86
      src/base/http/httperror.h
  7. 5
      src/base/http/responsebuilder.cpp
  8. 6
      src/base/http/responsebuilder.h
  9. 20
      src/base/preferences.cpp
  10. 4
      src/base/preferences.h
  11. 19
      src/base/utils/fs.cpp
  12. 1
      src/base/utils/fs.h
  13. 18
      src/base/utils/string.cpp
  14. 4
      src/base/utils/string.h
  15. 11
      src/gui/optionsdlg.cpp
  16. 25
      src/gui/optionsdlg.ui
  17. 29
      src/webui/CMakeLists.txt
  18. 525
      src/webui/abstractwebapplication.cpp
  19. 116
      src/webui/abstractwebapplication.h
  20. 91
      src/webui/api/apicontroller.cpp
  21. 72
      src/webui/api/apicontroller.h
  22. 40
      src/webui/api/apierror.cpp
  23. 38
      src/webui/api/apierror.h
  24. 73
      src/webui/api/appcontroller.cpp
  25. 50
      src/webui/api/appcontroller.h
  26. 108
      src/webui/api/authcontroller.cpp
  27. 59
      src/webui/api/authcontroller.h
  28. 49
      src/webui/api/isessionmanager.h
  29. 124
      src/webui/api/logcontroller.cpp
  30. 25
      src/webui/api/logcontroller.h
  31. 131
      src/webui/api/rsscontroller.cpp
  32. 51
      src/webui/api/rsscontroller.h
  33. 140
      src/webui/api/serialize/serialize_torrent.cpp
  34. 79
      src/webui/api/serialize/serialize_torrent.h
  35. 473
      src/webui/api/synccontroller.cpp
  36. 23
      src/webui/api/synccontroller.h
  37. 805
      src/webui/api/torrentscontroller.cpp
  38. 74
      src/webui/api/torrentscontroller.h
  39. 113
      src/webui/api/transfercontroller.cpp
  40. 49
      src/webui/api/transfercontroller.h
  41. 1134
      src/webui/btjson.cpp
  42. 62
      src/webui/btjson.h
  43. 1319
      src/webui/webapplication.cpp
  44. 189
      src/webui/webapplication.h
  45. 4
      src/webui/webui.h
  46. 29
      src/webui/webui.pri
  47. 82
      src/webui/webui.qrc
  48. 0
      src/webui/www/private/about.html
  49. 7
      src/webui/www/private/addtrackers.html
  50. 8
      src/webui/www/private/confirmdeletion.html
  51. 0
      src/webui/www/private/css/Core.css
  52. 0
      src/webui/www/private/css/Layout.css
  53. 0
      src/webui/www/private/css/Tabs.css
  54. 0
      src/webui/www/private/css/Window.css
  55. 0
      src/webui/www/private/css/dynamicTable.css
  56. 481
      src/webui/www/private/css/style.css
  57. 2
      src/webui/www/private/download.html
  58. 4
      src/webui/www/private/downloadlimit.html
  59. 0
      src/webui/www/private/filters.html
  60. 1
      src/webui/www/private/index.html
  61. 4
      src/webui/www/private/newcategory.html
  62. 0
      src/webui/www/private/preferences.html
  63. 23
      src/webui/www/private/preferences_content.html
  64. 0
      src/webui/www/private/properties.html
  65. 0
      src/webui/www/private/properties_content.html
  66. 2
      src/webui/www/private/rename.html
  67. 6
      src/webui/www/private/scripts/client.js
  68. 0
      src/webui/www/private/scripts/clipboard.min.js
  69. 0
      src/webui/www/private/scripts/contextmenu.js
  70. 2
      src/webui/www/private/scripts/download.js
  71. 0
      src/webui/www/private/scripts/dynamicTable.js
  72. 0
      src/webui/www/private/scripts/excanvas-compressed.js
  73. 0
      src/webui/www/private/scripts/misc.js
  74. 106
      src/webui/www/private/scripts/mocha-init.js
  75. 0
      src/webui/www/private/scripts/mocha-yc.js
  76. 0
      src/webui/www/private/scripts/mocha.js
  77. 527
      src/webui/www/private/scripts/mootools-1.2-core-yc.js
  78. 0
      src/webui/www/private/scripts/mootools-1.2-more.js
  79. 8
      src/webui/www/private/scripts/parametrics.js
  80. 0
      src/webui/www/private/scripts/progressbar.js
  81. 4
      src/webui/www/private/scripts/prop-files.js
  82. 2
      src/webui/www/private/scripts/prop-general.js
  83. 2
      src/webui/www/private/scripts/prop-trackers.js
  84. 2
      src/webui/www/private/scripts/prop-webseeds.js
  85. 2
      src/webui/www/private/setlocation.html
  86. 0
      src/webui/www/private/statistics.html
  87. 8
      src/webui/www/private/transferlist.html
  88. 10
      src/webui/www/private/upload.html
  89. 4
      src/webui/www/private/uploadlimit.html
  90. 3
      src/webui/www/public/login.html

2
src/base/CMakeLists.txt

@ -19,6 +19,7 @@ bittorrent/torrentinfo.h @@ -19,6 +19,7 @@ bittorrent/torrentinfo.h
bittorrent/tracker.h
bittorrent/trackerentry.h
http/connection.h
http/httperror.h
http/irequesthandler.h
http/requestparser.h
http/responsebuilder.h
@ -85,6 +86,7 @@ bittorrent/torrentinfo.cpp @@ -85,6 +86,7 @@ bittorrent/torrentinfo.cpp
bittorrent/tracker.cpp
bittorrent/trackerentry.cpp
http/connection.cpp
http/httperror.cpp
http/requestparser.cpp
http/responsebuilder.cpp
http/responsegenerator.cpp

2
src/base/base.pri

@ -21,6 +21,7 @@ HEADERS += \ @@ -21,6 +21,7 @@ HEADERS += \
$$PWD/filesystemwatcher.h \
$$PWD/global.h \
$$PWD/http/connection.h \
$$PWD/http/httperror.h \
$$PWD/http/irequesthandler.h \
$$PWD/http/requestparser.h \
$$PWD/http/responsebuilder.h \
@ -86,6 +87,7 @@ SOURCES += \ @@ -86,6 +87,7 @@ SOURCES += \
$$PWD/exceptions.cpp \
$$PWD/filesystemwatcher.cpp \
$$PWD/http/connection.cpp \
$$PWD/http/httperror.cpp \
$$PWD/http/requestparser.cpp \
$$PWD/http/responsebuilder.cpp \
$$PWD/http/responsegenerator.cpp \

2
src/base/bittorrent/tracker.cpp

@ -74,7 +74,7 @@ libtorrent::entry Peer::toEntry(bool noPeerId) const @@ -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))
{
}

3
src/base/bittorrent/tracker.h

@ -31,6 +31,7 @@ @@ -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 @@ -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)

81
src/base/http/httperror.cpp

@ -0,0 +1,81 @@ @@ -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

@ -0,0 +1,86 @@ @@ -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 = "");
};

5
src/base/http/responsebuilder.cpp

@ -30,11 +30,6 @@ @@ -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);

6
src/base/http/responsebuilder.h

@ -29,17 +29,13 @@ @@ -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);

20
src/base/preferences.cpp

@ -609,6 +609,26 @@ void Preferences::setWebUiHttpsKey(const QByteArray &data) @@ -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();

4
src/base/preferences.h

@ -204,6 +204,10 @@ public: @@ -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;

19
src/base/utils/fs.cpp

@ -30,6 +30,10 @@ @@ -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 @@ @@ -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() @@ -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;
}

1
src/base/utils/fs.h

@ -56,6 +56,7 @@ namespace Utils @@ -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);

18
src/base/utils/string.cpp

@ -40,6 +40,8 @@ @@ -40,6 +40,8 @@
#include <QThreadStorage>
#endif
#include "../tristatebool.h"
namespace
{
class NaturalCompare
@ -184,3 +186,19 @@ QString Utils::String::wildcardToRegex(const QString &pattern) @@ -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;
}

4
src/base/utils/string.h

@ -34,6 +34,7 @@ @@ -34,6 +34,7 @@
class QByteArray;
class QLatin1String;
class TriStateBool;
namespace Utils
{
@ -66,6 +67,9 @@ namespace Utils @@ -66,6 +67,9 @@ namespace Utils
return str;
}
bool parseBool(const QString &string, const bool defaultValue);
TriStateBool parseTriStateBool(const QString &string);
}
}

11
src/gui/optionsdlg.cpp

@ -183,6 +183,9 @@ OptionsDialog::OptionsDialog(QWidget *parent) @@ -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) @@ -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() @@ -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() @@ -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
}

25
src/gui/optionsdlg.ui

@ -3014,6 +3014,31 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string> @@ -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">

29
src/webui/CMakeLists.txt

@ -1,18 +1,31 @@ @@ -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
)

525
src/webui/abstractwebapplication.cpp

@ -1,525 +0,0 @@ @@ -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);
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
}
data.replace(QLatin1String("${LANG}"), locale.left(2));
data.replace(QLatin1String("${VERSION}"), QBT_VERSION);
}
}
bool AbstractWebApplication::isBanned() const
{
return clientFailedAttempts_.value(env_.clientAddress, 0) >= MAX_AUTH_FAILED_ATTEMPTS;
}
int AbstractWebApplication::failedAttempts() const
{
return clientFailedAttempts_.value(env_.clientAddress, 0);
}
void AbstractWebApplication::resetFailedAttempts()
{
clientFailedAttempts_.remove(env_.clientAddress);
}
void AbstractWebApplication::increaseFailedAttempts()
{
const int nb_fail = clientFailedAttempts_.value(env_.clientAddress, 0) + 1;
clientFailedAttempts_[env_.clientAddress] = nb_fail;
if (nb_fail == MAX_AUTH_FAILED_ATTEMPTS) {
// Max number of failed attempts reached
// Start ban period
UnbanTimer* ubantimer = new UnbanTimer(env_.clientAddress, this);
connect(ubantimer, SIGNAL(timeout()), SLOT(UnbanTimerEvent()));
ubantimer->start();
}
}
bool AbstractWebApplication::isAuthNeeded()
{
qDebug("Checking auth rules against client address %s", qPrintable(env().clientAddress.toString()));
const Preferences *pref = Preferences::instance();
if (!pref->isWebUiLocalAuthEnabled() && Utils::Net::isLoopbackAddress(env().clientAddress))
return false;
if (pref->isWebUiAuthSubnetWhitelistEnabled() && Utils::Net::isIPInRange(env().clientAddress, pref->getWebUiAuthSubnetWhitelist()))
return false;
return true;
}
void AbstractWebApplication::printFile(const QString& path)
{
QByteArray data;
QString type;
if (!readFile(path, data, type)) {
status(404, "Not Found");
return;
}
print(data, type);
}
bool AbstractWebApplication::sessionStart()
{
if (session_ == 0) {
session_ = new WebSession(generateSid());
sessions_[session_->id] = session_;
QNetworkCookie cookie(C_SID, session_->id.toUtf8());
cookie.setHttpOnly(true);
cookie.setPath(QLatin1String("/"));
header(Http::HEADER_SET_COOKIE, cookie.toRawForm());
return true;
}
return false;
}
bool AbstractWebApplication::sessionEnd()
{
if ((session_ != 0) && (sessions_.contains(session_->id))) {
QNetworkCookie cookie(C_SID);
cookie.setPath(QLatin1String("/"));
cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1));
sessions_.remove(session_->id);
delete session_;
session_ = 0;
header(Http::HEADER_SET_COOKIE, cookie.toRawForm());
return true;
}
return false;
}
QString AbstractWebApplication::saveTmpFile(const QByteArray &data)
{
QTemporaryFile tmpfile(Utils::Fs::tempPath() + "XXXXXX.torrent");
tmpfile.setAutoRemove(false);
if (tmpfile.open()) {
tmpfile.write(data);
tmpfile.close();
return tmpfile.fileName();
}
qWarning() << "I/O Error: Could not create temporary file";
return QString();
}
bool AbstractWebApplication::isCrossSiteRequest(const Http::Request &request) const
{
// https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers
const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool
{
// [rfc6454] 5. Comparing Origins
return ((left.port() == right.port())
// && (left.scheme() == right.scheme()) // not present in this context
&& (left.host() == right.host()));
};
const QString targetOrigin = request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST));
const QString originValue = request.headers.value(Http::HEADER_ORIGIN);
const QString refererValue = request.headers.value(Http::HEADER_REFERER);
if (originValue.isEmpty() && refererValue.isEmpty()) {
// owasp.org recommends to block this request, but doing so will inevitably lead Web API users to spoof headers
// so lets be permissive here
return false;
}
// sent with CORS requests, as well as with POST requests
if (!originValue.isEmpty()) {
const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue);
if (isInvalid)
Logger::instance()->addMessage(tr("WebUI: Origin header & Target origin mismatch!") + "\n"
+ tr("Source IP: '%1'. Origin header: '%2'. Target origin: '%3'")
.arg(env_.clientAddress.toString()).arg(originValue).arg(targetOrigin)
, Log::WARNING);
return isInvalid;
}
if (!refererValue.isEmpty()) {
const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue);
if (isInvalid)
Logger::instance()->addMessage(tr("WebUI: Referer header & Target origin mismatch!") + "\n"
+ tr("Source IP: '%1'. Referer header: '%2'. Target origin: '%3'")
.arg(env_.clientAddress.toString()).arg(refererValue).arg(targetOrigin)
, Log::WARNING);
return isInvalid;
}
return true;
}
bool AbstractWebApplication::validateHostHeader(const QStringList &domains) const
{
const QUrl hostHeader = urlFromHostHeader(request().headers[Http::HEADER_HOST]);
const QString requestHost = hostHeader.host();
// (if present) try matching host header's port with local port
const int requestPort = hostHeader.port();
if ((requestPort != -1) && (env().localPort != requestPort)) {
Logger::instance()->addMessage(tr("WebUI: Invalid Host header, port mismatch.") + "\n"
+ tr("Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'")
.arg(env().clientAddress.toString()).arg(env().localPort)
.arg(request().headers[Http::HEADER_HOST])
, Log::WARNING);
return false;
}
// try matching host header with local address
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
const bool sameAddr = env().localAddress.isEqual(QHostAddress(requestHost));
#else
const auto equal = [](const Q_IPV6ADDR &l, const Q_IPV6ADDR &r) -> bool {
for (int i = 0; i < 16; ++i) {
if (l[i] != r[i])
return false;
}
return true;
};
const bool sameAddr = equal(env().localAddress.toIPv6Address(), QHostAddress(requestHost).toIPv6Address());
#endif
if (sameAddr)
return true;
// try matching host header with domain list
for (const auto &domain : domains) {
QRegExp domainRegex(domain, Qt::CaseInsensitive, QRegExp::Wildcard);
if (requestHost.contains(domainRegex))
return true;
}
Logger::instance()->addMessage(tr("WebUI: Invalid Host header.") + "\n"
+ tr("Request source IP: '%1'. Received Host header: '%2'")
.arg(env().clientAddress.toString()).arg(request().headers[Http::HEADER_HOST])
, Log::WARNING);
return false;
}
const QStringMap AbstractWebApplication::CONTENT_TYPE_BY_EXT = {
{ "htm", Http::CONTENT_TYPE_HTML },
{ "html", Http::CONTENT_TYPE_HTML },
{ "css", Http::CONTENT_TYPE_CSS },
{ "gif", Http::CONTENT_TYPE_GIF },
{ "png", Http::CONTENT_TYPE_PNG },
{ "js", Http::CONTENT_TYPE_JS },
{ "svg", Http::CONTENT_TYPE_SVG }
};
QStringMap AbstractWebApplication::parseCookie(const Http::Request &request) const
{
// [rfc6265] 4.2.1. Syntax
QStringMap ret;
const QString cookieStr = request.headers.value(QLatin1String("cookie"));
const QVector<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;
}

116
src/webui/abstractwebapplication.h

@ -1,116 +0,0 @@ @@ -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

91
src/webui/api/apicontroller.cpp

@ -0,0 +1,91 @@ @@ -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);
}

72
src/webui/api/apicontroller.h

@ -0,0 +1,72 @@ @@ -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;
};

40
src/webui/api/apierror.cpp

@ -0,0 +1,40 @@ @@ -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;
}

38
src/webui/jsonutils.h → src/webui/api/apierror.h

@ -1,6 +1,6 @@ @@ -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 @@ @@ -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;
};

73
src/webui/prefjson.cpp → src/webui/api/appcontroller.cpp

@ -1,6 +1,8 @@ @@ -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 @@ @@ -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"
void AppController::webapiVersionAction()
{
setResult(static_cast<QString>(API_VERSION));
}
prefjson::prefjson()
void AppController::versionAction()
{
setResult(QBT_VERSION);
}
QByteArray prefjson::getPreferences()
void AppController::shutdownAction()
{
const Preferences* const pref = Preferences::instance();
qDebug() << "Shutdown request from Web UI";
// Special case handling for shutdown, we
// need to reply to the Web UI before
// actually shutting down.
QTimer::singleShot(100, qApp, &QCoreApplication::quit);
}
void AppController::preferencesAction()
{
const Preferences *const pref = Preferences::instance();
auto session = BitTorrent::Session::instance();
QVariantMap data;
@ -187,14 +210,22 @@ QByteArray prefjson::getPreferences() @@ -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) @@ -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());
}

50
src/webui/api/appcontroller.h

@ -0,0 +1,50 @@ @@ -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();
};

108
src/webui/api/authcontroller.cpp

@ -0,0 +1,108 @@ @@ -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();
}
}

59
src/webui/api/authcontroller.h

@ -0,0 +1,59 @@ @@ -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;
};

49
src/webui/api/isessionmanager.h

@ -0,0 +1,49 @@ @@ -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;
};

124
src/webui/api/logcontroller.cpp

@ -0,0 +1,124 @@ @@ -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));
}

25
src/webui/prefjson.h → src/webui/api/logcontroller.h

@ -1,6 +1,6 @@ @@ -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,21 @@ @@ -24,24 +24,21 @@
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*
* Contact : chris@qbittorrent.org
*/
#ifndef PREFJSON_H
#define PREFJSON_H
#pragma once
#include <QString>
#include "apicontroller.h"
class prefjson
class LogController : public APIController
{
private:
prefjson();
Q_OBJECT
Q_DISABLE_COPY(LogController)
public:
static QByteArray getPreferences();
static void setPreferences(const QString& json);
using APIController::APIController;
private slots:
void mainAction();
void peersAction();
};
#endif // PREFJSON_H

131
src/webui/api/rsscontroller.cpp

@ -0,0 +1,131 @@ @@ -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);
}

51
src/webui/api/rsscontroller.h

@ -0,0 +1,51 @@ @@ -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();
};

140
src/webui/api/serialize/serialize_torrent.cpp

@ -0,0 +1,140 @@ @@ -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;
}

79
src/webui/api/serialize/serialize_torrent.h

@ -0,0 +1,79 @@ @@ -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);

473
src/webui/api/synccontroller.cpp

@ -0,0 +1,473 @@ @@ -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);
}

23
src/webui/websessiondata.h → src/webui/api/synccontroller.h

@ -1,6 +1,6 @@ @@ -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 @@ @@ -26,18 +26,19 @@
* exception statement from your version.
*/
#ifndef WEBSESSIONDATA
#define WEBSESSIONDATA
#pragma once
#include <QVariant>
#include "apicontroller.h"
struct WebSessionData
class SyncController : public APIController
{
QVariantMap syncMainDataLastResponse;
QVariantMap syncMainDataLastAcceptedResponse;
QVariantMap syncTorrentPeersLastResponse;
QVariantMap syncTorrentPeersLastAcceptedResponse;
};
Q_OBJECT
Q_DISABLE_COPY(SyncController)
#endif // WEBSESSIONDATA
public:
using APIController::APIController;
private slots:
void maindataAction();
void torrentPeersAction();
};

805
src/webui/api/torrentscontroller.cpp

@ -0,0 +1,805 @@ @@ -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);
}

74
src/webui/api/torrentscontroller.h

@ -0,0 +1,74 @@ @@ -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();
};

113
src/webui/api/transfercontroller.cpp

@ -0,0 +1,113 @@ @@ -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()));
}

49
src/webui/api/transfercontroller.h

@ -0,0 +1,49 @@ @@ -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();
};

1134
src/webui/btjson.cpp

File diff suppressed because it is too large Load Diff

62
src/webui/btjson.h

@ -1,62 +0,0 @@ @@ -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

1319
src/webui/webapplication.cpp

File diff suppressed because it is too large Load Diff

189
src/webui/webapplication.h

@ -1,6 +1,6 @@ @@ -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 @@ @@ -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();
typedef void (WebApplication::*Action)();
QString scope_;
QString action_;
QStringList args_;
void doProcessRequest() override;
bool isPublicScope();
void parsePath();
static QMap<QString, QMap<QString, Action> > initializeActions();
static QMap<QString, QMap<QString, Action> > actions_;
};
void doProcessRequest();
void configure();
void registerAPIController(const QString &scope, APIController *controller);
void declarePublicAPI(const QString &apiPath);
void sendFile(const QString &path);
void sendWebUIFile();
#endif // WEBAPPLICATION_H
// Session management
QString generateSid() const;
void sessionInitialize();
bool isAuthNeeded();
bool isPublicAPI(const QString &scope, const QString &action) const;
bool isCrossSiteRequest(const Http::Request &request) const;
bool validateHostHeader(const QStringList &domains) const;
// Persistent data
QMap<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;
};

4
src/webui/webui.h

@ -42,7 +42,7 @@ namespace Net @@ -42,7 +42,7 @@ namespace Net
class DNSUpdater;
}
class AbstractWebApplication;
class WebApplication;
class WebUI : public QObject
{
@ -64,7 +64,7 @@ private: @@ -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;
};

29
src/webui/webui.pri

@ -1,17 +1,30 @@ @@ -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

82
src/webui/webui.qrc

@ -1,47 +1,49 @@ @@ -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>

0
src/webui/www/public/about.html → src/webui/www/private/about.html

7
src/webui/www/public/addtrackers.html → src/webui/www/private/addtrackers.html

@ -13,9 +13,12 @@ @@ -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();
}

8
src/webui/www/public/confirmdeletion.html → src/webui/www/private/confirmdeletion.html

@ -17,14 +17,14 @@ @@ -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();

0
src/webui/www/public/css/Core.css → src/webui/www/private/css/Core.css

0
src/webui/www/public/css/Layout.css → src/webui/www/private/css/Layout.css

0
src/webui/www/public/css/Tabs.css → src/webui/www/private/css/Tabs.css

0
src/webui/www/public/css/Window.css → src/webui/www/private/css/Window.css

0
src/webui/www/public/css/dynamicTable.css → src/webui/www/private/css/dynamicTable.css

481
src/webui/www/private/css/style.css

@ -0,0 +1,481 @@ @@ -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;
}

2
src/webui/www/public/download.html → src/webui/www/private/download.html

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
</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">
<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>

4
src/webui/www/public/downloadlimit.html → src/webui/www/private/downloadlimit.html

@ -25,7 +25,7 @@ @@ -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 @@ @@ -38,7 +38,7 @@
}
else {
new Request({
url: 'command/setTorrentsDlLimit',
url: 'api/v2/torrents/setDownloadLimit',
method: 'post',
data: {
'hashes': hashes.join('|'),

0
src/webui/www/public/filters.html → src/webui/www/private/filters.html

1
src/webui/www/private/index.html

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-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" />-->

4
src/webui/www/public/newcategory.html → src/webui/www/private/newcategory.html

@ -33,7 +33,7 @@ @@ -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 @@ @@ -46,7 +46,7 @@
else
{
new Request({
url: 'command/setCategory',
url: 'api/v2/torrents/setCategory',
method: 'post',
data: {
hashes: hashesList,

0
src/webui/www/public/preferences.html → src/webui/www/private/preferences.html

23
src/webui/www/public/preferences_content.html → src/webui/www/private/preferences_content.html

@ -826,7 +826,7 @@ time_padding = function(val) { @@ -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() { @@ -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();
};

0
src/webui/www/public/properties.html → src/webui/www/private/properties.html

0
src/webui/www/public/properties_content.html → src/webui/www/private/properties_content.html

2
src/webui/www/public/rename.html → src/webui/www/private/rename.html

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

6
src/webui/www/public/scripts/client.js → src/webui/www/private/scripts/client.js

@ -273,7 +273,7 @@ window.addEvent('load', function () { @@ -273,7 +273,7 @@ window.addEvent('load', function () {
var syncMainDataTimer;
var syncMainData = function () {
var url = new URI('sync/maindata');
var url = new URI('api/v2/sync/maindata');
url.setData('rid', syncMainDataLastResponseId);
var request = new Request.JSON({
url : url,
@ -445,7 +445,7 @@ window.addEvent('load', function () { @@ -445,7 +445,7 @@ window.addEvent('load', function () {
// Change icon immediately to give some feedback
updateAltSpeedIcon(!alternativeSpeedLimits);
new Request({url: 'command/toggleAlternativeSpeedLimits',
new Request({url: 'api/v2/transfer/toggleSpeedLimitsMode',
method: 'post',
onComplete: function() {
alternativeSpeedLimits = !alternativeSpeedLimits;
@ -665,7 +665,7 @@ var loadTorrentPeersData = function(){ @@ -665,7 +665,7 @@ var loadTorrentPeersData = function(){
loadTorrentPeersTimer = loadTorrentPeersData.delay(syncMainDataTimerPeriod);
return;
}
var url = new URI('sync/torrent_peers');
var url = new URI('api/v2/sync/torrentPeers');
url.setData('rid', syncTorrentPeersLastResponseId);
url.setData('hash', current_hash);
var request = new Request.JSON({

0
src/webui/www/public/scripts/clipboard.min.js → src/webui/www/private/scripts/clipboard.min.js vendored

0
src/webui/www/public/scripts/contextmenu.js → src/webui/www/private/scripts/contextmenu.js

2
src/webui/www/public/scripts/download.js → src/webui/www/private/scripts/download.js

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

0
src/webui/www/public/scripts/dynamicTable.js → src/webui/www/private/scripts/dynamicTable.js

0
src/webui/www/public/scripts/excanvas-compressed.js → src/webui/www/private/scripts/excanvas-compressed.js

0
src/webui/www/public/scripts/misc.js → src/webui/www/private/scripts/misc.js

106
src/webui/www/public/scripts/mocha-init.js → src/webui/www/private/scripts/mocha-init.js

@ -142,7 +142,7 @@ initializeWindows = function() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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();

0
src/webui/www/public/scripts/mocha-yc.js → src/webui/www/private/scripts/mocha-yc.js

0
src/webui/www/public/scripts/mocha.js → src/webui/www/private/scripts/mocha.js

527
src/webui/www/private/scripts/mootools-1.2-core-yc.js

@ -0,0 +1,527 @@ @@ -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);};})();

0
src/webui/www/public/scripts/mootools-1.2-more.js → src/webui/www/private/scripts/mootools-1.2-more.js

8
src/webui/www/public/scripts/parametrics.js → src/webui/www/private/scripts/parametrics.js

@ -21,7 +21,7 @@ MochaUI.extend({ @@ -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({ @@ -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({ @@ -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({ @@ -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: {

0
src/webui/www/public/scripts/progressbar.js → src/webui/www/private/scripts/progressbar.js

4
src/webui/www/public/scripts/prop-files.js → src/webui/www/private/scripts/prop-files.js

@ -94,7 +94,7 @@ var allCBUnchecked = function() { @@ -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() { @@ -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,

2
src/webui/www/public/scripts/prop-general.js → src/webui/www/private/scripts/prop-general.js

@ -41,7 +41,7 @@ var loadTorrentData = function() { @@ -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,

2
src/webui/www/public/scripts/prop-trackers.js → src/webui/www/private/scripts/prop-trackers.js

@ -70,7 +70,7 @@ var loadTrackersData = function() { @@ -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,

2
src/webui/www/public/scripts/prop-webseeds.js → src/webui/www/private/scripts/prop-webseeds.js

@ -70,7 +70,7 @@ var loadWebSeedsData = function() { @@ -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,

2
src/webui/www/public/setlocation.html → src/webui/www/private/setlocation.html

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

0
src/webui/www/public/statistics.html → src/webui/www/private/statistics.html

8
src/webui/www/public/transferlist.html → src/webui/www/private/transferlist.html

@ -44,16 +44,16 @@ @@ -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) {

10
src/webui/www/public/upload.html → src/webui/www/private/upload.html

@ -10,12 +10,10 @@ @@ -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>

4
src/webui/www/public/uploadlimit.html → src/webui/www/private/uploadlimit.html

@ -25,7 +25,7 @@ @@ -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 @@ @@ -38,7 +38,7 @@
}
else {
new Request({
url: 'command/setTorrentsUpLimit',
url: 'api/v2/torrents/set_upload_limit',
method: 'post',
data: {
'hashes': hashes.join('|'),

3
src/webui/www/private/login.html → src/webui/www/public/login.html

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
<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 @@ @@ -20,7 +21,7 @@
function submitLoginForm() {
new Request({
url: 'login',
url: 'api/v2/auth/login',
method: 'post',
data: $('loginform').toQueryString(),
onComplete: function() {
Loading…
Cancel
Save