/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/linux/specific_linux.h" #include "platform/linux/linux_libs.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "mainwindow.h" #include "platform/linux/linux_desktop_environment.h" #include "platform/linux/file_utilities_linux.h" #include "platform/platform_notifications_manager.h" #include "storage/localstorage.h" #include "core/crash_reports.h" #include "core/update_checker.h" #include #include #include #include #include #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION #include #include #include #include #include #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION #include #include #include #include #include #include #include using namespace Platform; using Platform::File::internal::EscapeShell; namespace Platform { namespace { constexpr auto kDesktopFile = ":/misc/telegramdesktop.desktop"_cs; constexpr auto kSnapLauncherDir = "/var/lib/snapd/desktop/applications/"_cs; constexpr auto kIconName = "telegram"_cs; #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION void SandboxAutostart(bool autostart, bool silent = false) { QVariantMap options; options["reason"] = tr::lng_settings_auto_start(tr::now); options["autostart"] = autostart; options["commandline"] = QStringList({ cExeName(), qsl("-autostart") }); options["dbus-activatable"] = false; auto message = QDBusMessage::createMethodCall( qsl("org.freedesktop.portal.Desktop"), qsl("/org/freedesktop/portal/desktop"), qsl("org.freedesktop.portal.Background"), qsl("RequestBackground")); message.setArguments({ QString(), options }); if (silent) { QDBusConnection::sessionBus().send(message); } else { const QDBusReply reply = QDBusConnection::sessionBus().call( message); if (!reply.isValid()) { LOG(("Flatpak autostart error: %1").arg(reply.error().message())); } } } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION bool RunShellCommand(const QByteArray &command) { auto result = system(command.constData()); if (result) { DEBUG_LOG(("App Error: command failed, code: %1, command (in utf8): %2").arg(result).arg(command.constData())); return false; } DEBUG_LOG(("App Info: command succeeded, command (in utf8): %1").arg(command.constData())); return true; } [[nodiscard]] bool CheckFontConfigCrash() { return InSnap(); } [[nodiscard]] QString FallbackFontConfigCheckPath() { return cWorkingDir() + "tdata/fc-check"; } #ifdef TDESKTOP_USE_FONTCONFIG_FALLBACK [[nodiscard]] bool BadFontConfigVersion() { if (CheckFontConfigCrash()) { return QFile(FallbackFontConfigCheckPath()).exists(); } QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); process.start("fc-list", QStringList() << "--version"); process.waitForFinished(); if (process.exitCode() > 0) { LOG(("App Error: Could not start fc-list. Process exited with code: %1.").arg(process.exitCode())); return false; } QString result(process.readAllStandardOutput()); DEBUG_LOG(("Fontconfig version string: ") + result); QVersionNumber version = QVersionNumber::fromString(result.split("version ").last()); if (version.isNull()) { LOG(("App Error: Could not get version from fc-list output.")); return false; } LOG(("Fontconfig version: %1.").arg(version.toString())); if (version < QVersionNumber::fromString("2.13")) { if (!qEnvironmentVariableIsSet("TDESKTOP_FORCE_CUSTOM_FONTCONFIG")) { return false; } } return true; } void FallbackFontConfig() { const auto custom = cWorkingDir() + "tdata/fc-custom-1.conf"; auto doFallback = [&] { if (QFile(custom).exists()) { LOG(("Custom FONTCONFIG_FILE: ") + custom); qputenv("FONTCONFIG_FILE", QFile::encodeName(custom)); return true; } return false; }; if (doFallback()) { return; } if (BadFontConfigVersion()) { QFile(":/fc/fc-custom.conf").copy(custom); doFallback(); } } #endif // TDESKTOP_USE_FONTCONFIG_FALLBACK bool GenerateDesktopFile( const QString &targetPath, const QString &args, bool silent = false) { DEBUG_LOG(("App Info: placing .desktop file to %1").arg(targetPath)); if (!QDir(targetPath).exists()) QDir().mkpath(targetPath); const auto sourceFile = [&] { if (InSnap()) { return kSnapLauncherDir.utf16() + GetLauncherFilename(); } else { return kDesktopFile.utf16(); } }(); const auto targetFile = targetPath + GetLauncherFilename(); QString fileText; QFile source(sourceFile); if (source.open(QIODevice::ReadOnly)) { QTextStream s(&source); fileText = s.readAll(); source.close(); } else { if (!silent) { LOG(("App Error: Could not open '%1' for read").arg(sourceFile)); } return false; } QFile target(targetFile); if (target.open(QIODevice::WriteOnly)) { if (IsStaticBinary() || InAppImage()) { fileText = fileText.replace( QRegularExpression( qsl("^TryExec=.*$"), QRegularExpression::MultilineOption), qsl("TryExec=") + QFile::encodeName(cExeDir() + cExeName()) .replace('\\', qsl("\\\\"))); fileText = fileText.replace( QRegularExpression( qsl("^Exec=.*$"), QRegularExpression::MultilineOption), qsl("Exec=") + EscapeShell(QFile::encodeName(cExeDir() + cExeName())) .replace('\\', qsl("\\\\")) + (args.isEmpty() ? QString() : ' ' + args)); } else { fileText = fileText.replace( QRegularExpression( qsl("^Exec=(.*) -- %u$"), QRegularExpression::MultilineOption), qsl("Exec=\\1") + (args.isEmpty() ? QString() : ' ' + args)); } target.write(fileText.toUtf8()); target.close(); DEBUG_LOG(("App Info: removing old .desktop file")); QFile(qsl("%1telegram.desktop").arg(targetPath)).remove(); return true; } else { if (!silent) { LOG(("App Error: Could not open '%1' for write").arg(targetFile)); } return false; } } } // namespace void SetApplicationIcon(const QIcon &icon) { QApplication::setWindowIcon(icon); } bool InSandbox() { static const auto Sandbox = QFileInfo::exists(qsl("/.flatpak-info")); return Sandbox; } bool InSnap() { static const auto Snap = qEnvironmentVariableIsSet("SNAP"); return Snap; } bool InAppImage() { static const auto AppImage = qEnvironmentVariableIsSet("APPIMAGE"); return AppImage; } bool IsStaticBinary() { #ifdef DESKTOP_APP_USE_PACKAGED return false; #else // DESKTOP_APP_USE_PACKAGED return true; #endif // !DESKTOP_APP_USE_PACKAGED } bool IsGtkFileDialogForced() { #ifdef TDESKTOP_FORCE_GTK_FILE_DIALOG return true; #else // TDESKTOP_FORCE_GTK_FILE_DIALOG return false; #endif // !TDESKTOP_FORCE_GTK_FILE_DIALOG } bool IsQtPluginsBundled() { #ifdef DESKTOP_APP_USE_PACKAGED_LAZY return true; #else // DESKTOP_APP_USE_PACKAGED_LAZY return false; #endif // !DESKTOP_APP_USE_PACKAGED_LAZY } bool IsXDGDesktopPortalPresent() { #ifdef TDESKTOP_DISABLE_DBUS_INTEGRATION static const auto XDGDesktopPortalPresent = false; #else // TDESKTOP_DISABLE_DBUS_INTEGRATION static const auto XDGDesktopPortalPresent = QDBusInterface( "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop").isValid(); #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION return XDGDesktopPortalPresent; } bool UseXDGDesktopPortal() { static const auto UsePortal = [&] { const auto envVar = qEnvironmentVariableIsSet("TDESKTOP_USE_PORTAL"); const auto portalPresent = IsXDGDesktopPortalPresent(); return ( DesktopEnvironment::IsKDE() || InSnap() || envVar ) && portalPresent; }(); return UsePortal; } QString ProcessNameByPID(const QString &pid) { constexpr auto kMaxPath = 1024; char result[kMaxPath] = { 0 }; auto count = readlink("/proc/" + pid.toLatin1() + "/exe", result, kMaxPath); if (count > 0) { auto filename = QFile::decodeName(result); auto deletedPostfix = qstr(" (deleted)"); if (filename.endsWith(deletedPostfix) && !QFileInfo(filename).exists()) { filename.chop(deletedPostfix.size()); } return filename; } return QString(); } QString RealExecutablePath(int argc, char *argv[]) { const auto processName = ProcessNameByPID(qsl("self")); // Fallback to the first command line argument. return !processName.isEmpty() ? processName : argc ? QFile::decodeName(argv[0]) : QString(); } QString CurrentExecutablePath(int argc, char *argv[]) { if (InAppImage()) { const auto appimagePath = QString::fromUtf8(qgetenv("APPIMAGE")); const auto appimagePathList = appimagePath.split('/'); if (qEnvironmentVariableIsSet("ARGV0") && appimagePathList.size() >= 5 && appimagePathList[1] == qstr("run") && appimagePathList[2] == qstr("user") && appimagePathList[4] == qstr("appimagelauncherfs")) { return QString::fromUtf8(qgetenv("ARGV0")); } return appimagePath; } return RealExecutablePath(argc, argv); } QString AppRuntimeDirectory() { static const auto RuntimeDirectory = [&] { auto runtimeDir = QStandardPaths::writableLocation( QStandardPaths::RuntimeLocation); if (InSandbox()) { const auto flatpakId = [&] { if (!qEnvironmentVariableIsEmpty("FLATPAK_ID")) { return QString::fromLatin1(qgetenv("FLATPAK_ID")); } else { return GetLauncherBasename(); } }(); runtimeDir += qsl("/app/") + flatpakId; } if (!QFileInfo::exists(runtimeDir)) { // non-systemd distros runtimeDir = QDir::tempPath(); } if (runtimeDir.isEmpty()) { runtimeDir = qsl("/tmp/"); } if (!runtimeDir.endsWith('/')) { runtimeDir += '/'; } return runtimeDir; }(); return RuntimeDirectory; } QString SingleInstanceLocalServerName(const QString &hash) { if (InSandbox() || InSnap()) { return AppRuntimeDirectory() + hash; } else { return AppRuntimeDirectory() + hash + '-' + cGUIDStr(); } } QString GetLauncherBasename() { static const auto LauncherBasename = [&] { if (InSnap()) { const auto snapNameKey = qEnvironmentVariableIsSet("SNAP_INSTANCE_NAME") ? "SNAP_INSTANCE_NAME" : "SNAP_NAME"; return qsl("%1_%2") .arg(QString::fromLatin1(qgetenv(snapNameKey))) .arg(cExeName()); } if (InAppImage()) { const auto appimagePath = qsl("file://%1%2") .arg(cExeDir()) .arg(cExeName()) .toUtf8(); char md5Hash[33] = { 0 }; hashMd5Hex(appimagePath.constData(), appimagePath.size(), md5Hash); return qsl("appimagekit_%1-%2") .arg(md5Hash) .arg(AppName.utf16().replace(' ', '_')); } const auto possibleBasenames = std::vector{ qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME)), qsl("Telegram") }; for (const auto &it : possibleBasenames) { if (!QStandardPaths::locate( QStandardPaths::ApplicationsLocation, it + qsl(".desktop")).isEmpty()) { return it; } } return possibleBasenames[0]; }(); return LauncherBasename; } QString GetLauncherFilename() { static const auto LauncherFilename = GetLauncherBasename() + qsl(".desktop"); return LauncherFilename; } QString GetIconName() { static const auto IconName = InSandbox() ? GetLauncherBasename() : kIconName.utf16(); return IconName; } std::optional LastUserInputTime() { // TODO: a fallback pure-X11 implementation, this one covers only major DEs on X11 and Wayland // an example: https://stackoverflow.com/q/9049087 #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION static auto NotSupported = false; if (NotSupported) { return std::nullopt; } static const auto message = QDBusMessage::createMethodCall( qsl("org.freedesktop.ScreenSaver"), qsl("/org/freedesktop/ScreenSaver"), qsl("org.freedesktop.ScreenSaver"), qsl("GetSessionIdleTime")); const QDBusReply reply = QDBusConnection::sessionBus().call( message); const auto notSupportedErrors = { QDBusError::ServiceUnknown, QDBusError::NotSupported, }; if (reply.isValid()) { return (crl::now() - static_cast(reply.value())); } else if (ranges::contains(notSupportedErrors, reply.error().type())) { NotSupported = true; } else { if (reply.error().type() == QDBusError::AccessDenied) { NotSupported = true; } LOG(("Unable to get last user input time: %1: %2") .arg(reply.error().name()) .arg(reply.error().message())); } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION return std::nullopt; } void FallbackFontConfigCheckBegin() { if (!CheckFontConfigCrash()) { return; } auto file = QFile(FallbackFontConfigCheckPath()); if (file.open(QIODevice::WriteOnly)) { file.write("1", 1); } } void FallbackFontConfigCheckEnd() { if (!CheckFontConfigCrash()) { return; } QFile(FallbackFontConfigCheckPath()).remove(); } } // namespace Platform namespace { QRect _monitorRect; auto _monitorLastGot = 0LL; } // namespace QRect psDesktopRect() { auto tnow = crl::now(); if (tnow > _monitorLastGot + 1000LL || tnow < _monitorLastGot) { _monitorLastGot = tnow; _monitorRect = QApplication::desktop()->availableGeometry(App::wnd()); } return _monitorRect; } void psWriteDump() { } bool _removeDirectory(const QString &path) { // from http://stackoverflow.com/questions/2256945/removing-a-non-empty-directory-programmatically-in-c-or-c QByteArray pathRaw = QFile::encodeName(path); DIR *d = opendir(pathRaw.constData()); if (!d) return false; while (struct dirent *p = readdir(d)) { /* Skip the names "." and ".." as we don't want to recurse on them. */ if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, "..")) continue; QString fname = path + '/' + p->d_name; QByteArray fnameRaw = QFile::encodeName(fname); struct stat statbuf; if (!stat(fnameRaw.constData(), &statbuf)) { if (S_ISDIR(statbuf.st_mode)) { if (!_removeDirectory(fname)) { closedir(d); return false; } } else { if (unlink(fnameRaw.constData())) { closedir(d); return false; } } } } closedir(d); return !rmdir(pathRaw.constData()); } void psDeleteDir(const QString &dir) { _removeDirectory(dir); } void psActivateProcess(uint64 pid) { // objc_activateProgram(); } namespace { QString getHomeDir() { auto home = QDir::homePath(); if (home != QDir::rootPath()) return home + '/'; struct passwd *pw = getpwuid(getuid()); return (pw && pw->pw_dir && strlen(pw->pw_dir)) ? (QFile::decodeName(pw->pw_dir) + '/') : QString(); } } // namespace QString psAppDataPath() { // Previously we used ~/.TelegramDesktop, so look there first. // If we find data there, we should still use it. auto home = getHomeDir(); if (!home.isEmpty()) { auto oldPath = home + qsl(".TelegramDesktop/"); auto oldSettingsBase = oldPath + qsl("tdata/settings"); if (QFile(oldSettingsBase + '0').exists() || QFile(oldSettingsBase + '1').exists() || QFile(oldSettingsBase + 's').exists()) { return oldPath; } } return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/'; } void psDoCleanup() { try { psAutoStart(false, true); psSendToMenu(false, true); } catch (...) { } } int psCleanup() { psDoCleanup(); return 0; } void psDoFixPrevious() { } int psFixPrevious() { psDoFixPrevious(); return 0; } namespace Platform { void start() { LOG(("Launcher filename: %1").arg(GetLauncherFilename())); #ifdef TDESKTOP_USE_FONTCONFIG_FALLBACK FallbackFontConfig(); #endif // TDESKTOP_USE_FONTCONFIG_FALLBACK qputenv("PULSE_PROP_application.name", AppName.utf8()); qputenv("PULSE_PROP_application.icon_name", GetIconName().toLatin1()); if(IsStaticBinary() || InAppImage() || InSandbox() || InSnap() || IsQtPluginsBundled()) { qputenv("QT_WAYLAND_DECORATION", "material"); } if(IsStaticBinary() || InAppImage() || InSnap() || IsGtkFileDialogForced() || IsQtPluginsBundled()) { LOG(("Checking for XDG Desktop Portal...")); // this can give us a chance to use a proper file dialog for current session if (IsXDGDesktopPortalPresent()) { LOG(("XDG Desktop Portal is present!")); if (UseXDGDesktopPortal()) { LOG(("Usage of XDG Desktop Portal is enabled.")); qputenv("QT_QPA_PLATFORMTHEME", "xdgdesktopportal"); } else { LOG(("Usage of XDG Desktop Portal is disabled.")); } } else { LOG(("XDG Desktop Portal is not present :(")); } } } void finish() { } void RegisterCustomScheme(bool force) { #ifndef TDESKTOP_DISABLE_REGISTER_CUSTOM_SCHEME const auto home = getHomeDir(); if (home.isEmpty() || cExeName().isEmpty()) return; static const auto disabledByEnv = qEnvironmentVariableIsSet( "TDESKTOP_DISABLE_DESKTOP_FILE_GENERATION"); // don't update desktop file for alpha version or if updater is disabled if ((cAlphaVersion() || Core::UpdaterDisabled() || disabledByEnv) && !force) return; const auto applicationsPath = QStandardPaths::writableLocation( QStandardPaths::ApplicationsLocation) + '/'; GenerateDesktopFile(applicationsPath, qsl("-- %u")); const auto icons = QStandardPaths::writableLocation( QStandardPaths::GenericDataLocation) + qsl("/icons/"); if (!QDir(icons).exists()) QDir().mkpath(icons); const auto icon = icons + qsl("telegram.png"); auto iconExists = QFile(icon).exists(); if (Local::oldSettingsVersion() < 10021 && iconExists) { // Icon was changed. if (QFile(icon).remove()) { iconExists = false; } } if (!iconExists) { if (QFile(qsl(":/gui/art/logo_256.png")).copy(icon)) { DEBUG_LOG(("App Info: Icon copied to '%1'").arg(icon)); } } RunShellCommand("update-desktop-database " + EscapeShell(QFile::encodeName(applicationsPath))); RunShellCommand("xdg-mime default " + GetLauncherFilename().toLatin1() + " x-scheme-handler/tg"); #endif // !TDESKTOP_DISABLE_REGISTER_CUSTOM_SCHEME } PermissionStatus GetPermissionStatus(PermissionType type) { return PermissionStatus::Granted; } void RequestPermission(PermissionType type, Fn resultCallback) { resultCallback(PermissionStatus::Granted); } void OpenSystemSettingsForPermission(PermissionType type) { } bool OpenSystemSettings(SystemSettingsType type) { if (type == SystemSettingsType::Audio) { auto options = std::vector(); const auto add = [&](const char *option) { options.emplace_back(option); }; if (DesktopEnvironment::IsUnity()) { add("unity-control-center sound"); } else if (DesktopEnvironment::IsKDE()) { add("kcmshell5 kcm_pulseaudio"); add("kcmshell4 phonon"); } else if (DesktopEnvironment::IsGnome()) { add("gnome-control-center sound"); } else if (DesktopEnvironment::IsMATE()) { add("mate-volume-control"); } add("pavucontrol-qt"); add("pavucontrol"); add("alsamixergui"); return ranges::find_if(options, [](const QString &command) { return QProcess::startDetached(command); }) != end(options); } return true; } namespace ThirdParty { void start() { Libs::start(); MainWindow::LibsLoaded(); } void finish() { } } // namespace ThirdParty } // namespace Platform void psNewVersion() { Platform::RegisterCustomScheme(); } bool psShowOpenWithMenu(int x, int y, const QString &file) { return false; } void psAutoStart(bool start, bool silent) { const auto home = getHomeDir(); if (home.isEmpty() || cExeName().isEmpty()) return; if (InSandbox()) { #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION SandboxAutostart(start, silent); #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION } else { const auto autostart = [&] { if (InSnap()) { QDir realHomeDir(home); realHomeDir.cd(qsl("../../..")); return realHomeDir .absoluteFilePath(qsl(".config/autostart/")); } else { return QStandardPaths::writableLocation( QStandardPaths::GenericConfigLocation) + qsl("/autostart/"); } }(); if (start) { GenerateDesktopFile(autostart, qsl("-autostart"), silent); } else { QFile::remove(autostart + GetLauncherFilename()); } } } void psSendToMenu(bool send, bool silent) { } bool linuxMoveFile(const char *from, const char *to) { FILE *ffrom = fopen(from, "rb"), *fto = fopen(to, "wb"); if (!ffrom) { if (fto) fclose(fto); return false; } if (!fto) { fclose(ffrom); return false; } static const int BufSize = 65536; char buf[BufSize]; while (size_t size = fread(buf, 1, BufSize, ffrom)) { fwrite(buf, 1, size, fto); } struct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c //let's say this wont fail since you already worked OK on that fp if (fstat(fileno(ffrom), &fst) != 0) { fclose(ffrom); fclose(fto); return false; } //update to the same uid/gid if (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) { fclose(ffrom); fclose(fto); return false; } //update the permissions if (fchmod(fileno(fto), fst.st_mode) != 0) { fclose(ffrom); fclose(fto); return false; } fclose(ffrom); fclose(fto); if (unlink(from)) { return false; } return true; } bool psLaunchMaps(const Data::LocationPoint &point) { return false; }