зеркало из https://github.com/nextcloud/desktop.git
Merge pull request #3186 from nextcloud/feature/share-obey-enforced-password-for-share-by-mail
Obey enforced password for share by email.
This commit is contained in:
Коммит
14843b7a62
|
@ -142,7 +142,8 @@ void OcsShareJob::createLinkShare(const QString &path,
|
|||
void OcsShareJob::createShare(const QString &path,
|
||||
const Share::ShareType shareType,
|
||||
const QString &shareWith,
|
||||
const Share::Permissions permissions)
|
||||
const Share::Permissions permissions,
|
||||
const QString &password)
|
||||
{
|
||||
Q_UNUSED(permissions)
|
||||
setVerb("POST");
|
||||
|
@ -151,6 +152,10 @@ void OcsShareJob::createShare(const QString &path,
|
|||
addParam(QString::fromLatin1("shareType"), QString::number(shareType));
|
||||
addParam(QString::fromLatin1("shareWith"), shareWith);
|
||||
|
||||
if (!password.isEmpty()) {
|
||||
addParam(QString::fromLatin1("password"), password);
|
||||
}
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
|
|
|
@ -113,11 +113,13 @@ public:
|
|||
* @param shareType The type of share (user/group/link/federated)
|
||||
* @param shareWith The uid/gid/federated id to share with
|
||||
* @param permissions The permissions the share will have
|
||||
* @param password The password to protect the share with
|
||||
*/
|
||||
void createShare(const QString &path,
|
||||
const Share::ShareType shareType,
|
||||
const QString &shareWith = "",
|
||||
const Share::Permissions permissions = SharePermissionRead);
|
||||
const Share::Permissions permissions = SharePermissionRead,
|
||||
const QString &password = "");
|
||||
|
||||
/**
|
||||
* Returns information on the items shared with the current user.
|
||||
|
|
|
@ -56,6 +56,7 @@ Share::Share(AccountPtr account,
|
|||
const QString &ownerDisplayName,
|
||||
const QString &path,
|
||||
const ShareType shareType,
|
||||
bool isPasswordSet,
|
||||
const Permissions permissions,
|
||||
const QSharedPointer<Sharee> shareWith)
|
||||
: _account(account)
|
||||
|
@ -64,6 +65,7 @@ Share::Share(AccountPtr account,
|
|||
, _ownerDisplayName(ownerDisplayName)
|
||||
, _path(path)
|
||||
, _shareType(shareType)
|
||||
, _isPasswordSet(isPasswordSet)
|
||||
, _permissions(permissions)
|
||||
, _shareWith(shareWith)
|
||||
{
|
||||
|
@ -104,6 +106,19 @@ QSharedPointer<Sharee> Share::getShareWith() const
|
|||
return _shareWith;
|
||||
}
|
||||
|
||||
void Share::setPassword(const QString &password)
|
||||
{
|
||||
auto * const job = new OcsShareJob(_account);
|
||||
connect(job, &OcsShareJob::shareJobFinished, this, &Share::slotPasswordSet);
|
||||
connect(job, &OcsJob::ocsError, this, &Share::slotSetPasswordError);
|
||||
job->setPassword(getId(), password);
|
||||
}
|
||||
|
||||
bool Share::isPasswordSet() const
|
||||
{
|
||||
return _isPasswordSet;
|
||||
}
|
||||
|
||||
void Share::setPermissions(Permissions permissions)
|
||||
{
|
||||
auto *job = new OcsShareJob(_account);
|
||||
|
@ -142,6 +157,17 @@ void Share::slotOcsError(int statusCode, const QString &message)
|
|||
emit serverError(statusCode, message);
|
||||
}
|
||||
|
||||
void Share::slotPasswordSet(const QJsonDocument &, const QVariant &value)
|
||||
{
|
||||
_isPasswordSet = !value.toString().isEmpty();
|
||||
emit passwordSet();
|
||||
}
|
||||
|
||||
void Share::slotSetPasswordError(int statusCode, const QString &message)
|
||||
{
|
||||
emit passwordSetError(statusCode, message);
|
||||
}
|
||||
|
||||
QUrl LinkShare::getLink() const
|
||||
{
|
||||
return _url;
|
||||
|
@ -159,11 +185,6 @@ QDate LinkShare::getExpireDate() const
|
|||
return _expireDate;
|
||||
}
|
||||
|
||||
bool LinkShare::isPasswordSet() const
|
||||
{
|
||||
return _passwordSet;
|
||||
}
|
||||
|
||||
LinkShare::LinkShare(AccountPtr account,
|
||||
const QString &id,
|
||||
const QString &uidowner,
|
||||
|
@ -172,13 +193,12 @@ LinkShare::LinkShare(AccountPtr account,
|
|||
const QString &name,
|
||||
const QString &token,
|
||||
Permissions permissions,
|
||||
bool passwordSet,
|
||||
bool isPasswordSet,
|
||||
const QUrl &url,
|
||||
const QDate &expireDate)
|
||||
: Share(account, id, uidowner, ownerDisplayName, path, Share::TypeLink, permissions)
|
||||
: Share(account, id, uidowner, ownerDisplayName, path, Share::TypeLink, isPasswordSet, permissions)
|
||||
, _name(name)
|
||||
, _token(token)
|
||||
, _passwordSet(passwordSet)
|
||||
, _expireDate(expireDate)
|
||||
, _url(url)
|
||||
{
|
||||
|
@ -231,20 +251,6 @@ QString LinkShare::getToken() const
|
|||
return _token;
|
||||
}
|
||||
|
||||
void LinkShare::setPassword(const QString &password)
|
||||
{
|
||||
auto *job = new OcsShareJob(_account);
|
||||
connect(job, &OcsShareJob::shareJobFinished, this, &LinkShare::slotPasswordSet);
|
||||
connect(job, &OcsJob::ocsError, this, &LinkShare::slotSetPasswordError);
|
||||
job->setPassword(getId(), password);
|
||||
}
|
||||
|
||||
void LinkShare::slotPasswordSet(const QJsonDocument &, const QVariant &value)
|
||||
{
|
||||
_passwordSet = value.toString() != "";
|
||||
emit passwordSet();
|
||||
}
|
||||
|
||||
void LinkShare::setExpireDate(const QDate &date)
|
||||
{
|
||||
auto *job = new OcsShareJob(_account);
|
||||
|
@ -269,11 +275,6 @@ void LinkShare::slotExpireDateSet(const QJsonDocument &reply, const QVariant &va
|
|||
emit expireDateSet();
|
||||
}
|
||||
|
||||
void LinkShare::slotSetPasswordError(int statusCode, const QString &message)
|
||||
{
|
||||
emit passwordSetError(statusCode, message);
|
||||
}
|
||||
|
||||
void LinkShare::slotNameSet(const QJsonDocument &, const QVariant &value)
|
||||
{
|
||||
_name = value.toString();
|
||||
|
@ -286,15 +287,16 @@ UserGroupShare::UserGroupShare(AccountPtr account,
|
|||
const QString &ownerDisplayName,
|
||||
const QString &path,
|
||||
const ShareType shareType,
|
||||
bool isPasswordSet,
|
||||
const Permissions permissions,
|
||||
const QSharedPointer<Sharee> shareWith,
|
||||
const QDate &expireDate,
|
||||
const QString ¬e)
|
||||
: Share(account, id, owner, ownerDisplayName, path, shareType, permissions, shareWith)
|
||||
, _expireDate(expireDate)
|
||||
: Share(account, id, owner, ownerDisplayName, path, shareType, isPasswordSet, permissions, shareWith)
|
||||
, _note(note)
|
||||
, _expireDate(expireDate)
|
||||
{
|
||||
Q_ASSERT(shareType == TypeUser || shareType == TypeGroup);
|
||||
Q_ASSERT(shareType == TypeUser || shareType == TypeGroup || shareType == TypeEmail);
|
||||
Q_ASSERT(shareWith);
|
||||
}
|
||||
|
||||
|
@ -389,7 +391,8 @@ void ShareManager::slotLinkShareCreated(const QJsonDocument &reply)
|
|||
void ShareManager::createShare(const QString &path,
|
||||
const Share::ShareType shareType,
|
||||
const QString shareWith,
|
||||
const Share::Permissions desiredPermissions)
|
||||
const Share::Permissions desiredPermissions,
|
||||
const QString &password)
|
||||
{
|
||||
auto job = new OcsShareJob(_account);
|
||||
connect(job, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
|
||||
|
@ -416,7 +419,7 @@ void ShareManager::createShare(const QString &path,
|
|||
auto *job = new OcsShareJob(_account);
|
||||
connect(job, &OcsShareJob::shareJobFinished, this, &ShareManager::slotShareCreated);
|
||||
connect(job, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
|
||||
job->createShare(path, shareType, shareWith, validPermissions);
|
||||
job->createShare(path, shareType, shareWith, validPermissions, password);
|
||||
});
|
||||
job->getSharedWithMe();
|
||||
}
|
||||
|
@ -458,7 +461,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply)
|
|||
|
||||
if (shareType == Share::TypeLink) {
|
||||
newShare = parseLinkShare(data);
|
||||
} else if (shareType == Share::TypeGroup || shareType == Share::TypeUser) {
|
||||
} else if (shareType == Share::TypeGroup || shareType == Share::TypeUser || shareType == Share::TypeEmail) {
|
||||
newShare = parseUserGroupShare(data);
|
||||
} else {
|
||||
newShare = parseShare(data);
|
||||
|
@ -493,6 +496,7 @@ QSharedPointer<UserGroupShare> ShareManager::parseUserGroupShare(const QJsonObje
|
|||
data.value("displayname_owner").toVariant().toString(),
|
||||
data.value("path").toString(),
|
||||
static_cast<Share::ShareType>(data.value("share_type").toInt()),
|
||||
!data.value("password").toString().isEmpty(),
|
||||
static_cast<Share::Permissions>(data.value("permissions").toInt()),
|
||||
sharee,
|
||||
expireDate,
|
||||
|
@ -546,6 +550,7 @@ QSharedPointer<Share> ShareManager::parseShare(const QJsonObject &data)
|
|||
data.value("displayname_owner").toVariant().toString(),
|
||||
data.value("path").toString(),
|
||||
(Share::ShareType)data.value("share_type").toInt(),
|
||||
!data.value("password").toString().isEmpty(),
|
||||
(Share::Permissions)data.value("permissions").toInt(),
|
||||
sharee));
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ public:
|
|||
const QString &ownerDisplayName,
|
||||
const QString &path,
|
||||
const ShareType shareType,
|
||||
bool isPasswordSet = false,
|
||||
const Permissions permissions = SharePermissionDefault,
|
||||
const QSharedPointer<Sharee> shareWith = QSharedPointer<Sharee>(nullptr));
|
||||
|
||||
|
@ -109,7 +110,17 @@ public:
|
|||
*/
|
||||
void setPermissions(Permissions permissions);
|
||||
|
||||
/**
|
||||
/*
|
||||
* Set the password for remote share
|
||||
*
|
||||
* On success the passwordSet signal is emitted
|
||||
* In case of a server error the passwordSetError signal is emitted.
|
||||
*/
|
||||
void setPassword(const QString &password);
|
||||
|
||||
bool isPasswordSet() const;
|
||||
|
||||
/*
|
||||
* Deletes a share
|
||||
*
|
||||
* On success the shareDeleted signal is emitted
|
||||
|
@ -121,6 +132,8 @@ signals:
|
|||
void permissionsSet();
|
||||
void shareDeleted();
|
||||
void serverError(int code, const QString &message);
|
||||
void passwordSet();
|
||||
void passwordSetError(int statusCode, const QString &message);
|
||||
|
||||
protected:
|
||||
AccountPtr _account;
|
||||
|
@ -129,11 +142,14 @@ protected:
|
|||
QString _ownerDisplayName;
|
||||
QString _path;
|
||||
ShareType _shareType;
|
||||
bool _isPasswordSet;
|
||||
Permissions _permissions;
|
||||
QSharedPointer<Sharee> _shareWith;
|
||||
|
||||
protected slots:
|
||||
void slotOcsError(int statusCode, const QString &message);
|
||||
void slotPasswordSet(const QJsonDocument &, const QVariant &value);
|
||||
void slotSetPasswordError(int statusCode, const QString &message);
|
||||
|
||||
private slots:
|
||||
void slotDeleted();
|
||||
|
@ -157,7 +173,7 @@ public:
|
|||
const QString &name,
|
||||
const QString &token,
|
||||
const Permissions permissions,
|
||||
bool passwordSet,
|
||||
bool isPasswordSet,
|
||||
const QUrl &url,
|
||||
const QDate &expireDate);
|
||||
|
||||
|
@ -210,19 +226,6 @@ public:
|
|||
*/
|
||||
QString getToken() const;
|
||||
|
||||
/*
|
||||
* Set the password
|
||||
*
|
||||
* On success the passwordSet signal is emitted
|
||||
* In case of a server error the serverError signal is emitted.
|
||||
*/
|
||||
void setPassword(const QString &password);
|
||||
|
||||
/*
|
||||
* Is the password set?
|
||||
*/
|
||||
bool isPasswordSet() const;
|
||||
|
||||
/*
|
||||
* Get the expiration date
|
||||
*/
|
||||
|
@ -238,22 +241,17 @@ public:
|
|||
|
||||
signals:
|
||||
void expireDateSet();
|
||||
void passwordSet();
|
||||
void noteSet();
|
||||
void passwordSetError(int statusCode, const QString &message);
|
||||
void nameSet();
|
||||
|
||||
private slots:
|
||||
void slotPasswordSet(const QJsonDocument &, const QVariant &value);
|
||||
void slotNoteSet(const QJsonDocument &, const QVariant &value);
|
||||
void slotExpireDateSet(const QJsonDocument &reply, const QVariant &value);
|
||||
void slotSetPasswordError(int statusCode, const QString &message);
|
||||
void slotNameSet(const QJsonDocument &, const QVariant &value);
|
||||
|
||||
private:
|
||||
QString _name;
|
||||
QString _token;
|
||||
bool _passwordSet;
|
||||
QString _note;
|
||||
QDate _expireDate;
|
||||
QUrl _url;
|
||||
|
@ -269,6 +267,7 @@ public:
|
|||
const QString &ownerDisplayName,
|
||||
const QString &path,
|
||||
const ShareType shareType,
|
||||
bool isPasswordSet,
|
||||
const Permissions permissions,
|
||||
const QSharedPointer<Sharee> shareWith,
|
||||
const QDate &expireDate,
|
||||
|
@ -335,7 +334,8 @@ public:
|
|||
void createShare(const QString &path,
|
||||
const Share::ShareType shareType,
|
||||
const QString shareWith,
|
||||
const Share::Permissions permissions);
|
||||
const Share::Permissions permissions,
|
||||
const QString &password = "");
|
||||
|
||||
/**
|
||||
* Fetch all the shares for path
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QDesktopServices>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QCryptographicHash>
|
||||
#include <QColor>
|
||||
|
@ -48,6 +49,10 @@
|
|||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
const char *passwordIsSetPlaceholder = "●●●●●●●●";
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
ShareUserGroupWidget::ShareUserGroupWidget(AccountPtr account,
|
||||
|
@ -97,7 +102,7 @@ ShareUserGroupWidget::ShareUserGroupWidget(AccountPtr account,
|
|||
|
||||
_manager = new ShareManager(_account, this);
|
||||
connect(_manager, &ShareManager::sharesFetched, this, &ShareUserGroupWidget::slotSharesFetched);
|
||||
connect(_manager, &ShareManager::shareCreated, this, &ShareUserGroupWidget::getShares);
|
||||
connect(_manager, &ShareManager::shareCreated, this, &ShareUserGroupWidget::slotShareCreated);
|
||||
connect(_manager, &ShareManager::serverError, this, &ShareUserGroupWidget::displayError);
|
||||
connect(_ui->shareeLineEdit, &QLineEdit::returnPressed, this, &ShareUserGroupWidget::slotLineEditReturn);
|
||||
connect(_ui->confirmShare, &QAbstractButton::clicked, this, &ShareUserGroupWidget::slotLineEditReturn);
|
||||
|
@ -202,6 +207,16 @@ void ShareUserGroupWidget::getShares()
|
|||
_manager->fetchShares(_sharePath);
|
||||
}
|
||||
|
||||
void ShareUserGroupWidget::slotShareCreated(const QSharedPointer<Share> &share)
|
||||
{
|
||||
if (share && _account->capabilities().shareEmailPasswordEnabled() && !_account->capabilities().shareEmailPasswordEnforced()) {
|
||||
// remember this share Id so we can set it's password Line Edit to focus later
|
||||
_lastCreatedShareId = share->getId();
|
||||
}
|
||||
// fetch all shares including the one we've just created
|
||||
getShares();
|
||||
}
|
||||
|
||||
void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>> &shares)
|
||||
{
|
||||
QScrollArea *scrollArea = _parentScrollArea;
|
||||
|
@ -213,6 +228,8 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
|
|||
int height = 0;
|
||||
QList<QString> linkOwners({});
|
||||
|
||||
ShareUserLine *justCreatedShareThatNeedsPassword = nullptr;
|
||||
|
||||
foreach (const auto &share, shares) {
|
||||
// We don't handle link shares, only TypeUser or TypeGroup
|
||||
if (share->getShareType() == Share::TypeLink) {
|
||||
|
@ -230,9 +247,9 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
|
|||
}
|
||||
|
||||
|
||||
Q_ASSERT(share->getShareType() == Share::TypeUser || share->getShareType() == Share::TypeGroup);
|
||||
Q_ASSERT(share->getShareType() == Share::TypeUser || share->getShareType() == Share::TypeGroup || share->getShareType() == Share::TypeEmail);
|
||||
auto userGroupShare = qSharedPointerDynamicCast<UserGroupShare>(share);
|
||||
auto *s = new ShareUserLine(userGroupShare, _maxSharingPermissions, _isFile, _parentScrollArea);
|
||||
auto *s = new ShareUserLine(_account, userGroupShare, _maxSharingPermissions, _isFile, _parentScrollArea);
|
||||
connect(s, &ShareUserLine::resizeRequested, this, &ShareUserGroupWidget::slotAdjustScrollWidgetSize);
|
||||
connect(s, &ShareUserLine::visualDeletionDone, this, &ShareUserGroupWidget::getShares);
|
||||
s->setBackgroundRole(layout->count() % 2 == 0 ? QPalette::Base : QPalette::AlternateBase);
|
||||
|
@ -242,6 +259,13 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
|
|||
|
||||
layout->addWidget(s);
|
||||
|
||||
if (!_lastCreatedShareId.isEmpty() && share->getId() == _lastCreatedShareId) {
|
||||
_lastCreatedShareId = QString();
|
||||
if (_account->capabilities().shareEmailPasswordEnabled() && !_account->capabilities().shareEmailPasswordEnforced()) {
|
||||
justCreatedShareThatNeedsPassword = s;
|
||||
}
|
||||
}
|
||||
|
||||
x++;
|
||||
if (x <= 3) {
|
||||
height = newViewPort->sizeHint().height();
|
||||
|
@ -266,6 +290,11 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
|
|||
|
||||
_disableCompleterActivated = false;
|
||||
activateShareeLineEdit();
|
||||
|
||||
if (justCreatedShareThatNeedsPassword) {
|
||||
// always set focus to a password Line Edit when the new email share is created on a server with optional passwords enabled for email shares
|
||||
justCreatedShareThatNeedsPassword->focusPasswordLineEdit();
|
||||
}
|
||||
}
|
||||
|
||||
void ShareUserGroupWidget::slotAdjustScrollWidgetSize()
|
||||
|
@ -338,6 +367,9 @@ void ShareUserGroupWidget::slotCompleterActivated(const QModelIndex &index)
|
|||
* https://github.com/owncloud/core/issues/22122#issuecomment-185637344
|
||||
* https://github.com/owncloud/client/issues/4996
|
||||
*/
|
||||
|
||||
_lastCreatedShareId = QString();
|
||||
|
||||
if (sharee->type() == Sharee::Federated
|
||||
&& _account->serverVersionInt() < Account::makeServerVersion(9, 1, 0)) {
|
||||
int permissions = SharePermissionRead | SharePermissionUpdate;
|
||||
|
@ -347,16 +379,36 @@ void ShareUserGroupWidget::slotCompleterActivated(const QModelIndex &index)
|
|||
_manager->createShare(_sharePath, Share::ShareType(sharee->type()),
|
||||
sharee->shareWith(), SharePermission(permissions));
|
||||
} else {
|
||||
QString password;
|
||||
if (sharee->type() == Sharee::Email && _account->capabilities().shareEmailPasswordEnforced()) {
|
||||
_ui->shareeLineEdit->clear();
|
||||
// always show a dialog for password-enforced email shares
|
||||
bool ok = false;
|
||||
|
||||
do {
|
||||
password = QInputDialog::getText(
|
||||
this,
|
||||
tr("Password for share required"),
|
||||
tr("Please enter a password for your email share:"),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&ok);
|
||||
} while (password.isEmpty() && ok);
|
||||
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default permissions on creation
|
||||
int permissions = SharePermissionCreate | SharePermissionUpdate
|
||||
| SharePermissionDelete | SharePermissionShare;
|
||||
_manager->createShare(_sharePath, Share::ShareType(sharee->type()),
|
||||
sharee->shareWith(), SharePermission(permissions));
|
||||
sharee->shareWith(), SharePermission(permissions), password);
|
||||
}
|
||||
|
||||
_ui->shareeLineEdit->setEnabled(false);
|
||||
_ui->shareeLineEdit->setText(QString());
|
||||
_ui->shareeLineEdit->clear();
|
||||
}
|
||||
|
||||
void ShareUserGroupWidget::slotCompleterHighlighted(const QModelIndex &index)
|
||||
|
@ -424,12 +476,14 @@ void ShareUserGroupWidget::activateShareeLineEdit()
|
|||
_ui->shareeLineEdit->setFocus();
|
||||
}
|
||||
|
||||
ShareUserLine::ShareUserLine(QSharedPointer<UserGroupShare> share,
|
||||
SharePermissions maxSharingPermissions,
|
||||
bool isFile,
|
||||
QWidget *parent)
|
||||
ShareUserLine::ShareUserLine(AccountPtr account,
|
||||
QSharedPointer<UserGroupShare> share,
|
||||
SharePermissions maxSharingPermissions,
|
||||
bool isFile,
|
||||
QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, _ui(new Ui::ShareUserLine)
|
||||
, _account(account)
|
||||
, _share(share)
|
||||
, _isFile(isFile)
|
||||
{
|
||||
|
@ -454,6 +508,9 @@ ShareUserLine::ShareUserLine(QSharedPointer<UserGroupShare> share,
|
|||
connect(_share.data(), &UserGroupShare::noteSetError, this, &ShareUserLine::disableProgessIndicatorAnimation);
|
||||
connect(_share.data(), &UserGroupShare::expireDateSet, this, &ShareUserLine::disableProgessIndicatorAnimation);
|
||||
|
||||
connect(_ui->confirmPassword, &QToolButton::clicked, this, &ShareUserLine::slotConfirmPasswordClicked);
|
||||
connect(_ui->lineEdit_password, &QLineEdit::returnPressed, this, &ShareUserLine::slotLineEditPasswordReturnPressed);
|
||||
|
||||
// create menu with checkable permissions
|
||||
auto *menu = new QMenu(this);
|
||||
_permissionReshare= new QAction(tr("Can reshare"), this);
|
||||
|
@ -463,25 +520,35 @@ ShareUserLine::ShareUserLine(QSharedPointer<UserGroupShare> share,
|
|||
connect(_permissionReshare, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged);
|
||||
|
||||
showNoteOptions(false);
|
||||
_noteLinkAction = new QAction(tr("Note to recipient"));
|
||||
_noteLinkAction->setCheckable(true);
|
||||
menu->addAction(_noteLinkAction);
|
||||
connect(_noteLinkAction, &QAction::triggered, this, &ShareUserLine::toggleNoteOptions);
|
||||
if (!_share->getNote().isEmpty()) {
|
||||
_noteLinkAction->setChecked(true);
|
||||
showNoteOptions(true);
|
||||
|
||||
// email shares do not support notes and expiration dates
|
||||
const bool isNoteAndExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail;
|
||||
|
||||
if (isNoteAndExpirationDateSupported) {
|
||||
_noteLinkAction = new QAction(tr("Note to recipient"));
|
||||
_noteLinkAction->setCheckable(true);
|
||||
menu->addAction(_noteLinkAction);
|
||||
connect(_noteLinkAction, &QAction::triggered, this, &ShareUserLine::toggleNoteOptions);
|
||||
if (!_share->getNote().isEmpty()) {
|
||||
_noteLinkAction->setChecked(true);
|
||||
showNoteOptions(true);
|
||||
}
|
||||
}
|
||||
|
||||
showExpireDateOptions(false);
|
||||
_expirationDateLinkAction = new QAction(tr("Set expiration date"));
|
||||
_expirationDateLinkAction->setCheckable(true);
|
||||
menu->addAction(_expirationDateLinkAction);
|
||||
connect(_expirationDateLinkAction, &QAction::triggered, this, &ShareUserLine::toggleExpireDateOptions);
|
||||
const auto expireDate = _share->getExpireDate().isValid() ? share.data()->getExpireDate() : QDate();
|
||||
if (!expireDate.isNull()) {
|
||||
_ui->calendar->setDate(expireDate);
|
||||
_expirationDateLinkAction->setChecked(true);
|
||||
showExpireDateOptions(true);
|
||||
|
||||
if (isNoteAndExpirationDateSupported) {
|
||||
// email shares do not support expiration dates
|
||||
_expirationDateLinkAction = new QAction(tr("Set expiration date"));
|
||||
_expirationDateLinkAction->setCheckable(true);
|
||||
menu->addAction(_expirationDateLinkAction);
|
||||
connect(_expirationDateLinkAction, &QAction::triggered, this, &ShareUserLine::toggleExpireDateOptions);
|
||||
const auto expireDate = _share->getExpireDate().isValid() ? share.data()->getExpireDate() : QDate();
|
||||
if (!expireDate.isNull()) {
|
||||
_ui->calendar->setDate(expireDate);
|
||||
_expirationDateLinkAction->setChecked(true);
|
||||
showExpireDateOptions(true);
|
||||
}
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
@ -516,9 +583,32 @@ ShareUserLine::ShareUserLine(QSharedPointer<UserGroupShare> share,
|
|||
connect(_permissionDelete, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged);
|
||||
}
|
||||
|
||||
// Adds action to display password widget (check box)
|
||||
if (_share->getShareType() == Share::TypeEmail && (_share->isPasswordSet() || _account->capabilities().shareEmailPasswordEnabled())) {
|
||||
_passwordProtectLinkAction = new QAction(tr("Password protect"), this);
|
||||
_passwordProtectLinkAction->setCheckable(true);
|
||||
_passwordProtectLinkAction->setChecked(_share->isPasswordSet());
|
||||
// checkbox can be checked/unchedkec if the password is not yet set or if it's not enforced
|
||||
_passwordProtectLinkAction->setEnabled(!_share->isPasswordSet() || !_account->capabilities().shareEmailPasswordEnforced());
|
||||
|
||||
menu->addAction(_passwordProtectLinkAction);
|
||||
connect(_passwordProtectLinkAction, &QAction::triggered, this, &ShareUserLine::slotPasswordCheckboxChanged);
|
||||
|
||||
refreshPasswordLineEditPlaceholder();
|
||||
|
||||
connect(_share.data(), &Share::passwordSet, this, &ShareUserLine::slotPasswordSet);
|
||||
connect(_share.data(), &Share::passwordSetError, this, &ShareUserLine::slotPasswordSetError);
|
||||
}
|
||||
|
||||
refreshPasswordOptions();
|
||||
|
||||
_ui->errorLabel->hide();
|
||||
|
||||
_ui->permissionToolButton->setMenu(menu);
|
||||
_ui->permissionToolButton->setPopupMode(QToolButton::InstantPopup);
|
||||
|
||||
_ui->passwordProgressIndicator->setVisible(false);
|
||||
|
||||
// Set the permissions checkboxes
|
||||
displayPermissions();
|
||||
|
||||
|
@ -675,6 +765,29 @@ void ShareUserLine::slotPermissionsChanged()
|
|||
_share->setPermissions(permissions);
|
||||
}
|
||||
|
||||
void ShareUserLine::slotPasswordCheckboxChanged()
|
||||
{
|
||||
if (!_passwordProtectLinkAction->isChecked()) {
|
||||
_ui->errorLabel->hide();
|
||||
_ui->errorLabel->clear();
|
||||
|
||||
if (!_share->isPasswordSet()) {
|
||||
_ui->lineEdit_password->clear();
|
||||
refreshPasswordOptions();
|
||||
} else {
|
||||
// do not call refreshPasswordOptions here, as it will be called after the network request is complete
|
||||
togglePasswordSetProgressAnimation(true);
|
||||
_share->setPassword(QString());
|
||||
}
|
||||
} else {
|
||||
refreshPasswordOptions();
|
||||
|
||||
if (_ui->lineEdit_password->isVisible() && _ui->lineEdit_password->isEnabled()) {
|
||||
focusPasswordLineEdit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ShareUserLine::slotDeleteAnimationFinished()
|
||||
{
|
||||
emit resizeRequested();
|
||||
|
@ -687,6 +800,63 @@ void ShareUserLine::slotDeleteAnimationFinished()
|
|||
connect(this, SIGNAL(destroyed(QObject *)), parentWidget(), SLOT(repaint()));
|
||||
}
|
||||
|
||||
void ShareUserLine::refreshPasswordOptions()
|
||||
{
|
||||
const bool isPasswordEnabled = _share->getShareType() == Share::TypeEmail && _passwordProtectLinkAction->isChecked();
|
||||
|
||||
_ui->passwordLabel->setVisible(isPasswordEnabled);
|
||||
_ui->lineEdit_password->setEnabled(isPasswordEnabled);
|
||||
_ui->lineEdit_password->setVisible(isPasswordEnabled);
|
||||
_ui->confirmPassword->setVisible(isPasswordEnabled);
|
||||
|
||||
emit resizeRequested();
|
||||
}
|
||||
|
||||
void ShareUserLine::refreshPasswordLineEditPlaceholder()
|
||||
{
|
||||
if (_share->isPasswordSet()) {
|
||||
_ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder));
|
||||
} else {
|
||||
_ui->lineEdit_password->setPlaceholderText("");
|
||||
}
|
||||
}
|
||||
|
||||
void ShareUserLine::slotPasswordSet()
|
||||
{
|
||||
togglePasswordSetProgressAnimation(false);
|
||||
_ui->lineEdit_password->setEnabled(true);
|
||||
_ui->confirmPassword->setEnabled(true);
|
||||
|
||||
_ui->lineEdit_password->setText("");
|
||||
|
||||
_passwordProtectLinkAction->setEnabled(!_share->isPasswordSet() || !_account->capabilities().shareEmailPasswordEnforced());
|
||||
|
||||
refreshPasswordLineEditPlaceholder();
|
||||
|
||||
refreshPasswordOptions();
|
||||
}
|
||||
|
||||
void ShareUserLine::slotPasswordSetError(int statusCode, const QString &message)
|
||||
{
|
||||
qCWarning(lcSharing) << "Error from server" << statusCode << message;
|
||||
|
||||
togglePasswordSetProgressAnimation(false);
|
||||
|
||||
_ui->lineEdit_password->setEnabled(true);
|
||||
_ui->confirmPassword->setEnabled(true);
|
||||
|
||||
refreshPasswordLineEditPlaceholder();
|
||||
|
||||
refreshPasswordOptions();
|
||||
|
||||
focusPasswordLineEdit();
|
||||
|
||||
_ui->errorLabel->show();
|
||||
_ui->errorLabel->setText(message);
|
||||
|
||||
emit resizeRequested();
|
||||
}
|
||||
|
||||
void ShareUserLine::slotShareDeleted()
|
||||
{
|
||||
auto *animation = new QPropertyAnimation(this, "maximumHeight", this);
|
||||
|
@ -743,6 +913,11 @@ void ShareUserLine::slotStyleChanged()
|
|||
customizeStyle();
|
||||
}
|
||||
|
||||
void ShareUserLine::focusPasswordLineEdit()
|
||||
{
|
||||
_ui->lineEdit_password->setFocus();
|
||||
}
|
||||
|
||||
void ShareUserLine::customizeStyle()
|
||||
{
|
||||
_ui->permissionToolButton->setIcon(Theme::createColorAwareIcon(":/client/theme/more.svg"));
|
||||
|
@ -753,6 +928,9 @@ void ShareUserLine::customizeStyle()
|
|||
_ui->noteConfirmButton->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
|
||||
_ui->confirmExpirationDate->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
|
||||
_ui->progressIndicator->setColor(QGuiApplication::palette().color(QPalette::WindowText));
|
||||
|
||||
// make sure to force BackgroundRole to QPalette::WindowText for a lable, because it's parent always has a different role set that applies to children unless customized
|
||||
_ui->errorLabel->setBackgroundRole(QPalette::WindowText);
|
||||
}
|
||||
|
||||
void ShareUserLine::showNoteOptions(bool show)
|
||||
|
@ -834,8 +1012,48 @@ void ShareUserLine::enableProgessIndicatorAnimation(bool enable)
|
|||
}
|
||||
}
|
||||
|
||||
void ShareUserLine::togglePasswordSetProgressAnimation(bool show)
|
||||
{
|
||||
// button and progress indicator are interchanged depending on if the network request is in progress or not
|
||||
_ui->confirmPassword->setVisible(!show && _passwordProtectLinkAction->isChecked());
|
||||
_ui->passwordProgressIndicator->setVisible(show);
|
||||
if (show) {
|
||||
if (!_ui->passwordProgressIndicator->isAnimated()) {
|
||||
_ui->passwordProgressIndicator->startAnimation();
|
||||
}
|
||||
} else {
|
||||
_ui->passwordProgressIndicator->stopAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
void ShareUserLine::disableProgessIndicatorAnimation()
|
||||
{
|
||||
enableProgessIndicatorAnimation(false);
|
||||
}
|
||||
|
||||
void ShareUserLine::setPasswordConfirmed()
|
||||
{
|
||||
if (_ui->lineEdit_password->text().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ui->lineEdit_password->setEnabled(false);
|
||||
_ui->confirmPassword->setEnabled(false);
|
||||
|
||||
_ui->errorLabel->hide();
|
||||
_ui->errorLabel->clear();
|
||||
|
||||
togglePasswordSetProgressAnimation(true);
|
||||
_share->setPassword(_ui->lineEdit_password->text());
|
||||
}
|
||||
|
||||
void ShareUserLine::slotLineEditPasswordReturnPressed()
|
||||
{
|
||||
setPasswordConfirmed();
|
||||
}
|
||||
|
||||
void ShareUserLine::slotConfirmPasswordClicked()
|
||||
{
|
||||
setPasswordConfirmed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ signals:
|
|||
|
||||
public slots:
|
||||
void getShares();
|
||||
void slotShareCreated(const QSharedPointer<Share> &share);
|
||||
void slotStyleChanged();
|
||||
|
||||
private slots:
|
||||
|
@ -110,6 +111,8 @@ private:
|
|||
ShareManager *_manager;
|
||||
|
||||
QProgressIndicator _pi_sharee;
|
||||
|
||||
QString _lastCreatedShareId;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -120,7 +123,8 @@ class ShareUserLine : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ShareUserLine(QSharedPointer<UserGroupShare> share,
|
||||
explicit ShareUserLine(AccountPtr account,
|
||||
QSharedPointer<UserGroupShare> Share,
|
||||
SharePermissions maxSharingPermissions,
|
||||
bool isFile,
|
||||
QWidget *parent = nullptr);
|
||||
|
@ -135,17 +139,33 @@ signals:
|
|||
public slots:
|
||||
void slotStyleChanged();
|
||||
|
||||
void focusPasswordLineEdit();
|
||||
|
||||
private slots:
|
||||
void on_deleteShareButton_clicked();
|
||||
void slotPermissionsChanged();
|
||||
void slotEditPermissionsChanged();
|
||||
void slotPasswordCheckboxChanged();
|
||||
void slotDeleteAnimationFinished();
|
||||
|
||||
void refreshPasswordOptions();
|
||||
|
||||
void refreshPasswordLineEditPlaceholder();
|
||||
|
||||
void slotPasswordSet();
|
||||
void slotPasswordSetError(int statusCode, const QString &message);
|
||||
|
||||
void slotShareDeleted();
|
||||
void slotPermissionsSet();
|
||||
|
||||
void slotAvatarLoaded(QImage avatar);
|
||||
|
||||
void setPasswordConfirmed();
|
||||
|
||||
void slotLineEditPasswordReturnPressed();
|
||||
|
||||
void slotConfirmPasswordClicked();
|
||||
|
||||
private:
|
||||
void displayPermissions();
|
||||
void loadAvatar();
|
||||
|
@ -160,10 +180,13 @@ private:
|
|||
void showExpireDateOptions(bool show);
|
||||
void setExpireDate();
|
||||
|
||||
void togglePasswordSetProgressAnimation(bool show);
|
||||
|
||||
void enableProgessIndicatorAnimation(bool enable);
|
||||
void disableProgessIndicatorAnimation();
|
||||
|
||||
Ui::ShareUserLine *_ui;
|
||||
AccountPtr _account;
|
||||
QSharedPointer<UserGroupShare> _share;
|
||||
bool _isFile;
|
||||
|
||||
|
@ -175,6 +198,7 @@ private:
|
|||
QAction *_permissionDelete;
|
||||
QAction *_noteLinkAction;
|
||||
QAction *_expirationDateLinkAction;
|
||||
QAction *_passwordProtectLinkAction;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>980</width>
|
||||
<height>210</height>
|
||||
<height>239</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -116,11 +116,23 @@
|
|||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="noteLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>78</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Note:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -134,7 +146,7 @@
|
|||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>60</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
|
@ -158,8 +170,83 @@
|
|||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="passwordElementsLayout">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="passwordLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>78</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Password:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_password">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="confirmPassword">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>28</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../theme.qrc">
|
||||
<normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressIndicator" name="passwordProgressIndicator" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>28</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="expirationLabel">
|
||||
<property name="minimumSize">
|
||||
|
@ -172,7 +259,7 @@
|
|||
<string>Expires:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -202,20 +289,92 @@
|
|||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="errorLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="palette">
|
||||
<palette>
|
||||
<active>
|
||||
<colorrole role="WindowText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>255</red>
|
||||
<green>0</green>
|
||||
<blue>0</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
</active>
|
||||
<inactive>
|
||||
<colorrole role="WindowText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>255</red>
|
||||
<green>0</green>
|
||||
<blue>0</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
</inactive>
|
||||
<disabled>
|
||||
<colorrole role="WindowText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>123</red>
|
||||
<green>121</green>
|
||||
<blue>134</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
</disabled>
|
||||
</palette>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">Placeholder for Error text</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>OCC::ElidedLabel</class>
|
||||
<extends>QLabel</extends>
|
||||
<header>elidedlabel.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QProgressIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>QProgressIndicator.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>OCC::ElidedLabel</class>
|
||||
<extends>QLabel</extends>
|
||||
<header>elidedlabel.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../theme.qrc"/>
|
||||
|
|
|
@ -40,6 +40,16 @@ bool Capabilities::shareAPI() const
|
|||
}
|
||||
}
|
||||
|
||||
bool Capabilities::shareEmailPasswordEnabled() const
|
||||
{
|
||||
return _capabilities["files_sharing"].toMap()["sharebymail"].toMap()["password"].toMap()["enabled"].toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::shareEmailPasswordEnforced() const
|
||||
{
|
||||
return _capabilities["files_sharing"].toMap()["sharebymail"].toMap()["password"].toMap()["enforced"].toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLink() const
|
||||
{
|
||||
if (_capabilities["files_sharing"].toMap().contains("public")) {
|
||||
|
|
|
@ -46,6 +46,8 @@ public:
|
|||
Capabilities(const QVariantMap &capabilities);
|
||||
|
||||
bool shareAPI() const;
|
||||
bool shareEmailPasswordEnabled() const;
|
||||
bool shareEmailPasswordEnforced() const;
|
||||
bool sharePublicLink() const;
|
||||
bool sharePublicLinkAllowUpload() const;
|
||||
bool sharePublicLinkSupportsUploadOnly() const;
|
||||
|
|
Загрузка…
Ссылка в новой задаче