| 1 | /* |
| 2 | SPDX-FileCopyrightText: 1997 Torben Weis <weis@stud.uni-frankfurt.de> |
| 3 | SPDX-FileCopyrightText: 1999 Dirk Mueller <mueller@kde.org> |
| 4 | Portions SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org> |
| 5 | SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org> |
| 6 | SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org> |
| 7 | |
| 8 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 9 | */ |
| 10 | |
| 11 | #include "openwith.h" |
| 12 | |
| 13 | #include <QFileInfo> |
| 14 | #include <QStandardPaths> |
| 15 | |
| 16 | #include <KConfigGroup> |
| 17 | #include <KDesktopFile> |
| 18 | #include <KLocalizedString> |
| 19 | #include <KSharedConfig> |
| 20 | |
| 21 | #include "desktopexecparser.h" |
| 22 | #include "kiocoredebug.h" |
| 23 | |
| 24 | namespace |
| 25 | { |
| 26 | |
| 27 | QString simplifiedExecLineFromService(const QString &fullExec) |
| 28 | { |
| 29 | QString exec = fullExec; |
| 30 | exec.remove(s: QLatin1String("%u" ), cs: Qt::CaseInsensitive); |
| 31 | exec.remove(s: QLatin1String("%f" ), cs: Qt::CaseInsensitive); |
| 32 | exec.remove(s: QLatin1String("-caption %c" )); |
| 33 | exec.remove(s: QLatin1String("-caption \"%c\"" )); |
| 34 | exec.remove(s: QLatin1String("%i" )); |
| 35 | exec.remove(s: QLatin1String("%m" )); |
| 36 | return exec.simplified(); |
| 37 | } |
| 38 | |
| 39 | void addToMimeAppsList(const QString &serviceId /*menu id or storage id*/, const QString &qMimeType) |
| 40 | { |
| 41 | KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list" ), mode: KConfig::NoGlobals, type: QStandardPaths::GenericConfigLocation); |
| 42 | |
| 43 | // Save the default application according to mime-apps-spec 1.0 |
| 44 | KConfigGroup defaultApp(profile, QStringLiteral("Default Applications" )); |
| 45 | defaultApp.writeXdgListEntry(pKey: qMimeType, value: QStringList(serviceId)); |
| 46 | |
| 47 | KConfigGroup addedApps(profile, QStringLiteral("Added Associations" )); |
| 48 | QStringList apps = addedApps.readXdgListEntry(pKey: qMimeType); |
| 49 | apps.removeAll(t: serviceId); |
| 50 | apps.prepend(t: serviceId); // make it the preferred app |
| 51 | addedApps.writeXdgListEntry(pKey: qMimeType, value: apps); |
| 52 | |
| 53 | profile->sync(); |
| 54 | |
| 55 | // Also make sure the "auto embed" setting for this MIME type is off |
| 56 | KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc" ), mode: KConfig::NoGlobals); |
| 57 | fileTypesConfig->group(QStringLiteral("EmbedSettings" )).writeEntry(QStringLiteral("embed-" ) + qMimeType, value: false); |
| 58 | fileTypesConfig->sync(); |
| 59 | } |
| 60 | |
| 61 | } // namespace |
| 62 | |
| 63 | namespace KIO |
| 64 | { |
| 65 | |
| 66 | OpenWith::AcceptResult OpenWith::accept(KService::Ptr &service, |
| 67 | const QString &typedExec, |
| 68 | bool remember, |
| 69 | const QString &mimeType, |
| 70 | bool openInTerminal, |
| 71 | bool lingerTerminal, |
| 72 | bool saveNewApps) |
| 73 | { |
| 74 | QString fullExec(typedExec); |
| 75 | |
| 76 | KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General" )); |
| 77 | const QString preferredTerminal = confGroup.readPathEntry(key: "TerminalApplication" , QStringLiteral("konsole" )); |
| 78 | |
| 79 | QString serviceName; |
| 80 | QString initialServiceName; |
| 81 | QString configPath; |
| 82 | QString serviceExec; |
| 83 | bool rebuildSycoca = false; |
| 84 | if (!service) { |
| 85 | // No service selected - check the command line |
| 86 | |
| 87 | // Find out the name of the service from the command line, removing args and paths |
| 88 | serviceName = KIO::DesktopExecParser::executableName(execLine: typedExec); |
| 89 | if (serviceName.isEmpty()) { |
| 90 | return {.accept = false, .error = i18n("Could not extract executable name from '%1', please type a valid program name." , serviceName)}; |
| 91 | } |
| 92 | initialServiceName = serviceName; |
| 93 | // Also remember the executableName with a path, if any, for the |
| 94 | // check that the executable exists. |
| 95 | qCDebug(KIO_CORE) << "initialServiceName=" << initialServiceName; |
| 96 | int i = 1; // We have app, app-2, app-3... Looks better for the user. |
| 97 | bool ok = false; |
| 98 | // Check if there's already a service by that name, with the same Exec line |
| 99 | do { |
| 100 | qCDebug(KIO_CORE) << "looking for service" << serviceName; |
| 101 | KService::Ptr serv = KService::serviceByDesktopName(name: serviceName); |
| 102 | ok = !serv; // ok if no such service yet |
| 103 | // also ok if we find the exact same service (well, "kwrite" == "kwrite %U") |
| 104 | if (serv && !serv->noDisplay() /* #297720 */) { |
| 105 | if (serv->isApplication()) { |
| 106 | qCDebug(KIO_CORE) << "typedExec=" << typedExec << "serv->exec=" << serv->exec() |
| 107 | << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec); |
| 108 | serviceExec = simplifiedExecLineFromService(fullExec: serv->exec()); |
| 109 | if (typedExec == serviceExec) { |
| 110 | ok = true; |
| 111 | service = serv; |
| 112 | qCDebug(KIO_CORE) << "OK, found identical service: " << serv->entryPath(); |
| 113 | } else { |
| 114 | qCDebug(KIO_CORE) << "Exec line differs, service says:" << serviceExec; |
| 115 | configPath = serv->entryPath(); |
| 116 | serviceExec = serv->exec(); |
| 117 | } |
| 118 | } else { |
| 119 | qCDebug(KIO_CORE) << "Found, but not an application:" << serv->entryPath(); |
| 120 | } |
| 121 | } |
| 122 | if (!ok) { // service was found, but it was different -> keep looking |
| 123 | ++i; |
| 124 | serviceName = initialServiceName + QLatin1Char('-') + QString::number(i); |
| 125 | } |
| 126 | } while (!ok); |
| 127 | } |
| 128 | if (service) { |
| 129 | // Existing service selected |
| 130 | serviceName = service->name(); |
| 131 | initialServiceName = serviceName; |
| 132 | fullExec = service->exec(); |
| 133 | } else { |
| 134 | const QString binaryName = KIO::DesktopExecParser::executablePath(execLine: typedExec); |
| 135 | qCDebug(KIO_CORE) << "binaryName=" << binaryName; |
| 136 | // Ensure that the typed binary name actually exists (#81190) |
| 137 | if (QStandardPaths::findExecutable(executableName: binaryName).isEmpty()) { |
| 138 | // QStandardPaths::findExecutable does not find non-executable files. |
| 139 | // Give a better error message for the case of a existing but non-executable file. |
| 140 | // https://bugs.kde.org/show_bug.cgi?id=437880 |
| 141 | const QString msg = QFileInfo::exists(file: binaryName) |
| 142 | ? xi18nc("@info" , "<filename>%1</filename> does not appear to be an executable program." , binaryName) |
| 143 | : xi18nc("@info" , "<filename>%1</filename> was not found; please enter a valid path to an executable program." , binaryName); |
| 144 | return {.accept = false, .error = msg}; |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | if (service && openInTerminal != service->terminal()) { |
| 149 | service = nullptr; // It's not exactly this service we're running |
| 150 | } |
| 151 | |
| 152 | qCDebug(KIO_CORE) << "bRemember=" << remember << "service found=" << service; |
| 153 | if (service) { |
| 154 | if (remember) { |
| 155 | // Associate this app with qMimeType in mimeapps.list |
| 156 | Q_ASSERT(!mimeType.isEmpty()); // we don't show the remember checkbox otherwise |
| 157 | addToMimeAppsList(serviceId: service->storageId(), qMimeType: mimeType); |
| 158 | rebuildSycoca = true; |
| 159 | } |
| 160 | } else { |
| 161 | const bool createDesktopFile = remember || saveNewApps; |
| 162 | if (!createDesktopFile) { |
| 163 | // Create temp service |
| 164 | if (configPath.isEmpty()) { |
| 165 | service = new KService(initialServiceName, fullExec, QString()); |
| 166 | } else { |
| 167 | if (!typedExec.contains(s: QLatin1String("%u" ), cs: Qt::CaseInsensitive) && !typedExec.contains(s: QLatin1String("%f" ), cs: Qt::CaseInsensitive)) { |
| 168 | int index = serviceExec.indexOf(s: QLatin1String("%u" ), from: 0, cs: Qt::CaseInsensitive); |
| 169 | if (index == -1) { |
| 170 | index = serviceExec.indexOf(s: QLatin1String("%f" ), from: 0, cs: Qt::CaseInsensitive); |
| 171 | } |
| 172 | if (index > -1) { |
| 173 | fullExec += QLatin1Char(' ') + QStringView(serviceExec).mid(pos: index, n: 2); |
| 174 | } |
| 175 | } |
| 176 | // qDebug() << "Creating service with Exec=" << fullExec; |
| 177 | service = new KService(configPath); |
| 178 | service->setExec(fullExec); |
| 179 | } |
| 180 | if (openInTerminal) { |
| 181 | service->setTerminal(true); |
| 182 | // only add --noclose when we are sure it is konsole we're using |
| 183 | if (preferredTerminal == QLatin1String("konsole" ) && lingerTerminal) { |
| 184 | service->setTerminalOptions(QStringLiteral("--noclose" )); |
| 185 | } |
| 186 | } |
| 187 | } else { |
| 188 | // If we got here, we can't seem to find a service for what they wanted. Create one. |
| 189 | |
| 190 | QString ; |
| 191 | #ifdef Q_OS_WIN32 |
| 192 | // on windows, do not use the complete path, but only the default name. |
| 193 | serviceName = QFileInfo(serviceName).fileName(); |
| 194 | #endif |
| 195 | QString newPath = KService::newServicePath(showInMenu: false /* ignored argument */, suggestedName: serviceName, menuId: &menuId); |
| 196 | // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId; |
| 197 | |
| 198 | KDesktopFile desktopFile(newPath); |
| 199 | KConfigGroup cg = desktopFile.desktopGroup(); |
| 200 | cg.writeEntry(key: "Type" , value: "Application" ); |
| 201 | |
| 202 | // For the user visible name, use the executable name with any |
| 203 | // arguments appended, but with desktop-file specific expansion |
| 204 | // arguments removed. This is done to more clearly communicate the |
| 205 | // actual command used to the user and makes it easier to |
| 206 | // distinguish things like "qdbus". |
| 207 | QString name = KIO::DesktopExecParser::executableName(execLine: fullExec); |
| 208 | auto view = QStringView{fullExec}.trimmed(); |
| 209 | int index = view.indexOf(c: QLatin1Char(' ')); |
| 210 | if (index > 0) { |
| 211 | name.append(v: view.mid(pos: index)); |
| 212 | } |
| 213 | cg.writeEntry(key: "Name" , value: simplifiedExecLineFromService(fullExec: name)); |
| 214 | |
| 215 | // if we select a binary for a scheme handler, then it's safe to assume it can handle URLs |
| 216 | if (mimeType.startsWith(s: QLatin1String("x-scheme-handler/" ))) { |
| 217 | if (!typedExec.contains(s: QLatin1String("%u" ), cs: Qt::CaseInsensitive) && !typedExec.contains(s: QLatin1String("%f" ), cs: Qt::CaseInsensitive)) { |
| 218 | fullExec += QStringLiteral(" %u" ); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | cg.writeEntry(key: "Exec" , value: fullExec); |
| 223 | cg.writeEntry(key: "NoDisplay" , value: true); // don't make it appear in the K menu |
| 224 | if (openInTerminal) { |
| 225 | cg.writeEntry(key: "Terminal" , value: true); |
| 226 | // only add --noclose when we are sure it is konsole we're using |
| 227 | if (preferredTerminal == QLatin1String("konsole" ) && lingerTerminal) { |
| 228 | cg.writeEntry(key: "TerminalOptions" , value: "--noclose" ); |
| 229 | } |
| 230 | } |
| 231 | if (!mimeType.isEmpty()) { |
| 232 | cg.writeXdgListEntry(key: "MimeType" , value: QStringList() << mimeType); |
| 233 | } |
| 234 | cg.sync(); |
| 235 | |
| 236 | if (!mimeType.isEmpty()) { |
| 237 | addToMimeAppsList(serviceId: menuId, qMimeType: mimeType); |
| 238 | rebuildSycoca = true; |
| 239 | } |
| 240 | service = new KService(newPath); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return {.accept = true, .error = {}, .rebuildSycoca = rebuildSycoca}; |
| 245 | } |
| 246 | |
| 247 | } // namespace KIO |
| 248 | |