gecko-dev/browser/modules/LiveBookmarkMigrator.jsm

230 строки
8.7 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
OS: "resource://gre/modules/osfile.jsm",
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
});
XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "XMLSerializer"]);
XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
return Services.strings.createBundle("chrome://browser/locale/browser.properties");
});
const kMigrationPref = "browser.livebookmarks.migrationAttemptsLeft";
function migrationSucceeded() {
Services.prefs.clearUserPref(kMigrationPref);
}
function migrationError() {
// Decrement the number of remaining attempts.
let remainingAttempts = Math.max(0, Services.prefs.getIntPref(kMigrationPref, 1) - 1);
Services.prefs.setIntPref(kMigrationPref, remainingAttempts);
}
var LiveBookmarkMigrator = {
_isOldDefaultBookmark(liveBookmark) {
if (!liveBookmark.feedURI || !liveBookmark.feedURI.host) {
return false;
}
let {host} = liveBookmark.feedURI;
return host.endsWith("fxfeeds.mozilla.com") || host.endsWith("fxfeeds.mozilla.org");
},
async _fetch() {
function getAnnoSQLFragment(aAnnoParam) {
return `SELECT a.content
FROM moz_items_annos a
JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
WHERE a.item_id = b.id
AND n.name = ${aAnnoParam}`;
}
// Copied and modified from nsLivemarkService.js, which we'll want to remove even when
// we keep this migration for a while.
const LB_SQL =
`SELECT b.title, b.guid, b.dateAdded, b.position as 'index', p.guid AS parentGuid,
( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
FROM moz_bookmarks b
JOIN moz_bookmarks p ON b.parent = p.id
JOIN moz_items_annos a ON a.item_id = b.id
JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
WHERE b.type = :folder_type
AND n.name = :feedURI_anno
ORDER BY b.position DESC`;
// We sort by position so we go over items last-to-first. This way, we can insert a
// duplicate "normal" bookmark for each livemark, without causing future insertions
// to be off-by-N in their positioning because of the insertions.
let conn = await PlacesUtils.promiseDBConnection();
let rows = await conn.execute(LB_SQL,
{ folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
siteURI_anno: PlacesUtils.LMANNO_SITEURI });
// Create a JS object out of the sqlite result:
let liveBookmarks = [];
for (let row of rows) {
let siteURI = row.getResultByName("siteURI");
let feedURI = row.getResultByName("feedURI");
try {
feedURI = new URL(feedURI);
siteURI = siteURI ? new URL(siteURI) : null;
} catch (ex) {
// Skip items with broken URLs:
Cu.reportError(ex);
continue;
}
liveBookmarks.push({
guid: row.getResultByName("guid"),
index: row.getResultByName("index"),
dateAdded: PlacesUtils.toDate(row.getResultByName("dateAdded")),
parentGuid: row.getResultByName("parentGuid"),
title: row.getResultByName("title"),
siteURI,
feedURI,
});
}
return liveBookmarks;
},
async _writeOPML(liveBookmarks) {
const appName = Services.appinfo.name;
let hiddenBrowser = Services.appShell.createWindowlessBrowser();
let opmlString = "";
try {
let hiddenDOMDoc = hiddenBrowser.document;
// Create head:
let doc = hiddenDOMDoc.implementation.createDocument("", "opml", null);
let root = doc.documentElement;
root.setAttribute("version", "1.0");
let head = doc.createElement("head");
root.appendChild(head);
let title = doc.createElement("title");
title.textContent =
gBrowserBundle.formatStringFromName("livebookmarkMigration.title", [appName], 1);
head.appendChild(title);
let body = doc.createElement("body");
root.appendChild(body);
// Make things vaguely readable:
body.textContent = "\n";
for (let lb of liveBookmarks) {
if (this._isOldDefaultBookmark(lb)) {
// Ignore the old default bookmarks and don't back them up.
continue;
}
let outline = doc.createElement("outline");
outline.setAttribute("type", "rss");
outline.setAttribute("title", lb.title);
outline.setAttribute("text", lb.title);
outline.setAttribute("xmlUrl", lb.feedURI.href);
if (lb.siteURI) {
outline.setAttribute("htmlUrl", lb.siteURI.href);
}
body.appendChild(outline);
body.appendChild(doc.createTextNode("\n"));
}
let serializer = new XMLSerializer();
// The serializer doesn't add an XML declaration (bug 318086), so we add it manually.
opmlString = '<?xml version="1.0"?>\n' + serializer.serializeToString(doc);
} finally {
hiddenBrowser.close();
}
let {path: basePath} = Services.dirsvc.get("Desk", Ci.nsIFile);
let feedFileName = appName + " feeds backup.opml";
basePath = OS.Path.join(basePath, feedFileName);
let {file, path} = await OS.File.openUnique(basePath, {humanReadable: true});
await file.close();
return OS.File.writeAtomic(path, opmlString, {encoding: "utf-8"});
},
async _transformBookmarks(liveBookmarks) {
let itemsToReplace = liveBookmarks.filter(lb => !this._isOldDefaultBookmark(lb));
let itemsToInsert = itemsToReplace.map(item => ({
url: item.siteURI || item.feedURI,
parentGuid: item.parentGuid,
index: item.index,
title: item.title,
dateAdded: item.dateAdded,
}));
// Insert new bookmarks at the same index. The list is sorted by position
// in reverse order, so we'll insert later items before the earlier ones.
// This avoids the indices getting outdated for later insertions.
for (let item of itemsToInsert) {
await PlacesUtils.bookmarks.insert(item).catch(Cu.reportError);
}
// Now remove all of the actual live bookmarks. Avoid mismatches due to the
// bookmarks having been moved or altered in the meantime, just remove
// anything with a matching guid:
let itemsToRemove = liveBookmarks.map(lb => ({guid: lb.guid}));
await PlacesUtils.bookmarks.remove(itemsToRemove).catch(Cu.reportError);
},
_openSUMOPage() {
let sumoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "live-bookmarks-migration";
let topWin = BrowserWindowTracker.getTopWindow({private: false});
if (!topWin) {
let args = PlacesUtils.toISupportsString(sumoURL);
Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, "_blank", "chrome,dialog=no,all", args);
} else {
topWin.openTrustedLinkIn(sumoURL, "tab");
}
},
async migrate() {
try {
// First fetch all live bookmark folders:
let liveBookmarks = await this._fetch();
if (!liveBookmarks || !liveBookmarks.length) {
migrationSucceeded();
return;
}
let haveNonDefaultBookmarks = liveBookmarks.some(lb => !this._isOldDefaultBookmark(lb));
// Then generate OPML file content, write to disk, if we've got anything to back up:
if (haveNonDefaultBookmarks) {
await this._writeOPML(liveBookmarks);
}
// Replace all live bookmarks with normal bookmarks.
await this._transformBookmarks(liveBookmarks).catch(ex => {
// Don't stop migrating at this point, because we've written the exported OPML file.
// We shouldn't ever hit this - transformLiveBookmarks is supposed to take
// care of its own failures.
Cu.reportError(ex);
});
try {
if (haveNonDefaultBookmarks) {
this._openSUMOPage();
}
} catch (ex) {
// Note that if we get here, we've removed any extant livemarks, so there's no point
// re-running the migration - there won't be any livemarks left and we won't re-show the SUMO
// page. So just report the error, but mark migration as successful.
Cu.reportError(new Error("Live bookmarks migration didn't manage to show the support page: " + ex));
}
} catch (ex) {
migrationError();
throw ex;
}
migrationSucceeded();
},
};
var EXPORTED_SYMBOLS = ["LiveBookmarkMigrator"];