Merge lp:~abreu-alexandre/webbrowser-app/rtm-intent-support into lp:webbrowser-app/rtm-14.09
- rtm-intent-support
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Olivier Tilloy | Approve | ||
Review via email:
|
Commit message
Backport for the intent:// support.
Description of the change
Backport for the intent:// support.
To post a comment you must log in.
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" |
LGTM.