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