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