File indexing completed on 2024-05-12 17:08:41

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