1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQml module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qqmlfile.h"
41
42#include <QtCore/qurl.h>
43#include <QtCore/qobject.h>
44#include <QtCore/qmetaobject.h>
45#include <QtCore/qfile.h>
46#include <private/qqmlengine_p.h>
47#include <private/qqmlglobal_p.h>
48
49/*!
50\class QQmlFile
51\brief The QQmlFile class gives access to local and remote files.
52
53\internal
54
55Supports file:// and qrc:/ uris and whatever QNetworkAccessManager supports.
56*/
57
58#define QQMLFILE_MAX_REDIRECT_RECURSION 16
59
60QT_BEGIN_NAMESPACE
61
62static char qrc_string[] = "qrc";
63static char file_string[] = "file";
64
65#if defined(Q_OS_ANDROID)
66static char assets_string[] = "assets";
67static char content_string[] = "content";
68#endif
69
70class QQmlFilePrivate;
71
72#if QT_CONFIG(qml_network)
73class QQmlFileNetworkReply : public QObject
74{
75Q_OBJECT
76public:
77 QQmlFileNetworkReply(QQmlEngine *, QQmlFilePrivate *, const QUrl &);
78 ~QQmlFileNetworkReply();
79
80signals:
81 void finished();
82 void downloadProgress(qint64, qint64);
83
84public slots:
85 void networkFinished();
86 void networkDownloadProgress(qint64, qint64);
87
88public:
89 static int finishedIndex;
90 static int downloadProgressIndex;
91 static int networkFinishedIndex;
92 static int networkDownloadProgressIndex;
93 static int replyFinishedIndex;
94 static int replyDownloadProgressIndex;
95
96private:
97 QQmlEngine *m_engine;
98 QQmlFilePrivate *m_p;
99
100 int m_redirectCount;
101 QNetworkReply *m_reply;
102};
103#endif
104
105class QQmlFilePrivate
106{
107public:
108 QQmlFilePrivate();
109
110 mutable QUrl url;
111 mutable QString urlString;
112
113 QByteArray data;
114
115 enum Error {
116 None, NotFound, CaseMismatch, Network
117 };
118
119 Error error;
120 QString errorString;
121#if QT_CONFIG(qml_network)
122 QQmlFileNetworkReply *reply;
123#endif
124};
125
126#if QT_CONFIG(qml_network)
127int QQmlFileNetworkReply::finishedIndex = -1;
128int QQmlFileNetworkReply::downloadProgressIndex = -1;
129int QQmlFileNetworkReply::networkFinishedIndex = -1;
130int QQmlFileNetworkReply::networkDownloadProgressIndex = -1;
131int QQmlFileNetworkReply::replyFinishedIndex = -1;
132int QQmlFileNetworkReply::replyDownloadProgressIndex = -1;
133
134QQmlFileNetworkReply::QQmlFileNetworkReply(QQmlEngine *e, QQmlFilePrivate *p, const QUrl &url)
135: m_engine(e), m_p(p), m_redirectCount(0), m_reply(nullptr)
136{
137 if (finishedIndex == -1) {
138 finishedIndex = QMetaMethod::fromSignal(&QQmlFileNetworkReply::finished).methodIndex();
139 downloadProgressIndex = QMetaMethod::fromSignal(&QQmlFileNetworkReply::downloadProgress).methodIndex();
140 const QMetaObject *smo = &staticMetaObject;
141 networkFinishedIndex = smo->indexOfMethod("networkFinished()");
142 networkDownloadProgressIndex = smo->indexOfMethod("networkDownloadProgress(qint64,qint64)");
143
144 replyFinishedIndex = QMetaMethod::fromSignal(&QNetworkReply::finished).methodIndex();
145 replyDownloadProgressIndex = QMetaMethod::fromSignal(&QNetworkReply::downloadProgress).methodIndex();
146 }
147 Q_ASSERT(finishedIndex != -1 && downloadProgressIndex != -1 &&
148 networkFinishedIndex != -1 && networkDownloadProgressIndex != -1 &&
149 replyFinishedIndex != -1 && replyDownloadProgressIndex != -1);
150
151 QNetworkRequest req(url);
152 req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
153
154 m_reply = m_engine->networkAccessManager()->get(req);
155 QMetaObject::connect(m_reply, replyFinishedIndex, this, networkFinishedIndex);
156 QMetaObject::connect(m_reply, replyDownloadProgressIndex, this, networkDownloadProgressIndex);
157}
158
159QQmlFileNetworkReply::~QQmlFileNetworkReply()
160{
161 if (m_reply) {
162 m_reply->disconnect();
163 m_reply->deleteLater();
164 }
165}
166
167void QQmlFileNetworkReply::networkFinished()
168{
169 ++m_redirectCount;
170 if (m_redirectCount < QQMLFILE_MAX_REDIRECT_RECURSION) {
171 QVariant redirect = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
172 if (redirect.isValid()) {
173 QUrl url = m_reply->url().resolved(redirect.toUrl());
174
175 QNetworkRequest req(url);
176 req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
177
178 m_reply->deleteLater();
179 m_reply = m_engine->networkAccessManager()->get(req);
180
181 QMetaObject::connect(m_reply, replyFinishedIndex,
182 this, networkFinishedIndex);
183 QMetaObject::connect(m_reply, replyDownloadProgressIndex,
184 this, networkDownloadProgressIndex);
185
186 return;
187 }
188 }
189
190 if (m_reply->error()) {
191 m_p->errorString = m_reply->errorString();
192 m_p->error = QQmlFilePrivate::Network;
193 } else {
194 m_p->data = m_reply->readAll();
195 }
196
197 m_reply->deleteLater();
198 m_reply = nullptr;
199
200 m_p->reply = nullptr;
201 emit finished();
202 delete this;
203}
204
205void QQmlFileNetworkReply::networkDownloadProgress(qint64 a, qint64 b)
206{
207 emit downloadProgress(a, b);
208}
209#endif // qml_network
210
211QQmlFilePrivate::QQmlFilePrivate()
212: error(None)
213#if QT_CONFIG(qml_network)
214, reply(nullptr)
215#endif
216{
217}
218
219QQmlFile::QQmlFile()
220: d(new QQmlFilePrivate)
221{
222}
223
224QQmlFile::QQmlFile(QQmlEngine *e, const QUrl &url)
225: d(new QQmlFilePrivate)
226{
227 load(e, url);
228}
229
230QQmlFile::QQmlFile(QQmlEngine *e, const QString &url)
231 : QQmlFile(e, QUrl(url))
232{
233}
234
235QQmlFile::~QQmlFile()
236{
237#if QT_CONFIG(qml_network)
238 delete d->reply;
239#endif
240 delete d;
241 d = nullptr;
242}
243
244bool QQmlFile::isNull() const
245{
246 return status() == Null;
247}
248
249bool QQmlFile::isReady() const
250{
251 return status() == Ready;
252}
253
254bool QQmlFile::isError() const
255{
256 return status() == Error;
257}
258
259bool QQmlFile::isLoading() const
260{
261 return status() == Loading;
262}
263
264QUrl QQmlFile::url() const
265{
266 if (!d->urlString.isEmpty()) {
267 d->url = QUrl(d->urlString);
268 d->urlString = QString();
269 }
270 return d->url;
271}
272
273QQmlFile::Status QQmlFile::status() const
274{
275 if (d->url.isEmpty() && d->urlString.isEmpty())
276 return Null;
277#if QT_CONFIG(qml_network)
278 else if (d->reply)
279 return Loading;
280#endif
281 else if (d->error != QQmlFilePrivate::None)
282 return Error;
283 else
284 return Ready;
285}
286
287QString QQmlFile::error() const
288{
289 switch (d->error) {
290 default:
291 case QQmlFilePrivate::None:
292 return QString();
293 case QQmlFilePrivate::NotFound:
294 return QLatin1String("File not found");
295 case QQmlFilePrivate::CaseMismatch:
296 return QLatin1String("File name case mismatch");
297 }
298}
299
300qint64 QQmlFile::size() const
301{
302 return d->data.size();
303}
304
305const char *QQmlFile::data() const
306{
307 return d->data.constData();
308}
309
310QByteArray QQmlFile::dataByteArray() const
311{
312 return d->data;
313}
314
315void QQmlFile::load(QQmlEngine *engine, const QUrl &url)
316{
317 Q_ASSERT(engine);
318
319 clear();
320 d->url = url;
321
322 if (isLocalFile(url)) {
323 QString lf = urlToLocalFileOrQrc(url);
324
325 if (!QQml_isFileCaseCorrect(lf)) {
326 d->error = QQmlFilePrivate::CaseMismatch;
327 return;
328 }
329
330 QFile file(lf);
331 if (file.open(QFile::ReadOnly)) {
332 d->data = file.readAll();
333 } else {
334 d->error = QQmlFilePrivate::NotFound;
335 }
336 } else {
337#if QT_CONFIG(qml_network)
338 d->reply = new QQmlFileNetworkReply(engine, d, url);
339#else
340 d->error = QQmlFilePrivate::NotFound;
341#endif
342 }
343}
344
345void QQmlFile::load(QQmlEngine *engine, const QString &url)
346{
347 Q_ASSERT(engine);
348
349 clear();
350
351 d->urlString = url;
352
353 if (isLocalFile(url)) {
354 QString lf = urlToLocalFileOrQrc(url);
355
356 if (!QQml_isFileCaseCorrect(lf)) {
357 d->error = QQmlFilePrivate::CaseMismatch;
358 return;
359 }
360
361 QFile file(lf);
362 if (file.open(QFile::ReadOnly)) {
363 d->data = file.readAll();
364 } else {
365 d->error = QQmlFilePrivate::NotFound;
366 }
367 } else {
368#if QT_CONFIG(qml_network)
369 QUrl qurl(url);
370 d->url = qurl;
371 d->urlString = QString();
372 d->reply = new QQmlFileNetworkReply(engine, d, qurl);
373#else
374 d->error = QQmlFilePrivate::NotFound;
375#endif
376 }
377}
378
379void QQmlFile::clear()
380{
381 d->url = QUrl();
382 d->urlString = QString();
383 d->data = QByteArray();
384 d->error = QQmlFilePrivate::None;
385}
386
387void QQmlFile::clear(QObject *)
388{
389 clear();
390}
391
392#if QT_CONFIG(qml_network)
393bool QQmlFile::connectFinished(QObject *object, const char *method)
394{
395 if (!d || !d->reply) {
396 qWarning("QQmlFile: connectFinished() called when not loading.");
397 return false;
398 }
399
400 return QObject::connect(d->reply, SIGNAL(finished()),
401 object, method);
402}
403
404bool QQmlFile::connectFinished(QObject *object, int method)
405{
406 if (!d || !d->reply) {
407 qWarning("QQmlFile: connectFinished() called when not loading.");
408 return false;
409 }
410
411 return QMetaObject::connect(d->reply, QQmlFileNetworkReply::finishedIndex,
412 object, method);
413}
414
415bool QQmlFile::connectDownloadProgress(QObject *object, const char *method)
416{
417 if (!d || !d->reply) {
418 qWarning("QQmlFile: connectDownloadProgress() called when not loading.");
419 return false;
420 }
421
422 return QObject::connect(d->reply, SIGNAL(downloadProgress(qint64,qint64)),
423 object, method);
424}
425
426bool QQmlFile::connectDownloadProgress(QObject *object, int method)
427{
428 if (!d || !d->reply) {
429 qWarning("QQmlFile: connectDownloadProgress() called when not loading.");
430 return false;
431 }
432
433 return QMetaObject::connect(d->reply, QQmlFileNetworkReply::downloadProgressIndex,
434 object, method);
435}
436#endif
437
438/*!
439Returns true if QQmlFile will open \a url synchronously.
440
441Synchronous urls have a qrc:/ or file:// scheme.
442
443\note On Android, urls with assets:/ scheme are also considered synchronous.
444*/
445bool QQmlFile::isSynchronous(const QUrl &url)
446{
447 QString scheme = url.scheme();
448
449 if ((scheme.length() == 4 && 0 == scheme.compare(QLatin1String(file_string), Qt::CaseInsensitive)) ||
450 (scheme.length() == 3 && 0 == scheme.compare(QLatin1String(qrc_string), Qt::CaseInsensitive))) {
451 return true;
452
453#if defined(Q_OS_ANDROID)
454 } else if (scheme.length() == 6 && 0 == scheme.compare(QLatin1String(assets_string), Qt::CaseInsensitive)) {
455 return true;
456 } else if (scheme.length() == 7 && 0 == scheme.compare(QLatin1String(content_string), Qt::CaseInsensitive)) {
457 return true;
458#endif
459
460 } else {
461 return false;
462 }
463}
464
465/*!
466Returns true if QQmlFile will open \a url synchronously.
467
468Synchronous urls have a qrc:/ or file:// scheme.
469
470\note On Android, urls with assets:/ scheme are also considered synchronous.
471*/
472bool QQmlFile::isSynchronous(const QString &url)
473{
474 if (url.length() < 5 /* qrc:/ */)
475 return false;
476
477 QChar f = url[0];
478
479 if (f == QLatin1Char('f') || f == QLatin1Char('F')) {
480
481 return url.length() >= 7 /* file:// */ &&
482 url.startsWith(QLatin1String(file_string), Qt::CaseInsensitive) &&
483 url[4] == QLatin1Char(':') && url[5] == QLatin1Char('/') && url[6] == QLatin1Char('/');
484
485 } else if (f == QLatin1Char('q') || f == QLatin1Char('Q')) {
486
487 return url.length() >= 5 /* qrc:/ */ &&
488 url.startsWith(QLatin1String(qrc_string), Qt::CaseInsensitive) &&
489 url[3] == QLatin1Char(':') && url[4] == QLatin1Char('/');
490
491 }
492
493#if defined(Q_OS_ANDROID)
494 else if (f == QLatin1Char('a') || f == QLatin1Char('A')) {
495 return url.length() >= 8 /* assets:/ */ &&
496 url.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive) &&
497 url[6] == QLatin1Char(':') && url[7] == QLatin1Char('/');
498 } else if (f == QLatin1Char('c') || f == QLatin1Char('C')) {
499 return url.length() >= 9 /* content:/ */ &&
500 url.startsWith(QLatin1String(content_string), Qt::CaseInsensitive) &&
501 url[7] == QLatin1Char(':') && url[8] == QLatin1Char('/');
502 }
503#endif
504
505 return false;
506}
507
508/*!
509Returns true if \a url is a local file that can be opened with QFile.
510
511Local file urls have either a qrc:/ or file:// scheme.
512
513\note On Android, urls with assets:/ scheme are also considered local files.
514*/
515bool QQmlFile::isLocalFile(const QUrl &url)
516{
517 QString scheme = url.scheme();
518
519 if ((scheme.length() == 4 && 0 == scheme.compare(QLatin1String(file_string), Qt::CaseInsensitive)) ||
520 (scheme.length() == 3 && 0 == scheme.compare(QLatin1String(qrc_string), Qt::CaseInsensitive))) {
521 return true;
522
523#if defined(Q_OS_ANDROID)
524 } else if (scheme.length() == 6 && 0 == scheme.compare(QLatin1String(assets_string), Qt::CaseInsensitive)) {
525 return true;
526#endif
527
528 } else {
529 return false;
530 }
531}
532
533/*!
534Returns true if \a url is a local file that can be opened with QFile.
535
536Local file urls have either a qrc:/ or file:// scheme.
537
538\note On Android, urls with assets:/ scheme are also considered local files.
539*/
540bool QQmlFile::isLocalFile(const QString &url)
541{
542 if (url.length() < 5 /* qrc:/ */)
543 return false;
544
545 QChar f = url[0];
546
547 if (f == QLatin1Char('f') || f == QLatin1Char('F')) {
548
549 return url.length() >= 7 /* file:// */ &&
550 url.startsWith(QLatin1String(file_string), Qt::CaseInsensitive) &&
551 url[4] == QLatin1Char(':') && url[5] == QLatin1Char('/') && url[6] == QLatin1Char('/');
552
553 } else if (f == QLatin1Char('q') || f == QLatin1Char('Q')) {
554
555 return url.length() >= 5 /* qrc:/ */ &&
556 url.startsWith(QLatin1String(qrc_string), Qt::CaseInsensitive) &&
557 url[3] == QLatin1Char(':') && url[4] == QLatin1Char('/');
558
559 }
560#if defined(Q_OS_ANDROID)
561 else if (f == QLatin1Char('a') || f == QLatin1Char('A')) {
562 return url.length() >= 8 /* assets:/ */ &&
563 url.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive) &&
564 url[6] == QLatin1Char(':') && url[7] == QLatin1Char('/');
565 } else if (f == QLatin1Char('c') || f == QLatin1Char('C')) {
566 return url.length() >= 9 /* content:/ */ &&
567 url.startsWith(QLatin1String(content_string), Qt::CaseInsensitive) &&
568 url[7] == QLatin1Char(':') && url[8] == QLatin1Char('/');
569 }
570#endif
571
572 return false;
573}
574
575/*!
576If \a url is a local file returns a path suitable for passing to QFile. Otherwise returns an
577empty string.
578*/
579QString QQmlFile::urlToLocalFileOrQrc(const QUrl& url)
580{
581 if (url.scheme().compare(QLatin1String("qrc"), Qt::CaseInsensitive) == 0) {
582 if (url.authority().isEmpty())
583 return QLatin1Char(':') + url.path();
584 return QString();
585 }
586
587#if defined(Q_OS_ANDROID)
588 else if (url.scheme().compare(QLatin1String("assets"), Qt::CaseInsensitive) == 0) {
589 if (url.authority().isEmpty())
590 return url.toString();
591 return QString();
592 } else if (url.scheme().compare(QLatin1String("content"), Qt::CaseInsensitive) == 0) {
593 return url.toString();
594 }
595#endif
596
597 return url.toLocalFile();
598}
599
600static QString toLocalFile(const QString &url)
601{
602 const QUrl file(url);
603 if (!file.isLocalFile())
604 return QString();
605
606 //XXX TODO: handle windows hostnames: "//servername/path/to/file.txt"
607
608 return file.toLocalFile();
609}
610
611/*!
612If \a url is a local file returns a path suitable for passing to QFile. Otherwise returns an
613empty string.
614*/
615QString QQmlFile::urlToLocalFileOrQrc(const QString& url)
616{
617 if (url.startsWith(QLatin1String("qrc://"), Qt::CaseInsensitive)) {
618 if (url.length() > 6)
619 return QLatin1Char(':') + url.midRef(6);
620 return QString();
621 }
622
623 if (url.startsWith(QLatin1String("qrc:"), Qt::CaseInsensitive)) {
624 if (url.length() > 4)
625 return QLatin1Char(':') + url.midRef(4);
626 return QString();
627 }
628
629#if defined(Q_OS_ANDROID)
630 else if (url.startsWith(QLatin1String("assets:"), Qt::CaseInsensitive)) {
631 return url;
632 } else if (url.startsWith(QLatin1String("content:"), Qt::CaseInsensitive)) {
633 return url;
634 }
635#endif
636
637 return toLocalFile(url);
638}
639
640QT_END_NAMESPACE
641
642#include "qqmlfile.moc"
643