1 // Copyright 2005-2019 The Mumble Developers. All rights reserved.
2 // Use of this source code is governed by a BSD-style license
3 // that can be found in the LICENSE file at the root of the
4 // Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6 #include "mumble_pch.hpp"
7
8 #include "ConnectDialog.h"
9
10 #ifdef USE_BONJOUR
11 #include "BonjourClient.h"
12 #include "BonjourServiceBrowser.h"
13 #include "BonjourServiceResolver.h"
14 #endif
15
16 #include "Channel.h"
17 #include "Database.h"
18 #include "ServerHandler.h"
19 #include "WebFetch.h"
20 #include "ServerResolver.h"
21
22 // We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include.
23 #include "Global.h"
24
25 QMap<QString, QIcon> ServerItem::qmIcons;
26 QList<PublicInfo> ConnectDialog::qlPublicServers;
27 QString ConnectDialog::qsUserCountry, ConnectDialog::qsUserCountryCode, ConnectDialog::qsUserContinentCode;
28 Timer ConnectDialog::tPublicServers;
29
30
PingStats()31 PingStats::PingStats() {
32 init();
33 }
34
~PingStats()35 PingStats::~PingStats() {
36 delete asQuantile;
37 }
38
init()39 void PingStats::init() {
40 boost::array<double, 3> probs = {{0.75, 0.80, 0.95 }};
41
42 asQuantile = new asQuantileType(boost::accumulators::tag::extended_p_square::probabilities = probs);
43 dPing = 0.0;
44 uiPing = 0;
45 uiPingSort = 0;
46 uiUsers = 0;
47 uiMaxUsers = 0;
48 uiBandwidth = 0;
49 uiSent = 0;
50 uiRecv = 0;
51 uiVersion = 0;
52 }
53
reset()54 void PingStats::reset() {
55 delete asQuantile;
56 init();
57 }
58
ServerViewDelegate(QObject * p)59 ServerViewDelegate::ServerViewDelegate(QObject *p) : QStyledItemDelegate(p) {
60 }
61
~ServerViewDelegate()62 ServerViewDelegate::~ServerViewDelegate() {
63 }
64
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const65 void ServerViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
66 // Allow a ServerItem's BackgroundRole to override the current theme's default color.
67 QVariant bg = index.data(Qt::BackgroundRole);
68 if (bg.isValid()) {
69 painter->fillRect(option.rect, bg.value<QBrush>());
70 }
71
72 QStyledItemDelegate::paint(painter, option, index);
73 }
74
ServerView(QWidget * p)75 ServerView::ServerView(QWidget *p) : QTreeWidget(p) {
76 siFavorite = new ServerItem(tr("Favorite"), ServerItem::FavoriteType);
77 addTopLevelItem(siFavorite);
78 siFavorite->setExpanded(true);
79 siFavorite->setHidden(true);
80
81 #ifdef USE_BONJOUR
82 siLAN = new ServerItem(tr("LAN"), ServerItem::LANType);
83 addTopLevelItem(siLAN);
84 siLAN->setExpanded(true);
85 siLAN->setHidden(true);
86 #else
87 siLAN = NULL;
88 #endif
89
90 if (!g.s.disablePublicList) {
91 siPublic = new ServerItem(tr("Public Internet"), ServerItem::PublicType);
92 siPublic->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
93 addTopLevelItem(siPublic);
94
95 siPublic->setExpanded(false);
96
97 // The continent code is empty when the server's IP address is not in the GeoIP database
98 qmContinentNames.insert(QLatin1String(""), tr("Unknown"));
99
100 qmContinentNames.insert(QLatin1String("af"), tr("Africa"));
101 qmContinentNames.insert(QLatin1String("as"), tr("Asia"));
102 qmContinentNames.insert(QLatin1String("na"), tr("North America"));
103 qmContinentNames.insert(QLatin1String("sa"), tr("South America"));
104 qmContinentNames.insert(QLatin1String("eu"), tr("Europe"));
105 qmContinentNames.insert(QLatin1String("oc"), tr("Oceania"));
106 } else {
107 qWarning()<< "Public list disabled";
108
109 siPublic = NULL;
110 }
111 }
112
~ServerView()113 ServerView::~ServerView() {
114 delete siFavorite;
115 delete siLAN;
116 delete siPublic;
117 }
118
mimeData(const QList<QTreeWidgetItem * > mimeitems) const119 QMimeData *ServerView::mimeData(const QList<QTreeWidgetItem *> mimeitems) const {
120 if (mimeitems.isEmpty())
121 return NULL;
122
123 ServerItem *si = static_cast<ServerItem *>(mimeitems.first());
124 return si->toMimeData();
125 }
126
mimeTypes() const127 QStringList ServerView::mimeTypes() const {
128 QStringList qsl;
129 qsl << QStringList(QLatin1String("text/uri-list"));
130 qsl << QStringList(QLatin1String("text/plain"));
131 return qsl;
132 }
133
supportedDropActions() const134 Qt::DropActions ServerView::supportedDropActions() const {
135 return Qt::CopyAction | Qt::LinkAction;
136 }
137
138 /* Extract and append (2), (3) etc to the end of a servers name if it is cloned. */
fixupName(ServerItem * si)139 void ServerView::fixupName(ServerItem *si) {
140 QString name = si->qsName;
141
142 int tag = 1;
143
144 QRegExp tmatch(QLatin1String("(.+)\\((\\d+)\\)"));
145 tmatch.setMinimal(true);
146 if (tmatch.exactMatch(name)) {
147 name = tmatch.capturedTexts().at(1).trimmed();
148 tag = tmatch.capturedTexts().at(2).toInt();
149 }
150
151 bool found;
152 QString cmpname;
153 do {
154 found = false;
155 if (tag > 1)
156 cmpname = name + QString::fromLatin1(" (%1)").arg(tag);
157 else
158 cmpname = name;
159
160 foreach(ServerItem *f, siFavorite->qlChildren)
161 if (f->qsName == cmpname)
162 found = true;
163
164 ++tag;
165 } while (found);
166
167 si->qsName = cmpname;
168 }
169
dropMimeData(QTreeWidgetItem *,int,const QMimeData * mime,Qt::DropAction)170 bool ServerView::dropMimeData(QTreeWidgetItem *, int, const QMimeData *mime, Qt::DropAction) {
171 ServerItem *si = ServerItem::fromMimeData(mime);
172 if (! si)
173 return false;
174
175 fixupName(si);
176
177 qobject_cast<ConnectDialog *>(parent())->qlItems << si;
178 siFavorite->addServerItem(si);
179
180 qobject_cast<ConnectDialog *>(parent())->startDns(si);
181
182 setCurrentItem(si);
183
184 return true;
185 }
186
getParent(const QString & continentcode,const QString & countrycode,const QString & countryname,const QString & usercontinent,const QString & usercountry)187 ServerItem *ServerView::getParent(const QString &continentcode, const QString &countrycode, const QString &countryname, const QString &usercontinent, const QString &usercountry) {
188 ServerItem *continent = qmContinent.value(continentcode);
189 if (!continent) {
190 QString name = qmContinentNames.value(continentcode);
191 if (name.isEmpty())
192 name = continentcode;
193 continent = new ServerItem(name, ServerItem::PublicType, continentcode);
194 qmContinent.insert(continentcode, continent);
195 siPublic->addServerItem(continent);
196
197 if (!continentcode.isEmpty()) {
198 if (continentcode == usercontinent) {
199 continent->setExpanded(true);
200 scrollToItem(continent, QAbstractItemView::PositionAtTop);
201 }
202 } else {
203 continent->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
204 }
205 }
206
207 // If the continent code is empty, we put the server directly into the "Unknown" continent
208 if (continentcode.isEmpty()) {
209 return continent;
210 }
211
212 ServerItem *country = qmCountry.value(countrycode);
213 if (!country) {
214 country = new ServerItem(countryname, ServerItem::PublicType, continentcode, countrycode);
215 qmCountry.insert(countrycode, country);
216 country->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
217
218 continent->addServerItem(country);
219
220 if (!countrycode.isEmpty() && countrycode == usercountry) {
221 country->setExpanded(true);
222 scrollToItem(country, QAbstractItemView::PositionAtTop);
223 }
224 }
225 return country;
226 }
227
228
init()229 void ServerItem::init() {
230 // Without this, columncount is wrong.
231 setData(0, Qt::DisplayRole, QVariant());
232 setData(1, Qt::DisplayRole, QVariant());
233 setData(2, Qt::DisplayRole, QVariant());
234 emitDataChanged();
235 }
236
ServerItem(const FavoriteServer & fs)237 ServerItem::ServerItem(const FavoriteServer &fs) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
238 siParent = NULL;
239 bParent = false;
240
241 itType = FavoriteType;
242 qsName = fs.qsName;
243 usPort = fs.usPort;
244
245 qsUsername = fs.qsUsername;
246 qsPassword = fs.qsPassword;
247
248 qsUrl = fs.qsUrl;
249
250 bCA = false;
251
252 if (fs.qsHostname.startsWith(QLatin1Char('@'))) {
253 qsBonjourHost = fs.qsHostname.mid(1);
254 brRecord = BonjourRecord(qsBonjourHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
255 } else {
256 qsHostname = fs.qsHostname;
257 }
258
259 init();
260 }
261
ServerItem(const PublicInfo & pi)262 ServerItem::ServerItem(const PublicInfo &pi) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
263 siParent = NULL;
264 bParent = false;
265 itType = PublicType;
266 qsName = pi.qsName;
267 qsHostname = pi.qsIp;
268 usPort = pi.usPort;
269 qsUrl = pi.quUrl.toString();
270 qsCountry = pi.qsCountry;
271 qsCountryCode = pi.qsCountryCode;
272 qsContinentCode = pi.qsContinentCode;
273 bCA = pi.bCA;
274
275 init();
276 }
277
ServerItem(const QString & name,const QString & host,unsigned short port,const QString & username,const QString & password)278 ServerItem::ServerItem(const QString &name, const QString &host, unsigned short port, const QString &username, const QString &password) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
279 siParent = NULL;
280 bParent = false;
281 itType = FavoriteType;
282 qsName = name;
283 usPort = port;
284 qsUsername = username;
285 qsPassword = password;
286
287 bCA = false;
288
289 if (host.startsWith(QLatin1Char('@'))) {
290 qsBonjourHost = host.mid(1);
291 brRecord = BonjourRecord(qsBonjourHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
292 } else {
293 qsHostname = host;
294 }
295
296 init();
297 }
298
ServerItem(const BonjourRecord & br)299 ServerItem::ServerItem(const BonjourRecord &br) : QTreeWidgetItem(QTreeWidgetItem::UserType) {
300 siParent = NULL;
301 bParent = false;
302 itType = LANType;
303 qsName = br.serviceName;
304 qsBonjourHost = qsName;
305 brRecord = br;
306 usPort = 0;
307 bCA = false;
308
309 init();
310 }
311
ServerItem(const QString & name,ItemType itype,const QString & continent,const QString & country)312 ServerItem::ServerItem(const QString &name, ItemType itype, const QString &continent, const QString &country) {
313 siParent = NULL;
314 bParent = true;
315 qsName = name;
316 itType = itype;
317 if (itType == PublicType) {
318 qsCountryCode = country;
319 qsContinentCode = continent;
320 }
321 setFlags(flags() & ~Qt::ItemIsDragEnabled);
322 bCA = false;
323
324 init();
325 }
326
ServerItem(const ServerItem * si)327 ServerItem::ServerItem(const ServerItem *si) {
328 siParent = NULL;
329 bParent = false;
330 itType = FavoriteType;
331
332 qsName = si->qsName;
333 qsHostname = si->qsHostname;
334 usPort = si->usPort;
335 qsUsername = si->qsUsername;
336 qsPassword = si->qsPassword;
337 qsCountry = si->qsCountry;
338 qsCountryCode = si->qsCountryCode;
339 qsContinentCode = si->qsContinentCode;
340 qsUrl = si->qsUrl;
341 qsBonjourHost = si->qsBonjourHost;
342 brRecord = si->brRecord;
343 qlAddresses = si->qlAddresses;
344 bCA = si->bCA;
345
346 uiVersion = si->uiVersion;
347 uiPing = si->uiPing;
348 uiPingSort = si->uiPing;
349 uiUsers = si->uiUsers;
350 uiMaxUsers = si->uiMaxUsers;
351 uiBandwidth = si->uiBandwidth;
352 uiSent = si->uiSent;
353 dPing = si->dPing;
354 *asQuantile = * si->asQuantile;
355 }
356
~ServerItem()357 ServerItem::~ServerItem() {
358 if (siParent) {
359 siParent->qlChildren.removeAll(this);
360 if (siParent->bParent && siParent->qlChildren.isEmpty())
361 siParent->setHidden(true);
362 }
363
364 // This is just for cleanup when exiting the dialog, it won't stop pending DNS for the children.
365 foreach(ServerItem *si, qlChildren)
366 delete si;
367 }
368
fromMimeData(const QMimeData * mime,bool default_name,QWidget * p,bool convertHttpUrls)369 ServerItem *ServerItem::fromMimeData(const QMimeData *mime, bool default_name, QWidget *p, bool convertHttpUrls) {
370 if (mime->hasFormat(QLatin1String("OriginatedInMumble")))
371 return NULL;
372
373 QUrl url;
374 if (mime->hasUrls() && ! mime->urls().isEmpty())
375 url = mime->urls().at(0);
376 else if (mime->hasText())
377 url = QUrl::fromEncoded(mime->text().toUtf8());
378
379 QString qsFile = url.toLocalFile();
380 if (! qsFile.isEmpty()) {
381 QFile f(qsFile);
382 // Make sure we don't accidently read something big the user
383 // happened to have in his clipboard. We only want to look
384 // at small link files.
385 if (f.open(QIODevice::ReadOnly) && f.size() < 10240) {
386 QByteArray qba = f.readAll();
387 f.close();
388
389 url = QUrl::fromEncoded(qba, QUrl::StrictMode);
390 if (! url.isValid()) {
391 // Windows internet shortcut files (.url) are an ini with an URL value
392 QSettings qs(qsFile, QSettings::IniFormat);
393 url = QUrl::fromEncoded(qs.value(QLatin1String("InternetShortcut/URL")).toByteArray(), QUrl::StrictMode);
394 }
395 }
396 }
397
398 if (default_name) {
399 #if QT_VERSION >= 0x050000
400 QUrlQuery query(url);
401 if (! query.hasQueryItem(QLatin1String("title"))) {
402 query.addQueryItem(QLatin1String("title"), url.host());
403 }
404 #else
405 if (! url.hasQueryItem(QLatin1String("title"))) {
406 url.addQueryItem(QLatin1String("title"), url.host());
407 }
408 #endif
409 }
410
411 if (! url.isValid()) {
412 return NULL;
413 }
414
415 // An URL from text without a scheme will have the hostname text
416 // in the QUrl scheme and no hostname. We do not want to use that.
417 if (url.host().isEmpty()) {
418 return NULL;
419 }
420
421 // Some communication programs automatically create http links from domains.
422 // When a user sends another user a domain to connect to, and http is added wrongly,
423 // we do our best to remove it again.
424 if (convertHttpUrls && (
425 url.scheme() == QLatin1String("http")
426 || url.scheme() == QLatin1String("https"))) {
427 url.setScheme(QLatin1String("mumble"));
428 }
429
430 return fromUrl(url, p);
431 }
432
fromUrl(QUrl url,QWidget * p)433 ServerItem *ServerItem::fromUrl(QUrl url, QWidget *p) {
434 if (! url.isValid() || (url.scheme() != QLatin1String("mumble"))) {
435 return NULL;
436 }
437
438 #if QT_VERSION >= 0x050000
439 QUrlQuery query(url);
440 #endif
441
442 if (url.userName().isEmpty()) {
443 if (g.s.qsUsername.isEmpty()) {
444 bool ok;
445 QString defUserName = QInputDialog::getText(p, ConnectDialog::tr("Adding host %1").arg(url.host()), ConnectDialog::tr("Enter username"), QLineEdit::Normal, g.s.qsUsername, &ok).trimmed();
446 if (! ok)
447 return NULL;
448 if (defUserName.isEmpty())
449 return NULL;
450 g.s.qsUsername = defUserName;
451 }
452 url.setUserName(g.s.qsUsername);
453 }
454
455 #if QT_VERSION >= 0x050000
456 ServerItem *si = new ServerItem(query.queryItemValue(QLatin1String("title")), url.host(), static_cast<unsigned short>(url.port(DEFAULT_MUMBLE_PORT)), url.userName(), url.password());
457
458 if (query.hasQueryItem(QLatin1String("url")))
459 si->qsUrl = query.queryItemValue(QLatin1String("url"));
460 #else
461 ServerItem *si = new ServerItem(url.queryItemValue(QLatin1String("title")), url.host(), static_cast<unsigned short>(url.port(DEFAULT_MUMBLE_PORT)), url.userName(), url.password());
462
463 if (url.hasQueryItem(QLatin1String("url")))
464 si->qsUrl = url.queryItemValue(QLatin1String("url"));
465 #endif
466
467 return si;
468 }
469
data(int column,int role) const470 QVariant ServerItem::data(int column, int role) const {
471 if (bParent) {
472 if (column == 0) {
473 switch (role) {
474 case Qt::DisplayRole:
475 return qsName;
476 case Qt::DecorationRole:
477 if (itType == FavoriteType)
478 return loadIcon(QLatin1String("skin:emblems/emblem-favorite.svg"));
479 else if (itType == LANType)
480 return loadIcon(QLatin1String("skin:places/network-workgroup.svg"));
481 else if (! qsCountryCode.isEmpty()) {
482 QString flag = QString::fromLatin1(":/flags/%1.svg").arg(qsCountryCode);
483 if (!QFileInfo(flag).exists()) {
484 flag = QLatin1String("skin:categories/applications-internet.svg");
485 }
486 return loadIcon(flag);
487 }
488 else
489 return loadIcon(QLatin1String("skin:categories/applications-internet.svg"));
490 }
491 }
492 } else {
493 if (role == Qt::DisplayRole) {
494 switch (column) {
495 case 0:
496 return qsName;
497 case 1:
498 return (dPing > 0.0) ? QString::number(uiPing) : QVariant();
499 case 2:
500 return uiUsers ? QString::fromLatin1("%1/%2 ").arg(uiUsers).arg(uiMaxUsers) : QVariant();
501 }
502 } else if (role == Qt::ToolTipRole) {
503 QStringList qsl;
504 foreach(const ServerAddress &addr, qlAddresses) {
505 qsl << Qt::escape(addr.host.toString() + QLatin1String(":") + QString::number(static_cast<unsigned long>(addr.port)));
506 }
507
508 double ploss = 100.0;
509
510 if (uiSent > 0)
511 ploss = (uiSent - qMin(uiRecv, uiSent)) * 100. / uiSent;
512
513 QString qs;
514 qs +=
515 QLatin1String("<table>") +
516 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Servername"), Qt::escape(qsName)) +
517 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Hostname"), Qt::escape(qsHostname));
518
519 if (! qsBonjourHost.isEmpty())
520 qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Bonjour name"), Qt::escape(qsBonjourHost));
521
522 qs +=
523 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Port")).arg(usPort) +
524 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Addresses"), qsl.join(QLatin1String(", ")));
525
526 if (! qsUrl.isEmpty())
527 qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Website"), Qt::escape(qsUrl));
528
529 if (uiSent > 0) {
530 qs += QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Packet loss"), QString::fromLatin1("%1% (%2/%3)").arg(ploss, 0, 'f', 1).arg(uiRecv).arg(uiSent));
531 if (uiRecv > 0) {
532 qs +=
533 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Ping (80%)"), ConnectDialog::tr("%1 ms").
534 arg(boost::accumulators::extended_p_square(* asQuantile)[1] / 1000., 0, 'f', 2)) +
535 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Ping (95%)"), ConnectDialog::tr("%1 ms").
536 arg(boost::accumulators::extended_p_square(* asQuantile)[2] / 1000., 0, 'f', 2)) +
537 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Bandwidth"), ConnectDialog::tr("%1 kbit/s").arg(uiBandwidth / 1000)) +
538 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Users"), QString::fromLatin1("%1/%2").arg(uiUsers).arg(uiMaxUsers)) +
539 QString::fromLatin1("<tr><th align=left>%1</th><td>%2</td></tr>").arg(ConnectDialog::tr("Version")).arg(MumbleVersion::toString(uiVersion));
540 }
541 }
542 qs += QLatin1String("</table>");
543 return qs;
544 } else if (role == Qt::BackgroundRole) {
545 if (bCA) {
546 QColor qc(Qt::green);
547 qc.setAlpha(32);
548 return qc;
549 }
550 }
551 }
552 return QTreeWidgetItem::data(column, role);
553 }
554
addServerItem(ServerItem * childitem)555 void ServerItem::addServerItem(ServerItem *childitem) {
556 Q_ASSERT(childitem->siParent == NULL);
557
558 childitem->siParent = this;
559 qlChildren.append(childitem);
560 childitem->hideCheck();
561
562 if (bParent && (itType != PublicType) && isHidden())
563 setHidden(false);
564 }
565
566 // If all child items are hidden, there is no child indicator, regardless of policy, so we have to add/remove instead.
hideCheck()567 void ServerItem::hideCheck() {
568 bool hide = false;
569 bool ishidden = (parent() == NULL);
570
571 if (! bParent && (itType == PublicType)) {
572 if (g.s.ssFilter == Settings::ShowReachable)
573 hide = (dPing == 0.0);
574 else if (g.s.ssFilter == Settings::ShowPopulated)
575 hide = (uiUsers == 0);
576 }
577 if (hide != ishidden) {
578 if (hide)
579 siParent->removeChild(this);
580 else
581 siParent->addChild(this);
582 }
583 }
584
setDatas(double elapsed,quint32 users,quint32 maxusers)585 void ServerItem::setDatas(double elapsed, quint32 users, quint32 maxusers) {
586 if (elapsed == 0.0) {
587 emitDataChanged();
588 return;
589 }
590
591 (*asQuantile)(static_cast<double>(elapsed));
592 dPing = boost::accumulators::extended_p_square(*asQuantile)[0];
593 if (dPing == 0.0)
594 dPing = elapsed;
595
596 quint32 ping = static_cast<quint32>(lround(dPing / 1000.));
597 uiRecv = static_cast<quint32>(boost::accumulators::count(* asQuantile));
598
599 bool changed = (ping != uiPing) || (users != uiUsers) || (maxusers != uiMaxUsers);
600
601 uiUsers = users;
602 uiMaxUsers = maxusers;
603 uiPing = ping;
604
605 double grace = qMax(5000., 50. * uiPingSort);
606 double diff = fabs(1000. * uiPingSort - dPing);
607
608 if ((uiPingSort == 0) || ((uiSent >= 10) && (diff >= grace)))
609 uiPingSort = ping;
610
611 if (changed)
612 emitDataChanged();
613 }
614
toFavoriteServer() const615 FavoriteServer ServerItem::toFavoriteServer() const {
616 FavoriteServer fs;
617 fs.qsName = qsName;
618 if (! qsBonjourHost.isEmpty())
619 fs.qsHostname = QLatin1Char('@') + qsBonjourHost;
620 else
621 fs.qsHostname = qsHostname;
622 fs.usPort = usPort;
623 fs.qsUsername = qsUsername;
624 fs.qsPassword = qsPassword;
625 fs.qsUrl = qsUrl;
626 return fs;
627 }
628
629
630 /**
631 * This function turns a ServerItem object into a QMimeData object holding a URL to the server.
632 */
toMimeData() const633 QMimeData *ServerItem::toMimeData() const {
634 QMimeData *mime = ServerItem::toMimeData(qsName, qsHostname, usPort);
635
636 if (itType == FavoriteType)
637 mime->setData(QLatin1String("OriginatedInMumble"), QByteArray());
638
639 return mime;
640 }
641
642 /**
643 * This function creates a QMimeData object containing a URL to the server at host and port. name is passed in the
644 * query string as "title", which is used for adding a server to favorites. channel may be omitted, but if specified it
645 * should be in the format of "/path/to/channel".
646 */
toMimeData(const QString & name,const QString & host,unsigned short port,const QString & channel)647 QMimeData *ServerItem::toMimeData(const QString &name, const QString &host, unsigned short port, const QString &channel) {
648 QUrl url;
649 url.setScheme(QLatin1String("mumble"));
650 url.setHost(host);
651 if (port != DEFAULT_MUMBLE_PORT)
652 url.setPort(port);
653 url.setPath(channel);
654
655 #if QT_VERSION >= 0x050000
656 QUrlQuery query;
657 query.addQueryItem(QLatin1String("title"), name);
658 query.addQueryItem(QLatin1String("version"), QLatin1String("1.2.0"));
659 url.setQuery(query);
660 #else
661 url.addQueryItem(QLatin1String("title"), name);
662 url.addQueryItem(QLatin1String("version"), QLatin1String("1.2.0"));
663 #endif
664
665 QString qs = QLatin1String(url.toEncoded());
666
667 QMimeData *mime = new QMimeData;
668
669 #ifdef Q_OS_WIN
670 QString contents = QString::fromLatin1("[InternetShortcut]\r\nURL=%1\r\n").arg(qs);
671 QString urlname = QString::fromLatin1("%1.url").arg(name);
672
673 FILEGROUPDESCRIPTORA fgda;
674 ZeroMemory(&fgda, sizeof(fgda));
675 fgda.cItems = 1;
676 fgda.fgd[0].dwFlags = FD_LINKUI | FD_FILESIZE;
677 fgda.fgd[0].nFileSizeLow=contents.length();
678 strcpy_s(fgda.fgd[0].cFileName, MAX_PATH, urlname.toLocal8Bit().constData());
679 mime->setData(QLatin1String("FileGroupDescriptor"), QByteArray(reinterpret_cast<const char *>(&fgda), sizeof(fgda)));
680
681 FILEGROUPDESCRIPTORW fgdw;
682 ZeroMemory(&fgdw, sizeof(fgdw));
683 fgdw.cItems = 1;
684 fgdw.fgd[0].dwFlags = FD_LINKUI | FD_FILESIZE;
685 fgdw.fgd[0].nFileSizeLow=contents.length();
686 wcscpy_s(fgdw.fgd[0].cFileName, MAX_PATH, urlname.toStdWString().c_str());
687 mime->setData(QLatin1String("FileGroupDescriptorW"), QByteArray(reinterpret_cast<const char *>(&fgdw), sizeof(fgdw)));
688
689 mime->setData(QString::fromWCharArray(CFSTR_FILECONTENTS), contents.toLocal8Bit());
690
691 DWORD context[4];
692 context[0] = 0;
693 context[1] = 1;
694 context[2] = 0;
695 context[3] = 0;
696 mime->setData(QLatin1String("DragContext"), QByteArray(reinterpret_cast<const char *>(&context[0]), sizeof(context)));
697
698 DWORD dropaction;
699 dropaction = DROPEFFECT_LINK;
700 mime->setData(QString::fromWCharArray(CFSTR_PREFERREDDROPEFFECT), QByteArray(reinterpret_cast<const char *>(&dropaction), sizeof(dropaction)));
701 #endif
702 QList<QUrl> urls;
703 urls << url;
704 mime->setUrls(urls);
705
706 mime->setText(qs);
707 mime->setHtml(QString::fromLatin1("<a href=\"%1\">%2</a>").arg(qs).arg(Qt::escape(name)));
708
709 return mime;
710 }
711
operator <(const QTreeWidgetItem & o) const712 bool ServerItem::operator <(const QTreeWidgetItem &o) const {
713 const ServerItem &other = static_cast<const ServerItem &>(o);
714 const QTreeWidget *w = treeWidget();
715
716 const int column = w ? w->sortColumn() : 0;
717
718 if (itType != other.itType) {
719 const bool inverse = w ? (w->header()->sortIndicatorOrder() == Qt::DescendingOrder) : false;
720 bool less;
721
722 if (itType == FavoriteType)
723 less = true;
724 else if ((itType == LANType) && (other.itType == PublicType))
725 less = true;
726 else
727 less = false;
728 return less ^ inverse;
729 }
730
731 if (bParent) {
732 const bool inverse = w ? (w->header()->sortIndicatorOrder() == Qt::DescendingOrder) : false;
733 return (qsName < other.qsName) ^ inverse;
734 }
735
736 if (column == 0) {
737 QString a = qsName.toLower();
738 QString b = other.qsName.toLower();
739
740 QRegExp re(QLatin1String("[^0-9a-z]"));
741 a.remove(re);
742 b.remove(re);
743 return a < b;
744 } else if (column == 1) {
745 quint32 a = uiPingSort ? uiPingSort : UINT_MAX;
746 quint32 b = other.uiPingSort ? other.uiPingSort : UINT_MAX;
747 return a < b;
748 } else if (column == 2) {
749 return uiUsers < other.uiUsers;
750 }
751 return false;
752 }
753
loadIcon(const QString & name)754 QIcon ServerItem::loadIcon(const QString &name) {
755 if (! qmIcons.contains(name))
756 qmIcons.insert(name, QIcon(name));
757 return qmIcons.value(name);
758 }
759
ConnectDialogEdit(QWidget * p,const QString & name,const QString & host,const QString & user,unsigned short port,const QString & password)760 ConnectDialogEdit::ConnectDialogEdit(QWidget *p, const QString &name, const QString &host, const QString &user, unsigned short port, const QString &password) : QDialog(p) {
761 setupUi(this);
762 init();
763
764 bCustomLabel = ! name.simplified().isEmpty();
765
766 qleName->setText(name);
767 qleServer->setText(host);
768 qleUsername->setText(user);
769 qlePort->setText(QString::number(port));
770 qlePassword->setText(password);
771
772 validate();
773 }
774
ConnectDialogEdit(QWidget * parent)775 ConnectDialogEdit::ConnectDialogEdit(QWidget *parent) : QDialog(parent) {
776 setupUi(this);
777 setWindowTitle(tr("Add Server"));
778 init();
779
780 if (!updateFromClipboard()) {
781 // If connected to a server assume the user wants to add it
782 if (g.sh && g.sh->isRunning()) {
783 QString host, name, user, pw;
784 unsigned short port = DEFAULT_MUMBLE_PORT;
785
786 g.sh->getConnectionInfo(host, port, user, pw);
787 Channel *c = Channel::get(0);
788 if (c && c->qsName != QLatin1String("Root")) {
789 name = c->qsName;
790 }
791
792 showNotice(tr("You are currently connected to a server.\nDo you want to fill the dialog with the connection data of this server?\nHost: %1 Port: %2").arg(host).arg(port));
793 m_si = new ServerItem(name, host, port, user, pw);
794 }
795 }
796 qleUsername->setText(g.s.qsUsername);
797 }
798
init()799 void ConnectDialogEdit::init() {
800 m_si = NULL;
801 usPort = 0;
802 bOk = true;
803 bCustomLabel = false;
804
805 qwInlineNotice->hide();
806
807 qlePort->setValidator(new QIntValidator(1, 65535, qlePort));
808 qlePort->setText(QString::number(DEFAULT_MUMBLE_PORT));
809 qlePassword->setEchoMode(QLineEdit::Password);
810
811 connect(qleName, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
812 connect(qleServer, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
813 connect(qlePort, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
814 connect(qleUsername, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
815 connect(qlePassword, SIGNAL(textChanged(const QString &)), this, SLOT(validate()));
816
817 validate();
818 }
819
~ConnectDialogEdit()820 ConnectDialogEdit::~ConnectDialogEdit() {
821 delete m_si;
822 }
823
showNotice(const QString & text)824 void ConnectDialogEdit::showNotice(const QString &text) {
825 QLabel *label = qwInlineNotice->findChild<QLabel *>(QLatin1String("qlPasteNotice"));
826 Q_ASSERT(label);
827 label->setText(text);
828 qwInlineNotice->show();
829 adjustSize();
830 }
831
updateFromClipboard()832 bool ConnectDialogEdit::updateFromClipboard() {
833 delete m_si;
834 m_si = ServerItem::fromMimeData(QApplication::clipboard()->mimeData(), false, NULL, true);
835 bool hasServerData = m_si != NULL;
836 if (hasServerData) {
837 showNotice(tr("You have an URL in your clipboard.\nDo you want to fill the dialog with this data?\nHost: %1 Port: %2").arg(m_si->qsHostname).arg(m_si->usPort));
838 return true;
839 } else {
840 qwInlineNotice->hide();
841 adjustSize();
842 return false;
843 }
844 }
845
on_qbFill_clicked()846 void ConnectDialogEdit::on_qbFill_clicked() {
847 Q_ASSERT(m_si);
848
849 qwInlineNotice->hide();
850 adjustSize();
851
852 qleName->setText(m_si->qsName);
853 qleServer->setText(m_si->qsHostname);
854 qleUsername->setText(m_si->qsUsername);
855 qlePort->setText(QString::number(m_si->usPort));
856 qlePassword->setText(m_si->qsPassword);
857
858 delete m_si;
859 m_si = NULL;
860 }
861
on_qbDiscard_clicked()862 void ConnectDialogEdit::on_qbDiscard_clicked() {
863 qwInlineNotice->hide();
864 adjustSize();
865 }
866
on_qleName_textEdited(const QString & name)867 void ConnectDialogEdit::on_qleName_textEdited(const QString& name) {
868 if (bCustomLabel) {
869 // If empty, then reset to automatic label.
870 // NOTE(nik@jnstw.us): You may be tempted to set qleName to qleServer, but that results in the odd
871 // UI behavior that clearing the field doesn't clear it; it'll immediately equal qleServer. Instead,
872 // leave it empty and let it update the next time qleServer updates. Code in accept will default it
873 // to qleServer if it isn't updated beforehand.
874 if (name.simplified().isEmpty()) {
875 bCustomLabel = false;
876 }
877 } else {
878 // If manually edited, set to Custom
879 bCustomLabel = true;
880 }
881 }
882
on_qleServer_textEdited(const QString & server)883 void ConnectDialogEdit::on_qleServer_textEdited(const QString& server) {
884 // If using automatic label, update it
885 if (!bCustomLabel) {
886 qleName->setText(server);
887 }
888 }
889
validate()890 void ConnectDialogEdit::validate() {
891 qsName = qleName->text().simplified();
892 qsHostname = qleServer->text().simplified();
893 usPort = qlePort->text().toUShort();
894 qsUsername = qleUsername->text().simplified();
895 qsPassword = qlePassword->text();
896
897 // For bonjour hosts disable the port field as it's auto-detected
898 qlePort->setDisabled(!qsHostname.isEmpty() && qsHostname.startsWith(QLatin1Char('@')));
899
900 // For SuperUser show password edit
901 if (qsUsername.toLower() == QLatin1String("superuser")) {
902 qliPassword->setVisible(true);
903 qlePassword->setVisible(true);
904 qcbShowPassword->setVisible(true);
905 adjustSize();
906 } else if (qsPassword.isEmpty()) {
907 qliPassword->setVisible(false);
908 qlePassword->setVisible(false);
909 qcbShowPassword->setVisible(false);
910 adjustSize();
911 }
912
913 bOk = ! qsHostname.isEmpty() && ! qsUsername.isEmpty() && usPort;
914 qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(bOk);
915 }
916
accept()917 void ConnectDialogEdit::accept() {
918 validate();
919 if (bOk) {
920 QString server = qleServer->text().simplified();
921
922 // If the user accidentally added a schema or path part, drop it now.
923 // We can't do so during editing as that is quite jarring.
924 const int schemaPos = server.indexOf(QLatin1String("://"));
925 if (schemaPos != -1) {
926 server.remove(0, schemaPos + 3);
927 }
928
929 const int pathPos = server.indexOf(QLatin1Char('/'));
930 if (pathPos != -1) {
931 server.resize(pathPos);
932 }
933
934 qleServer->setText(server);
935
936 if (qleName->text().simplified().isEmpty() || !bCustomLabel) {
937 qleName->setText(server);
938 }
939
940 QDialog::accept();
941 }
942 }
943
on_qcbShowPassword_toggled(bool checked)944 void ConnectDialogEdit::on_qcbShowPassword_toggled(bool checked) {
945 qlePassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
946 }
947
ConnectDialog(QWidget * p,bool autoconnect)948 ConnectDialog::ConnectDialog(QWidget *p, bool autoconnect) : QDialog(p), bAutoConnect(autoconnect) {
949 setupUi(this);
950 #ifdef Q_OS_MAC
951 setWindowModality(Qt::WindowModal);
952 #endif
953 bPublicInit = false;
954
955 siAutoConnect = NULL;
956
957 bAllowPing = g.s.ptProxyType == Settings::NoProxy;
958 bAllowHostLookup = g.s.ptProxyType == Settings::NoProxy;
959 bAllowBonjour = g.s.ptProxyType == Settings::NoProxy;
960 bAllowFilters = g.s.ptProxyType == Settings::NoProxy;
961
962 if (tPublicServers.elapsed() >= 60 * 24 * 1000000ULL) {
963 qlPublicServers.clear();
964 }
965
966 qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
967 qdbbButtonBox->button(QDialogButtonBox::Ok)->setText(tr("&Connect"));
968
969 QPushButton *qpbAdd = new QPushButton(tr("&Add New..."), this);
970 qpbAdd->setDefault(false);
971 qpbAdd->setAutoDefault(false);
972 connect(qpbAdd, SIGNAL(clicked()), qaFavoriteAddNew, SIGNAL(triggered()));
973 qdbbButtonBox->addButton(qpbAdd, QDialogButtonBox::ActionRole);
974
975
976 qpbEdit = new QPushButton(tr("&Edit..."), this);
977 qpbEdit->setEnabled(false);
978 qpbEdit->setDefault(false);
979 qpbEdit->setAutoDefault(false);
980 connect(qpbEdit, SIGNAL(clicked()), qaFavoriteEdit, SIGNAL(triggered()));
981 qdbbButtonBox->addButton(qpbEdit, QDialogButtonBox::ActionRole);
982
983 qpbAdd->setHidden(g.s.disableConnectDialogEditing);
984 qpbEdit->setHidden(g.s.disableConnectDialogEditing);
985
986 qtwServers->setItemDelegate(new ServerViewDelegate());
987
988 // Hide ping and user count if we are not allowed to ping.
989 if (!bAllowPing) {
990 qtwServers->setColumnCount(1);
991 }
992
993 qtwServers->sortItems(1, Qt::AscendingOrder);
994
995 #if QT_VERSION >= 0x050000
996 qtwServers->header()->setSectionResizeMode(0, QHeaderView::Stretch);
997 if (qtwServers->columnCount() >= 2) {
998 qtwServers->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
999 }
1000 if (qtwServers->columnCount() >= 3) {
1001 qtwServers->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
1002 }
1003 #else
1004 qtwServers->header()->setResizeMode(0, QHeaderView::Stretch);
1005 if (qtwServers->columnCount() >= 2) {
1006 qtwServers->header()->setResizeMode(1, QHeaderView::ResizeToContents);
1007 }
1008 if (qtwServers->columnCount() >= 3) {
1009 qtwServers->header()->setResizeMode(2, QHeaderView::ResizeToContents);
1010 }
1011 #endif
1012
1013 connect(qtwServers->header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), this, SLOT(OnSortChanged(int, Qt::SortOrder)));
1014
1015 qaShowAll->setChecked(false);
1016 qaShowReachable->setChecked(false);
1017 qaShowPopulated->setChecked(false);
1018
1019 if (bAllowFilters) {
1020 switch (g.s.ssFilter) {
1021 case Settings::ShowPopulated:
1022 qaShowPopulated->setChecked(true);
1023 break;
1024 case Settings::ShowReachable:
1025 qaShowReachable->setChecked(true);
1026 break;
1027 default:
1028 qaShowAll->setChecked(true);
1029 break;
1030 }
1031 } else {
1032 qaShowAll->setChecked(true);
1033 }
1034
1035 qagFilters = new QActionGroup(this);
1036 qagFilters->addAction(qaShowAll);
1037 qagFilters->addAction(qaShowReachable);
1038 qagFilters->addAction(qaShowPopulated);
1039
1040 connect(qagFilters, SIGNAL(triggered(QAction *)), this, SLOT(onFiltersTriggered(QAction *)));
1041
1042 qmPopup = new QMenu(this);
1043 qmFilters = new QMenu(tr("&Filters"), this);
1044 qmFilters->addAction(qaShowAll);
1045 qmFilters->addAction(qaShowReachable);
1046 qmFilters->addAction(qaShowPopulated);
1047
1048 if (!bAllowFilters) {
1049 qmFilters->setEnabled(false);
1050 }
1051
1052 QList<QTreeWidgetItem *> ql;
1053 QList<FavoriteServer> favorites = g.db->getFavorites();
1054
1055 foreach(const FavoriteServer &fs, favorites) {
1056 ServerItem *si = new ServerItem(fs);
1057 qlItems << si;
1058 startDns(si);
1059 qtwServers->siFavorite->addServerItem(si);
1060 }
1061
1062 #ifdef USE_BONJOUR
1063 // Make sure the we got the objects we need, then wire them up
1064 if (bAllowBonjour && g.bc->bsbBrowser && g.bc->bsrResolver) {
1065 connect(g.bc->bsbBrowser, SIGNAL(error(DNSServiceErrorType)),
1066 this, SLOT(onLanBrowseError(DNSServiceErrorType)));
1067 connect(g.bc->bsbBrowser, SIGNAL(currentBonjourRecordsChanged(const QList<BonjourRecord> &)),
1068 this, SLOT(onUpdateLanList(const QList<BonjourRecord> &)));
1069 connect(g.bc->bsrResolver, SIGNAL(error(BonjourRecord, DNSServiceErrorType)),
1070 this, SLOT(onLanResolveError(BonjourRecord, DNSServiceErrorType)));
1071 connect(g.bc->bsrResolver, SIGNAL(bonjourRecordResolved(BonjourRecord, QString, int)),
1072 this, SLOT(onResolved(BonjourRecord, QString, int)));
1073 onUpdateLanList(g.bc->bsbBrowser->currentRecords());
1074 }
1075 #endif
1076
1077 qtPingTick = new QTimer(this);
1078 connect(qtPingTick, SIGNAL(timeout()), this, SLOT(timeTick()));
1079
1080 qusSocket4 = new QUdpSocket(this);
1081 qusSocket6 = new QUdpSocket(this);
1082 bIPv4 = qusSocket4->bind(QHostAddress(QHostAddress::Any), 0);
1083 bIPv6 = qusSocket6->bind(QHostAddress(QHostAddress::AnyIPv6), 0);
1084 connect(qusSocket4, SIGNAL(readyRead()), this, SLOT(udpReply()));
1085 connect(qusSocket6, SIGNAL(readyRead()), this, SLOT(udpReply()));
1086
1087 if (qtwServers->siFavorite->isHidden()
1088 && (!qtwServers->siLAN || qtwServers->siLAN->isHidden())
1089 && qtwServers->siPublic != NULL) {
1090 qtwServers->siPublic->setExpanded(true);
1091 }
1092
1093 iPingIndex = -1;
1094 qtPingTick->start(50);
1095
1096 new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(on_qaFavoriteCopy_triggered()));
1097 new QShortcut(QKeySequence(QKeySequence::Paste), this, SLOT(on_qaFavoritePaste_triggered()));
1098
1099 qtwServers->setCurrentItem(NULL);
1100 bLastFound = false;
1101
1102 qmPingCache = g.db->getPingCache();
1103
1104 if (! g.s.qbaConnectDialogGeometry.isEmpty())
1105 restoreGeometry(g.s.qbaConnectDialogGeometry);
1106 if (! g.s.qbaConnectDialogHeader.isEmpty())
1107 qtwServers->header()->restoreState(g.s.qbaConnectDialogHeader);
1108 }
1109
~ConnectDialog()1110 ConnectDialog::~ConnectDialog() {
1111 ServerItem::qmIcons.clear();
1112
1113 QList<FavoriteServer> ql;
1114 qmPingCache.clear();
1115
1116 foreach(ServerItem *si, qlItems) {
1117 if (si->uiPing)
1118 qmPingCache.insert(UnresolvedServerAddress(si->qsHostname, si->usPort), si->uiPing);
1119
1120 if (si->itType != ServerItem::FavoriteType)
1121 continue;
1122 ql << si->toFavoriteServer();
1123 }
1124 g.db->setFavorites(ql);
1125 g.db->setPingCache(qmPingCache);
1126
1127 g.s.qbaConnectDialogHeader = qtwServers->header()->saveState();
1128 g.s.qbaConnectDialogGeometry = saveGeometry();
1129 }
1130
accept()1131 void ConnectDialog::accept() {
1132 ServerItem *si = static_cast<ServerItem *>(qtwServers->currentItem());
1133 if (! si || (bAllowHostLookup && si->qlAddresses.isEmpty()) || si->qsHostname.isEmpty()) {
1134 qWarning() << "Invalid server";
1135 return;
1136 }
1137
1138 qsPassword = si->qsPassword;
1139 qsServer = si->qsHostname;
1140 usPort = si->usPort;
1141
1142 if (si->qsUsername.isEmpty()) {
1143 bool ok;
1144 QString defUserName = QInputDialog::getText(this, tr("Connecting to %1").arg(si->qsName), tr("Enter username"), QLineEdit::Normal, g.s.qsUsername, &ok).trimmed();
1145 if (! ok)
1146 return;
1147 g.s.qsUsername = si->qsUsername = defUserName;
1148 }
1149
1150 qsUsername = si->qsUsername;
1151
1152 g.s.qsLastServer = si->qsName;
1153
1154 QDialog::accept();
1155 }
1156
OnSortChanged(int logicalIndex,Qt::SortOrder)1157 void ConnectDialog::OnSortChanged(int logicalIndex, Qt::SortOrder) {
1158 if (logicalIndex != 2) {
1159 return;
1160 }
1161
1162 foreach(ServerItem *si, qlItems) {
1163 if (si->uiPing && (si->uiPing != si->uiPingSort)) {
1164 si->uiPingSort = si->uiPing;
1165 si->setDatas();
1166 }
1167 }
1168 }
1169
on_qaFavoriteAdd_triggered()1170 void ConnectDialog::on_qaFavoriteAdd_triggered() {
1171 ServerItem *si = static_cast<ServerItem *>(qtwServers->currentItem());
1172 if (! si || (si->itType == ServerItem::FavoriteType))
1173 return;
1174
1175 si = new ServerItem(si);
1176 qtwServers->fixupName(si);
1177 qlItems << si;
1178 qtwServers->siFavorite->addServerItem(si);
1179 qtwServers->setCurrentItem(si);
1180 startDns(si);
1181 }
1182
on_qaFavoriteAddNew_triggered()1183 void ConnectDialog::on_qaFavoriteAddNew_triggered() {
1184 ConnectDialogEdit *cde = new ConnectDialogEdit(this);
1185
1186 if (cde->exec() == QDialog::Accepted) {
1187 ServerItem *si = new ServerItem(cde->qsName, cde->qsHostname, cde->usPort, cde->qsUsername, cde->qsPassword);
1188 qlItems << si;
1189 qtwServers->siFavorite->addServerItem(si);
1190 qtwServers->setCurrentItem(si);
1191 startDns(si);
1192 }
1193 delete cde;
1194 }
1195
on_qaFavoriteEdit_triggered()1196 void ConnectDialog::on_qaFavoriteEdit_triggered() {
1197 ServerItem *si = static_cast<ServerItem *>(qtwServers->currentItem());
1198 if (! si || (si->itType != ServerItem::FavoriteType))
1199 return;
1200
1201 QString host;
1202 if (! si->qsBonjourHost.isEmpty())
1203 host = QLatin1Char('@') + si->qsBonjourHost;
1204 else
1205 host = si->qsHostname;
1206
1207 ConnectDialogEdit *cde = new ConnectDialogEdit(this, si->qsName, host, si->qsUsername, si->usPort, si->qsPassword);
1208
1209 if (cde->exec() == QDialog::Accepted) {
1210
1211 si->qsName = cde->qsName;
1212 si->qsUsername = cde->qsUsername;
1213 si->qsPassword = cde->qsPassword;
1214 if ((cde->qsHostname != host) || (cde->usPort != si->usPort)) {
1215 stopDns(si);
1216
1217 si->qlAddresses.clear();
1218 si->reset();
1219
1220 si->usPort = cde->usPort;
1221
1222 if (cde->qsHostname.startsWith(QLatin1Char('@'))) {
1223 si->qsHostname = QString();
1224 si->qsBonjourHost = cde->qsHostname.mid(1);
1225 si->brRecord = BonjourRecord(si->qsBonjourHost, QLatin1String("_mumble._tcp."), QLatin1String("local."));
1226 } else {
1227 si->qsHostname = cde->qsHostname;
1228 si->qsBonjourHost = QString();
1229 si->brRecord = BonjourRecord();
1230 }
1231 startDns(si);
1232 }
1233 si->setDatas();
1234 }
1235 delete cde;
1236 }
1237
on_qaFavoriteRemove_triggered()1238 void ConnectDialog::on_qaFavoriteRemove_triggered() {
1239 ServerItem *si = static_cast<ServerItem *>(qtwServers->currentItem());
1240 if (! si || (si->itType != ServerItem::FavoriteType))
1241 return;
1242
1243 stopDns(si);
1244 qlItems.removeAll(si);
1245 delete si;
1246 }
1247
on_qaFavoriteCopy_triggered()1248 void ConnectDialog::on_qaFavoriteCopy_triggered() {
1249 ServerItem *si = static_cast<ServerItem *>(qtwServers->currentItem());
1250 if (! si)
1251 return;
1252
1253 QApplication::clipboard()->setMimeData(si->toMimeData());
1254 }
1255
on_qaFavoritePaste_triggered()1256 void ConnectDialog::on_qaFavoritePaste_triggered() {
1257 ServerItem *si = ServerItem::fromMimeData(QApplication::clipboard()->mimeData());
1258 if (! si)
1259 return;
1260
1261 qlItems << si;
1262 qtwServers->siFavorite->addServerItem(si);
1263 qtwServers->setCurrentItem(si);
1264 startDns(si);
1265 }
1266
on_qaUrl_triggered()1267 void ConnectDialog::on_qaUrl_triggered() {
1268 ServerItem *si = static_cast<ServerItem *>(qtwServers->currentItem());
1269 if (! si || si->qsUrl.isEmpty())
1270 return;
1271
1272 QDesktopServices::openUrl(QUrl(si->qsUrl));
1273 }
1274
onFiltersTriggered(QAction * act)1275 void ConnectDialog::onFiltersTriggered(QAction *act) {
1276 if (act == qaShowAll)
1277 g.s.ssFilter = Settings::ShowAll;
1278 else if (act == qaShowReachable)
1279 g.s.ssFilter = Settings::ShowReachable;
1280 else if (act == qaShowPopulated)
1281 g.s.ssFilter = Settings::ShowPopulated;
1282
1283 foreach(ServerItem *si, qlItems)
1284 si->hideCheck();
1285 }
1286
on_qtwServers_customContextMenuRequested(const QPoint & mpos)1287 void ConnectDialog::on_qtwServers_customContextMenuRequested(const QPoint &mpos) {
1288 ServerItem *si = static_cast<ServerItem *>(qtwServers->itemAt(mpos));
1289 qmPopup->clear();
1290
1291 if (si != NULL && si->bParent) {
1292 si = NULL;
1293 }
1294
1295 if (si != NULL) {
1296
1297 if (!g.s.disableConnectDialogEditing) {
1298 if (si->itType == ServerItem::FavoriteType) {
1299 qmPopup->addAction(qaFavoriteEdit);
1300 qmPopup->addAction(qaFavoriteRemove);
1301 } else {
1302 qmPopup->addAction(qaFavoriteAdd);
1303 }
1304 }
1305
1306 if (!si->qsUrl.isEmpty()) {
1307 qmPopup->addAction(qaUrl);
1308 }
1309 }
1310
1311 if (! qmPopup->isEmpty()) {
1312 qmPopup->addSeparator();
1313 }
1314
1315 qmPopup->addMenu(qmFilters);
1316
1317 qmPopup->popup(qtwServers->viewport()->mapToGlobal(mpos), NULL);
1318 }
1319
on_qtwServers_itemDoubleClicked(QTreeWidgetItem * item,int)1320 void ConnectDialog::on_qtwServers_itemDoubleClicked(QTreeWidgetItem *item, int) {
1321 qtwServers->setCurrentItem(item);
1322 accept();
1323 }
1324
on_qtwServers_currentItemChanged(QTreeWidgetItem * item,QTreeWidgetItem *)1325 void ConnectDialog::on_qtwServers_currentItemChanged(QTreeWidgetItem *item, QTreeWidgetItem *) {
1326 ServerItem *si = static_cast<ServerItem *>(item);
1327
1328 if (si->siParent == qtwServers->siFavorite) {
1329 qpbEdit->setEnabled(true);
1330 } else {
1331 qpbEdit->setEnabled(false);
1332 }
1333
1334 bool bOk = !si->qlAddresses.isEmpty();
1335 if (!bAllowHostLookup) {
1336 bOk = true;
1337 }
1338 qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(bOk);
1339
1340 bLastFound = true;
1341 }
1342
on_qtwServers_itemExpanded(QTreeWidgetItem * item)1343 void ConnectDialog::on_qtwServers_itemExpanded(QTreeWidgetItem *item) {
1344 if (qtwServers->siPublic != NULL && item == qtwServers->siPublic) {
1345 initList();
1346 fillList();
1347 }
1348
1349 ServerItem *p = static_cast<ServerItem *>(item);
1350
1351 foreach(ServerItem *si, p->qlChildren)
1352 startDns(si);
1353 }
1354
initList()1355 void ConnectDialog::initList() {
1356 if (bPublicInit || (qlPublicServers.count() > 0))
1357 return;
1358
1359 bPublicInit = true;
1360
1361 QUrl url;
1362 url.setPath(QLatin1String("/v1/list"));
1363 #if QT_VERSION >= 0x050000
1364 QUrlQuery query;
1365 query.addQueryItem(QLatin1String("version"), QLatin1String(MUMTEXT(MUMBLE_VERSION_STRING)));
1366 url.setQuery(query);
1367 #else
1368 url.addQueryItem(QLatin1String("version"), QLatin1String(MUMTEXT(MUMBLE_VERSION_STRING)));
1369 #endif
1370
1371 WebFetch::fetch(QLatin1String("publist"), url, this, SLOT(fetched(QByteArray,QUrl,QMap<QString,QString>)));
1372 }
1373
1374 #ifdef USE_BONJOUR
onResolved(BonjourRecord record,QString host,int port)1375 void ConnectDialog::onResolved(BonjourRecord record, QString host, int port) {
1376 qlBonjourActive.removeAll(record);
1377 foreach(ServerItem *si, qlItems) {
1378 if (si->brRecord == record) {
1379 unsigned short usport = static_cast<unsigned short>(port);
1380 if ((host != si->qsHostname) || (usport != si->usPort)) {
1381 stopDns(si);
1382 si->usPort = static_cast<unsigned short>(port);
1383 si->qsHostname = host;
1384 startDns(si);
1385 }
1386 }
1387 }
1388 }
1389
onUpdateLanList(const QList<BonjourRecord> & list)1390 void ConnectDialog::onUpdateLanList(const QList<BonjourRecord> &list) {
1391 QSet<ServerItem *> items;
1392 QSet<ServerItem *> old = qtwServers->siLAN->qlChildren.toSet();
1393
1394 foreach(const BonjourRecord &record, list) {
1395 bool found = false;
1396 foreach(ServerItem *si, old) {
1397 if (si->brRecord == record) {
1398 items.insert(si);
1399 found = true;
1400 break;
1401 }
1402 }
1403 if (! found) {
1404 ServerItem *si = new ServerItem(record);
1405 qlItems << si;
1406 g.bc->bsrResolver->resolveBonjourRecord(record);
1407 startDns(si);
1408 qtwServers->siLAN->addServerItem(si);
1409 }
1410 }
1411 QSet<ServerItem *> remove = old.subtract(items);
1412 foreach(ServerItem *si, remove) {
1413 stopDns(si);
1414 qlItems.removeAll(si);
1415 delete si;
1416 }
1417 }
1418
onLanBrowseError(DNSServiceErrorType err)1419 void ConnectDialog::onLanBrowseError(DNSServiceErrorType err) {
1420 qWarning()<<"Bonjour reported browser error "<< err;
1421 }
1422
onLanResolveError(BonjourRecord br,DNSServiceErrorType err)1423 void ConnectDialog::onLanResolveError(BonjourRecord br, DNSServiceErrorType err) {
1424 qlBonjourActive.removeAll(br);
1425 qWarning()<<"Bonjour reported resolver error "<< err;
1426 }
1427 #endif
1428
fillList()1429 void ConnectDialog::fillList() {
1430 QList<QTreeWidgetItem *> ql;
1431 QList<QTreeWidgetItem *> qlNew;
1432
1433 foreach(const PublicInfo &pi, qlPublicServers) {
1434 bool found = false;
1435 foreach(ServerItem *si, qlItems) {
1436 if ((pi.qsIp == si->qsHostname) && (pi.usPort == si->usPort)) {
1437 si->qsCountry = pi.qsCountry;
1438 si->qsCountryCode = pi.qsCountryCode;
1439 si->qsContinentCode = pi.qsContinentCode;
1440 si->qsUrl = pi.quUrl.toString();
1441 si->bCA = pi.bCA;
1442 si->setDatas();
1443
1444 if (si->itType == ServerItem::PublicType)
1445 found = true;
1446 }
1447 }
1448 if (! found)
1449 ql << new ServerItem(pi);
1450 }
1451
1452 while (! ql.isEmpty()) {
1453 ServerItem *si = static_cast<ServerItem *>(ql.takeAt(qrand() % ql.count()));
1454 qlNew << si;
1455 qlItems << si;
1456 }
1457
1458 foreach(QTreeWidgetItem *qtwi, qlNew) {
1459 ServerItem *si = static_cast<ServerItem *>(qtwi);
1460 ServerItem *p = qtwServers->getParent(si->qsContinentCode, si->qsCountryCode, si->qsCountry, qsUserContinentCode, qsUserCountryCode);
1461 p->addServerItem(si);
1462
1463 if (p->isExpanded() && p->parent()->isExpanded())
1464 startDns(si);
1465 }
1466 }
1467
timeTick()1468 void ConnectDialog::timeTick() {
1469 if (! bLastFound && ! g.s.qsLastServer.isEmpty()) {
1470 QList<QTreeWidgetItem *> items = qtwServers->findItems(g.s.qsLastServer, Qt::MatchExactly | Qt::MatchRecursive);
1471 if (!items.isEmpty()) {
1472 bLastFound = true;
1473 qtwServers->setCurrentItem(items.at(0));
1474 if (g.s.bAutoConnect && bAutoConnect) {
1475 siAutoConnect = static_cast<ServerItem *>(items.at(0));
1476 if (! siAutoConnect->qlAddresses.isEmpty()) {
1477 accept();
1478 return;
1479 } else if (!bAllowHostLookup) {
1480 accept();
1481 return;
1482 }
1483 }
1484 }
1485 }
1486
1487 if (bAllowHostLookup) {
1488 // Start DNS Lookup of first unknown hostname
1489 foreach(const UnresolvedServerAddress &unresolved, qlDNSLookup) {
1490 if (qsDNSActive.contains(unresolved)) {
1491 continue;
1492 }
1493
1494 qlDNSLookup.removeAll(unresolved);
1495 qlDNSLookup.append(unresolved);
1496
1497 qsDNSActive.insert(unresolved);
1498 ServerResolver *sr = new ServerResolver();
1499 QObject::connect(sr, SIGNAL(resolved()), this, SLOT(lookedUp()));
1500 sr->resolve(unresolved.hostname, unresolved.port);
1501 break;
1502 }
1503 }
1504
1505 ServerItem *current = static_cast<ServerItem *>(qtwServers->currentItem());
1506 ServerItem *hover = static_cast<ServerItem *>(qtwServers->itemAt(qtwServers->viewport()->mapFromGlobal(QCursor::pos())));
1507
1508 ServerItem *si = NULL;
1509
1510 if (tCurrent.elapsed() >= 1000000ULL)
1511 si = current;
1512 if (! si && (tHover.elapsed() >= 1000000ULL))
1513 si = hover;
1514
1515 if (si) {
1516 QString hostname = si->qsHostname.toLower();
1517 unsigned short port = si->usPort;
1518 UnresolvedServerAddress unresolved(hostname, port);
1519
1520 if (si->qlAddresses.isEmpty()) {
1521 if (! hostname.isEmpty()) {
1522 qlDNSLookup.removeAll(unresolved);
1523 qlDNSLookup.prepend(unresolved);
1524 }
1525 si = NULL;
1526 }
1527 }
1528
1529 if (!si) {
1530 if (qlItems.isEmpty())
1531 return;
1532
1533 bool expanded;
1534
1535 do {
1536 ++iPingIndex;
1537 if (iPingIndex >= qlItems.count()) {
1538 if (tRestart.isElapsed(1000000ULL))
1539 iPingIndex = 0;
1540 else
1541 return;
1542 }
1543 si = qlItems.at(iPingIndex);
1544
1545 ServerItem *p = si->siParent;
1546 expanded = true;
1547 while (p && expanded) {
1548 expanded = expanded && p->isExpanded();
1549 p = p->siParent;
1550 }
1551 } while (si->qlAddresses.isEmpty() || ! expanded);
1552 }
1553
1554 if (si == current)
1555 tCurrent.restart();
1556 if (si == hover)
1557 tHover.restart();
1558
1559 foreach(const ServerAddress &addr, si->qlAddresses) {
1560 sendPing(addr.host.toAddress(), addr.port);
1561 }
1562 }
1563
1564
startDns(ServerItem * si)1565 void ConnectDialog::startDns(ServerItem *si) {
1566 if (!bAllowHostLookup) {
1567 return;
1568 }
1569
1570 QString hostname = si->qsHostname.toLower();
1571 unsigned short port = si->usPort;
1572 UnresolvedServerAddress unresolved(hostname, port);
1573
1574 if (si->qlAddresses.isEmpty()) {
1575 // Determine if qsHostname is an IP address
1576 // or a hostname. If it is an IP address, we
1577 // can treat it as resolved as-is.
1578 QHostAddress qha(si->qsHostname);
1579 bool hostnameIsIPAddress = !qha.isNull();
1580 if (hostnameIsIPAddress) {
1581 si->qlAddresses.append(ServerAddress(HostAddress(qha), port));
1582 } else {
1583 si->qlAddresses = qhDNSCache.value(unresolved);
1584 }
1585 }
1586
1587 if (qtwServers->currentItem() == si)
1588 qdbbButtonBox->button(QDialogButtonBox::Ok)->setEnabled(! si->qlAddresses.isEmpty());
1589
1590 if (! si->qlAddresses.isEmpty()) {
1591 foreach(const ServerAddress &addr, si->qlAddresses) {
1592 qhPings[addr].insert(si);
1593 }
1594 return;
1595 }
1596
1597 #ifdef USE_BONJOUR
1598 if (bAllowBonjour && si->qsHostname.isEmpty() && ! si->brRecord.serviceName.isEmpty()) {
1599 if (! qlBonjourActive.contains(si->brRecord)) {
1600 g.bc->bsrResolver->resolveBonjourRecord(si->brRecord);
1601 qlBonjourActive.append(si->brRecord);
1602 }
1603 return;
1604 }
1605 #endif
1606
1607 if (! qhDNSWait.contains(unresolved)) {
1608 if (si->itType == ServerItem::PublicType)
1609 qlDNSLookup.append(unresolved);
1610 else
1611 qlDNSLookup.prepend(unresolved);
1612 }
1613 qhDNSWait[unresolved].insert(si);
1614 }
1615
stopDns(ServerItem * si)1616 void ConnectDialog::stopDns(ServerItem *si) {
1617 if (!bAllowHostLookup) {
1618 return;
1619 }
1620
1621 foreach(const ServerAddress &addr, si->qlAddresses) {
1622 if (qhPings.contains(addr)) {
1623 qhPings[addr].remove(si);
1624 if (qhPings[addr].isEmpty()) {
1625 qhPings.remove(addr);
1626 qhPingRand.remove(addr);
1627 }
1628 }
1629 }
1630
1631 QString hostname = si->qsHostname.toLower();
1632 unsigned short port = si->usPort;
1633 UnresolvedServerAddress unresolved(hostname, port);
1634
1635 if (qhDNSWait.contains(unresolved)) {
1636 qhDNSWait[unresolved].remove(si);
1637 if (qhDNSWait[unresolved].isEmpty()) {
1638 qhDNSWait.remove(unresolved);
1639 qlDNSLookup.removeAll(unresolved);
1640 }
1641 }
1642 }
1643
lookedUp()1644 void ConnectDialog::lookedUp() {
1645 ServerResolver *sr = qobject_cast<ServerResolver *>(QObject::sender());
1646 sr->deleteLater();
1647
1648 QString hostname = sr->hostname().toLower();
1649 unsigned short port = sr->port();
1650 UnresolvedServerAddress unresolved(hostname, port);
1651
1652 qsDNSActive.remove(unresolved);
1653
1654 // An error occurred, or no records were found.
1655 if (sr->records().size() == 0) {
1656 return;
1657 }
1658
1659 QSet<ServerAddress> qs;
1660 foreach (ServerResolverRecord record, sr->records()) {
1661 foreach(const HostAddress &ha, record.addresses()) {
1662 qs.insert(ServerAddress(ha, record.port()));
1663 }
1664 }
1665
1666 QSet<ServerItem *> waiting = qhDNSWait[unresolved];
1667 foreach(ServerItem *si, waiting) {
1668 foreach (const ServerAddress &addr, qs) {
1669 qhPings[addr].insert(si);
1670 }
1671
1672 si->qlAddresses = qs.toList();
1673 }
1674
1675 qlDNSLookup.removeAll(unresolved);
1676 qhDNSCache.insert(unresolved, qs.toList());
1677 qhDNSWait.remove(unresolved);
1678
1679 foreach(ServerItem *si, waiting) {
1680 if (si == qtwServers->currentItem()) {
1681 on_qtwServers_currentItemChanged(si, si);
1682 if (si == siAutoConnect)
1683 accept();
1684 }
1685 }
1686
1687 if (bAllowPing) {
1688 foreach(const ServerAddress &addr, qs) {
1689 sendPing(addr.host.toAddress(), addr.port);
1690 }
1691 }
1692 }
1693
sendPing(const QHostAddress & host,unsigned short port)1694 void ConnectDialog::sendPing(const QHostAddress &host, unsigned short port) {
1695 char blob[16];
1696
1697 ServerAddress addr(HostAddress(host), port);
1698
1699 quint64 uiRand;
1700 if (qhPingRand.contains(addr)) {
1701 uiRand = qhPingRand.value(addr);
1702 } else {
1703 uiRand = (static_cast<quint64>(qrand()) << 32) | static_cast<quint64>(qrand());
1704 qhPingRand.insert(addr, uiRand);
1705 }
1706
1707 memset(blob, 0, sizeof(blob));
1708 * reinterpret_cast<quint64 *>(blob+8) = tPing.elapsed() ^ uiRand;
1709
1710 if (bIPv4 && host.protocol() == QAbstractSocket::IPv4Protocol)
1711 qusSocket4->writeDatagram(blob+4, 12, host, port);
1712 else if (bIPv6 && host.protocol() == QAbstractSocket::IPv6Protocol)
1713 qusSocket6->writeDatagram(blob+4, 12, host, port);
1714 else
1715 return;
1716
1717 const QSet<ServerItem *> &qs = qhPings.value(addr);
1718
1719 foreach(ServerItem *si, qs)
1720 ++ si->uiSent;
1721 }
1722
udpReply()1723 void ConnectDialog::udpReply() {
1724 QUdpSocket *sock = qobject_cast<QUdpSocket *>(sender());
1725
1726 while (sock->hasPendingDatagrams()) {
1727 char blob[64];
1728
1729 QHostAddress host;
1730 unsigned short port;
1731
1732 qint64 len = sock->readDatagram(blob+4, 24, &host, &port);
1733 if (len == 24) {
1734 if (host.scopeId() == QLatin1String("0"))
1735 host.setScopeId(QLatin1String(""));
1736
1737 ServerAddress address(HostAddress(host), port);
1738
1739 if (qhPings.contains(address)) {
1740 quint32 *ping = reinterpret_cast<quint32 *>(blob+4);
1741 quint64 *ts = reinterpret_cast<quint64 *>(blob+8);
1742
1743 quint64 elapsed = tPing.elapsed() - (*ts ^ qhPingRand.value(address));
1744
1745 foreach(ServerItem *si, qhPings.value(address)) {
1746 si->uiVersion = qFromBigEndian(ping[0]);
1747 quint32 users = qFromBigEndian(ping[3]);
1748 quint32 maxusers = qFromBigEndian(ping[4]);
1749 si->uiBandwidth = qFromBigEndian(ping[5]);
1750
1751 if (! si->uiPingSort)
1752 si->uiPingSort = qmPingCache.value(UnresolvedServerAddress(si->qsHostname, si->usPort));
1753
1754 si->setDatas(static_cast<double>(elapsed), users, maxusers);
1755 si->hideCheck();
1756 }
1757 }
1758 }
1759 }
1760 }
1761
fetched(QByteArray xmlData,QUrl,QMap<QString,QString> headers)1762 void ConnectDialog::fetched(QByteArray xmlData, QUrl, QMap<QString, QString> headers) {
1763 if (xmlData.isNull()) {
1764 QMessageBox::warning(this, QLatin1String("Mumble"), tr("Failed to fetch server list"), QMessageBox::Ok);
1765 return;
1766 }
1767
1768 QDomDocument doc;
1769 doc.setContent(xmlData);
1770
1771 qlPublicServers.clear();
1772 qsUserCountry = headers.value(QLatin1String("Geo-Country"));
1773 qsUserCountryCode = headers.value(QLatin1String("Geo-Country-Code")).toLower();
1774 qsUserContinentCode = headers.value(QLatin1String("Geo-Continent-Code")).toLower();
1775
1776 QDomElement root=doc.documentElement();
1777 QDomNode n = root.firstChild();
1778 while (!n.isNull()) {
1779 QDomElement e = n.toElement();
1780 if (!e.isNull()) {
1781 if (e.tagName() == QLatin1String("server")) {
1782 PublicInfo pi;
1783 pi.qsName = e.attribute(QLatin1String("name"));
1784 pi.quUrl = e.attribute(QLatin1String("url"));
1785 pi.qsIp = e.attribute(QLatin1String("ip"));
1786 pi.usPort = e.attribute(QLatin1String("port")).toUShort();
1787 pi.qsCountry = e.attribute(QLatin1String("country"), tr("Unknown"));
1788 pi.qsCountryCode = e.attribute(QLatin1String("country_code")).toLower();
1789 pi.qsContinentCode = e.attribute(QLatin1String("continent_code")).toLower();
1790 pi.bCA = e.attribute(QLatin1String("ca")).toInt() ? true : false;
1791
1792 qlPublicServers << pi;
1793 }
1794 }
1795 n = n.nextSibling();
1796 }
1797
1798 tPublicServers.restart();
1799
1800 fillList();
1801 }
1802