File indexing completed on 2024-04-28 16:51:33

0001 /*
0002     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "purposeplugin.h"
0008 
0009 #include <QClipboard>
0010 #include <QGuiApplication>
0011 #include <QJsonArray>
0012 #include <QJsonObject>
0013 
0014 #include <KIO/MimeTypeFinderJob>
0015 
0016 #include <Purpose/AlternativesModel>
0017 #include <PurposeWidgets/Menu>
0018 
0019 PurposePlugin::PurposePlugin(QObject *parent)
0020     : AbstractBrowserPlugin(QStringLiteral("purpose"), 1, parent)
0021 {
0022 }
0023 
0024 PurposePlugin::~PurposePlugin()
0025 {
0026     onUnload();
0027 }
0028 
0029 bool PurposePlugin::onUnload()
0030 {
0031     m_menu.reset();
0032     return true;
0033 }
0034 
0035 QJsonObject PurposePlugin::handleData(int serial, const QString &event, const QJsonObject &data)
0036 {
0037     if (event == QLatin1String("share")) {
0038         if (m_pendingReplySerial != -1 || (m_menu && m_menu->isVisible())) {
0039             return {
0040                 {QStringLiteral("success"), false},
0041                 {QStringLiteral("errorCode"), QStringLiteral("BUSY")},
0042             };
0043         }
0044 
0045         // store request serial for asynchronous reply
0046         m_pendingReplySerial = serial;
0047 
0048         const QJsonObject shareData = data.value(QStringLiteral("data")).toObject();
0049 
0050         QString title = shareData.value(QStringLiteral("title")).toString();
0051         const QString text = shareData.value(QStringLiteral("text")).toString();
0052         const QString urlString = shareData.value(QStringLiteral("url")).toString();
0053 
0054         // Purpose ShareUrl plug-in type mandates a title.
0055         if (title.isEmpty()) {
0056             title = urlString;
0057         }
0058 
0059         if (!m_menu) {
0060             m_menu.reset(new Purpose::Menu());
0061             m_menu->model()->setPluginType(QStringLiteral("ShareUrl"));
0062 
0063             connect(m_menu.data(), &QMenu::aboutToShow, this, [this] {
0064                 m_menu->setProperty("actionInvoked", false);
0065             });
0066 
0067             connect(m_menu.data(), &QMenu::aboutToHide, this, [this] {
0068                 // aboutToHide is emitted before an action is triggered and activeAction() is
0069                 // the action currently hovered. This means we can't properly tell that the prompt
0070                 // got canceled, when hovering an action and then hitting Escape to close the menu.
0071                 // Hence delaying this and checking if an action got invoked :(
0072 
0073                 QMetaObject::invokeMethod(
0074                     this,
0075                     [this] {
0076                         if (!m_menu->property("actionInvoked").toBool()) {
0077                             sendPendingReply(false,
0078                                              {
0079                                                  {QStringLiteral("errorCode"), QStringLiteral("CANCELED")},
0080                                              });
0081                         }
0082                     },
0083                     Qt::QueuedConnection);
0084             });
0085 
0086             connect(m_menu.data(), &QMenu::triggered, this, [this] {
0087                 m_menu->setProperty("actionInvoked", true);
0088             });
0089 
0090             connect(m_menu.data(), &Purpose::Menu::finished, this, [this](const QJsonObject &output, int errorCode, const QString &errorMessage) {
0091                 if (errorCode) {
0092                     debug() << "Error:" << errorCode << errorMessage;
0093 
0094                     sendPendingReply(false,
0095                                      {
0096                                          {QStringLiteral("errorCode"), errorCode},
0097                                          {QStringLiteral("errorMessage"), errorMessage},
0098                                      });
0099                     return;
0100                 }
0101 
0102                 const QString url = output.value(QStringLiteral("url")).toString();
0103                 if (!url.isEmpty()) {
0104                     // Do this here rather than on the extension side to avoid having to request an additional permission after updating
0105                     QGuiApplication::clipboard()->setText(url);
0106                 }
0107 
0108                 debug() << "Finished:" << output;
0109                 sendPendingReply(true, {{QStringLiteral("response"), output}});
0110             });
0111         }
0112 
0113         QJsonObject shareJson;
0114 
0115         if (!title.isEmpty()) {
0116             shareJson.insert(QStringLiteral("title"), title);
0117         }
0118 
0119         QJsonArray urls;
0120         if (!urlString.isEmpty()) {
0121             urls.append(urlString);
0122         }
0123         // Sends even text as URL...
0124         if (!text.isEmpty()) {
0125             urls.append(text);
0126         }
0127 
0128         if (!urls.isEmpty()) {
0129             shareJson.insert(QStringLiteral("urls"), urls);
0130         }
0131 
0132         if (!text.isEmpty()) {
0133             showShareMenu(shareJson, QStringLiteral("text/plain"));
0134             return {};
0135         }
0136 
0137         if (!urls.isEmpty()) {
0138             auto *mimeJob = new KIO::MimeTypeFinderJob(QUrl(urlString));
0139             mimeJob->setAuthenticationPromptEnabled(false);
0140             connect(mimeJob, &KIO::MimeTypeFinderJob::result, this, [this, mimeJob, shareJson] {
0141                 showShareMenu(shareJson, mimeJob->mimeType());
0142             });
0143             mimeJob->start();
0144             return {};
0145         }
0146 
0147         // navigator.share({title: "foo"}) is valid but makes no sense
0148         // and we also cannot share via Purpose without "urls"
0149         m_pendingReplySerial = -1;
0150         return {
0151             {QStringLiteral("success"), false},
0152             {QStringLiteral("errorCode"), QStringLiteral("INVALID_ARGUMENT")},
0153         };
0154     }
0155 
0156     return {};
0157 }
0158 
0159 void PurposePlugin::sendPendingReply(bool success, const QJsonObject &data)
0160 {
0161     QJsonObject reply = data;
0162     reply.insert(QStringLiteral("success"), success);
0163 
0164     sendReply(m_pendingReplySerial, reply);
0165     m_pendingReplySerial = -1;
0166 }
0167 
0168 void PurposePlugin::showShareMenu(const QJsonObject &data, const QString &mimeType)
0169 {
0170     QJsonObject shareData = data;
0171 
0172     if (!mimeType.isEmpty() && mimeType != QLatin1String("application/octet-stream")) {
0173         shareData.insert(QStringLiteral("mimeType"), mimeType);
0174     } else {
0175         shareData.insert(QStringLiteral("mimeType"), QStringLiteral("*/*"));
0176     }
0177 
0178     debug() << "Share mime type" << mimeType << "with data" << data;
0179 
0180     m_menu->model()->setInputData(shareData);
0181     m_menu->reload();
0182 
0183     m_menu->popup(QCursor::pos());
0184 }