1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2006  Christophe Dumez <chris@qbittorrent.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL".  If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
28 
29 #include "misc.h"
30 
31 #include <optional>
32 
33 #ifdef Q_OS_WIN
34 #include <windows.h>
35 #include <powrprof.h>
36 #include <Shlobj.h>
37 #else
38 #include <sys/types.h>
39 #include <unistd.h>
40 #endif
41 
42 #ifdef Q_OS_MACOS
43 #include <Carbon/Carbon.h>
44 #include <CoreServices/CoreServices.h>
45 #endif
46 
47 #include <boost/version.hpp>
48 #include <libtorrent/version.hpp>
49 #include <openssl/crypto.h>
50 #include <openssl/opensslv.h>
51 #include <zlib.h>
52 
53 #include <QCoreApplication>
54 #include <QMimeDatabase>
55 #include <QRegularExpression>
56 #include <QSet>
57 #include <QSysInfo>
58 #include <QVector>
59 
60 #if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) && defined(QT_DBUS_LIB)
61 #include <QDBusInterface>
62 #endif
63 
64 #include "base/types.h"
65 #include "base/unicodestrings.h"
66 #include "base/utils/fs.h"
67 #include "base/utils/string.h"
68 
69 namespace
70 {
71     const struct { const char *source; const char *comment; } units[] =
72     {
73         QT_TRANSLATE_NOOP3("misc", "B", "bytes"),
74         QT_TRANSLATE_NOOP3("misc", "KiB", "kibibytes (1024 bytes)"),
75         QT_TRANSLATE_NOOP3("misc", "MiB", "mebibytes (1024 kibibytes)"),
76         QT_TRANSLATE_NOOP3("misc", "GiB", "gibibytes (1024 mibibytes)"),
77         QT_TRANSLATE_NOOP3("misc", "TiB", "tebibytes (1024 gibibytes)"),
78         QT_TRANSLATE_NOOP3("misc", "PiB", "pebibytes (1024 tebibytes)"),
79         QT_TRANSLATE_NOOP3("misc", "EiB", "exbibytes (1024 pebibytes)")
80     };
81 
82     // return best userfriendly storage unit (B, KiB, MiB, GiB, TiB, ...)
83     // use Binary prefix standards from IEC 60027-2
84     // see http://en.wikipedia.org/wiki/Kilobyte
85     // value must be given in bytes
86     // to send numbers instead of strings with suffixes
87     struct SplitToFriendlyUnitResult
88     {
89         qreal value;
90         Utils::Misc::SizeUnit unit;
91     };
92 
splitToFriendlyUnit(const qint64 bytes)93     std::optional<SplitToFriendlyUnitResult> splitToFriendlyUnit(const qint64 bytes)
94     {
95         if (bytes < 0)
96             return std::nullopt;
97 
98         int i = 0;
99         auto value = static_cast<qreal>(bytes);
100 
101         while ((value >= 1024) && (i < static_cast<int>(Utils::Misc::SizeUnit::ExbiByte)))
102         {
103             value /= 1024;
104             ++i;
105         }
106         return {{value, static_cast<Utils::Misc::SizeUnit>(i)}};
107     }
108 }
109 
shutdownComputer(const ShutdownDialogAction & action)110 void Utils::Misc::shutdownComputer(const ShutdownDialogAction &action)
111 {
112 #if defined(Q_OS_WIN)
113     HANDLE hToken;            // handle to process token
114     TOKEN_PRIVILEGES tkp;     // pointer to token structure
115     if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
116         return;
117     // Get the LUID for shutdown privilege.
118     LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME,
119                          &tkp.Privileges[0].Luid);
120 
121     tkp.PrivilegeCount = 1; // one privilege to set
122     tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
123 
124     // Get shutdown privilege for this process.
125 
126     AdjustTokenPrivileges(hToken, FALSE, &tkp, 0,
127                           (PTOKEN_PRIVILEGES) NULL, 0);
128 
129     // Cannot test the return value of AdjustTokenPrivileges.
130 
131     if (GetLastError() != ERROR_SUCCESS)
132         return;
133 
134     if (action == ShutdownDialogAction::Suspend)
135     {
136         ::SetSuspendState(false, false, false);
137     }
138     else if (action == ShutdownDialogAction::Hibernate)
139     {
140         ::SetSuspendState(true, false, false);
141     }
142     else
143     {
144         const QString msg = QCoreApplication::translate("misc", "qBittorrent will shutdown the computer now because all downloads are complete.");
145         auto msgWchar = std::make_unique<wchar_t[]>(msg.length() + 1);
146         msg.toWCharArray(msgWchar.get());
147         ::InitiateSystemShutdownW(nullptr, msgWchar.get(), 10, true, false);
148     }
149 
150     // Disable shutdown privilege.
151     tkp.Privileges[0].Attributes = 0;
152     AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES) NULL, 0);
153 
154 #elif defined(Q_OS_MACOS)
155     AEEventID EventToSend;
156     if (action != ShutdownDialogAction::Shutdown)
157         EventToSend = kAESleep;
158     else
159         EventToSend = kAEShutDown;
160     AEAddressDesc targetDesc;
161     const ProcessSerialNumber kPSNOfSystemProcess = {0, kSystemProcess};
162     AppleEvent eventReply = {typeNull, NULL};
163     AppleEvent appleEventToSend = {typeNull, NULL};
164 
165     OSStatus error = AECreateDesc(typeProcessSerialNumber, &kPSNOfSystemProcess,
166                                   sizeof(kPSNOfSystemProcess), &targetDesc);
167 
168     if (error != noErr)
169         return;
170 
171     error = AECreateAppleEvent(kCoreEventClass, EventToSend, &targetDesc,
172                                kAutoGenerateReturnID, kAnyTransactionID, &appleEventToSend);
173 
174     AEDisposeDesc(&targetDesc);
175     if (error != noErr)
176         return;
177 
178     error = AESend(&appleEventToSend, &eventReply, kAENoReply,
179                    kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
180 
181     AEDisposeDesc(&appleEventToSend);
182     if (error != noErr)
183         return;
184 
185     AEDisposeDesc(&eventReply);
186 
187 #elif (defined(Q_OS_UNIX) && defined(QT_DBUS_LIB))
188     // Use dbus to power off / suspend the system
189     if (action != ShutdownDialogAction::Shutdown)
190     {
191         // Some recent systems use systemd's logind
192         QDBusInterface login1Iface("org.freedesktop.login1", "/org/freedesktop/login1",
193                                    "org.freedesktop.login1.Manager", QDBusConnection::systemBus());
194         if (login1Iface.isValid())
195         {
196             if (action == ShutdownDialogAction::Suspend)
197                 login1Iface.call("Suspend", false);
198             else
199                 login1Iface.call("Hibernate", false);
200             return;
201         }
202         // Else, other recent systems use UPower
203         QDBusInterface upowerIface("org.freedesktop.UPower", "/org/freedesktop/UPower",
204                                    "org.freedesktop.UPower", QDBusConnection::systemBus());
205         if (upowerIface.isValid())
206         {
207             if (action == ShutdownDialogAction::Suspend)
208                 upowerIface.call("Suspend");
209             else
210                 upowerIface.call("Hibernate");
211             return;
212         }
213         // HAL (older systems)
214         QDBusInterface halIface("org.freedesktop.Hal", "/org/freedesktop/Hal/devices/computer",
215                                 "org.freedesktop.Hal.Device.SystemPowerManagement",
216                                 QDBusConnection::systemBus());
217         if (action == ShutdownDialogAction::Suspend)
218             halIface.call("Suspend", 5);
219         else
220             halIface.call("Hibernate");
221     }
222     else
223     {
224         // Some recent systems use systemd's logind
225         QDBusInterface login1Iface("org.freedesktop.login1", "/org/freedesktop/login1",
226                                    "org.freedesktop.login1.Manager", QDBusConnection::systemBus());
227         if (login1Iface.isValid())
228         {
229             login1Iface.call("PowerOff", false);
230             return;
231         }
232         // Else, other recent systems use ConsoleKit
233         QDBusInterface consolekitIface("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager",
234                                        "org.freedesktop.ConsoleKit.Manager", QDBusConnection::systemBus());
235         if (consolekitIface.isValid())
236         {
237             consolekitIface.call("Stop");
238             return;
239         }
240         // HAL (older systems)
241         QDBusInterface halIface("org.freedesktop.Hal", "/org/freedesktop/Hal/devices/computer",
242                                 "org.freedesktop.Hal.Device.SystemPowerManagement",
243                                 QDBusConnection::systemBus());
244         halIface.call("Shutdown");
245     }
246 
247 #else
248     Q_UNUSED(action);
249 #endif
250 }
251 
unitString(const SizeUnit unit,const bool isSpeed)252 QString Utils::Misc::unitString(const SizeUnit unit, const bool isSpeed)
253 {
254     const auto &unitString = units[static_cast<int>(unit)];
255     QString ret = QCoreApplication::translate("misc", unitString.source, unitString.comment);
256     if (isSpeed)
257         ret += QCoreApplication::translate("misc", "/s", "per second");
258     return ret;
259 }
260 
friendlyUnit(const qint64 bytes,const bool isSpeed)261 QString Utils::Misc::friendlyUnit(const qint64 bytes, const bool isSpeed)
262 {
263     const std::optional<SplitToFriendlyUnitResult> result = splitToFriendlyUnit(bytes);
264     if (!result)
265         return QCoreApplication::translate("misc", "Unknown", "Unknown (size)");
266     return Utils::String::fromDouble(result->value, friendlyUnitPrecision(result->unit))
267            + QString::fromUtf8(C_NON_BREAKING_SPACE)
268            + unitString(result->unit, isSpeed);
269 }
270 
friendlyUnitPrecision(const SizeUnit unit)271 int Utils::Misc::friendlyUnitPrecision(const SizeUnit unit)
272 {
273     // friendlyUnit's number of digits after the decimal point
274     switch (unit)
275     {
276     case SizeUnit::Byte:
277         return 0;
278     case SizeUnit::KibiByte:
279     case SizeUnit::MebiByte:
280         return 1;
281     case SizeUnit::GibiByte:
282         return 2;
283     default:
284         return 3;
285     }
286 }
287 
sizeInBytes(qreal size,const Utils::Misc::SizeUnit unit)288 qlonglong Utils::Misc::sizeInBytes(qreal size, const Utils::Misc::SizeUnit unit)
289 {
290     for (int i = 0; i < static_cast<int>(unit); ++i)
291         size *= 1024;
292     return size;
293 }
294 
isPreviewable(const QString & filename)295 bool Utils::Misc::isPreviewable(const QString &filename)
296 {
297     const QString mime = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension).name();
298 
299     if (mime.startsWith(QLatin1String("audio"), Qt::CaseInsensitive)
300         || mime.startsWith(QLatin1String("video"), Qt::CaseInsensitive))
301     {
302         return true;
303     }
304 
305     const QSet<QString> multimediaExtensions =
306     {
307         "3GP",
308         "AAC",
309         "AC3",
310         "AIF",
311         "AIFC",
312         "AIFF",
313         "ASF",
314         "AU",
315         "AVI",
316         "FLAC",
317         "FLV",
318         "M3U",
319         "M4A",
320         "M4P",
321         "M4V",
322         "MID",
323         "MKV",
324         "MOV",
325         "MP2",
326         "MP3",
327         "MP4",
328         "MPC",
329         "MPE",
330         "MPEG",
331         "MPG",
332         "MPP",
333         "OGG",
334         "OGM",
335         "OGV",
336         "QT",
337         "RA",
338         "RAM",
339         "RM",
340         "RMV",
341         "RMVB",
342         "SWA",
343         "SWF",
344         "TS",
345         "VOB",
346         "WAV",
347         "WMA",
348         "WMV"
349     };
350     return multimediaExtensions.contains(Utils::Fs::fileExtension(filename).toUpper());
351 }
352 
userFriendlyDuration(const qlonglong seconds,const qlonglong maxCap)353 QString Utils::Misc::userFriendlyDuration(const qlonglong seconds, const qlonglong maxCap)
354 {
355     if (seconds < 0)
356         return QString::fromUtf8(C_INFINITY);
357     if ((maxCap >= 0) && (seconds >= maxCap))
358         return QString::fromUtf8(C_INFINITY);
359 
360     if (seconds == 0)
361         return "0";
362 
363     if (seconds < 60)
364         return QCoreApplication::translate("misc", "< 1m", "< 1 minute");
365 
366     qlonglong minutes = (seconds / 60);
367     if (minutes < 60)
368         return QCoreApplication::translate("misc", "%1m", "e.g: 10minutes").arg(QString::number(minutes));
369 
370     qlonglong hours = (minutes / 60);
371     if (hours < 24)
372     {
373         minutes -= (hours * 60);
374         return QCoreApplication::translate("misc", "%1h %2m", "e.g: 3hours 5minutes").arg(QString::number(hours), QString::number(minutes));
375     }
376 
377     qlonglong days = (hours / 24);
378     if (days < 365)
379     {
380         hours -= (days * 24);
381         return QCoreApplication::translate("misc", "%1d %2h", "e.g: 2days 10hours").arg(QString::number(days), QString::number(hours));
382     }
383 
384     qlonglong years = (days / 365);
385     days -= (years * 365);
386     return QCoreApplication::translate("misc", "%1y %2d", "e.g: 2years 10days").arg(QString::number(years), QString::number(days));
387 }
388 
getUserIDString()389 QString Utils::Misc::getUserIDString()
390 {
391     QString uid = "0";
392 #ifdef Q_OS_WIN
393     const int UNLEN = 256;
394     WCHAR buffer[UNLEN + 1] = {0};
395     DWORD buffer_len = sizeof(buffer) / sizeof(*buffer);
396     if (GetUserNameW(buffer, &buffer_len))
397         uid = QString::fromWCharArray(buffer);
398 #else
399     uid = QString::number(getuid());
400 #endif
401     return uid;
402 }
403 
parseHtmlLinks(const QString & rawText)404 QString Utils::Misc::parseHtmlLinks(const QString &rawText)
405 {
406     QString result = rawText;
407     static const QRegularExpression reURL(
408         "(\\s|^)"                                             // start with whitespace or beginning of line
409         "("
410         "("                                              // case 1 -- URL with scheme
411         "(http(s?))\\://"                            // start with scheme
412         "([a-zA-Z0-9_-]+\\.)+"                       //  domainpart.  at least one of these must exist
413         "([a-zA-Z0-9\\?%=&/_\\.:#;-]+)"              //  everything to 1st non-URI char, must be at least one char after the previous dot (cannot use ".*" because it can be too greedy)
414         ")"
415         "|"
416         "("                                             // case 2a -- no scheme, contains common TLD  example.com
417         "([a-zA-Z0-9_-]+\\.)+"                      //  domainpart.  at least one of these must exist
418         "(?="                                       //  must be followed by TLD
419         "AERO|aero|"                          // N.B. assertions are non-capturing
420         "ARPA|arpa|"
421         "ASIA|asia|"
422         "BIZ|biz|"
423         "CAT|cat|"
424         "COM|com|"
425         "COOP|coop|"
426         "EDU|edu|"
427         "GOV|gov|"
428         "INFO|info|"
429         "INT|int|"
430         "JOBS|jobs|"
431         "MIL|mil|"
432         "MOBI|mobi|"
433         "MUSEUM|museum|"
434         "NAME|name|"
435         "NET|net|"
436         "ORG|org|"
437         "PRO|pro|"
438         "RO|ro|"
439         "RU|ru|"
440         "TEL|tel|"
441         "TRAVEL|travel"
442         ")"
443         "([a-zA-Z0-9\\?%=&/_\\.:#;-]+)"             //  everything to 1st non-URI char, must be at least one char after the previous dot (cannot use ".*" because it can be too greedy)
444         ")"
445         "|"
446         "("                                             // case 2b no scheme, no TLD, must have at least 2 alphanum strings plus uncommon TLD string  --> del.icio.us
447         "([a-zA-Z0-9_-]+\\.) {2,}"                   // 2 or more domainpart.   --> del.icio.
448         "[a-zA-Z]{2,}"                              // one ab  (2 char or longer) --> us
449         "([a-zA-Z0-9\\?%=&/_\\.:#;-]*)"             // everything to 1st non-URI char, maybe nothing  in case of del.icio.us/path
450         ")"
451         ")"
452         );
453 
454     // Capture links
455     result.replace(reURL, "\\1<a href=\"\\2\">\\2</a>");
456 
457     // Capture links without scheme
458     static const QRegularExpression reNoScheme("<a\\s+href=\"(?!https?)([a-zA-Z0-9\\?%=&/_\\.-:#]+)\\s*\">");
459     result.replace(reNoScheme, "<a href=\"http://\\1\">");
460 
461     // to preserve plain text formatting
462     result = "<p style=\"white-space: pre-wrap;\">" + result + "</p>";
463     return result;
464 }
465 
osName()466 QString Utils::Misc::osName()
467 {
468     // static initialization for usage in signal handler
469     static const QString name =
470         QString("%1 %2 %3")
471         .arg(QSysInfo::prettyProductName()
472             , QSysInfo::kernelVersion()
473             , QSysInfo::currentCpuArchitecture());
474     return name;
475 }
476 
boostVersionString()477 QString Utils::Misc::boostVersionString()
478 {
479     // static initialization for usage in signal handler
480     static const QString ver = QString("%1.%2.%3")
481         .arg(QString::number(BOOST_VERSION / 100000)
482             , QString::number((BOOST_VERSION / 100) % 1000)
483             , QString::number(BOOST_VERSION % 100));
484     return ver;
485 }
486 
libtorrentVersionString()487 QString Utils::Misc::libtorrentVersionString()
488 {
489     // static initialization for usage in signal handler
490     static const auto version {QString::fromLatin1(lt::version())};
491     return version;
492 }
493 
opensslVersionString()494 QString Utils::Misc::opensslVersionString()
495 {
496 #if (OPENSSL_VERSION_NUMBER >= 0x1010000f)
497     static const auto version {QString::fromLatin1(OpenSSL_version(OPENSSL_VERSION))};
498 #else
499     static const auto version {QString::fromLatin1(SSLeay_version(SSLEAY_VERSION))};
500 #endif
501     return version.splitRef(' ', QString::SkipEmptyParts)[1].toString();
502 }
503 
zlibVersionString()504 QString Utils::Misc::zlibVersionString()
505 {
506     // static initialization for usage in signal handler
507     static const auto version {QString::fromLatin1(zlibVersion())};
508     return version;
509 }
510 
511 #ifdef Q_OS_WIN
windowsSystemPath()512 QString Utils::Misc::windowsSystemPath()
513 {
514     static const QString path = []() -> QString
515     {
516         WCHAR systemPath[MAX_PATH] = {0};
517         GetSystemDirectoryW(systemPath, sizeof(systemPath) / sizeof(WCHAR));
518         return QString::fromWCharArray(systemPath);
519     }();
520     return path;
521 }
522 #endif
523