1 /* This file is part of the Calligra libraries
2    Copyright (C) 2001 Werner Trobin <trobin@kde.org>
3 
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
8 
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 Library General Public License for more details.
13 
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB.  If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
18 */
19 
20 // clazy:excludeall=qstring-arg
21 #include "KoFilterChain.h"
22 
23 #include "KoFilterManager.h"  // KoFilterManager::filterAvailable, private API
24 #include "KoDocumentEntry.h"
25 #include "KoFilterEntry.h"
26 #include "KoDocument.h"
27 #include "KoPart.h"
28 
29 #include "PriorityQueue_p.h"
30 #include "KoFilterGraph.h"
31 #include "KoFilterEdge.h"
32 #include "KoFilterChainLink.h"
33 #include "KoFilterVertex.h"
34 
35 #include <QMetaMethod>
36 #include <QTemporaryFile>
37 #include <QMimeDatabase>
38 
39 #include <MainDebug.h>
40 
41 #include <limits.h> // UINT_MAX
42 
43 // Those "defines" are needed in the setupConnections method below.
44 // Please always keep the strings and the length in sync!
45 using namespace CalligraFilter;
46 
47 
KoFilterChain(const KoFilterManager * manager)48 KoFilterChain::KoFilterChain(const KoFilterManager* manager) :
49         m_manager(manager), m_state(Beginning), m_inputStorage(0),
50         m_inputStorageDevice(0), m_outputStorage(0), m_outputStorageDevice(0),
51         m_inputDocument(0), m_outputDocument(0), m_inputTempFile(0),
52         m_outputTempFile(0), m_inputQueried(Nil), m_outputQueried(Nil), d(0)
53 {
54 }
55 
56 
~KoFilterChain()57 KoFilterChain::~KoFilterChain()
58 {
59     m_chainLinks.deleteAll();
60 
61     if (filterManagerParentChain() && filterManagerParentChain()->m_outputStorage)
62         filterManagerParentChain()->m_outputStorage->leaveDirectory();
63     manageIO(); // Called for the 2nd time in a row -> clean up
64 }
65 
invokeChain()66 KoFilter::ConversionStatus KoFilterChain::invokeChain()
67 {
68     KoFilter::ConversionStatus status = KoFilter::OK;
69 
70     m_state = Beginning;
71     int count = m_chainLinks.count();
72 
73     // This is needed due to nasty Microsoft design
74     const ChainLink* parentChainLink = 0;
75     if (filterManagerParentChain())
76         parentChainLink = filterManagerParentChain()->m_chainLinks.current();
77 
78     // No iterator here, as we need m_chainLinks.current() in outputDocument()
79     m_chainLinks.first();
80     for (; count > 1 && m_chainLinks.current() && status == KoFilter::OK;
81             m_chainLinks.next(), --count) {
82         status = m_chainLinks.current()->invokeFilter(parentChainLink);
83         m_state = Middle;
84         manageIO();
85     }
86 
87     if (!m_chainLinks.current()) {
88         warnFilter << "Huh?? Found a null pointer in the chain";
89         return KoFilter::StupidError;
90     }
91 
92     if (status == KoFilter::OK) {
93         if (m_state & Beginning)
94             m_state |= End;
95         else
96             m_state = End;
97         status = m_chainLinks.current()->invokeFilter(parentChainLink);
98         manageIO();
99     }
100 
101     m_state = Done;
102     if (status == KoFilter::OK)
103         finalizeIO();
104     return status;
105 }
106 
chainOutput() const107 QString KoFilterChain::chainOutput() const
108 {
109     if (m_state == Done)
110         return m_inputFile; // as we already called manageIO()
111     return QString();
112 }
113 
inputFile()114 QString KoFilterChain::inputFile()
115 {
116     if (m_inputQueried == File)
117         return m_inputFile;
118     else if (m_inputQueried != Nil) {
119         warnFilter << "You already asked for some different source.";
120         return QString();
121     }
122     m_inputQueried = File;
123 
124     if (m_state & Beginning) {
125         if (static_cast<KoFilterManager::Direction>(filterManagerDirection()) ==
126                 KoFilterManager::Import)
127             m_inputFile = filterManagerImportFile();
128         else
129             inputFileHelper(filterManagerKoDocument(), filterManagerImportFile());
130     } else
131         if (m_inputFile.isEmpty())
132             inputFileHelper(m_inputDocument, QString());
133 
134     return m_inputFile;
135 }
136 
outputFile()137 QString KoFilterChain::outputFile()
138 {
139     // sanity check: No embedded filter should ask for a plain file
140     // ###### CHECK: This will break as soon as we support exporting embedding filters
141     if (filterManagerParentChain())
142         warnFilter << "An embedded filter has to use storageFile()!";
143 
144     if (m_outputQueried == File)
145         return m_outputFile;
146     else if (m_outputQueried != Nil) {
147         warnFilter << "You already asked for some different destination.";
148         return QString();
149     }
150     m_outputQueried = File;
151 
152     if (m_state & End) {
153         if (static_cast<KoFilterManager::Direction>(filterManagerDirection()) ==
154                 KoFilterManager::Import)
155             outputFileHelper(false);    // This (last) one gets deleted by the caller
156         else
157             m_outputFile = filterManagerExportFile();
158     } else
159         outputFileHelper(true);
160 
161     return m_outputFile;
162 }
163 
storageFile(const QString & name,KoStore::Mode mode)164 KoStoreDevice* KoFilterChain::storageFile(const QString& name, KoStore::Mode mode)
165 {
166     // Plain normal use case
167     if (m_inputQueried == Storage && mode == KoStore::Read &&
168             m_inputStorage && m_inputStorage->mode() == KoStore::Read)
169         return storageNewStreamHelper(&m_inputStorage, &m_inputStorageDevice, name);
170     else if (m_outputQueried == Storage && mode == KoStore::Write &&
171              m_outputStorage && m_outputStorage->mode() == KoStore::Write)
172         return storageNewStreamHelper(&m_outputStorage, &m_outputStorageDevice, name);
173     else if (m_inputQueried == Nil && mode == KoStore::Read)
174         return storageHelper(inputFile(), name, KoStore::Read,
175                              &m_inputStorage, &m_inputStorageDevice);
176     else if (m_outputQueried == Nil && mode == KoStore::Write)
177         return storageHelper(outputFile(), name, KoStore::Write,
178                              &m_outputStorage, &m_outputStorageDevice);
179     else {
180         warnFilter << "Oooops, how did we get here? You already asked for a"
181         << " different source/destination?" << endl;
182         return 0;
183     }
184 }
185 
inputDocument()186 KoDocument* KoFilterChain::inputDocument()
187 {
188     if (m_inputQueried == Document)
189         return m_inputDocument;
190     else if (m_inputQueried != Nil) {
191         warnFilter << "You already asked for some different source.";
192         return 0;
193     }
194 
195     if ((m_state & Beginning) &&
196             static_cast<KoFilterManager::Direction>(filterManagerDirection()) == KoFilterManager::Export &&
197             filterManagerKoDocument())
198         m_inputDocument = filterManagerKoDocument();
199     else if (!m_inputDocument)
200         m_inputDocument = createDocument(inputFile());
201 
202     m_inputQueried = Document;
203     return m_inputDocument;
204 }
205 
outputDocument()206 KoDocument* KoFilterChain::outputDocument()
207 {
208     // sanity check: No embedded filter should ask for a document
209     // ###### CHECK: This will break as soon as we support exporting embedding filters
210     if (filterManagerParentChain()) {
211         warnFilter << "An embedded filter has to use storageFile()!";
212         return 0;
213     }
214 
215     if (m_outputQueried == Document)
216         return m_outputDocument;
217     else if (m_outputQueried != Nil) {
218         warnFilter << "You already asked for some different destination.";
219         return 0;
220     }
221 
222     if ((m_state & End) &&
223             static_cast<KoFilterManager::Direction>(filterManagerDirection()) == KoFilterManager::Import &&
224             filterManagerKoDocument())
225         m_outputDocument = filterManagerKoDocument();
226     else
227         m_outputDocument = createDocument(m_chainLinks.current()->to());
228 
229     m_outputQueried = Document;
230     return m_outputDocument;
231 }
232 
dump()233 void KoFilterChain::dump()
234 {
235     debugFilter << "########## KoFilterChain with" << m_chainLinks.count() << " members:";
236     ChainLink* link = m_chainLinks.first();
237     while (link) {
238         link->dump();
239         link = m_chainLinks.next();
240     }
241     debugFilter << "########## KoFilterChain (done) ##########";
242 }
243 
appendChainLink(KoFilterEntry::Ptr filterEntry,const QByteArray & from,const QByteArray & to)244 void KoFilterChain::appendChainLink(KoFilterEntry::Ptr filterEntry, const QByteArray& from, const QByteArray& to)
245 {
246     m_chainLinks.append(new ChainLink(this, filterEntry, from, to));
247 }
248 
prependChainLink(KoFilterEntry::Ptr filterEntry,const QByteArray & from,const QByteArray & to)249 void KoFilterChain::prependChainLink(KoFilterEntry::Ptr filterEntry, const QByteArray& from, const QByteArray& to)
250 {
251     m_chainLinks.prepend(new ChainLink(this, filterEntry, from, to));
252 }
253 
filterManagerImportFile() const254 QString KoFilterChain::filterManagerImportFile() const
255 {
256     return m_manager->importFile();
257 }
258 
filterManagerExportFile() const259 QString KoFilterChain::filterManagerExportFile() const
260 {
261     return m_manager->exportFile();
262 }
263 
filterManagerKoDocument() const264 KoDocument* KoFilterChain::filterManagerKoDocument() const
265 {
266     return m_manager->document();
267 }
268 
filterManagerDirection() const269 int KoFilterChain::filterManagerDirection() const
270 {
271     return m_manager->direction();
272 }
273 
filterManagerParentChain() const274 KoFilterChain* KoFilterChain::filterManagerParentChain() const
275 {
276     return m_manager->parentChain();
277 }
278 
manageIO()279 void KoFilterChain::manageIO()
280 {
281     m_inputQueried = Nil;
282     m_outputQueried = Nil;
283 
284     delete m_inputStorageDevice;
285     m_inputStorageDevice = 0;
286     if (m_inputStorage) {
287         m_inputStorage->close();
288         delete m_inputStorage;
289         m_inputStorage = 0;
290     }
291     delete m_inputTempFile;  // autodelete
292     m_inputTempFile = 0;
293     m_inputFile.clear();
294 
295     if (!m_outputFile.isEmpty()) {
296         if (m_outputTempFile == 0) {
297             m_inputTempFile = new QTemporaryFile;
298             m_inputTempFile->setAutoRemove(true);
299             m_inputTempFile->setFileName(m_outputFile);
300         }
301         else {
302             m_inputTempFile = m_outputTempFile;
303             m_outputTempFile = 0;
304         }
305         m_inputFile = m_outputFile;
306         m_outputFile.clear();
307         m_inputTempFile = m_outputTempFile;
308         m_outputTempFile = 0;
309 
310         delete m_outputStorageDevice;
311         m_outputStorageDevice = 0;
312         if (m_outputStorage) {
313             m_outputStorage->close();
314             // Don't delete the storage if we're just pointing to the
315             // storage of the parent filter chain
316             if (!filterManagerParentChain() || m_outputStorage->mode() != KoStore::Write)
317                 delete m_outputStorage;
318             m_outputStorage = 0;
319         }
320     }
321 
322     if (m_inputDocument != filterManagerKoDocument())
323         delete m_inputDocument;
324     m_inputDocument = m_outputDocument;
325     m_outputDocument = 0;
326 }
327 
finalizeIO()328 void KoFilterChain::finalizeIO()
329 {
330     // In case we export (to a file, of course) and the last
331     // filter chose to output a KoDocument we have to save it.
332     // Should be very rare, but well...
333     // Note: m_*input*Document as we already called manageIO()
334     if (m_inputDocument &&
335             static_cast<KoFilterManager::Direction>(filterManagerDirection()) == KoFilterManager::Export) {
336         debugFilter << "Saving the output document to the export file " << m_chainLinks.current()->to();
337         m_inputDocument->setOutputMimeType(m_chainLinks.current()->to());
338         m_inputDocument->saveNativeFormat(filterManagerExportFile());
339         m_inputFile = filterManagerExportFile();
340     }
341 }
342 
createTempFile(QTemporaryFile ** tempFile,bool autoDelete)343 bool KoFilterChain::createTempFile(QTemporaryFile** tempFile, bool autoDelete)
344 {
345     if (*tempFile) {
346         errorFilter << "Ooops, why is there already a temp file???" << endl;
347         return false;
348     }
349     *tempFile = new QTemporaryFile();
350     (*tempFile)->setAutoRemove(autoDelete);
351     return (*tempFile)->open();
352 }
353 
354 /*  Note about Windows & usage of QTemporaryFile
355 
356     The QTemporaryFile objects m_inputTempFile and m_outputTempFile are just used
357     to reserve a temporary file with a unique name which then can be used to store
358     an intermediate format. The filters themselves do not get access to these objects,
359     but can query KoFilterChain only for the filename and then have to open the files
360     themselves with their own file handlers (TODO: change this).
361     On Windows this seems to be a problem and results in content not sync'ed to disk etc.
362 
363     So unless someone finds out which flags might be needed on opening the files on
364     Windows to prevent this behaviour (unless these details are hidden away by the
365     Qt abstraction and cannot be influenced), a workaround is to destruct the
366     QTemporaryFile objects right after creation again and just take the name,
367     to avoid having two file handlers on the same file.
368 
369     A better fix might be to use the QTemporaryFile objects also by the filters,
370     instead of having them open the same file on their own again, but that needs more work
371     and is left for... you :)
372 */
373 
inputFileHelper(KoDocument * document,const QString & alternativeFile)374 void KoFilterChain::inputFileHelper(KoDocument* document, const QString& alternativeFile)
375 {
376     if (document) {
377         if (!createTempFile(&m_inputTempFile)) {
378             delete m_inputTempFile;
379             m_inputTempFile = 0;
380             m_inputFile.clear();
381             return;
382         }
383         m_inputFile = m_inputTempFile->fileName();
384         // See "Note about Windows & usage of QTemporaryFile" above
385 #ifdef Q_OS_WIN
386         m_inputTempFile->close();
387         m_inputTempFile->setAutoRemove(true);
388         delete m_inputTempFile;
389         m_inputTempFile = 0;
390 #endif
391         document->setOutputMimeType(m_chainLinks.current()->from());
392         if (!document->saveNativeFormat(m_inputFile)) {
393             delete m_inputTempFile;
394             m_inputTempFile = 0;
395             m_inputFile.clear();
396             return;
397         }
398     } else
399         m_inputFile = alternativeFile;
400 }
401 
outputFileHelper(bool autoDelete)402 void KoFilterChain::outputFileHelper(bool autoDelete)
403 {
404     if (!createTempFile(&m_outputTempFile, autoDelete)) {
405         delete m_outputTempFile;
406         m_outputTempFile = 0;
407         m_outputFile.clear();
408     } else {
409         m_outputFile = m_outputTempFile->fileName();
410 
411         // See "Note about Windows & usage of QTemporaryFile" above
412 #ifdef Q_OS_WIN
413         m_outputTempFile->close();
414         m_outputTempFile->setAutoRemove(true);
415         delete m_outputTempFile;
416         m_outputTempFile = 0;
417 #endif
418     }
419 }
420 
storageNewStreamHelper(KoStore ** storage,KoStoreDevice ** device,const QString & name)421 KoStoreDevice* KoFilterChain::storageNewStreamHelper(KoStore** storage, KoStoreDevice** device,
422         const QString& name)
423 {
424     delete *device;
425     *device = 0;
426     if ((*storage)->isOpen())
427         (*storage)->close();
428     if ((*storage)->bad())
429         return storageCleanupHelper(storage);
430     if (!(*storage)->open(name))
431         return 0;
432 
433     *device = new KoStoreDevice(*storage);
434     return *device;
435 }
436 
storageHelper(const QString & file,const QString & streamName,KoStore::Mode mode,KoStore ** storage,KoStoreDevice ** device)437 KoStoreDevice* KoFilterChain::storageHelper(const QString& file, const QString& streamName,
438         KoStore::Mode mode, KoStore** storage,
439         KoStoreDevice** device)
440 {
441     if (file.isEmpty())
442         return 0;
443     if (*storage) {
444         debugFilter << "Uh-oh, we forgot to clean up...";
445         return 0;
446     }
447 
448     storageInit(file, mode, storage);
449 
450     if ((*storage)->bad())
451         return storageCleanupHelper(storage);
452 
453     // Seems that we got a valid storage, at least. Even if we can't open
454     // the stream the "user" asked us to open, we nonetheless change the
455     // IOState from File to Storage, as it might be possible to open other streams
456     if (mode == KoStore::Read)
457         m_inputQueried = Storage;
458     else // KoStore::Write
459         m_outputQueried = Storage;
460 
461     return storageCreateFirstStream(streamName, storage, device);
462 }
463 
storageInit(const QString & file,KoStore::Mode mode,KoStore ** storage)464 void KoFilterChain::storageInit(const QString& file, KoStore::Mode mode, KoStore** storage)
465 {
466     QByteArray appIdentification("");
467     if (mode == KoStore::Write) {
468         // To create valid storages we also have to add the mimetype
469         // magic "applicationIndentifier" to the storage.
470         // As only filters with a Calligra destination should query
471         // for a storage to write to, we don't check the content of
472         // the mimetype here. It doesn't do a lot of harm if someone
473         // "abuses" this method.
474         appIdentification = m_chainLinks.current()->to();
475     }
476     *storage = KoStore::createStore(file, mode, appIdentification);
477 }
478 
storageCreateFirstStream(const QString & streamName,KoStore ** storage,KoStoreDevice ** device)479 KoStoreDevice* KoFilterChain::storageCreateFirstStream(const QString& streamName, KoStore** storage,
480         KoStoreDevice** device)
481 {
482     if (!(*storage)->open(streamName))
483         return 0;
484 
485     if (*device) {
486         debugFilter << "Uh-oh, we forgot to clean up the storage device!";
487         (*storage)->close();
488         return storageCleanupHelper(storage);
489     }
490     *device = new KoStoreDevice(*storage);
491     return *device;
492 }
493 
storageCleanupHelper(KoStore ** storage)494 KoStoreDevice* KoFilterChain::storageCleanupHelper(KoStore** storage)
495 {
496     // Take care not to delete the storage of the parent chain
497     if (*storage != m_outputStorage || !filterManagerParentChain() ||
498             (*storage)->mode() != KoStore::Write)
499         delete *storage;
500     *storage = 0;
501     return 0;
502 }
503 
createDocument(const QString & file)504 KoDocument* KoFilterChain::createDocument(const QString& file)
505 {
506     QUrl url;
507     url.setPath(file);
508     QMimeType t = QMimeDatabase().mimeTypeForUrl(url);
509     if (t.isDefault()) {
510         errorFilter << "No mimetype found for " << file << endl;
511         return 0;
512     }
513 
514     KoDocument *doc = createDocument(t.name().toLatin1());
515 
516     if (!doc || !doc->loadNativeFormat(file)) {
517         errorFilter << "Couldn't load from the file" << endl;
518         delete doc;
519         return 0;
520     }
521     return doc;
522 }
523 
createDocument(const QByteArray & mimeType)524 KoDocument* KoFilterChain::createDocument(const QByteArray& mimeType)
525 {
526     KoDocumentEntry entry = KoDocumentEntry::queryByMimeType(mimeType);
527 
528     if (entry.isEmpty()) {
529         errorFilter << "Couldn't find a part that can handle mimetype " << mimeType << endl;
530     }
531 
532     QString errorMsg;
533     KoPart *part = entry.createKoPart(&errorMsg);
534     if (!part) {
535         errorFilter << "Couldn't create the document: " << errorMsg << endl;
536         return 0;
537     }
538     return part->document();
539 }
540 
weight() const541 int KoFilterChain::weight() const
542 {
543     return m_chainLinks.count();
544 }
545