1 /*
2 Drawpile - a collaborative drawing program.
3
4 Copyright (C) 2017-2019 Calle Laakkonen
5
6 Drawpile is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Drawpile is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Drawpile. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "sessionsettings.h"
21 #include "utils/listservermodel.h"
22 #include "net/banlistmodel.h"
23 #include "net/announcementlist.h"
24 #include "document.h"
25 #include "../libshared/net/meta2.h"
26 #include "canvas/canvasmodel.h"
27 #include "canvas/aclfilter.h"
28 #include "parentalcontrols/parentalcontrols.h"
29
30 #include "ui_sessionsettings.h"
31
32 #include <QDebug>
33 #include <QStringListModel>
34 #include <QMenu>
35 #include <QTimer>
36 #include <QInputDialog>
37 #include <QFile>
38 #include <QJsonDocument>
39
40 namespace dialogs {
41
SessionSettingsDialog(Document * doc,QWidget * parent)42 SessionSettingsDialog::SessionSettingsDialog(Document *doc, QWidget *parent)
43 : QDialog(parent), m_ui(new Ui_SessionSettingsDialog), m_doc(doc)
44 {
45 Q_ASSERT(doc);
46 m_ui->setupUi(this);
47
48 initPermissionComboBoxes();
49
50 connect(m_doc, &Document::canvasChanged, this, &SessionSettingsDialog::onCanvasChanged);
51
52 // Set up the settings page
53 m_saveTimer = new QTimer(this);
54 m_saveTimer->setSingleShot(true);
55 m_saveTimer->setInterval(1000);
56 connect(m_saveTimer, &QTimer::timeout, this, &SessionSettingsDialog::sendSessionConf);
57
58 connect(m_ui->title, &QLineEdit::textEdited, this, &SessionSettingsDialog::titleChanged);
59 connect(m_ui->maxUsers, &QSpinBox::editingFinished, this, &SessionSettingsDialog::maxUsersChanged);
60 connect(m_ui->denyJoins, &QCheckBox::clicked, this, &SessionSettingsDialog::denyJoinsChanged);
61 connect(m_ui->authOnly, &QCheckBox::clicked, this, &SessionSettingsDialog::authOnlyChanged);
62 connect(m_ui->autoresetThreshold, &QDoubleSpinBox::editingFinished, this, &SessionSettingsDialog::autoresetThresholdChanged);
63 connect(m_ui->preserveChat, &QCheckBox::clicked, this, &SessionSettingsDialog::keepChatChanged);
64 connect(m_ui->persistent, &QCheckBox::clicked, this, &SessionSettingsDialog::persistenceChanged);
65 connect(m_ui->nsfm, &QCheckBox::clicked, this, &SessionSettingsDialog::nsfmChanged);
66 connect(m_ui->deputies, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SessionSettingsDialog::deputiesChanged);
67
68 connect(m_ui->sessionPassword, &QLabel::linkActivated, this, &SessionSettingsDialog::changePassword);
69 connect(m_ui->opword, &QLabel::linkActivated, this, &SessionSettingsDialog::changeOpword);
70
71 connect(m_doc, &Document::sessionTitleChanged, m_ui->title, &QLineEdit::setText);
72 connect(m_doc, &Document::sessionPreserveChatChanged, m_ui->preserveChat, &QCheckBox::setChecked);
73 connect(m_doc, &Document::sessionPersistentChanged, m_ui->persistent, &QCheckBox::setChecked);
74 connect(m_doc, &Document::sessionClosedChanged, m_ui->denyJoins, &QCheckBox::setChecked);
75 connect(m_doc, &Document::sessionAuthOnlyChanged, this, [this](bool authOnly) {
76 m_ui->authOnly->setEnabled(m_op && (authOnly || m_isAuth));
77 m_ui->authOnly->setChecked(authOnly);
78 });
79 connect(m_doc, &Document::sessionPasswordChanged, this, [this](bool hasPassword) {
80 m_ui->sessionPassword->setProperty("haspass", hasPassword);
81 updatePasswordLabel(m_ui->sessionPassword);
82 });
83 connect(m_doc, &Document::sessionOpwordChanged, this, [this](bool hasPassword) {
84 m_ui->opword->setProperty("haspass", hasPassword);
85 updatePasswordLabel(m_ui->opword);
86 });
87 connect(m_doc, &Document::sessionNsfmChanged, m_ui->nsfm, &QCheckBox::setChecked);
88 connect(m_doc, &Document::sessionDeputiesChanged, this, [this](bool deputies) { m_ui->deputies->setCurrentIndex(deputies ? 1 : 0); });
89 connect(m_doc, &Document::sessionMaxUserCountChanged, m_ui->maxUsers, &QSpinBox::setValue);
90 connect(m_doc, &Document::sessionResetThresholdChanged, m_ui->autoresetThreshold, &QDoubleSpinBox::setValue);
91 connect(m_doc, &Document::baseResetThresholdChanged, this, [this](int threshold) {
92 m_ui->baseResetThreshold->setText(QStringLiteral("+ %1 MB").arg(threshold/(1024.0*1024.0), 0, 'f', 1));
93 });
94
95
96 // Set up permissions tab
97 connect(m_ui->permissionPresets, &widgets::PresetSelector::saveRequested, this, &SessionSettingsDialog::permissionPresetSaving);
98 connect(m_ui->permissionPresets, &widgets::PresetSelector::loadRequested, this, &SessionSettingsDialog::permissionPresetSelected);
99
100 // Set up banlist tab
101 m_ui->banlistView->setModel(doc->banlist());
102 connect(m_ui->removeBan, &QPushButton::clicked, [this]() {
103 const int id = m_ui->banlistView->selectionModel()->currentIndex().data(Qt::UserRole).toInt();
104 if(id>0) {
105 qDebug() << "requesting removal of in-session ban entry" << id;
106 m_doc->sendUnban(id);
107 }
108 });
109
110 // Set up announcements tab
111 m_ui->announcementTableView->setModel(doc->announcementList());
112 QHeaderView *announcementHeader = m_ui->announcementTableView->horizontalHeader();
113 announcementHeader->setSectionResizeMode(0, QHeaderView::Stretch);
114
115 QMenu *addAnnouncementMenu = new QMenu(this);
116 QMenu *addPrivateAnnouncementMenu = new QMenu(this);
117
118 m_ui->addAnnouncement->setMenu(addAnnouncementMenu);
119 m_ui->addPrivateAnnouncement->setMenu(addPrivateAnnouncementMenu);
120
121 connect(addAnnouncementMenu, &QMenu::triggered, [this](QAction *a) {
122 const QString apiUrl = a->property("API_URL").toString();
123 qDebug() << "Requesting pbulic announcement:" << apiUrl;
124 m_doc->sendAnnounce(apiUrl, false);
125 });
126 connect(addPrivateAnnouncementMenu, &QMenu::triggered, [this](QAction *a) {
127 const QString apiUrl = a->property("API_URL").toString();
128 qDebug() << "Requesting private announcement:" << apiUrl;
129 m_doc->sendAnnounce(apiUrl, true);
130 });
131
132 connect(m_ui->removeAnnouncement, &QPushButton::clicked, [this]() {
133 auto sel = m_ui->announcementTableView->selectionModel()->selection();
134 QString apiUrl;
135 if(!sel.isEmpty())
136 apiUrl = sel.first().indexes().first().data(Qt::UserRole).toString();
137 if(!apiUrl.isEmpty()) {
138 qDebug() << "Requesting unlisting:" << apiUrl;
139 m_doc->sendUnannounce(apiUrl);
140 }
141 });
142 }
143
~SessionSettingsDialog()144 SessionSettingsDialog::~SessionSettingsDialog()
145 {
146 delete m_ui;
147 }
148
showEvent(QShowEvent * event)149 void SessionSettingsDialog::showEvent(QShowEvent *event)
150 {
151 QDialog::showEvent(event);
152 reloadSettings();
153 }
154
reloadSettings()155 void SessionSettingsDialog::reloadSettings()
156 {
157 qInfo("Realoding settings");
158 const auto listservers = sessionlisting::ListServerModel::listServers(false);
159 auto *addAnnouncementMenu = m_ui->addAnnouncement->menu();
160 auto *addPrivateAnnouncementMenu = m_ui->addPrivateAnnouncement->menu();
161
162 addAnnouncementMenu->clear();
163 addPrivateAnnouncementMenu->clear();
164
165 for(const auto &listserver : listservers) {
166 if(listserver.publicListings) {
167 QAction *a = addAnnouncementMenu->addAction(listserver.icon, listserver.name);
168 a->setProperty("API_URL", listserver.url);
169 }
170
171 if(listserver.privateListings) {
172 QAction *a2 = addPrivateAnnouncementMenu->addAction(listserver.icon, listserver.name);
173 a2->setProperty("API_URL", listserver.url);
174 }
175 }
176
177 m_ui->addAnnouncement->setEnabled(!addAnnouncementMenu->isEmpty());
178 m_ui->addPrivateAnnouncement->setEnabled(!addPrivateAnnouncementMenu->isEmpty());
179 }
180
setPersistenceEnabled(bool enable)181 void SessionSettingsDialog::setPersistenceEnabled(bool enable)
182 {
183 m_ui->persistent->setEnabled(m_op && enable);
184 m_canPersist = enable;
185 }
186
setAutoResetEnabled(bool enable)187 void SessionSettingsDialog::setAutoResetEnabled(bool enable)
188 {
189 m_ui->autoresetThreshold->setEnabled(m_op && enable);
190 m_canAutoreset = enable;
191 }
192
setAuthenticated(bool auth)193 void SessionSettingsDialog::setAuthenticated(bool auth)
194 {
195 m_isAuth = auth;
196 }
197
onCanvasChanged(canvas::CanvasModel * canvas)198 void SessionSettingsDialog::onCanvasChanged(canvas::CanvasModel *canvas)
199 {
200 if(!canvas)
201 return;
202
203 canvas::AclFilter *acl = canvas->aclFilter();
204
205 connect(acl, &canvas::AclFilter::localOpChanged, this, &SessionSettingsDialog::onOperatorModeChanged);
206 connect(acl, &canvas::AclFilter::featureTierChanged, this, &SessionSettingsDialog::onFeatureTierChanged);
207
208 for(int i=0;i<canvas::FeatureCount;++i)
209 onFeatureTierChanged(canvas::Feature(i), acl->featureTier(canvas::Feature(i)));
210 }
211
onOperatorModeChanged(bool op)212 void SessionSettingsDialog::onOperatorModeChanged(bool op)
213 {
214 QWidget *w[] = {
215 m_ui->title,
216 m_ui->maxUsers,
217 m_ui->denyJoins,
218 m_ui->preserveChat,
219 m_ui->nsfm,
220 m_ui->deputies,
221 m_ui->sessionPassword,
222 m_ui->opword,
223 m_ui->addAnnouncement,
224 m_ui->removeAnnouncement,
225 m_ui->removeBan
226 };
227 m_op = op;
228 for(unsigned int i=0;i<sizeof(w)/sizeof(*w);++i)
229 w[i]->setEnabled(op);
230
231 for(int i=0;i<canvas::FeatureCount;++i)
232 featureBox(canvas::Feature(i))->setEnabled(op);
233
234 m_ui->persistent->setEnabled(m_canPersist && op);
235 m_ui->autoresetThreshold->setEnabled(m_canAutoreset && op);
236 m_ui->authOnly->setEnabled(op && (m_isAuth || m_ui->authOnly->isChecked()));
237 m_ui->permissionPresets->setWriteOnly(!op);
238 updatePasswordLabel(m_ui->sessionPassword);
239 updatePasswordLabel(m_ui->opword);
240 }
241
featureBox(canvas::Feature f)242 QComboBox *SessionSettingsDialog::featureBox(canvas::Feature f)
243 {
244 switch(f) {
245 using canvas::Feature;
246 case Feature::PutImage: return m_ui->permPutImage;
247 case Feature::RegionMove: return m_ui->permRegionMove;
248 case Feature::Resize: return m_ui->permResize;
249 case Feature::Background: return m_ui->permBackground;
250 case Feature::EditLayers: return m_ui->permEditLayers;
251 case Feature::OwnLayers: return m_ui->permOwnLayers;
252 case Feature::CreateAnnotation: return m_ui->permCreateAnnotation;
253 case Feature::Laser: return m_ui->permLaser;
254 case Feature::Undo: return m_ui->permUndo;
255 }
256 Q_ASSERT_X(false, "featureBox", "unhandled case");
257 return nullptr;
258 }
onFeatureTierChanged(canvas::Feature feature,canvas::Tier tier)259 void SessionSettingsDialog::onFeatureTierChanged(canvas::Feature feature, canvas::Tier tier)
260 {
261 featureBox(feature)->setCurrentIndex(int(tier));
262 }
263
initPermissionComboBoxes()264 void SessionSettingsDialog::initPermissionComboBoxes()
265 {
266 // Note: these must match the canvas::Tier enum
267 const QString items[] = {
268 tr("Operators"),
269 tr("Trusted"),
270 tr("Registered"),
271 tr("Everyone")
272 };
273
274 for(uint i=0;i<canvas::FeatureCount;++i) {
275 QComboBox *box = featureBox(canvas::Feature(i));
276 for(uint j=0;j<sizeof(items)/sizeof(QString);++j)
277 box->addItem(items[j]);
278
279 box->setProperty("featureIdx", i);
280 connect(box, QOverload<int>::of(&QComboBox::activated), this, &SessionSettingsDialog::permissionChanged);
281 }
282 }
283
permissionChanged()284 void SessionSettingsDialog::permissionChanged()
285 {
286 m_featureTiersChanged = true;
287 m_saveTimer->start();
288 }
289
permissionPresetSelected(const QString & presetFile)290 void SessionSettingsDialog::permissionPresetSelected(const QString &presetFile)
291 {
292 QFile f(presetFile);
293 if(!f.open(QFile::ReadOnly)) {
294 qWarning("%s: could not open file", qPrintable(presetFile));
295 return;
296 }
297
298 QJsonObject cfg = QJsonDocument::fromJson(f.readAll()).object();
299
300 // Normal features
301 for(int i=0;i<canvas::FeatureCount;++i) {
302 auto *box = featureBox(canvas::Feature(i));
303 box->setCurrentIndex(
304 cfg.value(box->objectName()).toInt(box->currentIndex())
305 );
306 }
307 permissionChanged();
308
309 // Deputies
310 {
311 auto *box = m_ui->deputies;
312 box->setCurrentIndex(
313 cfg.value(box->objectName()).toInt(box->currentIndex())
314 );
315 deputiesChanged(box->currentIndex());
316 }
317
318 }
319
permissionPresetSaving(const QString & presetFile)320 void SessionSettingsDialog::permissionPresetSaving(const QString &presetFile)
321 {
322 QJsonObject cfg;
323
324 // Normal features
325 for(int i=0;i<canvas::FeatureCount;++i) {
326 auto *box = featureBox(canvas::Feature(i));
327 cfg[box->objectName()] = box->currentIndex();
328 }
329
330 // Deputies
331 {
332 auto *box = m_ui->deputies;
333 cfg[box->objectName()] = box->currentIndex();
334 }
335
336 // Save
337 QFile f(presetFile);
338 if(!f.open(QFile::WriteOnly)) {
339 qWarning("%s: could not open file", qPrintable(presetFile));
340 return;
341 }
342 f.write(QJsonDocument(cfg).toJson());
343 }
344
updatePasswordLabel(QLabel * label)345 void SessionSettingsDialog::updatePasswordLabel(QLabel *label)
346 {
347 QString txt;
348 if(m_op)
349 txt = QStringLiteral("<b>%1</b> (<a href=\"#\">%2</a>)");
350 else
351 txt = QStringLiteral("<b>%1</b>");
352
353 if(label->property("haspass").toBool())
354 txt = txt.arg(tr("yes", "password"), tr("change", "password"));
355 else
356 txt = txt.arg(tr("no", "password"), tr("assign", "password"));
357
358 label->setText(txt);
359 }
360
sendSessionConf()361 void SessionSettingsDialog::sendSessionConf()
362 {
363 if(!m_sessionconf.isEmpty()) {
364 if(m_sessionconf.contains("title") && parentalcontrols::isNsfmTitle(m_sessionconf["title"].toString()))
365 m_sessionconf["nsfm"] = true;
366
367 m_doc->sendSessionConf(m_sessionconf);
368 m_sessionconf = QJsonObject();
369 }
370
371 if(m_featureTiersChanged) {
372 uint8_t tiers[canvas::FeatureCount];
373 for(int i=0;i<canvas::FeatureCount;++i)
374 tiers[i] = featureBox(canvas::Feature(i))->currentIndex();
375
376 m_doc->sendFeatureAccessLevelChange(tiers);
377 m_featureTiersChanged = false;
378 }
379 }
380
changeSesionConf(const QString & key,const QJsonValue & value,bool now)381 void SessionSettingsDialog::changeSesionConf(const QString &key, const QJsonValue &value, bool now)
382 {
383 m_sessionconf[key] = value;
384 if(now) {
385 m_saveTimer->stop();
386 sendSessionConf();
387 } else {
388 m_saveTimer->start();
389 }
390 }
391
titleChanged(const QString & title)392 void SessionSettingsDialog::titleChanged(const QString &title) { changeSesionConf("title", title); }
maxUsersChanged()393 void SessionSettingsDialog::maxUsersChanged() { changeSesionConf("maxUserCount", m_ui->maxUsers->value()); }
denyJoinsChanged(bool set)394 void SessionSettingsDialog::denyJoinsChanged(bool set) { changeSesionConf("closed", set); }
authOnlyChanged(bool set)395 void SessionSettingsDialog::authOnlyChanged(bool set)
396 {
397 changeSesionConf("authOnly", set);
398 if(!set && !m_isAuth)
399 m_ui->authOnly->setEnabled(false);
400 }
401
autoresetThresholdChanged()402 void SessionSettingsDialog::autoresetThresholdChanged() { changeSesionConf("resetThreshold", int(m_ui->autoresetThreshold->value()* 1024 * 1024)); }
keepChatChanged(bool set)403 void SessionSettingsDialog::keepChatChanged(bool set) { changeSesionConf("preserveChat", set); }
persistenceChanged(bool set)404 void SessionSettingsDialog::persistenceChanged(bool set) { changeSesionConf("persistent", set); }
nsfmChanged(bool set)405 void SessionSettingsDialog::nsfmChanged(bool set) { changeSesionConf("nsfm", set); }
deputiesChanged(int idx)406 void SessionSettingsDialog::deputiesChanged(int idx) { changeSesionConf("deputies", idx>0); }
407
changePassword()408 void SessionSettingsDialog::changePassword()
409 {
410 QString prompt;
411 if(m_doc->isSessionPasswordProtected())
412 prompt = tr("Set a new password or leave blank to remove.");
413 else
414 prompt = tr("Set a password for the session.");
415
416 bool ok;
417 QString newpass = QInputDialog::getText(
418 this,
419 tr("Session Password"),
420 prompt,
421 QLineEdit::Password,
422 QString(),
423 &ok
424 );
425 if(ok)
426 changeSesionConf("password", newpass, true);
427 }
428
changeOpword()429 void SessionSettingsDialog::changeOpword()
430 {
431 QString prompt;
432 if(m_doc->isSessionOpword())
433 prompt = tr("Set a new password or leave blank to remove.");
434 else
435 prompt = tr("Set a password for gaining operator status.");
436
437 bool ok;
438 QString newpass = QInputDialog::getText(
439 this,
440 tr("Operator Password"),
441 prompt,
442 QLineEdit::Password,
443 QString(),
444 &ok
445 );
446 if(ok)
447 changeSesionConf("opword", newpass, true);
448 }
449
450 }
451