Merge lp:~mandel/ubuntu-download-manager/support-data-uri into lp:ubuntu-download-manager

Proposed by Manuel de la Peña
Status: Merged
Approved by: Michael Sheldon
Approved revision: 340
Merged at revision: 335
Proposed branch: lp:~mandel/ubuntu-download-manager/support-data-uri
Merge into: lp:ubuntu-download-manager
Diff against target: 1154 lines (+613/-232)
5 files modified
src/downloads/priv/ubuntu/downloads/file_download.cpp (+319/-188)
src/downloads/priv/ubuntu/downloads/file_download.h (+3/-4)
tests/downloads/daemon/CMakeLists.txt (+28/-28)
tests/downloads/daemon/test_download.cpp (+254/-12)
tests/downloads/daemon/test_download.h (+9/-0)
To merge this branch: bzr merge lp:~mandel/ubuntu-download-manager/support-data-uri
Reviewer Review Type Date Requested Status
Michael Sheldon (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+247317@code.launchpad.net

Commit message

Add support for data uris.

Description of the change

Add support to data uri to download manager with out changing the client API/ABI. The idea is that a client can provide a data uri and the download manager will extract as much information as possible from it regarding the mime type etc.. and will write the data in the file system following the exact same security checks and supporting things such as the post processing of the files and its deflation.

To post a comment you must log in.
336. By Manuel de la Peña

Remove no needed header added by the IDE. Remove some left over debug msgs.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
337. By Manuel de la Peña

Deal with the case in which http is in the data uri.

338. By Manuel de la Peña

Ensure we use the entire path.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
339. By Manuel de la Peña

Remove log that brings not info.

340. By Manuel de la Peña

Add a comment stating the hack.

Revision history for this message
Michael Sheldon (michael-sheldon) wrote :

Looks good :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/downloads/priv/ubuntu/downloads/file_download.cpp'
2--- src/downloads/priv/ubuntu/downloads/file_download.cpp 2014-11-17 20:34:24 +0000
3+++ src/downloads/priv/ubuntu/downloads/file_download.cpp 2015-01-23 11:59:29 +0000
4@@ -16,20 +16,22 @@
5 * Boston, MA 02110-1301, USA.
6 */
7
8+#include <map>
9+
10+#include <glog/logging.h>
11+
12+#include <QBuffer>
13+#include <QCryptographicHash>
14 #include <QDBusConnection>
15 #include <QDBusMessage>
16-#include <QBuffer>
17-#include <QCryptographicHash>
18 #include <QDir>
19-#include <QStringList>
20 #include <QFile>
21 #include <QFileInfo>
22+#include <QMimeDatabase>
23+#include <QMimeType>
24+#include <QStringList>
25 #include <QSslError>
26
27-#include <glog/logging.h>
28-
29-#include <map>
30-
31 #include <ubuntu/transfers/metadata.h>
32 #include <ubuntu/transfers/system/dbus_connection.h>
33 #include <ubuntu/transfers/system/hash_algorithm.h>
34@@ -45,15 +47,6 @@
35
36 #define DOWN_LOG(LEVEL) LOG(LEVEL) << ((parent() != nullptr)?"GroupDownload {" + parent()->objectName() + " } ":"") << "Download ID{" << objectName() << " } "
37
38-namespace {
39- const QString PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties";
40- const QString DOWNLOAD_INTERFACE = "com.canonical.applications.Download";
41- const QString PROPERTIES_CHANGED = "PropertiesChanged";
42- const QString CLICK_PACKAGE_PROPERTY = "ClickPackage";
43- const QString SHOW_INDICATOR_PROPERTY = "ShowInIndicator";
44- const QString TITLE_PROPERTY = "Title";
45-}
46-
47 std::ostream& operator<<(std::ostream &out, QNetworkReply::NetworkError err) {
48 static std::map<QNetworkReply::NetworkError, std::string> errorsMap {
49 {QNetworkReply::ConnectionRefusedError, "ConnectionRefusedError: the remote "
50@@ -148,7 +141,12 @@
51 }
52
53 namespace {
54-
55+ const QString PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties";
56+ const QString DOWNLOAD_INTERFACE = "com.canonical.applications.Download";
57+ const QString PROPERTIES_CHANGED = "PropertiesChanged";
58+ const QString CLICK_PACKAGE_PROPERTY = "ClickPackage";
59+ const QString SHOW_INDICATOR_PROPERTY = "ShowInIndicator";
60+ const QString TITLE_PROPERTY = "Title";
61 const QString DATA_FILE_NAME = "data.download";
62 const QString NETWORK_ERROR = "NETWORK ERROR";
63 const QString HASH_ERROR = "HASH ERROR";
64@@ -161,6 +159,7 @@
65 const QString UNEXPECTED_ERROR = "UNEXPECTED_ERROR";
66 const QByteArray CONTENT_DISPOSITION = "Content-Disposition";
67 const QByteArray CONTENT_TYPE = "Content-Type";
68+ const QString DATA_URI_PREFIX = "data:";
69 }
70
71 namespace Ubuntu {
72@@ -245,31 +244,37 @@
73 void
74 FileDownload::pauseTransfer() {
75 TRACE << _url;
76- if (_reply == nullptr) {
77- // cannot pause because is not running
78- DOWN_LOG(INFO) << "Cannot pause download because reply is NULL";
79- DOWN_LOG(INFO) << "EMIT paused(false)";
80- emit paused(false);
81- return;
82- }
83-
84- DOWN_LOG(INFO) << "Pausing download" << _url;
85- // we need to disconnect the signals to ensure that they are not
86- // emitted due to the operation we are going to perform. We read
87- // the data in the reply and store it in a file
88- disconnectFromReplySignals();
89-
90- // do abort before reading
91- _reply->abort();
92- _currentData->write(_reply->readAll());
93- if (!flushFile()) {
94+ if (_url.toString().contains(DATA_URI_PREFIX)) {
95+ DOWN_LOG(INFO) << "EMIT paused(false) since we are using a data uri";
96+ _downloading = false;
97 emit paused(false);
98 } else {
99- _reply->deleteLater();
100- _reply = nullptr;
101- DOWN_LOG(INFO) << "EMIT paused(true)";
102- _downloading = false;
103- emit paused(true);
104+ if (_reply == nullptr) {
105+ // cannot pause because is not running
106+ DOWN_LOG(INFO) << "Cannot pause download because reply is NULL";
107+ DOWN_LOG(INFO) << "EMIT paused(false)";
108+ emit paused(false);
109+ return;
110+ }
111+
112+ DOWN_LOG(INFO) << "Pausing download" << _url;
113+ // we need to disconnect the signals to ensure that they are not
114+ // emitted due to the operation we are going to perform. We read
115+ // the data in the reply and store it in a file
116+ disconnectFromReplySignals();
117+
118+ // do abort before reading
119+ _reply->abort();
120+ _currentData->write(_reply->readAll());
121+ if (!flushFile()) {
122+ emit paused(false);
123+ } else {
124+ _reply->deleteLater();
125+ _reply = nullptr;
126+ DOWN_LOG(INFO) << "EMIT paused(true)";
127+ _downloading = false;
128+ emit paused(true);
129+ }
130 }
131 }
132
133@@ -285,23 +290,32 @@
134 return;
135 }
136
137- DOWN_LOG(INFO) << "Resuming download.";
138- QNetworkRequest request = buildRequest();
139-
140- // overrides the range header, we do not let clients set the range!!!
141- qint64 currentDataSize = _currentData->size();
142- QByteArray rangeHeaderValue = "bytes=" +
143- QByteArray::number(currentDataSize) + "-";
144- request.setRawHeader("Range", rangeHeaderValue);
145-
146- _reply = _requestFactory->get(request);
147- _reply->setReadBufferSize(throttle());
148-
149- connectToReplySignals();
150-
151- DOWN_LOG(INFO) << "EMIT resumed(true)";
152- _downloading = true;
153- emit resumed(true);
154+ // it is not very probable, yet possible that we do reach this point with a data uri
155+
156+ if (_url.toString().contains(DATA_URI_PREFIX)) {
157+ DOWN_LOG(INFO) << "EMIT resumed(true)";
158+ _downloading = true;
159+ emit resumed(true);
160+ writeDataUri();
161+ } else {
162+ DOWN_LOG(INFO) << "Resuming download.";
163+ QNetworkRequest request = buildRequest();
164+
165+ // overrides the range header, we do not let clients set the range!!!
166+ qint64 currentDataSize = _currentData->size();
167+ QByteArray rangeHeaderValue = "bytes=" +
168+ QByteArray::number(currentDataSize) + "-";
169+ request.setRawHeader("Range", rangeHeaderValue);
170+
171+ _reply = _requestFactory->get(request);
172+ _reply->setReadBufferSize(throttle());
173+
174+ connectToReplySignals();
175+
176+ DOWN_LOG(INFO) << "EMIT resumed(true)";
177+ _downloading = true;
178+ emit resumed(true);
179+ }
180 }
181
182 void
183@@ -326,16 +340,29 @@
184 return;
185 }
186
187- DOWN_LOG(INFO) << "Network is accessible, performing download request";
188- // signals should take care of calling deleteLater on the
189- // NetworkReply object
190- _reply = _requestFactory->get(buildRequest());
191- _reply->setReadBufferSize(throttle());
192-
193- connectToReplySignals();
194- DOWN_LOG(INFO) << "EMIT started(true)";
195- _downloading = true;
196- emit started(true);
197+ // make a diff between a data uri and a "normal" download
198+ if (_url.toString().contains(DATA_URI_PREFIX)) {
199+ DOWN_LOG(INFO) << "Performing a data uri download.";
200+ // we emit the signals and then write the data in the file system, which will emit the download finished signal
201+ // the signals are queued so they will happen in the correct order
202+
203+ DOWN_LOG(INFO) << "EMIT started(true)";
204+ _downloading = true;
205+ emit started(true);
206+
207+ writeDataUri();
208+ } else {
209+ DOWN_LOG(INFO) << "Performing a network download.";
210+ // signals should take care of calling deleteLater on the
211+ // NetworkReply object
212+ _reply = _requestFactory->get(buildRequest());
213+ _reply->setReadBufferSize(throttle());
214+
215+ connectToReplySignals();
216+ DOWN_LOG(INFO) << "EMIT started(true)";
217+ _downloading = true;
218+ emit started(true);
219+ }
220 }
221
222 qulonglong
223@@ -472,7 +499,7 @@
224 if (!flushFile()) {
225 return;
226 }
227- qulonglong received = _currentData->size();
228+ auto received = static_cast<qulonglong>(_currentData->size());
229
230 if (bytesTotal == -1) {
231 // we do not know the size of the download, simply return
232@@ -481,12 +508,11 @@
233 return;
234 } else {
235 if (_totalSize == 0) {
236- qlonglong uBytestTotal = bytesTotal;
237 // bytesTotal is different when we have resumed because we
238 // are not counting the size that we already downloaded,
239 // therefore we only do this once
240 // update the metadata
241- _totalSize = uBytestTotal;
242+ _totalSize = static_cast<qulonglong>(bytesTotal);
243 }
244 emit Download::progress(received, _totalSize);
245 return;
246@@ -579,122 +605,10 @@
247 // ensure that if content-disposition is present we will use it
248 updateFileNamePerContentDisposition();
249
250- if(!hashIsValid()) {
251- emitError(HASH_ERROR);
252- return;
253- }
254-
255- // if the download was set to be deflated and the content type is correct
256- // we will do our best to deflate the file
257- if (_metadata.contains(Metadata::DEFLATE_KEY)
258- && _metadata[Metadata::DEFLATE_KEY].toBool()
259- && _reply->hasRawHeader(CONTENT_TYPE)
260- && HeaderParser::isDeflatableContentType(
261- _reply->rawHeader(CONTENT_TYPE))){
262- }
263-
264-
265- // there are three possible cases, in the first case we are requested
266- // to extract the file, in which case we start a special helper process.
267- // In the second case we are requested to execute a specific command, in
268- // either of these two cases we only raise the finished signal once the
269- // processing is complete. Or in the third case we have no special requests
270- // and we indicate that the download is finished.
271- if (_metadata.contains(Metadata::EXTRACT_KEY)
272- && _metadata[Metadata::EXTRACT_KEY].toBool()
273- && _reply->hasRawHeader(CONTENT_TYPE)
274- && _reply->rawHeader(CONTENT_TYPE) == "application/zip") {
275-
276- Process* postDownloadProcess =
277- ProcessFactory::instance()->createProcess();
278-
279- CHECK(connect(postDownloadProcess, &Process::finished,
280- this, &FileDownload::onProcessFinished))
281- << "Could not connect to signal";
282- CHECK(connect(postDownloadProcess, &Process::error,
283- this, &FileDownload::onProcessError))
284- << "Could not connect to signal";
285-
286- DOWN_LOG(INFO) << "Renaming '" << _tempFilePath << "'"
287- << "' to '" << _filePath << "'";
288-
289- auto fileMan = FileManager::instance();
290- if (fileMan->exists(_tempFilePath)) {
291- LOG(INFO) << "Renaming: '" << _tempFilePath << "' to '"
292- << _filePath << "'";
293- fileMan->rename(_tempFilePath, _filePath);
294- }
295-
296- QFileInfo fileInfo(filePath());
297-
298- QString command = HELPER_DIR + QString(QDir::separator()) + "udm-extractor";
299- QStringList args;
300- args << "--unzip";
301- args << "--path";
302- args << filePath();
303- args << "--destination";
304- args << fileInfo.dir().absolutePath();
305-
306- DOWN_LOG(INFO) << "Executing" << command << args;
307- postDownloadProcess->start(command, args);
308- return;
309- } else if (_metadata.contains(Metadata::COMMAND_KEY)) {
310- // just emit processing if we DO NOT have a hash because else we
311- // already emitted it.
312- if (_hash.isEmpty()) {
313- emit processing(filePath());
314- }
315- // toStringList will return an empty list if it cannot be converted
316- QStringList commandData =
317- _metadata[Metadata::COMMAND_KEY].toStringList();
318- if (commandData.count() == 0) {
319- DOWN_LOG(ERROR) << "COMMAND DATA MISSING";
320- emitError(COMMAND_ERROR);
321- return;
322- } else {
323- // first item of the string list is the command
324- // the rest is the arguments
325- DOWN_LOG(INFO) << "Renaming '" << _tempFilePath << "'"
326- << "' to '" << _filePath << "'";
327-
328- auto fileMan = FileManager::instance();
329- if (fileMan->exists(_tempFilePath)) {
330- LOG(INFO) << "Renaming: '" << _tempFilePath << "' to '"
331- << _filePath << "'";
332- fileMan->rename(_tempFilePath, _filePath);
333- }
334-
335- QString command = commandData.at(0);
336- commandData.removeAt(0);
337- QStringList args;
338-
339- foreach(const QString& arg, commandData) {
340- if (arg == Metadata::COMMAND_FILE_KEY)
341- args << filePath();
342- else
343- args << arg;
344- }
345-
346- Process* postDownloadProcess =
347- ProcessFactory::instance()->createProcess();
348-
349- // connect to signals so that we can tell the clients that
350- // the operation succeed
351-
352- CHECK(connect(postDownloadProcess, &Process::finished,
353- this, &FileDownload::onProcessFinished))
354- << "Could not connect to signal";
355- CHECK(connect(postDownloadProcess, &Process::error,
356- this, &FileDownload::onProcessError))
357- << "Could not connect to signal";
358-
359- DOWN_LOG(INFO) << "Executing" << command << args;
360- postDownloadProcess->start(command, args);
361- return;
362- }
363- } else {
364- emitFinished();
365- }
366+ auto contentType = (_reply->hasRawHeader(CONTENT_TYPE))?
367+ QString(_reply->rawHeader(CONTENT_TYPE)) : QString();
368+
369+ downloadPostProcessing(contentType);
370
371 // clean the reply
372 _reply->deleteLater();
373@@ -702,6 +616,114 @@
374 }
375
376 void
377+FileDownload::writeDataUri() {
378+ // the expected format of the data is as follows:
379+ //
380+ // data:[<MIME-type>][;charset=<encoding>][;base64],<data>
381+ //
382+ // The encoding is indicated by ;base64. If it is present the data is encoded as base64. Without it the data
383+ // (as a sequence of octets) is represented using ASCII encoding for octets inside the range of safe URL
384+ // characters. If <MIME-type> is omitted, it defaults to text/plain;charset=US-ASCII.
385+ // An example uri is:
386+ //
387+ // data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
388+
389+ // HACK:
390+ // it can also be seen with the url prefixed as it happens in google images:
391+ //
392+ // http://images.google.com/data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
393+ //
394+ // this is due to a bug found in oxide: https://bugs.launchpad.net/oxide/+bug/1413964 and should be removed
395+ // whenever asap
396+ QString urlString = QString::null;
397+ auto urlStringParts = _url.toString().split(DATA_URI_PREFIX);
398+ if (urlStringParts.count() > 1) {
399+ urlString = urlStringParts[1];
400+ } else {
401+ urlString = urlStringParts[0];
402+ }
403+
404+ QByteArray data;
405+ QMimeDatabase db;
406+ QMimeType mimeType; // init to an invalid type
407+ QString extension = QString::null;
408+ bool isBase64 = false;
409+
410+ foreach(const QMimeType& type, db.allMimeTypes()) {
411+ // we need to check for each of the aliases that a mime type has
412+ foreach(const QString& alias, type.aliases()) {
413+ if (urlString.startsWith(alias)) {
414+ DOWN_LOG(INFO) << "Data mime type is " << alias.toStdString();
415+ mimeType = type;
416+ urlString = urlString.replace(alias + ";", "");
417+ break;
418+ }
419+ }
420+
421+ // we could have set the mime type and jumped outside the previous loop
422+ if (mimeType.isValid()) {
423+ break;
424+ }
425+ }
426+ if (!mimeType.isValid()) {
427+ LOG(INFO) << "MIME TYPE IS NOT VALID";
428+ if (!urlString.startsWith(';')) {
429+ DOWN_LOG(INFO) << "Database does not have type yet it is present.";
430+ auto parts = urlString.split(';');
431+ extension = parts[0].split('/')[1];
432+ urlString = parts[1];
433+ } else {
434+ //defaults to text/plain;charset=US-ASCII.
435+ mimeType = db.mimeTypeForData("text/plain");
436+ urlString = urlString.split(';')[1];
437+ }
438+ }
439+
440+ if (urlString.startsWith("base64,")) {
441+ DOWN_LOG(INFO) << "Data uri is base64 encoded";
442+ isBase64 = true;
443+ urlString = urlString.replace("base64,", "");
444+ } else {
445+ auto parts = urlString.split(',');
446+ if (parts.count() != 2) {
447+ DOWN_LOG(WARNING) << "Data uri malformed.";
448+ } else {
449+ urlString = parts[1];
450+ }
451+ }
452+ // load the data from the string and write it in the file system, the uuid will be used as the
453+ // file name + the mime type extension
454+ if (isBase64) {
455+ QByteArray encoded;
456+ encoded.append(urlString);
457+ data = QByteArray::fromBase64(encoded);
458+ } else {
459+ QByteArray encoded;
460+ encoded.append(urlString);
461+ data = QByteArray::fromPercentEncoding(encoded);
462+ }
463+
464+ _totalSize = static_cast<qulonglong>(data.size());
465+
466+ // we need to update the file name using the mime type extension (the file name should be ok already), nevertheless
467+ // we are going to write the data in the temp file so that the post processing of the download works correctky
468+ QFileInfo fiFilePath(_filePath);
469+ auto currentFileName = fiFilePath.fileName();
470+ extension = (mimeType.isValid())?mimeType.suffixes()[0]:extension;
471+ auto newPath = _filePath.replace(currentFileName, transferId() + "." + extension);
472+
473+ // unlock the old path and lock the new one
474+ _fileNameMutex->unlockFileName(_filePath);
475+ _filePath = _fileNameMutex->lockFileName(newPath);
476+
477+ DOWN_LOG(INFO) << "Data uri file path is '" << _filePath << "'";
478+ _currentData->write(data);
479+
480+ // deal with the post processing of the created file
481+ downloadPostProcessing(mimeType.aliases()[0]);
482+}
483+
484+void
485 FileDownload::onFinished() {
486 TRACE << _url;
487 // the reply has finished but the resource might have been moved
488@@ -907,10 +929,10 @@
489 // addData is smart enough to not load the entire file in memory
490 hash->addData(_currentData->device());
491 QString fileSig = QString(hash->result().toHex());
492- if (fileSig != _hash) {
493+ if (fileSig != _hash) {
494 DOWN_LOG(ERROR) << HASH_ERROR << fileSig << "!=" << _hash;
495 return false;
496- }
497+ }
498 }
499 return true;
500 }
501@@ -948,6 +970,115 @@
502 }
503
504 void
505+FileDownload::downloadPostProcessing(const QString& contentType) {
506+ TRACE << _url;
507+
508+ if(!hashIsValid()) {
509+ emitError(HASH_ERROR);
510+ return;
511+ }
512+
513+ // there are three possible cases, in the first case we are requested
514+ // to extract the file, in which case we start a special helper process.
515+ // In the second case we are requested to execute a specific command, in
516+ // either of these two cases we only raise the finished signal once the
517+ // processing is complete. Or in the third case we have no special requests
518+ // and we indicate that the download is finished.
519+ if (_metadata.contains(Metadata::EXTRACT_KEY)
520+ && _metadata[Metadata::EXTRACT_KEY].toBool()
521+ && contentType == "application/zip") {
522+
523+ auto postDownloadProcess = ProcessFactory::instance()->createProcess();
524+
525+ CHECK(connect(postDownloadProcess, &Process::finished,
526+ this, &FileDownload::onProcessFinished))
527+ << "Could not connect to signal";
528+ CHECK(connect(postDownloadProcess, &Process::error,
529+ this, &FileDownload::onProcessError))
530+ << "Could not connect to signal";
531+
532+ DOWN_LOG(INFO) << "Renaming '" << _tempFilePath << "'"
533+ << "' to '" << _filePath << "'";
534+
535+ auto fileMan = FileManager::instance();
536+ if (fileMan->exists(_tempFilePath)) {
537+ LOG(INFO) << "Renaming: '" << _tempFilePath << "' to '"
538+ << _filePath << "'";
539+ fileMan->rename(_tempFilePath, _filePath);
540+ }
541+
542+ QFileInfo fileInfo(filePath());
543+
544+ QString command = HELPER_DIR + QString(QDir::separator()) + "udm-extractor";
545+ QStringList args;
546+ args << "--unzip";
547+ args << "--path";
548+ args << filePath();
549+ args << "--destination";
550+ args << fileInfo.dir().absolutePath();
551+
552+ DOWN_LOG(INFO) << "Executing" << command << args;
553+ postDownloadProcess->start(command, args);
554+ return;
555+ } else if (_metadata.contains(Metadata::COMMAND_KEY)) {
556+ // just emit processing if we DO NOT have a hash because else we
557+ // already emitted it.
558+ if (_hash.isEmpty()) {
559+ emit processing(filePath());
560+ }
561+ // toStringList will return an empty list if it cannot be converted
562+ QStringList commandData =
563+ _metadata[Metadata::COMMAND_KEY].toStringList();
564+ if (commandData.count() == 0) {
565+ DOWN_LOG(ERROR) << "COMMAND DATA MISSING";
566+ emitError(COMMAND_ERROR);
567+ return;
568+ } else {
569+ // first item of the string list is the command
570+ // the rest is the arguments
571+ DOWN_LOG(INFO) << "Renaming '" << _tempFilePath << "'"
572+ << "' to '" << _filePath << "'";
573+
574+ auto fileMan = FileManager::instance();
575+ if (fileMan->exists(_tempFilePath)) {
576+ LOG(INFO) << "Renaming: '" << _tempFilePath << "' to '"
577+ << _filePath << "'";
578+ fileMan->rename(_tempFilePath, _filePath);
579+ }
580+
581+ QString command = commandData.at(0);
582+ commandData.removeAt(0);
583+ QStringList args;
584+
585+ foreach(const QString& arg, commandData) {
586+ if (arg == Metadata::COMMAND_FILE_KEY)
587+ args << filePath();
588+ else
589+ args << arg;
590+ }
591+
592+ auto postDownloadProcess = ProcessFactory::instance()->createProcess();
593+
594+ // connect to signals so that we can tell the clients that
595+ // the operation succeed
596+
597+ CHECK(connect(postDownloadProcess, &Process::finished,
598+ this, &FileDownload::onProcessFinished))
599+ << "Could not connect to signal";
600+ CHECK(connect(postDownloadProcess, &Process::error,
601+ this, &FileDownload::onProcessError))
602+ << "Could not connect to signal";
603+
604+ DOWN_LOG(INFO) << "Executing" << command << args;
605+ postDownloadProcess->start(command, args);
606+ return;
607+ }
608+ } else {
609+ emitFinished();
610+ }
611+}
612+
613+void
614 FileDownload::emitFinished() {
615 auto fileMan = FileManager::instance();
616
617
618=== modified file 'src/downloads/priv/ubuntu/downloads/file_download.h'
619--- src/downloads/priv/ubuntu/downloads/file_download.h 2014-10-22 23:40:53 +0000
620+++ src/downloads/priv/ubuntu/downloads/file_download.h 2015-01-23 11:59:29 +0000
621@@ -16,8 +16,7 @@
622 * Boston, MA 02110-1301, USA.
623 */
624
625-#ifndef DOWNLOADER_LIB_SINGLE_DOWNLOAD_H
626-#define DOWNLOADER_LIB_SINGLE_DOWNLOAD_H
627+#pragma once
628
629 #include <QDBusContext>
630 #include <QFile>
631@@ -123,8 +122,10 @@
632 bool hashIsValid();
633 void init();
634 void initFileNames();
635+ void downloadPostProcessing(const QString& contentType);
636 void unlockFilePath();
637 void updateFileNamePerContentDisposition();
638+ void writeDataUri();
639
640 // slots used to react to signals
641 void onDownloadProgress(qint64 currentProgress, qint64);
642@@ -161,5 +162,3 @@
643 } // DownloadManager
644
645 } // Ubuntu
646-
647-#endif // DOWNLOADER_LIB_SINGLE_DOWNLOAD_H
648
649=== modified file 'tests/downloads/daemon/CMakeLists.txt'
650--- tests/downloads/daemon/CMakeLists.txt 2014-12-01 09:20:00 +0000
651+++ tests/downloads/daemon/CMakeLists.txt 2015-01-23 11:59:29 +0000
652@@ -15,26 +15,26 @@
653 # Authored by: Manuel de la Peña <manuel.delapena@canonical.com>
654
655 set(DAEMON_TESTS
656- test_apn_request_factory
657- test_apparmor
658+ test_apn_request_factory
659+ test_apparmor
660 test_base_download
661- test_cancel_download_transition
662+ test_cancel_download_transition
663 test_daemon
664- test_download
665+ test_download
666 test_download_factory
667- test_download_manager
668- test_downloads_db
669- test_file_download_sm
670- test_filename_mutex
671- test_final_state
672+ test_download_manager
673+ test_downloads_db
674+ test_file_download_sm
675+ test_filename_mutex
676+ test_final_state
677 test_group_download
678 test_metadata
679- test_mms_download
680- test_network_error_transition
681- test_resume_download_transition
682- test_ssl_error_transition
683- test_start_download_transition
684- test_stop_request_transition
685+ test_mms_download
686+ test_network_error_transition
687+ test_resume_download_transition
688+ test_ssl_error_transition
689+ test_start_download_transition
690+ test_stop_request_transition
691 test_transfers_queue
692 )
693
694@@ -47,14 +47,14 @@
695 set(${test}_DAEMON_HEADERS
696 application.h
697 cryptographic_hash.h
698- database.h
699- dbus_proxy.h
700- dbus_proxy_factory.h
701- download.h
702+ database.h
703+ dbus_proxy.h
704+ dbus_proxy_factory.h
705+ download.h
706 factory.h
707 filename_mutex.h
708- manager.h
709- pending_reply.h
710+ manager.h
711+ pending_reply.h
712 queue.h
713 sm_file_download.h
714 timer.h
715@@ -86,18 +86,18 @@
716 link_directories(${GLOG_DBUS_LIBDIR})
717
718 set(DAEMON_TESTS_LIBS
719- ${NIH_DBUS_LIBRARIES}
720- ${GLOG_LIBRARIES}
721+ ${NIH_DBUS_LIBRARIES}
722+ ${GLOG_LIBRARIES}
723 ${Qt5Core_LIBRARIES}
724 ${Qt5Sql_LIBRARIES}
725 ${Qt5DBus_LIBRARIES}
726- ${Qt5Test_LIBRARIES}
727+ ${Qt5Test_LIBRARIES}
728 ${GMOCK_LIBRARY}
729 ${GTEST_BOTH_LIBRARIES}
730- udm-common
731- udm-priv-common
732- ubuntu-download-manager-common
733- ubuntu-download-manager-priv
734+ udm-common
735+ udm-priv-common
736+ ubuntu-download-manager-common
737+ ubuntu-download-manager-priv
738 ubuntu-download-manager-test-lib
739 )
740
741
742=== modified file 'tests/downloads/daemon/test_download.cpp'
743--- tests/downloads/daemon/test_download.cpp 2014-11-17 20:34:24 +0000
744+++ tests/downloads/daemon/test_download.cpp 2015-01-23 11:59:29 +0000
745@@ -1262,7 +1262,8 @@
746 .WillOnce(Return(QVariant(200)));
747
748 EXPECT_CALL(*reply.data(), hasRawHeader(_))
749- .Times(1)
750+ .Times(2)
751+ .WillOnce(Return(false))
752 .WillOnce(Return(false));
753
754 // file system expectations
755@@ -1331,7 +1332,8 @@
756 .WillOnce(Return(QVariant(200)));
757
758 EXPECT_CALL(*reply.data(), hasRawHeader(_))
759- .Times(1)
760+ .Times(2)
761+ .WillOnce(Return(false))
762 .WillOnce(Return(false));
763
764 // file system expectations
765@@ -1421,7 +1423,8 @@
766 .WillOnce(Return(QVariant(200)));
767
768 EXPECT_CALL(*reply.data(), hasRawHeader(_))
769- .Times(1)
770+ .Times(2)
771+ .WillOnce(Return(false))
772 .WillOnce(Return(false));
773
774 // file system expectations
775@@ -2143,7 +2146,8 @@
776 .WillOnce(Return(QVariant(200)));
777
778 EXPECT_CALL(*reply, hasRawHeader(_))
779- .Times(1)
780+ .Times(2)
781+ .WillOnce(Return(false))
782 .WillOnce(Return(false));
783
784 // file system expectations
785@@ -2196,6 +2200,7 @@
786 QVERIFY(Mock::VerifyAndClearExpectations(reply));
787 QVERIFY(Mock::VerifyAndClearExpectations(process.data()));
788 verifyMocks();
789+ delete reply;
790 }
791
792 void
793@@ -2254,7 +2259,8 @@
794 .WillOnce(Return(QVariant(200)));
795
796 EXPECT_CALL(*reply, hasRawHeader(_))
797- .Times(1)
798+ .Times(2)
799+ .WillOnce(Return(false))
800 .WillOnce(Return(false));
801
802 // file system expectations
803@@ -2307,6 +2313,8 @@
804 QVERIFY(Mock::VerifyAndClearExpectations(reply));
805 QVERIFY(Mock::VerifyAndClearExpectations(process.data()));
806 verifyMocks();
807+
808+ delete reply;
809 }
810
811 void
812@@ -2365,7 +2373,8 @@
813 .WillOnce(Return(QVariant(200)));
814
815 EXPECT_CALL(*reply, hasRawHeader(_))
816- .Times(1)
817+ .Times(2)
818+ .WillOnce(Return(false))
819 .WillOnce(Return(false));
820
821 // file system expectations
822@@ -2426,6 +2435,8 @@
823 QVERIFY(Mock::VerifyAndClearExpectations(reply));
824 QVERIFY(Mock::VerifyAndClearExpectations(process.data()));
825 verifyMocks();
826+
827+ delete reply;
828 }
829
830 void
831@@ -2458,7 +2469,8 @@
832 .WillOnce(Return(QVariant(200)));
833
834 EXPECT_CALL(*reply.data(), hasRawHeader(_))
835- .Times(1)
836+ .Times(2)
837+ .WillOnce(Return(false))
838 .WillOnce(Return(false));
839
840 // file system expectations
841@@ -2572,7 +2584,8 @@
842 .WillOnce(Return(QVariant(200)));
843
844 EXPECT_CALL(*reply.data(), hasRawHeader(_))
845- .Times(1)
846+ .Times(2)
847+ .WillOnce(Return(false))
848 .WillOnce(Return(false));
849
850 // file system expectations
851@@ -2677,7 +2690,8 @@
852 .WillOnce(Return(QVariant(200)));
853
854 EXPECT_CALL(*reply.data(), hasRawHeader(_))
855- .Times(1)
856+ .Times(2)
857+ .WillOnce(Return(false))
858 .WillOnce(Return(false));
859
860 // file system expectations
861@@ -3169,7 +3183,8 @@
862 .WillOnce(Return(QVariant(200)));
863
864 EXPECT_CALL(*reply, hasRawHeader(_))
865- .Times(1)
866+ .Times(2)
867+ .WillOnce(Return(false))
868 .WillOnce(Return(false));
869
870 // file system expectations
871@@ -3234,6 +3249,8 @@
872 QVERIFY(Mock::VerifyAndClearExpectations(file));
873 QVERIFY(Mock::VerifyAndClearExpectations(reply));
874 verifyMocks();
875+
876+ delete reply;
877 }
878
879 void
880@@ -3508,7 +3525,8 @@
881 .WillOnce(Return(QVariant()));
882
883 EXPECT_CALL(*secondReply.data(), hasRawHeader(_))
884- .Times(1)
885+ .Times(2)
886+ .WillOnce(Return(false))
887 .WillOnce(Return(false));
888
889 // file system expectations
890@@ -3667,7 +3685,8 @@
891 .WillOnce(Return(QVariant(200)));
892
893 EXPECT_CALL(*reply, hasRawHeader(_))
894- .Times(1)
895+ .Times(2)
896+ .WillOnce(Return(false))
897 .WillOnce(Return(false));
898
899 // file system expectations
900@@ -3722,6 +3741,8 @@
901 QVERIFY(Mock::VerifyAndClearExpectations(mutex));
902 verifyMocks();
903 FileNameMutex::deleteInstance();
904+
905+ delete reply;
906 }
907
908 void
909@@ -3930,4 +3951,225 @@
910 verifyMocks();
911 }
912
913+void
914+TestDownload::testDataUriIsValid() {
915+ EXPECT_CALL(*_networkSession, isOnline())
916+ .WillRepeatedly(Return(true));
917+
918+ QUrl url("data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");
919+ QScopedPointer<FileDownload> download(new FileDownload(_id, _appId, _path,
920+ _isConfined, _rootPath, url, _metadata, _headers));
921+
922+ QVERIFY(download->isValid());
923+ verifyMocks();
924+}
925+
926+void
927+TestDownload::testDataUriIsValidWithHttpPrefix() {
928+ // perform the download and assert that the mime extension used is txt
929+ auto file = new MockFile("test");
930+ EXPECT_CALL(*_networkSession, isOnline())
931+ .WillRepeatedly(Return(true));
932+
933+ QUrl url("http://images.google.com/data:;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");
934+ QScopedPointer<FileDownload> download(new FileDownload(_id, _appId, _path,
935+ _isConfined, _rootPath, url, _metadata, _headers));
936+
937+ QVERIFY(download->isValid());
938+
939+ // file system expectations
940+ EXPECT_CALL(*_fileManager, createFile(_))
941+ .Times(1)
942+ .WillOnce(Return(file));
943+
944+ EXPECT_CALL(*file, open(QIODevice::ReadWrite | QFile::Append))
945+ .Times(1)
946+ .WillOnce(Return(true));
947+
948+ EXPECT_CALL(*file, write(_))
949+ .Times(1)
950+ .WillOnce(Return(0));
951+
952+ EXPECT_CALL(*file, close())
953+ .Times(1);
954+
955+ download->startTransfer();
956+ QVERIFY(download->filePath().endsWith("txt"));
957+}
958+
959+void
960+TestDownload::testDataUriMissingMimeType() {
961+ // perform the download and assert that the mime extension used is txt
962+ auto file = new MockFile("test");
963+ EXPECT_CALL(*_networkSession, isOnline())
964+ .WillRepeatedly(Return(true));
965+
966+ QUrl url("data:;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");
967+ QScopedPointer<FileDownload> download(new FileDownload(_id, _appId, _path,
968+ _isConfined, _rootPath, url, _metadata, _headers));
969+
970+ QVERIFY(download->isValid());
971+
972+ // file system expectations
973+ EXPECT_CALL(*_fileManager, createFile(_))
974+ .Times(1)
975+ .WillOnce(Return(file));
976+
977+ EXPECT_CALL(*file, open(QIODevice::ReadWrite | QFile::Append))
978+ .Times(1)
979+ .WillOnce(Return(true));
980+
981+ EXPECT_CALL(*file, write(_))
982+ .Times(1)
983+ .WillOnce(Return(0));
984+
985+ EXPECT_CALL(*file, close())
986+ .Times(1);
987+
988+ download->startTransfer();
989+ QVERIFY(download->filePath().endsWith("txt"));
990+}
991+
992+void
993+TestDownload::testDataUriMimeType_data() {
994+ QTest::addColumn<QString>("mime");
995+ QTest::addColumn<QString>("extension");
996+
997+ QTest::newRow("Image gif") << "image/gif" << "gif";
998+ QTest::newRow("Image ief") << "image/ief" << "ief";
999+ QTest::newRow("Image jpg") << "image/jpeg" << "jpeg";
1000+ QTest::newRow("Image tiff") << "image/tiff" << "tiff";
1001+ QTest::newRow("Image rgb") << "image/x-rgb" << "rgb";
1002+ QTest::newRow("Image bitmap") << "image/x-xbitmap" << "x-xbitmap";
1003+ QTest::newRow("View MPG") << "video/mpeg" << "mpeg";
1004+
1005+}
1006+
1007+void
1008+TestDownload::testDataUriMimeType() {
1009+ QFETCH(QString, mime);
1010+ QFETCH(QString, extension);
1011+ auto file = new MockFile("test");
1012+ EXPECT_CALL(*_networkSession, isOnline())
1013+ .WillRepeatedly(Return(true));
1014+
1015+ QUrl url("data:" + mime + ";base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");
1016+ QScopedPointer<FileDownload> download(new FileDownload(_id, _appId, _path,
1017+ _isConfined, _rootPath, url, _metadata, _headers));
1018+
1019+ QVERIFY(download->isValid());
1020+
1021+ // file system expectations
1022+ EXPECT_CALL(*_fileManager, createFile(_))
1023+ .Times(1)
1024+ .WillOnce(Return(file));
1025+
1026+ EXPECT_CALL(*file, open(QIODevice::ReadWrite | QFile::Append))
1027+ .Times(1)
1028+ .WillOnce(Return(true));
1029+
1030+ EXPECT_CALL(*file, write(_))
1031+ .Times(1)
1032+ .WillOnce(Return(0));
1033+
1034+ EXPECT_CALL(*file, close())
1035+ .Times(1);
1036+
1037+ download->startTransfer();
1038+ QVERIFY(download->filePath().endsWith(extension));
1039+}
1040+
1041+void
1042+TestDownload::testDataUriPostProcessing_data() {
1043+ QTest::addColumn<QString>("command");
1044+ QTest::addColumn<QVariantMap>("metadata");
1045+ QVariantMap first, second, third;
1046+ QStringList firstCommand, secondCommand, thirdCommand;
1047+
1048+ firstCommand << "touch";
1049+ first["post-download-command"] = firstCommand;
1050+
1051+ QTest::newRow("First row") << firstCommand[0] << first;
1052+
1053+ secondCommand << "sudo";
1054+ second["post-download-command"] = secondCommand;
1055+
1056+ QTest::newRow("Second row") << secondCommand[0] << second;
1057+
1058+ thirdCommand << "grep";
1059+ third["post-download-command"] = thirdCommand;
1060+
1061+ QTest::newRow("Third row") << thirdCommand[0] << third;
1062+}
1063+
1064+void
1065+TestDownload::testDataUriPostProcessing() {
1066+ QFETCH(QString, command);
1067+ QFETCH(QVariantMap, metadata);
1068+ QStringList args; // not args
1069+
1070+ QUrl url("data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");
1071+ QScopedPointer<MockFile> file(new MockFile("test"));
1072+ QScopedPointer<MockProcess> process(new MockProcess());
1073+
1074+ // write the expectations of the reply which is what we are
1075+ // really testing
1076+
1077+ EXPECT_CALL(*_networkSession, isOnline())
1078+ .WillRepeatedly(Return(true));
1079+
1080+ // file system expectations
1081+ EXPECT_CALL(*_fileManager, createFile(_))
1082+ .Times(1)
1083+ .WillOnce(Return(file.data()));
1084+
1085+ EXPECT_CALL(*file.data(), open(QIODevice::ReadWrite | QFile::Append))
1086+ .Times(1)
1087+ .WillOnce(Return(true));
1088+
1089+ EXPECT_CALL(*file, write(_))
1090+ .Times(1)
1091+ .WillOnce(Return(0));
1092+
1093+ EXPECT_CALL(*file.data(), remove())
1094+ .Times(1)
1095+ .WillOnce(Return(true));
1096+
1097+ EXPECT_CALL(*_cryptoFactory, createCryptographicHash(_, _))
1098+ .Times(0);
1099+
1100+ // process factory and process expectation
1101+ EXPECT_CALL(*_processFactory, createProcess())
1102+ .Times(1)
1103+ .WillOnce(Return(process.data()));
1104+
1105+ EXPECT_CALL(*process.data(), start(command, StringListEq(args), _))
1106+ .Times(1);
1107+
1108+ auto download = new FileDownload(_id, _appId, _path,
1109+ _isConfined, _rootPath, url, metadata, _headers);
1110+
1111+ SignalBarrier spy(download, SIGNAL(finished(QString)));
1112+ SignalBarrier startedSpy(download, SIGNAL(started(bool)));
1113+ SignalBarrier processingSpy(download, SIGNAL(processing(QString)));
1114+
1115+ download->start(); // change state
1116+ download->startTransfer();
1117+
1118+ QVERIFY(startedSpy.ensureSignalEmitted());
1119+
1120+ // emit the finish signal and expect it to be raised
1121+ emit process->finished(0, QProcess::NormalExit);
1122+
1123+ QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000);
1124+ QTRY_COMPARE_WITH_TIMEOUT(processingSpy.count(), 1, 20000);
1125+ QCOMPARE(download->state(), Download::FINISH);
1126+
1127+ delete download;
1128+
1129+ QVERIFY(Mock::VerifyAndClearExpectations(file.data()));
1130+ QVERIFY(Mock::VerifyAndClearExpectations(process.data()));
1131+ verifyMocks();
1132+}
1133+
1134 QTEST_MAIN(TestDownload)
1135
1136=== modified file 'tests/downloads/daemon/test_download.h'
1137--- tests/downloads/daemon/test_download.h 2014-11-17 20:34:24 +0000
1138+++ tests/downloads/daemon/test_download.h 2015-01-23 11:59:29 +0000
1139@@ -186,6 +186,15 @@
1140 void testDeflateConstructorNoError();
1141 void testDeflateOnRequest();
1142
1143+ // void data uri tests
1144+ void testDataUriIsValid();
1145+ void testDataUriIsValidWithHttpPrefix();
1146+ void testDataUriMissingMimeType();
1147+ void testDataUriMimeType_data();
1148+ void testDataUriMimeType();
1149+ void testDataUriPostProcessing_data();
1150+ void testDataUriPostProcessing();
1151+
1152 private:
1153 QString _id;
1154 QString _appId;

Subscribers

People subscribed via source and target branches