]> git.lyx.org Git - lyx.git/blob - src/EmbeddedFiles.cpp
Fix two embedding bugs that lead to deadloop when loading a bundled .lyx file without...
[lyx.git] / src / EmbeddedFiles.cpp
1 // -*- C++ -*-
2 /**
3  * \file EmbeddedFiles.cpp
4  * This file is part of LyX, the document processor.
5  * Licence details can be found in the file COPYING.
6  *
7  * \author Bo Peng
8  *
9  * Full author contact details are available in file CREDITS.
10  *
11  */
12
13 #include <config.h>
14
15 #include "EmbeddedFiles.h"
16 #include "Buffer.h"
17 #include "BufferParams.h"
18 #include "Paragraph.h"
19 #include "ParIterator.h"
20 #include "debug.h"
21 #include "gettext.h"
22 #include "Format.h"
23
24 #include "frontends/alert.h"
25
26 #include <boost/filesystem/operations.hpp>
27
28 #include "support/filetools.h"
29 #include "support/fs_extras.h"
30 #include "support/convert.h"
31 #include "support/lyxlib.h"
32 #include "support/lstrings.h"
33
34 #include <sstream>
35 #include <fstream>
36 #include <utility>
37
38 using std::ofstream;
39 using std::endl;
40 using std::vector;
41 using std::string;
42 using std::pair;
43 using std::make_pair;
44 using std::istream;
45 using std::ostream;
46 using std::getline;
47 using std::istringstream;
48
49 namespace lyx {
50
51 namespace fs = boost::filesystem;
52 namespace Alert = frontend::Alert;
53
54 using support::FileName;
55 using support::DocFileName;
56 using support::makeAbsPath;
57 using support::addName;
58 using support::onlyPath;
59 using support::absolutePath;
60 using support::onlyFilename;
61 using support::makeRelPath;
62 using support::changeExtension;
63 using support::bformat;
64 using support::prefixIs;
65 using support::sum;
66 using support::makedir;
67
68
69 EmbeddedFile::EmbeddedFile(string const & file, string const & inzip_name,
70         STATUS status, ParConstIterator const & pit)
71         : DocFileName(file, true), inzip_name_(inzip_name), status_(status),
72                 valid_(true), par_it_(pit)
73 {}
74
75
76 string EmbeddedFile::embeddedFile(Buffer const * buf) const
77 {
78         return addName(buf->temppath(), inzip_name_);
79 }
80
81
82 int const EmbeddedFile::parID() const
83 {
84         // some embedded file do not have a valid par iterator
85         return par_it_ == ParConstIterator() ? 0 : par_it_->id();
86 }
87
88
89 void EmbeddedFile::setParIter(ParConstIterator const & pit)
90 {
91         par_it_ = pit;
92 }
93
94
95 string EmbeddedFile::availableFile(Buffer const * buf) const
96 {
97         string ext_file = absFilename();
98         string emb_file = embeddedFile(buf);
99         if (status_ == AUTO) {
100                 // use external file first
101                 if (fs::exists(ext_file))
102                         return ext_file;
103                 else if (fs::exists(emb_file))
104                         return emb_file;
105                 else
106                         return string();
107         } else if (status_ == EMBEDDED) {
108                 // use embedded file first
109                 if (fs::exists(emb_file))
110                         return emb_file;
111                 else if (fs::exists(ext_file))
112                         return ext_file;
113                 else
114                         return string();
115         } else
116                 return string();
117 }
118
119
120 bool EmbeddedFile::extract(Buffer const * buf) const
121 {
122         string ext_file = absFilename();
123         string emb_file = embeddedFile(buf);
124         bool copyFile = false;
125         // both files exist, are different, and in EMBEDDED status
126         if (fs::exists(ext_file) && fs::exists(emb_file) && status_ == EMBEDDED
127                 && sum(*this) != sum(FileName(emb_file))) {
128                 int const ret = Alert::prompt(
129                         _("Overwrite external file?"),
130                         bformat(_("External file %1$s already exists, do you want to overwrite it"),
131                                 from_utf8(ext_file)), 1, 1, _("&Overwrite"), _("&Cancel"));
132                 copyFile = ret == 0;
133         }
134         // copy file in the previous case, and a new case
135         if (copyFile || (!fs::exists(ext_file) && fs::exists(emb_file))) {
136                 try {
137                         // need to make directory?
138                         string path = onlyPath(ext_file);
139                         if (!fs::is_directory(path))
140                                 makedir(const_cast<char*>(path.c_str()), 0755);
141                         fs::copy_file(emb_file, ext_file, false);
142                         return true;
143                 } catch (fs::filesystem_error const & fe) {
144                         Alert::error(_("Copy file failure"),
145                                  bformat(_("Cannot copy file %1$s to %2$s.\n"
146                                            "Please check whether the directory exists and is writeable."),
147                                                 from_utf8(emb_file), from_utf8(ext_file)));
148                         LYXERR(Debug::DEBUG) << "Fs error: " << fe.what() << endl;
149                 }
150         }
151         return false;
152 }
153
154  
155 bool EmbeddedFiles::enabled() const
156 {
157         return buffer_->params().embedded;
158 }
159
160
161 void EmbeddedFiles::enable(bool flag)
162 {
163         if (enabled() != flag) {
164                 // if disable embedding, first extract all embedded files
165                 if (flag || (!flag && extractAll())) {
166                         // file will be changed
167                         buffer_->markDirty();
168                         buffer_->params().embedded = flag;
169                 }
170         }
171 }
172
173
174 void EmbeddedFiles::registerFile(string const & filename,
175         EmbeddedFile::STATUS status, ParConstIterator const & pit)
176 {
177         string abs_filename = makeAbsPath(filename, buffer_->filePath()).absFilename();
178         // try to find this file from the list
179         EmbeddedFileList::iterator it = file_list_.begin();
180         EmbeddedFileList::iterator it_end = file_list_.end();
181         for (; it != it_end; ++it)
182                 if (it->absFilename() == abs_filename || it->embeddedFile(buffer_) == abs_filename)
183                         break;
184         // find this filename
185         if (it != file_list_.end()) {
186                 it->setParIter(pit);
187                 it->setStatus(status);
188                 it->validate();
189                 return;
190         }
191         file_list_.push_back(EmbeddedFile(abs_filename, 
192                 getInzipName(abs_filename), status, pit));
193 }
194
195
196 void EmbeddedFiles::update()
197 {
198         // invalidate all files, obsolete files will then not be validated by the
199         // following document scan. These files will still be kept though, because
200         // they may be added later and their embedding status will be meaningful
201         // again (thinking of cut/paste of an InsetInclude).
202         EmbeddedFileList::iterator it = file_list_.begin();
203         EmbeddedFileList::iterator it_end = file_list_.end();
204         for (; it != it_end; ++it)
205                 // Only AUTO files will be updated. If the status of a file is EMBEDDED, 
206                 // it will be embedded even if it is not referred by a document.
207                 if (it->status() == EmbeddedFile::AUTO)
208                         it->invalidate();
209
210         ParIterator pit = buffer_->par_iterator_begin();
211         ParIterator pit_end = buffer_->par_iterator_end();
212         for (; pit != pit_end; ++pit) {
213                 // For each paragraph, traverse its insets and register embedded files
214                 InsetList::const_iterator iit = pit->insetlist.begin();
215                 InsetList::const_iterator iit_end = pit->insetlist.end();
216                 for (; iit != iit_end; ++iit) {
217                         Inset & inset = *iit->inset;
218                         inset.registerEmbeddedFiles(*buffer_, *this, pit);
219                 }
220         }
221         LYXERR(Debug::FILES) << "Manifest updated: " << endl
222                 << *this
223                 << "End Manifest" << endl;
224 }
225
226
227 bool EmbeddedFiles::write(DocFileName const & filename)
228 {
229         // file in the temporary path has the content
230         string const content = FileName(addName(buffer_->temppath(),
231                 onlyFilename(filename.toFilesystemEncoding()))).toFilesystemEncoding();
232
233         // get a file list and write a manifest file
234         vector<pair<string, string> > filenames;
235         string const manifest = FileName(
236                 addName(buffer_->temppath(), "manifest.txt")).toFilesystemEncoding();
237
238         // write a manifest file
239         ofstream os(manifest.c_str());
240         os << *this;
241         os.close();
242         // prepare list of embedded file
243         EmbeddedFileList::iterator it = file_list_.begin();
244         EmbeddedFileList::iterator it_end = file_list_.end();
245         for (; it != it_end; ++it) {
246                 if (it->valid() && it->embedded()) {
247                         string file = it->availableFile(buffer_);
248                         if (file.empty())
249                                 lyxerr << "File " << it->absFilename() << " does not exist. Skip embedding it. " << endl;
250                         else
251                                 filenames.push_back(make_pair(file, it->inzipName()));
252                 }
253         }
254         // add filename (.lyx) and manifest to filenames
255         filenames.push_back(make_pair(content, onlyFilename(filename.toFilesystemEncoding())));
256         filenames.push_back(make_pair(manifest, "manifest.txt"));
257         // write a zip file with all these files. Write to a temp file first, to
258         // avoid messing up the original file in case something goes terribly wrong.
259         DocFileName zipfile(addName(buffer_->temppath(),
260                 onlyFilename(changeExtension(
261                         filename.toFilesystemEncoding(), ".zip"))));
262
263         ::zipFiles(zipfile.toFilesystemEncoding(), filenames);
264         // copy file back
265         try {
266                 fs::copy_file(zipfile.toFilesystemEncoding(), filename.toFilesystemEncoding(), false);
267         } catch (fs::filesystem_error const & fe) {
268                 Alert::error(_("Save failure"),
269                                  bformat(_("Cannot create file %1$s.\n"
270                                            "Please check whether the directory exists and is writeable."),
271                                          from_utf8(filename.absFilename())));
272                 LYXERR(Debug::DEBUG) << "Fs error: " << fe.what() << endl;
273         }
274         return true;
275 }
276
277
278 EmbeddedFiles::EmbeddedFileList::const_iterator EmbeddedFiles::find(std::string filename) const
279 {
280         EmbeddedFileList::const_iterator it = file_list_.begin();
281         EmbeddedFileList::const_iterator it_end = file_list_.end();
282         for (; it != it_end; ++it)
283                 if (it->absFilename() == filename || it->embeddedFile(buffer_) == filename)     
284                         return it;
285         return file_list_.end();
286 }
287
288
289 bool EmbeddedFiles::extractAll() const
290 {
291         EmbeddedFileList::const_iterator it = file_list_.begin();
292         EmbeddedFileList::const_iterator it_end = file_list_.end();
293         // FIXME: the logic here is hard to decide, we should allow cancel for
294         // 'do not overwrite' this file, and cancel for 'cancel extract all files'.
295         // I am not sure how to do this now.
296         for (; it != it_end; ++it)
297                 if (it->valid() && it->status() != EmbeddedFile::EXTERNAL)
298                         it->extract(buffer_);
299         return true;
300 }
301
302
303 string const EmbeddedFiles::getInzipName(string const & abs_filename)
304 {
305         // register a new one, using relative file path as inzip_name
306         string inzip_name = to_utf8(makeRelPath(from_utf8(abs_filename),
307                 from_utf8(buffer_->fileName())));
308         // if inzip_name is an absolute path, use filename only to avoid
309         // leaking of filesystem information in inzip_name
310         // The second case covers cases '../path/file' and '.'
311         if (absolutePath(inzip_name) || prefixIs(inzip_name, "."))
312                 inzip_name = onlyFilename(abs_filename);
313         // if this name has been used...
314         // use _1_name, _2_name etc
315         string tmp = inzip_name;
316         EmbeddedFileList::iterator it;
317         EmbeddedFileList::iterator it_end = file_list_.end();
318         bool unique_name = false;
319         size_t i = 0;
320         while (!unique_name) {
321                 unique_name = true;
322                 if (i > 0)
323                         inzip_name = convert<string>(i) + "_" + tmp;
324                 it = file_list_.begin();
325                 for (; it != it_end; ++it)
326                         if (it->inzipName() == inzip_name) {
327                                 unique_name = false;
328                                 ++i;
329                                 break;
330                         }
331         }
332         return inzip_name;
333 }
334
335
336 istream & operator>> (istream & is, EmbeddedFiles & files)
337 {
338         files.clear();
339         string tmp;
340         getline(is, tmp);
341         // get version
342         istringstream itmp(tmp);
343         int version;
344         itmp.ignore(string("# LyX manifest version ").size());
345         itmp >> version;
346
347         if (version != 1) {
348                 lyxerr << "This version of LyX can only read LyX manifest version 1" << endl;
349                 return is;
350         }
351
352         getline(is, tmp);
353         if (tmp != "<manifest>") {
354                 lyxerr << "Invalid manifest file, lacking <manifest>" << endl;
355                 return is;
356         }
357         // manifest file may be messed up, be carefully
358         while (is.good()) {
359                 getline(is, tmp);
360                 if (tmp != "<file>")
361                         break;
362
363                 string fname;
364                 getline(is, fname);
365                 string inzip_name;
366                 getline(is, inzip_name);
367                 getline(is, tmp);
368                 istringstream itmp(tmp);
369                 int status;
370                 itmp >> status;
371
372                 getline(is, tmp);
373                 if (tmp != "</file>") {
374                         lyxerr << "Invalid manifest file, lacking </file>" << endl;
375                         break;
376                 }
377
378                 files.registerFile(fname, static_cast<EmbeddedFile::STATUS>(status));
379         };
380         // the last line must be </manifest>
381         if (tmp != "</manifest>") {
382                 lyxerr << "Invalid manifest file, lacking </manifest>" << endl;
383                 return is;
384         }
385         return is;
386 }
387
388
389 ostream & operator<< (ostream & os, EmbeddedFiles const & files)
390 {
391         // store a version so that operator >> can read later versions
392         // using version information.
393         os << "# lyx manifest version 1\n";
394         os << "<manifest>\n";
395         EmbeddedFiles::EmbeddedFileList::const_iterator it = files.begin();
396         EmbeddedFiles::EmbeddedFileList::const_iterator it_end = files.end();
397         for (; it != it_end; ++it) {
398                 if (!it->valid())
399                         continue;
400                 // use differnt lines to make reading easier.
401                 os << "<file>\n"
402                         // save the relative path
403                         << to_utf8(makeRelPath(from_utf8(it->absFilename()),
404                                 from_utf8(files.buffer_->filePath()))) << '\n'
405                         << it->inzipName() << '\n'
406                         << it->status() << '\n'
407                         << "</file>\n";
408         }
409         os << "</manifest>\n";
410         return os;
411 }
412
413 }