Bug 1274112 - Part 1: Make update request v4. r=francois

MozReview-Commit-ID: NgV4QYbDll

--HG--
extra : rebase_source : 0c6c000e81e73617c6616dfa39fa868e35a43f9c
This commit is contained in:
Henry Chang 2016-08-04 18:10:06 +08:00
Родитель 8ce157e06f
Коммит 019c6a51fd
13 изменённых файлов: 173 добавлений и 72 удалений

Просмотреть файл

@ -5133,7 +5133,7 @@ pref("browser.safebrowsing.provider.google.reportURL", "https://safebrowsing.goo
// Prefs for v4.
pref("browser.safebrowsing.provider.google4.pver", "4");
pref("browser.safebrowsing.provider.google4.lists", "goog-phish-proto,googpub-phish-proto,goog-malware-proto,goog-unwanted-proto");
pref("browser.safebrowsing.provider.google4.updateURL", "https://safebrowsing.googleapis.com/v4/threatListUpdates:fetch?$req=%REQUEST_BASE64%&$ct=application/x-protobuf&key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.provider.google4.updateURL", "https://safebrowsing.googleapis.com/v4/threatListUpdates:fetch?$ct=application/x-protobuf&key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.provider.google4.gethashURL", "https://safebrowsing.googleapis.com/v4/fullHashes:find?$req=%REQUEST_BASE64%&$ct=application/x-protobuf&key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.provider.google4.reportURL", "https://safebrowsing.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");

Просмотреть файл

@ -115,7 +115,7 @@ add_test(function test_safebrowsing_update() {
}
streamUpdater.downloadUpdates("test-phish-simple,test-malware-simple", "",
URL + safebrowsingUpdatePath, onSuccess, onUpdateError, onDownloadError);
true, URL + safebrowsingUpdatePath, onSuccess, onUpdateError, onDownloadError);
});
add_test(function test_non_safebrowsing_cookie() {

Просмотреть файл

@ -220,6 +220,7 @@ add_test(function test_local_list() {
streamUpdater.downloadUpdates(
"goog-downloadwhite-digest256,goog-badbinurl-shavar",
"goog-downloadwhite-digest256,goog-badbinurl-shavar;\n",
true, // isPostRequest.
"http://localhost:4444/downloads",
updateSuccess, handleError, handleError);
});

Просмотреть файл

@ -197,6 +197,7 @@ function waitForUpdates() {
streamUpdater.downloadUpdates(
"goog-downloadwhite-digest256",
"goog-downloadwhite-digest256;\n",
true,
"http://localhost:4444/downloads",
updateSuccess, handleError, handleError);
return deferred.promise;

Просмотреть файл

@ -297,6 +297,7 @@ function waitForUpdates() {
streamUpdater.downloadUpdates(
"goog-downloadwhite-digest256",
"goog-downloadwhite-digest256;\n",
true,
"http://localhost:4444/downloads",
updateSuccess, handleError, handleError);
return deferred.promise;

Просмотреть файл

@ -352,8 +352,13 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
// tableNames: map of tables that need updating,
// request: list of tables and existing chunk ranges from tableData
// }
var streamerMap = { tableList: null, tableNames: {}, request: "" };
var streamerMap = { tableList: null,
tableNames: {},
requestPayload: "",
isPostRequest: true };
let useProtobuf = false;
let onceThru = false;
for (var tableName in this.tablesData) {
// Skip tables not matching this update url
if (this.tablesData[tableName].updateUrl != updateUrl) {
@ -364,11 +369,13 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
// We use the table name 'goog-*-proto' and an additional provider "google4"
// to describe the v4 settings.
let isCurTableProto = tableName.endsWith('-proto');
if (useProtobuf && !isCurTableProto) {
log('ERROR: Tables for the same updateURL should all be "proto" or none. ' +
'Check "browser.safebrowsing.provider.google4.lists"');
}
if (!onceThru) {
useProtobuf = isCurTableProto;
onceThru = true;
} else if (useProtobuf !== isCurTableProto) {
log('ERROR: Cannot mix "proto" tables with other types ' +
'within the same provider.');
}
if (this.needsUpdate_[this.tablesData[tableName].updateUrl][tableName]) {
streamerMap.tableNames[tableName] = true;
@ -381,8 +388,24 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
}
if (useProtobuf) {
// TODO: Bug 1275507 - XPCOM API to build v4 update request.
streamerMap.request = "";
let tableArray = streamerMap.tableList.split(',');
// The state is a byte stream which server told us from the
// last table update. The state would be used to do the partial
// update and the empty string means the table has
// never been downloaded. See Bug 1287058 for supporting
// partial update.
let stateArray = [];
tableArray.forEach(() => stateArray.push(''));
let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
.getService(Ci.nsIUrlClassifierUtils);
let requestPayload = urlUtils.makeUpdateRequestV4(tableArray,
stateArray,
tableArray.length);
// Use a base64-encoded request.
streamerMap.requestPayload = btoa(requestPayload);
streamerMap.isPostRequest = false;
} else {
// Build the request. For each table already in the database, include the
// chunk data from the database
@ -391,23 +414,26 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
var fields = lines[i].split(";");
var name = fields[0];
if (streamerMap.tableNames[name]) {
streamerMap.request += lines[i] + "\n";
streamerMap.requestPayload += lines[i] + "\n";
delete streamerMap.tableNames[name];
}
}
// For each requested table that didn't have chunk data in the database,
// request it fresh
for (let tableName in streamerMap.tableNames) {
streamerMap.request += tableName + ";\n";
streamerMap.requestPayload += tableName + ";\n";
}
streamerMap.isPostRequest = true;
}
log("update request: " + JSON.stringify(streamerMap, undefined, 2) + "\n");
// Don't send an empty request.
if (streamerMap.request.length > 0) {
if (streamerMap.requestPayload.length > 0) {
this.makeUpdateRequestForEntry_(updateUrl, streamerMap.tableList,
streamerMap.request);
streamerMap.requestPayload,
streamerMap.isPostRequest);
} else {
// We were disabled between kicking off getTables and now.
log("Not sending empty request");
@ -416,8 +442,9 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
PROT_ListManager.prototype.makeUpdateRequestForEntry_ = function(updateUrl,
tableList,
request) {
log("makeUpdateRequestForEntry_: request " + request +
requestPayload,
isPostRequest) {
log("makeUpdateRequestForEntry_: requestPayload " + requestPayload +
" update: " + updateUrl + " tablelist: " + tableList + "\n");
var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"]
.getService(Ci.nsIUrlClassifierStreamUpdater);
@ -426,7 +453,8 @@ PROT_ListManager.prototype.makeUpdateRequestForEntry_ = function(updateUrl,
if (!streamer.downloadUpdates(
tableList,
request,
requestPayload,
isPostRequest,
updateUrl,
BindToObject(this.updateSuccess_, this, tableList, updateUrl),
BindToObject(this.updateError_, this, tableList, updateUrl),

Просмотреть файл

@ -20,7 +20,9 @@ interface nsIUrlClassifierStreamUpdater : nsISupports
* as well as in testing.
* @param aRequestTables Comma-separated list of tables included in this
* update.
* @param aRequestBody The body for the request.
* @param aRequestPayload The payload for the request.
* @param aIsPostRequest Whether the request should be sent by POST method.
* Should be 'true' for v2 usage.
* @param aUpdateUrl The plaintext url from which to request updates.
* @param aSuccessCallback Called after a successful update.
* @param aUpdateErrorCallback Called for problems applying the update
@ -28,7 +30,8 @@ interface nsIUrlClassifierStreamUpdater : nsISupports
* connection refused error.
*/
boolean downloadUpdates(in ACString aRequestTables,
in ACString aRequestBody,
in ACString aRequestPayload,
in boolean aIsPostRequest,
in ACString aUpdateUrl,
in nsIUrlClassifierCallback aSuccessCallback,
in nsIUrlClassifierCallback aUpdateErrorCallback,

Просмотреть файл

@ -103,7 +103,8 @@ nsUrlClassifierStreamUpdater::DownloadDone()
nsresult
nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl,
const nsACString & aRequestBody,
const nsACString & aRequestPayload,
bool aIsPostRequest,
const nsACString & aStreamTable)
{
@ -111,7 +112,7 @@ nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl,
{
nsCString spec;
aUpdateUrl->GetSpec(spec);
LOG(("Fetching update %s from %s", aRequestBody.Data(), spec.get()));
LOG(("Fetching update %s from %s", aRequestPayload.Data(), spec.get()));
}
#endif
@ -134,9 +135,26 @@ nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl,
mBeganStream = false;
// If aRequestBody is empty, construct it for the test.
if (!aRequestBody.IsEmpty()) {
rv = AddRequestBody(aRequestBody);
if (!aIsPostRequest) {
// We use POST method to send our request in v2. In v4, the request
// needs to be embedded to the URL and use GET method to send.
// However, from the Chromium source code, a extended HTTP header has
// to be sent along with the request to make the request succeed.
// The following description is from Chromium source code:
//
// "The following header informs the envelope server (which sits in
// front of Google's stubby server) that the received GET request should be
// interpreted as a POST."
//
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-HTTP-Method-Override"),
NS_LITERAL_CSTRING("POST"),
false);
NS_ENSURE_SUCCESS(rv, rv);
} else if (!aRequestPayload.IsEmpty()) {
rv = AddRequestBody(aRequestPayload);
NS_ENSURE_SUCCESS(rv, rv);
}
@ -176,13 +194,19 @@ nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl,
nsresult
nsUrlClassifierStreamUpdater::FetchUpdate(const nsACString & aUpdateUrl,
const nsACString & aRequestBody,
const nsACString & aRequestPayload,
bool aIsPostRequest,
const nsACString & aStreamTable)
{
LOG(("(pre) Fetching update from %s\n", PromiseFlatCString(aUpdateUrl).get()));
nsCString updateUrl(aUpdateUrl);
if (!aIsPostRequest) {
updateUrl.AppendPrintf("&$req=%s", nsCString(aRequestPayload).get());
}
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aUpdateUrl);
nsresult rv = NS_NewURI(getter_AddRefs(uri), updateUrl);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString urlSpec;
@ -190,13 +214,14 @@ nsUrlClassifierStreamUpdater::FetchUpdate(const nsACString & aUpdateUrl,
LOG(("(post) Fetching update from %s\n", urlSpec.get()));
return FetchUpdate(uri, aRequestBody, aStreamTable);
return FetchUpdate(uri, aRequestPayload, aIsPostRequest, aStreamTable);
}
NS_IMETHODIMP
nsUrlClassifierStreamUpdater::DownloadUpdates(
const nsACString &aRequestTables,
const nsACString &aRequestBody,
const nsACString &aRequestPayload,
bool aIsPostRequest,
const nsACString &aUpdateUrl,
nsIUrlClassifierCallback *aSuccessCallback,
nsIUrlClassifierCallback *aUpdateErrorCallback,
@ -208,12 +233,13 @@ nsUrlClassifierStreamUpdater::DownloadUpdates(
NS_ENSURE_ARG(aDownloadErrorCallback);
if (mIsUpdating) {
LOG(("Already updating, queueing update %s from %s", aRequestBody.Data(),
LOG(("Already updating, queueing update %s from %s", aRequestPayload.Data(),
aUpdateUrl.Data()));
*_retval = false;
PendingRequest *request = mPendingRequests.AppendElement();
request->mTables = aRequestTables;
request->mRequest = aRequestBody;
request->mRequestPayload = aRequestPayload;
request->mIsPostRequest = aIsPostRequest;
request->mUrl = aUpdateUrl;
request->mSuccessCallback = aSuccessCallback;
request->mUpdateErrorCallback = aUpdateErrorCallback;
@ -248,11 +274,12 @@ nsUrlClassifierStreamUpdater::DownloadUpdates(
rv = mDBService->BeginUpdate(this, aRequestTables);
if (rv == NS_ERROR_NOT_AVAILABLE) {
LOG(("Service busy, already updating, queuing update %s from %s",
aRequestBody.Data(), aUpdateUrl.Data()));
aRequestPayload.Data(), aUpdateUrl.Data()));
*_retval = false;
PendingRequest *request = mPendingRequests.AppendElement();
request->mTables = aRequestTables;
request->mRequest = aRequestBody;
request->mRequestPayload = aRequestPayload;
request->mIsPostRequest = aIsPostRequest;
request->mUrl = aUpdateUrl;
request->mSuccessCallback = aSuccessCallback;
request->mUpdateErrorCallback = aUpdateErrorCallback;
@ -272,9 +299,8 @@ nsUrlClassifierStreamUpdater::DownloadUpdates(
*_retval = true;
LOG(("FetchUpdate: %s", aUpdateUrl.Data()));
//LOG(("requestBody: %s", aRequestBody.Data()));
return FetchUpdate(aUpdateUrl, aRequestBody, EmptyCString());
return FetchUpdate(aUpdateUrl, aRequestPayload, aIsPostRequest, EmptyCString());
}
///////////////////////////////////////////////////////////////////////////////
@ -318,7 +344,9 @@ nsUrlClassifierStreamUpdater::FetchNext()
PendingUpdate &update = mPendingUpdates[0];
LOG(("Fetching update url: %s\n", update.mUrl.get()));
nsresult rv = FetchUpdate(update.mUrl, EmptyCString(),
nsresult rv = FetchUpdate(update.mUrl,
EmptyCString(),
true, // This method is for v2 and v2 is always a POST.
update.mTable);
if (NS_FAILED(rv)) {
LOG(("Error fetching update url: %s\n", update.mUrl.get()));
@ -349,7 +377,8 @@ nsUrlClassifierStreamUpdater::FetchNextRequest()
bool dummy;
DownloadUpdates(
request.mTables,
request.mRequest,
request.mRequestPayload,
request.mIsPostRequest,
request.mUrl,
request.mSuccessCallback,
request.mUpdateErrorCallback,

Просмотреть файл

@ -54,11 +54,13 @@ private:
// Fetches an update for a single table.
nsresult FetchUpdate(nsIURI *aURI,
const nsACString &aRequestBody,
const nsACString &aRequest,
bool aIsPostRequest,
const nsACString &aTable);
// Dumb wrapper so we don't have to create URIs.
nsresult FetchUpdate(const nsACString &aURI,
const nsACString &aRequestBody,
const nsACString &aRequest,
bool aIsPostRequest,
const nsACString &aTable);
// Fetches the next table, from mPendingUpdates.
@ -78,7 +80,8 @@ private:
struct PendingRequest {
nsCString mTables;
nsCString mRequest;
nsCString mRequestPayload;
bool mIsPostRequest;
nsCString mUrl;
nsCOMPtr<nsIUrlClassifierCallback> mSuccessCallback;
nsCOMPtr<nsIUrlClassifierCallback> mUpdateErrorCallback;

Просмотреть файл

@ -211,6 +211,9 @@ static const struct {
{ "googpub-phish-proto", SOCIAL_ENGINEERING_PUBLIC}, // 2
{ "goog-unwanted-proto", UNWANTED_SOFTWARE}, // 3
{ "goog-phish-proto", SOCIAL_ENGINEERING}, // 5
// For testing purpose.
{ "test-phish-proto", SOCIAL_ENGINEERING_PUBLIC}, // 2
};
NS_IMETHODIMP

Просмотреть файл

@ -200,7 +200,7 @@ function doStreamUpdate(updateText, success, failure, downloadFailure) {
downloadFailure = failure;
}
streamUpdater.downloadUpdates(allTables, "",
streamUpdater.downloadUpdates(allTables, "", true,
dataUpdate, success, failure, downloadFailure);
}

Просмотреть файл

@ -118,6 +118,7 @@ add_test(function test_update() {
streamUpdater.downloadUpdates(
"goog-downloadwhite-digest256",
"goog-downloadwhite-digest256;\n",
true,
"http://localhost:4444/downloads",
updateSuccess, handleError, handleError);
});

Просмотреть файл

@ -31,31 +31,36 @@ const TEST_TABLE_DATA_LIST = [
}
];
// This table has a different update URL.
const TEST_TABLE_DATA_ANOTHER = {
tableName: "test-listmanageranother-digest256",
providerName: "google",
updateUrl: "http://localhost:5555/safebrowsing/update",
gethashUrl: "http://localhost:5555/safebrowsing/gethash-another",
// This table has a different update URL (for v4).
const TEST_TABLE_DATA_V4 = {
tableName: "test-phish-proto",
providerName: "google4",
updateUrl: "http://localhost:5555/safebrowsing/update?",
gethashUrl: "http://localhost:5555/safebrowsing/gethash-v4",
};
const PREF_NEXTUPDATETIME = "browser.safebrowsing.provider.google.nextupdatetime";
const PREF_NEXTUPDATETIME_V4 = "browser.safebrowsing.provider.google4.nextupdatetime";
let gListManager = Cc["@mozilla.org/url-classifier/listmanager;1"]
.getService(Ci.nsIUrlListManager);
let gUrlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
.getService(Ci.nsIUrlClassifierUtils);
// Global test server for serving safebrowsing updates.
let gHttpServ = null;
let gUpdateResponse = "";
let gExpectedUpdateRequest = "";
let gExpectedQueryV4 = "";
// Handles request for TEST_TABLE_DATA_ANOTHER.
let gHttpServAnother = null;
// Handles request for TEST_TABLE_DATA_V4.
let gHttpServV4 = null;
// These two variables are used to synchronize the last two racing updates
// (in terms of "update URL") in test_update_all_tables().
let gUpdatedCntForTableData = 0; // For TEST_TABLE_DATA_LIST.
let gIsAnotherUpdated = false; // For TEST_TABLE_DATA_ANOTHER.
let gIsV4Updated = false; // For TEST_TABLE_DATA_V4.
prefBranch.setBoolPref("browser.safebrowsing.debug", true);
@ -66,10 +71,11 @@ TEST_TABLE_DATA_LIST.forEach(function(t) {
t.updateUrl,
t.gethashUrl);
});
gListManager.registerTable(TEST_TABLE_DATA_ANOTHER.tableName,
TEST_TABLE_DATA_ANOTHER.providerName,
TEST_TABLE_DATA_ANOTHER.updateUrl,
TEST_TABLE_DATA_ANOTHER.gethashUrl);
gListManager.registerTable(TEST_TABLE_DATA_V4.tableName,
TEST_TABLE_DATA_V4.providerName,
TEST_TABLE_DATA_V4.updateUrl,
TEST_TABLE_DATA_V4.gethashUrl);
const SERVER_INVOLVED_TEST_CASE_LIST = [
// - Do table0 update.
@ -110,17 +116,26 @@ const SERVER_INVOLVED_TEST_CASE_LIST = [
function test_update_all_tables() {
disableAllUpdates();
// Enable all tables including TEST_TABLE_DATA_ANOTHER!
// Enable all tables including TEST_TABLE_DATA_V4!
TEST_TABLE_DATA_LIST.forEach(function(t) {
gListManager.enableUpdate(t.tableName);
});
gListManager.enableUpdate(TEST_TABLE_DATA_ANOTHER.tableName);
gListManager.enableUpdate(TEST_TABLE_DATA_V4.tableName);
// Expected results for v2.
gExpectedUpdateRequest = TEST_TABLE_DATA_LIST[0].tableName + ";a:5:s:2-12\n" +
TEST_TABLE_DATA_LIST[1].tableName + ";\n" +
TEST_TABLE_DATA_LIST[2].tableName + ";\n";
gUpdateResponse = "n:1000\n";
// We test the request against the query string since v4 request
// would be appened to the query string. The request is generated
// by protobuf API (binary) then encoded to base64 format.
let requestV4 = gUrlUtils.makeUpdateRequestV4([TEST_TABLE_DATA_V4.tableName],
[""],
1);
gExpectedQueryV4 = "&$req=" + btoa(requestV4);
forceTableUpdate();
},
@ -133,8 +148,8 @@ add_test(function test_getGethashUrl() {
TEST_TABLE_DATA_LIST.forEach(function (t) {
equal(gListManager.getGethashUrl(t.tableName), t.gethashUrl);
});
equal(gListManager.getGethashUrl(TEST_TABLE_DATA_ANOTHER.tableName),
TEST_TABLE_DATA_ANOTHER.gethashUrl);
equal(gListManager.getGethashUrl(TEST_TABLE_DATA_V4.tableName),
TEST_TABLE_DATA_V4.gethashUrl);
run_next_test();
});
@ -165,36 +180,43 @@ function run_test() {
return;
}
if (gIsAnotherUpdated) {
if (gIsV4Updated) {
run_next_test(); // All tests are done. Just finish.
return;
}
do_print("Waiting for TEST_TABLE_DATA_ANOTHER to be tested ...");
do_print("Waiting for TEST_TABLE_DATA_V4 to be tested ...");
});
gHttpServ.start(4444);
// Setup another testing server for the different update URL.
gHttpServAnother = new HttpServer();
gHttpServAnother.registerDirectory("/", do_get_cwd());
// Setup v4 testing server for the different update URL.
gHttpServV4 = new HttpServer();
gHttpServV4.registerDirectory("/", do_get_cwd());
gHttpServAnother.registerPathHandler("/safebrowsing/update", function(request, response) {
let body = NetUtil.readInputStreamToString(request.bodyInputStream,
request.bodyInputStream.available());
gHttpServV4.registerPathHandler("/safebrowsing/update", function(request, response) {
// V4 update request body should be empty.
equal(request.bodyInputStream.available(), 0);
// Verify if the request is as expected.
equal(body, TEST_TABLE_DATA_ANOTHER.tableName + ";\n");
// Not on the spec. Found in Chromium source code...
equal(request.getHeader("X-HTTP-Method-Override"), "POST");
// Respond with no chunk control.
// V4 update request uses GET.
equal(request.method, "GET");
// V4 append the base64 encoded request to the query string.
equal(request.queryString, gExpectedQueryV4);
// Respond a V2 compatible content for now. In the future we can
// send a meaningful response to test Bug 1284178 to see if the
// update is successfully stored to database.
response.setHeader("Content-Type",
"application/vnd.google.safebrowsing-update", false);
response.setStatusLine(request.httpVersion, 200, "OK");
let content = "n:1000\n";
response.bodyOutputStream.write(content, content.length);
gIsAnotherUpdated = true;
gIsV4Updated = true;
if (gUpdatedCntForTableData === SERVER_INVOLVED_TEST_CASE_LIST.length) {
// All tests are done!
@ -205,7 +227,7 @@ function run_test() {
do_print("Wait for all sever-involved tests to be done ...");
});
gHttpServAnother.start(5555);
gHttpServV4.start(5555);
run_next_test();
}
@ -214,12 +236,13 @@ function run_test() {
// call disableAllUpdates() first to clean up the updateCheckers in listmanager.
function forceTableUpdate() {
prefBranch.setCharPref(PREF_NEXTUPDATETIME, "1");
prefBranch.setCharPref(PREF_NEXTUPDATETIME_V4, "1");
gListManager.maybeToggleUpdateChecking();
}
function disableAllUpdates() {
TEST_TABLE_DATA_LIST.forEach(t => gListManager.disableUpdate(t.tableName));
gListManager.disableUpdate(TEST_TABLE_DATA_ANOTHER.tableName);
gListManager.disableUpdate(TEST_TABLE_DATA_V4.tableName);
}
// Since there's no public interface on listmanager to know the update success,
@ -243,3 +266,11 @@ function readFileToString(aFilename) {
let buf = NetUtil.readInputStreamToString(stream, stream.available());
return buf;
}
function buildUpdateRequestV4InBase64() {
let request = urlUtils.makeUpdateRequestV4([TEST_TABLE_DATA_V4.tableName],
[""],
1);
return btoa(request);
}