mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1089011: make sure to only import contacts that are part of the default contacts group. r=MattN
This commit is contained in:
parent
7a2555531e
commit
ba7411aaf5
@ -48,7 +48,7 @@ const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, w
|
||||
if (!nodeList[0].firstChild) {
|
||||
continue;
|
||||
}
|
||||
let value = nodeList[0].firstChild.nodeValue;
|
||||
let value = nodeList[0].textContent;
|
||||
target[field] = wrapInArray ? [value] : value;
|
||||
}
|
||||
}
|
||||
@ -168,8 +168,8 @@ this.GoogleImporter.prototype = {
|
||||
Task.spawn(function* () {
|
||||
let code = yield this._promiseAuthCode(windowRef);
|
||||
let tokenSet = yield this._promiseTokenSet(code);
|
||||
let contactEntries = yield this._promiseContactEntries(tokenSet);
|
||||
let {total, success, ids} = yield this._processContacts(contactEntries, db);
|
||||
let contactEntries = yield this._getContactEntries(tokenSet);
|
||||
let {total, success, ids} = yield this._processContacts(contactEntries, db, tokenSet);
|
||||
yield this._purgeContacts(ids, db);
|
||||
|
||||
return {
|
||||
@ -286,22 +286,12 @@ this.GoogleImporter.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all the contacts in a users' address book.
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
|
||||
*
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
|
||||
* @returns An `Error` object upon failure or an Array of contact XML nodes.
|
||||
*/
|
||||
_promiseContactEntries: function(tokenSet) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
_promiseRequestXML: function(URL, tokenSet) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||
.createInstance(Ci.nsIXMLHttpRequest);
|
||||
|
||||
request.open("GET", getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
|
||||
"loop.oauth.google.getContactsURL",
|
||||
false) + "?max-results=" + kContactsMaxResults);
|
||||
request.open("GET", URL);
|
||||
|
||||
request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
|
||||
request.setRequestHeader("GData-Version", "3.0");
|
||||
@ -310,19 +300,17 @@ this.GoogleImporter.prototype = {
|
||||
request.onload = function() {
|
||||
if (request.status < 400) {
|
||||
let doc = request.responseXML;
|
||||
// First get the profile id.
|
||||
// First get the profile id, which is present in each XML request.
|
||||
let currNode = doc.documentElement.firstChild;
|
||||
while (currNode) {
|
||||
if (currNode.nodeType == 1 && currNode.localName == "id") {
|
||||
gProfileId = currNode.firstChild.nodeValue;
|
||||
gProfileId = currNode.textContent;
|
||||
break;
|
||||
}
|
||||
currNode = currNode.nextSibling;
|
||||
}
|
||||
|
||||
// Then kick of the importing of contact entries.
|
||||
let entries = Array.prototype.slice.call(doc.querySelectorAll("entry"));
|
||||
resolve(entries);
|
||||
resolve(doc);
|
||||
} else {
|
||||
reject(new Error(request.status + " " + request.statusText));
|
||||
}
|
||||
@ -336,6 +324,46 @@ this.GoogleImporter.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all the contacts in a users' address book.
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
|
||||
*
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
|
||||
* @returns An `Error` object upon failure or an Array of contact XML nodes.
|
||||
*/
|
||||
_getContactEntries: Task.async(function* (tokenSet) {
|
||||
let URL = getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
|
||||
"loop.oauth.google.getContactsURL",
|
||||
false) + "?max-results=" + kContactsMaxResults;
|
||||
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
|
||||
// Then kick of the importing of contact entries.
|
||||
return Array.prototype.slice.call(xmlDoc.querySelectorAll("entry"));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Fetches the default group from a users' address book, called 'Contacts'.
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contact_groups
|
||||
*
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
|
||||
* @returns An `Error` object upon failure or the String group ID.
|
||||
*/
|
||||
_getContactsGroupId: Task.async(function* (tokenSet) {
|
||||
let URL = getUrlParam("https://www.google.com/m8/feeds/groups/default/full",
|
||||
"loop.oauth.google.getGroupsURL",
|
||||
false) + "?max-results=" + kContactsMaxResults;
|
||||
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
|
||||
let contactsEntry = xmlDoc.querySelector("systemGroup[id=\"Contacts\"]");
|
||||
if (!contactsEntry) {
|
||||
throw new Error("Contacts group not present");
|
||||
}
|
||||
// Select the actual <entry> node, which is the parent of the <systemGroup>
|
||||
// node we just selected.
|
||||
contactsEntry = contactsEntry.parentNode;
|
||||
return contactsEntry.getElementsByTagName("id")[0].textContent;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Process the contact XML nodes that Google provides, convert them to the MozContact
|
||||
* format, check if the contact already exists in the database and when it doesn't,
|
||||
@ -343,21 +371,28 @@ this.GoogleImporter.prototype = {
|
||||
* During this process statistics are collected about the amount of successful
|
||||
* imports. The consumer of this class may use these statistics to inform the
|
||||
* user.
|
||||
* Note: only contacts that are part of the 'Contacts' system group will be
|
||||
* imported.
|
||||
*
|
||||
* @param {Array} contactEntries List of XML DOMNodes contact entries.
|
||||
* @param {LoopContacts} db Instance of the LoopContacts database
|
||||
* object, which will store the newly found
|
||||
* contacts.
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate a
|
||||
* request
|
||||
* @returns An `Error` object upon failure or an Object with statistics in the
|
||||
* following format: `{ total: 25, success: 13, ids: {} }`.
|
||||
*/
|
||||
_processContacts: Task.async(function* (contactEntries, db) {
|
||||
_processContacts: Task.async(function* (contactEntries, db, tokenSet) {
|
||||
let stats = {
|
||||
total: contactEntries.length,
|
||||
success: 0,
|
||||
ids: {}
|
||||
};
|
||||
|
||||
// Contacts that are _not_ part of the 'Contacts' group will be ignored.
|
||||
let contactsGroupId = yield this._getContactsGroupId(tokenSet);
|
||||
|
||||
for (let entry of contactEntries) {
|
||||
let contact = this._processContactFields(entry);
|
||||
|
||||
@ -367,6 +402,12 @@ this.GoogleImporter.prototype = {
|
||||
yield db.promise("remove", existing._guid);
|
||||
}
|
||||
|
||||
// After contact removal, check if the entry is part of the correct group.
|
||||
if (!entry.querySelector("groupMembershipInfo[deleted=\"false\"][href=\"" +
|
||||
contactsGroupId + "\"]")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the contact contains neither email nor phone number, then it is not
|
||||
// useful in the Loop address book: do not add.
|
||||
if (!("email" in contact) && !("tel" in contact)) {
|
||||
@ -450,7 +491,7 @@ this.GoogleImporter.prototype = {
|
||||
for (let [,phoneNode] of Iterator(phoneNodes)) {
|
||||
let phoneNumber = phoneNode.hasAttribute("uri") ?
|
||||
phoneNode.getAttribute("uri").replace("tel:", "") :
|
||||
phoneNode.firstChild.nodeValue;
|
||||
phoneNode.textContent;
|
||||
contact.tel.push({
|
||||
pref: (phoneNode.getAttribute("primary") == "true"),
|
||||
type: [getFieldType(phoneNode)],
|
||||
@ -466,8 +507,8 @@ this.GoogleImporter.prototype = {
|
||||
for (let [,orgNode] of Iterator(orgNodes)) {
|
||||
let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
|
||||
let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
|
||||
contact.org.push(orgElement ? orgElement.firstChild.nodeValue : "")
|
||||
contact.jobTitle.push(titleElement ? titleElement.firstChild.nodeValue : "");
|
||||
contact.org.push(orgElement ? orgElement.textContent : "")
|
||||
contact.jobTitle.push(titleElement ? titleElement.textContent : "");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
support-files =
|
||||
fixtures/google_auth.txt
|
||||
fixtures/google_contacts.txt
|
||||
fixtures/google_groups.txt
|
||||
fixtures/google_token.txt
|
||||
google_service.sjs
|
||||
head.js
|
||||
|
@ -17,23 +17,22 @@ function promiseImport() {
|
||||
});
|
||||
}
|
||||
|
||||
const kContactsCount = 7;
|
||||
const kIncomingTotalContactsCount = 8;
|
||||
const kExpectedImportCount = 7;
|
||||
|
||||
add_task(function* test_GoogleImport() {
|
||||
let stats;
|
||||
// An error may throw and the test will fail when that happens.
|
||||
stats = yield promiseImport();
|
||||
|
||||
let contactsCount = mockDb.size;
|
||||
|
||||
// Assert the world.
|
||||
Assert.equal(stats.total, contactsCount, "Five contacts should get processed");
|
||||
Assert.equal(stats.success, contactsCount, "Five contacts should be imported");
|
||||
Assert.equal(stats.total, kIncomingTotalContactsCount, kIncomingTotalContactsCount + " contacts should get processed");
|
||||
Assert.equal(stats.success, kExpectedImportCount, kExpectedImportCount + " contacts should be imported");
|
||||
|
||||
yield promiseImport();
|
||||
Assert.equal(Object.keys(mockDb._store).length, contactsCount, "Database should be the same size after reimport");
|
||||
Assert.equal(mockDb.size, kExpectedImportCount, "Database should be the same size after reimport");
|
||||
|
||||
let currentContact = contactsCount;
|
||||
let currentContact = kExpectedImportCount;
|
||||
|
||||
let c = mockDb._store[mockDb._next_guid - currentContact];
|
||||
Assert.equal(c.name[0], "John Smith", "Full name should match");
|
||||
@ -96,4 +95,7 @@ add_task(function* test_GoogleImport() {
|
||||
Assert.equal(c.tel[0].pref, false, "Pref should match");
|
||||
Assert.equal(c.category[0], "google", "Category should match");
|
||||
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6", "UID should match and be scoped to provider");
|
||||
|
||||
c = yield mockDb.promise("getByServiceId", "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9");
|
||||
Assert.equal(c, null, "Contacts that are not part of the default group should not be imported");
|
||||
});
|
||||
|
@ -34,6 +34,7 @@
|
||||
</gd:name>
|
||||
<gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
|
||||
<gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
|
||||
@ -51,6 +52,7 @@
|
||||
</gd:name>
|
||||
<gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
|
||||
<gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
|
||||
@ -68,6 +70,7 @@
|
||||
</gd:name>
|
||||
<gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
|
||||
<gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""Q3w7ezVSLit7I2A9WB5WGUkNRgE."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
|
||||
@ -79,6 +82,7 @@
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
|
||||
<gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""Q3w7ezVSLit7I2A9WB5WGUkNRgE."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
|
||||
@ -90,6 +94,7 @@
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
|
||||
<gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""RXkzfjVSLit7I2A9XRdRGUgITgA."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8</id>
|
||||
@ -101,6 +106,7 @@
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="self" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="edit" type="application/atom+xml"/>
|
||||
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile" uri="tel:+31-6-12345678">0612345678</gd:phoneNumber>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""SX8-ejVSLit7I2A9XRdQFUkDRgY."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6</id>
|
||||
@ -114,4 +120,21 @@
|
||||
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile">215234523452345</gd:phoneNumber>
|
||||
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
|
||||
</entry>
|
||||
<entry gd:etag=""Rn8zejVSLit7I2A9WhVRFUQOQQc."">
|
||||
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9</id>
|
||||
<updated>2012-03-24T13:10:37.182Z</updated>
|
||||
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-03-24T13:10:37.182Z</app:edited>
|
||||
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
|
||||
<title>Little Smith</title>
|
||||
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/9" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="self" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="edit" type="application/atom+xml"/>
|
||||
<gd:name>
|
||||
<gd:fullName>Little Smith</gd:fullName>
|
||||
<gd:givenName>Little</gd:givenName>
|
||||
<gd:familyName>Smith</gd:familyName>
|
||||
</gd:name>
|
||||
<gd:email address="littlebabysmith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
|
||||
<gContact:website href="http://www.google.com/profiles/111456826635924971693" rel="profile"/>
|
||||
</entry>
|
||||
</feed>
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<feed gd:etag="W/"CEIAQngzfyt7I2A9XRdXFEQ."" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
|
||||
<id>tester@mochi.com</id>
|
||||
<updated>2014-10-28T10:35:43.687Z</updated>
|
||||
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
|
||||
<title>Mochi Tester's Contact Groups</title>
|
||||
<link href="http://www.google.com/" rel="alternate" type="text/html"/>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full?max-results=10000000" rel="self" type="application/atom+xml"/>
|
||||
<author>
|
||||
<name>Mochi Tester</name>
|
||||
<email>tester@mochi.com</email>
|
||||
</author>
|
||||
<generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
|
||||
<openSearch:totalResults>4</openSearch:totalResults>
|
||||
<openSearch:startIndex>1</openSearch:startIndex>
|
||||
<openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
|
||||
<entry gd:etag=""YDwreyM."">
|
||||
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6</id>
|
||||
<updated>1970-01-01T00:00:00.000Z</updated>
|
||||
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
|
||||
<title>System Group: My Contacts</title>
|
||||
<content>System Group: My Contacts</content>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/6" rel="self" type="application/atom+xml"/>
|
||||
<gContact:systemGroup id="Contacts"/>
|
||||
</entry>
|
||||
<entry gd:etag=""YDwreyM."">
|
||||
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/d</id>
|
||||
<updated>1970-01-01T00:00:00.000Z</updated>
|
||||
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
|
||||
<title>System Group: Friends</title>
|
||||
<content>System Group: Friends</content>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/d" rel="self" type="application/atom+xml"/>
|
||||
<gContact:systemGroup id="Friends"/>
|
||||
</entry>
|
||||
<entry gd:etag=""YDwreyM."">
|
||||
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/e</id>
|
||||
<updated>1970-01-01T00:00:00.000Z</updated>
|
||||
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
|
||||
<title>System Group: Family</title>
|
||||
<content>System Group: Family</content>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/e" rel="self" type="application/atom+xml"/>
|
||||
<gContact:systemGroup id="Family"/>
|
||||
</entry>
|
||||
<entry gd:etag=""YDwreyM."">
|
||||
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/f</id>
|
||||
<updated>1970-01-01T00:00:00.000Z</updated>
|
||||
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
|
||||
<title>System Group: Coworkers</title>
|
||||
<content>System Group: Coworkers</content>
|
||||
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/f" rel="self" type="application/atom+xml"/>
|
||||
<gContact:systemGroup id="Coworkers"/>
|
||||
</entry>
|
||||
</feed>
|
@ -143,5 +143,15 @@ const methodHandlers = {
|
||||
}
|
||||
|
||||
respondWithFile(res, "google_contacts.txt", "text/xml");
|
||||
},
|
||||
|
||||
groups: function(req, res, params) {
|
||||
try {
|
||||
checkAuth(req);
|
||||
} catch (ex) {
|
||||
sendError(res, ex, ex.code);
|
||||
}
|
||||
|
||||
respondWithFile(res, "google_groups.txt", "text/xml");
|
||||
}
|
||||
};
|
||||
|
@ -252,6 +252,7 @@ user_pref("loop.enabled", true);
|
||||
user_pref("loop.throttled", false);
|
||||
user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
|
||||
user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
|
||||
user_pref("loop.oauth.google.getGroupsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=groups");
|
||||
user_pref("loop.CSP","default-src 'self' about: file: chrome: data: wss://* http://* https://*");
|
||||
|
||||
// Ensure UITour won't hit the network
|
||||
|
Loading…
Reference in New Issue
Block a user