File indexing completed on 2024-05-12 05:37:01

0001 /*
0002     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "iconapplet.h"
0008 
0009 #include <QAction>
0010 #include <QApplication>
0011 #include <QDir>
0012 #include <QDropEvent>
0013 #include <QFileInfo>
0014 #include <QIcon>
0015 #include <QJsonArray>
0016 #include <QMenu>
0017 #include <QMimeData>
0018 #include <QMimeDatabase>
0019 #include <QProcess>
0020 
0021 #include <KAuthorized>
0022 #include <KDesktopFile>
0023 #include <KFileItemActions>
0024 #include <KFileItemListProperties>
0025 #include <KFileUtils>
0026 #include <KJobWidgets>
0027 #include <KLocalizedString>
0028 #include <KNotificationJobUiDelegate>
0029 #include <KProtocolManager>
0030 #include <KService>
0031 #include <KServiceAction>
0032 
0033 #include <KIO/ApplicationLauncherJob>
0034 #include <KIO/DropJob>
0035 #include <KIO/FavIconRequestJob>
0036 #include <KIO/OpenFileManagerWindowJob>
0037 #include <KIO/OpenUrlJob>
0038 #include <KIO/StatJob>
0039 
0040 #include <abstracttasksmodel.h>
0041 #include <startuptasksmodel.h>
0042 
0043 using namespace Qt::StringLiterals;
0044 
0045 IconApplet::IconApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0046     : Plasma::Applet(parent, data, args)
0047 {
0048 }
0049 
0050 IconApplet::~IconApplet()
0051 {
0052     // in a handler connected to IconApplet::appletDeleted m_localPath will be empty?!
0053     if (destroyed()) {
0054         QFile::remove(m_localPath);
0055     }
0056 }
0057 
0058 void IconApplet::init()
0059 {
0060     populate();
0061 }
0062 
0063 void IconApplet::configChanged()
0064 {
0065     populate();
0066 }
0067 
0068 void IconApplet::populate()
0069 {
0070     const QString entryName = u"url"_s;
0071     m_url = config().readEntry(entryName, QUrl());
0072 
0073     if (!m_url.isValid()) {
0074         // the old applet that used a QML plugin and stored its url
0075         // in plasmoid.configuration.url had its entries stored in [Configuration][General]
0076         // so we look here as well to provide an upgrade path
0077         m_url = config().group(u"General"_s).readEntry(entryName, QUrl());
0078     }
0079 
0080     // our backing desktop file already exists? just read all the things from it
0081     const QString path = localPath();
0082     if (QFileInfo::exists(path)) {
0083         populateFromDesktopFile(path);
0084         return;
0085     }
0086 
0087     if (!m_url.isValid()) {
0088         // invalid url, use dummy data
0089         populateFromDesktopFile(QString());
0090         return;
0091     }
0092 
0093     const QString plasmaIconsFolderPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/plasma_icons");
0094     if (!QDir().mkpath(plasmaIconsFolderPath)) {
0095         setLaunchErrorMessage(i18n("Failed to create icon widgets folder '%1'", plasmaIconsFolderPath));
0096         return;
0097     }
0098 
0099     setBusy(true); // unset in populateFromDesktopFile where we'll end up in if all goes well
0100 
0101     auto *statJob = KIO::stat(m_url, KIO::HideProgressInfo);
0102     connect(statJob, &KIO::StatJob::finished, this, [=, this] {
0103         QString desiredDesktopFileName = m_url.fileName();
0104 
0105         // in doubt, just encode the entire URL, e.g. https://www.kde.org/ has no filename
0106         if (desiredDesktopFileName.isEmpty()) {
0107             desiredDesktopFileName = KIO::encodeFileName(m_url.toDisplayString());
0108         }
0109 
0110         // We always want it to be a .desktop file (e.g. also for the "Type=Link" at the end)
0111         if (!desiredDesktopFileName.endsWith(QLatin1String(".desktop"))) {
0112             desiredDesktopFileName.append(QLatin1String(".desktop"));
0113         }
0114 
0115         QString backingDesktopFile = plasmaIconsFolderPath + QLatin1Char('/');
0116         // KFileUtils::suggestName always appends a suffix, i.e. it expects that we already know the file already exists
0117         if (QFileInfo::exists(backingDesktopFile + desiredDesktopFileName)) {
0118             desiredDesktopFileName = KFileUtils::suggestName(QUrl::fromLocalFile(plasmaIconsFolderPath), desiredDesktopFileName);
0119         }
0120         backingDesktopFile.append(desiredDesktopFileName);
0121 
0122         QString name; // ends up as "Name" in the .desktop file for "Link" files below
0123 
0124         const QUrl url = statJob->mostLocalUrl();
0125         if (url.isLocalFile()) {
0126             const QString localUrlString = url.toLocalFile();
0127 
0128             // if desktop file just copy it over
0129             if (KDesktopFile::isDesktopFile(localUrlString)) {
0130                 // if this restriction is set, KIO won't allow running desktop files from outside
0131                 // registered services, applications, and so on, in this case we'll use the original
0132                 // .desktop file and lose the ability to customize it
0133                 if (!KAuthorized::authorize(KAuthorized::RUN_DESKTOP_FILES)) {
0134                     populateFromDesktopFile(localUrlString);
0135                     // we don't call setLocalPath here as we don't want to store localPath to be a system-location
0136                     // so that the fact that we cannot edit is re-evaluated every time
0137                     return;
0138                 }
0139 
0140                 if (!QFile::copy(localUrlString, backingDesktopFile)) {
0141                     setLaunchErrorMessage(i18n("Failed to copy icon widget desktop file from '%1' to '%2'", localUrlString, backingDesktopFile));
0142                     setBusy(false);
0143                     return;
0144                 }
0145 
0146                 // set executable flag on the desktop file so KIO doesn't complain about executing it
0147                 QFile file(backingDesktopFile);
0148                 file.setPermissions(file.permissions() | QFile::ExeOwner);
0149 
0150                 populateFromDesktopFile(backingDesktopFile);
0151                 setLocalPath(backingDesktopFile);
0152 
0153                 return;
0154             }
0155         }
0156 
0157         // in all other cases just make it a link
0158 
0159         QString iconName;
0160         QString genericName;
0161 
0162         if (!statJob->error()) {
0163             KFileItem item(statJob->statResult(), url);
0164 
0165             if (name.isEmpty()) {
0166                 name = item.text();
0167             }
0168 
0169             if (item.mimetype() != QLatin1String("application/octet-stream")) {
0170                 iconName = item.iconName();
0171                 genericName = item.mimeComment();
0172             }
0173         }
0174 
0175         // KFileItem might return "." as text for e.g. root folders
0176         if (name == QLatin1Char('.')) {
0177             name.clear();
0178         }
0179 
0180         if (name.isEmpty()) {
0181             name = url.fileName();
0182         }
0183 
0184         if (name.isEmpty()) {
0185             // TODO would be cool to just show the parent folder name instead of the full path
0186             name = url.path();
0187         }
0188 
0189         // For websites the filename e.g. "index.php" is usually not what you want
0190         // also "/" isn't very descript when it's not our local "root" folder
0191         if (name.isEmpty() || url.scheme().startsWith(QLatin1String("http")) || (!url.isLocalFile() && name == QLatin1String("/"))) {
0192             name = url.host();
0193         }
0194 
0195         if (iconName.isEmpty()) {
0196             // In doubt ask KIO::iconNameForUrl, KFileItem can't cope with http:// URLs for instance
0197             iconName = KIO::iconNameForUrl(url);
0198         }
0199 
0200         bool downloadFavIcon = false;
0201 
0202         if (url.scheme().startsWith(QLatin1String("http"))) {
0203             const QString favIcon = KIO::favIconForUrl(url);
0204 
0205             if (!favIcon.isEmpty()) {
0206                 iconName = favIcon;
0207             } else {
0208                 downloadFavIcon = true;
0209             }
0210         }
0211 
0212         KDesktopFile linkDesktopFile(backingDesktopFile);
0213         auto desktopGroup = linkDesktopFile.desktopGroup();
0214 
0215         desktopGroup.writeEntry(QStringLiteral("Name"), name);
0216         desktopGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Link"));
0217         desktopGroup.writeEntry(QStringLiteral("URL"), url);
0218         desktopGroup.writeEntry(QStringLiteral("Icon"), iconName);
0219         if (!genericName.isEmpty()) {
0220             desktopGroup.writeEntry(QStringLiteral("GenericName"), genericName);
0221         }
0222 
0223         linkDesktopFile.sync();
0224 
0225         populateFromDesktopFile(backingDesktopFile);
0226         setLocalPath(backingDesktopFile);
0227 
0228         if (downloadFavIcon) {
0229             KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(m_url);
0230             connect(job, &KIO::FavIconRequestJob::result, this, [job, backingDesktopFile, this](KJob *) {
0231                 if (!job->error()) {
0232                     KDesktopFile(backingDesktopFile).desktopGroup().writeEntry(QStringLiteral("Icon"), job->iconFile());
0233 
0234                     m_iconName = job->iconFile();
0235                     Q_EMIT iconNameChanged(m_iconName);
0236                 }
0237             });
0238         }
0239     });
0240 }
0241 
0242 void IconApplet::populateFromDesktopFile(const QString &path)
0243 {
0244     // path empty? just set icon to "unknown" and call it a day
0245     if (path.isEmpty()) {
0246         setIconName({});
0247         return;
0248     }
0249 
0250     KDesktopFile desktopFile(path);
0251 
0252     const QString &name = desktopFile.readName();
0253     if (m_name != name) {
0254         m_name = name;
0255         Q_EMIT nameChanged(name);
0256     }
0257 
0258     const QString &genericName = desktopFile.readGenericName();
0259     if (m_genericName != genericName) {
0260         m_genericName = genericName;
0261         Q_EMIT genericNameChanged(genericName);
0262     }
0263 
0264     setIconName(desktopFile.readIcon());
0265 
0266     delete m_openContainingFolderAction;
0267     m_openContainingFolderAction = nullptr;
0268     m_openWithActions.clear();
0269     m_jumpListActions.clear();
0270 
0271     setLocalPath(path);
0272 
0273     setBusy(false);
0274 }
0275 
0276 QUrl IconApplet::url() const
0277 {
0278     return m_url;
0279 }
0280 
0281 void IconApplet::setUrl(const QUrl &url)
0282 {
0283     if (m_url != url) {
0284         m_url = url;
0285         Q_EMIT urlChanged(url);
0286 
0287         config().writeEntry(QStringLiteral("url"), url);
0288 
0289         populate();
0290     }
0291 }
0292 
0293 void IconApplet::setIconName(const QString &iconName)
0294 {
0295     const QString newIconName = (!iconName.isEmpty() ? iconName : QStringLiteral("unknown"));
0296     if (m_iconName != newIconName) {
0297         m_iconName = newIconName;
0298         Q_EMIT iconNameChanged(newIconName);
0299     }
0300 }
0301 
0302 QString IconApplet::name() const
0303 {
0304     return m_name;
0305 }
0306 
0307 QString IconApplet::iconName() const
0308 {
0309     return m_iconName;
0310 }
0311 
0312 QString IconApplet::genericName() const
0313 {
0314     return m_genericName;
0315 }
0316 
0317 bool IconApplet::isValid() const
0318 {
0319     return !m_localPath.isEmpty();
0320 }
0321 
0322 QList<QAction *> IconApplet::extraActions()
0323 {
0324     QList<QAction *> actions = Applet::contextualActions();
0325     if (m_localPath.isEmpty()) {
0326         return actions;
0327     }
0328 
0329     KDesktopFile desktopFile(m_localPath);
0330 
0331     if (desktopFile.hasLinkType()) {
0332         const QUrl linkUrl = QUrl(desktopFile.readUrl());
0333 
0334         if (linkUrl.isValid() && !linkUrl.scheme().isEmpty()) {
0335             if (m_openWithActions.isEmpty()) {
0336                 if (!m_fileItemActions) {
0337                     m_fileItemActions = new KFileItemActions(this);
0338                 }
0339                 KFileItemListProperties itemProperties(KFileItemList({KFileItem(linkUrl)}));
0340                 m_fileItemActions->setItemListProperties(itemProperties);
0341 
0342                 if (!m_openWithMenu) {
0343                     m_openWithMenu.reset(new QMenu());
0344                 }
0345                 m_openWithMenu->clear();
0346                 m_fileItemActions->insertOpenWithActionsTo(nullptr, m_openWithMenu.get(), QStringList());
0347 
0348                 m_openWithActions = m_openWithMenu->actions();
0349             }
0350 
0351             if (!m_openContainingFolderAction) {
0352                 if (KProtocolManager::supportsListing(linkUrl)) {
0353                     m_openContainingFolderAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("Open Containing Folder"), this);
0354                     connect(m_openContainingFolderAction, &QAction::triggered, this, [linkUrl] {
0355                         KIO::highlightInFileManager({linkUrl});
0356                     });
0357                 }
0358             }
0359 
0360             actions << m_openWithActions;
0361         }
0362     } else {
0363         if (m_jumpListActions.isEmpty()) {
0364             const KService service(m_localPath);
0365 
0366             const auto jumpListActions = service.actions();
0367             for (const KServiceAction &serviceAction : jumpListActions) {
0368                 if (serviceAction.noDisplay()) {
0369                     continue;
0370                 }
0371 
0372                 QAction *action = new QAction(QIcon::fromTheme(serviceAction.icon()), serviceAction.text(), this);
0373                 if (serviceAction.isSeparator()) {
0374                     action->setSeparator(true);
0375                 }
0376 
0377                 connect(action, &QAction::triggered, this, [serviceAction]() {
0378                     auto *job = new KIO::ApplicationLauncherJob(serviceAction);
0379                     auto *delegate = new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled);
0380                     job->setUiDelegate(delegate);
0381                     job->start();
0382                 });
0383 
0384                 m_jumpListActions << action;
0385             }
0386         }
0387         actions << m_jumpListActions;
0388     }
0389 
0390     if (!actions.isEmpty()) {
0391         if (!m_separatorAction) {
0392             m_separatorAction = new QAction(this);
0393             m_separatorAction->setSeparator(true);
0394         }
0395         actions << m_separatorAction;
0396     }
0397 
0398     if (m_openContainingFolderAction) {
0399         actions << m_openContainingFolderAction;
0400     }
0401 
0402     return actions;
0403 }
0404 
0405 void IconApplet::run()
0406 {
0407     if (!m_startupTasksModel) {
0408         m_startupTasksModel = new TaskManager::StartupTasksModel(this);
0409 
0410         auto handleRow = [this](bool busy, const QModelIndex &parent, int first, int last) {
0411             Q_UNUSED(parent);
0412             for (int i = first; i <= last; ++i) {
0413                 const QModelIndex idx = m_startupTasksModel->index(i, 0);
0414                 if (idx.data(TaskManager::AbstractTasksModel::LauncherUrlWithoutIcon).toUrl() == QUrl::fromLocalFile(m_localPath)) {
0415                     setBusy(busy);
0416                     break;
0417                 }
0418             }
0419         };
0420 
0421         using namespace std::placeholders;
0422         connect(m_startupTasksModel, &QAbstractItemModel::rowsInserted, this, std::bind(handleRow, true /*busy*/, _1, _2, _3));
0423         connect(m_startupTasksModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, std::bind(handleRow, false /*busy*/, _1, _2, _3));
0424     }
0425 
0426     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(m_localPath));
0427     job->setRunExecutables(true); // so it can launch apps
0428     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0429     job->start();
0430 }
0431 
0432 void IconApplet::processDrop(QObject *dropEvent)
0433 {
0434     Q_ASSERT(dropEvent);
0435     const bool isAcceptable = isAcceptableDrag(dropEvent);
0436     Q_ASSERT(isAcceptable);
0437 
0438     const auto &urls = urlsFromDrop(dropEvent);
0439 
0440     if (urls.isEmpty()) {
0441         return;
0442     }
0443 
0444     const QString &localPath = m_url.toLocalFile();
0445 
0446     if (KDesktopFile::isDesktopFile(localPath)) {
0447         auto service = new KService(localPath);
0448 
0449         if (service->isApplication()) {
0450             KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(KService::Ptr(service));
0451             job->setUrls(urls);
0452             job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0453             job->start();
0454             return;
0455         }
0456     }
0457 
0458     QMimeDatabase db;
0459     const QMimeType mimeType = db.mimeTypeForUrl(m_url);
0460 
0461     if (isExecutable(mimeType)) { // isAcceptableDrag has the KAuthorized check for this
0462         QProcess::startDetached(m_url.toLocalFile(), QUrl::toStringList(urls));
0463         return;
0464     }
0465 
0466     if (mimeType.inherits(QStringLiteral("inode/directory"))) {
0467         QMimeData mimeData;
0468         mimeData.setUrls(urls);
0469 
0470         // DeclarativeDropEvent isn't public
0471         QDropEvent de(QPointF(dropEvent->property("x").toInt(), dropEvent->property("y").toInt()),
0472                       static_cast<Qt::DropActions>(dropEvent->property("proposedActions").toInt()),
0473                       &mimeData,
0474                       static_cast<Qt::MouseButtons>(dropEvent->property("buttons").toInt()),
0475                       static_cast<Qt::KeyboardModifiers>(dropEvent->property("modifiers").toInt()));
0476 
0477         KIO::drop(&de, m_url);
0478         return;
0479     }
0480 }
0481 
0482 bool IconApplet::isAcceptableDrag(QObject *dropEvent)
0483 {
0484     Q_ASSERT(dropEvent);
0485 
0486     const auto &urls = urlsFromDrop(dropEvent);
0487 
0488     if (urls.isEmpty()) {
0489         return false;
0490     }
0491 
0492     const QString &localPath = m_url.toLocalFile();
0493     if (KDesktopFile::isDesktopFile(localPath)) {
0494         return true;
0495     }
0496 
0497     QMimeDatabase db;
0498     const QMimeType mimeType = db.mimeTypeForUrl(m_url);
0499 
0500     if (KAuthorized::authorize(KAuthorized::SHELL_ACCESS) && isExecutable(mimeType)) {
0501         return true;
0502     }
0503 
0504     if (mimeType.inherits(QStringLiteral("inode/directory"))) {
0505         return true;
0506     }
0507 
0508     return false;
0509 }
0510 
0511 QList<QUrl> IconApplet::urlsFromDrop(QObject *dropEvent)
0512 {
0513     // DeclarativeDropEvent and co aren't public
0514     const QObject *mimeData = qvariant_cast<QObject *>(dropEvent->property("mimeData"));
0515     Q_ASSERT(mimeData);
0516 
0517     const QJsonArray &droppedUrls = mimeData->property("urls").toJsonArray();
0518 
0519     QList<QUrl> urls;
0520     urls.reserve(droppedUrls.count());
0521     for (const QJsonValue &droppedUrl : droppedUrls) {
0522         const QUrl url(droppedUrl.toString());
0523         if (url.isValid()) {
0524             urls.append(url);
0525         }
0526     }
0527 
0528     return urls;
0529 }
0530 
0531 bool IconApplet::isExecutable(const QMimeType &mimeType)
0532 {
0533     return (mimeType.inherits(QStringLiteral("application/x-executable")) || mimeType.inherits(QStringLiteral("application/x-shellscript")));
0534 }
0535 
0536 void IconApplet::configure()
0537 {
0538     KPropertiesDialog *dialog = m_configDialog.data();
0539 
0540     if (dialog) {
0541         dialog->show();
0542         dialog->raise();
0543         return;
0544     }
0545 
0546     dialog = new KPropertiesDialog(QUrl::fromLocalFile(m_localPath));
0547     m_configDialog = dialog;
0548 
0549     connect(dialog, &KPropertiesDialog::applied, this, [this] {
0550         KDesktopFile desktopFile(m_localPath);
0551         if (desktopFile.hasLinkType()) {
0552             const QUrl newUrl(desktopFile.readUrl());
0553 
0554             if (m_url != newUrl) {
0555                 // make sure to fully repopulate in case the user changed the Link URL
0556                 QFile::remove(m_localPath);
0557 
0558                 setUrl(newUrl); // calls populate() itself, but only if it changed
0559                 return;
0560             }
0561         }
0562 
0563         populate();
0564     });
0565 
0566     dialog->setAttribute(Qt::WA_DeleteOnClose, true);
0567     dialog->setFileNameReadOnly(true);
0568     dialog->setWindowTitle(i18n("Properties for %1", m_name));
0569     dialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
0570     dialog->show();
0571 }
0572 
0573 QString IconApplet::localPath() const
0574 {
0575     return config().readEntry(QStringLiteral("localPath"));
0576 }
0577 
0578 void IconApplet::setLocalPath(const QString &localPath)
0579 {
0580     const bool oldValid = isValid();
0581     m_localPath = localPath;
0582     config().writeEntry(QStringLiteral("localPath"), localPath);
0583     if (oldValid != isValid()) {
0584         Q_EMIT isValidChanged();
0585     }
0586 }
0587 
0588 K_PLUGIN_CLASS(IconApplet)
0589 
0590 #include "iconapplet.moc"