Merge lp:~mardy/webbrowser-app/per-account-dir into lp:webbrowser-app

Proposed by Alberto Mardegan
Status: Rejected
Rejected by: Alberto Mardegan
Proposed branch: lp:~mardy/webbrowser-app/per-account-dir
Merge into: lp:webbrowser-app
Diff against target: 555 lines (+140/-136)
12 files modified
src/app/WebViewImpl.qml (+5/-0)
src/app/webcontainer/AccountsPage.qml (+5/-29)
src/app/webcontainer/WebApp.qml (+1/-0)
src/app/webcontainer/WebViewImplWebkit.qml (+1/-0)
src/app/webcontainer/WebappContainerWebview.qml (+4/-1)
src/app/webcontainer/chrome-cookie-store.cpp (+0/-13)
src/app/webcontainer/chrome-cookie-store.h (+0/-7)
src/app/webcontainer/cookie-store.cpp (+12/-20)
src/app/webcontainer/online-accounts-cookie-store.cpp (+4/-5)
src/app/webcontainer/oxide-cookie-helper.cpp (+5/-4)
src/app/webcontainer/webapp-container.cpp (+2/-0)
src/app/webcontainer/webapp-container.qml (+101/-57)
To merge this branch: bzr merge lp:~mardy/webbrowser-app/per-account-dir
Reviewer Review Type Date Requested Status
Alberto Mardegan (community) Needs Resubmitting
PS Jenkins bot continuous-integration Needs Fixing
Review via email: mp+237428@code.launchpad.net

Commit message

Use a different data location for different accounts

Move the webview behind a Loader, and set the dataPath according to the account number.

Description of the change

Use a different data location for different accounts

Move the webview behind a Loader, and set the dataPath according to the account number.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
758. By Alberto Mardegan

Remove unused property

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

Watch URL changes

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)
760. By Alberto Mardegan

from trunk

[ Alberto Mardegan ]
Filter out account services which belong to other webapps

761. By Alberto Mardegan

Use dataLocation

Apparmor rules prevent oxide from using ~/.cache/

762. By Alberto Mardegan

Check dates at a later stage

763. By Alberto Mardegan

Fix timestamp conversion

764. By Alberto Mardegan

Remove error msg

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)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Alberto Mardegan (mardy) wrote :
review: Needs Resubmitting

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/WebViewImpl.qml'
2--- src/app/WebViewImpl.qml 2014-09-15 14:53:14 +0000
3+++ src/app/WebViewImpl.qml 2014-10-08 14:35:55 +0000
4@@ -29,6 +29,7 @@
5 property var certificateError
6 // Invalid certificates the user has explicitly allowed for this session
7 property var allowedCertificates: []
8+ property url dataPath
9
10 /*experimental.certificateVerificationDialog: CertificateVerificationDialog {}
11 experimental.authenticationDialog: AuthenticationDialog {}
12@@ -39,6 +40,10 @@
13 beforeUnloadDialog: BeforeUnloadDialog {}
14 filePicker: filePickerLoader.item
15
16+ context: WebContext {
17+ dataPath: webview.dataPath
18+ }
19+
20 onDownloadRequested: {
21 if (downloadLoader.status == Loader.Ready) {
22 var headers = { }
23
24=== modified file 'src/app/webcontainer/AccountsPage.qml'
25--- src/app/webcontainer/AccountsPage.qml 2014-08-21 15:15:21 +0000
26+++ src/app/webcontainer/AccountsPage.qml 2014-10-08 14:35:55 +0000
27@@ -25,12 +25,10 @@
28
29 property alias accountProvider: accountsLogin.accountProvider
30 property alias applicationName: accountsLogin.applicationName
31- property var webappCookieStore: null
32- property var onlineAccountStoreComponent: null
33-
34- signal done()
35-
36- visible: false
37+
38+ signal done(bool successful, var credentialsId)
39+
40+ visible: true
41 anchors.fill: parent
42
43 AccountsLoginPage {
44@@ -38,32 +36,10 @@
45
46 anchors.fill: parent
47
48- QtObject {
49- id: internal
50- function onMoved(result) {
51- webappCookieStore.moved.disconnect(internal.onMoved)
52- if (!result) {
53- console.error("Unable to move cookies")
54- }
55- accountsPage.done()
56- }
57- }
58-
59 onDone: {
60 if (!accountsPage.visible)
61 return
62- if (!credentialsId) {
63- accountsPage.done()
64- return
65- }
66-
67- if (webappCookieStore) {
68- var instance = onlineAccountStoreComponent.createObject(accountsLogin, { "accountId": credentialsId })
69- webappCookieStore.moved.connect(internal.onMoved)
70- webappCookieStore.moveFrom(instance)
71- } else {
72- accountsPage.done()
73- }
74+ accountsPage.done(credentialsId != null, credentialsId)
75 }
76 }
77 }
78
79=== modified file 'src/app/webcontainer/WebApp.qml'
80--- src/app/webcontainer/WebApp.qml 2014-10-03 14:41:14 +0000
81+++ src/app/webcontainer/WebApp.qml 2014-10-08 14:35:55 +0000
82@@ -38,6 +38,7 @@
83 property alias webappUrlPatterns: webview.webappUrlPatterns
84 property alias popupRedirectionUrlPrefix: webview.popupRedirectionUrlPrefix
85 property alias webviewOverrideFile: webview.webviewOverrideFile
86+ property alias dataPath: webview.dataPath
87 property string localUserAgentOverride: ""
88
89 property bool backForwardButtonsVisible: false
90
91=== modified file 'src/app/webcontainer/WebViewImplWebkit.qml'
92--- src/app/webcontainer/WebViewImplWebkit.qml 2014-07-29 21:51:07 +0000
93+++ src/app/webcontainer/WebViewImplWebkit.qml 2014-10-08 14:35:55 +0000
94@@ -35,6 +35,7 @@
95 property var webappUrlPatterns: null
96 property string localUserAgentOverride: ""
97 property string popupRedirectionUrlPrefix: ""
98+ property url dataPath // unused
99
100 function getUAString() {
101 return webview.localUserAgentOverride.length === 0 ? undefined : webview.localUserAgentOverride
102
103=== modified file 'src/app/webcontainer/WebappContainerWebview.qml'
104--- src/app/webcontainer/WebappContainerWebview.qml 2014-09-02 06:29:12 +0000
105+++ src/app/webcontainer/WebappContainerWebview.qml 2014-10-08 14:35:55 +0000
106@@ -30,6 +30,7 @@
107 property bool withOxide: false
108 property bool developerExtrasEnabled: false
109 property string webappName: ""
110+ property url dataPath
111 property var currentWebview: webappContainerWebViewLoader.item
112 property var webappUrlPatterns
113 property string localUserAgentOverride: ""
114@@ -39,9 +40,10 @@
115 Loader {
116 id: webappContainerWebViewLoader
117 anchors.fill: parent
118- asynchronous: true
119 }
120
121+ onUrlChanged: if (webappContainerWebViewLoader.item) webappContainerWebViewLoader.item.url = url
122+
123 Component.onCompleted: {
124 var webappEngineSource =
125 withOxide ?
126@@ -60,6 +62,7 @@
127 { localUserAgentOverride: containerWebview.localUserAgentOverride
128 , url: containerWebview.url
129 , webappName: containerWebview.webappName
130+ , dataPath: dataPath
131 , webappUrlPatterns: containerWebview.webappUrlPatterns
132 , developerExtrasEnabled: containerWebview.developerExtrasEnabled
133 , popupRedirectionUrlPrefix: containerWebview.popupRedirectionUrlPrefix})
134
135=== modified file 'src/app/webcontainer/chrome-cookie-store.cpp'
136--- src/app/webcontainer/chrome-cookie-store.cpp 2014-09-18 13:37:07 +0000
137+++ src/app/webcontainer/chrome-cookie-store.cpp 2014-10-08 14:35:55 +0000
138@@ -33,19 +33,6 @@
139 this, SLOT(oxideCookiesUpdated(const QList<QNetworkCookie>&)));
140 }
141
142-void ChromeCookieStore::setHomepage(const QUrl& homepage) {
143- if (homepage == m_homepage)
144- return;
145-
146- m_homepage = homepage;
147-
148- emit homepageChanged();
149-}
150-
151-QUrl ChromeCookieStore::homepage() const {
152- return m_homepage;
153-}
154-
155 void ChromeCookieStore::setOxideStoreBackend(QObject* backend)
156 {
157 m_cookieHelper->setOxideStoreBackend(backend);
158
159=== modified file 'src/app/webcontainer/chrome-cookie-store.h'
160--- src/app/webcontainer/chrome-cookie-store.h 2014-09-18 13:37:07 +0000
161+++ src/app/webcontainer/chrome-cookie-store.h 2014-10-08 14:35:55 +0000
162@@ -30,7 +30,6 @@
163 {
164 Q_OBJECT
165
166- Q_PROPERTY(QUrl homepage READ homepage WRITE setHomepage NOTIFY homepageChanged)
167 Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
168 Q_PROPERTY(QObject* oxideStoreBackend READ oxideStoreBackend WRITE setOxideStoreBackend NOTIFY oxideStoreBackendChanged)
169
170@@ -41,10 +40,6 @@
171 void setDbPath(const QString& path);
172 QString dbPath() const;
173
174- // dbpaths
175- void setHomepage(const QUrl& path);
176- QUrl homepage() const;
177-
178 // oxideStoreBackend
179 void setOxideStoreBackend(QObject* backend);
180 QObject* oxideStoreBackend() const;
181@@ -55,7 +50,6 @@
182 Q_SIGNALS:
183 void dbPathChanged();
184 void oxideStoreBackendChanged();
185- void homepageChanged();
186
187 private Q_SLOTS:
188 void oxideCookiesReceived(int requestId, const QVariant& cookies);
189@@ -67,7 +61,6 @@
190
191 private:
192 OxideCookieHelper* m_cookieHelper;
193- QUrl m_homepage;
194 QString m_dbPath;
195 };
196
197
198=== modified file 'src/app/webcontainer/cookie-store.cpp'
199--- src/app/webcontainer/cookie-store.cpp 2014-08-26 09:49:32 +0000
200+++ src/app/webcontainer/cookie-store.cpp 2014-10-08 14:35:55 +0000
201@@ -91,8 +91,20 @@
202 if (Q_UNLIKELY(!request))
203 return;
204
205+ QDateTime lastRemoteCookieUpdate =
206+ request->_cookieStore->lastUpdateTimeStamp();
207+ QDateTime lastLocalCookieUpdate = lastUpdateTimeStamp();
208+
209 delete request;
210
211+ if (lastRemoteCookieUpdate.isValid() &&
212+ lastLocalCookieUpdate.isValid() &&
213+ (lastRemoteCookieUpdate < lastLocalCookieUpdate))
214+ {
215+ Q_EMIT moved(false);
216+ return;
217+ }
218+
219 connect(this, &CookieStore::cookiesSet,
220 this, &CookieStore::moved);
221
222@@ -104,26 +116,6 @@
223 if (Q_UNLIKELY(!store))
224 return;
225
226- QDateTime lastRemoteCookieUpdate = store->lastUpdateTimeStamp();
227- QDateTime lastLocalCookieUpdate = lastUpdateTimeStamp();
228-
229- // Disabled for now since.
230- // There is an obvious race if the WebView is instanciated
231- // (since it creates a cookies db file at creation time).
232- // But when delaying the creation, only using the WebContext to
233- // access the cookieManager, and manually creating a cookies db file
234- // if none is found (since the cookie manager does not create one
235- // when setting its cookies), something fails.
236-#if 0
237- if (lastRemoteCookieUpdate.isValid() &&
238- lastLocalCookieUpdate.isValid() &&
239- (lastRemoteCookieUpdate < lastLocalCookieUpdate))
240- {
241- Q_EMIT moved(false);
242- return;
243- }
244-#endif
245-
246 CookieStoreRequest* storeRequest = new CookieStoreRequest(store);
247 _currentStoreRequests.insert(storeRequest, true);
248
249
250=== modified file 'src/app/webcontainer/online-accounts-cookie-store.cpp'
251--- src/app/webcontainer/online-accounts-cookie-store.cpp 2014-08-05 00:25:20 +0000
252+++ src/app/webcontainer/online-accounts-cookie-store.cpp 2014-10-08 14:35:55 +0000
253@@ -116,15 +116,14 @@
254
255 if (arguments.count() > 1)
256 {
257- QDateTime t;
258- QVariant timeStampVariant(arguments.at(1));
259- if (timeStampVariant.canConvert(QMetaType::LongLong))
260+ qint64 timeStamp = arguments.at(1).toLongLong();
261+ if (timeStamp != 0)
262 {
263 qDebug() << "Got a cookie timestamp of"
264- << arguments.at(1).toLongLong()
265+ << timeStamp
266 << "from Online Accounts DBUS cookiesForIdentity() call.";
267
268- t.fromMSecsSinceEpoch(arguments.at(1).toLongLong() * 1000);
269+ QDateTime t = QDateTime::fromMSecsSinceEpoch(timeStamp * 1000);
270 updateLastUpdateTimestamp(t);
271 }
272 }
273
274=== modified file 'src/app/webcontainer/oxide-cookie-helper.cpp'
275--- src/app/webcontainer/oxide-cookie-helper.cpp 2014-10-03 11:47:36 +0000
276+++ src/app/webcontainer/oxide-cookie-helper.cpp 2014-10-08 14:35:55 +0000
277@@ -53,6 +53,7 @@
278 m_backend(0),
279 q_ptr(q)
280 {
281+ qRegisterMetaType<QList<QNetworkCookie> >();
282 }
283
284 void OxideCookieHelperPrivate::setCookies(const QList<QNetworkCookie>& cookies)
285@@ -75,7 +76,7 @@
286 /* We don't simply use Q_EMIT because we want the signal to be emitted
287 * asynchronously */
288 QMetaObject::invokeMethod(q, "cookiesSet", Qt::QueuedConnection,
289- Q_ARG(const QList<QNetworkCookie>&, cookies));
290+ Q_ARG(QList<QNetworkCookie>, cookies));
291 return;
292 }
293
294@@ -119,8 +120,8 @@
295 QMetaObject::invokeMethod(m_backend, "setNetworkCookies",
296 Qt::DirectConnection,
297 Q_RETURN_ARG(int, requestId),
298- Q_ARG(const QUrl&, url),
299- Q_ARG(const QList<QNetworkCookie>&, it.value()));
300+ Q_ARG(QUrl, url),
301+ Q_ARG(QList<QNetworkCookie>, it.value()));
302 if (Q_UNLIKELY(requestId == -1)) {
303 m_failedCookies.append(cookiesWithDomain(it.value(), url.host()));
304 } else {
305@@ -133,7 +134,7 @@
306 /* We don't simply use Q_EMIT because we want the signal to be emitted
307 * asynchronously */
308 QMetaObject::invokeMethod(q, "cookiesSet", Qt::QueuedConnection,
309- Q_ARG(const QList<QNetworkCookie>&, m_failedCookies));
310+ Q_ARG(QList<QNetworkCookie>, m_failedCookies));
311 }
312 }
313
314
315=== modified file 'src/app/webcontainer/webapp-container.cpp'
316--- src/app/webcontainer/webapp-container.cpp 2014-10-03 14:41:14 +0000
317+++ src/app/webcontainer/webapp-container.cpp 2014-10-08 14:35:55 +0000
318@@ -157,6 +157,8 @@
319 }
320
321 context->setContextProperty("webappContainerHelper", m_webappContainerHelper.data());
322+ context->setContextProperty("cacheLocation",
323+ QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
324
325 if ( ! m_popupRedirectionUrlPrefix.isEmpty()) {
326 m_window->setProperty("popupRedirectionUrlPrefix", m_popupRedirectionUrlPrefix);
327
328=== modified file 'src/app/webcontainer/webapp-container.qml'
329--- src/app/webcontainer/webapp-container.qml 2014-10-03 14:41:14 +0000
330+++ src/app/webcontainer/webapp-container.qml 2014-10-08 14:35:55 +0000
331@@ -53,54 +53,52 @@
332 title: getWindowTitle()
333
334 function getWindowTitle() {
335+ var webappViewTitle = webappViewLoader.item ? webappViewLoader.item.title : ""
336 if (typeof(webappName) === 'string' && webappName.length !== 0) {
337 return webappName
338- } else if (browser.title) {
339+ } else if (webappViewTitle) {
340 // TRANSLATORS: %1 refers to the current page’s title
341- return i18n.tr("%1 - Ubuntu Web Browser").arg(browser.title)
342+ return i18n.tr("%1 - Ubuntu Web Browser").arg(webappViewTitle)
343 } else {
344 return i18n.tr("Ubuntu Web Browser")
345 }
346 }
347
348- WebApp {
349- id: browser
350-
351- // Initially set as non visible to leave a chance
352- // for the OA dialog to appear
353- visible: false
354-
355- url: accountProvider.length === 0 ? root.url : ""
356-
357- chromeVisible: root.chromeVisible
358- backForwardButtonsVisible: root.backForwardButtonsVisible
359- developerExtrasEnabled: root.developerExtrasEnabled
360- oxide: root.oxide
361- webappModelSearchPath: root.webappModelSearchPath
362- webappUrlPatterns: root.webappUrlPatterns
363-
364- localUserAgentOverride: root.localUserAgentOverride
365-
366- popupRedirectionUrlPrefix: root.popupRedirectionUrlPrefix
367- webviewOverrideFile: root.webviewOverrideFile
368-
369- anchors.fill: parent
370-
371- webbrowserWindow: webbrowserWindowProxy
372-
373- onWebappNameChanged: {
374- if (root.webappName !== browser.webappName) {
375- root.webappName = browser.webappName;
376- root.title = getWindowTitle();
377+ Component {
378+ id: webappViewComponent
379+
380+ WebApp {
381+ id: browser
382+
383+ url: accountProvider.length !== 0 ? "" : root.url
384+
385+ dataPath: webappDataLocation
386+ webappName: root.webappName
387+ chromeVisible: root.chromeVisible
388+ backForwardButtonsVisible: root.backForwardButtonsVisible
389+ developerExtrasEnabled: root.developerExtrasEnabled
390+ oxide: root.oxide
391+ webappModelSearchPath: root.webappModelSearchPath
392+ webappUrlPatterns: root.webappUrlPatterns
393+
394+ localUserAgentOverride: root.localUserAgentOverride
395+
396+ popupRedirectionUrlPrefix: root.popupRedirectionUrlPrefix
397+ webviewOverrideFile: root.webviewOverrideFile
398+
399+ anchors.fill: parent
400+
401+ webbrowserWindow: webbrowserWindowProxy
402+
403+ onWebappNameChanged: {
404+ if (root.webappName !== browser.webappName) {
405+ root.webappName = browser.webappName;
406+ root.title = getWindowTitle();
407+ }
408 }
409- }
410-
411- onCurrentWebviewChanged: {
412- if (currentWebview)
413- root.updateCurrentView()
414- }
415-
416- Component.onCompleted: i18n.domain = "webbrowser-app"
417+
418+ Component.onCompleted: i18n.domain = "webbrowser-app"
419+ }
420 }
421
422 UnityWebApps.UnityWebappsAppModel {
423@@ -117,7 +115,9 @@
424
425 // XXX: work around https://bugs.launchpad.net/unity8/+bug/1328839
426 // by toggling fullscreen on the window only on desktop.
427- visibility: browser.currentWebview && browser.currentWebview.fullscreen &&
428+ visibility: webappViewLoader.item &&
429+ webappViewLoader.item.currentWebview &&
430+ webappViewLoader.item.currentWebview.fullscreen &&
431 (formFactor === "desktop") ? Window.FullScreen : Window.AutomaticVisibility
432
433 Loader {
434@@ -134,17 +134,62 @@
435 }
436 }
437
438+ Loader {
439+ id: webappViewLoader
440+ anchors.fill: parent
441+
442+ property var credentialsId: null
443+ property var webappDataLocation: credentialsId != null ? dataLocation + "/id-" + credentialsId : dataLocation
444+ }
445+
446+ function onCookiesMoved(result) {
447+ if (__webappCookieStore) {
448+ __webappCookieStore.moved.disconnect(onCookiesMoved)
449+ }
450+ if (!result) {
451+ console.log("Cookies were not moved")
452+ }
453+ webappViewLoader.item.url = root.url
454+ }
455+
456+ function moveCookies(credentialsId) {
457+ if (!__webappCookieStore) {
458+ var context = webappViewLoader.item.currentWebview.context
459+ __webappCookieStore = oxideCookieStoreComponent.createObject(this, {
460+ "oxideStoreBackend": context.cookieManager,
461+ "dbPath": context.dataPath + "/cookies.sqlite"
462+ })
463+ }
464+
465+ var storeComponent = localCookieStoreDbPath.length !== 0 ?
466+ localCookieStoreComponent : onlineAccountStoreComponent
467+
468+ var instance = storeComponent.createObject(root, { "accountId": credentialsId })
469+ __webappCookieStore.moved.connect(onCookiesMoved)
470+ __webappCookieStore.moveFrom(instance)
471+ }
472+
473 Connections {
474 target: accountsPageComponentLoader.item
475- onDone: loadWebAppView()
476+ onDone: {
477+ if (successful && credentialsId) {
478+ webappViewLoader.loaded.connect(function () {
479+ if (webappViewLoader.status == Loader.Ready) {
480+ moveCookies(webappViewLoader.credentialsId)
481+ }
482+ });
483+ webappViewLoader.credentialsId = credentialsId
484+ webappViewLoader.sourceComponent = webappViewComponent
485+ }
486+ else {
487+ loadWebAppView()
488+ }
489+ }
490 }
491
492 Component {
493 id: oxideCookieStoreComponent
494 ChromeCookieStore {
495- dbPath: dataLocation + "/cookies.sqlite"
496- homepage: root.url
497- oxideStoreBackend: browser.currentWebview ? browser.currentWebview.context.cookieManager : null
498 }
499 }
500
501@@ -155,6 +200,10 @@
502 }
503 }
504
505+ Component.onCompleted: {
506+ updateCurrentView()
507+ }
508+
509 Component {
510 id: onlineAccountStoreComponent
511 OnlineAccountsCookieStore { }
512@@ -163,7 +212,7 @@
513 function updateCurrentView() {
514 // check if we are to display the login view
515 // or directly switch to the webapp view
516- if (accountProvider.length !== 0 && !__webappCookieStore && oxide) {
517+ if (accountProvider.length !== 0 && oxide) {
518 loadLoginView();
519 } else {
520 loadWebAppView();
521@@ -171,27 +220,22 @@
522 }
523
524 function loadLoginView() {
525- if (!__webappCookieStore) {
526- __webappCookieStore = oxideCookieStoreComponent.createObject(this)
527- }
528 accountsPageComponentLoader.setSource("AccountsPage.qml", {
529 "accountProvider": accountProvider,
530 "applicationName": unversionedAppId,
531- "webappCookieStore": __webappCookieStore,
532- "onlineAccountStoreComponent": localCookieStoreDbPath.length !== 0 ?
533- localCookieStoreComponent : onlineAccountStoreComponent
534 })
535 }
536
537 function loadWebAppView() {
538 if (accountsPageComponentLoader.item)
539 accountsPageComponentLoader.item.visible = false
540- browser.visible = true;
541- if (browser.currentWebview) {
542- browser.currentWebview.visible = true;
543- browser.currentWebview.url = root.url
544- browser.webappName = root.webappName
545- }
546+
547+ webappViewLoader.loaded.connect(function () {
548+ if (webappViewLoader.status === Loader.Ready) {
549+ webappViewLoader.item.currentWebview.url = root.url
550+ }
551+ });
552+ webappViewLoader.sourceComponent = webappViewComponent
553 }
554
555 // Handle runtime requests to open urls as defined

Subscribers

People subscribed via source and target branches

to status/vote changes: