1 /*
2  * This file Copyright (C) 2009-2015 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <ctime>
10 #include <iostream>
11 
12 #include <QIcon>
13 #include <QLibraryInfo>
14 #include <QMessageBox>
15 #include <QProcess>
16 #include <QRect>
17 #include <QSystemTrayIcon>
18 
19 #ifdef QT_DBUS_LIB
20 #include <QDBusConnection>
21 #include <QDBusMessage>
22 #include <QDBusReply>
23 #endif
24 
25 #include <libtransmission/transmission.h>
26 #include <libtransmission/tr-getopt.h>
27 #include <libtransmission/utils.h>
28 #include <libtransmission/version.h>
29 
30 #include "AddData.h"
31 #include "Application.h"
32 #include "Formatter.h"
33 #include "InteropHelper.h"
34 #include "MainWindow.h"
35 #include "OptionsDialog.h"
36 #include "Prefs.h"
37 #include "Session.h"
38 #include "TorrentModel.h"
39 #include "WatchDir.h"
40 
41 namespace
42 {
43 
44 QLatin1String const MY_CONFIG_NAME("transmission");
45 QLatin1String const MY_READABLE_NAME("transmission-qt");
46 
47 tr_option const opts[] =
48 {
49     { 'g', "config-dir", "Where to look for configuration files", "g", true, "<path>" },
50     { 'm', "minimized", "Start minimized in system tray", "m", false, nullptr },
51     { 'p', "port", "Port to use when connecting to an existing session", "p", true, "<port>" },
52     { 'r', "remote", "Connect to an existing session at the specified hostname", "r", true, "<host>" },
53     { 'u', "username", "Username to use when connecting to an existing session", "u", true, "<username>" },
54     { 'v', "version", "Show version number and exit", "v", false, nullptr },
55     { 'w', "password", "Password to use when connecting to an existing session", "w", true, "<password>" },
56     { 0, nullptr, nullptr, nullptr, false, nullptr }
57 };
58 
getUsage()59 char const* getUsage()
60 {
61     return "Usage:\n"
62         "  transmission [OPTIONS...] [torrent files]";
63 }
64 
65 enum
66 {
67     STATS_REFRESH_INTERVAL_MSEC = 3000,
68     SESSION_REFRESH_INTERVAL_MSEC = 3000,
69     MODEL_REFRESH_INTERVAL_MSEC = 3000
70 };
71 
loadTranslation(QTranslator & translator,QString const & name,QLocale const & locale,QStringList const & searchDirectories)72 bool loadTranslation(QTranslator& translator, QString const& name, QLocale const& locale, QStringList const& searchDirectories)
73 {
74     for (QString const& directory : searchDirectories)
75     {
76         if (translator.load(locale, name, QLatin1String("_"), directory))
77         {
78             return true;
79         }
80     }
81 
82     return false;
83 }
84 
85 } // namespace
86 
Application(int & argc,char ** argv)87 Application::Application(int& argc, char** argv) :
88     QApplication(argc, argv),
89     myPrefs(nullptr),
90     mySession(nullptr),
91     myModel(nullptr),
92     myWindow(nullptr),
93     myWatchDir(nullptr),
94     myLastFullUpdateTime(0)
95 {
96     setApplicationName(MY_CONFIG_NAME);
97     loadTranslations();
98 
99     Formatter::initUnits();
100 
101 #if defined(_WIN32) || defined(__APPLE__)
102 
103     if (QIcon::themeName().isEmpty())
104     {
105         QIcon::setThemeName(QLatin1String("Faenza"));
106     }
107 
108 #endif
109 
110     // set the default icon
111     QIcon icon = QIcon::fromTheme(QLatin1String("transmission"));
112 
113     if (icon.isNull())
114     {
115         QList<int> sizes;
116         sizes << 16 << 22 << 24 << 32 << 48 << 64 << 72 << 96 << 128 << 192 << 256;
117 
118         for (int const size : sizes)
119         {
120             icon.addPixmap(QPixmap(QString::fromLatin1(":/icons/transmission-%1.png").arg(size)));
121         }
122     }
123 
124     setWindowIcon(icon);
125 
126 #ifdef __APPLE__
127     setAttribute(Qt::AA_DontShowIconsInMenus);
128 #endif
129 
130     // parse the command-line arguments
131     int c;
132     bool minimized = false;
133     char const* optarg;
134     QString host;
135     QString port;
136     QString username;
137     QString password;
138     QString configDir;
139     QStringList filenames;
140 
141     while ((c = tr_getopt(getUsage(), argc, const_cast<char const**>(argv), opts, &optarg)) != TR_OPT_DONE)
142     {
143         switch (c)
144         {
145         case 'g':
146             configDir = QString::fromUtf8(optarg);
147             break;
148 
149         case 'p':
150             port = QString::fromUtf8(optarg);
151             break;
152 
153         case 'r':
154             host = QString::fromUtf8(optarg);
155             break;
156 
157         case 'u':
158             username = QString::fromUtf8(optarg);
159             break;
160 
161         case 'w':
162             password = QString::fromUtf8(optarg);
163             break;
164 
165         case 'm':
166             minimized = true;
167             break;
168 
169         case 'v':
170             std::cerr << MY_READABLE_NAME.latin1() << ' ' << LONG_VERSION_STRING << std::endl;
171             quitLater();
172             return;
173 
174         case TR_OPT_ERR:
175             std::cerr << qPrintable(QObject::tr("Invalid option")) << std::endl;
176             tr_getopt_usage(MY_READABLE_NAME.latin1(), getUsage(), opts);
177             quitLater();
178             return;
179 
180         default:
181             filenames.append(QString::fromUtf8(optarg));
182             break;
183         }
184     }
185 
186     // try to delegate the work to an existing copy of Transmission
187     // before starting ourselves...
188     InteropHelper interopClient;
189 
190     if (interopClient.isConnected())
191     {
192         bool delegated = false;
193 
194         for (QString const& filename : filenames)
195         {
196             QString metainfo;
197 
198             AddData a(filename);
199 
200             switch (a.type)
201             {
202             case AddData::URL:
203                 metainfo = a.url.toString();
204                 break;
205 
206             case AddData::MAGNET:
207                 metainfo = a.magnet;
208                 break;
209 
210             case AddData::FILENAME:
211                 metainfo = QString::fromLatin1(a.toBase64());
212                 break;
213 
214             case AddData::METAINFO:
215                 metainfo = QString::fromLatin1(a.toBase64());
216                 break;
217 
218             default:
219                 break;
220             }
221 
222             if (!metainfo.isEmpty() && interopClient.addMetainfo(metainfo))
223             {
224                 delegated = true;
225             }
226         }
227 
228         if (delegated)
229         {
230             quitLater();
231             return;
232         }
233     }
234 
235     // set the fallback config dir
236     if (configDir.isNull())
237     {
238         configDir = QString::fromUtf8(tr_getDefaultConfigDir("transmission"));
239     }
240 
241     // ensure our config directory exists
242     QDir dir(configDir);
243 
244     if (!dir.exists())
245     {
246         dir.mkpath(configDir);
247     }
248 
249     // is this the first time we've run transmission?
250     bool const firstTime = !dir.exists(QLatin1String("settings.json"));
251 
252     // initialize the prefs
253     myPrefs = new Prefs(configDir);
254 
255     if (!host.isNull())
256     {
257         myPrefs->set(Prefs::SESSION_REMOTE_HOST, host);
258     }
259 
260     if (!port.isNull())
261     {
262         myPrefs->set(Prefs::SESSION_REMOTE_PORT, port.toUInt());
263     }
264 
265     if (!username.isNull())
266     {
267         myPrefs->set(Prefs::SESSION_REMOTE_USERNAME, username);
268     }
269 
270     if (!password.isNull())
271     {
272         myPrefs->set(Prefs::SESSION_REMOTE_PASSWORD, password);
273     }
274 
275     if (!host.isNull() || !port.isNull() || !username.isNull() || !password.isNull())
276     {
277         myPrefs->set(Prefs::SESSION_IS_REMOTE, true);
278     }
279 
280     if (myPrefs->getBool(Prefs::START_MINIMIZED))
281     {
282         minimized = true;
283     }
284 
285     // start as minimized only if the system tray present
286     if (!myPrefs->getBool(Prefs::SHOW_TRAY_ICON))
287     {
288         minimized = false;
289     }
290 
291     mySession = new Session(configDir, *myPrefs);
292     myModel = new TorrentModel(*myPrefs);
293     myWindow = new MainWindow(*mySession, *myPrefs, *myModel, minimized);
294     myWatchDir = new WatchDir(*myModel);
295 
296     connect(myModel, &TorrentModel::torrentsAdded, this, &Application::onTorrentsAdded);
297     connect(myModel, &TorrentModel::torrentsCompleted, this, &Application::onTorrentsCompleted);
298     connect(myModel, &TorrentModel::torrentsNeedInfo, this, &Application::onTorrentsNeedInfo);
299     connect(myPrefs, &Prefs::changed, this, &Application::refreshPref);
300     connect(mySession, &Session::sourceChanged, this, &Application::onSessionSourceChanged);
301     connect(mySession, &Session::torrentsRemoved, myModel, &TorrentModel::removeTorrents);
302     connect(mySession, &Session::torrentsUpdated, myModel, &TorrentModel::updateTorrents);
303     connect(myWatchDir, &WatchDir::torrentFileAdded, this, &Application::addTorrent);
304 
305     // init from preferences
306     for (auto const key : { Prefs::DIR_WATCH })
307     {
308         refreshPref(key);
309     }
310 
311     QTimer* timer = &myModelTimer;
312     connect(timer, &QTimer::timeout, this, &Application::refreshTorrents);
313     timer->setSingleShot(false);
314     timer->setInterval(MODEL_REFRESH_INTERVAL_MSEC);
315     timer->start();
316 
317     timer = &myStatsTimer;
318     connect(timer, &QTimer::timeout, mySession, &Session::refreshSessionStats);
319     timer->setSingleShot(false);
320     timer->setInterval(STATS_REFRESH_INTERVAL_MSEC);
321     timer->start();
322 
323     timer = &mySessionTimer;
324     connect(timer, &QTimer::timeout, mySession, &Session::refreshSessionInfo);
325     timer->setSingleShot(false);
326     timer->setInterval(SESSION_REFRESH_INTERVAL_MSEC);
327     timer->start();
328 
329     maybeUpdateBlocklist();
330 
331     if (!firstTime)
332     {
333         mySession->restart();
334     }
335     else
336     {
337         myWindow->openSession();
338     }
339 
340     if (!myPrefs->getBool(Prefs::USER_HAS_GIVEN_INFORMED_CONSENT))
341     {
342         QMessageBox* dialog = new QMessageBox(QMessageBox::Information, QString(),
343             tr("<b>Transmission is a file sharing program.</b>"), QMessageBox::Ok | QMessageBox::Cancel, myWindow);
344         dialog->setInformativeText(tr("When you run a torrent, its data will be made available to others by means of upload. "
345             "Any content you share is your sole responsibility."));
346         dialog->button(QMessageBox::Ok)->setText(tr("I &Agree"));
347         dialog->setDefaultButton(QMessageBox::Ok);
348         dialog->setModal(true);
349 
350         connect(dialog, SIGNAL(finished(int)), this, SLOT(consentGiven(int)));
351 
352         dialog->setAttribute(Qt::WA_DeleteOnClose);
353         dialog->show();
354     }
355 
356     for (QString const& filename : filenames)
357     {
358         addTorrent(filename);
359     }
360 
361     InteropHelper::registerObject(this);
362 }
363 
loadTranslations()364 void Application::loadTranslations()
365 {
366     QStringList const qtQmDirs = QStringList() << QLibraryInfo::location(QLibraryInfo::TranslationsPath) <<
367 #ifdef TRANSLATIONS_DIR
368         QString::fromUtf8(TRANSLATIONS_DIR) <<
369 #endif
370         (applicationDirPath() + QLatin1String("/translations"));
371 
372     QStringList const appQmDirs = QStringList() <<
373 #ifdef TRANSLATIONS_DIR
374         QString::fromUtf8(TRANSLATIONS_DIR) <<
375 #endif
376         (applicationDirPath() + QLatin1String("/translations"));
377 
378     QString const qtFileName = QLatin1String("qtbase");
379 
380     QLocale const locale;
381     QLocale const englishLocale(QLocale::English, QLocale::UnitedStates);
382 
383     if (loadTranslation(myQtTranslator, qtFileName, locale, qtQmDirs) ||
384         loadTranslation(myQtTranslator, qtFileName, englishLocale, qtQmDirs))
385     {
386         installTranslator(&myQtTranslator);
387     }
388 
389     if (loadTranslation(myAppTranslator, MY_CONFIG_NAME, locale, appQmDirs) ||
390         loadTranslation(myAppTranslator, MY_CONFIG_NAME, englishLocale, appQmDirs))
391     {
392         installTranslator(&myAppTranslator);
393     }
394 }
395 
quitLater()396 void Application::quitLater()
397 {
398     QTimer::singleShot(0, this, SLOT(quit()));
399 }
400 
onTorrentsEdited(torrent_ids_t const & ids)401 void Application::onTorrentsEdited(torrent_ids_t const& ids)
402 {
403     // the backend's tr_info has changed, so reload those fields
404     mySession->initTorrents(ids);
405 }
406 
getNames(torrent_ids_t const & ids) const407 QStringList Application::getNames(torrent_ids_t const& ids) const
408 {
409     QStringList names;
410     for (auto const& id : ids)
411     {
412         names.push_back(myModel->getTorrentFromId(id)->name());
413     }
414 
415     names.sort();
416     return names;
417 }
418 
onTorrentsAdded(torrent_ids_t const & ids)419 void Application::onTorrentsAdded(torrent_ids_t const& ids)
420 {
421     if (myPrefs->getBool(Prefs::SHOW_NOTIFICATION_ON_ADD))
422     {
423         auto const title = tr("Torrent(s) Added", nullptr, ids.size());
424         auto const body = getNames(ids).join(QStringLiteral("\n"));
425         notifyApp(title, body);
426     }
427 }
428 
onTorrentsCompleted(torrent_ids_t const & ids)429 void Application::onTorrentsCompleted(torrent_ids_t const& ids)
430 {
431     if (myPrefs->getBool(Prefs::SHOW_NOTIFICATION_ON_COMPLETE))
432     {
433         auto const title = tr("Torrent Completed", nullptr, ids.size());
434         auto const body = getNames(ids).join(QStringLiteral("\n"));
435         notifyApp(title, body);
436     }
437 
438     if (myPrefs->getBool(Prefs::COMPLETE_SOUND_ENABLED))
439     {
440 #if defined(Q_OS_WIN) || defined(Q_OS_MAC)
441         beep();
442 #else
443         QProcess::execute(myPrefs->getString(Prefs::COMPLETE_SOUND_COMMAND));
444 #endif
445     }
446 }
447 
onTorrentsNeedInfo(torrent_ids_t const & ids)448 void Application::onTorrentsNeedInfo(torrent_ids_t const& ids)
449 {
450     if (!ids.empty())
451     {
452         mySession->initTorrents(ids);
453     }
454 }
455 
456 /***
457 ****
458 ***/
459 
consentGiven(int result)460 void Application::consentGiven(int result)
461 {
462     if (result == QMessageBox::Ok)
463     {
464         myPrefs->set<bool>(Prefs::USER_HAS_GIVEN_INFORMED_CONSENT, true);
465     }
466     else
467     {
468         quit();
469     }
470 }
471 
~Application()472 Application::~Application()
473 {
474     if (myPrefs != nullptr && myWindow != nullptr)
475     {
476         QRect const mainwinRect(myWindow->geometry());
477         myPrefs->set(Prefs::MAIN_WINDOW_HEIGHT, std::max(100, mainwinRect.height()));
478         myPrefs->set(Prefs::MAIN_WINDOW_WIDTH, std::max(100, mainwinRect.width()));
479         myPrefs->set(Prefs::MAIN_WINDOW_X, mainwinRect.x());
480         myPrefs->set(Prefs::MAIN_WINDOW_Y, mainwinRect.y());
481     }
482 
483     delete myWatchDir;
484     delete myWindow;
485     delete myModel;
486     delete mySession;
487     delete myPrefs;
488 }
489 
490 /***
491 ****
492 ***/
493 
refreshPref(int key)494 void Application::refreshPref(int key)
495 {
496     switch (key)
497     {
498     case Prefs::BLOCKLIST_UPDATES_ENABLED:
499         maybeUpdateBlocklist();
500         break;
501 
502     case Prefs::DIR_WATCH:
503     case Prefs::DIR_WATCH_ENABLED:
504         {
505             QString const path(myPrefs->getString(Prefs::DIR_WATCH));
506             bool const isEnabled(myPrefs->getBool(Prefs::DIR_WATCH_ENABLED));
507             myWatchDir->setPath(path, isEnabled);
508             break;
509         }
510 
511     default:
512         break;
513     }
514 }
515 
maybeUpdateBlocklist()516 void Application::maybeUpdateBlocklist()
517 {
518     if (!myPrefs->getBool(Prefs::BLOCKLIST_UPDATES_ENABLED))
519     {
520         return;
521     }
522 
523     QDateTime const lastUpdatedAt = myPrefs->getDateTime(Prefs::BLOCKLIST_DATE);
524     QDateTime const nextUpdateAt = lastUpdatedAt.addDays(7);
525     QDateTime const now = QDateTime::currentDateTime();
526 
527     if (now < nextUpdateAt)
528     {
529         mySession->updateBlocklist();
530         myPrefs->set(Prefs::BLOCKLIST_DATE, now);
531     }
532 }
533 
onSessionSourceChanged()534 void Application::onSessionSourceChanged()
535 {
536     mySession->initTorrents();
537     mySession->refreshSessionStats();
538     mySession->refreshSessionInfo();
539 }
540 
refreshTorrents()541 void Application::refreshTorrents()
542 {
543     // usually we just poll the torrents that have shown recent activity,
544     // but we also periodically ask for updates on the others to ensure
545     // nothing's falling through the cracks.
546     time_t const now = time(nullptr);
547 
548     if (myLastFullUpdateTime + 60 >= now)
549     {
550         mySession->refreshActiveTorrents();
551     }
552     else
553     {
554         myLastFullUpdateTime = now;
555         mySession->refreshAllTorrents();
556     }
557 }
558 
559 /***
560 ****
561 ***/
562 
addTorrent(AddData const & addme)563 void Application::addTorrent(AddData const& addme)
564 {
565     if (addme.type == addme.NONE)
566     {
567         return;
568     }
569 
570     if (!myPrefs->getBool(Prefs::OPTIONS_PROMPT))
571     {
572         mySession->addTorrent(addme);
573     }
574     else
575     {
576         auto o = new OptionsDialog(*mySession, *myPrefs, addme, myWindow);
577         o->show();
578     }
579 
580     raise();
581 }
582 
583 /***
584 ****
585 ***/
586 
raise()587 void Application::raise()
588 {
589     alert(myWindow);
590 }
591 
notifyApp(QString const & title,QString const & body) const592 bool Application::notifyApp(QString const& title, QString const& body) const
593 {
594 #ifdef QT_DBUS_LIB
595 
596     QLatin1String const dbusServiceName("org.freedesktop.Notifications");
597     QLatin1String const dbusInterfaceName("org.freedesktop.Notifications");
598     QLatin1String const dbusPath("/org/freedesktop/Notifications");
599 
600     QDBusConnection bus = QDBusConnection::sessionBus();
601 
602     if (bus.isConnected())
603     {
604         QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QLatin1String("Notify"));
605         QVariantList args;
606         args.append(QLatin1String("Transmission")); // app_name
607         args.append(0U); // replaces_id
608         args.append(QLatin1String("transmission")); // icon
609         args.append(title); // summary
610         args.append(body); // body
611         args.append(QStringList()); // actions - unused for plain passive popups
612         args.append(QVariantMap()); // hints - unused atm
613         args.append(static_cast<int32_t>(-1)); // use the default timeout period
614         m.setArguments(args);
615         QDBusReply<quint32> const replyMsg = bus.call(m);
616 
617         if (replyMsg.isValid() && replyMsg.value() > 0)
618         {
619             return true;
620         }
621     }
622 
623 #endif
624 
625     myWindow->trayIcon().showMessage(title, body);
626     return true;
627 }
628 
faviconCache()629 FaviconCache& Application::faviconCache()
630 {
631     return myFavicons;
632 }
633 
634 /***
635 ****
636 ***/
637 
tr_main(int argc,char * argv[])638 int tr_main(int argc, char* argv[])
639 {
640     InteropHelper::initialize();
641 
642 #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
643     Application::setAttribute(Qt::AA_EnableHighDpiScaling);
644 #endif
645 
646     Application::setAttribute(Qt::AA_UseHighDpiPixmaps);
647 
648     Application app(argc, argv);
649     return app.exec();
650 }
651