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

Proposed by Alexandre Abreu
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 890
Merged at revision: 883
Proposed branch: lp:~abreu-alexandre/webbrowser-app/intent
Merge into: lp:webbrowser-app
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/intent
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Olivier Tilloy Approve
Ted Gould Pending
Review via email: mp+247421@code.launchpad.net

Commit message

Add support for intent:// schemes in the container.

Description of the change

Add support for intent:// schemes in the container.

The current approach is to have a basic proper support for intent:// URIs in the URIHandler. Those URIs are detected and the host/uri/scheme are extracted to make up the target requested URI.

In order to add a bit of flexibility on the webapp side on how the intent should be treated, an optional javascript file (as a drop-in file) can be added to the webapp. This file should export a function such as:

(function(intent) { return intent; });

(the body of the function being obviously webapp dependant).

This function gets as a parameter an object with the following keys:

'scheme': the scheme for the intent
'host': the host (optional) for the intent
'uri': the uri for the intent

and is supposed to return an object that has the same "shape". Any failure to do so will make the webapp intent mechanism fallback on a function that acts as the identity function.

One has to add a specific argument to specific a specific custom intent filtering file:

--use-local-intent-filter[=intent-filename]

if no intent filename is specified a default one is looked up with the name:

"local-intent-filter.js"

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

I’m not seeing the handleIntentUri() function being called anywhere.

And to be honest I’m not sure I understand the whole logic of this change.

Here is my understanding of how it should work: when an intent:// URL is clicked either in the browser or in a webapp, its handling will be delegated to the URL dispatcher, which will special case it to determine which app to target with it. The app will be invoked with the intent:// URL unchanged, so in the case of a webapp that registered for it, it needs to transform this URL to a valid http:// one.

So there are two places in the webapp-container’s code that need to handle such a transformation:
 - if the app was not previously running, it will be invoked with the intent:// URL as a parameter, the transformation needs to happen before setting the URL on the webview
 - if the app was already running, the UriHandler.onOpened handler needs to do the transformation

From a quick glance at the code, I don’t see this happening. Can you shed a light on this please?

Revision history for this message
Olivier Tilloy (osomon) wrote :

OK, now that looks better (my comment went in just after your change).

I’m wondering, why does the function receive an object that has 'scheme', 'host' and 'uri' as keys?
Wouldn’t it be enough to pass the function a string (the full intent:// URI) and expect a string (the transformed http:// URI) in return?

Also, is it really useful to allow the custom intent file to have a different name than the default one? It seems it adds a lot of logic for no apparent gain.

Revision history for this message
Olivier Tilloy (osomon) wrote :

If I’m not mistaken, when the custom JS file is parsed (in intent-filter.cpp), the whole of its contents are evaluated. So there’s no check on whether it contains something else than the expected function. What if the file contains several functions? Or something else?

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> OK, now that looks better (my comment went in just after your change).
>
> I’m wondering, why does the function receive an object that has 'scheme',
> 'host' and 'uri' as keys?
> Wouldn’t it be enough to pass the function a string (the full intent:// URI)
> and expect a string (the transformed http:// URI) in return?

imo no, I wrote it this way to be very strict & avoid any error that could come
from something that is user defined. I did not want to let the intent parsing
be hosted in the function itself, it would not make sense imo and be possibly
error prone.

Hence, a strict (already parsed) intent description is passed down to the function
and something very strict is expected in return, not some random data,

> Also, is it really useful to allow the custom intent file to have a different
> name than the default one? It seems it adds a lot of logic for no apparent
> gain.

I though it could be something useful, but I can remove this, I am not strongly
opinionated about it :)

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> If I’m not mistaken, when the custom JS file is parsed (in intent-filter.cpp),
> the whole of its contents are evaluated. So there’s no check on whether it
> contains something else than the expected function. What if the file contains
> several functions? Or something else?

Then the check fails and we fallback to the default filter.
Again, I tried to be strict about what is expected from the intent file (also in its
IO), one need to expose a callback object and if other functions are needed they
need to be hosted in the top level callable object,

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

> > Also, is it really useful to allow the custom intent file to have a
> different
> > name than the default one? It seems it adds a lot of logic for no apparent
> > gain.
>
> I though it could be something useful, but I can remove this, I am not
> strongly
> opinionated about it :)

Yeah, I wouldn’t mind if you removed it, that would result in less code to review, and again I think it doesn’t add any real value for webapp developers anyway. Thanks!

875. By Launchpad Translations on behalf of phablet-team

Launchpad automatic translations update.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> > > Also, is it really useful to allow the custom intent file to have a
> > different
> > > name than the default one? It seems it adds a lot of logic for no apparent
> > > gain.
> >
> > I though it could be something useful, but I can remove this, I am not
> > strongly
> > opinionated about it :)
>
> Yeah, I wouldn’t mind if you removed it, that would result in less code to
> review, and again I think it doesn’t add any real value for webapp developers
> anyway. Thanks!

done.

876. By Launchpad Translations on behalf of phablet-team

Launchpad automatic translations update.

877. By Launchpad Translations on behalf of phablet-team

Launchpad automatic translations update.

Revision history for this message
Olivier Tilloy (osomon) wrote :

Can’t we get rid of the --use-local-intent-filter command-line option? I.e., make its use implicit: if there is a well-formed 'local-intent-filter.js' file, use it, otherwise use the default passthrough filter. Just like for the 'webapp-properties.json' file.

Revision history for this message
Olivier Tilloy (osomon) wrote :

In src/app/webcontainer/CMakeLists.txt, you should add "Qml" to the list of Qt5 modules that are required (qt5_use_modules(…)).

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

Cosmetics: now that the ability to provide a custom filename for the intent filter has been removed, maybe DEFAULT_LOCAL_INTENT_FILTER_FILENAME could be renamed to LOCAL_INTENT_FILTER_FILENAME ?

Revision history for this message
Olivier Tilloy (osomon) wrote :

Why not make the parameter of parseIntentUri(…) a QUrl ?
This would make it much easier to parse the URI, without the need for regexpes. E.g.:

    QUrl url("intent://…");

    qDebug() << "URL:" << url;
    qDebug() << "scheme:" << url.scheme();
    qDebug() << "host:" << url.host();
    qDebug() << "path:" << url.path();
    qDebug() << "query:" << url.query();
    qDebug() << "fragment:" << url.fragment();

    QStringList fragments = url.fragment().split(";");
    assert(fragments.takeFirst() == "Intent");
    assert(fragments.takeLast() == "end");
    QMap<QString, QString> tokens;
    Q_FOREACH(const QString& fragment, fragments) {
        QStringList token = fragment.split("=");
        assert(token.size() == 2);
        tokens.insert(token[0], token[1]);
    }
    qDebug() << "tokens:" << tokens;

Revision history for this message
Olivier Tilloy (osomon) wrote :

There is a missing autopilot test case: handling of an intent:// URI for a webapp that doesn’t ship a 'local-intent-filter.js' file.

Revision history for this message
Olivier Tilloy (osomon) wrote :

676 + def get_intent_filtered_uri(self, uri):
[…]
681 + webviewContainer.slots.handleIntentUri(uri)

This is wrong, the test case shouldn’t call a slot on the QML object: this is not testing a real-world use-case any longer. Instead, the webapp’s homepage should have an "intent://" link and the test should click that link and ensure that as a result the URL of the webview changes to the expected transformed URL.

review: Needs Fixing
Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> 676 + def get_intent_filtered_uri(self, uri):
> […]
> 681 + webviewContainer.slots.handleIntentUri(uri)
>
> This is wrong, the test case shouldn’t call a slot on the QML object: this is
> not testing a real-world use-case any longer. Instead, the webapp’s homepage
> should have an "intent://" link and the test should click that link and ensure
> that as a result the URL of the webview changes to the expected transformed
> URL.

I know that this is "wrong", but I did it this way after exploring how
'intrumentable' the url-dispatcher was. And it is not at the required
level to test the whole flow ...

The way that it is it test *some* of the logic and bits of code (closer
to something at the unit level),

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> There is a missing autopilot test case: handling of an intent:// URI for a
> webapp that doesn’t ship a 'local-intent-filter.js' file.

the test_basic_intent_parsing AP test is meant to tackle that ...

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> Why not make the parameter of parseIntentUri(…) a QUrl ?
> This would make it much easier to parse the URI, without the need for
> regexpes. E.g.:
>
> QUrl url("intent://…");
>
> qDebug() << "URL:" << url;
> qDebug() << "scheme:" << url.scheme();
> qDebug() << "host:" << url.host();
> qDebug() << "path:" << url.path();
> qDebug() << "query:" << url.query();
> qDebug() << "fragment:" << url.fragment();
>
> QStringList fragments = url.fragment().split(";");
> assert(fragments.takeFirst() == "Intent");
> assert(fragments.takeLast() == "end");
> QMap<QString, QString> tokens;
> Q_FOREACH(const QString& fragment, fragments) {
> QStringList token = fragment.split("=");
> assert(token.size() == 2);
> tokens.insert(token[0], token[1]);
> }
> qDebug() << "tokens:" << tokens;

I'd agree, except that it also brings extra logic to reconstruct
the intent URI parts:
- path + query (if any),
- host (if any) and path,
etc.
plus some other things,

which in the end makes it also a bit convoluted imo,

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

all other comments addressed

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Francis Ginther (fginther) wrote :

The jenkins node for the i386 and amd64 tests failed, I've restarted a new ci run.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
878. By Launchpad Translations on behalf of phablet-team

Launchpad automatic translations update.

879. By Olivier Tilloy

Update the m.youtube.com UA override to fix video playback. Fixes: #1415107
Approved by: Alexandre Abreu

880. By CI Train Bot Account

Releasing 0.23+15.04.20150127-0ubuntu1

881. By CI Train Bot Account

Resync trunk

Revision history for this message
Olivier Tilloy (osomon) wrote :

> > Why not make the parameter of parseIntentUri(…) a QUrl ?
> > This would make it much easier to parse the URI, without the need for
> > regexpes. E.g.:
> >
> > QUrl url("intent://…");
> >
> > qDebug() << "URL:" << url;
> > qDebug() << "scheme:" << url.scheme();
> > qDebug() << "host:" << url.host();
> > qDebug() << "path:" << url.path();
> > qDebug() << "query:" << url.query();
> > qDebug() << "fragment:" << url.fragment();
> >
> > QStringList fragments = url.fragment().split(";");
> > assert(fragments.takeFirst() == "Intent");
> > assert(fragments.takeLast() == "end");
> > QMap<QString, QString> tokens;
> > Q_FOREACH(const QString& fragment, fragments) {
> > QStringList token = fragment.split("=");
> > assert(token.size() == 2);
> > tokens.insert(token[0], token[1]);
> > }
> > qDebug() << "tokens:" << tokens;
>
> I'd agree, except that it also brings extra logic to reconstruct
> the intent URI parts:
> - path + query (if any),
> - host (if any) and path,
> etc.
> plus some other things,
>
> which in the end makes it also a bit convoluted imo,

Fair enough (I disagree but let’s agree to disagree). A couple of comments on the logic though:

 - the intentRe regexp should end with the ";end;" token
 - the assignment of result.host and result.uriPath will fail if s has more than 2 items (e.g. if the path is several levels deep, like example.org/example/path/to/some/resource)

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

> > 676 + def get_intent_filtered_uri(self, uri):
> > […]
> > 681 + webviewContainer.slots.handleIntentUri(uri)
> >
> > This is wrong, the test case shouldn’t call a slot on the QML object: this
> is
> > not testing a real-world use-case any longer. Instead, the webapp’s homepage
> > should have an "intent://" link and the test should click that link and
> ensure
> > that as a result the URL of the webview changes to the expected transformed
> > URL.
>
> I know that this is "wrong", but I did it this way after exploring how
> 'intrumentable' the url-dispatcher was. And it is not at the required
> level to test the whole flow ...
>
> The way that it is it test *some* of the logic and bits of code (closer
> to something at the unit level),

Can’t the 'UriHandler' global object be mocked and made to emit its "opened" signal (without the need to tinker with the URL dispatcher)?

If not, at the very least this test case should have a comment to explain what it does and why it’s not a real integration test.

882. By Alexandre Abreu

rever pot commit

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> > > Why not make the parameter of parseIntentUri(…) a QUrl ?
> > > This would make it much easier to parse the URI, without the need for
> > > regexpes. E.g.:
> > >
> > > QUrl url("intent://…");
> > >
> > > qDebug() << "URL:" << url;
> > > qDebug() << "scheme:" << url.scheme();
> > > qDebug() << "host:" << url.host();
> > > qDebug() << "path:" << url.path();
> > > qDebug() << "query:" << url.query();
> > > qDebug() << "fragment:" << url.fragment();
> > >
> > > QStringList fragments = url.fragment().split(";");
> > > assert(fragments.takeFirst() == "Intent");
> > > assert(fragments.takeLast() == "end");
> > > QMap<QString, QString> tokens;
> > > Q_FOREACH(const QString& fragment, fragments) {
> > > QStringList token = fragment.split("=");
> > > assert(token.size() == 2);
> > > tokens.insert(token[0], token[1]);
> > > }
> > > qDebug() << "tokens:" << tokens;
> >
> > I'd agree, except that it also brings extra logic to reconstruct
> > the intent URI parts:
> > - path + query (if any),
> > - host (if any) and path,
> > etc.
> > plus some other things,
> >
> > which in the end makes it also a bit convoluted imo,
>
> Fair enough (I disagree but let’s agree to disagree). A couple of comments on
> the logic though:
>
> - the intentRe regexp should end with the ";end;" token
> - the assignment of result.host and result.uriPath will fail if s has more
> than 2 items (e.g. if the path is several levels deep, like
> example.org/example/path/to/some/resource)

ok, I agree to disagree w/ my disagreement, ... this stupid inattention mistake
convinced me,

done (usage of QUrl)

Revision history for this message
Olivier Tilloy (osomon) wrote :

In tests/unittests/intent-filter/CMakeLists.txt:

838 +set(TEST tst_IntentFilterTests)
839 +set(SOURCES
840 + ${CMAKE_SOURCE_DIR}/src/app/webcontainer/intent-filter.cpp

  this should be ${webapp-container_SOURCE_DIR}/intent-filter.cpp

841 + tst_IntentFilterTests.cpp
842 +)
843 +set(WEBAPP_CONTAINER_INTENT_FILTER webapp-container-intent-filter)

  the variable defined here seems unused

844 +include_directories(${CMAKE_SOURCE_DIR})

  this should be ${webapp-container_SOURCE_DIR}

845 +add_executable(${TEST} ${SOURCES})
846 +qt5_use_modules(${TEST} Core Test Qml)
847 +add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)

review: Needs Fixing
883. By Alexandre Abreu

addressed cmakelist.txt fixes

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> In tests/unittests/intent-filter/CMakeLists.txt:
>
> 838 +set(TEST tst_IntentFilterTests)
> 839 +set(SOURCES
> 840 + ${CMAKE_SOURCE_DIR}/src/app/webcontainer/intent-filter.cpp
>
> this should be ${webapp-container_SOURCE_DIR}/intent-filter.cpp
>
> 841 + tst_IntentFilterTests.cpp
> 842 +)
> 843 +set(WEBAPP_CONTAINER_INTENT_FILTER webapp-container-intent-filter)
>
> the variable defined here seems unused
>
> 844 +include_directories(${CMAKE_SOURCE_DIR})
>
> this should be ${webapp-container_SOURCE_DIR}
>
> 845 +add_executable(${TEST} ${SOURCES})
> 846 +qt5_use_modules(${TEST} Core Test Qml)
> 847 +add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o
> ${TEST}.xml)

done

Revision history for this message
Olivier Tilloy (osomon) wrote :

342 +#include "intent-filter.moc"

this is not needed and generates warning at build time

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
884. By Alexandre Abreu

remove *.moc file unneeded

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> 342 +#include "intent-filter.moc"
>
> this is not needed and generates warning at build time

sorry it was a leftover from a previous version

885. By Alexandre Abreu

add AP test comment

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> > > 676 + def get_intent_filtered_uri(self, uri):
> > > […]
> > > 681 + webviewContainer.slots.handleIntentUri(uri)
> > >
> > > This is wrong, the test case shouldn’t call a slot on the QML object: this
> > is
> > > not testing a real-world use-case any longer. Instead, the webapp’s
> homepage
> > > should have an "intent://" link and the test should click that link and
> > ensure
> > > that as a result the URL of the webview changes to the expected
> transformed
> > > URL.
> >
> > I know that this is "wrong", but I did it this way after exploring how
> > 'intrumentable' the url-dispatcher was. And it is not at the required
> > level to test the whole flow ...
> >
> > The way that it is it test *some* of the logic and bits of code (closer
> > to something at the unit level),
>
> Can’t the 'UriHandler' global object be mocked and made to emit its "opened"
> signal (without the need to tinker with the URL dispatcher)?
>
> If not, at the very least this test case should have a comment to explain what
> it does and why it’s not a real integration test.

I am convinced by that approach at this level (of testing) ... we would still strain
a bit far from a proper function test, and imo wouldn't be that different from
the selective testing above really, ... I added a comment in the test

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

In tests/unittests/intent-filter/tst_IntentFilterTests.cpp:

1004 + IntentFilter * pf = new IntentFilter(QString());
1050 + IntentFilter * pf = new IntentFilter(filterFunctionSource);

pf is leaked. Could it be instantiated on the stack instead?

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

In IntentFilter::isValidIntentFilterResult(…), the checks expect the result to have a host, but in the declaration of the IntentUriDescription struct, 'host' is marked optional. So, is it optional or required?

Revision history for this message
Olivier Tilloy (osomon) wrote :

In IntentFilter, d_ptr is never destroyed, so it’s leaked. You could use a QScopedPointer to automate memory management.

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

279 + // Fallback to a noop
280 + result = d->evaluate(
281 + IntentFilterPrivate::DEFAULT_PASS_THROUGH_FILTER
282 + , intentDescription).toVariant().toMap();

This looks like a rather complex (and expensive) way of transforming intentDescription into a QVariantMap.
What about getting rid of the IntentUriDescription struct altogether, to replace it everywhere with QVariantMap?

Revision history for this message
Olivier Tilloy (osomon) wrote :

307 + trimUriSeparator(path);

Is this correct? A valid URI path normally starts with a leading forward slash, so why trim it? And a path that ends with a slash is (I think) different from a path that doesn’t. E.g.:

    http://example.com/test/?q=r

    !=

    http://example.com/test?q=r

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> In tests/unittests/intent-filter/tst_IntentFilterTests.cpp:
>
> 1004 + IntentFilter * pf = new IntentFilter(QString());
> 1050 + IntentFilter * pf = new IntentFilter(filterFunctionSource);
>
> pf is leaked. Could it be instantiated on the stack instead?

why 'need fixing'? I did it on purpose ... IntentFilter is a QObject which is not
copy constructiblem and ... this is a test ...

886. By Alexandre Abreu

oops fix leaked intent dptr

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> In IntentFilter, d_ptr is never destroyed, so it’s leaked. You could use a
> QScopedPointer to automate memory management.

totally, fixed

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> 279 + // Fallback to a noop
> 280 + result = d->evaluate(
> 281 +
> IntentFilterPrivate::DEFAULT_PASS_THROUGH_FILTER
> 282 + , intentDescription).toVariant().toMap();
>
> This looks like a rather complex (and expensive) way of transforming
> intentDescription into a QVariantMap.
> What about getting rid of the IntentUriDescription struct altogether, to
> replace it everywhere with QVariantMap?

The 'expensive' factor is very relative to the usage context ... but in this
case it not that, ... it has nothing to do w/ intentDescription, but w/ the result
of QJSValue -> QVAriantMap ...

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> In IntentFilter::isValidIntentFilterResult(…), the checks expect the result to
> have a host, but in the declaration of the IntentUriDescription struct, 'host'
> is marked optional. So, is it optional or required?

optional as you known and as I know ..., but as I said before
I wanted the object sent to the js have a strict & fixed structure,

I can remove if you strongly object,

Revision history for this message
Olivier Tilloy (osomon) wrote :

> > In tests/unittests/intent-filter/tst_IntentFilterTests.cpp:
> >
> > 1004 + IntentFilter * pf = new IntentFilter(QString());
> > 1050 + IntentFilter * pf = new IntentFilter(filterFunctionSource);
> >
> > pf is leaked. Could it be instantiated on the stack instead?
>
> why 'need fixing'? I did it on purpose ... IntentFilter is a QObject which is
> not
> copy constructiblem and ... this is a test ...

I’m not sure I understand your point. The fact that it is a test doesn’t mean that it shouldn’t follow good practices and free memory after itself.
But really, why not instantiate the objects on the stack and let them be destroyed automatically when they go out of scope? I.e.:

    IntentFilter pf(QString());
    QVERIFY(pf.isValidIntentUri(intentUris) == isValid);

Revision history for this message
Olivier Tilloy (osomon) wrote :

> > 279 + // Fallback to a noop
> > 280 + result = d->evaluate(
> > 281 +
> > IntentFilterPrivate::DEFAULT_PASS_THROUGH_FILTER
> > 282 + , intentDescription).toVariant().toMap();
> >
> > This looks like a rather complex (and expensive) way of transforming
> > intentDescription into a QVariantMap.
> > What about getting rid of the IntentUriDescription struct altogether, to
> > replace it everywhere with QVariantMap?
>
> The 'expensive' factor is very relative to the usage context ... but in this
> case it not that, ... it has nothing to do w/ intentDescription, but w/ the
> result
> of QJSValue -> QVAriantMap ...

Sure, the cost is probably overall negligible. Still, in this case d->evaluate(…) is called only to turn an IntentUriDescription into a QVariantMap (there’s no need to use a temporary QJSValue for that), and this feels like killing a fly with a sledgehammer to me. But I’m fine with it either way, after all, it works.

Revision history for this message
Olivier Tilloy (osomon) wrote :

> > In IntentFilter::isValidIntentFilterResult(…), the checks expect the result
> to
> > have a host, but in the declaration of the IntentUriDescription struct,
> 'host'
> > is marked optional. So, is it optional or required?
>
> optional as you known and as I know ..., but as I said before
> I wanted the object sent to the js have a strict & fixed structure,
>
> I can remove if you strongly object,

I won’t strongly object :)
I’m just concerned that if someone has to pick up that code in the future, she will be puzzled as to whether host is optional or required.

By the way, could you add a link to https://developer.chrome.com/multidevice/android/intents somewhere in the code, for reference?

887. By Alexandre Abreu

fix path trimmning

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> 307 + trimUriSeparator(path);
>
> Is this correct? A valid URI path normally starts with a leading forward
> slash, so why trim it? And a path that ends with a slash is (I think)
> different from a path that doesn’t. E.g.:
>
> http://example.com/test/?q=r
>
> !=
>
> http://example.com/test?q=r

I want things to be somewhat normalized (if I can call it this way), for the
web and *in practice* I dont think it makes a difference (unless I miss an
obvious case).

After thinking about it, I think you are right on this, rather be strict ...

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> > > In tests/unittests/intent-filter/tst_IntentFilterTests.cpp:
> > >
> > > 1004 + IntentFilter * pf = new IntentFilter(QString());
> > > 1050 + IntentFilter * pf = new
> IntentFilter(filterFunctionSource);
> > >
> > > pf is leaked. Could it be instantiated on the stack instead?
> >

ok, I had a compiler error that led me to quickly think that the qobject
based was not statically instantiatable,

+1

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> > > In IntentFilter::isValidIntentFilterResult(…), the checks expect the
> result
> > to
> > > have a host, but in the declaration of the IntentUriDescription struct,
> > 'host'
> > > is marked optional. So, is it optional or required?
> >
> > optional as you known and as I know ..., but as I said before
> > I wanted the object sent to the js have a strict & fixed structure,
> >
> > I can remove if you strongly object,
>
> I won’t strongly object :)
> I’m just concerned that if someone has to pick up that code in the future, she
> will be puzzled as to whether host is optional or required.
>
> By the way, could you add a link to
> https://developer.chrome.com/multidevice/android/intents somewhere in the
> code, for reference?

+1

888. By Alexandre Abreu

fixes

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
889. By Alexandre Abreu

fix additional free ptr

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

I tested the branch on a device (using silo 28, in conjunction with https://code.launchpad.net/~osomon/webapps-core/gmaps-intent-hook/+merge/245982), and it works nicely, both when the webapp is already running and when it isn’t.

One thing that I think would be useful is a console.log() to inform that an intent URI was transformed into an http[s] one (with both URIs, to ease debugging in case of problems).

review: Approve
890. By Alexandre Abreu

Add log

Revision history for this message
Alexandre Abreu (abreu-alexandre) wrote :

> One thing that I think would be useful is a console.log() to inform that an
> intent URI was transformed into an http[s] one (with both URIs, to ease
> debugging in case of problems).

+1, done,

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

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 13:56:12 +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 2014-11-25 15:00:45 +0000
23+++ src/app/webcontainer/WebViewImplWebkit.qml 2015-02-02 13:56:12 +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 13:56:12 +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 13:56:12 +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 2014-11-19 14:38:16 +0000
456+++ src/app/webcontainer/url-pattern-utils.h 2015-02-02 13:56:12 +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 2014-11-26 15:55:26 +0000
486+++ src/app/webcontainer/webapp-container.cpp 2015-02-02 13:56:12 +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 2014-11-20 16:46:17 +0000
547+++ src/app/webcontainer/webapp-container.h 2015-02-02 13:56:12 +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-06 19:14:09 +0000
581+++ src/app/webcontainer/webapp-container.qml 2015-02-02 13:56:12 +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-15 08:26:53 +0000
683+++ tests/autopilot/webapp_container/tests/__init__.py 2015-02-02 13:56:12 +0000
684@@ -21,7 +21,7 @@
685 import fixtures
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@@ -96,6 +96,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 13:56:12 +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 2014-12-02 09:21:38 +0000
837+++ tests/unittests/CMakeLists.txt 2015-02-02 13:56:12 +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 13:56:12 +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 13:56:12 +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

to status/vote changes: