Merge lp:~abreu-alexandre/webbrowser-app/rtm-intent-support into lp:webbrowser-app/rtm-14.09

Proposed by Alexandre Abreu
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 788
Merged at revision: 790
Proposed branch: lp:~abreu-alexandre/webbrowser-app/rtm-intent-support
Merge into: lp:webbrowser-app/rtm-14.09
Diff against target: 1080 lines (+856/-23)
13 files modified
src/app/webcontainer/CMakeLists.txt (+2/-1)
src/app/webcontainer/WebViewImplWebkit.qml (+28/-21)
src/app/webcontainer/intent-filter.cpp (+243/-0)
src/app/webcontainer/intent-filter.h (+105/-0)
src/app/webcontainer/url-pattern-utils.h (+14/-0)
src/app/webcontainer/webapp-container.cpp (+29/-0)
src/app/webcontainer/webapp-container.h (+7/-0)
src/app/webcontainer/webapp-container.qml (+70/-0)
tests/autopilot/webapp_container/tests/__init__.py (+14/-1)
tests/autopilot/webapp_container/tests/test_intent_uri_support.py (+116/-0)
tests/unittests/CMakeLists.txt (+1/-0)
tests/unittests/intent-filter/CMakeLists.txt (+9/-0)
tests/unittests/intent-filter/tst_IntentFilterTests.cpp (+218/-0)
To merge this branch: bzr merge lp:~abreu-alexandre/webbrowser-app/rtm-intent-support
Reviewer Review Type Date Requested Status
Olivier Tilloy Approve
Review via email: mp+248250@code.launchpad.net

Commit message

Backport for the intent:// support.

Description of the change

Backport for the intent:// support.

To post a comment you must log in.
Revision history for this message
Olivier Tilloy (osomon) wrote :

LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/webcontainer/CMakeLists.txt'
2--- src/app/webcontainer/CMakeLists.txt 2014-09-18 13:37:07 +0000
3+++ src/app/webcontainer/CMakeLists.txt 2015-02-02 14:22:27 +0000
4@@ -18,13 +18,14 @@
5 webapp-container-helper.cpp
6 session-utils.cpp
7 url-pattern-utils.cpp
8+ intent-filter.cpp
9 )
10
11 add_executable(${WEBAPP_CONTAINER} ${WEBAPP_CONTAINER_SRC})
12
13 target_link_libraries(${WEBAPP_CONTAINER} ${COMMONLIB})
14
15-qt5_use_modules(${WEBAPP_CONTAINER} Core Widgets Quick Sql DBus)
16+qt5_use_modules(${WEBAPP_CONTAINER} Core Widgets Quick Qml Sql DBus)
17
18 install(TARGETS ${WEBAPP_CONTAINER}
19 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
20
21=== modified file 'src/app/webcontainer/WebViewImplWebkit.qml'
22--- src/app/webcontainer/WebViewImplWebkit.qml 2015-01-19 10:00:13 +0000
23+++ src/app/webcontainer/WebViewImplWebkit.qml 2015-02-02 14:22:27 +0000
24@@ -96,29 +96,12 @@
25 return webappUrlPatterns && webappUrlPatterns.length !== 0
26 }
27
28- function navigationRequestedDelegate(request) {
29- if (!request.isMainFrame) {
30- request.action = WebView.AcceptRequest
31- return
32- }
33-
34- // Pass-through if we are not running as a named webapp (--webapp='Gmail')
35- // or if we dont have a list of url patterns specified to filter the
36- // browsing actions
37- if ( ! haveValidUrlPatterns() && ! isRunningAsANamedWebapp()) {
38- request.action = WebView.AcceptRequest
39- return
40- }
41-
42- var action = WebView.IgnoreRequest
43- var url = request.url.toString()
44-
45+ function shouldAllowNavigationTo(url) {
46 // The list of url patterns defined by the webapp takes precedence over command line
47 if (isRunningAsANamedWebapp()) {
48 if (unityWebapps.model.exists(unityWebapps.name) &&
49 unityWebapps.model.doesUrlMatchesWebapp(unityWebapps.name, url)) {
50- request.action = WebView.AcceptRequest
51- return;
52+ return true;
53 }
54 }
55
56@@ -129,12 +112,36 @@
57 for (var i = 0; i < webappUrlPatterns.length; ++i) {
58 var pattern = webappUrlPatterns[i]
59 if (url.match(pattern)) {
60- action = WebView.AcceptRequest
61- break
62+ return true;
63 }
64 }
65 }
66
67+ return false;
68+ }
69+
70+ function navigationRequestedDelegate(request) {
71+ if (!request.isMainFrame) {
72+ request.action = WebView.AcceptRequest
73+ return
74+ }
75+
76+ // Pass-through if we are not running as a named webapp (--webapp='Gmail')
77+ // or if we dont have a list of url patterns specified to filter the
78+ // browsing actions
79+ if ( ! haveValidUrlPatterns() && ! isRunningAsANamedWebapp()) {
80+ request.action = WebView.AcceptRequest
81+ return
82+ }
83+
84+ var action = WebView.IgnoreRequest
85+ var url = request.url.toString()
86+
87+ if (shouldAllowNavigationTo(url)) {
88+ request.action = WebView.AcceptRequest
89+ return;
90+ }
91+
92 request.action = action
93 if (action === WebView.IgnoreRequest) {
94 console.debug('Opening: ' + url + ' in the browser window.')
95
96=== added file 'src/app/webcontainer/intent-filter.cpp'
97--- src/app/webcontainer/intent-filter.cpp 1970-01-01 00:00:00 +0000
98+++ src/app/webcontainer/intent-filter.cpp 2015-02-02 14:22:27 +0000
99@@ -0,0 +1,243 @@
100+/*
101+ * Copyright 2014 Canonical Ltd.
102+ *
103+ * This file is part of webbrowser-app.
104+ *
105+ * webbrowser-app is free software; you can redistribute it and/or modify
106+ * it under the terms of the GNU General Public License as published by
107+ * the Free Software Foundation; version 3.
108+ *
109+ * webbrowser-app is distributed in the hope that it will be useful,
110+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
111+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
112+ * GNU General Public License for more details.
113+ *
114+ * You should have received a copy of the GNU General Public License
115+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
116+ */
117+
118+#include "intent-filter.h"
119+
120+#include <QtCore/QRegularExpression>
121+#include <QDebug>
122+#include <QFile>
123+#include <QJSEngine>
124+#include <QJSValue>
125+#include <QUrl>
126+
127+
128+namespace {
129+
130+const char INTENT_SCHEME_STRING[] = "intent";
131+const char INTENT_START_FRAGMENT_TAG[] = "Intent";
132+const char INTENT_URI_PACKAGE_PREFIX[] = "package=";
133+const char INTENT_URI_ACTION_PREFIX[] = "action=";
134+const char INTENT_URI_CATEGORY_PREFIX[] = "category=";
135+const char INTENT_URI_COMPONENT_PREFIX[] = "component=";
136+const char INTENT_URI_SCHEME_PREFIX[] = "scheme=";
137+const char INTENT_END_FRAGMENT_TAG[] = ";end";
138+
139+void trimUriSeparator(QString& uriComponent)
140+{
141+ uriComponent.remove(QRegExp("^/*")).remove(QRegExp("/*$"));
142+}
143+
144+}
145+
146+class IntentFilterPrivate
147+{
148+public:
149+
150+ static const QString DEFAULT_PASS_THROUGH_FILTER;
151+
152+public:
153+
154+ IntentFilterPrivate(const QString& content);
155+
156+ QJSValue evaluate(const IntentUriDescription& intent);
157+ QJSValue evaluate(const QString& customContent
158+ , const IntentUriDescription& intent);
159+
160+private:
161+
162+ QJSValue callFunction(
163+ QJSValue & function
164+ , const IntentUriDescription& intent);
165+
166+ QString _content;
167+ QJSEngine _engine;
168+ QJSValue _function;
169+
170+};
171+
172+// static
173+const QString IntentFilterPrivate::DEFAULT_PASS_THROUGH_FILTER =
174+ "(function(intent) { return intent; })";
175+
176+IntentFilterPrivate::IntentFilterPrivate(const QString& content)
177+ : _content(content)
178+{
179+ if (_content.isEmpty())
180+ {
181+ _content = DEFAULT_PASS_THROUGH_FILTER;
182+ }
183+ _function = _engine.evaluate(_content);
184+}
185+
186+QJSValue IntentFilterPrivate::callFunction(QJSValue & function
187+ , const IntentUriDescription& intent)
188+{
189+ if (!function.isCallable())
190+ {
191+ qCritical() << "Invalid intent filter function (not callable)";
192+ return QJSValue();
193+ }
194+
195+ QVariantMap o;
196+ o.insert("scheme", intent.scheme);
197+ o.insert("uri", intent.uriPath);
198+ o.insert("host", intent.host);
199+
200+ QJSValueList jsargs;
201+ jsargs << _engine.toScriptValue(o);
202+ return function.call(jsargs);
203+}
204+
205+QJSValue IntentFilterPrivate::evaluate(const QString& customContent
206+ , const IntentUriDescription& intent)
207+{
208+ QJSValue f = _engine.evaluate(customContent);
209+ return callFunction(f, intent);
210+}
211+
212+QJSValue IntentFilterPrivate::evaluate(const IntentUriDescription& intent)
213+{
214+ return callFunction(_function, intent);
215+}
216+
217+// static
218+bool IntentFilter::isValidLocalIntentFilterFile(const QString& filename)
219+{
220+ QFile f(filename);
221+ if (!f.exists() || !f.open(QIODevice::ReadOnly))
222+ {
223+ return false;
224+ }
225+
226+ // Perform basic validation
227+ QJSEngine engine;
228+ QJSValue result = engine.evaluate(QString(f.readAll()), filename);
229+ return !result.isNull() && result.isCallable();
230+}
231+
232+// static
233+bool IntentFilter::isValidIntentFilterResult(const QVariantMap& result)
234+{
235+ return result.contains("scheme")
236+ && result.value("scheme").canConvert(QVariant::String)
237+ && result.contains("uri")
238+ && result.value("uri").canConvert(QVariant::String);
239+}
240+
241+// static
242+bool IntentFilter::isValidIntentDescription(const IntentUriDescription& intentDescription)
243+{
244+ return !intentDescription.uriPath.isEmpty()
245+ || !intentDescription.package.isEmpty();
246+}
247+
248+
249+IntentFilter::IntentFilter(const QString& content, QObject *parent) :
250+ QObject(parent),
251+ d_ptr(new IntentFilterPrivate(content))
252+{
253+}
254+
255+IntentFilter::~IntentFilter()
256+{
257+ delete d_ptr;
258+}
259+
260+QVariantMap IntentFilter::applyFilter(const QString& intentUri)
261+{
262+ Q_D(IntentFilter);
263+
264+ QVariantMap result;
265+ IntentUriDescription intentDescription =
266+ parseIntentUri(QUrl::fromUserInput(intentUri));
267+ if (!isValidIntentDescription(intentDescription))
268+ {
269+ return result;
270+ }
271+ QJSValue value = d->evaluate(intentDescription);
272+ if (value.isObject()
273+ && value.toVariant().canConvert(QVariant::Map))
274+ {
275+ QVariantMap r = value.toVariant().toMap();
276+ if (isValidIntentFilterResult(r))
277+ {
278+ result = r;
279+ }
280+ else
281+ {
282+ // Fallback to a noop
283+ result = d->evaluate(
284+ IntentFilterPrivate::DEFAULT_PASS_THROUGH_FILTER
285+ , intentDescription).toVariant().toMap();
286+ }
287+ }
288+ return result;
289+}
290+
291+bool IntentFilter::isValidIntentUri(const QString& intentUri) const
292+{
293+ // a bit overkill but anyway ...
294+ return isValidIntentDescription(parseIntentUri(QUrl::fromUserInput(intentUri)));
295+}
296+
297+IntentUriDescription
298+parseIntentUri(const QUrl& intentUri)
299+{
300+ IntentUriDescription result;
301+ if (intentUri.scheme() != INTENT_SCHEME_STRING
302+ || !intentUri.fragment().startsWith(INTENT_START_FRAGMENT_TAG)
303+ || !intentUri.fragment().endsWith(INTENT_END_FRAGMENT_TAG))
304+ {
305+ return result;
306+ }
307+ QString host = intentUri.host();
308+ trimUriSeparator(host);
309+ QString path = intentUri.path();
310+ if (intentUri.hasQuery())
311+ {
312+ path += "?" + intentUri.query();
313+ trimUriSeparator(path);
314+ }
315+ result.host = host;
316+ result.uriPath = path;
317+ QStringList infos = intentUri.fragment().split(";");
318+ Q_FOREACH(const QString& info, infos)
319+ {
320+ if (info.startsWith(INTENT_URI_PACKAGE_PREFIX))
321+ {
322+ result.package = info.split(INTENT_URI_PACKAGE_PREFIX)[1];
323+ }
324+ else if (info.startsWith(INTENT_URI_ACTION_PREFIX))
325+ {
326+ result.action = info.split(INTENT_URI_ACTION_PREFIX)[1];
327+ }
328+ else if (info.startsWith(INTENT_URI_CATEGORY_PREFIX))
329+ {
330+ result.category = info.split(INTENT_URI_CATEGORY_PREFIX)[1];
331+ }
332+ else if (info.startsWith(INTENT_URI_COMPONENT_PREFIX))
333+ {
334+ result.component = info.split(INTENT_URI_COMPONENT_PREFIX)[1];
335+ }
336+ else if (info.startsWith(INTENT_URI_SCHEME_PREFIX))
337+ {
338+ result.scheme = info.split(INTENT_URI_SCHEME_PREFIX)[1];
339+ }
340+ }
341+ return result;
342+}
343
344=== added file 'src/app/webcontainer/intent-filter.h'
345--- src/app/webcontainer/intent-filter.h 1970-01-01 00:00:00 +0000
346+++ src/app/webcontainer/intent-filter.h 2015-02-02 14:22:27 +0000
347@@ -0,0 +1,105 @@
348+/*
349+ * Copyright 2014 Canonical Ltd.
350+ *
351+ * This file is part of webbrowser-app.
352+ *
353+ * webbrowser-app is free software; you can redistribute it and/or modify
354+ * it under the terms of the GNU General Public License as published by
355+ * the Free Software Foundation; version 3.
356+ *
357+ * webbrowser-app is distributed in the hope that it will be useful,
358+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
359+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
360+ * GNU General Public License for more details.
361+ *
362+ * You should have received a copy of the GNU General Public License
363+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
364+ */
365+
366+#ifndef _INTENT_FILTER_H_
367+#define _INTENT_FILTER_H_
368+
369+#include <QObject>
370+#include <QString>
371+#include <QVariantMap>
372+
373+
374+class QUrl;
375+class IntentFilterPrivate;
376+struct IntentUriDescription;
377+
378+/**
379+ * @brief The IntentFilter class
380+ */
381+class IntentFilter : public QObject
382+{
383+ Q_OBJECT
384+
385+public:
386+ IntentFilter(const QString& content,
387+ QObject *parent = 0);
388+ ~IntentFilter();
389+
390+ /**
391+ * @brief isValidLocalIntentFilterFile
392+ * @return
393+ */
394+ static bool isValidLocalIntentFilterFile(const QString& filename);
395+
396+ /**
397+ * @brief isValidIntentDescription
398+ * @return
399+ */
400+ static bool isValidIntentDescription(const IntentUriDescription& );
401+
402+ /**
403+ * @brief isValidIntentFilterResult
404+ * @return
405+ */
406+ static bool isValidIntentFilterResult(const QVariantMap& );
407+
408+ /**
409+ * @brief apply
410+ * @return
411+ */
412+ Q_INVOKABLE QVariantMap applyFilter(const QString& intentUri);
413+
414+ /**
415+ * @brief isValidIntentUri
416+ * @return
417+ */
418+ Q_INVOKABLE bool isValidIntentUri(const QString& intentUri) const;
419+
420+
421+private:
422+
423+ IntentFilterPrivate* d_ptr;
424+ Q_DECLARE_PRIVATE(IntentFilter)
425+};
426+
427+
428+struct IntentUriDescription
429+{
430+ QString uriPath;
431+
432+ // optional
433+ QString host;
434+
435+ QString package;
436+ QString action;
437+ QString category;
438+ QString component;
439+ QString scheme;
440+};
441+
442+/**
443+ * @brief Parse a URI that is supposed to be an intent as defined here
444+ *
445+ * https://developer.chrome.com/multidevice/android/intents
446+ *
447+ * @param intentUri
448+ * @return
449+ */
450+IntentUriDescription parseIntentUri(const QUrl& intentUri);
451+
452+#endif // _INTENT_FILTER_H_
453
454=== modified file 'src/app/webcontainer/url-pattern-utils.h'
455--- src/app/webcontainer/url-pattern-utils.h 2015-01-19 10:00:13 +0000
456+++ src/app/webcontainer/url-pattern-utils.h 2015-02-02 14:22:27 +0000
457@@ -27,11 +27,25 @@
458
459 namespace UrlPatternUtils {
460
461+/**
462+ * @brief transformWebappSearchPatternToSafePattern
463+ * @param doTransformUrlPath
464+ * @return
465+ */
466 QString transformWebappSearchPatternToSafePattern(const QString&
467 , bool doTransformUrlPath = true);
468
469+/**
470+ * @brief isLocalHtml5ApplicationHomeUrl
471+ * @return
472+ */
473 bool isLocalHtml5ApplicationHomeUrl(const QUrl&);
474
475+/**
476+ * @brief filterAndTransformUrlPatterns
477+ * @param includePatterns
478+ * @return
479+ */
480 QStringList filterAndTransformUrlPatterns(const QStringList & includePatterns);
481
482 }
483
484=== modified file 'src/app/webcontainer/webapp-container.cpp'
485--- src/app/webcontainer/webapp-container.cpp 2015-01-19 10:00:13 +0000
486+++ src/app/webcontainer/webapp-container.cpp 2015-02-02 14:22:27 +0000
487@@ -20,6 +20,7 @@
488 #include "webapp-container.h"
489
490 #include "chrome-cookie-store.h"
491+#include "intent-filter.h"
492 #include "local-cookie-store.h"
493 #include "online-accounts-cookie-store.h"
494 #include "session-utils.h"
495@@ -104,6 +105,7 @@
496 }
497
498 const QString WebappContainer::URL_PATTERN_SEPARATOR = ",";
499+const QString WebappContainer::LOCAL_INTENT_FILTER_FILENAME = "local-intent-filter.js";
500
501
502 WebappContainer::WebappContainer(int& argc, char** argv):
503@@ -212,6 +214,9 @@
504 return false;
505 }
506
507+ // Handle an optional intent filter. The default filter does nothing.
508+ setupLocalIntentFilterIfAny(context);
509+
510 m_component->completeCreate();
511
512 return true;
513@@ -220,6 +225,30 @@
514 }
515 }
516
517+void WebappContainer::setupLocalIntentFilterIfAny(QQmlContext* context)
518+{
519+ if(!context)
520+ {
521+ return;
522+ }
523+
524+ QString localIntentFilterFileContent;
525+ if (IntentFilter::isValidLocalIntentFilterFile(LOCAL_INTENT_FILTER_FILENAME))
526+ {
527+ QFile f(LOCAL_INTENT_FILTER_FILENAME);
528+ if (f.open(QIODevice::ReadOnly))
529+ {
530+ localIntentFilterFileContent = QString(f.readAll());
531+ }
532+ f.close();
533+
534+ qDebug() << "Using local intent filter file:"
535+ << LOCAL_INTENT_FILTER_FILENAME;
536+ }
537+ m_intentFilter.reset(new IntentFilter(localIntentFilterFileContent, NULL));
538+ context->setContextProperty("webappIntentFilter", m_intentFilter.data());
539+}
540+
541 bool WebappContainer::isValidLocalApplicationRunningContext() const
542 {
543 return m_webappModelSearchPath.isEmpty() &&
544
545=== modified file 'src/app/webcontainer/webapp-container.h'
546--- src/app/webcontainer/webapp-container.h 2015-01-19 10:00:13 +0000
547+++ src/app/webcontainer/webapp-container.h 2015-02-02 14:22:27 +0000
548@@ -28,6 +28,9 @@
549 #include <QStringList>
550 #include <QScopedPointer>
551
552+class IntentFilter;
553+class QQmlContext;
554+
555 class WebappContainer : public BrowserApplication
556 {
557 Q_OBJECT
558@@ -49,6 +52,8 @@
559 bool isValidLocalApplicationRunningContext() const;
560 bool isValidLocalResource(const QString& resourceName) const;
561 bool shouldNotValidateCommandLineUrls() const;
562+ bool isValidLocalIntentFilterFile(const QString& filename) const;
563+ void setupLocalIntentFilterIfAny(QQmlContext* context);
564
565 private:
566 QString m_webappName;
567@@ -64,8 +69,10 @@
568 QString m_localCookieStoreDbPath;
569 QString m_userAgentOverride;
570 QScopedPointer<WebappContainerHelper> m_webappContainerHelper;
571+ QScopedPointer<IntentFilter> m_intentFilter;
572
573 static const QString URL_PATTERN_SEPARATOR;
574+ static const QString LOCAL_INTENT_FILTER_FILENAME;
575 };
576
577 #endif // __WEBAPP_CONTAINER_H__
578
579=== modified file 'src/app/webcontainer/webapp-container.qml'
580--- src/app/webcontainer/webapp-container.qml 2015-01-20 15:47:51 +0000
581+++ src/app/webcontainer/webapp-container.qml 2015-02-02 14:22:27 +0000
582@@ -32,6 +32,7 @@
583
584 property string localCookieStoreDbPath: ""
585
586+ property var intentFilterHandler
587 property string url: ""
588 property string webappName: ""
589 property string webappModelSearchPath: ""
590@@ -50,6 +51,9 @@
591
592 title: getWindowTitle()
593
594+ // Used for testing
595+ signal intentUriHandleResult(string uri)
596+
597 function getWindowTitle() {
598 var webappViewTitle =
599 webappViewLoader.item
600@@ -274,6 +278,63 @@
601 webappViewLoader.sourceComponent = webappViewComponent
602 }
603
604+ function makeUrlFromIntentResult(intentFilterResult) {
605+ var scheme = null
606+ var hostname = null
607+ var url = root.currentWebview.url || root.url
608+ if (intentFilterResult.host
609+ && intentFilterResult.host.length !== 0) {
610+ hostname = intentFilterResult.host
611+ }
612+ else {
613+ var matchHostname = url.toString().match(/.*:\/\/([^/]*)\/.*/)
614+ if (matchHostname.length > 1) {
615+ hostname = matchHostname[1]
616+ }
617+ }
618+ if (intentFilterResult.scheme
619+ && intentFilterResult.scheme.length !== 0) {
620+ scheme = intentFilterResult.scheme
621+ }
622+ else {
623+ var matchScheme = url.toString().match(/(.*):\/\/[^/]*\/.*/)
624+ if (matchScheme.length > 1) {
625+ scheme = matchScheme[1]
626+ }
627+ }
628+ return scheme
629+ + '://'
630+ + hostname
631+ + "/"
632+ + (intentFilterResult.uri
633+ ? intentFilterResult.uri : "")
634+ }
635+
636+ /**
637+ * Identity function for non-intent URIs.
638+ *
639+ * Otherwise if the URI is an intent, tries to apply a webapp
640+ * local filter (or identity) and reconstruct the target URI based
641+ * on its result.
642+ */
643+ function handleIntentUri(uri) {
644+ var _uri = uri;
645+ if (webappIntentFilter
646+ && webappIntentFilter.isValidIntentUri(_uri)) {
647+ var result = webappIntentFilter.applyFilter(_uri)
648+ _uri = makeUrlFromIntentResult(result)
649+
650+ console.log("Intent URI '" + uri + "' was mapped to '" + _uri + "'")
651+ }
652+
653+ // Report the result of the intent uri filtering (if any)
654+ // Done for testing purposed. It is not possible at this point
655+ // to have AP call a slot and retrieve its result synchronously.
656+ intentUriHandleResult(_uri)
657+
658+ return _uri
659+ }
660+
661 // Handle runtime requests to open urls as defined
662 // by the freedesktop application dbus interface's open
663 // method for DBUS application activation:
664@@ -294,6 +355,15 @@
665 return;
666 }
667
668+ requestedUrl = handleIntentUri(requestedUrl);
669+
670+ // Add a small guard to prevent browsing to invalid urls
671+ if (currentWebview
672+ && currentWebview.shouldAllowNavigationTo
673+ && !currentWebview.shouldAllowNavigationTo(requestedUrl)) {
674+ return;
675+ }
676+
677 root.url = requestedUrl
678 root.currentWebview.url = requestedUrl
679 }
680
681=== modified file 'tests/autopilot/webapp_container/tests/__init__.py'
682--- tests/autopilot/webapp_container/tests/__init__.py 2015-01-19 10:00:13 +0000
683+++ tests/autopilot/webapp_container/tests/__init__.py 2015-02-02 14:22:27 +0000
684@@ -20,7 +20,7 @@
685
686 from autopilot.testcase import AutopilotTestCase
687 from autopilot.platform import model
688-from testtools.matchers import Equals
689+from testtools.matchers import Equals, GreaterThan
690 from autopilot.matchers import Eventually
691
692 import ubuntuuitoolkit as uitk
693@@ -94,6 +94,19 @@
694 Eventually(Equals(100), timeout=20))
695 self.assertThat(webview.loading, Eventually(Equals(False)))
696
697+ def get_intent_filtered_uri(self, uri):
698+ webviewContainer = self.get_webcontainer_window()
699+ watcher = webviewContainer.watch_signal(
700+ 'intentUriHandleResult(QString)')
701+ previous = watcher.num_emissions
702+ webviewContainer.slots.handleIntentUri(uri)
703+ self.assertThat(
704+ lambda: watcher.num_emissions,
705+ Eventually(GreaterThan(previous)))
706+ result = webviewContainer.get_signal_emissions(
707+ 'intentUriHandleResult(QString)')[-1][0]
708+ return result
709+
710 def browse_to(self, url):
711 webview = self.get_oxide_webview()
712 webview.url = url
713
714=== added file 'tests/autopilot/webapp_container/tests/test_intent_uri_support.py'
715--- tests/autopilot/webapp_container/tests/test_intent_uri_support.py 1970-01-01 00:00:00 +0000
716+++ tests/autopilot/webapp_container/tests/test_intent_uri_support.py 2015-02-02 14:22:27 +0000
717@@ -0,0 +1,116 @@
718+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
719+# Copyright 2014 Canonical
720+#
721+# This program is free software: you can redistribute it and/or modify it
722+# under the terms of the GNU General Public License version 3, as published
723+# by the Free Software Foundation.
724+#
725+# This program is distributed in the hope that it will be useful,
726+# but WITHOUT ANY WARRANTY; without even the implied warranty of
727+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
728+# GNU General Public License for more details.
729+#
730+# You should have received a copy of the GNU General Public License
731+# along with this program. If not, see <http://www.gnu.org/licenses/>.
732+
733+from contextlib import contextmanager
734+import os
735+import tempfile
736+import shutil
737+
738+from testtools.matchers import Equals
739+from autopilot.matchers import Eventually
740+
741+from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase
742+
743+
744+@contextmanager
745+def generate_temp_webapp_with_intent(intent_filter_content=""):
746+ tmpdir = tempfile.mkdtemp()
747+ manifest_content = """
748+ {
749+ "includes": ["http://www.test.com/*"],
750+ "name": "test",
751+ "domain":"",
752+ "homepage":"http://www.test.com/"
753+ }
754+ """
755+ manifest_file = "{}/webapp-properties.json".format(tmpdir)
756+ with open(manifest_file, "w+") as f:
757+ f.write(manifest_content)
758+ if len(intent_filter_content) != 0:
759+ intent_filter_file = "{}/local-intent-filter.js".format(tmpdir)
760+ with open(intent_filter_file, "w+") as f:
761+ f.write(intent_filter_content)
762+ old_cwd = os.getcwd()
763+ try:
764+ os.chdir(tmpdir)
765+ yield tmpdir
766+ finally:
767+ os.chdir(old_cwd)
768+ shutil.rmtree(tmpdir)
769+
770+
771+# Those tests rely on get_intent_filtered_uri() which
772+# relies on implementation detail to trigger part of the intent handling
773+# code. This comes from the fact that the url-dispatcher is not easily
774+# instrumentable , so a full feature flow coverage is quite tricky to get.
775+# Those tests are not really functional in that sense.
776+class WebappContainerIntentUriSupportTestCase(
777+ WebappContainerTestCaseWithLocalContentBase):
778+ def test_basic_intent_parsing(self):
779+ rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname()
780+ with generate_temp_webapp_with_intent() as webapp_install_path:
781+ args = ['--webappModelSearchPath='+webapp_install_path]
782+ self.launch_webcontainer_app(
783+ args,
784+ {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule})
785+ webview = self.get_oxide_webview()
786+ webapp_url = 'http://www.test.com/'
787+ self.assertThat(webview.url, Eventually(Equals(webapp_url)))
788+
789+ intent_uri = 'intent://maps.google.es/maps?ie=utf-8&gl=es\
790+#Intent;scheme=http;package=com.google.android.apps.maps;end'
791+ self.assertThat(
792+ 'http://maps.google.es/maps?ie=utf-8&gl=es',
793+ Equals(self.get_intent_filtered_uri(intent_uri)))
794+
795+ def test_webapp_with_invalid_default_local_intent(self):
796+ rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname()
797+ filter = "1"
798+ with generate_temp_webapp_with_intent(filter) as webapp_install_path:
799+ args = ['--webappModelSearchPath='+webapp_install_path]
800+ self.launch_webcontainer_app(
801+ args,
802+ {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule})
803+ webview = self.get_oxide_webview()
804+ webapp_url = 'http://www.test.com/'
805+ self.assertThat(webview.url, Eventually(Equals(webapp_url)))
806+
807+ intent_uri = 'intent://www.test.com/maps?ie=utf-8&gl=es\
808+#Intent;scheme=http;package=com.google.android.apps.maps;end'
809+ self.assertThat(
810+ 'http://www.test.com/maps?ie=utf-8&gl=es',
811+ Equals(self.get_intent_filtered_uri(intent_uri)))
812+
813+ def test_with_valid_default_local_intent(self):
814+ rule = 'MAP *.test.com:80 ' + self.get_base_url_hostname()
815+ filter = "(function(r) { \
816+ return { \
817+ 'scheme': 'https', \
818+ 'host': 'maps.test.com', \
819+ 'uri': r.uri }; })"
820+ with generate_temp_webapp_with_intent(filter) as webapp_install_path:
821+ args = ['--webappModelSearchPath='+webapp_install_path]
822+ self.launch_webcontainer_app(
823+ args,
824+ {'UBUNTU_WEBVIEW_HOST_MAPPING_RULES': rule})
825+ webview = self.get_oxide_webview()
826+ webapp_url = 'http://www.test.com/'
827+ self.assertThat(webview.url, Eventually(Equals(webapp_url)))
828+
829+ intent_uri = 'intent://www.test.com/maps?ie=utf-8&gl=es\
830+#Intent;scheme=http;package=com.google.android.apps.maps;end'
831+ self.assertThat(
832+ 'https://maps.test.com/maps?ie=utf-8&gl=es',
833+ Equals(self.get_intent_filtered_uri(intent_uri)))
834
835=== modified file 'tests/unittests/CMakeLists.txt'
836--- tests/unittests/CMakeLists.txt 2015-01-19 10:00:13 +0000
837+++ tests/unittests/CMakeLists.txt 2015-02-02 14:22:27 +0000
838@@ -18,3 +18,4 @@
839 add_subdirectory(session-storage)
840 add_subdirectory(favicon-fetcher)
841 add_subdirectory(webapp-container-hook)
842+add_subdirectory(intent-filter)
843
844=== added directory 'tests/unittests/intent-filter'
845=== added file 'tests/unittests/intent-filter/CMakeLists.txt'
846--- tests/unittests/intent-filter/CMakeLists.txt 1970-01-01 00:00:00 +0000
847+++ tests/unittests/intent-filter/CMakeLists.txt 2015-02-02 14:22:27 +0000
848@@ -0,0 +1,9 @@
849+set(TEST tst_IntentFilterTests)
850+set(SOURCES
851+ ${webapp-container_SOURCE_DIR}/intent-filter.cpp
852+ tst_IntentFilterTests.cpp
853+)
854+include_directories(${webapp-container_SOURCE_DIR})
855+add_executable(${TEST} ${SOURCES})
856+qt5_use_modules(${TEST} Core Test Qml)
857+add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
858
859=== added file 'tests/unittests/intent-filter/tst_IntentFilterTests.cpp'
860--- tests/unittests/intent-filter/tst_IntentFilterTests.cpp 1970-01-01 00:00:00 +0000
861+++ tests/unittests/intent-filter/tst_IntentFilterTests.cpp 2015-02-02 14:22:27 +0000
862@@ -0,0 +1,218 @@
863+/*
864+ * Copyright 2014 Canonical Ltd.
865+ *
866+ * This file is part of webbrowser-app.
867+ *
868+ * webbrowser-app is free software; you can redistribute it and/or modify
869+ * it under the terms of the GNU General Public License as published by
870+ * the Free Software Foundation; version 3.
871+ *
872+ * webbrowser-app is distributed in the hope that it will be useful,
873+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
874+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
875+ * GNU General Public License for more details.
876+ *
877+ * You should have received a copy of the GNU General Public License
878+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
879+ */
880+
881+// Qt
882+#include <QtCore/QString>
883+#include <QtTest/QtTest>
884+
885+// local
886+#include "intent-filter.h"
887+
888+class IntentFilterTests : public QObject
889+{
890+ Q_OBJECT
891+
892+private Q_SLOTS:
893+
894+ void parseIntentUris_data()
895+ {
896+ QTest::addColumn<QString>("intentUris");
897+
898+ QTest::addColumn<QString>("scheme");
899+ QTest::addColumn<QString>("package");
900+ QTest::addColumn<QString>("uri");
901+ QTest::addColumn<QString>("host");
902+ QTest::addColumn<QString>("action");
903+ QTest::addColumn<QString>("component");
904+ QTest::addColumn<QString>("category");
905+
906+ QTest::addColumn<bool>("isValid");
907+
908+ QTest::newRow("Valid intent - host only")
909+ << "intent://scan/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
910+ << "zxing"
911+ << "com.google.zxing.client.android"
912+ << "/"
913+ << "scan"
914+ << "com"
915+ << "com"
916+ << "BROWSABLE"
917+ << true;
918+
919+ QTest::newRow("Valid intent - no host w/ uri")
920+ << "intent://scan/?a=1/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
921+ << "zxing"
922+ << "com.google.zxing.client.android"
923+ << "?a=1"
924+ << "scan"
925+ << "com"
926+ << "com"
927+ << "BROWSABLE"
928+ << true;
929+
930+ QTest::newRow("Valid intent - w/ host")
931+ << "intent://host/my/long/path?a=1/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
932+ << "zxing"
933+ << "com.google.zxing.client.android"
934+ << "my/long/path?a=1"
935+ << "host"
936+ << "com"
937+ << "com"
938+ << "BROWSABLE"
939+ << true;
940+
941+ QTest::newRow("Valid intent - w/o host & uri-path")
942+ << "intent://#Intent;scheme=trusper.referrertests;package=trusper.referrertests;end"
943+ << "trusper.referrertests"
944+ << "trusper.referrertests"
945+ << ""
946+ << ""
947+ << ""
948+ << ""
949+ << ""
950+ << true;
951+
952+ QTest::newRow("Valid intent - w/o host & uri-path and extra /")
953+ << "intent:///#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
954+ << "zxing"
955+ << "com.google.zxing.client.android"
956+ << "/"
957+ << ""
958+ << "com"
959+ << "com"
960+ << "BROWSABLE"
961+ << true;
962+
963+ QTest::newRow("Valid intent - w/o host & uri-path impler syntax")
964+ << "intent://#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
965+ << "zxing"
966+ << "com.google.zxing.client.android"
967+ << ""
968+ << ""
969+ << "com"
970+ << "com"
971+ << "BROWSABLE"
972+ << true;
973+
974+ QTest::newRow("Invalid intent")
975+ << "intent:///#Inttent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
976+ << ""
977+ << ""
978+ << ""
979+ << ""
980+ << ""
981+ << ""
982+ << ""
983+ << false;
984+ }
985+
986+ void parseIntentUris()
987+ {
988+ QFETCH(QString, intentUris);
989+
990+ QFETCH(QString, scheme);
991+ QFETCH(QString, package);
992+ QFETCH(QString, uri);
993+ QFETCH(QString, host);
994+ QFETCH(QString, action);
995+ QFETCH(QString, component);
996+ QFETCH(QString, category);
997+
998+ QFETCH(bool, isValid);
999+
1000+ IntentUriDescription d = parseIntentUri(intentUris);
1001+
1002+ QCOMPARE(d.scheme, scheme);
1003+ QCOMPARE(d.package, package);
1004+ QCOMPARE(d.uriPath, uri);
1005+ QCOMPARE(d.host, host);
1006+ QCOMPARE(d.action, action);
1007+ QCOMPARE(d.component, component);
1008+ QCOMPARE(d.category, category);
1009+
1010+ QVERIFY(IntentFilter::isValidIntentDescription(d) == isValid);
1011+
1012+ QString emptyContent;
1013+ IntentFilter pf(emptyContent);
1014+ QVERIFY(pf.isValidIntentUri(intentUris) == isValid);
1015+ }
1016+
1017+ void applyFilters_data()
1018+ {
1019+ QTest::addColumn<QString>("intentUris");
1020+
1021+ QTest::addColumn<QString>("filterFunctionSource");
1022+
1023+ QTest::addColumn<QString>("scheme");
1024+ QTest::addColumn<QString>("uri");
1025+ QTest::addColumn<QString>("host");
1026+
1027+ QTest::newRow("Valid intent - default filter function")
1028+ << "intent://scan/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
1029+ << ""
1030+ << "zxing"
1031+ << "/"
1032+ << "scan";
1033+
1034+ QTest::newRow("Valid intent - default filter function")
1035+ << "intent://scan/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
1036+ << "(function(result) {return {'scheme': result.scheme+'custom', 'uri': result.uri+'custom', 'host': result.host+'custom'}; })"
1037+ << "zxingcustom"
1038+ << "/custom"
1039+ << "scancustom";
1040+
1041+ QTest::newRow("Valid intent - no (optional) host in filter result")
1042+ << "intent://host/my/long/path?a=1/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
1043+ << "(function(result) {return {'scheme': result.scheme+'custom', 'uri': result.uri+'custom' }; })"
1044+ << "zxingcustom"
1045+ << "my/long/path?a=1custom"
1046+ << "";
1047+
1048+ QTest::newRow("Valid intent - invalid filter fallback to default")
1049+ << "intent://host/my/long/path?a=1/#Intent;component=com;scheme=zxing;category=BROWSABLE;action=com;package=com.google.zxing.client.android;end"
1050+ << "(function(result) {return { 'uri': result.uri+'custom' }; })"
1051+ << "zxing"
1052+ << "my/long/path?a=1"
1053+ << "host";
1054+ }
1055+
1056+ void applyFilters()
1057+ {
1058+ QFETCH(QString, intentUris);
1059+
1060+ QFETCH(QString, filterFunctionSource);
1061+
1062+ QFETCH(QString, scheme);
1063+ QFETCH(QString, uri);
1064+ QFETCH(QString, host);
1065+
1066+ IntentFilter pf(filterFunctionSource);
1067+ QVERIFY(pf.isValidIntentUri(intentUris));
1068+
1069+ QVariantMap r = pf.applyFilter(intentUris);
1070+ QVERIFY(r.contains("scheme"));
1071+ QVERIFY(r.contains("uri"));
1072+
1073+ QCOMPARE(r.value("scheme").toString(), scheme);
1074+ QCOMPARE(r.value("host").toString(), host);
1075+ QCOMPARE(r.value("uri").toString(), uri);
1076+ }
1077+};
1078+
1079+QTEST_MAIN(IntentFilterTests)
1080+#include "tst_IntentFilterTests.moc"

Subscribers

People subscribed via source and target branches