File indexing completed on 2024-06-23 05:18:24

0001 /*
0002     SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "editorwatcher.h"
0008 #include <config-messagecomposer.h>
0009 
0010 #include "messagecomposer_debug.h"
0011 
0012 #include <KApplicationTrader>
0013 #include <KIO/DesktopExecParser>
0014 #include <KLocalizedString>
0015 #include <KMessageBox>
0016 #include <KOpenWithDialog>
0017 #include <KProcess>
0018 
0019 #include <QSocketNotifier>
0020 
0021 #include <cassert>
0022 #include <memory>
0023 
0024 // inotify stuff taken from kdelibs/kio/kio/kdirwatch.cpp
0025 #ifdef HAVE_SYS_INOTIFY_H
0026 #include <fcntl.h>
0027 #include <sys/inotify.h>
0028 #include <sys/ioctl.h>
0029 #include <unistd.h>
0030 #endif
0031 
0032 using namespace MessageComposer;
0033 
0034 EditorWatcher::EditorWatcher(const QUrl &url, const QString &mimeType, OpenWithOption option, QObject *parent, QWidget *parentWidget)
0035     : QObject(parent)
0036     , mUrl(url)
0037     , mMimeType(mimeType)
0038     , mParentWidget(parentWidget)
0039     , mOpenWithOption(option)
0040 {
0041     assert(mUrl.isLocalFile());
0042     mTimer.setSingleShot(true);
0043     connect(&mTimer, &QTimer::timeout, this, &EditorWatcher::checkEditDone);
0044 }
0045 
0046 EditorWatcher::~EditorWatcher()
0047 {
0048 #ifdef HAVE_SYS_INOTIFY_H
0049     ::close(mInotifyFd);
0050 #endif
0051 }
0052 
0053 EditorWatcher::ErrorEditorWatcher EditorWatcher::start()
0054 {
0055     // find an editor
0056     QList<QUrl> list;
0057     list.append(mUrl);
0058     KService::Ptr offer = KApplicationTrader::preferredService(mMimeType);
0059     if ((mOpenWithOption == OpenWithDialog) || !offer) {
0060         std::unique_ptr<KOpenWithDialog> dlg(new KOpenWithDialog(list, i18n("Edit with:"), QString(), mParentWidget));
0061         const int dlgrc = dlg->exec();
0062         if (dlgrc) {
0063             offer = dlg->service();
0064         }
0065         if (!dlgrc) {
0066             return Canceled;
0067         }
0068         if (!offer) {
0069             return NoServiceFound;
0070         }
0071     }
0072 
0073 #ifdef HAVE_SYS_INOTIFY_H
0074     // monitor file
0075     mInotifyFd = inotify_init();
0076     if (mInotifyFd > 0) {
0077         (void)fcntl(mInotifyFd, F_SETFD, FD_CLOEXEC);
0078         mInotifyWatch = inotify_add_watch(mInotifyFd, mUrl.path().toLatin1().constData(), IN_CLOSE | IN_OPEN | IN_MODIFY | IN_ATTRIB);
0079         if (mInotifyWatch >= 0) {
0080             auto sn = new QSocketNotifier(mInotifyFd, QSocketNotifier::Read, this);
0081             connect(sn, &QSocketNotifier::activated, this, &EditorWatcher::inotifyEvent);
0082             mHaveInotify = true;
0083             mFileModified = false;
0084         }
0085     } else {
0086         qCWarning(MESSAGECOMPOSER_LOG()) << "Failed to activate INOTIFY!";
0087     }
0088 #endif
0089 
0090     // start the editor
0091     KIO::DesktopExecParser parser(*offer, list);
0092     parser.setUrlsAreTempFiles(false);
0093     const QStringList params = parser.resultingArguments();
0094     mEditor = new KProcess(this);
0095     mEditor->setProgram(params);
0096     connect(mEditor, &KProcess::finished, this, &EditorWatcher::editorExited);
0097     mEditor->start();
0098     if (!mEditor->waitForStarted()) {
0099         return CannotStart;
0100     }
0101     mEditorRunning = true;
0102 
0103     mEditTime.start();
0104     return NoError;
0105 }
0106 
0107 bool EditorWatcher::fileChanged() const
0108 {
0109     return mFileModified;
0110 }
0111 
0112 QUrl EditorWatcher::url() const
0113 {
0114     return mUrl;
0115 }
0116 
0117 void EditorWatcher::inotifyEvent()
0118 {
0119     assert(mHaveInotify);
0120 
0121 #ifdef HAVE_SYS_INOTIFY_H
0122     int pending = -1;
0123     int offsetStartRead = 0; // where we read into buffer
0124     char buf[8192];
0125     assert(mInotifyFd > -1);
0126     ioctl(mInotifyFd, FIONREAD, &pending);
0127 
0128     while (pending > 0) {
0129         const int bytesToRead = qMin(pending, (int)sizeof(buf) - offsetStartRead);
0130 
0131         int bytesAvailable = read(mInotifyFd, &buf[offsetStartRead], bytesToRead);
0132         pending -= bytesAvailable;
0133         bytesAvailable += offsetStartRead;
0134         offsetStartRead = 0;
0135 
0136         int offsetCurrent = 0;
0137         while (bytesAvailable >= (int)sizeof(struct inotify_event)) {
0138             const struct inotify_event *const event = (struct inotify_event *)&buf[offsetCurrent];
0139             const int eventSize = sizeof(struct inotify_event) + event->len;
0140             if (bytesAvailable < eventSize) {
0141                 break;
0142             }
0143 
0144             bytesAvailable -= eventSize;
0145             offsetCurrent += eventSize;
0146             if (event->mask & IN_OPEN) {
0147                 mFileOpen = true;
0148             }
0149             if (event->mask & IN_CLOSE) {
0150                 mFileOpen = false;
0151             }
0152             if (event->mask & (IN_MODIFY | IN_ATTRIB)) {
0153                 mFileModified = true;
0154             }
0155         }
0156         if (bytesAvailable > 0) {
0157             // copy partial event to beginning of buffer
0158             memmove(buf, &buf[offsetCurrent], bytesAvailable);
0159             offsetStartRead = bytesAvailable;
0160         }
0161     }
0162 #endif
0163     mTimer.start(500);
0164 }
0165 
0166 void EditorWatcher::editorExited()
0167 {
0168     mEditorRunning = false;
0169     mTimer.start(500);
0170 }
0171 
0172 void EditorWatcher::checkEditDone()
0173 {
0174     if (mEditorRunning || (mFileOpen && mHaveInotify) || mDone) {
0175         return;
0176     }
0177 
0178     static QStringList readOnlyMimeTypes;
0179     if (readOnlyMimeTypes.isEmpty()) {
0180         readOnlyMimeTypes << QStringLiteral("message/rfc822") << QStringLiteral("application/pdf");
0181     }
0182 
0183     // protect us against double-deletion by calling this method again while
0184     // the subeventloop of the message box is running
0185     mDone = true;
0186 
0187     // check if it's a mime type that's mostly handled read-only
0188     const bool isReadOnlyMimeType = (readOnlyMimeTypes.contains(mMimeType) || mMimeType.startsWith(QLatin1StringView("image/")));
0189 
0190     // nobody can edit that fast, we seem to be unable to detect
0191     // when the editor will be closed
0192     if (mEditTime.elapsed() <= 3000 && !isReadOnlyMimeType) {
0193         KMessageBox::information(mParentWidget,
0194                                  i18n("KMail is unable to detect when the chosen editor is closed. "
0195                                       "To avoid data loss, editing the attachment will be aborted."),
0196                                  i18nc("@title:window", "Unable to edit attachment"),
0197                                  QStringLiteral("UnableToEditAttachment"));
0198     }
0199 
0200     Q_EMIT editDone(this);
0201     deleteLater();
0202 }
0203 
0204 #include "moc_editorwatcher.cpp"