| 1 | /* |
| 2 | This file is part of the KDE project |
| 3 | SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org> |
| 4 | SPDX-FileCopyrightText: 1999-2005 David Faure <faure@kde.org> |
| 5 | |
| 6 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 7 | */ |
| 8 | |
| 9 | #include "readwritepart.h" |
| 10 | #include "readwritepart_p.h" |
| 11 | |
| 12 | #include "kparts_logging.h" |
| 13 | |
| 14 | #define HAVE_KDIRNOTIFY __has_include(<KDirNotify>) |
| 15 | #if HAVE_KDIRNOTIFY |
| 16 | #include <KDirNotify> |
| 17 | #endif |
| 18 | |
| 19 | #include <KIO/FileCopyJob> |
| 20 | #include <KJobWidgets> |
| 21 | #include <KLocalizedString> |
| 22 | #include <KMessageBox> |
| 23 | |
| 24 | #include <QApplication> |
| 25 | #include <QFileDialog> |
| 26 | #include <QTemporaryFile> |
| 27 | |
| 28 | #include <qplatformdefs.h> |
| 29 | |
| 30 | #ifdef Q_OS_WIN |
| 31 | #include <qt_windows.h> //CreateHardLink() |
| 32 | #endif |
| 33 | |
| 34 | using namespace KParts; |
| 35 | |
| 36 | ReadWritePart::ReadWritePart(QObject *parent, const KPluginMetaData &data) |
| 37 | : ReadOnlyPart(*new ReadWritePartPrivate(this, data), parent) |
| 38 | { |
| 39 | } |
| 40 | |
| 41 | ReadWritePart::~ReadWritePart() |
| 42 | { |
| 43 | // parent destructor will delete temp file |
| 44 | // we can't call our own closeUrl() here, because |
| 45 | // "cancel" wouldn't cancel anything. We have to assume |
| 46 | // the app called closeUrl() before destroying us. |
| 47 | } |
| 48 | |
| 49 | void ReadWritePart::setReadWrite(bool readwrite) |
| 50 | { |
| 51 | Q_D(ReadWritePart); |
| 52 | |
| 53 | // Perhaps we should check isModified here and issue a warning if true |
| 54 | d->m_bReadWrite = readwrite; |
| 55 | } |
| 56 | |
| 57 | void ReadWritePart::setModified(bool modified) |
| 58 | { |
| 59 | Q_D(ReadWritePart); |
| 60 | |
| 61 | // qDebug() << "setModified(" << (modified ? "true" : "false") << ")"; |
| 62 | if (!d->m_bReadWrite && modified) { |
| 63 | qCCritical(KPARTSLOG) << "Can't set a read-only document to 'modified' !" ; |
| 64 | return; |
| 65 | } |
| 66 | d->m_bModified = modified; |
| 67 | } |
| 68 | |
| 69 | void ReadWritePart::setModified() |
| 70 | { |
| 71 | setModified(true); |
| 72 | } |
| 73 | |
| 74 | bool ReadWritePart::queryClose() |
| 75 | { |
| 76 | Q_D(ReadWritePart); |
| 77 | |
| 78 | if (!isReadWrite() || !isModified()) { |
| 79 | return true; |
| 80 | } |
| 81 | |
| 82 | QString docName = url().fileName(); |
| 83 | if (docName.isEmpty()) { |
| 84 | docName = i18n("Untitled" ); |
| 85 | } |
| 86 | |
| 87 | QWidget *parentWidget = widget(); |
| 88 | if (!parentWidget) { |
| 89 | parentWidget = QApplication::activeWindow(); |
| 90 | } |
| 91 | |
| 92 | int res = KMessageBox::warningTwoActionsCancel(parent: parentWidget, |
| 93 | i18n("The document \"%1\" has been modified.\n" |
| 94 | "Do you want to save your changes or discard them?" , |
| 95 | docName), |
| 96 | i18n("Close Document" ), |
| 97 | primaryAction: KStandardGuiItem::save(), |
| 98 | secondaryAction: KStandardGuiItem::discard()); |
| 99 | |
| 100 | bool abortClose = false; |
| 101 | bool handled = false; |
| 102 | |
| 103 | switch (res) { |
| 104 | case KMessageBox::PrimaryAction: |
| 105 | Q_EMIT sigQueryClose(handled: &handled, abortClosing: &abortClose); |
| 106 | if (!handled) { |
| 107 | if (d->m_url.isEmpty()) { |
| 108 | QUrl url = QFileDialog::getSaveFileUrl(parent: parentWidget); |
| 109 | if (url.isEmpty()) { |
| 110 | return false; |
| 111 | } |
| 112 | |
| 113 | saveAs(url); |
| 114 | } else { |
| 115 | save(); |
| 116 | } |
| 117 | } else if (abortClose) { |
| 118 | return false; |
| 119 | } |
| 120 | return waitSaveComplete(); |
| 121 | case KMessageBox::SecondaryAction: |
| 122 | return true; |
| 123 | default: // case KMessageBox::Cancel : |
| 124 | return false; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | bool ReadWritePart::closeUrl() |
| 129 | { |
| 130 | abortLoad(); // just in case |
| 131 | if (isReadWrite() && isModified()) { |
| 132 | if (!queryClose()) { |
| 133 | return false; |
| 134 | } |
| 135 | } |
| 136 | // Not modified => ok and delete temp file. |
| 137 | return ReadOnlyPart::closeUrl(); |
| 138 | } |
| 139 | |
| 140 | bool ReadWritePart::closeUrl(bool promptToSave) |
| 141 | { |
| 142 | return promptToSave ? closeUrl() : ReadOnlyPart::closeUrl(); |
| 143 | } |
| 144 | |
| 145 | bool ReadWritePart::save() |
| 146 | { |
| 147 | Q_D(ReadWritePart); |
| 148 | |
| 149 | d->m_saveOk = false; |
| 150 | if (d->m_file.isEmpty()) { // document was created empty |
| 151 | d->prepareSaving(); |
| 152 | } |
| 153 | if (saveFile()) { |
| 154 | return saveToUrl(); |
| 155 | } else { |
| 156 | Q_EMIT canceled(errMsg: QString()); |
| 157 | } |
| 158 | return false; |
| 159 | } |
| 160 | |
| 161 | bool ReadWritePart::saveAs(const QUrl &url) |
| 162 | { |
| 163 | Q_D(ReadWritePart); |
| 164 | |
| 165 | if (!url.isValid()) { |
| 166 | qCCritical(KPARTSLOG) << "saveAs: Malformed URL" << url; |
| 167 | return false; |
| 168 | } |
| 169 | d->m_duringSaveAs = true; |
| 170 | d->m_originalURL = d->m_url; |
| 171 | d->m_originalFilePath = d->m_file; |
| 172 | d->m_url = url; // Store where to upload in saveToURL |
| 173 | d->prepareSaving(); |
| 174 | bool result = save(); // Save local file and upload local file |
| 175 | if (result) { |
| 176 | if (d->m_originalURL != d->m_url) { |
| 177 | Q_EMIT urlChanged(url: d->m_url); |
| 178 | } |
| 179 | |
| 180 | Q_EMIT setWindowCaption(d->m_url.toDisplayString(options: QUrl::PreferLocalFile)); |
| 181 | } else { |
| 182 | d->m_url = d->m_originalURL; |
| 183 | d->m_file = d->m_originalFilePath; |
| 184 | d->m_duringSaveAs = false; |
| 185 | d->m_originalURL = QUrl(); |
| 186 | d->m_originalFilePath.clear(); |
| 187 | } |
| 188 | |
| 189 | return result; |
| 190 | } |
| 191 | |
| 192 | // Set m_file correctly for m_url |
| 193 | void ReadWritePartPrivate::prepareSaving() |
| 194 | { |
| 195 | // Local file |
| 196 | if (m_url.isLocalFile()) { |
| 197 | if (m_bTemp) { // get rid of a possible temp file first |
| 198 | // (happens if previous url was remote) |
| 199 | QFile::remove(fileName: m_file); |
| 200 | m_bTemp = false; |
| 201 | } |
| 202 | m_file = m_url.toLocalFile(); |
| 203 | } else { |
| 204 | // Remote file |
| 205 | // We haven't saved yet, or we did but locally - provide a temp file |
| 206 | if (m_file.isEmpty() || !m_bTemp) { |
| 207 | QTemporaryFile tempFile; |
| 208 | tempFile.setAutoRemove(false); |
| 209 | tempFile.open(); |
| 210 | m_file = tempFile.fileName(); |
| 211 | m_bTemp = true; |
| 212 | } |
| 213 | // otherwise, we already had a temp file |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | static inline bool makeHardLink(const QString &src, const QString &dest) |
| 218 | { |
| 219 | #ifndef Q_OS_WIN |
| 220 | return ::link(from: QFile::encodeName(fileName: src).constData(), to: QFile::encodeName(fileName: dest).constData()) == 0; |
| 221 | #else |
| 222 | return CreateHardLinkW((LPCWSTR)dest.utf16(), (LPCWSTR)src.utf16(), nullptr) != 0; |
| 223 | #endif |
| 224 | } |
| 225 | |
| 226 | bool ReadWritePart::saveToUrl() |
| 227 | { |
| 228 | Q_D(ReadWritePart); |
| 229 | |
| 230 | if (d->m_url.isLocalFile()) { |
| 231 | setModified(false); |
| 232 | Q_EMIT completed(); |
| 233 | // if m_url is a local file there won't be a temp file -> nothing to remove |
| 234 | Q_ASSERT(!d->m_bTemp); |
| 235 | d->m_saveOk = true; |
| 236 | d->m_duringSaveAs = false; |
| 237 | d->m_originalURL = QUrl(); |
| 238 | d->m_originalFilePath.clear(); |
| 239 | return true; // Nothing to do |
| 240 | } else { |
| 241 | if (d->m_uploadJob) { |
| 242 | QFile::remove(fileName: d->m_uploadJob->srcUrl().toLocalFile()); |
| 243 | d->m_uploadJob->kill(); |
| 244 | d->m_uploadJob = nullptr; |
| 245 | } |
| 246 | QTemporaryFile *tempFile = new QTemporaryFile(); |
| 247 | tempFile->open(); |
| 248 | QString uploadFile = tempFile->fileName(); |
| 249 | delete tempFile; |
| 250 | QUrl uploadUrl = QUrl::fromLocalFile(localfile: uploadFile); |
| 251 | // Create hardlink |
| 252 | if (!makeHardLink(src: d->m_file, dest: uploadFile)) { |
| 253 | // Uh oh, some error happened. |
| 254 | return false; |
| 255 | } |
| 256 | d->m_uploadJob = KIO::file_move(src: uploadUrl, dest: d->m_url, permissions: -1, flags: KIO::Overwrite); |
| 257 | KJobWidgets::setWindow(job: d->m_uploadJob, widget: widget()); |
| 258 | |
| 259 | connect(sender: d->m_uploadJob, signal: &KJob::result, context: this, slot: [d](KJob *job) { |
| 260 | d->slotUploadFinished(job); |
| 261 | }); |
| 262 | |
| 263 | return true; |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | void ReadWritePartPrivate::slotUploadFinished(KJob *) |
| 268 | { |
| 269 | Q_Q(ReadWritePart); |
| 270 | |
| 271 | if (m_uploadJob->error()) { |
| 272 | QFile::remove(fileName: m_uploadJob->srcUrl().toLocalFile()); |
| 273 | QString error = m_uploadJob->errorString(); |
| 274 | m_uploadJob = nullptr; |
| 275 | if (m_duringSaveAs) { |
| 276 | q->setUrl(m_originalURL); |
| 277 | m_file = m_originalFilePath; |
| 278 | } |
| 279 | Q_EMIT q->canceled(errMsg: error); |
| 280 | } else { |
| 281 | #if HAVE_KDIRNOTIFY |
| 282 | ::org::kde::KDirNotify::emitFilesAdded(directory: m_url.adjusted(options: QUrl::RemoveFilename)); |
| 283 | #endif |
| 284 | |
| 285 | m_uploadJob = nullptr; |
| 286 | q->setModified(false); |
| 287 | Q_EMIT q->completed(); |
| 288 | m_saveOk = true; |
| 289 | } |
| 290 | m_duringSaveAs = false; |
| 291 | m_originalURL = QUrl(); |
| 292 | m_originalFilePath.clear(); |
| 293 | if (m_waitForSave) { |
| 294 | m_eventLoop.quit(); |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | bool ReadWritePart::isReadWrite() const |
| 299 | { |
| 300 | Q_D(const ReadWritePart); |
| 301 | |
| 302 | return d->m_bReadWrite; |
| 303 | } |
| 304 | |
| 305 | bool ReadWritePart::isModified() const |
| 306 | { |
| 307 | Q_D(const ReadWritePart); |
| 308 | |
| 309 | return d->m_bModified; |
| 310 | } |
| 311 | |
| 312 | bool ReadWritePart::waitSaveComplete() |
| 313 | { |
| 314 | Q_D(ReadWritePart); |
| 315 | |
| 316 | if (!d->m_uploadJob) { |
| 317 | return d->m_saveOk; |
| 318 | } |
| 319 | |
| 320 | d->m_waitForSave = true; |
| 321 | |
| 322 | d->m_eventLoop.exec(flags: QEventLoop::ExcludeUserInputEvents); |
| 323 | |
| 324 | d->m_waitForSave = false; |
| 325 | |
| 326 | return d->m_saveOk; |
| 327 | } |
| 328 | |
| 329 | #include "moc_readwritepart.cpp" |
| 330 | |