Move cookies APIs to webContents.session.cookies namespace.
This commit is contained in:
parent
245dc01e33
commit
99bfc9b7f5
12 changed files with 211 additions and 112 deletions
|
@ -353,16 +353,3 @@ mate::Handle<Cookies> Cookies::Create(v8::Isolate* isolate) {
|
||||||
} // namespace api
|
} // namespace api
|
||||||
|
|
||||||
} // namespace atom
|
} // namespace atom
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void Initialize(v8::Local<v8::Object> exports, v8::Local<v8::Value> unused,
|
|
||||||
v8::Local<v8::Context> context, void* priv) {
|
|
||||||
v8::Isolate* isolate = context->GetIsolate();
|
|
||||||
mate::Dictionary dict(isolate, exports);
|
|
||||||
dict.Set("cookies", atom::api::Cookies::Create(isolate));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_cookies, Initialize);
|
|
||||||
|
|
45
atom/browser/api/atom_api_session.cc
Normal file
45
atom/browser/api/atom_api_session.cc
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) 2015 GitHub, Inc.
|
||||||
|
// Use of this source code is governed by the MIT license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "atom/browser/api/atom_api_session.h"
|
||||||
|
|
||||||
|
#include "atom/browser/api/atom_api_cookies.h"
|
||||||
|
#include "native_mate/callback.h"
|
||||||
|
#include "native_mate/dictionary.h"
|
||||||
|
#include "native_mate/object_template_builder.h"
|
||||||
|
|
||||||
|
#include "atom/common/node_includes.h"
|
||||||
|
|
||||||
|
namespace atom {
|
||||||
|
|
||||||
|
namespace api {
|
||||||
|
|
||||||
|
Session::Session() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::~Session() {
|
||||||
|
}
|
||||||
|
|
||||||
|
v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
|
||||||
|
if (cookies_.IsEmpty()) {
|
||||||
|
auto handle = atom::api::Cookies::Create(isolate);
|
||||||
|
cookies_.Reset(isolate, handle.ToV8());
|
||||||
|
}
|
||||||
|
return v8::Local<v8::Value>::New(isolate, cookies_);
|
||||||
|
}
|
||||||
|
|
||||||
|
mate::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
||||||
|
v8::Isolate* isolate) {
|
||||||
|
return mate::ObjectTemplateBuilder(isolate)
|
||||||
|
.SetProperty("cookies", &Session::Cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
mate::Handle<Session> Session::Create(v8::Isolate* isolate) {
|
||||||
|
return CreateHandle(isolate, new Session);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace api
|
||||||
|
|
||||||
|
} // namespace atom
|
39
atom/browser/api/atom_api_session.h
Normal file
39
atom/browser/api/atom_api_session.h
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright (c) 2015 GitHub, Inc.
|
||||||
|
// Use of this source code is governed by the MIT license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
#ifndef ATOM_BROWSER_API_ATOM_API_SESSION_H_
|
||||||
|
#define ATOM_BROWSER_API_ATOM_API_SESSION_H_
|
||||||
|
|
||||||
|
#include "native_mate/handle.h"
|
||||||
|
#include "native_mate/wrappable.h"
|
||||||
|
|
||||||
|
namespace atom {
|
||||||
|
|
||||||
|
namespace api {
|
||||||
|
|
||||||
|
class Session: public mate::Wrappable {
|
||||||
|
public:
|
||||||
|
static mate::Handle<Session> Create(v8::Isolate* isolate);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Session();
|
||||||
|
~Session();
|
||||||
|
|
||||||
|
// mate::Wrappable implementations:
|
||||||
|
mate::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||||
|
v8::Isolate* isolate) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
v8::Local<v8::Value> Cookies(v8::Isolate* isolate);
|
||||||
|
|
||||||
|
v8::Global<v8::Value> cookies_;
|
||||||
|
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(Session);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace api
|
||||||
|
|
||||||
|
} // namespace atom
|
||||||
|
|
||||||
|
#endif // ATOM_BROWSER_API_ATOM_API_SESSION_H_
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
|
#include "atom/browser/api/atom_api_session.h"
|
||||||
#include "atom/browser/atom_browser_client.h"
|
#include "atom/browser/atom_browser_client.h"
|
||||||
#include "atom/browser/atom_browser_context.h"
|
#include "atom/browser/atom_browser_context.h"
|
||||||
#include "atom/browser/atom_browser_main_parts.h"
|
#include "atom/browser/atom_browser_main_parts.h"
|
||||||
|
@ -584,6 +585,14 @@ void WebContents::InspectServiceWorker() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v8::Local<v8::Value> WebContents::Session(v8::Isolate* isolate) {
|
||||||
|
if (session_.IsEmpty()) {
|
||||||
|
auto handle = Session::Create(isolate);
|
||||||
|
session_.Reset(isolate, handle.ToV8());
|
||||||
|
}
|
||||||
|
return v8::Local<v8::Value>::New(isolate, session_);
|
||||||
|
}
|
||||||
|
|
||||||
void WebContents::HasServiceWorker(
|
void WebContents::HasServiceWorker(
|
||||||
const base::Callback<void(bool)>& callback) {
|
const base::Callback<void(bool)>& callback) {
|
||||||
auto context = GetServiceWorkerContext(web_contents());
|
auto context = GetServiceWorkerContext(web_contents());
|
||||||
|
@ -804,6 +813,7 @@ mate::ObjectTemplateBuilder WebContents::GetObjectTemplateBuilder(
|
||||||
.SetMethod("inspectServiceWorker", &WebContents::InspectServiceWorker)
|
.SetMethod("inspectServiceWorker", &WebContents::InspectServiceWorker)
|
||||||
.SetMethod("print", &WebContents::Print)
|
.SetMethod("print", &WebContents::Print)
|
||||||
.SetMethod("_printToPDF", &WebContents::PrintToPDF)
|
.SetMethod("_printToPDF", &WebContents::PrintToPDF)
|
||||||
|
.SetProperty("session", &WebContents::Session)
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
return mate::ObjectTemplateBuilder(
|
return mate::ObjectTemplateBuilder(
|
||||||
|
|
|
@ -239,6 +239,10 @@ class WebContents : public mate::EventEmitter,
|
||||||
// Returns the default size of the guestview.
|
// Returns the default size of the guestview.
|
||||||
gfx::Size GetDefaultSize() const;
|
gfx::Size GetDefaultSize() const;
|
||||||
|
|
||||||
|
v8::Local<v8::Value> Session(v8::Isolate* isolate);
|
||||||
|
|
||||||
|
v8::Global<v8::Value> session_;
|
||||||
|
|
||||||
// Stores whether the contents of the guest can be transparent.
|
// Stores whether the contents of the guest can be transparent.
|
||||||
bool guest_opaque_;
|
bool guest_opaque_;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
bindings = process.atomBinding 'cookies'
|
|
||||||
|
|
||||||
module.exports = bindings.cookies
|
|
|
@ -32,7 +32,6 @@ using content::BrowserThread;
|
||||||
REFERENCE_MODULE(atom_browser_app);
|
REFERENCE_MODULE(atom_browser_app);
|
||||||
REFERENCE_MODULE(atom_browser_auto_updater);
|
REFERENCE_MODULE(atom_browser_auto_updater);
|
||||||
REFERENCE_MODULE(atom_browser_content_tracing);
|
REFERENCE_MODULE(atom_browser_content_tracing);
|
||||||
REFERENCE_MODULE(atom_browser_cookies);
|
|
||||||
REFERENCE_MODULE(atom_browser_dialog);
|
REFERENCE_MODULE(atom_browser_dialog);
|
||||||
REFERENCE_MODULE(atom_browser_menu);
|
REFERENCE_MODULE(atom_browser_menu);
|
||||||
REFERENCE_MODULE(atom_browser_power_monitor);
|
REFERENCE_MODULE(atom_browser_power_monitor);
|
||||||
|
|
|
@ -32,7 +32,6 @@ Modules for the main process:
|
||||||
* [auto-updater](api/auto-updater.md)
|
* [auto-updater](api/auto-updater.md)
|
||||||
* [browser-window](api/browser-window.md)
|
* [browser-window](api/browser-window.md)
|
||||||
* [content-tracing](api/content-tracing.md)
|
* [content-tracing](api/content-tracing.md)
|
||||||
* [cookies](api/cookies.md)
|
|
||||||
* [dialog](api/dialog.md)
|
* [dialog](api/dialog.md)
|
||||||
* [global-shortcut](api/global-shortcut.md)
|
* [global-shortcut](api/global-shortcut.md)
|
||||||
* [ipc (main process)](api/ipc-main-process.md)
|
* [ipc (main process)](api/ipc-main-process.md)
|
||||||
|
|
|
@ -1020,3 +1020,77 @@ app.on('ready', function() {
|
||||||
is different from the handlers on the main process.
|
is different from the handlers on the main process.
|
||||||
2. There is no way to send synchronous messages from the main process to a
|
2. There is no way to send synchronous messages from the main process to a
|
||||||
renderer process, because it would be very easy to cause dead locks.
|
renderer process, because it would be very easy to cause dead locks.
|
||||||
|
|
||||||
|
## Class: WebContents.session.cookies
|
||||||
|
|
||||||
|
The `cookies` gives you ability to query and modify cookies, an example is:
|
||||||
|
|
||||||
|
```javascipt
|
||||||
|
var BrowserWindow = require('browser-window');
|
||||||
|
|
||||||
|
var win = new BrowserWindow({ width: 800, height: 600 });
|
||||||
|
|
||||||
|
win.loadUrl('https://github.com');
|
||||||
|
|
||||||
|
win.webContents.on('did-finish-load', function() {
|
||||||
|
// Query all cookies.
|
||||||
|
win.webContents.session.cookies.get({}, function(error, cookies) {
|
||||||
|
if (error) throw error;
|
||||||
|
console.log(cookies);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query all cookies that are associated with a specific url.
|
||||||
|
win.webContents.session.cookies.get({ url : "http://www.github.com" },
|
||||||
|
function(error, cookies) {
|
||||||
|
if (error) throw error;
|
||||||
|
console.log(cookies);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set a cookie with the given cookie data;
|
||||||
|
// may overwrite equivalent cookies if they exist.
|
||||||
|
win.webContents.session.cookies.set(
|
||||||
|
{ url : "http://www.github.com", name : "dummy_name", value : "dummy"},
|
||||||
|
function(error, cookies) {
|
||||||
|
if (error) throw error;
|
||||||
|
console.log(cookies);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebContents.session.cookies.get(details, callback)
|
||||||
|
|
||||||
|
* `details` Object
|
||||||
|
* `url` String - Retrieves cookies which are associated with `url`.
|
||||||
|
Empty imples retrieving cookies of all urls.
|
||||||
|
* `name` String - Filters cookies by name
|
||||||
|
* `domain` String - Retrieves cookies whose domains match or are subdomains of `domains`
|
||||||
|
* `path` String - Retrieves cookies whose path matches `path`
|
||||||
|
* `secure` Boolean - Filters cookies by their Secure property
|
||||||
|
* `session` Boolean - Filters out session or persistent cookies.
|
||||||
|
* `callback` Function - function(error, cookies)
|
||||||
|
* `error` Error
|
||||||
|
* `cookies` Array - array of cookie objects.
|
||||||
|
|
||||||
|
### WebContents.session.cookies.set(details, callback)
|
||||||
|
|
||||||
|
* `details` Object
|
||||||
|
* `url` String - Retrieves cookies which are associated with `url`
|
||||||
|
* `name` String - The name of the cookie. Empty by default if omitted.
|
||||||
|
* `value` String - The value of the cookie. Empty by default if omitted.
|
||||||
|
* `domain` String - The domain of the cookie. Empty by default if omitted.
|
||||||
|
* `path` String - The path of the cookie. Empty by default if omitted.
|
||||||
|
* `secure` Boolean - Whether the cookie should be marked as Secure. Defaults to false.
|
||||||
|
* `session` Boolean - Whether the cookie should be marked as HttpOnly. Defaults to false.
|
||||||
|
* `expirationDate` Double - The expiration date of the cookie as the number of
|
||||||
|
seconds since the UNIX epoch. If omitted, the cookie becomes a session cookie.
|
||||||
|
|
||||||
|
* `callback` Function - function(error)
|
||||||
|
* `error` Error
|
||||||
|
|
||||||
|
### WebContents.session.cookies.remove(details, callback)
|
||||||
|
|
||||||
|
* `details` Object
|
||||||
|
* `url` String - The URL associated with the cookie
|
||||||
|
* `name` String - The name of cookie to remove
|
||||||
|
* `callback` Function - function(error)
|
||||||
|
* `error` Error
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
# cookies
|
|
||||||
|
|
||||||
The `cookies` module gives you ability to query and modify cookies, an example
|
|
||||||
is:
|
|
||||||
|
|
||||||
```javascipt
|
|
||||||
var cookies = require('cookies');
|
|
||||||
|
|
||||||
// Query all cookies.
|
|
||||||
cookies.get({}, function(error, cookies) {
|
|
||||||
if (error) throw error;
|
|
||||||
console.log(cookies);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Query all cookies that are associated with a specific url.
|
|
||||||
cookies.get({ url : "http://www.github.com" }, function(error, cookies) {
|
|
||||||
if (error) throw error;
|
|
||||||
console.log(cookies);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set a cookie with the given cookie data; may overwrite equivalent cookies if they exist.
|
|
||||||
cookies.set({ url : "http://www.github.com", name : "dummy_name", value : "dummy"},
|
|
||||||
function(error, cookies) {
|
|
||||||
if (error) throw error;
|
|
||||||
console.log(cookies);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## cookies.get(details, callback)
|
|
||||||
|
|
||||||
* `details` Object
|
|
||||||
* `url` String - Retrieves cookies which are associated with `url`. Empty imples retrieving cookies of all urls.
|
|
||||||
* `name` String - Filters cookies by name
|
|
||||||
* `domain` String - Retrieves cookies whose domains match or are subdomains of `domains`
|
|
||||||
* `path` String - Retrieves cookies whose path matches `path`
|
|
||||||
* `secure` Boolean - Filters cookies by their Secure property
|
|
||||||
* `session` Boolean - Filters out session or persistent cookies.
|
|
||||||
* `callback` Function - function(error, cookies)
|
|
||||||
* `error` Error
|
|
||||||
* `cookies` Array - array of cookie objects.
|
|
||||||
|
|
||||||
## cookies.set(details, callback)
|
|
||||||
|
|
||||||
* `details` Object
|
|
||||||
* `url` String - Retrieves cookies which are associated with `url`
|
|
||||||
* `name` String - The name of the cookie. Empty by default if omitted.
|
|
||||||
* `value` String - The value of the cookie. Empty by default if omitted.
|
|
||||||
* `domain` String - The domain of the cookie. Empty by default if omitted.
|
|
||||||
* `path` String - The path of the cookie. Empty by default if omitted.
|
|
||||||
* `secure` Boolean - Whether the cookie should be marked as Secure. Defaults to false.
|
|
||||||
* `session` Boolean - Whether the cookie should be marked as HttpOnly. Defaults to false.
|
|
||||||
* `expirationDate` Double - The expiration date of the cookie as the number of seconds since the UNIX epoch. If omitted, the cookie becomes a session cookie.
|
|
||||||
|
|
||||||
* `callback` Function - function(error)
|
|
||||||
* `error` Error
|
|
||||||
|
|
||||||
## cookies.remove(details, callback)
|
|
||||||
|
|
||||||
* `details` Object
|
|
||||||
* `url` String - The URL associated with the cookie
|
|
||||||
* `name` String - The name of cookie to remove
|
|
||||||
* `callback` Function - function(error)
|
|
||||||
* `error` Error
|
|
|
@ -13,7 +13,6 @@
|
||||||
'atom/browser/api/lib/auto-updater.coffee',
|
'atom/browser/api/lib/auto-updater.coffee',
|
||||||
'atom/browser/api/lib/browser-window.coffee',
|
'atom/browser/api/lib/browser-window.coffee',
|
||||||
'atom/browser/api/lib/content-tracing.coffee',
|
'atom/browser/api/lib/content-tracing.coffee',
|
||||||
'atom/browser/api/lib/cookies.coffee',
|
|
||||||
'atom/browser/api/lib/dialog.coffee',
|
'atom/browser/api/lib/dialog.coffee',
|
||||||
'atom/browser/api/lib/global-shortcut.coffee',
|
'atom/browser/api/lib/global-shortcut.coffee',
|
||||||
'atom/browser/api/lib/ipc.coffee',
|
'atom/browser/api/lib/ipc.coffee',
|
||||||
|
@ -87,6 +86,8 @@
|
||||||
'atom/browser/api/atom_api_protocol.h',
|
'atom/browser/api/atom_api_protocol.h',
|
||||||
'atom/browser/api/atom_api_screen.cc',
|
'atom/browser/api/atom_api_screen.cc',
|
||||||
'atom/browser/api/atom_api_screen.h',
|
'atom/browser/api/atom_api_screen.h',
|
||||||
|
'atom/browser/api/atom_api_session.cc',
|
||||||
|
'atom/browser/api/atom_api_session.h',
|
||||||
'atom/browser/api/atom_api_tray.cc',
|
'atom/browser/api/atom_api_tray.cc',
|
||||||
'atom/browser/api/atom_api_tray.h',
|
'atom/browser/api/atom_api_tray.h',
|
||||||
'atom/browser/api/atom_api_web_contents.cc',
|
'atom/browser/api/atom_api_web_contents.cc',
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
assert = require 'assert'
|
assert = require 'assert'
|
||||||
remote = require 'remote'
|
remote = require 'remote'
|
||||||
http = require 'http'
|
http = require 'http'
|
||||||
cookies = remote.require 'cookies'
|
path = require 'path'
|
||||||
BrowserWindow = remote.require 'browser-window'
|
BrowserWindow = remote.require 'browser-window'
|
||||||
|
|
||||||
describe 'cookies module', ->
|
describe 'cookies module', ->
|
||||||
|
fixtures = path.resolve __dirname, 'fixtures'
|
||||||
w = null
|
w = null
|
||||||
url = "http://127.0.0.1:9999"
|
url = "http://127.0.0.1:9999"
|
||||||
|
|
||||||
beforeEach -> w = new BrowserWindow(show: true)
|
beforeEach -> w = new BrowserWindow(show: true)
|
||||||
afterEach -> w.destroy()
|
afterEach -> w.destroy()
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ describe 'cookies module', ->
|
||||||
{port} = server.address()
|
{port} = server.address()
|
||||||
w.loadUrl url
|
w.loadUrl url
|
||||||
w.webContents.on 'did-finish-load', ()->
|
w.webContents.on 'did-finish-load', ()->
|
||||||
cookies.get {url:url}, (error, cookies) ->
|
w.webContents.session.cookies.get {url:url}, (error, cookies) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
assert.equal 1, cookies.length
|
assert.equal 1, cookies.length
|
||||||
assert.equal 'type', cookies[0].name
|
assert.equal 'type', cookies[0].name
|
||||||
|
@ -28,9 +30,11 @@ describe 'cookies module', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'should overwrite the existent cookie', (done) ->
|
it 'should overwrite the existent cookie', (done) ->
|
||||||
cookies.set {url:url, name:'type', value:'dummy2'}, (error) ->
|
w.loadUrl 'file://' + path.join(fixtures, 'page', 'a.html')
|
||||||
|
w.webContents.on 'did-finish-load', ()->
|
||||||
|
w.webContents.session.cookies.set {url:url, name:'type', value:'dummy2'}, (error) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
cookies.get {url:url}, (error, cookies_list) ->
|
w.webContents.session.cookies.get {url:url}, (error, cookies_list) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
assert.equal 1, cookies_list.length
|
assert.equal 1, cookies_list.length
|
||||||
assert.equal 'type', cookies_list[0].name
|
assert.equal 'type', cookies_list[0].name
|
||||||
|
@ -38,9 +42,11 @@ describe 'cookies module', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'should set new cookie', (done) ->
|
it 'should set new cookie', (done) ->
|
||||||
cookies.set {url:url, name:'key', value:'dummy2'}, (error) ->
|
w.loadUrl 'file://' + path.join(fixtures, 'page', 'a.html')
|
||||||
|
w.webContents.on 'did-finish-load', ()->
|
||||||
|
w.webContents.session.cookies.set {url:url, name:'key', value:'dummy2'}, (error) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
cookies.get {url:url}, (error, cookies_list) ->
|
w.webContents.session.cookies.get {url:url}, (error, cookies_list) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
assert.equal 2, cookies_list.length
|
assert.equal 2, cookies_list.length
|
||||||
for cookie in cookies_list
|
for cookie in cookies_list
|
||||||
|
@ -49,15 +55,16 @@ describe 'cookies module', ->
|
||||||
done();
|
done();
|
||||||
|
|
||||||
it 'should remove cookies', (done) ->
|
it 'should remove cookies', (done) ->
|
||||||
cookies.get {url:url}, (error, cookies_list) ->
|
w.loadUrl 'file://' + path.join(fixtures, 'page', 'a.html')
|
||||||
|
w.webContents.on 'did-finish-load', ()->
|
||||||
|
w.webContents.session.cookies.get {url:url}, (error, cookies_list) ->
|
||||||
count = 0
|
count = 0
|
||||||
console.log cookies_list
|
|
||||||
for cookie in cookies_list
|
for cookie in cookies_list
|
||||||
cookies.remove {url:url, name:cookie.name}, (error) ->
|
w.webContents.session.cookies.remove {url:url, name:cookie.name}, (error) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
++count
|
++count
|
||||||
if count == cookies_list.length
|
if count == cookies_list.length
|
||||||
cookies.get {url:url}, (error, cookies_list) ->
|
w.webContents.session.cookies.get {url:url}, (error, cookies_list) ->
|
||||||
throw error if error
|
throw error if error
|
||||||
assert.equal 0, cookies_list.length
|
assert.equal 0, cookies_list.length
|
||||||
done()
|
done()
|
||||||
|
|
Loading…
Reference in a new issue