diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5b45a75
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+### 1.0.0
+- initial release
+- add full Pale Moon compatibility
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..14e2f77
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ 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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fcf8ca6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# MozArchiver
+Extension to allow the creation and viewing of MAFF and MHT archive files within Pale Moon. Fork of the extension [Mozilla Archive Format](https://addons.mozilla.org/en-US/firefox/addon/mozilla-archive-format/) by Christopher Ottley and Paolo Amadini for Pale Moon.
+
+## Building
+Simply download the contents of the "src" folder and pack the contents into a .zip file. Then, rename the file to .xpi and drag into the browser.
diff --git a/src/chrome.manifest b/src/chrome.manifest
new file mode 100644
index 0000000..8496961
--- /dev/null
+++ b/src/chrome.manifest
@@ -0,0 +1,55 @@
+content mza chrome/content/
+
+locale mza en-US chrome/locale/en-US/
+
+locale mza cs chrome/locale/cs/
+locale mza da chrome/locale/da/
+locale mza de chrome/locale/de/
+locale mza es-ES chrome/locale/es-ES/
+locale mza fr chrome/locale/fr/
+locale mza hu chrome/locale/hu/
+locale mza hy-AM chrome/locale/hy-AM/
+locale mza it chrome/locale/it/
+locale mza ja-JP chrome/locale/ja-JP/
+locale mza ko-KR chrome/locale/ko-KR/
+locale mza mk-MK chrome/locale/mk-MK/
+locale mza pl chrome/locale/pl/
+locale mza pt-BR chrome/locale/pt-BR/
+locale mza ro chrome/locale/ro/
+locale mza ru chrome/locale/ru/
+locale mza sv-SE chrome/locale/sv-SE/
+locale mza tr chrome/locale/tr/
+locale mza zh-CN chrome/locale/zh-CN/
+locale mza zh-TW chrome/locale/zh-TW/
+
+skin mza classic/1.0 chrome/skin/
+skin mza-icons classic/1.0 ./
+
+component {3b2f1177-d918-44ee-91a6-ba95954064bb} components/DocumentLoaderFactory.js
+contract @mozarchiver/document-loader-factory;1 {3b2f1177-d918-44ee-91a6-ba95954064bb}
+
+component {37116274-8df3-4d48-8533-00eae60c844c} components/Startup.js
+contract @mozarchiver/startup;1 {37116274-8df3-4d48-8533-00eae60c844c}
+category profile-after-change MozArchiver @mozarchiver/startup;1
+
+component {7380f280-ab36-4a23-b213-35c64f8586a0} components/ContentPolicy.js
+contract @mozarchiver/content-policy;1 {7380f280-ab36-4a23-b213-35c64f8586a0}
+category content-policy MozArchiver @mozarchiver/content-policy;1
+
+# Integration with the browsing windows
+overlay chrome://browser/content/browser.xul chrome://mza/content/integration/mafBaseBrowserOverlay.xul
+
+# Integration with the "Multiple Tab Handler" extension
+overlay chrome://multipletab/content/multipletab.xul chrome://mza/content/integration/mafMultipleTabOverlay.xul
+
+# The Archives dialog
+overlay chrome://mza/content/archives/archivesDialog.xul chrome://mza/content/archives/archivesDialogBrowserOverlay.xul application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
+
+# The preferences dialog
+overlay chrome://mza/content/preferences/prefsDialog.xul chrome://mza/content/preferences/prefsDialogBrowserOverlay.xul application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
+
+# Use the preferences image file from the current theme
+override chrome://mza/skin/preferences/Options.png chrome://browser/skin/preferences/Options.png application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
+
+# Once loaded, use the 48x48 pixels application icon instead of the 32x32 pixels one
+override chrome://mza-icons/skin/icon.png chrome://mza-icons/skin/icon48.png
diff --git a/src/chrome/content/MozillaArchiveFormat.jsm b/src/chrome/content/MozillaArchiveFormat.jsm
new file mode 100644
index 0000000..4bdf6b7
--- /dev/null
+++ b/src/chrome/content/MozillaArchiveFormat.jsm
@@ -0,0 +1,96 @@
+/**
+ * Exports all the common JavaScript objects for Mozilla Archive Format.
+ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let objectsByFolder = {
+ general: [
+ "AsyncEnumerator",
+ "DataSourceWrapper",
+ "Interface",
+ "PersistBundle",
+ "PersistFolder",
+ "PersistResource",
+ "SourceFragment",
+ "CssSourceFragment",
+ "HtmlSourceFragment",
+ "TagSourceFragment",
+ "UrlListSourceFragment",
+ "UrlSourceFragment",
+ ],
+ archives: [
+ "ArchiveAnnotations",
+ "ArchiveHistoryObserver",
+ ],
+ convert: [
+ "Candidate",
+ "CandidateFinder",
+ "CandidateLocation",
+ "CandidatesDataSource",
+ ],
+ engine: [
+ "Archive",
+ "ArchiveCache",
+ "ArchivePage",
+ "MaffArchive",
+ "MaffArchivePage",
+ "MaffDataSource",
+ "MhtmlArchive",
+ "MhtmlArchivePage",
+ "MimePart",
+ "MimeSupport",
+ "MultipartMimePart",
+ "ZipCreator",
+ "ZipDirectory",
+ ],
+ integration: [
+ "FileFilters",
+ "TabsDataSource",
+ ],
+ loading: [
+ "ArchiveLoader",
+ "ArchiveStreamConverter",
+ ],
+ preferences: [
+ "DynamicPrefs",
+ "FileAssociations",
+ "Prefs",
+ ],
+ savecomplete: [
+ "MafSaveComplete",
+ "SaveCompletePersist",
+ ],
+ saving: [
+ "Job",
+ "JobRunner",
+ "ExactPersistInitialJob",
+ "ExactPersistJob",
+ "ExactPersist",
+ "ExactPersistParsedJob",
+ "ExactPersistReference",
+ "ExactPersistUnparsedJob",
+ "MafArchivePersist",
+ "MafWebProgressListener",
+ "SaveArchiveJob",
+ "SaveContentJob",
+ "SaveJob",
+ ],
+ startup: [
+ "HelperAppsWrapper",
+ "StartupEvents",
+ "StartupInitializer",
+ ],
+};
+
+let EXPORTED_SYMBOLS = [];
+for (let folderName of Object.keys(objectsByFolder)) {
+ for (let objectName of objectsByFolder[folderName]) {
+ EXPORTED_SYMBOLS.push(objectName);
+ Services.scriptloader.loadSubScript("chrome://mza/content/" + folderName +
+ "/" + objectName + ".js");
+ }
+}
diff --git a/src/chrome/content/archives/ArchiveAnnotations.js b/src/chrome/content/archives/ArchiveAnnotations.js
new file mode 100644
index 0000000..f4c949d
--- /dev/null
+++ b/src/chrome/content/archives/ArchiveAnnotations.js
@@ -0,0 +1,208 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This object handles annotations on archive pages for this session.
+ *
+ * Annotations are used to query and display information about the known
+ * archives using the Places interfaces. At present, the actual annotations are
+ * only used to sort the results properly in the Places view of the Archives
+ * dialog. For more information, see
+ *
+ * (retrieved 2009-05-23).
+ */
+var ArchiveAnnotations = {
+
+ /** MAF annotation names */
+ MAFANNO_TITLE: "maf/title",
+ MAFANNO_ORIGINALURL: "maf/originalurl",
+ MAFANNO_DATEARCHIVED: "maf/datearchived",
+ MAFANNO_ARCHIVENAME: "maf/archivename",
+ MAFANNO_TEMPURI: "maf/tempuri",
+ MAFANNO_DIRECTARCHIVEURI: "maf/directarchiveuri",
+
+ /**
+ * Sets all the annotations associated with the given ArchivePage object.
+ */
+ setAnnotationsForPage: function(aPage) {
+ try {
+ // For all the possible annotations
+ [
+ [ArchiveAnnotations.MAFANNO_TITLE, aPage.title],
+ [ArchiveAnnotations.MAFANNO_ORIGINALURL, aPage.originalUrl],
+ [ArchiveAnnotations.MAFANNO_DATEARCHIVED, aPage.dateArchived],
+ [ArchiveAnnotations.MAFANNO_ARCHIVENAME, aPage.archive.name],
+ [ArchiveAnnotations.MAFANNO_TEMPURI,
+ (aPage.tempUri ? aPage.tempUri.spec : "")],
+ [ArchiveAnnotations.MAFANNO_DIRECTARCHIVEURI,
+ (aPage.directArchiveUri ? aPage.directArchiveUri.spec : "")],
+ ].forEach(function([annotationName, annotationValue]) {
+ // Set the annotation while handling the data types correctly.
+ ArchiveAnnotations._setAnnotationForPage(aPage, annotationName,
+ annotationValue);
+ });
+ } catch (e if (e instanceof Ci.nsIException && (e.result ==
+ Cr.NS_ERROR_INVALID_ARG))) {
+ // This error is raised if the page is not present in history when the
+ // functions that set annotations are called. In this case, we set a flag
+ // on the page object indicating that the history observer should add the
+ // annotations after the page visit information is added.
+ aPage.annotationsPending = true;
+ }
+ },
+
+ /**
+ * Removes all the annotations associated with the given ArchivePage object.
+ */
+ removeAnnotationsForPage: function(aPage) {
+ // For all the possible annotations
+ [
+ ArchiveAnnotations.MAFANNO_TITLE,
+ ArchiveAnnotations.MAFANNO_ORIGINALURL,
+ ArchiveAnnotations.MAFANNO_DATEARCHIVED,
+ ArchiveAnnotations.MAFANNO_ARCHIVENAME,
+ ArchiveAnnotations.MAFANNO_TEMPURI,
+ ArchiveAnnotations.MAFANNO_DIRECTARCHIVEURI,
+ ].forEach(function(annotationName) {
+ try {
+ // Clear the annotation if present on the page's specific archive URI.
+ ArchiveAnnotations._annotationService.removePageAnnotation(
+ aPage.archiveUri, annotationName);
+ } catch (e if (e instanceof Ci.nsIException && (e.result ==
+ Cr.NS_ERROR_INVALID_ARG))) {
+ // Ignore errors due to the fact that the page is not in history.
+ }
+ });
+ },
+
+ /**
+ * Returns the value of an annotation for the given ArchivePage object.
+ *
+ * This function returns a Date object for annotations that represent dates.
+ *
+ * In order to avoid problems due to the asynchronous adding of history
+ * entries, the annotation values are not actually read using the Places
+ * services, but extracted from the provided object.
+ */
+ getAnnotationForPage: function(aPage, aAnnotationName) {
+ var annotationValue;
+ switch (aAnnotationName) {
+ case ArchiveAnnotations.MAFANNO_TITLE:
+ annotationValue = aPage.title;
+ break;
+ case ArchiveAnnotations.MAFANNO_ORIGINALURL:
+ annotationValue = aPage.originalUrl;
+ break;
+ case ArchiveAnnotations.MAFANNO_DATEARCHIVED:
+ annotationValue = aPage.dateArchived;
+ break;
+ case ArchiveAnnotations.MAFANNO_ARCHIVENAME:
+ annotationValue = aPage.archive.name;
+ break;
+ case ArchiveAnnotations.MAFANNO_TEMPURI:
+ annotationValue = (aPage.tempUri ? aPage.tempUri.spec : "");
+ break;
+ case ArchiveAnnotations.MAFANNO_DIRECTARCHIVEURI:
+ annotationValue = (aPage.directArchiveUri ?
+ aPage.directArchiveUri.spec : "");
+ break;
+ }
+ // If the value represents an URI, store this information in the value
+ // itself. This allows for properly displaying the value later. The
+ // annotation should not be added if the value is empty, otherwise the tests
+ // to detect this condition will provide incorrect results.
+ if (annotationValue && ArchiveAnnotations.annotationIsEscapedAsUri(
+ aAnnotationName)) {
+ annotationValue = new String(annotationValue);
+ annotationValue.isEscapedAsUri = true;
+ }
+ // Return the string, numeric or date value.
+ return annotationValue;
+ },
+
+ /**
+ * Returns True if the specified annotation represents a date.
+ */
+ annotationIsDate: function(aAnnotationName) {
+ return (aAnnotationName === ArchiveAnnotations.MAFANNO_DATEARCHIVED);
+ },
+
+ /**
+ * Returns True if the specified annotation represents an URI or part of it.
+ */
+ annotationIsEscapedAsUri: function(aAnnotationName) {
+ return [
+ ArchiveAnnotations.MAFANNO_ORIGINALURL,
+ ArchiveAnnotations.MAFANNO_ARCHIVENAME,
+ ArchiveAnnotations.MAFANNO_TEMPURI,
+ ArchiveAnnotations.MAFANNO_DIRECTARCHIVEURI,
+ ].indexOf(aAnnotationName) >= 0;
+ },
+
+ /**
+ * Sets the value of an annotation for the given ArchivePage object.
+ *
+ * This function accepts a Date object for annotations that represent dates.
+ */
+ _setAnnotationForPage: function(aPage, aAnnotationName, aAnnotationValue) {
+ var annotationValue = aAnnotationValue;
+ // If this annotation represents a date, convert the Date object to a
+ // comparable numeric value. If the date is unspecified, store the numeric
+ // value 0 in the annotation.
+ if (ArchiveAnnotations.annotationIsDate(aAnnotationName)) {
+ annotationValue = annotationValue ? annotationValue.getTime() : 0;
+ // In order for the sorting to work correctly, we must store the
+ // annotation as a string, and not a numeric value. Dates before 01
+ // January, 1970 UTC are not supported at present.
+ annotationValue = (annotationValue < 0) ? 0 : annotationValue;
+ annotationValue = ("000000000000000" + annotationValue).slice(-16);
+ }
+ // Set the annotation on the page's specific archive URI. The annotations
+ // will expire when the current session terminates.
+ ArchiveAnnotations._annotationService.setPageAnnotation(
+ aPage.archiveUri, aAnnotationName, annotationValue || "", 0,
+ Ci.nsIAnnotationService.EXPIRE_SESSION);
+ },
+
+ /**
+ * Returns a reference to the annotation service.
+ */
+ get _annotationService() {
+ return Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+ },
+};
diff --git a/src/chrome/content/archives/ArchiveHistoryObserver.js b/src/chrome/content/archives/ArchiveHistoryObserver.js
new file mode 100644
index 0000000..7cde431
--- /dev/null
+++ b/src/chrome/content/archives/ArchiveHistoryObserver.js
@@ -0,0 +1,63 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This object listens for new page history additions and sets the required
+ * Places annotations for all pages that were loaded and for which this
+ * operation is still pending.
+ */
+var ArchiveHistoryObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavHistoryObserver,
+ ]),
+ onBeginUpdateBatch: function() { },
+ onEndUpdateBatch: function() { },
+ onVisit: function(aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType, aGUID, aAdded) {
+ // If the page that was just visited is waiting for adding annotations
+ var page = ArchiveCache.pageFromUri(aURI);
+ if (page && page.annotationsPending) {
+ delete page.annotationsPending;
+ ArchiveAnnotations.setAnnotationsForPage(page);
+ }
+ },
+ onTitleChanged: function(aURI, aPageTitle) { },
+ onDeleteURI: function(aURI, aGUID) { },
+ onClearHistory: function() { },
+ onPageChanged: function(aURI, aWhat, aValue) { },
+ onDeleteVisits: function(aURI, aVisitTime, aGUID) { },
+};
diff --git a/src/chrome/content/archives/archivesDialog.js b/src/chrome/content/archives/archivesDialog.js
new file mode 100644
index 0000000..e9998eb
--- /dev/null
+++ b/src/chrome/content/archives/archivesDialog.js
@@ -0,0 +1,622 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("chrome://mza/content/MozillaArchiveFormat.jsm");
+
+/**
+ * Handles the MAF dialog that displays information about the known archives.
+ */
+var ArchivesDialog = {
+
+ /**
+ * The tree element that lists the known archive pages.
+ */
+ archivesTree: null,
+
+ // --- Interactive dialog functions and events ---
+
+ /**
+ * Initializes the dialog when the window is opened initially.
+ *
+ * This function prepares the initial Places view. For more information, see
+ *
+ * (retrieved 2009-05-23).
+ */
+ onLoadDialog: function() {
+ // Execute the initialization functions that are specific to SeaMonkey.
+ if (window.onNavigatorLoadDialog) {
+ window.onNavigatorLoadDialog();
+ }
+
+ // Get a reference to the tree that will display the main Places view.
+ ArchivesDialog.archivesTree = document.getElementById("treeArchives");
+
+ // Customize the Places view and the operations that can be performed on it.
+ ArchivesDialog.customizePlacesView();
+ ArchivesDialog.customizePlacesController();
+
+ // Rename the "delete" command in the Places context menu to reflect the
+ // actual operation it performs on the customized Places tree.
+ var btnDelete = document.getElementById("btnDelete");
+ var contextMenuItem =
+ document.getElementById("placesContext_delete_history") ||
+ document.getElementById("placesContext_delete");
+ contextMenuItem.setAttribute("label", btnDelete.getAttribute("label"));
+ contextMenuItem.setAttribute("accesskey", btnDelete.getAttribute(
+ "cxtaccesskey"));
+
+ // Remove the original history handling commands in the Places context menu
+ // because their presence in the Archives view is confusing.
+ for (var [, commandName] in Iterator([
+ "placesContext_deleteHost",
+ "placesContext_deleteByHostname",
+ "placesContext_deleteByDomain"
+ ])) {
+ var element = document.getElementById(commandName);
+ if (element) {
+ document.getElementById("placesContext").removeChild(element);
+ }
+ }
+
+ // Execute the initial update of the controls.
+ ArchivesDialog.checkShowMore();
+ ArchivesDialog.checkPlaceInfo();
+ },
+
+ /**
+ * Updates the displayed information on the current selection in the tree.
+ */
+ onTreeSelect: function(aEvent) {
+ ArchivesDialog.checkPlaceInfo();
+ },
+
+ /**
+ * Opens the selected node when return is pressed.
+ */
+ onTreeKeyPress: function(aEvent) {
+ if (aEvent.keyCode != KeyEvent.DOM_VK_RETURN)
+ return;
+
+ // Open the page represented by the selected node in the archives tree.
+ PlacesUIUtils.openNodeWithEvent(ArchivesDialog.archivesTree.selectedNode,
+ aEvent);
+ },
+
+ /**
+ * Opens the selected node on middle click.
+ */
+ onTreeClick: function(aEvent) {
+ if (aEvent.target.localName != "treechildren" || aEvent.button != 1)
+ return;
+
+ // Open the page represented by the selected node in the archives tree.
+ PlacesUIUtils.openNodeWithEvent(ArchivesDialog.archivesTree.selectedNode,
+ aEvent);
+ },
+
+ /**
+ * Opens the selected node on double click.
+ */
+ onTreeDblClick: function(aEvent) {
+ if (aEvent.target.localName != "treechildren")
+ return;
+
+ // Open the page represented by the selected node in the archives tree.
+ PlacesUIUtils.openNodeWithEvent(ArchivesDialog.archivesTree.selectedNode,
+ aEvent);
+ },
+
+ /**
+ * Toggles the visibility of the additional archive page details.
+ */
+ onShowMoreClick: function() {
+ // Update the "hidden" attribute on the broadcaster.
+ var brShowMore = document.getElementById("brShowMore");
+ brShowMore.setAttribute("hidden", brShowMore.hidden ? "false" : "true");
+ // Update the label on the button.
+ ArchivesDialog.checkShowMore();
+ },
+
+ /**
+ * Loads information about archive files, without opening them in the browser.
+ */
+ onAddClick: function(aEvent) {
+ // Determine the title of the file picker dialog.
+ var title = document.getElementById("btnAdd").getAttribute("fptitle");
+
+ // Initialize a new file picker with filters for web archives.
+ var filePicker = Cc["@mozilla.org/filepicker;1"].
+ createInstance(Ci.nsIFilePicker);
+ filePicker.init(window, title, Ci.nsIFilePicker.modeOpenMultiple);
+ FileFilters.openFilters.forEach(function(curFilter) {
+ filePicker.appendFilter(curFilter.title, curFilter.extensionString);
+ });
+ filePicker.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ // Show the file picker and exit now if canceled.
+ if (filePicker.show() !== Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ // For every selected file
+ var filesEnumerator = filePicker.files;
+ while (filesEnumerator.hasMoreElements())
+ {
+ var file = filesEnumerator.getNext().QueryInterface(Ci.nsILocalFile);
+ // Attempt to load the archive and register it in the cache.
+ try {
+ var archive = ArchiveLoader.extractAndRegister(file);
+ for (var [, page] in Iterator(archive.pages)) {
+ // Ensure that a history visit is added for the page, otherwise the
+ // page would not appear in the Places view. The visit is recorded as
+ // a top level typed entry, so that history listeners are able to be
+ // notified about the new entry.
+ PlacesUtils.asyncHistory.updatePlaces({
+ uri: page.archiveUri,
+ visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ visitDate: Date.now() * 1000,
+ }],
+ });
+ }
+ } catch (e) {
+ // If opening the archive failed, skip it and report the error.
+ Cu.reportError(e);
+ }
+ }
+
+ // Ensure that the archives tree is refreshed immediately.
+ ArchivesDialog.requeryPlaces();
+ },
+
+ /**
+ * Invokes the delete command on the tree view.
+ */
+ onDeleteClick: function(aEvent) {
+ ArchivesDialog.deleteSelection();
+ },
+
+ /**
+ * Opens the selected nodes in tabs, using the URLs from the column associated
+ * with the button.
+ */
+ onOpenTabsClick: function(aEvent) {
+ // Determine the column for which the command has been invoked.
+ var columnId = "tc" + aEvent.target.id.slice("btnOpenTabs".length);
+ // Open the associated URLs if possible.
+ ArchivesDialog.openSelectionInTabs(columnId, aEvent);
+ },
+
+ // --- Functions that handle and customize the Places tree ---
+
+ /**
+ * Refreshes the archives tree.
+ */
+ requeryPlaces: function() {
+ // Execute the same query on the Places database again.
+ ArchivesDialog.archivesTree.place = ArchivesDialog.archivesTree.place;
+ // Since the above call restores the standard view, we must apply our
+ // customization again. The list of controllers on the tree is unaffected.
+ ArchivesDialog.customizePlacesView();
+ },
+
+ /**
+ * Replaces the Places view on the archives tree with a customized one. For
+ * more information, see
+ *
+ * (retrieved 2009-05-23).
+ */
+ customizePlacesView: function() {
+ // Create a new default Places view and override some of its functions. The
+ // functions are copied, not referenced, from the ArchivesDialog object.
+ var view = new PlacesTreeView(false, null,
+ ArchivesDialog.archivesTree.view._controller);
+
+ view._originalGetCellText = view.getCellText;
+ view._originalCycleHeader = view.cycleHeader;
+ view.getCellText = ArchivesDialog.viewGetCellText;
+ view.cycleHeader = ArchivesDialog.viewCycleHeader;
+ view.sortingChanged = function() { };
+
+ // Get a reference to the current query result object, created by the
+ // standard Places tree. Ensure that the required interface is available on
+ // the object, since this is not done by the standard Places tree.
+ var result = ArchivesDialog.getResult();
+
+ // Before detaching the old view from the tree, ensure that its reference to
+ // the tree is removed. Failing to do this would cause the new view to
+ // malfunction.
+ ArchivesDialog.archivesTree.view.QueryInterface(Ci.nsITreeView).
+ setTree(null);
+
+ // Apply the new view to the appropriate objects.
+ if (result.addObserver) {
+ result.addObserver(view, false);
+ } else {
+ result.viewer = view;
+ }
+ ArchivesDialog.archivesTree.view = view;
+ },
+
+ /**
+ * Adds a custom controller on the archives tree for overriding the clipboard
+ * commands and disabling history management commands. For more information,
+ * see (retrieved
+ * 2009-05-24).
+ */
+ customizePlacesController: function() {
+ ArchivesDialog.archivesTree.controllers.insertControllerAt(0, {
+ supportsCommand: function(aCommand) {
+ // This object takes control of all the clipboard commands.
+ return [
+ "cmd_cut", "cmd_copy", "cmd_paste", "cmd_delete"
+ ].indexOf(aCommand) >= 0;
+ },
+ isCommandEnabled: function(aCommand) {
+ // The cut and the paste commands are always disabled.
+ if (["cmd_cut", "cmd_paste"].indexOf(aCommand) >= 0) {
+ return false;
+ }
+ // Other commands require that at least one item is selected.
+ return ArchivesDialog.archivesTree.hasSelection;
+ },
+ doCommand: function(aCommand) {
+ switch (aCommand) {
+ // Copy information about the selected nodes to the clipboard.
+ case "cmd_copy":
+ ArchivesDialog.copySelection();
+ break;
+ // Forget the information about the selected archives.
+ case "cmd_delete":
+ ArchivesDialog.deleteSelection();
+ break;
+ }
+ },
+ onEvent: function(aEventName) { },
+ });
+ ArchivesDialog.archivesTree.controllers.insertControllerAt(0, {
+ supportsCommand: function(aCommand) {
+ // Disable all the history handling commands.
+ return [
+ "placesCmd_deleteDataHost", "placesCmd_delete:hostname",
+ "placesCmd_delete:domain"
+ ].indexOf(aCommand) >= 0;
+ },
+ isCommandEnabled: function(aCommand) {
+ return false;
+ },
+ doCommand: function(aCommand) { },
+ onEvent: function(aEventName) { },
+ });
+ },
+
+ /**
+ * This function is copied to the Places view object to handle the special
+ * columns that display the MAF annotations. In this function, "this" refers
+ * to the Places view object.
+ */
+ viewGetCellText: function(aRow, aCol) {
+ // Get the annotation value with the appropriate data type.
+ var value = ArchivesDialog.getNodeValue(this.nodeForTreeIndex(aRow),
+ aCol.element.id);
+ // Display localized short dates or plain string values.
+ return Interface.formatValueForDisplay(value, true);
+ },
+
+ /**
+ * This function is copied to the Places view object to handle the special
+ * columns that display the MAF annotations. In this function, "this" refers
+ * to the Places view object.
+ */
+ viewCycleHeader: function(aCol) {
+ const kAsc = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING;
+ const kDesc = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING;
+
+ // Handle the columns that display custom annotations.
+ var annotationName = ArchivesDialog.getColumnAnnotationName(aCol.element);
+ if (annotationName) {
+ // Get a reference to the current query result.
+ var result = ArchivesDialog.getResult();
+ // If the result was already sorted using the selected annotation, just
+ // reverse the sort order.
+ if (result.sortingMode == kAsc &&
+ result.sortingAnnotation == annotationName) {
+ result.sortingMode = kDesc;
+ } else if (result.sortingMode == kDesc &&
+ result.sortingAnnotation == annotationName) {
+ result.sortingMode = kAsc;
+ } else {
+ // Sort in ascending order using the specified annotation.
+ result.sortingAnnotation = annotationName;
+ result.sortingMode = kAsc;
+ }
+ return;
+ }
+
+ // If this is not a custom column, forward the call to the original function.
+ this._originalCycleHeader(aCol);
+ },
+
+ // --- Functions that execute the custom commands on the tree ---
+
+ /**
+ * Copies to the clipboard the details about the selected nodes, in various
+ * formats. The original location of the page is used instead of its local
+ * archive URL.
+ *
+ * This function is similar to the standard Places function implemented in
+ * "controller.js", except that history nodes are handled and no entry is
+ * generated for the flavor TYPE_X_MOZ_PLACE, since the override URL has no
+ * effect for that flavor. For more information, see
+ *
+ * (retrieved 2009-05-25).
+ */
+ copySelection: function() {
+ // Find the selected nodes, and exit now if no node is selected.
+ var selectedNodes = ArchivesDialog.getSelectedNodes();
+ if (!selectedNodes.length)
+ return;
+
+ // Create a new object to hold the data to be copied.
+ var transferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ if (transferable.init) {
+ transferable.init(null);
+ }
+
+ // Add the data flavors to the object in the appropriate order.
+ [
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.TYPE_UNICODE,
+ PlacesUtils.TYPE_HTML,
+ ].forEach(function(type) {
+ // For every node in the selection
+ var dataString = "";
+ for (var [, node] in Iterator(selectedNodes)) {
+ // Add the concatenation separator if necessary.
+ if (dataString) {
+ dataString += NEWLINE;
+ }
+ // Use the original location of the page when copying the node. If the
+ // original location is unspecified, the page archive URL will be used.
+ var overrideUri = ArchivesDialog.getNodeValue(node, "tcMafOriginalUrl");
+ // Add the current node's data to the string.
+ dataString += PlacesUtils.wrapNode(node, type, overrideUri);
+ }
+ // Convert the string type for the transferable object.
+ var dataSupportsString = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ dataSupportsString.data = dataString;
+ // Add the concatenated data to the transferable object.
+ transferable.addDataFlavor(type);
+ transferable.setTransferData(type, dataSupportsString,
+ dataString.length * 2);
+ });
+
+ // Copy the data to the clipboard.
+ PlacesUIUtils.clipboard.setData(transferable, null,
+ Ci.nsIClipboard.kGlobalClipboard);
+ },
+
+ /**
+ * Removes from the cache the archives containing the selected pages.
+ */
+ deleteSelection: function() {
+ // Find the selected nodes, and exit now if no node is selected.
+ var selectedNodes = ArchivesDialog.getSelectedNodes();
+ if (!selectedNodes.length)
+ return;
+
+ // For every node in the selection
+ for (var [, node] in Iterator(selectedNodes)) {
+ // Find a reference to the page from the archives cache.
+ var page = this.getNodePage(node);
+ // If the page is still cached, remove its archive from the cache.
+ if (page) {
+ ArchiveCache.unregisterArchive(page.archive);
+ }
+ }
+
+ // Ensure that the archives tree is refreshed immediately.
+ ArchivesDialog.requeryPlaces();
+ },
+
+ /**
+ * Opens the selected nodes in tabs, using the URLs from the specified column.
+ */
+ openSelectionInTabs: function(aColumnId, aEvent) {
+ // Find the selected nodes, and exit now if no node is selected.
+ var selectedNodes = ArchivesDialog.getSelectedNodes();
+ if (!selectedNodes.length)
+ return;
+
+ // For every node in the selection
+ var urlsToOpen = [];
+ for (var [, node] in Iterator(selectedNodes)) {
+ // Find the associated URL, and add it to the list if actually specified.
+ var value = ArchivesDialog.getNodeValue(node, aColumnId);
+ if (value) {
+ urlsToOpen.push({ uri: value, isBookmark: false });
+ }
+ }
+
+ // If the operation resulted in actual URLs to be opened
+ if (urlsToOpen.length) {
+ // Open the URLs using a private function of the standard Places
+ // utilities for JavaScript.
+ PlacesUIUtils._openTabset(urlsToOpen, aEvent);
+ }
+ },
+
+ // --- Dialog state check functions ---
+
+ /**
+ * Update the label of the button that toggles the visibility of the
+ * additional archive page details.
+ */
+ checkShowMore: function() {
+ var brShowMore = document.getElementById("brShowMore");
+ var btnShowMore = document.getElementById("btnShowMore");
+ btnShowMore.setAttribute("label", btnShowMore.getAttribute(
+ brShowMore.hidden ? "morelabel" : "lesslabel"));
+ btnShowMore.setAttribute("accesskey", btnShowMore.getAttribute(
+ brShowMore.hidden ? "moreaccesskey" : "lessaccesskey"));
+ },
+
+ /**
+ * Updates the displayed information on the current selection in the tree.
+ */
+ checkPlaceInfo: function() {
+ var selectedNodes = ArchivesDialog.getSelectedNodes();
+
+ // Disable the buttons that require a selection.
+ document.getElementById("btnDelete").disabled = !selectedNodes.length;
+
+ // For all the possible columns in the archives tree
+ var column = ArchivesDialog.archivesTree.columns.getFirstColumn();
+ do {
+ // Find the elements associated with the current column.
+ var fieldName = column.id.slice("tc".length);
+ var txtValue = document.getElementById("txt" + fieldName);
+ var btnOpenTabs = document.getElementById("btnOpenTabs" + fieldName);
+
+ // Assume that the value is missing, for example if no element is
+ // selected.
+ var displayValue = "";
+ var actionDisabled = true;
+
+ // Find the first node for which a value is actually present.
+ for (var [, node] in Iterator(selectedNodes)) {
+ // Get the node value with the appropriate data type.
+ var value = ArchivesDialog.getNodeValue(node, column.id);
+ if (value) {
+ // Enable the action associated with the column.
+ actionDisabled = false;
+ if (selectedNodes.length == 1) {
+ // Display a localized long date or the plain string value.
+ displayValue = Interface.formatValueForDisplay(value, false);
+ } else {
+ // Display a placeholder for multiple nodes.
+ displayValue = txtValue.getAttribute("multivalue");
+ }
+ break;
+ }
+ }
+
+ // Update the elements.
+ txtValue.value = displayValue;
+ if (btnOpenTabs) {
+ btnOpenTabs.disabled = actionDisabled;
+ }
+ } while ((column = column.getNext()));
+ },
+
+ // --- Dialog support functions ---
+
+ /**
+ * Returns the current query result of the Places view.
+ */
+ getResult: function() {
+ var result = ArchivesDialog.archivesTree.result ||
+ ArchivesDialog.archivesTree.getResult();
+ return result.QueryInterface(Ci.nsINavHistoryResult);
+ },
+
+ /**
+ * Returns the currently selected nodes in the Places view.
+ */
+ getSelectedNodes: function() {
+ return ArchivesDialog.archivesTree.selectedNodes ||
+ ArchivesDialog.archivesTree.getSelectionNodes();
+ },
+
+ /**
+ * Returns the name of the custom annotation associated with the given tree
+ * column element, or false if the column is a standard one.
+ */
+ getColumnAnnotationName: function(aElement) {
+ var useAnnotation = (aElement.id.slice(0, "tcMaf".length) === "tcMaf");
+ return useAnnotation && aElement.getAttribute("sort");
+ },
+
+ /**
+ * Returns the value associated with the given column identifier for the
+ * provided Places result node. The JavaScript type of the returned value
+ * depends on the column. If data for the requested item is not available,
+ * null is returned.
+ */
+ getNodeValue: function(aNode, aColumnId) {
+ // If this is a standard column that is actually present in the Places view,
+ // access the associated property of the node directly.
+ if (aColumnId === "tcPlacesTitle") {
+ return aNode.title;
+ } else if (aColumnId === "tcPlacesUrl") {
+ var annotationValue = new String(aNode.uri);
+ annotationValue.isEscapedAsUri = true;
+ return annotationValue;
+ }
+ // Get a reference to the page object associated with the node. If the page
+ // is not available anymore, exit now.
+ var page = this.getNodePage(aNode);
+ if (!page) {
+ return null;
+ }
+ // Access the value of the annotation associated with the column.
+ var element = document.getElementById(aColumnId);
+ var annotationName = ArchivesDialog.getColumnAnnotationName(element);
+ return ArchiveAnnotations.getAnnotationForPage(page, annotationName);
+ },
+
+ /**
+ * Returns the ArchivePage object associated with the given Places result
+ * node, or null if the page is not cached or the URI of the Places item is
+ * not valid anymore.
+ */
+ getNodePage: function(aNode) {
+ var nodeUri;
+ try {
+ nodeUri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newURI(aNode.uri, null, null);
+ } catch (e) {
+ // Return null if the URI is invalid.
+ return null;
+ }
+ return ArchiveCache.pageFromUri(nodeUri);
+ },
+}
diff --git a/src/chrome/content/archives/archivesDialog.xul b/src/chrome/content/archives/archivesDialog.xul
new file mode 100644
index 0000000..6953dd6
--- /dev/null
+++ b/src/chrome/content/archives/archivesDialog.xul
@@ -0,0 +1,394 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/chrome/content/archives/archivesDialogBrowserOverlay.xul b/src/chrome/content/archives/archivesDialogBrowserOverlay.xul
new file mode 100644
index 0000000..72c8166
--- /dev/null
+++ b/src/chrome/content/archives/archivesDialogBrowserOverlay.xul
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/src/chrome/content/convert/Candidate.js b/src/chrome/content/convert/Candidate.js
new file mode 100644
index 0000000..701a22e
--- /dev/null
+++ b/src/chrome/content/convert/Candidate.js
@@ -0,0 +1,630 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents a saved page that can be converted from one format to another.
+ */
+function Candidate() {
+
+}
+
+Candidate.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsIWebProgressListener2,
+ Ci.nsISupportsWeakReference,
+ ]),
+
+ /**
+ * String representing the source format of the file to be converted.
+ *
+ * Possible values:
+ * "complete" - Complete web page, only if a support folder is present.
+ * "plain" - Any web page, with or without a support folder.
+ * "mhtml" - MHTML archive.
+ * "maff" - MAFF archive.
+ */
+ sourceFormat: "complete",
+
+ /**
+ * String representing the destination format of the converted file.
+ *
+ * Possible values:
+ * "maff" - MAFF archive.
+ * "mhtml" - MHTML archive.
+ * "complete" - Plain web page. A support folder is created if required.
+ */
+ destFormat: "maff",
+
+ /**
+ * String representing the relative path, with regard to the root search
+ * location, of the folder where the candidate is located.
+ */
+ relativePath: "",
+
+ /**
+ * CandidateLocation object for the main file.
+ */
+ location: null,
+
+ /**
+ * CandidateLocation object for the support folder, if applicable.
+ */
+ dataFolderLocation: null,
+
+ /**
+ * True if one of the destination files or support folders already exists.
+ */
+ obstructed: false,
+
+ /**
+ * Identifier of the candidate in the candidates data source.
+ */
+ internalIndex: 0,
+
+ /**
+ * Sets the location of the source, destination and bin files based on the
+ * given parameters and the current source and destination file formats.
+ *
+ * @param aParentLocation
+ * CandidateLocation object pointing to the parent folder that contains
+ * the candidate.
+ * @param aLeafName
+ * File name of the candidate.
+ * @param aDataFolderLeafName
+ * Name of the folder containing the support files required by the main
+ * document. If unspecified, no support folder is present.
+ */
+ setLocation: function(aParentLocation, aLeafName, aDataFolderLeafName) {
+ // Set the initial location, relevant for the source and bin paths.
+ this.relativePath = aParentLocation.relativePath;
+ this.location = aParentLocation.getSubLocation(aLeafName);
+
+ // Set the location of the source support folder, if present.
+ if (aDataFolderLeafName) {
+ this.dataFolderLocation = aParentLocation.getSubLocation(
+ aDataFolderLeafName);
+ this.dataFolderLocation.dest = null;
+ }
+
+ // Determine the correct extension for the destination file.
+ var destExtension;
+ switch (this.destFormat) {
+ case "mhtml":
+ destExtension = Prefs.saveUseMhtmlExtension ? "mhtml" : "mht";
+ break;
+ case "maff":
+ destExtension = "maff";
+ break;
+ default:
+ switch (this.sourceFormat) {
+ case "mhtml":
+ case "maff":
+ // TODO: Open the source archive and determine the extension.
+ destExtension = "html";
+ break;
+ default:
+ throw "Unexpected combination of file formats for conversion";
+ }
+ }
+
+ // Determine the base name from the provided source leaf name.
+ var leafNameWithoutExtension = aLeafName.replace(/\.[^.]*$/, "");
+
+ // Modify the destination location with the correct file name.
+ var destLeafName = leafNameWithoutExtension + "." + destExtension;
+ this.location.dest = aParentLocation.getSubLocation(destLeafName).dest;
+
+ // If the destination can be a complete web page with a support folder
+ if (this.destFormat == "complete") {
+ // The source data folder location should not be present.
+ if (this.dataFolderLocation) {
+ throw "Unexpected specified for archive source file";
+ }
+ // Determine the name of the destination support folder for data files.
+ var destFolderName = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://global/locale/contentAreaCommands.properties").
+ formatStringFromName("filesFolder", [leafNameWithoutExtension], 1);
+ // Set the data folder location, where only the destination is relevant.
+ this.dataFolderLocation = aParentLocation.getSubLocation(destFolderName);
+ this.dataFolderLocation.source = null;
+ this.dataFolderLocation.bin = null;
+ }
+ },
+
+ /**
+ * Sets the "obstructed" property based on the existence of the destination or
+ * bin files.
+ */
+ checkObstructed: function() {
+ // Assume that the destination is obstructed.
+ this.obstructed = true;
+ // Check if the destination file already exists.
+ if (this.location.dest.exists()) {
+ return;
+ }
+ // Check if the bin file already exists.
+ if (this.location.bin && this.location.bin.exists()) {
+ return;
+ }
+ // If no support folder for data files is present, exit now.
+ if (!this.dataFolderLocation) {
+ this.obstructed = false;
+ return
+ }
+ // Check if the destination support folder already exists.
+ if (this.dataFolderLocation.dest && this.dataFolderLocation.dest.exists()) {
+ return;
+ }
+ // Check if the bin support folder already exists.
+ if (this.dataFolderLocation.bin && this.dataFolderLocation.bin.exists()) {
+ return;
+ }
+ // The destination files are not already present.
+ this.obstructed = false;
+ },
+
+ /**
+ * DOM window hosting the save infrastructure required for the conversion.
+ */
+ conversionWindow: null,
+
+ /**
+ * Reference to the "iframe" element to be used for the conversion.
+ */
+ conversionFrame: null,
+
+ /**
+ * Starts the actual conversion process. When the process is finished, the
+ * given function is called, passing true if the operation succeeded, or false
+ * if the operation failed.
+ */
+ convert: function(aCompleteFn) {
+ // Store a reference to the function to be called when finished.
+ this._onComplete = aCompleteFn;
+ try {
+ // Check the destination location for obstruction before starting.
+ this._checkDestination();
+ // Register the load listeners.
+ this._addLoadListeners();
+ // Load the URL associated with the source file in the conversion frame.
+ var sourceUrl = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(this.location.source);
+ this.conversionFrame.webNavigation.loadURI(sourceUrl.spec, 0, null, null,
+ null);
+ } catch (e) {
+ // Report the error and notify the caller.
+ this._onFailure(e);
+ }
+ },
+
+ /**
+ * Cancels the currently running conversion process, if any. The finish
+ * callback function will not be called in this case.
+ */
+ cancelConversion: function() {
+ // Remember that the conversion was canceled.
+ this._canceled = true;
+ // Ensure that all the event listeners are removed immediately.
+ this._removeLoadListeners();
+ },
+
+ // nsIWebProgressListener
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Remember if at least one failure notification was received while loading
+ // or saving. This will cause the load or save to fail when finished.
+ if (aStatus != Cr.NS_OK) {
+ this._listeningException = new Components.Exception("Operation failed",
+ aStatus);
+ }
+ // Detect when the current load or save operation is finished.
+ if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
+ (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ // Notify the appropriate function based on the current state.
+ if (this._isListeningForLoad) {
+ // Notify that the network activity for the current load stopped.
+ this._loadNetworkDone = true;
+ this._onLoadCompleted();
+ } else if (this._isListeningForSave) {
+ // Notify that the save operation completed.
+ this._onSaveCompleted();
+ }
+ }
+ },
+
+ // nsIWebProgressListener
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { },
+
+ // nsIWebProgressListener
+ onLocationChange: function(aWebProgress, aRequest, aLocation) { },
+
+ // nsIWebProgressListener
+ onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) { },
+
+ // nsIWebProgressListener
+ onSecurityChange: function(aWebProgress, aRequest, aState) { },
+
+ // nsIWebProgressListener2
+ onProgressChange64: function(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { },
+
+ // nsIWebProgressListener2
+ onRefreshAttempted: function(aWebProgress, aRefreshURI, aMillis,
+ aSameURI) { },
+
+ /**
+ * Reference to the callback function to be called on completion.
+ */
+ _onComplete: null,
+
+ /**
+ * nsIWebProgress interface associated with the conversion frame.
+ */
+ _webProgress: null,
+
+ /**
+ * True while the load operation is in progress.
+ */
+ _isListeningForLoad: false,
+
+ /**
+ * Dynamically-generated listener function for the "load" event.
+ */
+ _loadListener: null,
+
+ /**
+ * True if the "load" event was fired for the conversion frame.
+ */
+ _loadContentDone: false,
+
+ /**
+ * True if the network activity for the current load stopped.
+ */
+ _loadNetworkDone: false,
+
+ /**
+ * True while the save operation is in progress.
+ */
+ _isListeningForSave: false,
+
+ /**
+ * Excpetion object representing an error that occurred during the load or
+ * save operations, or null if no error occurred.
+ */
+ _listeningException: null,
+
+ /**
+ * True if the operation was explicitly canceled.
+ */
+ _canceled: false,
+
+ /**
+ * Registers the required load listeners.
+ */
+ _addLoadListeners: function() {
+ // Get a reference to the interface to add and remove web progress
+ // listeners.
+ this._webProgress = this.conversionFrame.docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ // Build the event listener for the "load" event on the frame.
+ var self = this;
+ this._loadListener = function(aEvent) {
+ // If the current "load" event is for a subframe, ignore it.
+ if (aEvent.target != self.conversionFrame.contentDocument) {
+ return;
+ }
+ // Notify only if appropriate based on the current state.
+ if (self._isListeningForLoad) {
+ // Notify that the "load" event was fired.
+ self._loadContentDone = true;
+ self._onLoadCompleted();
+ }
+ };
+ // Register the web progress listener defined in this object.
+ this._webProgress.addProgressListener(this,
+ Ci.nsIWebProgress.NOTIFY_STATE_NETWORK);
+ // Register the load event listener defined in this object.
+ this.conversionFrame.addEventListener("load", this._loadListener, true);
+ // Set the state variables appropriately.
+ this._listeningException = null;
+ this._isListeningForLoad = true;
+ },
+
+ /**
+ * Removes the load listeners registered previously, if necessary.
+ */
+ _removeLoadListeners: function() {
+ // Check the current state before continuing.
+ if (!this._isListeningForLoad) {
+ return;
+ }
+ // Remove the web progress listener defined in this object.
+ this._webProgress.removeProgressListener(this);
+ // Remove the load event listener defined in this object.
+ this.conversionFrame.removeEventListener("load", this._loadListener, true);
+ // Set the state variables appropriately.
+ this._isListeningForLoad = false;
+ },
+
+ /**
+ * Called when the source page has been loaded.
+ */
+ _onLoadCompleted: function() {
+ // Wait for both triggering conditions be true.
+ if (!this._loadNetworkDone || !this._loadContentDone) {
+ return;
+ }
+ try {
+ // Remove the load listeners first.
+ this._removeLoadListeners();
+
+ // Report any error that occurred while loading, and stop the operation.
+ if (this._listeningException) {
+ throw this._listeningException;
+ }
+
+ // We must wait for all events to be processed before continuing,
+ // otherwise the conversion of some pages might fail because some elements
+ // in the page are not available for saving, or the current load can
+ // interfere with subsequent loads in the same frame.
+ this._waitForAllEventsStart = Date.now();
+ this._waitForAllEvents();
+ } catch (e) {
+ // Report the error and notify the caller.
+ this._onFailure(e);
+ }
+ },
+
+ /**
+ * Point in time when the current wait for all events started.
+ */
+ _waitForAllEventsStart: null,
+
+ /**
+ * Wait for pending events to be dispatched before continuing.
+ */
+ _waitForAllEvents: function() {
+ if (Date.now() > this._waitForAllEventsStart + 5000) {
+ // On timeout, continue even though not all events have been processed.
+ this._reportConversionError("Unable to process all events generated by" +
+ " the source page in a timely manner. Your computer might be busy." +
+ " The conversion operation will be tried anyway.");
+ this._afterLoadCompleted();
+ return;
+ }
+
+ // If there are pending events, process them and retry later.
+ if (this._mainThread.hasPendingEvents())
+ {
+ var self = this;
+ this._mainThread.dispatch(
+ { run: function() self._waitForAllEvents.apply(self) },
+ Ci.nsIThread.DISPATCH_NORMAL);
+ return;
+ }
+
+ // All events have been processed, end waiting.
+ this._afterLoadCompleted();
+ },
+
+ /**
+ * Called after the source page has been loaded and events processed.
+ */
+ _afterLoadCompleted: function() {
+ try {
+ // Check if the operation was canceled while processing the events.
+ if (this._canceled) {
+ return;
+ }
+
+ // Check the destination location for obstruction again.
+ this._checkDestination();
+ // Ensure that the destination folder exists, and create it if required.
+ if (!this.location.dest.parent.exists()) {
+ this.location.dest.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+ }
+
+ // Start the save operation.
+ this._startSaving();
+ } catch (e) {
+ // Report the error and notify the caller.
+ this._onFailure(e);
+ }
+ },
+
+ /**
+ * Starts the save operation, which is the second step of the conversion.
+ */
+ _startSaving: function() {
+ // Select the save behavior that is appropriate for the destination format.
+ var saveBehavior;
+ switch (this.destFormat) {
+ case "mhtml":
+ saveBehavior = this.conversionWindow.gMafMhtmlSaveBehavior;
+ break;
+ case "maff":
+ saveBehavior = this.conversionWindow.gMafMaffSaveBehavior;
+ break;
+ default:
+ saveBehavior =
+ this.conversionWindow.MozillaArchiveFormat.CompleteSaveBehavior;
+ }
+ // Set the state variables appropriately before starting the save operation.
+ this._listeningException = null;
+ this._isListeningForSave = true;
+ try {
+ // Use the global saveDocument function with the special MAF parameters.
+ this.conversionWindow.saveDocument(
+ this.conversionFrame.contentDocument, {
+ targetFile: this.location.dest,
+ saveBehavior: saveBehavior,
+ mafProgressListener: this
+ });
+ } catch (e) {
+ // If the operation failed before starting, reset the listening state.
+ this._isListeningForSave = false;
+ throw e;
+ }
+ },
+
+ /**
+ * Called when the source page has been saved.
+ */
+ _onSaveCompleted: function() {
+ try {
+ // Indicate that the save notification has been processed.
+ this._isListeningForSave = false;
+
+ // Check if the operation was canceled while saving.
+ if (this._canceled) {
+ return;
+ }
+
+ // Report any error that occurred while saving, and stop the operation.
+ if (this._listeningException) {
+ throw this._listeningException;
+ }
+
+ // Change the last modified time of the destination to match the source.
+ this.location.dest.lastModifiedTime =
+ this.location.source.lastModifiedTime;
+
+ // Conversion completed successfully, move the source to the bin folder.
+ this._moveToBin();
+ } catch (e) {
+ // Report the error and notify the caller, then exit.
+ this._onFailure(e);
+ return;
+ }
+ // Report that the conversion was successful.
+ this._onComplete(true);
+ },
+
+ /**
+ * Throws an exception if the destination location is obstructed.
+ */
+ _checkDestination: function() {
+ // Ensure that the destination file does not exist.
+ if (this.location.dest.exists()) {
+ throw new Components.Exception(
+ "The destination location is unexpectedly obstructed.");
+ }
+ // Ensure that the destination support folder does not exist.
+ if (this.dataFolderLocation && this.dataFolderLocation.dest &&
+ this.location.dest.exists()) {
+ throw new Components.Exception(
+ "The destination location is unexpectedly obstructed.");
+ }
+ },
+
+ /**
+ * Moves the source file and support folder to the bin folder, if required.
+ */
+ _moveToBin: function() {
+ // Move the source file to the bin folder.
+ if (this.location.bin) {
+ // Ensure that the destination does not exist.
+ if (this.location.bin.exists()) {
+ throw new Components.Exception(
+ "The bin location is unexpectedly obstructed.");
+ }
+ // Move the file as required.
+ this.location.source.moveTo(this.location.bin.parent,
+ this.location.bin.leafName);
+ }
+ // Move the source support folder, if present, to the bin folder.
+ if (this.dataFolderLocation) {
+ if (this.dataFolderLocation.source && this.dataFolderLocation.bin) {
+ // Ensure that the destination does not exist.
+ if (this.dataFolderLocation.bin.exists()) {
+ throw new Components.Exception(
+ "The bin location is unexpectedly obstructed.");
+ }
+ // Move the folder as required.
+ this.dataFolderLocation.source.moveTo(
+ this.dataFolderLocation.bin.parent,
+ this.dataFolderLocation.bin.leafName);
+ }
+ }
+ },
+
+ /**
+ * Reports the given exception that occurred during the conversion of this
+ * candidate, and notifies the appropriate object that the operation failed.
+ */
+ _onFailure: function(aException) {
+ try {
+ // Clean up all the possible registered listeners.
+ this._removeLoadListeners();
+ } catch (e) {
+ // Ignore errors during the cleanup phase.
+ Cu.reportError(e);
+ }
+ // Report the error message.
+ this._reportConversionError(aException);
+ // Report that the conversion of this candidate failed.
+ this._onComplete(false);
+ },
+
+ /**
+ * Reports the given exception that occurred during the conversion of this
+ * candidate, providing additional information about the error.
+ */
+ _reportConversionError: function(aException) {
+ try {
+ // Determine the first part of the message for the Error Console.
+ var messagePrefix = "The following error occurred while converting\n" +
+ this.location.source.path + ":\n\n";
+ // Report the complete message appropriately.
+ if (aException instanceof Ci.nsIXPCException) {
+ Cu.reportError(new Components.Exception(messagePrefix +
+ aException.message, aException.result, aException.location,
+ aException.data, aException.inner));
+ } else {
+ Cu.reportError(messagePrefix + aException);
+ }
+ } catch (e) {
+ // In case of errors, report only the original exception.
+ Cu.reportError(aException);
+ }
+ },
+
+ _mainThread: Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager).mainThread,
+}
diff --git a/src/chrome/content/convert/CandidateFinder.js b/src/chrome/content/convert/CandidateFinder.js
new file mode 100644
index 0000000..86ce80d
--- /dev/null
+++ b/src/chrome/content/convert/CandidateFinder.js
@@ -0,0 +1,280 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Locates the saved web pages that are candidates for batch conversion between
+ * different file formats.
+ */
+function CandidateFinder() {
+ // Initialize the contained objects.
+ this.location = new CandidateLocation();
+ // Initialize the list of valid suffixes for the support folders.
+ this.sourceDataFolderSuffixes = Prefs.convertDataFolderSuffixesArray;
+ // Add the files folder suffix for the current locale, if not the default.
+ var localizedFolderSuffix = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://global/locale/contentAreaCommands.properties").
+ formatStringFromName("filesFolder", [""], 1);
+ if (localizedFolderSuffix && localizedFolderSuffix != "_files") {
+ this.sourceDataFolderSuffixes.push(localizedFolderSuffix);
+ }
+}
+
+CandidateFinder.prototype = {
+ /**
+ * String representing the source format of the files to be converted.
+ *
+ * Possible values:
+ * "complete" - Complete web page, only if a support folder is present.
+ * "plain" - Any web page, with or without a support folder.
+ * "mhtml" - MHTML archive.
+ * "maff" - MAFF archive.
+ */
+ sourceFormat: "complete",
+
+ /**
+ * String representing the destination format of the converted files.
+ *
+ * Possible values:
+ * "maff" - MAFF archive.
+ * "mhtml" - MHTML archive.
+ * "complete" - Plain web page. A support folder is created if required.
+ */
+ destFormat: "maff",
+
+ /**
+ * CandidateLocation object representing the root directories involved in the
+ * conversion operation.
+ */
+ location: null,
+
+ /**
+ * True if the subfolders of the source folder must be sought too.
+ */
+ sourceIncludeSubfolders: true,
+
+ /**
+ * Array containing the suffixes used for recognizing the support folders in
+ * the source tree, for example "_files".
+ */
+ sourceDataFolderSuffixes: [],
+
+ /**
+ * Returns true if the values in the "sourceFormat" and "destFormat"
+ * properties are consistent.
+ */
+ validateFormats: function() {
+ // The "plain" and "complete" values indicate the same file format.
+ var effectiveSourceFormat =
+ (this.sourceFormat == "plain" ? "complete" : this.sourceFormat);
+ return (effectiveSourceFormat != this.destFormat);
+ },
+
+ /**
+ * This iterator yields the Candidate objects corresponding to the convertible
+ * files under the root search location. Sometimes a null value will be
+ * returned instead of a candidate to allow the caller to keep the user
+ * interface responsive while the search is in progress.
+ */
+ __iterator__: function() {
+ // Delegate the generation to the parameterized worker.
+ for (var item in this._candidatesGenerator(this.location)) {
+ yield item;
+ }
+ },
+
+ /**
+ * This generator function yields the Candidate objects corresponding to the
+ * convertible files under the specified location. Sometimes a null value will
+ * be returned instead of a candidate to allow the caller to keep the user
+ * interface responsive while the search is in progress.
+ */
+ _candidatesGenerator: function(aLocation) {
+ // Enumerate all the files and subdirectories in the specified directory,
+ // and generate three separate lists: one for folder names, one for file
+ // names, and a string containing the concatenation of all the file names,
+ // for faster access when searching for a particular file name in folders
+ // containing many files.
+ var dirEntries = aLocation.source.directoryEntries;
+ var subdirs = {};
+ var files = {};
+ var filesList = "::";
+ while (dirEntries.hasMoreElements()) {
+ var dirEntry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+ try {
+ // Add the entry to the appropriate lists.
+ if (dirEntry.isDirectory()) {
+ subdirs[dirEntry.leafName] = true;
+ } else {
+ files[dirEntry.leafName] = true;
+ filesList += dirEntry.leafName + "::";
+ }
+ } catch (e if (e instanceof Ci.nsIException && e.result ==
+ Cr.NS_ERROR_FILE_NOT_FOUND)) {
+ // In rare cases, invalid file names may generate this exception when
+ // checking isDirectory, even if they were returned by the iterator.
+ }
+ // Avoid blocking the user interface while scanning crowded folders.
+ yield null;
+ }
+
+ // Examine every available subfolder.
+ for (var [subdirName] in Iterator(subdirs)) {
+ // Ensure that the enumeration result is a JavaScript string.
+ subdirName = "" + subdirName;
+ // If the subfolder is a support folder for an existing web page
+ var name = this._isSupportFolderName(subdirName, filesList);
+ if (name) {
+ // If the search should include web pages among the source files
+ if (this.sourceFormat == "complete" || this.sourceFormat == "plain") {
+ // Check that the associated source file has not been already used
+ // together with another support folder.
+ if (files[name]) {
+ // Generate a new candidate for conversion.
+ yield this._newCandidate(aLocation, name, subdirName);
+ // Ensure that the file will not be used again as a candidate later.
+ delete files[name];
+ }
+ }
+ } else if (this.sourceIncludeSubfolders) {
+ // If required, examine the contents of this subfolder recursively. The
+ // contents of support folders for data files are never examined, even
+ // if the folder is not returned as a candidate for conversion.
+ var newLocation = aLocation.getSubLocation(subdirName);
+ for (var item in this._candidatesGenerator(newLocation)) {
+ yield item;
+ }
+ }
+ }
+
+ // Examine every remaining file.
+ for (var [fileName] in Iterator(files)) {
+ // Ensure that the enumeration result is a JavaScript string.
+ fileName = "" + fileName;
+ // If the file name matches the criteria
+ if (this._isSourceFileName(fileName)) {
+ // Generate a new candidate for conversion.
+ yield this._newCandidate(aLocation, fileName);
+ }
+ }
+ },
+
+ /**
+ * Creates a new candidate with the given properties.
+ */
+ _newCandidate: function(aParentLocation, aLeafName, aDataFolderLeafName) {
+ // Create a Candidate object for the requested file formats.
+ var candidate = new Candidate();
+ candidate.sourceFormat = this.sourceFormat;
+ candidate.destFormat = this.destFormat;
+ // Set the actual file names based on the file formats.
+ candidate.setLocation(aParentLocation, aLeafName, aDataFolderLeafName);
+ // Check if the destination or bin files already exist.
+ candidate.checkObstructed();
+ // Return the newly generated candidate.
+ return candidate;
+ },
+
+ /**
+ * Checks the extension in the given file name and returns true if it matches
+ * the selected source format.
+ */
+ _isSourceFileName: function(aLeafName) {
+ // Checks the extension case-insensitively.
+ switch (this.sourceFormat) {
+ case "plain":
+ return /\.(x?html|xht|htm|xml|svgz?)$/i.test(aLeafName);
+ case "mhtml":
+ return /\.mht(ml)?$/i.test(aLeafName);
+ case "maff":
+ return /\.maff$/i.test(aLeafName);
+ default:
+ return false;
+ }
+ },
+
+ /**
+ * Returns true if the given directory name contains the data files of an
+ * existing complete web page. The aFilesList parameter is a string containing
+ * the concatenation of all the files.
+ */
+ _isSupportFolderName: function(aLeafName, aFilesList) {
+ // Try with all the possible suffixes in order.
+ for (var [, suffix] in Iterator(this.sourceDataFolderSuffixes)) {
+ // Checks the suffix case-sensitively.
+ if (aLeafName.slice(-suffix.length) != suffix) {
+ continue;
+ }
+ // Extract the base folder name without the suffix.
+ var basePart = aLeafName.slice(0, -suffix.length);
+ if (!basePart) {
+ continue;
+ }
+ // Look into the provided list of file names to find the associated file.
+ var endPosition = 0;
+ var foundFileName = false;
+ while (true) {
+ // Search case-sensitively for a file name that begins with the base
+ // name obtained from the support folder name.
+ var position = aFilesList.indexOf("::" + basePart, endPosition);
+ if (position < 0) {
+ break;
+ }
+ // A file name was found, extract it from the list.
+ var startPosition = position + "::".length;
+ endPosition = aFilesList.indexOf("::", startPosition);
+ var fileName = aFilesList.slice(startPosition, endPosition);
+ var lastPart = fileName.slice(basePart.length);
+ // Ensure that the base name is the entire name or is followed by a dot.
+ if (lastPart && lastPart[0] != ".") {
+ continue;
+ }
+ // A file name that can be associated with the folder was found.
+ foundFileName = fileName;
+ // Give priority to names that match one of the known extensions.
+ if (/\.(x?html|xht|htm|xml|svgz?)$/i.test(lastPart)) {
+ return foundFileName;
+ }
+ }
+ // Either a comaptible file name was not found, or a file name that does
+ // not match one of the known extensions was found.
+ return foundFileName;
+ }
+ // The given name is not one of a support folder.
+ return false;
+ },
+}
diff --git a/src/chrome/content/convert/CandidateLocation.js b/src/chrome/content/convert/CandidateLocation.js
new file mode 100644
index 0000000..6097ff9
--- /dev/null
+++ b/src/chrome/content/convert/CandidateLocation.js
@@ -0,0 +1,86 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents a source, destination and bin location in the file system.
+ */
+function CandidateLocation() {
+
+}
+
+CandidateLocation.prototype = {
+ /**
+ * String representing the relative path with regard to the root location.
+ */
+ relativePath: "",
+
+ /**
+ * nsIFile representing the source file or folder. This may be null for
+ * support folders location representing a destination path only.
+ */
+ source: null,
+
+ /**
+ * nsIFile representing the destination file or folder. This may be the same
+ * path or object as the source file.
+ */
+ dest: null,
+
+ /**
+ * nsIFile representing the place where the source will be moved after a
+ * successful conversion. This must be the null if moving is not required.
+ */
+ bin: null,
+
+ /**
+ * Returns a new CandidateLocation object representing a subfolder or a file
+ * located under the current location.
+ */
+ getSubLocation: function(aLeafName) {
+ var newLocation = new CandidateLocation();
+ newLocation.relativePath = this.relativePath + aLeafName + "/";
+ newLocation.source = this.source.clone();
+ newLocation.source.append(aLeafName);
+ newLocation.dest = this.dest.clone();
+ newLocation.dest.append(aLeafName);
+ if (this.bin) {
+ newLocation.bin = this.bin.clone();
+ newLocation.bin.append(aLeafName);
+ }
+ return newLocation;
+ },
+}
diff --git a/src/chrome/content/convert/CandidatesDataSource.js b/src/chrome/content/convert/CandidatesDataSource.js
new file mode 100644
index 0000000..d9db670
--- /dev/null
+++ b/src/chrome/content/convert/CandidatesDataSource.js
@@ -0,0 +1,265 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Provides an RDF data source that represents the files that are candidates for
+ * being converted to a different format. For each candidate, a selection state
+ * is available, along with other state properties.
+ *
+ * This class derives from DataSourceWrapper. See the DataSourceWrapper
+ * documentation for details.
+ */
+function CandidatesDataSource(aBrowserWindow) {
+ // Construct the base class wrapping an in-memory RDF data source.
+ DataSourceWrapper.call(this,
+ Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource));
+
+ // Initialize the data.
+ this.candidates = [];
+ this.counts = {
+ total: 0,
+ checked: 0,
+ obstructed: 0,
+ converted: 0,
+ failed: 0,
+ };
+ this._initDataSource();
+}
+
+CandidatesDataSource.prototype = {
+ __proto__: DataSourceWrapper.prototype,
+
+ /**
+ * Note: These strings are converted to actual RDF resources by the base class
+ * as soon as this data source is constructed, so GetResource must not be
+ * called. See the DataSourceWrapper documentation for details.
+ */
+ resources: {
+ // Subjects and objects
+ root: "urn:root",
+ candidates: "urn:maf:candidates",
+ // Standard predicates
+ instanceOf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#instanceOf",
+ child: "http://home.netscape.com/NC-rdf#child",
+ // Custom predicates representing candidate properties
+ internalIndex: "urn:maf:vocabulary#internalIndex",
+ sourceName: "urn:maf:vocabulary#sourceName",
+ sourceDataFolderName: "urn:maf:vocabulary#sourceDataFolderName",
+ relativePath: "urn:maf:vocabulary#relativePath",
+ sourcePath: "urn:maf:vocabulary#sourcePath",
+ destPath: "urn:maf:vocabulary#destPath",
+ binPath: "urn:maf:vocabulary#binPath",
+ // Custom predicates representing candidate state properties
+ checked: "urn:maf:vocabulary#checked",
+ disabled: "urn:maf:vocabulary#disabled",
+ obstructed: "urn:maf:vocabulary#obstructed",
+ enqueued: "urn:maf:vocabulary#enqueued",
+ converting: "urn:maf:vocabulary#converting",
+ converted: "urn:maf:vocabulary#converted",
+ failed: "urn:maf:vocabulary#failed",
+ },
+
+ /**
+ * Getter for an RDF resource representing a candidate.
+ */
+ resourceForCandidate: function(aIndex) {
+ return this._rdf.GetResource("urn:maf:candidate#" + aIndex);
+ },
+
+ /**
+ * Actual Candidate objects associated with this data source.
+ */
+ candidates: [],
+
+ /**
+ * Provides properties that keep count of how many items have a property set.
+ */
+ counts: {},
+
+ /**
+ * This is used by other objects to determine if selection can be changed.
+ */
+ selectionDisabled: false,
+
+ // nsIRDFDataSource
+ Change: function(aSource, aProperty, aOldTarget, aNewTarget) {
+ // Propagate the change to the wrapped object.
+ this._wrappedObject.Change(aSource, aProperty, aOldTarget, aNewTarget);
+
+ // If a property of a candidate changed
+ if (aSource != this.resources.candidates) {
+ // Find the name of the countable property associated with the resource.
+ for (var [, propertyName] in
+ Iterator(["checked", "obstructed", "converted", "failed"])) {
+ if (aProperty == this.resources[propertyName]) {
+ // Update the associated counter appropriately.
+ if (aNewTarget.Value != aOldTarget.Value) {
+ // False may be expressed with an empty string or the word "false".
+ if (aNewTarget.Value == "" || aNewTarget.Value == "false") {
+ this.counts[propertyName]--;
+ } else {
+ this.counts[propertyName]++;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // Continue only if the "checked" property has changed.
+ if (aProperty != this.resources.checked) {
+ return;
+ }
+
+ // If the selection change is on a container, update the child elements.
+ if (aSource == this.resources.candidates) {
+ var candidateSequence = this._rdfSequence(aSource);
+ var candidateEnum = candidateSequence.GetElements();
+ while (candidateEnum.hasMoreElements()) {
+ var candidateResource = candidateEnum.getNext();
+ // If the item is not disabled
+ if (!this.getLiteralValue(candidateResource, this.resources.disabled)) {
+ // Change the selection on the element and update the counters.
+ this.replaceLiteral(candidateResource, this.resources.checked,
+ aNewTarget.Value, true);
+ }
+ }
+ } else {
+ // If the selection change is on a child element, update the container.
+ var candidatesResource = this.resources.candidates;
+ var allCandidatesSelected = (this.counts.checked == this.counts.total);
+ // Change the selection on the element, by removing the assertion that is
+ // no longer true and adding the new assertion.
+ this._wrappedObject.Assert(candidatesResource, this.resources.checked,
+ this._rdfBool(allCandidatesSelected), true);
+ this._wrappedObject.Unassert(candidatesResource, this.resources.checked,
+ this._rdfBool(!allCandidatesSelected));
+ }
+ },
+
+ /**
+ * Initializes the data source with the basic data needed to host candidates.
+ */
+ _initDataSource: function() {
+ // Shorthand for objects commonly used throughout this function.
+ var ds = this._wrappedObject;
+ var res = this.resources;
+
+ // Create the root of the tree, that has a single child pointing to the
+ // list of candidates. This is required for properly handling the recursive
+ // XUL template generation that is used to create XUL trees.
+ ds.Assert(res.root, res.instanceOf, res.root, true);
+ ds.Assert(res.root, res.child, res.candidates, true);
+
+ // Create the "candidates" resource, which is an RDF container.
+ var candidatesSequence = this._rdfSequence(res.candidates);
+ ds.Assert(res.candidates, res.instanceOf, res.candidates, true);
+
+ // Set additional properties of the "candidates" resource.
+ ds.Assert(res.candidates, res.checked, this._rdfBool(true), true);
+ ds.Assert(res.candidates, res.disabled, this._rdf.GetLiteral(""), true);
+ ds.Assert(res.candidates, res.obstructed, this._rdf.GetLiteral(""), true);
+ ds.Assert(res.candidates, res.enqueued, this._rdf.GetLiteral(""), true);
+ ds.Assert(res.candidates, res.converting, this._rdf.GetLiteral(""), true);
+ ds.Assert(res.candidates, res.converted, this._rdf.GetLiteral(""), true);
+ ds.Assert(res.candidates, res.failed, this._rdf.GetLiteral(""), true);
+ },
+
+ /**
+ * Adds a candidate to the data source.
+ */
+ addCandidate: function(aCandidate) {
+ // Shorthand for objects commonly used throughout this function.
+ var ds = this._wrappedObject;
+ var res = this.resources;
+
+ // Determine the index of the new candidate and retrieve its RDF resource.
+ var candidateIndex = this.candidates.length;
+ var candidateResource = this.resourceForCandidate(candidateIndex);
+
+ // Set the internal index in the array as an RDF integer.
+ ds.Assert(candidateResource, res.internalIndex,
+ this._rdf.GetIntLiteral(candidateIndex), true);
+
+ // Set the candidate properties as RDF literals.
+ ds.Assert(candidateResource, res.sourceName,
+ this._rdf.GetLiteral(aCandidate.location.source.leafName), true);
+ if (aCandidate.dataFolderLocation && aCandidate.dataFolderLocation.source) {
+ ds.Assert(candidateResource, res.sourceDataFolderName,
+ this._rdf.GetLiteral(aCandidate.dataFolderLocation.source.leafName),
+ true);
+ }
+ ds.Assert(candidateResource, res.relativePath,
+ this._rdf.GetLiteral(aCandidate.relativePath), true);
+ ds.Assert(candidateResource, res.sourcePath,
+ this._rdf.GetLiteral(aCandidate.location.source.path), true);
+ ds.Assert(candidateResource, res.destPath,
+ this._rdf.GetLiteral(aCandidate.location.dest.path), true);
+ if (aCandidate.location.bin) {
+ ds.Assert(candidateResource, res.binPath,
+ this._rdf.GetLiteral(aCandidate.location.bin.path), true);
+ }
+
+ // Set the candidate state properties as RDF literals. Candidates that have
+ // already been converted are disabled.
+ ds.Assert(candidateResource, res.checked, this._rdfBool(true), true);
+ ds.Assert(candidateResource, res.disabled, this._rdf.GetLiteral(
+ aCandidate.obstructed ? "disabled" : ""), true);
+ ds.Assert(candidateResource, res.obstructed, this._rdf.GetLiteral(
+ aCandidate.obstructed ? "obstructed" : ""), true);
+ ds.Assert(candidateResource, res.enqueued, this._rdf.GetLiteral(""), true);
+ ds.Assert(candidateResource, res.converting, this._rdf.GetLiteral(""),
+ true);
+ ds.Assert(candidateResource, res.converted, this._rdf.GetLiteral(""), true);
+ ds.Assert(candidateResource, res.failed, this._rdf.GetLiteral(""), true);
+
+ // Add the "candidate" resource to the parent container.
+ this._rdfSequence(res.candidates).AppendElement(candidateResource);
+
+ // Update the counts appropriately.
+ this.counts.total++;
+ this.counts.checked++;
+ if (aCandidate.obstructed) {
+ this.counts.obstructed++;
+ }
+
+ // Save the internal index of the candidate and add the item to the array.
+ aCandidate.internalIndex = candidateIndex;
+ this.candidates.push(aCandidate);
+ },
+}
diff --git a/src/chrome/content/convert/convertDialog.js b/src/chrome/content/convert/convertDialog.js
new file mode 100644
index 0000000..4d3db82
--- /dev/null
+++ b/src/chrome/content/convert/convertDialog.js
@@ -0,0 +1,633 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("chrome://mza/content/MozillaArchiveFormat.jsm");
+
+/**
+ * Handles the saved pages conversion dialog.
+ */
+var ConvertDialog = {
+ /**
+ * The main wizard element.
+ */
+ _wizard: null,
+
+ /**
+ * The candidates tree element.
+ */
+ _candidatesTree: null,
+
+ /**
+ * The customized view for the candidates tree. The purpose of this property
+ * is to keep a reference to the customized JavaScript object implementing
+ * the view. If this explicit reference is not kept, the tree view can lose
+ * its customizations during garbage collection.
+ */
+ _candidatesTreeView: null,
+
+ /**
+ * The data source for the candidates tree view, or null if the candidates
+ * page has not been reached yet. This data source will be populated with the
+ * results provided by the CandidateFinder worker object.
+ */
+ _candidatesDataSource: null,
+
+ /**
+ * Localized templates with the selection count for the candidates tree.
+ */
+ _searchingCountsText: "",
+ _convertingCountsText: "",
+
+ /**
+ * Original label and access key for the wizard's Finish button.
+ */
+ _wizardFinishLabel: "",
+ _wizardFinishAccessKey: "",
+
+ /**
+ * The CandidateFinder worker object. The properties of this object will be
+ * set by various pages of the wizard.
+ */
+ _finder: null,
+
+ /**
+ * The AsyncEnumerator object linked to the CandidateFinder worker object, or
+ * null if the search for source files is not running.
+ */
+ _finderEnumerator: null,
+
+ /**
+ * The AsyncEnumerator object for the candidates conversion process, or null
+ * if the conversion process is not running.
+ */
+ _conversionEnumerator: null,
+
+ /**
+ * The candidate that is being converted.
+ */
+ _currentCandidate: null,
+
+ /**
+ * Set to true when the conversion process has finished. In this case,
+ * clicking the finish button will close the window.
+ */
+ _conversionFinished: false,
+
+ /**
+ * Create the elements to be displayed.
+ */
+ onLoadDialog: function() {
+ // Initialize the member variables.
+ this._wizard = document.getElementById("convertDialog");
+ this._candidatesTree = document.getElementById("treeCandidates");
+ this._finder = new CandidateFinder();
+
+ // The main form contains some labels that mention the wizard buttons. Since
+ // the actual text of the wizard buttons depends on the current platform,
+ // the text is retrieved dynamically and replaced in all the labels that
+ // require it.
+ for (var [, [labelName, buttonName]] in Iterator([
+ ["lblIntroductionContinue", "next"],
+ ["lblFormatsContinue", "next"],
+ ["lblFoldersSourceContinue", "next"],
+ ["lblFoldersDestContinue", "next"],
+ ["lblCandidatesNone", "back"],
+ ["lblCandidatesFinish", "finish"],
+ ])) {
+ // Replace "$1" with the current label of the correct wizard button.
+ var labelElement = document.getElementById(labelName);
+ labelElement.value = labelElement.value.replace(/\$1/g,
+ this._getWizardButtonLabel(buttonName));
+ }
+
+ // Retrieve the text of some controls and store it for later use.
+ this._searchingCountsText = document.
+ getElementById("descCandidatesCounts").getAttribute("valuesearching");
+ this._convertingCountsText = document.
+ getElementById("descCandidatesCounts").getAttribute("valueconverting");
+ this._wizardFinishLabel = this._wizard.getButton("finish").label;
+ this._wizardFinishAccessKey = this._wizard.getButton("finish").
+ getAttribute("accesskey");
+
+ // Store a reference to the tree view to prevent it from losing its
+ // customizations during garbage collection.
+ this._candidatesTreeView = this._candidatesTree.view;
+
+ // When the checkbox column is modified, the setCellValue function of the
+ // tree view is called, with aValue set to either the string "true" or
+ // "false". This implementation propagates the change to the underlying data
+ // source, if the checkbox for the requested item is enabled.
+ var self = this;
+ this._candidatesTreeView.setCellValue = function(aRow, aCol, aValue) {
+ if (aCol.id == "tcChecked") {
+ // Ensure that the data source is available and modifiable.
+ var ds = self._candidatesDataSource;
+ if (!ds || ds.selectionDisabled) {
+ return;
+ }
+ // Check if the selection state of the selected item is modifiable.
+ var resource = self._candidatesTreeView.getResourceAtIndex(aRow);
+ if (ds.getLiteralValue(resource, ds.resources.disabled)) {
+ return;
+ }
+ // Execute the requested change.
+ ds.replaceLiteral(resource, ds.resources.checked, aValue);
+ // Update the dialog buttons.
+ self.checkCandidatesControls();
+ }
+ };
+
+ // In order to prevent the conversion process from blocking, disable the
+ // content features that may cause dialogs to be displayed, for example
+ // message boxes put up by embedded JavaScript.
+ var conversionDocShell = document.getElementById("frmConvert").docShell;
+ conversionDocShell.allowAuth = false;
+ conversionDocShell.allowJavascript = false;
+ conversionDocShell.allowPlugins = false;
+ },
+
+ /**
+ * On the introduction page, the button to advance to the next page is always
+ * enabled, and is selected instead of the website link.
+ */
+ checkIntroductionControls: function() {
+ document.getElementById("convertDialog").canAdvance = true;
+ document.getElementById("convertDialog").getButton("next").focus();
+ },
+
+ /**
+ * Copies the data from the controls in the "formats" wizard page to the
+ * worker object, and prevents advancing to the next page if the data is
+ * invalid.
+ */
+ checkFormatsControls: function() {
+ // Copy the data from the controls to the worker object.
+ this._finder.sourceFormat =
+ document.getElementById("rgrFormatsSource").value;
+ this._finder.destFormat =
+ document.getElementById("rgrFormatsDest").value;
+
+ // Ask the worker object if the data is valid.
+ var pageIsValid = this._finder.validateFormats();
+
+ // Show the appropriate controls based on the validation results.
+ document.getElementById("lblFormatsInvalid").hidden = pageIsValid;
+ document.getElementById("lblFormatsContinue").hidden = !pageIsValid;
+ this._wizard.canAdvance = pageIsValid;
+ },
+
+ /**
+ * Copies the data from the controls in the "source folders" wizard page to
+ * the worker object, and prevents advancing to the next page if the data is
+ * invalid.
+ */
+ checkFoldersSourceControls: function() {
+ // Validate the data from the controls and copy it to the worker object.
+ var pageIsValid = true;
+ try {
+ // Set the source folder, that must exist.
+ this._finder.location.source =
+ this._getFolderFromTextbox("txtFoldersSource", true);
+ } catch(e) {
+ // If an exception is thrown, the source folder is missing or invalid.
+ pageIsValid = false;
+ }
+ this._finder.sourceIncludeSubfolders =
+ document.getElementById("chkFoldersSourceSubfolders").checked;
+
+ // Show the appropriate controls based on the validation results.
+ document.getElementById("lblFoldersSourceInvalid").hidden = pageIsValid;
+ document.getElementById("lblFoldersSourceContinue").hidden = !pageIsValid;
+ this._wizard.canAdvance = pageIsValid;
+ },
+
+ /**
+ * Copies the data from the controls in the "destination folders" wizard page
+ * to the worker object, and prevents advancing to the next page if the data
+ * is invalid.
+ */
+ checkFoldersDestControls: function() {
+ // Check the status of the controls for the destination and bin folders.
+ var useDestFolder =
+ (document.getElementById("rgrFoldersDest").value == "folder");
+ var useBinFolder =
+ (document.getElementById("rgrFoldersBin").value == "folder");
+ document.getElementById("txtFoldersDest").disabled = !useDestFolder;
+ document.getElementById("btnFoldersDest").disabled = !useDestFolder;
+ document.getElementById("txtFoldersBin").disabled = !useBinFolder;
+ document.getElementById("btnFoldersBin").disabled = !useBinFolder;
+
+ // Validate the data from the controls and copy it to the worker object.
+ var pageIsValid = true;
+ try {
+ // Set the destination folder equal to the source folder if required.
+ this._finder.location.dest = (useDestFolder ?
+ this._getFolderFromTextbox("txtFoldersDest") :
+ this._finder.location.source);
+ // Set the bin folder only if required.
+ this._finder.location.bin = (useBinFolder ?
+ this._getFolderFromTextbox("txtFoldersBin") :
+ null);
+ } catch(e) {
+ // If an exception is thrown, at least one folder is invalid.
+ pageIsValid = false;
+ }
+
+ // Show the appropriate controls based on the validation results.
+ document.getElementById("lblFoldersDestInvalid").hidden = pageIsValid;
+ document.getElementById("lblFoldersDestContinue").hidden = !pageIsValid;
+ this._wizard.canAdvance = pageIsValid;
+ },
+
+ /**
+ * Determines the proper status of the controls when the "candidates" wizard
+ * page is displayed, based on the current state of the worker objects.
+ */
+ checkCandidatesControls: function() {
+ // The page can be in one of the following states:
+ // - Searching for candidates (searching)
+ // - Selecting which candidates should be converted (selecting)
+ // - Converting the selected candidates (converting)
+ // - Displaying the conversion results (finished)
+ var searching = !!this._finderEnumerator;
+ var converting = !!this._conversionEnumerator;
+ var finished = this._conversionFinished;
+ var selecting = !searching && !converting && !finished;
+
+ // Update the label with the counts related to the candidates tree.
+ var counts = this._candidatesDataSource.counts;
+ // Since candidates that have been already converted are always selected,
+ // they must be subtracted from the selected items count to determine how
+ // many candidates should be converted.
+ var selectableCount = counts.total - counts.obstructed;
+ var selectedCount = counts.checked - counts.obstructed;
+ // If candidate conversion hasn't started yet, show only the counts related
+ // to candidate selection, otherwise show all the available counts.
+ var countsText = (searching || selecting) ? this._searchingCountsText :
+ this._convertingCountsText;
+ document.getElementById("descCandidatesCounts").firstChild.nodeValue =
+ countsText.
+ replace(/\$1/g, counts.total).
+ replace(/\$2/g, counts.obstructed).
+ replace(/\$3/g, selectedCount).
+ replace(/\$4/g, counts.converted).
+ replace(/\$5/g, counts.failed);
+
+ // Show the appropriate controls based on the current state.
+ for (var [, [labelName, visible]] in Iterator([
+ ["lblCandidatesSearching", searching ],
+ ["lblCandidatesConverting",converting ],
+ ["lblCandidatesNone", selecting && !selectableCount ],
+ ["lblCandidatesInvalid", selecting && selectableCount && !selectedCount],
+ ["lblCandidatesConvert", selecting && selectedCount ],
+ ["lblCandidatesFinish", finished ],
+ ])) {
+ document.getElementById(labelName).hidden = !visible;
+ }
+
+ // Set the appropriate label for the Finish button.
+ var finishButton = this._wizard.getButton("finish");
+ if (searching || selecting) {
+ var convertButton = document.getElementById("btnCandidatesConvert");
+ finishButton.label = convertButton.label;
+ finishButton.setAttribute("accesskey",
+ convertButton.getAttribute("accesskey"));
+ } else {
+ finishButton.label = this._wizardFinishLabel;
+ finishButton.setAttribute("accesskey", this._wizardFinishAccessKey);
+ }
+
+ // Disable the finish button unless conversion is ready or finished.
+ this._wizard.getButton("finish").disabled =
+ !((selecting && selectedCount) || finished);
+ },
+
+ /**
+ * When the "candidates" wizard page, which is the last one, is displayed,
+ * this function initiates the scanning of the requested source folder. When
+ * scanning is completed, the user can select the files to convert and start
+ * the operation using the finish button.
+ */
+ onCandidatesPageShow: function() {
+ // If a data source for the tree is already present, detach it.
+ var tree = this._candidatesTree;
+ if (this._candidatesDataSource) {
+ tree.database.RemoveDataSource(this._candidatesDataSource);
+ }
+ // Create the new data source for the tree and assign it. The candidates
+ // data source created here initially contains only the root element.
+ var ds = new CandidatesDataSource();
+ this._candidatesDataSource = ds;
+ tree.database.AddDataSource(ds);
+ // Rebuild the tree contents from scratch.
+ tree.builder.rebuild();
+ // Ensure that the root container is open.
+ if (!tree.view.isContainerOpen(0)) {
+ tree.view.toggleOpenState(0);
+ }
+
+ // Indicate that a new conversion process is starting.
+ this._conversionFinished = false;
+
+ // Prepare the asynchronous enumerator to locate the candidates.
+ var self = this;
+ this._finderEnumerator = new AsyncEnumerator(
+ this._finder,
+ function(candidate) {
+ // The candidate finder may generate null values from time to time. This
+ // is done to keep the user interface responsive even while no results
+ // are being retrieved.
+ if (candidate) {
+ // Add the new candidate to the data source.
+ ds.addCandidate(candidate);
+ // Update the controls based on the current state.
+ self.checkCandidatesControls();
+ }
+ },
+ function() {
+ // The operation completed successfully. Update the current state by
+ // removing the reference to the enumerator.
+ self._finderEnumerator = null;
+ // If no candidates can be selected, disable the root of the tree.
+ if (!(ds.counts.total - ds.counts.obstructed)) {
+ ds.replaceLiteral(ds.resources.candidates, ds.resources.disabled,
+ "disabled");
+ }
+ // Update the controls based on the current state.
+ self.checkCandidatesControls();
+ }
+ );
+
+ // Update the controls based on the current state.
+ this.checkCandidatesControls();
+
+ // Start the asynchronous enumeration.
+ this._finderEnumerator.start();
+ },
+
+ /**
+ * This function is called when the "candidates" wizard page is being hidden
+ * because the back button has been clicked. This function is also called
+ * indirectly when the window is being closed.
+ */
+ onCandidatesPageRewound: function() {
+ // Before stopping the conversion enumerator, ensure that no candidate is
+ // being converted. If a candidate is being converted, the conversion
+ // enumerator is always present and paused. Canceling the conversion ensures
+ // that the conversion enumerator is not resumed.
+ var currentCandidate = this._currentCandidate;
+ if (currentCandidate) {
+ this._currentCandidate = null;
+ currentCandidate.cancelConversion();
+ }
+ // Ensure that the asynchronous enumerator for finding sources is stopped.
+ var finderEnumerator = this._finderEnumerator;
+ if (finderEnumerator) {
+ this._finderEnumerator = null;
+ finderEnumerator.stop();
+ }
+ // Ensure that the asynchronous enumerator for conversion is stopped.
+ var conversionEnumerator = this._conversionEnumerator;
+ if (conversionEnumerator) {
+ this._conversionEnumerator = null;
+ conversionEnumerator.stop();
+ }
+ },
+
+ /**
+ * Starts the actual conversion process or closes the window.
+ */
+ onWizardFinish: function() {
+ // Since this function may be re-entered if the Enter key is pressed, even
+ // if the finish button is disabled, explicitly check for this condition.
+ if (this._wizard.getButton("finish").disabled) {
+ return false;
+ }
+
+ // If the conversion process finished, close the window and exit now.
+ if (this._conversionFinished) {
+ return true;
+ }
+
+ // From now on, the selected candidates cannot be changed.
+ var ds = this._candidatesDataSource;
+ ds.selectionDisabled = true;
+ // Modify the properties of the root element to indicate that the conversion
+ // process is running and to prevent modification of the selection state.
+ var resource = ds.resources.candidates;
+ ds.replaceLiteral(resource, ds.resources.converting, "converting");
+ ds.replaceLiteral(resource, ds.resources.disabled, "disabled");
+ // For all the available candidates
+ for (var [, candidate] in Iterator(ds.candidates)) {
+ // Prevent modification of the selection state.
+ resource = ds.resourceForCandidate(candidate.internalIndex);
+ ds.replaceLiteral(resource, ds.resources.disabled, "disabled");
+ // If the candidate has not been already converted and has been selected
+ if (ds.getLiteralValue(resource, ds.resources.checked) == "true" &&
+ !candidate.obstructed) {
+ // Enqueue the candidate for conversion.
+ ds.replaceLiteral(resource, ds.resources.enqueued, "enqueued");
+ }
+ }
+
+ // Prepare the asynchronous enumerator to convert the candidates.
+ var self = this;
+ this._conversionEnumerator = new AsyncEnumerator(
+ ds.candidates,
+ function([, candidate]) {
+ // If the candidate is not enqueued, continue with the next item.
+ var resource = ds.resourceForCandidate(candidate.internalIndex);
+ if (!ds.getLiteralValue(resource, ds.resources.enqueued)) {
+ return;
+ }
+ // Show that the candidate is being converted.
+ ds.replaceLiteral(resource, ds.resources.converting, "converting");
+ // Update the controls based on the current state.
+ self.checkCandidatesControls();
+ // Set the required references to use this window for conversion.
+ candidate.conversionWindow = window;
+ candidate.conversionFrame = document.getElementById("frmConvert");
+ // Stop the enumeration temporarily and start the conversion process.
+ self._conversionEnumerator.pause();
+ self._currentCandidate = candidate;
+ candidate.convert(function(aSuccess) {
+ // Indicate that the candidate conversion finished.
+ self._currentCandidate = null;
+ // Show the conversion results for the candidate.
+ if (aSuccess) {
+ ds.replaceLiteral(resource, ds.resources.converted, "converted");
+ } else {
+ ds.replaceLiteral(resource, ds.resources.failed, "failed");
+ }
+ // Resume the enumeration.
+ self._conversionEnumerator.start();
+ });
+ },
+ function() {
+ // The operation completed successfully. Update the current state and
+ // remove the reference to the enumerator.
+ self._conversionFinished = true;
+ self._conversionEnumerator = null;
+ // Show the overall success status on the root element of the tree.
+ var resource = ds.resources.candidates;
+ if (ds.counts.failed == 0) {
+ ds.replaceLiteral(resource, ds.resources.converted, "converted");
+ } else {
+ ds.replaceLiteral(resource, ds.resources.failed, "failed");
+ }
+ // Update the controls based on the current state.
+ self.checkCandidatesControls();
+ }
+ );
+
+ // Update the controls based on the current state.
+ this.checkCandidatesControls();
+
+ // Start the asynchronous enumeration.
+ this._conversionEnumerator.start();
+
+ // Do not close the window.
+ return false;
+ },
+
+ /**
+ * Performs the necessary cleanup before closing the window.
+ */
+ onWizardCancel: function() {
+ this.onCandidatesPageRewound();
+ return true;
+ },
+
+ /**
+ * Inverts the checked state of the tree selection when space is pressed.
+ */
+ onTreeKeyPress: function(aEvent) {
+ if (aEvent.charCode != KeyEvent.DOM_VK_SPACE)
+ return;
+
+ var treeView = this._candidatesTreeView;
+ var checkboxColumn = this._candidatesTree.columns["tcChecked"];
+ var forbidChildChanges = false;
+ for (var i = 0; i < treeView.selection.getRangeCount(); i++) {
+ var start = {}, end = {};
+ treeView.selection.getRangeAt(i, start, end);
+ for (var rowNum = start.value; rowNum <= end.value; rowNum++) {
+ // If we are changing the state of a container, ignore the selection
+ // changes on its children.
+ var isContainer = (rowNum === 0);
+ if (isContainer) {
+ forbidChildChanges = true;
+ }
+ // Invert the checked state of the row.
+ if (isContainer || !forbidChildChanges) {
+ var oldValue = treeView.getCellValue(rowNum, checkboxColumn);
+ var newValue = (oldValue == "true" ? "false" : "true");
+ treeView.setCellValue(rowNum, checkboxColumn, newValue);
+ }
+ }
+ }
+ },
+
+ /**
+ * Shows a file selector allowing the user to select the absolute path of a
+ * folder to be placed in the textbox linked to this control. The "input"
+ * event handler of the textbox is called after a folder has been selected.
+ */
+ browseForFolder: function(aEvent) {
+ // Initialize the file picker component.
+ var filePicker = Cc["@mozilla.org/filepicker;1"].
+ createInstance(Ci.nsIFilePicker);
+ filePicker.init(window, document.title, Ci.nsIFilePicker.modeGetFolder);
+ // Get a reference to the linked textbox control.
+ var textboxElement = document.getElementById(
+ aEvent.target.getAttribute("control"));
+ // Find the directory currently displayed in the user interface. If there is
+ // already a directory selected, attempt to use it as the default in the
+ // file picker dialog. If the path is invalid, do nothing.
+ if (textboxElement.value) {
+ try {
+ var targetFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ targetFile.initWithPath(textboxElement.value);
+ filePicker.displayDirectory = targetFile;
+ } catch (e) { /* Ignore errors */ }
+ }
+ // If the user made a selection
+ if (filePicker.show() == Ci.nsIFilePicker.returnOK) {
+ // Update the displayed value.
+ textboxElement.value = filePicker.file.path;
+ // Call the event handler that updates the status of the other controls.
+ var event = document.createEvent("UIEvent");
+ event.initUIEvent("input", true, true, window, 0);
+ textboxElement.dispatchEvent(event);
+ }
+ },
+
+ /**
+ * Returns the label of the wizard button identified by the given name,
+ * excluding the characters that represent the arrows.
+ */
+ _getWizardButtonLabel: function(aButtonName) {
+ return this._wizard.getButton(aButtonName).label.
+ replace(/^[< ]+|[ >]+$/, "");
+ },
+
+ /**
+ * Creates an nsIFile object from the text in the given element. If the object
+ * cannot be created or the path does not refer to a folder, an exception is
+ * thrown. If the aMustExist parameter is true, an exception is thrown also if
+ * the folder does not exist.
+ */
+ _getFolderFromTextbox: function(aTextboxElementId, aMustExist) {
+ // Find the directory currently displayed in the user interface.
+ var folderPath = document.getElementById(aTextboxElementId).value;
+ // Create the object and check that it refers to a folder.
+ var targetFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ targetFile.initWithPath(folderPath);
+ if (aMustExist && !targetFile.exists()) {
+ throw "The path does not exist";
+ }
+ if (targetFile.exists() && !targetFile.isDirectory()) {
+ throw "The path does not refer to a directory";
+ }
+ // Return the new object.
+ return targetFile;
+ },
+}
diff --git a/src/chrome/content/convert/convertDialog.xul b/src/chrome/content/convert/convertDialog.xul
new file mode 100644
index 0000000..4bb56a4
--- /dev/null
+++ b/src/chrome/content/convert/convertDialog.xul
@@ -0,0 +1,533 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &introduction.para1.description;
+
+
+ &introduction.para2.description;
+
+
+
+ &introduction.visitwebsite.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &candidates.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This text node will be replaced with the value to be displayed.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/chrome/content/engine/Archive.js b/src/chrome/content/engine/Archive.js
new file mode 100644
index 0000000..f67276e
--- /dev/null
+++ b/src/chrome/content/engine/Archive.js
@@ -0,0 +1,157 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Base class representing web archives. Derived objects must implement specific
+ * methods.
+ *
+ * This object allows the creation and extraction of archives, and handles the
+ * metadata associated with the archive's contents.
+ */
+function Archive() {
+ // Initialize member variables explicitly for proper inheritance.
+ this.file = null;
+ this.pages = [];
+ this._tempDir = null;
+}
+
+Archive.prototype = {
+ /**
+ * nsIFile representing the compressed or encoded archive.
+ */
+ file: null,
+
+ /**
+ * Array of ArchivePage objects holding information on each individual web
+ * page included in the archive. Some formats may support only one page. The
+ * order of the items is important, and reflects the index that can be used
+ * to select a specific page in the archive.
+ */
+ pages: [],
+
+ /**
+ * Adds a new page to the archive and returns the new page object.
+ */
+ addPage: function() {
+ var page = this._newPage();
+ page._index = this.pages.length;
+ this.pages.push(page);
+ return page;
+ },
+
+ /**
+ * String representing the leaf name of the archive file, without extension.
+ */
+ get name() {
+ // Returns the base name extracted from the URI object of the archive, which
+ // always implements the nsIURL interface.
+ return this.uri.QueryInterface(Ci.nsIURL).fileBaseName;
+ },
+
+ /**
+ * nsIURI representing the original location of the web archive.
+ *
+ * This URI does not refer to a specific page in the archive. If this property
+ * is set to an URI containing a page reference, the reference is removed.
+ *
+ * By default, this property corresponds to the URI of the archive file.
+ */
+ _uri: null,
+ get uri() {
+ // If the original URI for the archive was not set explicitly, generate a
+ // new URI pointing to the local archive file.
+ if (!this._uri) {
+ this._uri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(this.file);
+ }
+ return this._uri;
+ },
+ set uri(aValue) {
+ var archiveUri = aValue.clone();
+ if (archiveUri instanceof Ci.nsIURL) {
+ // Ensure that the archive page number in the query part is removed.
+ archiveUri.query =
+ archiveUri.query.replace(/&?web_archive_page=\d+$/, "");
+ // Try and remove the hash part, if supported by the URL implementation.
+ try {
+ archiveUri.ref = "";
+ } catch (e) { }
+ }
+ this._uri = archiveUri;
+ },
+
+ /**
+ * String uniquely identifying the archive in the cache.
+ */
+ get cacheKey() {
+ // Store at most one archive object for every local file.
+ return this.file.path;
+ },
+
+ /**
+ * Reloads all the pages from the archive file.
+ *
+ * This method must be implemented by derived objects.
+ */
+ load: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Extracts all the pages from the archive file.
+ *
+ * This method must be implemented by derived objects.
+ */
+ extractAll: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Returns a new page object associated with this archive.
+ *
+ * This method must be implemented by derived objects.
+ */
+ _newPage: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * nsIFile representing a temporary directory whose subdirectories will
+ * contain the expanded contents of the archived pages.
+ */
+ _tempDir: null,
+}
diff --git a/src/chrome/content/engine/ArchiveCache.js b/src/chrome/content/engine/ArchiveCache.js
new file mode 100644
index 0000000..764e51f
--- /dev/null
+++ b/src/chrome/content/engine/ArchiveCache.js
@@ -0,0 +1,211 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This object stores all the archives that are known in this session.
+ */
+var ArchiveCache = {
+
+ /**
+ * Register the given archive object in the cache. After an archive object has
+ * been registered in the cache, it must not be modified without unregistering
+ * it first.
+ *
+ * @param aArchive
+ * Object of type Archive whose metadata will be cached.
+ */
+ registerArchive: function(aArchive) {
+ // Remove any previously registered archive object with the same key.
+ var oldArchive = this._archivesByKey[aArchive.cacheKey];
+ if (oldArchive) {
+ this.unregisterArchive(oldArchive);
+ }
+ // Register the archive in the cache.
+ this._archivesByKey[aArchive.cacheKey] = aArchive;
+ this._archivesByUri[aArchive.uri.spec] = aArchive;
+ // Add information about the individual pages.
+ for (var [, page] in Iterator(aArchive.pages)) {
+ // The following URLs are normally unique for every extracted page.
+ this._pagesByArchiveUri[page.archiveUri.spec] = page;
+ if (page.tempUri) {
+ this._pagesByTempUri[page.tempUri.spec] = page;
+ }
+ this._pagesByTempFolderUri[page.tempFolderUri.spec] = page;
+ if (page.directArchiveUri) {
+ this._pagesByDirectArchiveUri[page.directArchiveUri.spec] = page;
+ }
+ // Add places annotations for the cached page.
+ ArchiveAnnotations.setAnnotationsForPage(page);
+ }
+ },
+
+ /**
+ * Remove the given archive object from the cache.
+ *
+ * @param aArchive
+ * Object of type Archive to be removed from the cache.
+ */
+ unregisterArchive: function(aArchive) {
+ // Ensure that the archive is present in the cache.
+ if (!this._archivesByKey[aArchive.cacheKey]) {
+ return;
+ }
+ // Remove the archive from the cache.
+ delete this._archivesByKey[aArchive.cacheKey];
+ delete this._archivesByUri[aArchive.uri.spec];
+ // Remove information about the individual pages.
+ for (var [, page] in Iterator(aArchive.pages)) {
+ // The following URLs are normally unique for every extracted page.
+ delete this._pagesByArchiveUri[page.archiveUri.spec];
+ if (page.tempUri) {
+ delete this._pagesByTempUri[page.tempUri.spec];
+ }
+ delete this._pagesByTempFolderUri[page.tempFolderUri.spec];
+ if (page.directArchiveUri) {
+ delete this._pagesByDirectArchiveUri[page.directArchiveUri.spec];
+ }
+ // Clear the obsolete places annotations for the page.
+ ArchiveAnnotations.removeAnnotationsForPage(page);
+ }
+ },
+
+ /**
+ * Returns the archive object associated with the given URL.
+ *
+ * @param aUri
+ * nsIURI representing the original URL of the archive.
+ */
+ archiveFromUri: function(aUri) {
+ return this._archivesByUri[this._getLookupSpec(aUri)] || null;
+ },
+
+ /**
+ * Returns the page object associated with the file referenced by the given
+ * URL, if the URL represents a file in the temporary directory that is
+ * related to an available extracted page.
+ *
+ * @param aUri
+ * nsIURI to check.
+ */
+ pageFromAnyTempUri: function(aUri) {
+ // Return now if the provided URL is not a file URL, thus it cannot refer to
+ // a file in the temporary directory related to an extracted page.
+ if (!(aUri instanceof Ci.nsIFileURL)) {
+ return null;
+ }
+ // Check if this file is located under any archive's temporary folder.
+ for (var [, page] in Iterator(this._pagesByTempUri)) {
+ var folderUri = page.tempFolderUri.QueryInterface(Ci.nsIFileURL);
+ // The following function checks whether aUri is located under the folder
+ // represented by folderUri.
+ if (folderUri.getCommonBaseSpec(aUri) === folderUri.spec) {
+ return page;
+ }
+ }
+ // The URL is unrelated to any extracted page.
+ return null;
+ },
+
+ /**
+ * Returns the page object associated with the given URL.
+ *
+ * @param aUri
+ * nsIURI representing one of the URLs of the main file associated with
+ * the page. It can be the archive URL, the URL in the temporary
+ * folder, or the direct archive access URL (for example, a "jar" URL).
+ */
+ pageFromUri: function(aUri) {
+ var uriSpec = this._getLookupSpec(aUri);
+ return this._pagesByArchiveUri[uriSpec] ||
+ this._pagesByDirectArchiveUri[uriSpec] ||
+ this._pagesByTempUri[uriSpec] ||
+ null;
+ },
+
+ /**
+ * Associative array containing all the registered Archive objects.
+ */
+ _archivesByKey: {},
+
+ /**
+ * Associative array containing all the registered Archive objects, accessible
+ * by their original URI.
+ */
+ _archivesByUri: {},
+
+ /**
+ * Associative array containing all the available archived pages, accessible
+ * by their specific archive URI.
+ */
+ _pagesByArchiveUri: {},
+
+ /**
+ * Associative array containing all the available archived pages, accessible
+ * by the URI of their main file in the temporary directory.
+ */
+ _pagesByTempUri: {},
+
+ /**
+ * Associative array containing all the available archived pages, accessible
+ * by the URI of their specific temporary folder.
+ */
+ _pagesByTempFolderUri: {},
+
+ /**
+ * Associative array containing some of the available archived pages,
+ * accessible by their direct archive access URI (for example, a "jar:" URI).
+ */
+ _pagesByDirectArchiveUri: {},
+
+ /**
+ * Removes unnecessary elements from archive or page URIs, in order to look
+ * them up in the archive cache correctly.
+ *
+ * @param aUri
+ * nsIURI to process.
+ */
+ _getLookupSpec: function(aUri) {
+ var lookupUri = aUri.clone();
+ if (lookupUri instanceof Ci.nsIURL) {
+ // Try and remove the hash part, if supported by the URL implementation.
+ try {
+ lookupUri.ref = "";
+ } catch (e) { }
+ }
+ return lookupUri.spec;
+ },
+};
diff --git a/src/chrome/content/engine/ArchivePage.js b/src/chrome/content/engine/ArchivePage.js
new file mode 100644
index 0000000..b733775
--- /dev/null
+++ b/src/chrome/content/engine/ArchivePage.js
@@ -0,0 +1,302 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Base class representing a page within a web archive. Derived objects must
+ * implement specific methods.
+ *
+ * This object allows the creation and extraction of individual pages within
+ * web archives, and handles the metadata associated with the page's contents.
+ *
+ * Instances of this object must be created using the methods in the Archive
+ * object.
+ */
+function ArchivePage(aArchive) {
+ this.archive = aArchive;
+
+ // Initialize other member variables explicitly for proper inheritance.
+ this.tempDir = null;
+ this.indexLeafName = "";
+ this.title = "";
+ this.originalUrl = "";
+ this._dateArchived = null;
+ this.renderingCharacterSet = "";
+ this._index = 0;
+}
+
+ArchivePage.prototype = {
+ /**
+ * The parent Archive object.
+ */
+ archive: null,
+
+ /**
+ * nsIFile representing the temporary directory holding the expanded contents
+ * of the page.
+ */
+ tempDir: null,
+
+ /**
+ * Name of the main file associated with the page. This is often "index.htm".
+ */
+ indexLeafName: "",
+
+ /**
+ * Document title or description explicitly associated with this page.
+ */
+ title: "",
+
+ /**
+ * String representing the original location this page was saved from.
+ */
+ originalUrl: "",
+
+ /**
+ * Valid Date object representing the time the page was archived, or null if
+ * the information is not available. This property can also be set using a
+ * string value.
+ */
+ get dateArchived() {
+ return this._dateArchived;
+ },
+ set dateArchived(aValue) {
+ if (aValue) {
+ // If the provided value is not a Date object, create a new object.
+ var date = aValue.getTime ? aValue : new Date(aValue);
+ // Ensure that the provided date is valid.
+ this._dateArchived = isNaN(date.getTime()) ? null : date;
+ } else {
+ this._dateArchived = null;
+ }
+ },
+
+ /**
+ * String representing the character set selected by the user for rendering
+ * the page at the time it was archived. This information may be used when the
+ * archive is opened to override the default character set detected from the
+ * saved page.
+ */
+ renderingCharacterSet: "",
+
+ /**
+ * nsIURI representing the specific page inside the compressed or encoded
+ * archive.
+ */
+ get archiveUri() {
+ // For a single-page archive, there is no difference with the archive URI.
+ var pageArchiveUri = this.archive.uri.clone();
+ if (this.archive.pages.length == 1) {
+ return pageArchiveUri;
+ }
+
+ // Ensure that we can modify the URL to point to a specific page.
+ if (!(pageArchiveUri instanceof Ci.nsIURL)) {
+ throw new Components.Exception("Multi-page archives can only be opened" +
+ " from a location that supports relative URLs.");
+ }
+
+ // Use the query part to store the information about the page number.
+ if (pageArchiveUri.query) {
+ pageArchiveUri.query += "&";
+ }
+ pageArchiveUri.query += "web_archive_page=" + (this._index + 1);
+
+ return pageArchiveUri;
+ },
+
+ /**
+ * nsIURI representing the local temporary copy of the main file associated
+ * with the page, or null if the page was not extracted locally.
+ */
+ get tempUri() {
+ // Locate the main temporary file associated with with the page.
+ var indexFile = this.tempDir.clone();
+ indexFile.append(this.indexLeafName);
+ // Return the associated URI object.
+ return Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(indexFile);
+ },
+
+ /**
+ * nsIURI representing the local temporary folder associated with the page.
+ */
+ get tempFolderUri() {
+ return Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(this.tempDir);
+ },
+
+ /**
+ * Sets additional metadata about the page starting from the provided document
+ * and browser objects.
+ *
+ * This method can be overridden by derived objects.
+ */
+ setMetadataFromDocumentAndBrowser: function(aDocument, aBrowser) {
+ // Find the original metadata related to the page being saved, if present.
+ var documentUri = aDocument.documentURIObject;
+ var originalData = this._getOriginalMetadata(documentUri, aDocument);
+ // Set the other properties of this page object appropriately.
+ this.title = aDocument.title || "Unknown";
+ this.originalUrl = originalData.originalUrl || documentUri.spec;
+ this.dateArchived = originalData.dateArchived || new Date();
+ this.renderingCharacterSet = aDocument.characterSet;
+ },
+
+ /**
+ * Stores the page into the archive file.
+ */
+ save: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Zero-based index of the page in the archive.
+ */
+ _index: 0,
+
+ /**
+ * Returns an object containing the original metadata for the page, obtained
+ * from the current archive cache or from the local file the page is being
+ * saved from.
+ *
+ * @param aSaveUri
+ * nsIURI of the page being saved.
+ * @param aDocument
+ * Document that must be used to find the original URL the local page
+ * was saved from, if necessary.
+ */
+ _getOriginalMetadata: function(aSaveUri, aDocument) {
+ // When saving a page that was extracted from an archive in this session,
+ // use the metadata from the original archive.
+ var originalPage = ArchiveCache.pageFromUri(aSaveUri);
+ if (originalPage) {
+ return originalPage;
+ }
+
+ // If the page is part of an archive but is not one of the main pages, use
+ // only the date from the original archive.
+ var parentPage = ArchiveCache.pageFromAnyTempUri(aSaveUri);
+ if (parentPage) {
+ return { dateArchived: parentPage.dateArchived };
+ }
+
+ // Check if the metadata from a locally saved page should be used.
+ if (aSaveUri instanceof Ci.nsIFileURL) {
+ // Get the file object associated with the page being saved.
+ var file = aSaveUri.file;
+ // Ensure that the file being saved exists at this point.
+ if (file.exists()) {
+ // Use the date and time from the local file, and find the original save
+ // location from the document.
+ return {
+ dateArchived: new Date(file.lastModifiedTime),
+ originalUrl: this._getOriginalSaveUrl(aDocument)
+ };
+ }
+ }
+
+ // No additonal metadata is available.
+ return {};
+ },
+
+ /**
+ * Return the original URL the given document was saved from, if available.
+ */
+ _getOriginalSaveUrl: function(aDocument) {
+ // Find the first comment in the document, and return now if not found.
+ var firstCommentNode = aDocument.evaluate('//comment()', aDocument, null,
+ Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+ if (!firstCommentNode) {
+ return null;
+ }
+
+ // Check to see if the first comment in the document is the Mark of the Web
+ // specified by Internet Explorer when the page was saved. Even though this
+ // method is not exactly compliant with the original specification (see
+ // , retrieved
+ // 2009-05-10), it should provide accurate results most of the time.
+ var originalUrl = this._parseMotwComment(firstCommentNode.nodeValue);
+ if (originalUrl) {
+ // Exclude values with special meanings from being considered as the
+ // original save location. The comparisons are case-sensitive.
+ if (originalUrl !== "http://localhost" &&
+ originalUrl !== "about:internet") {
+ return originalUrl;
+ } else {
+ return null;
+ }
+ }
+
+ // Check to see if the page was saved using Save Complete.
+ originalUrl = this._parseSaveCompleteComment(firstCommentNode.nodeValue);
+ if (originalUrl) {
+ return originalUrl;
+ }
+
+ // No original save location is available.
+ return null;
+ },
+
+ /**
+ * Parses the provided Mark of the Web comment and returns the specified URL,
+ * or null if not available. For example, if the provided string contains
+ * " saved from url=(0023)http://www.example.org/ ", this function returns
+ * "http://www.example.org/".
+ */
+ _parseMotwComment: function(aMotwString) {
+ // Match "saved from url=" case-sensitively, followed by the mandatory
+ // character count in parentheses, followed by the actual URL, containing no
+ // whitespace. Ignore leading and trailing whitespace.
+ var match = /^\s*saved from url=\(\d{4}\)(\S+)\s*$/g.exec(aMotwString);
+ // Return the URL part, if found, or null if the format does not match.
+ return match && match[1];
+ },
+
+ /**
+ * Parses the provided Save Complete original location comment and returns the
+ * specified URL, or null if not available. For example, if the provided
+ * string contains " Source is http://www.example.org/ ", this function
+ * returns "http://www.example.org/".
+ */
+ _parseSaveCompleteComment: function(aSaveCompleteString) {
+ // Match "Source is" case-sensitively, followed by a space and the actual
+ // URL, containing no whitespace. Ignore leading and trailing whitespace.
+ var match = /^\s*Source is (\S+)\s*$/g.exec(aSaveCompleteString);
+ // Return the URL part, if found, or null if the format does not match.
+ return match && match[1];
+ },
+}
diff --git a/src/chrome/content/engine/MaffArchive.js b/src/chrome/content/engine/MaffArchive.js
new file mode 100644
index 0000000..5fea141
--- /dev/null
+++ b/src/chrome/content/engine/MaffArchive.js
@@ -0,0 +1,190 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents a MAFF web archive.
+ *
+ * This class derives from Archive. See the Archive documentation for details.
+ *
+ * @param aFile
+ * nsIFile representing the compressed archive. The file usually ends
+ * with the ".maff" extension.
+ */
+function MaffArchive(aFile) {
+ Archive.call(this);
+ this.file = aFile;
+
+ // Initialize other member variables explicitly for proper inheritance.
+ this._createNew = true;
+ this._useDirectAccess = false;
+}
+
+MaffArchive.prototype = {
+ __proto__: Archive.prototype,
+
+ // Archive
+ load: function() {
+ // Indicate that the file contains other saved pages that must be preserved.
+ this._createNew = false;
+ },
+
+ // Archive
+ extractAll: function() {
+ // Determine if the data files should be extracted.
+ this._useDirectAccess = Prefs.openUseJarProtocol;
+ // Prepare a list of all the page folders available in the archive.
+ var pageFolderNames = [];
+ // Prepare a map of all the detected default documents for each page.
+ var indexLeafNames = {};
+ // Open the archive file for reading.
+ var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ zipReader.open(this.file);
+ try {
+ // Enumerate all the entries in the archive. ZIP entries are ordinary
+ // strings representing the path of the file or directory, separated by a
+ // forward slash ("/"). Directory entries end with a forward slash.
+ var zipEntries = zipReader.findEntries(null);
+ while (zipEntries.hasMore()) {
+ var zipEntry = zipEntries.getNext();
+
+ // Store the name of every first-level folder contained in the archive.
+ // Since synthetic directory entries are created by the ZIP reader if
+ // they are not explicitly stored in the archive, all the pages in the
+ // archive will be detected.
+ if (/^[^/]+\/$/.test(zipEntry)) {
+ pageFolderNames.push(zipEntry.slice(0, -1));
+ } else {
+ // If the current entry is the name of a file directly contained in a
+ // page folder, having a base name of "index" and a simple extension,
+ // use the first of these files alphabetically as the suggested
+ // default document name for the page.
+ zipEntry.replace(/^([^/]+)\/(index\.[^/.]+)$/i,
+ function(aAll, aPageFolderName, aIndexFileName) {
+ if (!(aPageFolderName in indexLeafNames) ||
+ indexLeafNames[aPageFolderName] > aIndexFileName) {
+ indexLeafNames[aPageFolderName] = aIndexFileName;
+ }
+ }
+ )
+ }
+
+ // If the archive should be opened using direct access to the files.
+ var shouldExtract;
+ if (this._useDirectAccess) {
+ // Extract only the metadata files "index.rdf" and "history.rdf". The
+ // file names are compared case insensitively, even though their names
+ // in the archive should always be lowercase.
+ shouldExtract = /^[^/]+\/(index|history)\.rdf$/i.test(zipEntry);
+ } else {
+ // Extract all the file entries that are present in the archive.
+ shouldExtract = zipEntry.slice(-1) != "/";
+ }
+
+ // If the current entry must be extracted.
+ if (shouldExtract) {
+ // Find the file whose path corresponds to the ZIP entry.
+ var destFile = this._getFileForZipEntry(zipEntry);
+ // Ensure that the ancestors exist.
+ if (!destFile.parent.exists()) {
+ destFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+ }
+ // Extract the file in the temporary directory.
+ zipReader.extract(zipEntry, destFile);
+ }
+ }
+ } finally {
+ // Close the file when extraction is finished or in case of exception.
+ zipReader.close();
+ }
+ // Sort the page folder names alphabetically. This ensures that the pages
+ // will be displayed always in the same sequence regardless of the order in
+ // which their folder names are returned by the ZIP reader.
+ pageFolderNames.sort();
+ // For every page folder name in the correct order.
+ for (var [, pageFolderName] in Iterator(pageFolderNames)) {
+ // Add a new page object to the archive and set its temporary directory.
+ var newPage = this.addPage();
+ newPage.tempDir = this._tempDir.clone();
+ newPage.tempDir.append(pageFolderName);
+ newPage.indexLeafName = indexLeafNames[pageFolderName] || "";
+ // Load the metadata for the page. This overrides the autodetected value
+ // of the indexLeafName property if necessary.
+ try {
+ newPage._loadMetadata();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ // Archive
+ _newPage: function() {
+ return new MaffArchivePage(this);
+ },
+
+ /**
+ * Returns a new nsIFile pointing to the location indicated by the given ZIP
+ * entry, relative to the temporary directory for this archive.
+ *
+ * @param aZipEntry
+ * Unicode ZIP entry after which the file must be named.
+ */
+ _getFileForZipEntry: function(aZipEntry) {
+ var fileForEntry = this._tempDir.clone();
+ for (var [, pathSegment] in Iterator(aZipEntry.split("/"))) {
+ fileForEntry.append(pathSegment);
+ }
+ return fileForEntry;
+ },
+
+ /**
+ * Indicates that the archive file should be created from scratch. If false,
+ * indicates that at least one of the listed pages has already been saved, or
+ * that the archive contains other unloaded pages and should not be
+ * overwritten.
+ */
+ _createNew: true,
+
+ /**
+ * Indicates that the saved pages in the archive should be opened directly
+ * from the archive itself, instead of being extracted and read from the
+ * temporary directory. Even when this property is true, metadata files are
+ * still extracted in the temporary directory.
+ */
+ _useDirectAccess: false,
+}
diff --git a/src/chrome/content/engine/MaffArchivePage.js b/src/chrome/content/engine/MaffArchivePage.js
new file mode 100644
index 0000000..8f83852
--- /dev/null
+++ b/src/chrome/content/engine/MaffArchivePage.js
@@ -0,0 +1,200 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents a page within a MAFF web archive.
+ *
+ * This class derives from ArchivePage. See the ArchivePage documentation for
+ * details.
+ */
+function MaffArchivePage(aArchive) {
+ ArchivePage.call(this, aArchive);
+
+ // Initialize member variables explicitly for proper inheritance.
+ this._browserObjectForMetadata = null;
+}
+
+MaffArchivePage.prototype = {
+ __proto__: ArchivePage.prototype,
+
+ /**
+ * Internal path in the archive of the main file associated with the page.
+ */
+ get indexZipEntry() {
+ return this.tempDir.leafName + "/" + this.indexLeafName;
+ },
+
+ /**
+ * nsIURI providing direct access to the main file in the archive.
+ */
+ get directArchiveUri() {
+ // Compose the requested "jar:" URI.
+ var jarUriSpec = "jar:" + this.archive.uri.spec + "!/" + this.indexZipEntry;
+ // Return the associated URI object.
+ return Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newURI(jarUriSpec, null, null);
+ },
+
+ // ArchivePage
+ get tempUri() {
+ // If the archive that contains the page was extracted while requiring
+ // direct access to the page, no temporary local page is available.
+ if (this.archive._useDirectAccess) {
+ return null;
+ }
+ // By default, return the temporary URL determined by the base object.
+ return ArchivePage.prototype.__lookupGetter__("tempUri").call(this);
+ },
+
+ // ArchivePage
+ setMetadataFromDocumentAndBrowser: function(aDocument, aBrowser) {
+ // Set the page properties that are common to all archive types.
+ ArchivePage.prototype.setMetadataFromDocumentAndBrowser.call(this,
+ aDocument, aBrowser);
+ // Store the provided browser object.
+ this._browserObjectForMetadata = aBrowser;
+ },
+
+ // ArchivePage
+ save: function() {
+ // Create the "index.rdf" and "history.rdf" files near the main file.
+ this._saveMetadata();
+ // Prepare the archive for creation or modification.
+ var creator = new ZipCreator(this.archive.file, this.archive._createNew);
+ try {
+ // Add the contents of the temporary directory to the archive, under the
+ // ZIP entry with the same name as the temporary directory itself.
+ creator.addDirectory(this.tempDir, this.tempDir.leafName);
+ // In case of success, the new archive file should not be overwritten.
+ this.archive._createNew = false;
+ } finally {
+ creator.dispose();
+ }
+ },
+
+ /**
+ * Browser object to gather extended metadata from, or null if not available.
+ */
+ _browserObjectForMetadata: null,
+
+ /**
+ * Loads the metadata of this page from the "index.rdf" file in the temporary
+ * directory.
+ */
+ _loadMetadata: function() {
+ var ds = new MaffDataSource();
+ var res = ds.resources;
+
+ // Get a reference to the "index.rdf" file.
+ var indexFile = this.tempDir.clone();
+ indexFile.append("index.rdf");
+
+ // Load the metadata only if the file exists, otherwise use defaults.
+ if (indexFile.exists()) {
+ ds.loadFromFile(indexFile);
+ }
+
+ // Store the metadata in this object, using defaults for missing entries.
+ this.originalUrl = ds.getMafProperty(res.originalUrl);
+ this.title = ds.getMafProperty(res.title);
+ this.dateArchived = ds.getMafProperty(res.archiveTime);
+ this.indexLeafName = ds.getMafProperty(res.indexFileName) ||
+ this.indexLeafName || "index.html";
+ this.renderingCharacterSet = ds.getMafProperty(res.charset);
+ },
+
+ /**
+ * Saves the metadata of this page to the "index.rdf" and "history.rdf" files
+ * in the temporary directory.
+ */
+ _saveMetadata: function() {
+ // Set standard metadata for "index.rdf".
+ var indexMetadata = [
+ ["originalurl", this.originalUrl],
+ ["title", this.title],
+ ["archivetime", MimeSupport.getDateTimeSpecification(this.dateArchived)],
+ ["indexfilename", this.indexLeafName],
+ ["charset", this.renderingCharacterSet],
+ ];
+
+ var historyMetadata = null;
+ var browser = this._browserObjectForMetadata;
+ if (Prefs.saveMetadataExtended && browser) {
+ // Set extended metadata for "index.rdf".
+ indexMetadata.push(
+ ["textzoom", browser.markupDocumentViewer.textZoom],
+ ["scrollx", browser.contentWindow.scrollX],
+ ["scrolly", browser.contentWindow.scrollY]
+ );
+ // Set extended metadata for "history.rdf".
+ var sessionHistory = browser.sessionHistory;
+ historyMetadata = [
+ ["current", sessionHistory.index],
+ ["noofentries", sessionHistory.count]
+ ];
+ for (var i = 0; i < sessionHistory.count; i++) {
+ historyMetadata.push(
+ ["entry" + i, sessionHistory.getEntryAtIndex(i, false).URI.spec]
+ );
+ }
+ }
+
+ // Write the metadata to the required files.
+ this._savePropertiesToFile(indexMetadata, "index.rdf")
+ if (historyMetadata) {
+ this._savePropertiesToFile(historyMetadata, "history.rdf")
+ }
+ },
+
+ /**
+ * Save the provided metadata to the file with the given name in the temporary
+ * directory.
+ */
+ _savePropertiesToFile: function(aPropertyArray, aFileName) {
+ // Create a new data source for writing.
+ ds = new MaffDataSource();
+ ds.init();
+ // Set all the properties in the given order.
+ aPropertyArray.forEach(function([propertyname, value]) {
+ ds.setMafProperty(ds.resourceForProperty(propertyname), value);
+ });
+ // Actually save the metadata to the file with the provided name.
+ var destFile = this.tempDir.clone();
+ destFile.append(aFileName);
+ ds.saveToFile(destFile);
+ },
+}
diff --git a/src/chrome/content/engine/MaffDataSource.js b/src/chrome/content/engine/MaffDataSource.js
new file mode 100644
index 0000000..9fd4a65
--- /dev/null
+++ b/src/chrome/content/engine/MaffDataSource.js
@@ -0,0 +1,229 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Provides an RDF data source that gives access to the files containing the
+ * saved page metadata of MAFF archives, both for reading and for writing.
+ *
+ * This class derives from DataSourceWrapper. See the DataSourceWrapper
+ * documentation for details.
+ */
+function MaffDataSource() {
+ // Construct the base class wrapping an in-memory XML data source.
+ DataSourceWrapper.call(this,
+ Cc["@mozilla.org/rdf/datasource;1?name=xml-datasource"].
+ createInstance(Ci.nsIRDFDataSource));
+}
+
+MaffDataSource.prototype = {
+ __proto__: DataSourceWrapper.prototype,
+
+ /**
+ * Note: These strings are converted to actual RDF resources by the base class
+ * as soon as this data source is constructed, so GetResource must not be
+ * called. See the DataSourceWrapper documentation for details.
+ */
+ resources: {
+ // Subjects and objects
+ root: "urn:root",
+ // Custom predicates
+ title: "http://maf.mozdev.org/metadata/rdf#title",
+ originalUrl: "http://maf.mozdev.org/metadata/rdf#originalurl",
+ archiveTime: "http://maf.mozdev.org/metadata/rdf#archivetime",
+ indexFileName: "http://maf.mozdev.org/metadata/rdf#indexfilename",
+ charset: "http://maf.mozdev.org/metadata/rdf#charset",
+ },
+
+ /**
+ * Getter for an RDF resource representing a predicate in the MAF namespace.
+ */
+ resourceForProperty: function(aPropertyName) {
+ return this._rdf.GetResource(this._mafNamespacePrefix + aPropertyName);
+ },
+
+ /**
+ * Prepares the data source for receiving new data that will be saved later.
+ *
+ * This method may not be called if the data will be loaded from a file.
+ */
+ init: function() {
+ // Before saving the data source into an RDF/XML file, we need to add the
+ // proper XML namespace for the resources in the MAF vocabulary. Since the
+ // addNameSpace method of the nsIRDFXMLSink interface is not scriptable, we
+ // can only reach it by parsing an existing XML file into the data source.
+ // The file is generated in memory from an empty data source, then it is fed
+ // to an XML parser that drives the real data source.
+ this._feedString(this._getMafNamespaceXml());
+ },
+
+ /**
+ * Loads the data from the specified RDF file.
+ */
+ loadFromFile: function(aFile) {
+ // Since in the RDF files of the MAFF format some literals are persisted as
+ // RDF resource URLs, we must use a custom RDF/XML parser to prevent the
+ // default parser from trying to resolve the literals as relative URLs.
+ var fileContents = this._readEntireFile(aFile, "UTF-8");
+ this._feedString(fileContents);
+ },
+
+ /**
+ * Saves the data into the specified RDF file.
+ */
+ saveToFile: function(aFile) {
+ var fileUrl = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(aFile);
+ this._wrappedObject.QueryInterface(Ci.nsIRDFRemoteDataSource).
+ FlushTo(fileUrl.spec);
+ },
+
+ /**
+ * Retrieve a string representing the value of the provided property, or a
+ * value that evaluates to false if the property is missing or empty.
+ */
+ getMafProperty: function(aPredicate) {
+ // Get the target of the provided predicate, or null if missing.
+ var target = this._wrappedObject.
+ GetTarget(this.resources.root, aPredicate, true);
+ // In RDF files of MAFF archives, values are stored as resources.
+ return target && target.QueryInterface(Ci.nsIRDFResource).ValueUTF8;
+ },
+
+ /**
+ * Set the value of the provided property.
+ */
+ setMafProperty: function(aPredicate, aValue) {
+ // For MAFF format compatibility, store the value as an RDF resource.
+ var valueRes = this._rdf.GetResource(aValue);
+ // Store the value as the target of the provided predicate.
+ this._wrappedObject.Assert(this.resources.root, aPredicate, valueRes, true);
+ },
+
+ /**
+ * Namespace prefix for MAF resource URLs.
+ */
+ _mafNamespacePrefix: "http://maf.mozdev.org/metadata/rdf#",
+
+ /**
+ * Name for the MAF namespace in RDF/XML files.
+ */
+ _mafNamespaceName: "MAF",
+
+ /**
+ * Returns a string with the contents of the provided nsIFile, read using the
+ * specified encoding. An exception will be raised if any character in the
+ * file is not encoded properly.
+ */
+ _readEntireFile: function(aFile, aEncoding) {
+ // Create and initialize an input stream to read from the provided file.
+ var inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ inputStream.init(aFile, -1, 0, 0);
+ try {
+ // Create and initialize a converter that will raise an exception if any
+ // portion of the file is not valid according to the specified encoding.
+ var convInputStream = Cc["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Ci.nsIConverterInputStream);
+ convInputStream.init(inputStream, aEncoding, 0, 0);
+ try {
+ // Read as much of the file as possible in one go. According to the
+ // converter input stream interface, readString may return less bytes
+ // than expected, and must be called until it returns 0 to signify the
+ // end of the file. This loop is also required to properly raise an
+ // exception if the file is not valid according to the encoding, as the
+ // first call will only return the portion of the file that precedes
+ // the faulty character.
+ var entireContents = "";
+ var readContentsObject = {};
+ while (convInputStream.readString(0xFFFFFFFF, readContentsObject)) {
+ entireContents += readContentsObject.value;
+ }
+ // Return the entire contents to the caller.
+ return entireContents;
+ } finally {
+ // Close the converter stream before returning or in case of exception.
+ convInputStream.close();
+ }
+ } finally {
+ // Close the underlying stream. This instruction has no effect if the
+ // converter stream has been already closed successfully.
+ inputStream.close();
+ }
+ },
+
+ /**
+ * Parse the provided RDF/XML string and feed the results to this data source.
+ *
+ * Relative RDF resource URLs in the provided XML string are not resolved, and
+ * the declared XML namespaces are propagated to the data source.
+ */
+ _feedString: function(aXmlContents) {
+ var emptyUri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newURI("urn:none", null, null);
+ var rdfXmlParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser);
+ rdfXmlParser.parseString(this._wrappedObject, emptyUri, aXmlContents);
+ },
+
+ /**
+ * Returns an RDF/XML string representing an empty data source with the proper
+ * MAF XML namespace declarations.
+ */
+ _getMafNamespaceXml: function() {
+ // Create an RDF/XML serializer for an empty data source.
+ var emptyDataSource =
+ Cc["@mozilla.org/rdf/datasource;1?name=xml-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ var serializer = Cc["@mozilla.org/rdf/xml-serializer;1"].
+ createInstance(Ci.nsIRDFXMLSerializer);
+ serializer.init(emptyDataSource);
+ // Add the MAF namespace to the serializer.
+ var mafNamespaceAtom = Cc["@mozilla.org/atom-service;1"].
+ getService(Ci.nsIAtomService).getAtom(this._mafNamespaceName);
+ serializer.addNameSpace(mafNamespaceAtom, this._mafNamespacePrefix);
+ // Run the serializer using an output stream implemented in JavaScript.
+ var mafNamespaceXml = "";
+ serializer.QueryInterface(Ci.nsIRDFXMLSource).Serialize({
+ write: function(aBuf, aCount) {
+ mafNamespaceXml += aBuf;
+ return aCount;
+ }
+ });
+ // Return the generated string.
+ return mafNamespaceXml;
+ },
+}
diff --git a/src/chrome/content/engine/MhtmlArchive.js b/src/chrome/content/engine/MhtmlArchive.js
new file mode 100644
index 0000000..ae629ee
--- /dev/null
+++ b/src/chrome/content/engine/MhtmlArchive.js
@@ -0,0 +1,372 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents an MHTML web archive.
+ *
+ * This class derives from Archive. See the Archive documentation for details.
+ *
+ * @param aFile
+ * nsIFile representing the compressed archive. The file usually ends
+ * with the ".mht" or ".mhtml" extension.
+ */
+function MhtmlArchive(aFile) {
+ Archive.call(this);
+ this.file = aFile;
+
+ // Initialize member variables explicitly for proper inheritance.
+ this._resourcesByContentLocation = {};
+ this._resourcesByContentId = {};
+}
+
+MhtmlArchive.prototype = {
+ __proto__: Archive.prototype,
+
+ // Archive
+ extractAll: function() {
+ // Create and initialize the single page object for the archive.
+ var page = this.addPage();
+ page.tempDir = this._tempDir;
+ // Read and parse the contents of the MHTML file.
+ var part = new MimePart();
+ part.text = this._readArchiveFromFile();
+ part = part.promoteToDerivedClass();
+ // Set the metadata on the page object.
+ page.setMetadataFromMimePart(part);
+ // Prepare the extraction of the resources from the archive.
+ this._persistBundle = new PersistBundle();
+ this._persistFolder = new PersistFolder(this._tempDir);
+ // For MAF-specific MHMTL archives.
+ if (part.headersByLowercaseName["x-maf"] && part.parts) {
+ // Archives with the "X-MAF" header are either composed by one content
+ // part only, or by a single "multipart/related" MIME part that contains
+ // one content part for each file. In this case, the root part of the
+ // MHTML file is the first content part, and the other parts should be
+ // extracted locally while respecting the content locations specified in
+ // their "Content-Location" headers, which are always relative to the
+ // location of the root part.
+ this._collectResourcesFromMafPart(part);
+ } else {
+ // This is a normal MHTML archive. Build the base URL that is used to
+ // resolve relative references when there is no URI specified in the
+ // "Content-Location" headers. Instead of the URL "thismessage:/", the
+ // "resource:///" URL is used for the same purpose. Resource URLs are used
+ // only during resolution of relative references, and are never
+ // substituted in message bodies.
+ var baseUrl = this._ioService.newURI("resource:///", null, null);
+ // Collect resources recursively, and look for a root resource.
+ this._collectResourcesFromPart(part, baseUrl, true);
+ // Convert all the URIs in the content that reference resources that are
+ // available in the MHTML file, and resolve relative URIs based on the
+ // original locations of the saved files.
+ this._indexResourceLocations();
+ this._replaceContentUris();
+ }
+ // Set the metadata about the root resource.
+ var resource = this._persistBundle.resources[0];
+ page.indexLeafName = resource.file.leafName;
+ page.originalUrl = "Unknown";
+ if (resource.originalUri && !resource.originalUri.schemeIs("resource")) {
+ page.originalUrl = resource.originalUri.spec;
+ } else if (resource.contentLocation) {
+ page.originalUrl = resource.contentLocation;
+ }
+ // Save the resources locally.
+ this._persistBundle.writeAll();
+ },
+
+ // Archive
+ _newPage: function() {
+ return new MhtmlArchivePage(this);
+ },
+
+ /**
+ * PersistBundle object with the resources collected from the MHTML file.
+ */
+ _persistBundle: null,
+
+ /**
+ * PersistFolder object that is used to determine the local file names of
+ * resources in the MHTML file.
+ */
+ _persistFolder: null,
+
+ /**
+ * Collects all the resources from the given MIME part, which is a multipart
+ * message part of an MHTML file in the MAF-specific format, and stores them
+ * in the PersistBundle object associated with the current archive.
+ */
+ _collectResourcesFromMafPart: function(aMimePart) {
+ // Find the local file URL associated with the temporary directory.
+ var folderUrl = this._ioService.newFileURI(this._tempDir).
+ QueryInterface(Ci.nsIURL);
+ // Ensure that the local URL points to a directory.
+ if (folderUrl.path.slice(-1) !== "/") {
+ folderUrl.path += "/";
+ }
+ // Examine each content part in the MHTML file.
+ for (var [partIndex, contentPart] in Iterator(aMimePart.parts)) {
+ // Collect the resource in the PersistBundle object. The first resource
+ // is always the root resource.
+ var res = this._collectResourceFromContentPart(contentPart, !partIndex);
+ if (partIndex > 0) {
+ // For other resources, use the relative content location.
+ var location = contentPart.headersByLowercaseName["content-location"];
+ var fileUrl = this._ioService.newURI(location, null, folderUrl);
+ // The following function checks whether fileUrl is located under the
+ // folder represented by folderUrl.
+ if (folderUrl.getCommonBaseSpec(fileUrl) !== folderUrl.spec) {
+ throw new Components.Exception("Invalid relative content location");
+ }
+ // Update the local file name for saving the resource.
+ res.file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
+ }
+ }
+ },
+
+ /**
+ * Collects all the resources from the given MIME part, which is either a
+ * multipart or a message part of an MHTML file, and stores them in the
+ * PersistBundle object associated with the current archive. The given URL is
+ * used to resolve relative references in content locations.
+ */
+ _collectResourcesFromPart: function(aMimePart, aBaseUrl, aIsRootCandidate) {
+ // Resolve the content location for the current part.
+ var location = aMimePart.headersByLowercaseName["content-location"];
+ if (location) {
+ aMimePart.resolvedLocation = this._ioService.newURI(location, null,
+ aBaseUrl);
+ } else {
+ aMimePart.resolvedLocation = aBaseUrl;
+ }
+ // If this is a multipart MIME part
+ if (aMimePart.parts) {
+ // If required, find a root part candidate among the immediate children.
+ var startPart = aIsRootCandidate ? aMimePart.startPart : null;
+ // Collect all the children.
+ for (let [, contentPart] in Iterator(aMimePart.parts)) {
+ // Use the resolved URL of this part as a base URL for child parts, and
+ // indicate if the current part is a candidate for containing the root
+ // part or for being considered the root part itself.
+ this._collectResourcesFromPart(contentPart, aMimePart.resolvedLocation,
+ contentPart === startPart);
+ }
+ } else {
+ // Collect the resource associated with the content part. If a content
+ // part is a root candidate, then it is the actual root resource.
+ this._collectResourceFromContentPart(aMimePart, aIsRootCandidate);
+ }
+ },
+
+ /**
+ * Creates a new PersistResource object initialized with the information from
+ * the given MIME part, which is a content part. The resource is both added to
+ * the current PersistBundle object and returned by the function.
+ */
+ _collectResourceFromContentPart: function(aMimePart, aIsRootPart) {
+ // Create a new resource and initialize its contents.
+ var resource = new PersistResource();
+ resource.body = aMimePart.body;
+ // Set the MIME media type for the resource. If no media type is specified,
+ // use an appropriate default depending on whether this is the root part.
+ resource.mimeType = aMimePart.mediaType || (aIsRootPart ? "text/html" :
+ "application/octet-stream");
+ // Store the content location and identifier for later, if present.
+ resource.originalUri = aMimePart.resolvedLocation || null;
+ resource.contentId = aMimePart.headersByLowercaseName["content-id"];
+ // Determine the actual file name for the resource. For the root resource,
+ // the local base name "index" is always used.
+ resource.contentLocation = aIsRootPart ? "index" :
+ (aMimePart.resolvedLocation && aMimePart.resolvedLocation.spec);
+ this._persistFolder.addUnique(resource);
+ // Add this resource as the first or the last resource in the bundle.
+ if (aIsRootPart) {
+ this._persistBundle.resources.unshift(resource);
+ } else {
+ this._persistBundle.resources.push(resource);
+ }
+ // Return the resource for further manipulation if necessary.
+ return resource;
+ },
+
+ /**
+ * Associates the values of the "Content-Location" headers with the resources
+ * collected from the MHTML file.
+ */
+ _resourcesByContentLocation: {},
+
+ /**
+ * Associates the values of the "Content-ID" headers with resources collected
+ * from the MHTML file.
+ */
+ _resourcesByContentId: {},
+
+ /**
+ * Populates the associative arrays that are used to index resources by
+ * content identifiers or content locations.
+ */
+ _indexResourceLocations: function(aMimePart) {
+ // Examine each resource from the MHTML file.
+ for (var [, resource] in Iterator(this._persistBundle.resources)) {
+ // Create the entry in the associative array for content locations.
+ if (resource.originalUri) {
+ var compareUri = resource.originalUri.clone();
+ try {
+ // If the URI has URL syntax, remove the hash part.
+ compareUri = compareUri.QueryInterface(Ci.nsIURL);
+ compareUri.ref = "";
+ } catch (e) {
+ // In case of errors, use the original URI.
+ }
+ this._resourcesByContentLocation[compareUri.spec] = resource;
+ }
+ // Create the entry in the associative array for content identifiers.
+ var contentId = (resource.contentId || "").replace(/^<|>$/g, "");
+ if (contentId) {
+ this._resourcesByContentId[contentId] = resource;
+ }
+ }
+ },
+
+ /**
+ * Replaces all the URIs in the contents of the available resources with
+ * absolute ones, converting them to local file URLs if required.
+ */
+ _replaceContentUris: function() {
+ // Examine each resource from the MHTML file.
+ for (var [, resource] in Iterator(this._persistBundle.resources)) {
+ // Parse the body of the resource appropriately.
+ var entireSourceFile;
+ if (resource.mimeType == "text/html" ||
+ resource.mimeType == "application/xhtml+xml") {
+ // Use the HTML parser.
+ entireSourceFile = new HtmlSourceFragment(resource.body);
+ } else if (resource.mimeType == "text/css") {
+ // Use the CSS parser.
+ entireSourceFile = new CssSourceFragment(resource.body);
+ } else {
+ // The type is not recognized, the resource does not need modification.
+ continue;
+ }
+ // Search for all the URIs contained in the resource.
+ for (var curFragment in entireSourceFile) {
+ if (curFragment instanceof UrlSourceFragment) {
+ // Resolve the current URI to an absolute value. This operation does
+ // not consider base URLs specified in the content at present.
+ var fragmentUri = null;
+ try {
+ fragmentUri = this._ioService.newURI(curFragment.urlSpec, null,
+ resource.originalUri);
+ } catch (e) {
+ // The URI cannot be resolved to an absolute location.
+ }
+ // If an absolute URI was found
+ if (fragmentUri) {
+ var mapResource;
+ var hashPart = "";
+ // If this is a "cid:" URI
+ if (fragmentUri.schemeIs("cid")) {
+ // Perform a lookup using the content identifier.
+ contentId = fragmentUri.spec.slice("cid:".length);
+ mapResource = this._resourcesByContentId[contentId];
+ } else {
+ // Perform a lookup using the content location.
+ var compareUri = fragmentUri.clone();
+ try {
+ // If the URI has URL syntax, remove the hash part.
+ compareUri = compareUri.QueryInterface(Ci.nsIURL);
+ hashPart = compareUri.ref;
+ compareUri.ref = "";
+ } catch (e) {
+ // In case of errors, use the original URI.
+ }
+ mapResource = this._resourcesByContentLocation[compareUri.spec];
+ }
+ // If the URI points to a resource available in the MHTML file
+ if (mapResource) {
+ // Update the URI to point to the local file.
+ fragmentUri = this._ioService.newFileURI(mapResource.file).
+ QueryInterface(Ci.nsIURL);
+ // Add the hash part if required.
+ if (hashPart) {
+ fragmentUri.ref = hashPart;
+ }
+ }
+ // Update the actual URI in the content, unless the new URI resolves
+ // to a virtual location inside the MHMTL file.
+ if (!fragmentUri.schemeIs("resource")) {
+ curFragment.urlSpec = fragmentUri.spec;
+ }
+ }
+ }
+ }
+ // Update the body of the resource.
+ resource.body = entireSourceFile.sourceData;
+ }
+ },
+
+ /**
+ * Returns the contents read from the local archive file.
+ */
+ _readArchiveFromFile: function() {
+ // Create and initialize an input stream to read from the local file.
+ var inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ inputStream.init(this.file, -1, 0, 0);
+ try {
+ // Create and initialize a scriptable binary stream reader.
+ var binInputStream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ binInputStream.setInputStream(inputStream);
+ try {
+ // Read the entire file and return its contents. If the file is 4 GiB or
+ // more in size, an exception will be raised.
+ return binInputStream.readBytes(this.file.fileSize);
+ } finally {
+ // Close the binary stream before returning or in case of exception.
+ binInputStream.close();
+ }
+ } finally {
+ // Close the underlying stream. This instruction has no effect if the
+ // binary stream has been already closed successfully.
+ inputStream.close();
+ }
+ },
+
+ _ioService: Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService),
+}
diff --git a/src/chrome/content/engine/MhtmlArchivePage.js b/src/chrome/content/engine/MhtmlArchivePage.js
new file mode 100644
index 0000000..b5d6ec0
--- /dev/null
+++ b/src/chrome/content/engine/MhtmlArchivePage.js
@@ -0,0 +1,277 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents the complete web page contained within an MHTML web archive.
+ *
+ * This class derives from ArchivePage. See the ArchivePage documentation for
+ * details.
+ */
+function MhtmlArchivePage(aArchive) {
+ ArchivePage.call(this, aArchive);
+
+ // Initialize member variables explicitly for proper inheritance.
+ this._browserObjectForMetadata = null;
+}
+
+MhtmlArchivePage.prototype = {
+ __proto__: ArchivePage.prototype,
+
+ /**
+ * Stores the page into the archive file asynchronously. When the operation is
+ * completed, the onArchivingComplete method of the provided object is called,
+ * passing the error code as its first argument.
+ */
+ asyncSave: function(aCallbackObject) {
+ // Determine if the content was saved using the original content locations.
+ var persistObject = aCallbackObject.persistObject;
+ var useContentLocation = persistObject && (persistObject.persistBundle ||
+ persistObject.originalUriByPath) && persistObject.saveWithContentLocation;
+ // Collect the support files associated with this archiving operation.
+ var archiveBundle = new PersistBundle();
+ var originalBundle = useContentLocation && persistObject.persistBundle;
+ if (originalBundle) {
+ // Reuse the resources from the PersistBundle generated by the save
+ // component, excluding the resources that couldn't be saved locally.
+ archiveBundle.scanBundle(originalBundle);
+ } else {
+ // Collect the saved files from the temporary directory, and use the
+ // metadata generated by the Save Complete component if available.
+ archiveBundle.scanFolder(this.tempDir, useContentLocation &&
+ persistObject.originalUriByPath);
+ }
+ // Build a standard or MAF-specific MHTML message from the collected files.
+ var mhtmlMessage = this._buildMessage(archiveBundle, !useContentLocation);
+ // Write the MHTML archive to disk.
+ this._writeArchive(mhtmlMessage.text);
+ // Notify that the archiving operation is completed.
+ aCallbackObject.onArchivingComplete(0);
+ },
+
+ /**
+ * Reads the metadata about the current page from the specified MIME message.
+ */
+ setMetadataFromMimePart: function(aMimePart) {
+ // Shorthand for the object containing the unfolded headers in the MIME part.
+ var headers = aMimePart.headersByLowercaseName;
+ // The presence of the "X-MAF" or "X-MAF-Version" headers indicate that the
+ // "Subject" header is encoded on a single line, probably using UTF-8.
+ if (headers["x-maf"] || headers["x-maf-version"]) {
+ this.title = headers.subject || "";
+ // Convert the raw subject header value using UTF-8, if possible.
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ try {
+ this.title = converter.ConvertToUnicode(this.title);
+ } catch (e) {
+ // If the subject line is not valid UTF-8, use the original bytes, that
+ // are assumed to be in the ISO-8859-1 character set.
+ }
+ } else {
+ // The subject is properly encoded as an unstructured value.
+ this.title = MimeSupport.parseUnstructuredValue(headers.subject || "");
+ }
+ // Parse the date from the "Date" header.
+ this.dateArchived = headers.date || null;
+ },
+
+ /**
+ * Returns a MimePart object containing the entire encoded MHMTL message
+ * corresponding to the given resources.
+ *
+ * @param aPersistBundle
+ * The resources to encode as a MIME message.
+ * @param aUseMafVariant
+ * True if the resources have been prepared without using
+ * MHTML-compatible content locations. If true, the "Content-Location"
+ * and "Subject" headers will be compatible with the Mozilla Archive
+ * Format extension, but not with other browsers.
+ */
+ _buildMessage: function(aPersistBundle, aUseMafVariant) {
+ // Identify the root resource in the given bundle.
+ var rootResource = aPersistBundle.resources[0];
+
+ // When saving an archived page, the content location to use may be
+ // different from the one the root resource was currently saved from.
+ if (this.originalUrl) {
+ // Set the new content location. Since this value may have been copied
+ // from the metadata of another page, the resulting content location may
+ // not be a valid absolute or relative URL. As a basic validation, ensure
+ // at least that the new value contains printable ASCII characters only.
+ rootResource.contentLocation = this.originalUrl.
+ replace(/[^\x20-\x7E]+/g, "_");
+ }
+
+ // Create a new MIME message with normal or multipart content type.
+ var isMultipart = (aPersistBundle.resources.length > 1);
+ var mimeMessage = new (isMultipart ? MultipartMimePart : MimePart)();
+
+ // Add the general message headers.
+ mimeMessage.addRawHeader("From", this._getRawFromHeaderValue());
+ if (aUseMafVariant) {
+ // Add the custom version of the "Subject" header.
+ mimeMessage.addRawHeader("Subject",
+ this._getRawMafSubjectHeaderValue(this.title || "Unknown"));
+ } else {
+ // Add the "Subject" header with the proper encoding.
+ mimeMessage.addUnstructuredHeader("Subject", this.title || "");
+ }
+ if (this.dateArchived) {
+ mimeMessage.addRawHeader("Date", MimeSupport.getDateTimeSpecification(
+ this.dateArchived));
+ }
+ mimeMessage.addRawHeader("MIME-Version", "1.0");
+
+ // Add the content headers and the actual content.
+ if (isMultipart) {
+ // If the MAF variant of the MHTML format is used, specify a global
+ // content location for the message. This information will be used by
+ // other browsers to resolve relative references in content parts, and
+ // display the document correctly if possible.
+ if (aUseMafVariant) {
+ mimeMessage.addRawHeader("Content-Location",
+ rootResource.contentLocation);
+ }
+ // Add the content headers for the multipart type.
+ mimeMessage.addRawHeader("Content-Type",
+ 'multipart/related;\r\n\t' +
+ 'type="' + rootResource.mimeType + '";\r\n\t' +
+ 'boundary="' + mimeMessage.boundary + '"');
+ // Add the content parts with their own content headers.
+ for (var [, resource] in Iterator(aPersistBundle.resources)) {
+ var childPart = new MimePart();
+ this._setMimePartContent(childPart, resource);
+ mimeMessage.parts.push(childPart);
+ }
+ } else {
+ // Add the content headers and the content to the message.
+ this._setMimePartContent(mimeMessage, rootResource);
+ }
+
+ // If the MAF variant of the MHTML format is used, the "X-MAF" message
+ // header must be present to inform the recipient that content locations
+ // must be interpreted as relative to the root folder. For compatible MHTML
+ // files, instead, the "X-MAF" header must not be present, and in this case
+ // the "X-MAF-Information" header is added to store the version of MAF used
+ // for saving, that may be needed to help with decoding in the future. This
+ // version of MAF never includes the "X-MAF-Version" header, that indicates
+ // that the content locations conform to the original specification, but the
+ // "Subject" header is encoded on a single line using UTF-8.
+ var mafHeaderName = aUseMafVariant ? "X-MAF" : "X-MAF-Information";
+ mimeMessage.addRawHeader(mafHeaderName, this._getRawVersionHeaderValue());
+
+ // Return the newly built message.
+ return mimeMessage;
+ },
+
+ /**
+ * Returns the encoded string to be used in the "From" header of saved files.
+ */
+ _getRawFromHeaderValue: function() {
+ // Get the source object for the browser's version information.
+ var nav = Cc["@mozilla.org/appshell/appShellService;1"].
+ getService(Ci.nsIAppShellService).hiddenDOMWindow.navigator;
+ // Return the version information, assuming it is compatible with the
+ // encoding required for the structured "From" header.
+ return "";
+ },
+
+ /**
+ * Returns the encoded string to be used in the version header of saved files.
+ */
+ _getRawVersionHeaderValue: function() {
+ // Return the version information, which contains ASCII characters only.
+ // Note that the version information might be empty if the save operation
+ // was started shortly after application startup.
+ return "Produced By MAF V" + StartupInitializer.addonVersion;
+ },
+
+ /**
+ * Returns the string to be used in the "Subject" header when building the
+ * variant of MHTML that is not fully compatible with other browsers.
+ */
+ _getRawMafSubjectHeaderValue: function(aSubject) {
+ // Initialize the UTF-8 converter for the subject line.
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ // Build the subject as a single line of characters.
+ return (converter.ConvertFromUnicode(aSubject) + converter.Finish()).
+ replace(/[\r\n]+/g, "");
+ },
+
+ /**
+ * Adds to the given MIME part the body of the given resource, as well as the
+ * relevant headers describing it. The resource must have a MIME type and a
+ * content location set.
+ */
+ _setMimePartContent: function(aMimePart, aResource) {
+ // Add the content type header first.
+ aMimePart.addRawHeader("Content-Type", aResource.mimeType +
+ (aResource.charset ? (';\r\n\tcharset="' + aResource.charset + '"') : ""));
+ // Select the appropriate encoding for the body based on the MIME type.
+ var encoding = ["text/html", "application/xhtml+xml", "image/svg+xml",
+ "text/xml", "application/xml", "text/css", "text/javascript",
+ "application/x-javascript"].indexOf(aResource.mimeType) >= 0 ?
+ "quoted-printable" : "base64";
+ aMimePart.addRawHeader("Content-Transfer-Encoding", encoding);
+ // Add the content location header.
+ aMimePart.addRawHeader("Content-Location", aResource.contentLocation);
+ // Set the body of the MIME part, using the selected encoding.
+ aMimePart.contentTransferEncoding = encoding;
+ aMimePart.body = aResource.body;
+ },
+
+ /**
+ * Saves the given text in the current archive file.
+ */
+ _writeArchive: function(aContents) {
+ // Create and initialize an output stream to write to the archive file.
+ var outputStream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ outputStream.init(this.archive.file, -1, -1, 0);
+ try {
+ // Write the entire file to disk at once. If the content to be written is
+ // 4 GiB or more in size, an exception will be raised.
+ outputStream.write(aContents, aContents.length);
+ } finally {
+ // Close the underlying stream.
+ outputStream.close();
+ }
+ },
+}
diff --git a/src/chrome/content/engine/MimePart.js b/src/chrome/content/engine/MimePart.js
new file mode 100644
index 0000000..dc01f13
--- /dev/null
+++ b/src/chrome/content/engine/MimePart.js
@@ -0,0 +1,204 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents a single MIME message, or a content part in a multipart message.
+ */
+function MimePart() {
+ // Initialize member variables explicitly.
+ this.headersByLowercaseName = {};
+ this.contentTypeParameters = {};
+}
+
+MimePart.prototype = {
+ /**
+ * Raw octets with the concatenation of the header and body sections.
+ */
+ get text() {
+ return this.headerSection + "\r\n" + this.bodySection;
+ },
+ set text(aValue) {
+ // Find the two consecutive line breaks, or the line break at the beginning
+ // of the text, separating the headers from the body.
+ var headerSectionRe = /^(\r\n?|\n)|(\r\n?|\n)(\r\n?|\2)/g;
+ headerSectionRe.lastIndex = 0;
+ var matchResult = headerSectionRe.exec(aValue);
+ if (!matchResult) {
+ // If a match isn't found, only headers are present in this MIME part.
+ this.headerSection = aValue;
+ this.bodySection = "";
+ } else {
+ // Create the header and body sections. The last line separator in the
+ // header section is not included, since at present the header section is
+ // not re-encoded in any case.
+ this.headerSection = aValue.slice(0, matchResult.index);
+ this.bodySection = aValue.slice(headerSectionRe.lastIndex);
+ }
+ // Separate and unfold the various headers.
+ this.headersByLowercaseName = MimeSupport.collectHeadersFromSection(
+ this.headerSection);
+ // Parse the value of the "Content-Type" header, if present.
+ var contentTypeValue = this.headersByLowercaseName["content-type"];
+ this.contentTypeParameters = {};
+ if (contentTypeValue) {
+ this.mediaType = MimeSupport.parseContentTypeValue(contentTypeValue,
+ this.contentTypeParameters);
+ } else {
+ this.mediaType = "";
+ }
+ // Parse the value of the "Content-Transfer-Encoding" header, if present.
+ this.contentTransferEncoding =
+ (this.headersByLowercaseName["content-transfer-encoding"] || "").
+ replace(/^\s+|\s+$/g, "").toLowerCase();
+ },
+
+ /**
+ * Raw octets with the encoded header section of the MIME part. If at least
+ * one header is present, a single trailing CRLF sequence is also present.
+ */
+ headerSection: "",
+
+ /**
+ * Raw octets with the encoded body of the MIME part.
+ */
+ bodySection: "",
+
+ /**
+ * This object contains a lowercase property for every header contained in the
+ * header section of the MIME part. The value of each property is the raw
+ * text of the unfolded header.
+ */
+ headersByLowercaseName: {},
+
+ /**
+ * Lowercase media type contained in the value of the "Content-Type" header.
+ */
+ mediaType: "",
+
+ /**
+ * This object contains a lowercase property for every parameter contained in
+ * the value of the "Content-Type" header. The value of each property is the
+ * decoded value of the parameter.
+ */
+ contentTypeParameters: {},
+
+ /**
+ * Lowercase string representing the encoding to use for the body of the part.
+ * The empty string indicates that the "7bit" default encoding will be used.
+ * For more information, see
+ * (retrieved 2009-11-15).
+ */
+ contentTransferEncoding: "",
+
+ /**
+ * Raw octets with the decoded body of the MIME part.
+ */
+ get body() {
+ switch (this.contentTransferEncoding) {
+ case "quoted-printable":
+ return MimeSupport.decodeQuotedPrintable(this.bodySection);
+ case "base64":
+ return MimeSupport.decodeBase64(this.bodySection);
+ default:
+ return this.bodySection;
+ }
+ },
+ set body(aValue) {
+ // Decide the transformation to apply based on contentTransferEncoding.
+ switch (this.contentTransferEncoding) {
+ case "quoted-printable":
+ this.bodySection = MimeSupport.encodeQuotedPrintable(aValue);
+ break;
+ case "base64":
+ this.bodySection = MimeSupport.encodeBase64(aValue);
+ break;
+ default:
+ this.bodySection = aValue;
+ }
+ },
+
+ /**
+ * Adds the provided encoded header to the header section.
+ *
+ * @param aHeaderName
+ * Valid name of the header.
+ * @param aHeaderValue
+ * Value of the header. If the value spans multiple lines, it must
+ * already be properly folded.
+ */
+ addRawHeader: function(aHeaderName, aHeaderValue) {
+ this.headerSection += aHeaderName + ": " + aHeaderValue + "\r\n";
+ },
+
+ /**
+ * Adds the provided unstructured header to the header section.
+ *
+ * @param aHeaderName
+ * Valid name of the header.
+ * @param aUnstructuredValue
+ * Unicode string with the value of the header.
+ */
+ addUnstructuredHeader: function(aHeaderName, aUnstructuredValue) {
+ this.headerSection += aHeaderName + ": " +
+ MimeSupport.buildUnstructuredValue(aUnstructuredValue, "utf-8",
+ aHeaderName.length + 2) + "\r\n";
+ },
+
+ /**
+ * If this object represents a multipart MIME part, returns an instance of a
+ * MultipartMimePart object that can be used to further analyze the contents
+ * of the part, otherwise returns this object unaltered.
+ */
+ promoteToDerivedClass: function() {
+ // If the media type is not multipart, exit now.
+ if (this.mediaType.slice(0, "multipart/".length) !== "multipart/") {
+ return this;
+ }
+ // Create a new multipart class and propagate the relevant properties.
+ var newPart = new MultipartMimePart();
+ // Set the metadata properties first.
+ newPart.headerSection = this.headerSection;
+ newPart.headersByLowercaseName = this.headersByLowercaseName;
+ newPart.mediaType = this.mediaType;
+ newPart.contentTypeParameters = this.contentTypeParameters;
+ newPart.contentTransferEncoding = this.contentTransferEncoding;
+ newPart.boundary = this.contentTypeParameters.boundary;
+ // Set the body and parse it using the provided metadata.
+ newPart.bodySection = this.bodySection;
+ return newPart;
+ },
+}
diff --git a/src/chrome/content/engine/MimeSupport.js b/src/chrome/content/engine/MimeSupport.js
new file mode 100644
index 0000000..5f008a0
--- /dev/null
+++ b/src/chrome/content/engine/MimeSupport.js
@@ -0,0 +1,963 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * The MimeSupport global object provides helper functions for handling various
+ * MIME-related tasks.
+ */
+var MimeSupport = {
+ /**
+ * Returns the given string of bytes encoded to "Quoted-Printable". For more
+ * information on the "Quoted-Printable" encoding specification, see
+ * (retrieved 2008-05-14).
+ *
+ * @param aOctets
+ * String containing the octets to be encoded. Every single character
+ * in this string must have a character code between 0 and 255.
+ */
+ encodeQuotedPrintable: function(aOctets) {
+ // Encode the mandatory characters. Octets with decimal values of 33 through
+ // 60, inclusive, and 62 through 126, inclusive, are not encoded. Spaces,
+ // tabs, and line breaks will be encoded or normalized later, if necessary.
+ var aEncodedLines = aOctets.replace(
+ /[^\t\r\n \x21-\x3C\x3E-\x7E]/g,
+ function (aMatch) {
+ // Convert the octet to hexadecimal representation.
+ var hexString = "0" + aMatch.charCodeAt(0).toString(16).toUpperCase();
+ // Consider only the last two digits of the number.
+ return "=" + hexString.slice(-2);
+ }
+ );
+
+ // Limit the final line length to 76 characters, adding soft line breaks if
+ // necessary. Also convert every type of line break to CRLF. Since the
+ // regular expression used for the task cannot handle strings that don't end
+ // with a line break, add one now and remove it at the end.
+ return (aEncodedLines + "\r\n").replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aMain ( [^\r\n]{0,73} )
+ *
+ * The first 73 characters of each line, up to and excluding the end of
+ * line character, if present.
+ *
+ * aLastThree ( =.. or [ \t] or [^\r\n=]{2}[^\t\r\n =] )
+ *
+ * This group will match only if followed by a line ending. Can be an
+ * encoded octet representation, a space or tab (that will be encoded to
+ * three characters) or a sequence of three characters that does not
+ * contain the beginning of an encoded sequence ("=") and does not end
+ * with a space or tab.
+ *
+ * aLastThreeEOL ( $ or \r?\n or \r )
+ *
+ * Line ending after aLastThree. This group is empty only if the end of
+ * the string is reached.
+ *
+ * aLastTwo ( [^\t\r\n =]{0,2} )
+ *
+ * Up to two characters that normally precede a soft line break. None of
+ * these characters will need further encoding.
+ *
+ * aLastTwoEOL ( \r?\n? )
+ *
+ * Optional line ending. If this group matches, then no soft line break is
+ * needed.
+ */
+ /([^\r\n]{0,73})(?:(=..|[ \t]|[^\r\n=]{2}[^\t\r\n =])($|\r?\n|\r)|([^\t\r\n =]{0,2})(\r?\n?))/g,
+ function (aAll, aMain, aLastThree, aLastThreeEOL, aLastTwo, aLastTwoEOL,
+ aOffset) {
+ // Compose the main text of the line.
+ var line = aMain + (aLastThree || "") + (aLastTwo || "");
+ // If a line break was found in the original string
+ if (aLastThreeEOL || aLastTwoEOL) {
+ // If the last character in the line is a tab or a space and no soft
+ // line break will be added, encode the character.
+ if (line) {
+ var lastChar = line[line.length - 1];
+ if (lastChar === " " || lastChar === "\t") {
+ line = line.slice(0, -1) + (lastChar === " " ? "=20" : "=09");
+ }
+ }
+ // Return the line followed by a hard line break.
+ return line + "\r\n";
+ }
+ // Return the line followed by a soft line break. Since the regular
+ // expression also matches the empty string, this function is called
+ // one last time with empty parameters. In that case, do not add the
+ // soft line break.
+ return line ? (line + "=\r\n") : "";
+ }
+ ).slice(0, -2);
+ },
+
+ /**
+ * Returns a string containing the sequence of octets decoded from the given
+ * "Quoted-Printable"-encoded ASCII string.
+ *
+ * If the input string contains invalid characters or sequences, they are
+ * propagated to the output without errors. End-of-line character sequences in
+ * the input string are not altered when they are copied to the output.
+ *
+ * @param aAsciiString
+ * "Quoted-Printable"-encoded string to be decoded. The string may
+ * contain mixed CR, LF or CRLF end-of-line sequences.
+ */
+ decodeQuotedPrintable: function(aAsciiString) {
+ // Replace every soft line break and encoded character in the string. Soft
+ // line breaks are represented by an equal sign ("=") followed by any valid
+ // end-of-line sequence, while encoded characters are represented by an
+ // equal sign immediately followed by two hexadecimal digits, either
+ // uppercase or lowercase.
+ return aAsciiString.replace(
+ /=(?:\r?\n|\r|([A-Fa-f0-9]{2}))/g,
+ function(aAll, aEncodedOctet) {
+ return (aEncodedOctet ?
+ String.fromCharCode(parseInt(aEncodedOctet, 16)) : "");
+ }
+ );
+ },
+
+ /**
+ * Returns the given string of bytes encoded to "base64". For more
+ * information on the "base64" encoding specification, see
+ * (retrieved 2008-05-14).
+ *
+ * @param aOctets
+ * String containing the octets to be encoded. Every single character
+ * in this string must have a character code between 0 and 255.
+ */
+ encodeBase64: function(aOctets) {
+ // Encode to base64, and return the resulting string split across lines that
+ // are no longer than 76 characters.
+ return btoa(aOctets).replace(/.{76}/g, "$&\r\n");
+ },
+
+ /**
+ * Returns a string containing the sequence of octets decoded from the given
+ * "base64"-encoded ASCII string.
+ *
+ * Invalid characters and line breaks in the input string are filtered out.
+ *
+ * @param aAsciiString
+ * "base64"-encoded string to be decoded.
+ */
+ decodeBase64: function(aAsciiString) {
+ // Pass only the valid characters to the decoding function.
+ return atob(aAsciiString.replace(/[^A-Za-z0-9+\/=]+/g, ""));
+ },
+
+ /**
+ * Returns the given string of bytes encoded to "Q" encoding. For more
+ * information, see
+ * (retrieved 2009-11-12).
+ *
+ * @param aOctets
+ * String containing the octets to be encoded. Every single character
+ * in this string must have a character code between 0 and 255.
+ */
+ encodeQ: function(aOctets) {
+ // Encode the mandatory characters, that is any non-printable character, in
+ // addition to the space character, the underscore and the question mark.
+ return aOctets.replace(
+ /[^\x21-\x3C\x3E\x40-\x5E\x60-\x7E]/g,
+ function (aMatch) {
+ // Encode the space character as an underscore.
+ if (aMatch === " ") {
+ return "_";
+ }
+ // Convert the octet to hexadecimal representation.
+ var hexString = "0" + aMatch.charCodeAt(0).toString(16).toUpperCase();
+ // Consider only the last two digits of the number.
+ return "=" + hexString.slice(-2);
+ }
+ );
+ },
+
+ /**
+ * Returns a string containing the sequence of octets decoded from the given
+ * percent-encoded ASCII string. This function always decodes the entire range
+ * of byte values, and never applies character set conversions.
+ *
+ * If the input string contains invalid characters or sequences, they are
+ * propagated to the output without errors.
+ */
+ decodePercent: function(aAsciiString) {
+ return aAsciiString.replace(
+ /%([A-Fa-f0-9]{2})/g,
+ function(aAll, aEncodedOctet) {
+ return String.fromCharCode(parseInt(aEncodedOctet, 16));
+ }
+ );
+ },
+
+ /**
+ * Returns an object having one property for each header field in the given
+ * header section. For more information on header field syntax, see
+ * (retrieved 2008-05-17).
+ *
+ * The property names in the returned object are the names of the header
+ * fields, converted to lowercase. If more than one header field with the same
+ * name is present in the section, the behavior is undefined.
+ *
+ * The property values correspond to the raw characters in the unfolded
+ * headers. For more information on header folding and unfolding, see
+ * (retrieved 2008-05-17).
+ */
+ collectHeadersFromSection: function(aHeaderSection) {
+ // Remove any line break that is followed by a whitespace character.
+ var unfoldedHeders = aHeaderSection.replace(/(\r?\n|\r)(?=[\t ])/g, "");
+ // Examine each valid header line, that consists of a header name, followed
+ // by a colon, followed by the header value. Header names cannot contain
+ // whitespace. If whitespace is present around the colon or at the end of
+ // the value, it is ignored. Leading whitespace on the first line of the
+ // header section is also ignored. Lines that don't conform to this syntax
+ // are ignored.
+ var headers = {};
+ unfoldedHeders.replace(
+ /^[\t ]*([^\t\r\n :]+)[\t ]*:[\t ]*(.*)/gm,
+ function(aAll, aHeaderName, aHeaderValue) {
+ // Set the property of the object, and remove the trailing whitespace
+ // that may be still present in the header value.
+ headers[aHeaderName.toLowerCase()] = aHeaderValue.replace(/\s+$/, "");
+ }
+ );
+ return headers;
+ },
+
+ /**
+ * Returns an "encoded word" corresponding to the specified string, encoded
+ * using the given character set, or an empty string if the given constraints
+ * cannot be satisfied. For more information on encoded words, see
+ * (retrieved 2009-11-12).
+ *
+ * This function always returns encoded words using the "Q" encoding.
+ *
+ * @param aUnicodeString
+ * String to be encoded. Any character is allowed, even though
+ * characters that cannot be represented using the specified character
+ * set may be replaced.
+ * @param aCharset
+ * Character set to use for encoding the given string.
+ * @param aMaxLength
+ * Maximum length of the returned encoded word, including all
+ * delimiters. This value must be at most 75 characters to achieve
+ * proper results.
+ * @param aRemainder
+ * If the entire string in aUnicodeString does not fit in the allowed
+ * maximum length, the remaining portion is copied to the "value"
+ * property of this object.
+ */
+ encodeWord: function(aUnicodeString, aCharset, aMaxLength, aRemainder) {
+ // Initialize a converter for the specified charset.
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = aCharset;
+ // Set the value that will be returned if even a single character cannot be
+ // encoded while satisfying the given constraint on the maximum length.
+ var lastEncodedWord = "";
+ // Add one character at a time until the specified limit is reached.
+ for (var tryLength = 1; tryLength <= aUnicodeString.length; tryLength++) {
+ // Attempt to encode the initial portion of the string.
+ var tryString = aUnicodeString.slice(0, tryLength);
+ // Convert the characters to octets using the specified charset. Values
+ // that cannot be represented are replaced with a question mark ("?").
+ var octets = converter.ConvertFromUnicode(tryString) + converter.Finish();
+ // Build the entire encoded word using the "Q" encoding.
+ var encodedWord = "=?" + aCharset + "?Q?" + MimeSupport.encodeQ(octets) +
+ "?=";
+ // If the limit of characters to be returned is exceeded
+ if (encodedWord.length > aMaxLength) {
+ // Return the encoded word and the remainder from the previous attempt.
+ break;
+ }
+ // Store the successfully encoded word for later.
+ lastEncodedWord = encodedWord;
+ }
+ // Return the values from the last successful encoding attempt.
+ aRemainder.value = aUnicodeString.slice(tryLength - 1);
+ return lastEncodedWord;
+ },
+
+ /**
+ * Returns an ASCII string that can be used for the encoded value of an
+ * unstructured header field, with header folding already applied. For more
+ * information, see
+ * (retrieved 2009-11-12).
+ *
+ * This function does not encode words made entirely of printable ASCII
+ * characters, and attempts to create "encoded words" with the specified
+ * character set in other cases. For more information on encoded words, see
+ * (retrieved 2009-11-12).
+ * For more information on how the character set should be selected, see
+ * (retrieved 2009-11-12).
+ *
+ * This function does not ensure that character sets that use code-switching
+ * techniques are handled correctly according to section 3 of RFC 2047.
+ *
+ * The text lines generated by this function are limited to 76 characters,
+ * excluding the CRLF line ending, even if no encoded words are created. The
+ * first line may be shorter, based on the aFoldingCount parameter.
+ *
+ * @param aUnicodeString
+ * String to be encoded. Any character is allowed, even though
+ * characters that cannot be represented using the specified character
+ * set may be replaced.
+ * @param aCharset
+ * Character set to use for encoded words.
+ * @param aFoldingCount
+ * Number of characters to subtract from the maximum line length for
+ * the first line of returned text. This value must be at least 1
+ * character to achieve proper results.
+ */
+ buildUnstructuredValue: function(aUnicodeString, aCharset, aFoldingCount) {
+ // Define the absolute maximum limit on the length of the line.
+ const maxLineLimit = 76;
+ // Initialize the length limit for the first line.
+ var lineLimit = maxLineLimit - aFoldingCount;
+
+ // Initialize the line-based output buffers.
+ var resultLines = ""; // ASCII output buffer with completed lines
+ var lineStart = ""; // ASCII initial line part, may be empty at first
+ var wordMiddle = ""; // Unicode source string for the middle of the line
+ var lineMiddle = ""; // ASCII encoded word in the middle of the line
+ var lineEnd = ""; // ASCII part at the end of the line
+
+ // Define a function to apply header folding to the output buffer.
+ var fold = function() {
+ // Concatenate the current line to the output buffer.
+ resultLines += lineStart + lineMiddle + lineEnd + "\r\n";
+ // Reset the character count limit for the next line.
+ lineLimit = maxLineLimit;
+ // Reset the buffer for the current line.
+ lineStart = "";
+ wordMiddle = "";
+ lineMiddle = "";
+ lineEnd = "";
+ };
+
+ // Process individual words separated by whitespace.
+ aUnicodeString.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aWhitespace ( [\t ]* )
+ *
+ * Optional whitespace before the word to be encoded.
+ *
+ * aWord ( [^\t ]*([\t ]+$)? )
+ *
+ * The word that will be examined to determine if it should be encoded.
+ * This word includes trailing whitespace at the end of the string.
+ *
+ * aTrailingWhitespace ( [\t ]+$ )
+ *
+ * Trailing whitespace at the end of the string, if present. Whitespace
+ * between words is included in aWhitespace instead.
+ */
+ /([\t ]*)([^\t ]*([\t ]+$)?)/g,
+ function(aAll, aWhitespace, aWord, aTrailingWhitespace) {
+ // Prepare the variables required to handle all the variants of the
+ // encoding strategy for the current word.
+ var whitespace = aWhitespace;
+ var wordToEncode = aWord;
+ var encodedWord;
+ var remainder = {};
+ var mustFold = false;
+ var outputReady = false;
+
+ // The aWord parameter may be empty only if the string is made entirely
+ // of whitespace, or in the last iteration of the replace function.
+ if (!aWord) {
+ if (!aWhitespace) {
+ // This is the last iteration of the function, no action is needed.
+ return;
+ }
+ // Encode only the whitespace instead of a word.
+ whitespace = "";
+ wordToEncode = aWhitespace;
+ } else {
+ // Determine if the current word must be encoded, because it:
+ // - Contains non-ASCII characters or unprintable characters
+ // - Is the last word and contains trailing whitespace
+ // - Can't fit on a single line, including preceding whitespace
+ // - Can't fit on the first line, and isn't preceded by whitespace
+ // - Begins and ends with reserved character sequences
+ var mustEncode = !/^[\x20-\x7E]+$/.test(aWord) ||
+ aTrailingWhitespace ||
+ aAll.length > maxLineLimit ||
+ (!whitespace && aWord.length > lineLimit) ||
+ (aWord.slice(0, 2) === "=?" && aWord.slice(-2) === "?=");
+
+ // If the word doesn't need encoding
+ if (!mustEncode) {
+ // Fold the initial whitespace if there is not enough room.
+ if ((lineStart + lineMiddle + lineEnd + aAll).length > lineLimit) {
+ fold();
+ }
+ // If no encoded words are present on this line yet, add the plain
+ // word to the initial portion of the line, otherwise add it to the
+ // end. Words at the end may be absorbed by the encoded word later.
+ if (!lineMiddle) {
+ lineStart += aAll;
+ } else {
+ lineEnd += aAll;
+ }
+ // Continue with the next input word.
+ return;
+ }
+ }
+
+ // If another encoded word is already present on the same output line,
+ // and both words encoded together fit on the line, it's generally more
+ // efficient to encode both words together, including any unencoded word
+ // in the middle, to avoid the overhead of a separate character set and
+ // encoding declaration on the same or on the next line.
+ if (lineMiddle) {
+ // Compute the new encoded word. The length limit is at maximum 75
+ // characters, since lineStart contains at least one character.
+ encodedWord = MimeSupport.encodeWord(wordMiddle + lineEnd + aAll,
+ aCharset, lineLimit - lineStart.length, remainder);
+ // If the new encoded word does fit on the current line
+ if (!remainder.value) {
+ // Modify the current output.
+ whitespace = "";
+ wordToEncode = wordMiddle + lineEnd + aAll;
+ // Output for the current line is ready.
+ outputReady = true;
+ }
+ } else if (whitespace) {
+ // If no other encoded word is present, check if the encoded word
+ // alone fits entirely on the current line, without encoding the
+ // preceding whitespace. The length limit is at maximum 75 characters,
+ // since whitespace contains at least one character.
+ encodedWord = MimeSupport.encodeWord(wordToEncode, aCharset,
+ lineLimit - lineStart.length - whitespace.length, remainder);
+ // If the new encoded word does fit on the current line
+ if (!remainder.value) {
+ // Output for the current line is ready.
+ outputReady = true;
+ }
+ }
+
+ // At this point, we can check if the encoded word alone fits entirely
+ // on the next line, without encoding the preceding whitespace. This
+ // operation can be done only if the preceding output word is not
+ // encoded, and cannot be done on the first input word if not preceded
+ // by whitespace.
+ if (!outputReady && (!lineMiddle || lineEnd) && whitespace) {
+ // Compute the new encoded word. The length limit is at most 75
+ // characters, since whitespace contains at least one character.
+ encodedWord = MimeSupport.encodeWord(wordToEncode, aCharset,
+ maxLineLimit - whitespace.length, remainder);
+ // If the new encoded word fits entirely on the next line, or if it
+ // fits partially but an encoded word is not present on this line
+ if (!remainder.value || (encodedWord && !lineMiddle)) {
+ // Terminate the current line.
+ fold();
+ // Output for the next line is ready, including the remainder for
+ // the following line, if present.
+ outputReady = true;
+ }
+ }
+
+ // Unless the attempt to place the word on the next line succeeded, now
+ // we know that we must necessarily encode the word starting from the
+ // current line, wrapping it exactly when it reaches the end of the
+ // available line space.
+ if (!outputReady) {
+ // If another encoded word is present on the same line, we must
+ // concatenate it to the current word before starting, and encode all
+ // the whitespace in-between. Moreover, at this point, we must always
+ // encode the preceding whitespace, if present, if this is the first
+ // word of the entire encoding process.
+ if (lineMiddle || !lineStart) {
+ // Concatenate all the available data.
+ whitespace = "";
+ wordToEncode = wordMiddle + lineEnd + aAll;
+ } else {
+ // We may have to fold the initial whitespace, if present, but we
+ // must encode all of it except the first character, since there may
+ // be enough input whitespace to span multiple lines.
+ wordToEncode = whitespace.slice(1) + wordToEncode;
+ whitespace = whitespace.slice(0, 1);
+ }
+ }
+
+ // If a word is ready to be encoded on the current line or on the next
+ while (wordToEncode) {
+ // At the first iteration, we may encode the initial part of the word
+ // on the current line, if enough space is present. On subsequent
+ // iterations, if other portions of the same word are still present,
+ // the current line usually contains an encoded word until its end,
+ // and the encoding of the remaining portion must necessarily be done
+ // on the next line.
+ if (mustFold) {
+ // Send the line with the encoded word to the output buffer.
+ fold();
+ lineStart = whitespace;
+ whitespace = "";
+ }
+ // If output hasn't been already prepared outside of the loop
+ if (!outputReady) {
+ // Compute the new encoded word. The length limit is at most 75
+ // characters, since lineStart contains at least one character,
+ // except on the first line where lineLimit is always less than and
+ // never equal to maxLineLimit.
+ encodedWord = MimeSupport.encodeWord(wordToEncode, aCharset,
+ lineLimit - lineStart.length - whitespace.length, remainder);
+ // If the word does not fit on the current line
+ if (!encodedWord) {
+ // If the current line has its maximum length, or we cannot fold
+ // the encoded word on the next line since there is no whitespace.
+ if (mustFold || !whitespace) {
+ throw new Components.Exception(
+ "Unable to encode the input string in the available space.");
+ }
+ // Retry on the next line.
+ mustFold = true;
+ continue;
+ }
+ }
+ // Add the encoded output to the current line.
+ lineStart += whitespace;
+ wordMiddle = wordToEncode;
+ lineMiddle = encodedWord;
+ lineEnd = "";
+ // Prepare the word to be encoded on the next iteration, if present.
+ wordToEncode = remainder.value;
+ whitespace = " ";
+ mustFold = true;
+ outputReady = false;
+ }
+ }
+ );
+ // Return the generated lines.
+ return resultLines + lineStart + lineMiddle + lineEnd;
+ },
+
+ /**
+ * Returns a string of characters representing the decoded version of the
+ * provided unstructured header field value. For more information, see
+ * (retrieved 2009-08-01).
+ *
+ * This function attempts to decode "encoded words". For more information, see
+ * (retrieved 2009-08-01). The
+ * decoding algorithm is slightly different from the specification in that
+ * the maximum length of an encoded word is not taken into account.
+ *
+ * This function recognizes and ignores the optional language specification in
+ * encoded words. For more information on the subject, see
+ * (retrieved 2009-08-01).
+ *
+ * @param aHeaderValue
+ * ASCII encoded value of an unstructured header field. The string must
+ * consist of a single line of text.
+ */
+ parseUnstructuredValue: function(aHeaderValue) {
+ // Initialize the state variable used to find adjacent encoded words.
+ var wordWasEncoded = false;
+ // Process individual words separated by whitespace.
+ return aHeaderValue.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aWhitespace ( [\t ]* )
+ *
+ * Optional whitespace before the encoded or normal word. Whitespace
+ * between two encoded words will be omitted from the output.
+ *
+ * aWord ( [^\t ]+ )
+ *
+ * The word that will be examined to determine if it is encoded. This part
+ * will be replaced with the decoded word.
+ */
+ /([\t ]*)([^\t ]+)/g,
+ function(aAll, aWhitespace, aWord) {
+ // Remember if the previous word was encoded .
+ var previousWordWasEncoded = wordWasEncoded;
+ // Decode the current word and remember if decoding has been performed.
+ wordWasEncoded = false;
+ var decodedWord = aWord.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aCharset ( [^*?]+ )
+ *
+ * Character set specification defined inside the charset portion that
+ * immediately follows the initial "=?" sequence.
+ *
+ * aLanguage ( [^?]+ )
+ *
+ * Optional language tag, defined after the asterisk ("*") inside the
+ * charset portion that follows the initial "=?" sequence.
+ *
+ * aEncoding ( [^?]+ )
+ *
+ * Encoding portion that follows the first question mark ("?").
+ *
+ * aText ( [^?]+ )
+ *
+ * Encoded text portion that follows the second question mark ("?")
+ * and comes before the final "?=" sequence.
+ */
+ /^=\?([^*?]+)(?:\*([^?]+))?\?([^?]+)\?([^?]+)\?=$/,
+ function(aAll, aCharset, aLanguage, aEncoding, aText) {
+ // Decode the octets specified in the encoded text.
+ var octets;
+ switch (aEncoding.toUpperCase()) {
+ case "B":
+ // For the "B" encoding, we can use "base64" decoding.
+ octets = MimeSupport.decodeBase64(aText);
+ break;
+ case "Q":
+ // For the "Q" encoding, we can use "Quoted-Printable" decoding,
+ // except that the underscore ("_") must be translated to space
+ // before the operation.
+ octets = MimeSupport.decodeQuotedPrintable(
+ aText.replace("_", "=20", "g"));
+ break;
+ default:
+ // The encoding is unknown, stop now and don't alter the word.
+ return aAll;
+ }
+ // Decode the characters represented by the octets.
+ var decodedText;
+ try {
+ // Convert the octets to characters using the specified charset.
+ var converter =
+ Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = aCharset;
+ decodedText = converter.ConvertToUnicode(octets);
+ } catch (e) {
+ // If decoding failed, stop now and don't alter the word.
+ return aAll;
+ }
+ // Remember that the word was successfully decoded and replace it.
+ wordWasEncoded = true;
+ return decodedText;
+ }
+ );
+ // If both this word and the previous one have been decoded, remove the
+ // whitespace between the two.
+ return (previousWordWasEncoded && wordWasEncoded ? "" : aWhitespace) +
+ decodedWord;
+ }
+ );
+ },
+
+ /**
+ * Returns the lowercase media type obtained by parsing the given value of the
+ * "Content-Type" header, and populates the given object with one lowercase
+ * property for each of the additional parameters in the header. For more
+ * information on the syntax of the "Content-Type" header field, see
+ * (retrieved 2009-11-22).
+ *
+ * This function recognizes continuations in parameter values, as well as
+ * character set and language information, even though the latter is ignored.
+ * For more information, see
+ * (retrieved 2009-11-22).
+ *
+ * @param aHeaderValue
+ * Unfolded value of the "Content-Type" header, consisting of a single
+ * line of text.
+ * @param aParameters
+ * Empty object that will be populated with the parsed parameter
+ * values.
+ */
+ parseContentTypeValue: function(aHeaderValue, aParameters) {
+ // Get the content type and raw parameter values.
+ var rawParameters = {};
+ var contentType = MimeSupport.rawParseContentTypeValue(aHeaderValue,
+ rawParameters);
+ // Build the continuations map.
+ var knownParameters = {};
+ var knownCharsets = {};
+ for (let [paramName, paramValue] in Iterator(rawParameters)) {
+ // Separate the section number and extension flag from the parameter name.
+ var sectionNumber = -1;
+ var isExtended = false;
+ paramName = paramName.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aName ( [^*]+ )
+ *
+ * Actual name of the parameter.
+ *
+ * aSectionNumber ( [\d]{1,9} )
+ *
+ * Digits that represent the section number in a parameter continuation.
+ *
+ * aExtendedFlag ( \*? )
+ *
+ * If present, indicates that the parameter value uses the extended
+ * syntax for character set and language information.
+ */
+ /^([^*]+)(?:\*([\d]{1,9}))?(\*?)$/,
+ function(aAll, aName, aSectionNumber, aExtendedFlag) {
+ if (aSectionNumber) {
+ sectionNumber = parseInt(aSectionNumber);
+ }
+ isExtended = !!aExtendedFlag;
+ return aName;
+ }
+ );
+ // For the first section in a parameter with extended syntax
+ if (isExtended && sectionNumber === 0) {
+ // Separate the character set and language from the parameter value.
+ paramValue = paramValue.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aCharset ( [^']* )
+ *
+ * Optional character set specification that precedes the first single
+ * quote ("'") character.
+ *
+ * aLanguage ( [^']* )
+ *
+ * Language specification that follows the first "'" character.
+ *
+ * aValue ( .* )
+ *
+ * Encoded text portion that follows the second "'" character.
+ */
+ /^([^']*)'([^']*)'(.*)$/,
+ function(aAll, aCharset, aLanguage, aValue) {
+ knownCharsets[paramName] = aCharset;
+ return aValue;
+ }
+ );
+ }
+ // Ensure that the value array for the parameter name is present.
+ let paramValues = knownParameters[paramName];
+ if (!paramValues) {
+ paramValues = [];
+ knownParameters[paramName] = paramValues;
+ }
+ // Store either the individual parameter at index -1, or the sections at
+ // index 0 or above, indicating whether the value should be decoded.
+ paramValues[sectionNumber] = [isExtended, paramValue];
+ }
+ // Prepare an uninitialized converter object.
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ // Actually perform the parameter reordering and decoding.
+ for (let [paramName, paramValues] in Iterator(knownParameters)) {
+ // Reinitialize the conversion object using the specified character set.
+ var currentCharset = knownCharsets[paramName];
+ if (currentCharset) {
+ converter.charset = currentCharset;
+ }
+ // Examine each possible continuation starting from the first section.
+ var compositeValue = "";
+ for (var [, [paramIsExtended, paramValue]] in Iterator(paramValues)) {
+ // If the parameter has extended syntax, decode it now that the
+ // character set to use is certainly known, regardless of the order of
+ // the parameter continuations in the header.
+ if (paramIsExtended) {
+ // Obtain the octets from the original value.
+ paramValue = MimeSupport.decodePercent(paramValue);
+ // Use the given character set to obtain the characters if required.
+ if (currentCharset) {
+ try {
+ paramValue = converter.ConvertToUnicode(paramValue);
+ } catch (e) {
+ // If decoding failed, don't alter the value.
+ }
+ }
+ }
+ // Concatenate the value of the parameter.
+ compositeValue += paramValue;
+ }
+ // Return the composite value to the caller.
+ aParameters[paramName] = compositeValue;
+ }
+ // Finally, return the content type.
+ return contentType;
+ },
+
+ /**
+ * Returns the lowercase media type obtained by parsing the given value of the
+ * "Content-Type" header, and populates the given object with one lowercase
+ * property for each of the additional parameters in the header. For more
+ * information on the syntax of the "Content-Type" header field, see
+ * (retrieved 2009-11-22).
+ *
+ * @param aHeaderValue
+ * Unfolded value of the "Content-Type" header, consisting of a single
+ * line of text.
+ * @param aRawParameters
+ * Empty object that will be populated with the parsed raw parameter
+ * values. Continuations and language information are not parsed.
+ */
+ rawParseContentTypeValue: function(aHeaderValue, aRawParameters) {
+ // Since the header value may contain nested comments, we use a parsing
+ // strategy based on recursive parsing functions.
+ var currentText = aHeaderValue;
+ function eatComment() {
+ // If the current value starts with an open parenthesis
+ if (currentText && currentText[0] === "(") {
+ // Remove the opening parenthesis.
+ currentText = currentText.slice(1);
+ do {
+ // Eat all the characters until the next open or closed parenthesis,
+ // excluding parentheses appearing in quoted pairs.
+ currentText = currentText.replace(/^(\\.|[^()])*/, "");
+ // Recursively eat inner comments.
+ eatComment();
+ // Repeat until there is no more text in the comment.
+ } while (currentText && currentText[0] !== ")");
+ // Remove the closing parenthesis, if found.
+ currentText = currentText.slice(1);
+ }
+ }
+ function eatCommentsAndWhitespace() {
+ do {
+ var currentLength = currentText.length;
+ // Eat initial whitespace, if present.
+ currentText = currentText.replace(/^[\t ]*/, "");
+ // Eat one comment, if present.
+ eatComment();
+ // Repeat until there are no more comments and whitespace to remove.
+ } while (currentText.length < currentLength);
+ }
+ function getToken() {
+ // If no token is present, an empty string is returned.
+ var innerText = "";
+ // Look for a string of allowed characters, excluding the special ones.
+ currentText = currentText.replace(/^[^\t ()<>@,;:\\"\/\[\]?=]+/,
+ function(aAll) {
+ // A valid token was found.
+ innerText = aAll;
+ // Remove the token from the current text.
+ return "";
+ }
+ );
+ return innerText;
+ }
+ function getQuotedString() {
+ // If no quoted string is present, an empty string is returned.
+ var innerText = "";
+ // Look for a string that begins and ends with double quotes, excluding
+ // double quote characters appearing in quoted pairs inside the string.
+ currentText = currentText.replace(/^"((?:\\.|[^"])*)"/,
+ function(aAll, aInnerString) {
+ // Store the contents of the quoted string, while parsing quoted
+ // pairs.
+ innerText = aInnerString.replace(/\\(.)/g, "$1");
+ // Remove the quoted string from the current text.
+ return "";
+ }
+ );
+ return innerText;
+ }
+ function getMsgId() {
+ // This function looks for non-standard values of the "start" parameter
+ // usually contained in MHTML files generated by the Opera browser.
+ var innerText = "";
+ // Look for a string that begins and ends with angular parentheses,
+ // excluding only the presence of nested parentheses in the middle.
+ currentText = currentText.replace(/^<[^<>]+>/,
+ function(aAll) {
+ // A non-standard value was found.
+ innerText = aAll;
+ // Remove the value from the current text.
+ return "";
+ }
+ );
+ return innerText;
+ }
+ // Start by parsing the media type and subtype.
+ eatCommentsAndWhitespace();
+ var type = getToken();
+ var subtype = "";
+ if (currentText && currentText[0] === "/") {
+ currentText = currentText.slice(1);
+ subtype = getToken();
+ }
+ // Continue only if the header value is valid so far.
+ if (!type || !subtype) {
+ return "";
+ }
+ // Parse the parameters.
+ eatCommentsAndWhitespace();
+ while (currentText && currentText[0] === ";") {
+ // Remove the separating semicolon.
+ currentText = currentText.slice(1);
+ eatCommentsAndWhitespace();
+ // Parse the parameter name.
+ var paramName = getToken();
+ eatCommentsAndWhitespace();
+ // Parse the mandatory parameter value.
+ if (currentText && currentText[0] === "=") {
+ // Remove the separating equal sign.
+ currentText = currentText.slice(1);
+ eatCommentsAndWhitespace();
+ // Parse the parameter value.
+ var paramValue = getToken() || getQuotedString() || getMsgId();
+ eatCommentsAndWhitespace();
+ // Set the property on the provided object if the parameter is present.
+ if (paramName && paramValue) {
+ aRawParameters[paramName.toLowerCase()] = paramValue;
+ }
+ }
+ }
+ // Return the lowercase media type.
+ return (type + "/" + subtype).toLowerCase();
+ },
+
+ /**
+ * Returns a string with a date and time specification conforming to RFC 822,
+ * RFC 2822 or RFC 5322. For more information on the date format used, see
+ * (retrieved 2009-11-25).
+ *
+ * @param aDate
+ * Valid Date object representing the date to be encoded.
+ */
+ getDateTimeSpecification: function(aDate) {
+ // The following function converts the Mozilla JavaScript date format, like
+ // "Mon Sep 28 1998 14:36:22 GMT-0700 (Pacific Daylight Time)", to the
+ // expected RFC 5322 format, like "Mon, 28 Sep 1998 14:36:22 -0700".
+ return aDate.toString().replace(
+ /^(...) (...) (..) (.... ..:..:..) ...(.....).*$/,
+ "$1, $3 $2 $4 $5");
+ },
+}
diff --git a/src/chrome/content/engine/MultipartMimePart.js b/src/chrome/content/engine/MultipartMimePart.js
new file mode 100644
index 0000000..f1e0d1a
--- /dev/null
+++ b/src/chrome/content/engine/MultipartMimePart.js
@@ -0,0 +1,139 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Represents a multipart MIME part.
+ *
+ * This class derives from MimePart. See the MimePart documentation for details.
+ */
+function MultipartMimePart() {
+ MimePart.call(this);
+
+ // Initialize the boundary with a random string.
+ var randomHexString1 = Math.floor(Math.random() * 0x100000000).toString(16);
+ var randomHexString2 = Math.floor(Math.random() * 0x100000000).toString(16);
+ this.boundary = "----=_NextPart_000_0000_" +
+ ("0000000" + randomHexString1.toUpperCase()).slice(-8) + "." +
+ ("0000000" + randomHexString2.toUpperCase()).slice(-8);
+
+ // Initialize other member variables explicitly.
+ this.parts = [];
+}
+
+MultipartMimePart.prototype = {
+ __proto__: MimePart.prototype,
+
+ /**
+ * Boundary string separating the various parts.
+ */
+ boundary: "",
+
+ /**
+ * Array of MimePart objects that are children of this part.
+ */
+ parts: [],
+
+ /**
+ * Returns the MimePart object for the current start part. At least one child
+ * part must be present when this property is read.
+ */
+ get startPart() {
+ // Locate the start part using the "start" parameter of the "Content-Type"
+ // header, if present.
+ var startValue = this.contentTypeParameters.start;
+ if (startValue) {
+ for (let [, contentPart] in Iterator(this.parts)) {
+ if (startValue == contentPart.headersByLowercaseName["content-id"]) {
+ return contentPart;
+ }
+ }
+ }
+ // Locate the start part using the "type" parameter of the "Content-Type"
+ // header, if present.
+ var typeValue = this.contentTypeParameters.type;
+ if (typeValue) {
+ for (let [, contentPart] in Iterator(this.parts)) {
+ if (typeValue == contentPart.mediaType) {
+ return contentPart;
+ }
+ }
+ }
+ // The first part is assumed to be the start part.
+ return this.parts[0];
+ },
+
+ // MimePart
+ get bodySection() {
+ // Write the English explanatory message in the preamble.
+ var sectionText = "This is a multi-part message in MIME format.\r\n";
+ // Add the encoded parts separated by the boundaries.
+ for (var [, part] in Iterator(this.parts)) {
+ sectionText += "\r\n--" + this.boundary + "\r\n";
+ sectionText += part.text;
+ }
+ // Add the trailing boundary.
+ return sectionText + "\r\n--" + this.boundary + "--\r\n";
+ },
+ set bodySection(aValue) {
+ // Check that the boundary is actually set.
+ if (!this.boundary) {
+ throw new Components.Exception("Missing mandatory boundary field");
+ }
+ // Build a regular expression that will detect the provided boundary string
+ // preceded by a newline and optionally followed by a newline, even if the
+ // newline does not immediately follow the boundary string.
+ var boundaryRe = new RegExp("(?:\\r?\\n|\\r|^)--" +
+ this.boundary.replace(/[^\w]/g, "\\$&") +
+ "[^\\r\\n]*(?:\\r\\n?|\\n)?", "g");
+ // Get the various parts separated by the boundary strings.
+ var partsArray = aValue.split(boundaryRe);
+ // For all the parts except the preamble and the epilogue
+ for (var partNum = 1; partNum < partsArray.length - 1; partNum++) {
+ try {
+ // Create a new part to parse the contents.
+ var newPart = new MimePart();
+ newPart.text = partsArray[partNum];
+ newPart = newPart.promoteToDerivedClass();
+ // Add the new part to this object.
+ this.parts.push(newPart);
+ } catch (e) {
+ // Ignore content parts with parsing errors.
+ Cu.reportError(e);
+ }
+ }
+ },
+}
diff --git a/src/chrome/content/engine/ZipCreator.js b/src/chrome/content/engine/ZipCreator.js
new file mode 100644
index 0000000..c1184e3
--- /dev/null
+++ b/src/chrome/content/engine/ZipCreator.js
@@ -0,0 +1,113 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Allows the creation of ZIP archives using a ZIP writer object.
+ *
+ * @param aFile
+ * The nsIFile of the archive to be created or modified.
+ * @param aCreateNew
+ * True if an existing file should be overwritten, or false if new items
+ * should be appended to the file.
+ */
+function ZipCreator(aFile, aCreateNew) {
+ this._file = aFile;
+ this._createNew = aCreateNew;
+}
+
+ZipCreator.prototype = {
+ /**
+ * Adds to the archive the contents of a directory, including its
+ * subdirectories.
+ *
+ * The archive is opened automatically, and the dispose method should be
+ * called to close it afterwards.
+ *
+ * @param aDirectory
+ * nsIFile representing the directory to be added. The leaf name of the
+ * directory itself is not used.
+ * @param aZipEntry
+ * Name of the ZIP entry to be created for the directory.
+ */
+ addDirectory: function(aDirectory, aZipEntry) {
+ this._open();
+ new ZipDirectory(this, aDirectory, aZipEntry, null).save();
+ },
+
+ /**
+ * Ensures that the archive file is closed.
+ */
+ dispose: function() {
+ if (this._zipWriter) {
+ this._zipWriter.close();
+ this._zipWriter = null;
+ }
+ },
+
+ /** File open flags */
+ PR_RDONLY : 0x01,
+ PR_WRONLY : 0x02,
+ PR_RDWR : 0x04,
+ PR_CREATE_FILE : 0x08,
+ PR_APPEND : 0x10,
+ PR_TRUNCATE : 0x20,
+ PR_SYNC : 0x40,
+ PR_EXCL : 0x80,
+
+ /**
+ * Opens the archive file for writing.
+ */
+ _open: function() {
+ // Create the ZIP writer object.
+ var zipWriter = Cc["@mozilla.org/zipwriter;1"].
+ createInstance(Ci.nsIZipWriter);
+
+ // Add to an existing archive, or create a new archive.
+ var openFlags = this.PR_RDWR | this.PR_CREATE_FILE;
+ if (this._createNew) {
+ openFlags |= this.PR_TRUNCATE;
+ }
+ zipWriter.open(this._file, openFlags);
+
+ // Indicate that the archive is opened.
+ this._zipWriter = zipWriter;
+ },
+
+ _file: null,
+ _createNew: false,
+ _zipWriter: null,
+}
diff --git a/src/chrome/content/engine/ZipDirectory.js b/src/chrome/content/engine/ZipDirectory.js
new file mode 100644
index 0000000..9470771
--- /dev/null
+++ b/src/chrome/content/engine/ZipDirectory.js
@@ -0,0 +1,128 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This object is used by ZipCreator to add directory contents to an archive.
+ */
+function ZipDirectory(aZipCreator, aDirectory, aZipEntry, aParent) {
+ this._zipCreator = aZipCreator;
+ this._directory = aDirectory;
+ this._zipEntry = aZipEntry;
+ this._parent = aParent;
+}
+
+ZipDirectory.prototype = {
+ /**
+ * Stores the directory into the archive. If the directory and its
+ * subdirectories don't contain any file, no ZIP entry is created.
+ */
+ save: function() {
+ // Enumerate all the files and subdirectories in the specified directory.
+ var dirEntries = this._directory.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ // Get the file or directory object and the associated ZIP entry name.
+ var dirEntry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+ var zipEntry = this._zipEntry + "/" + dirEntry.leafName;
+ // Add subdirectories recursively.
+ if (dirEntry.isDirectory()) {
+ new ZipDirectory(this._zipCreator, dirEntry, zipEntry, this).save();
+ } else {
+ // Only before a file is actually added to the archive, ensure that the
+ // parent ZIP directory entry is present. This prevents the creation of
+ // ZIP directory entries for empty subdirectories.
+ this._addDirEntry();
+ // Add a new file to the archive.
+ this._zipCreator._zipWriter.addEntryFile(zipEntry,
+ this._compressionLevelForFile(dirEntry), dirEntry, false);
+ }
+ }
+ },
+
+ /** Constants */
+ PR_USEC_PER_MSEC: 1000,
+
+ /**
+ * Ensures that the ZIP directory entry for this item and its parent
+ * directories has been created.
+ */
+ _addDirEntry: function() {
+ if (!this._zipEntryPresent) {
+ if (this._parent) {
+ this._parent._addDirEntry();
+ }
+ // Add a new directory entry to the archive.
+ this._zipCreator._zipWriter.addEntryDirectory(this._zipEntry,
+ this._directory.lastModifiedTime * this.PR_USEC_PER_MSEC, false);
+ // Indicate that the entry has been created.
+ this._zipEntryPresent = true;
+ }
+ },
+
+ /**
+ * Returns the compression level to use when adding a file to the archive.
+ * The result is based on the file extension and the current preferences.
+ */
+ _compressionLevelForFile: function(aFile) {
+ // If all the files should be stored with maximum compression
+ if (Prefs.saveMaffCompression == Prefs.MAFFCOMPRESSION_BEST) {
+ return Ci.nsIZipWriter.COMPRESSION_BEST;
+ }
+ // If all the files should be stored uncompressed
+ if (Prefs.saveMaffCompression == Prefs.MAFFCOMPRESSION_NONE) {
+ return Ci.nsIZipWriter.COMPRESSION_NONE;
+ }
+ // Do not re-compress media files for which there's not a significant gain
+ // since they're already compressed. The file type is recognized using the
+ // extension, and currently only ".ogg", ".oga" and ".ogv" files are not
+ // re-compressed.
+ if (/\.og[gav]$/i.test(aFile.leafName)) {
+ return Ci.nsIZipWriter.COMPRESSION_NONE;
+ }
+ // Use the best compression for all the other files.
+ return Ci.nsIZipWriter.COMPRESSION_BEST;
+ },
+
+ /**
+ * Set to true if the directory entry for this object has been created.
+ */
+ _zipEntryPresent: false,
+
+ _zipCreator: null,
+ _directory: null,
+ _zipEntry: "",
+ _parent: null,
+}
diff --git a/src/chrome/content/general/AsyncEnumerator.js b/src/chrome/content/general/AsyncEnumerator.js
new file mode 100644
index 0000000..58ad7e0
--- /dev/null
+++ b/src/chrome/content/general/AsyncEnumerator.js
@@ -0,0 +1,182 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Allows the current thread to iterate through the items of an enumerable
+ * object, like an array, without blocking the dispatching of events like those
+ * raised by user interface interaction.
+ *
+ * @param aEnumerable
+ * The enumerable object to be examined. The Iterator global function
+ * will be used to get a reference to the object's iterator.
+ * @param aItemFn
+ * Callback function that will be called for every item. The item will be
+ * the function's first argument. The next item in the enumeration will
+ * not be examined until the function returns. If an exception is raised,
+ * the enumeration will be suspended until explicitly stopped.
+ * @param aSuccessFn
+ * Callback function that will be called if the enumeration terminates
+ * normally, and the "stop" method has not been called.
+ */
+function AsyncEnumerator(aEnumerable, aItemFn, aSuccessFn) {
+ // Get the iterator for the items in the enumerable object.
+ this._iterator = Iterator(aEnumerable);
+ // Save references to the callback function.
+ this._itemFn = aItemFn;
+ this._successFn = aSuccessFn;
+}
+
+AsyncEnumerator.prototype = {
+ /**
+ * Starts or resumes the enumeration of the items.
+ *
+ * This function may also be called from within a callback function.
+ */
+ start: function() {
+ // If the enumeration has already been stopped, return now.
+ if (!this._iterator) {
+ return;
+ }
+ // Ensure that the paused state is reset and enter the main loop.
+ this._paused = false;
+ this.run();
+ },
+
+ /**
+ * Pauses the enumeration of the items until the "start" method is called.
+ *
+ * This function may also be called from within a callback function.
+ */
+ pause: function() {
+ this._paused = true;
+ },
+
+ /**
+ * Pauses the enumeration of the items until the "run" method is called again.
+ *
+ * This function may also be called from within a callback function.
+ */
+ stop: function() {
+ // Force the "run" method to terminate as soon as the currently running
+ // callback, if any, terminates its execution. Since the iterator will
+ // become unavailable, the iteration will not be resumed even if the "start"
+ // method is called before the "run" method terminates.
+ this._paused = true;
+ // First make the iterator unavailable.
+ var iterator = this._iterator;
+ this._iterator = null;
+ // If the iterator is also a generator, ensure it is closed.
+ if (iterator.close) {
+ iterator.close();
+ }
+ },
+
+ /**
+ * Executes the main iteration loop. This is considered a private function.
+ */
+ run: function() {
+ // If the enumeration has already been stopped, return now.
+ if (!this._iterator) {
+ return;
+ }
+ // If the main iteration loop is already running, return now.
+ if (this._running) {
+ return;
+ }
+ // Enter the main iteration loop.
+ this._running = true;
+ try {
+ // The main loop is executed until one of the following occurs:
+ // - The maximum allowed consecutive execution time passes.
+ // - The enumeration is paused or stopped.
+ // - The last item in the enumeration is reached.
+ // - An exception is raised by the callback function.
+ var startTime = new Date();
+ do {
+ this._itemFn(this._iterator.next());
+ } while(!this._paused &&
+ new Date() - startTime < this._maxConsecutiveTimeMs);
+ // If the main loop terminated because the maximum allowed consecutive
+ // execution time passed, reschedule the "run" method immediately.
+ if (!this._paused) {
+ this._mainThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ } catch (e if e instanceof StopIteration) {
+ // Enumeration terminated successfully. Make the iterator unavailable and
+ // invoke the appropriate callback function.
+ this._iterator = null;
+ this._successFn();
+ } finally {
+ // Indicate that the main loop terminated, even in case of exceptions.
+ this._running = false;
+ }
+ },
+
+ /**
+ * Time interval, in milliseconds, after which the enumeration is suspended
+ * and automatically rescheduled on the current thread.
+ */
+ _maxConsecutiveTimeMs: 25,
+
+ /**
+ * Iterator over the enumerable object, or null if the enumeration terminated.
+ */
+ _iterator: null,
+
+ /**
+ * Callback function. See the constructor for details.
+ */
+ _itemFn: null,
+
+ /**
+ * Callback function. See the constructor for details.
+ */
+ _successFn: null,
+
+ /**
+ * True while the "run" method is being executed.
+ */
+ _running: false,
+
+ /**
+ * True if the enumeration should be paused until the "run" method is called.
+ */
+ _paused: false,
+
+ _mainThread: Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager).mainThread,
+}
diff --git a/src/chrome/content/general/CssSourceFragment.js b/src/chrome/content/general/CssSourceFragment.js
new file mode 100644
index 0000000..70854e1
--- /dev/null
+++ b/src/chrome/content/general/CssSourceFragment.js
@@ -0,0 +1,137 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Provides parsing of a CSS source file into significant fragments.
+ *
+ * This class derives from SourceFragment. See the SourceFragment documentation
+ * for details.
+ */
+function CssSourceFragment(aSourceData, aOptions) {
+ SourceFragment.call(this, aSourceData, aOptions);
+
+ // Parse the provided data immediately.
+ this.parse();
+}
+
+CssSourceFragment.prototype = {
+ __proto__: SourceFragment.prototype,
+
+ // SourceFragment
+ _executeParse: function(aAddFn) {
+ this._sourceData.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aBefore ( [\w\W]*? )
+ *
+ * Captures all the characters, including newlines, that are present
+ * before the text recognized by the following expressions.
+ *
+ * Parsing expressions group ( (?:<...>|<...>|$) )
+ *
+ * This non-captured group follows aBefore and contains the actual parsing
+ * expressions. The end of the string is matched explicitly in order for
+ * the aBefore group to capture the characters after the last part of the
+ * string that is recognized by the parsing expressions.
+ *
+ * URL parsing expression ( (\burl\((['"])?)([^\r\n]*?)(?=\3\)) )
+ *
+ * Recognizes the text that can introduce an URL in the sylesheet. It can
+ * be divided in the following parts:
+ *
+ * aUrlBefore ( \burl\(\s*(['"]|")? )
+ *
+ * Captures all the text before the beginning of the actual URL.
+ *
+ * aUrlQuote ( ['"]|" )
+ *
+ * This optional group is used in a backreference, and is already
+ * captured inside the outer group. We include """ in case we are
+ * processing a style declaration inside an attribute. We do that
+ * unconditionally because, even if the input is not encoded as HTML,
+ * optionally recognizing """ has no effect in practice.
+ *
+ * aUrlText ( [^\r\n]*? )
+ *
+ * Recognizes the body of the URL, that must be placed on a single line.
+ *
+ * End of URL lookahead ( (?=\s*\3\)) )
+ *
+ * This positive lookahead expression recognizes the end of the URL. The
+ * text in this section will be included in the aBefore part during the
+ * next iteration.
+ *
+ * Import URL parsing expression ( (@import\s+(['"]))([^\r\n]*?)(?=\6) )
+ *
+ * Recognizes the text that can introduce an URL in the sylesheet. It can
+ * be divided in the following parts:
+ *
+ * aImportUrlBefore ( @import\s+(['"]|") )
+ *
+ * Captures all the text before the beginning of the actual URL.
+ *
+ * aImportUrlQuote ( ['"]|" )
+ *
+ * This mandatory group is used in a backreference, and is already
+ * captured inside the outer group. We include """ in case we are
+ * processing a style declaration inside an attribute. We do that
+ * unconditionally because, even if the input is not encoded as HTML,
+ * optionally recognizing """ has no effect in practice.
+ *
+ * aImportUrlText ( [^\r\n]*? )
+ *
+ * Recognizes the body of the URL, that must be placed on a single line.
+ *
+ * End of URL lookahead ( (?=\s*\6) )
+ *
+ * This positive lookahead expression recognizes the end of the URL. The
+ * text in this section will be included in the aBefore part during the
+ * next iteration.
+ *
+ */
+ /([\w\W]*?)(?:(\burl\(\s*(['"]|")?)([^\r\n]*?)(?=\s*\3\))|(@import\s+(['"]|"))([^\r\n]*?)(?=\s*\6)|$)/gi,
+ function(aAll, aBefore, aUrlBefore, aUrlQuote, aUrlText, aImportUrlBefore,
+ aImportUrlQuote, aImportUrlText) {
+ aAddFn(SourceFragment, aBefore + (aUrlBefore || ""));
+ aAddFn(UrlSourceFragment, aUrlText);
+ aAddFn(SourceFragment, aImportUrlBefore);
+ aAddFn(UrlSourceFragment, aImportUrlText);
+ }
+ );
+ },
+}
diff --git a/src/chrome/content/general/DataSourceWrapper.js b/src/chrome/content/general/DataSourceWrapper.js
new file mode 100644
index 0000000..0884e6a
--- /dev/null
+++ b/src/chrome/content/general/DataSourceWrapper.js
@@ -0,0 +1,180 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Base class that can be used to implement RDF data sources by wrapping an
+ * inner data source. This class contains the wrapping logic and provides
+ * convenience methods for manipulating the underlying data source.
+ *
+ * For general information about RDF data sources in Mozilla, see
+ * (retrieved 2009-09-28).
+ * For more information on RDF data source implementation techniques, see
+ * (retrieved
+ * 2009-09-28).
+ *
+ * @param aInnerDataSource
+ * An object implementing the nsIRDFDataSource interface that will be
+ * wrapped.
+ */
+function DataSourceWrapper(aInnerDataSource) {
+ // This object allows the implementation of the nsIRDFDataSource interface by
+ // forwarding most of the calls to an in-memory data source. The first part of
+ // the initialization consists in creating the wrapper functions.
+
+ // This function creates a forwarding function for aInnerDataSource.
+ function makeForwardingFunction(functionName) {
+ return function() {
+ return aInnerDataSource[functionName].apply(aInnerDataSource, arguments);
+ }
+ }
+
+ // Forward all the functions that are not explicitly overridden.
+ for (var propertyName in aInnerDataSource) {
+ if (typeof aInnerDataSource[propertyName] == "function" &&
+ !(propertyName in this)) {
+ this[propertyName] = makeForwardingFunction(propertyName);
+ }
+ }
+
+ // We also set up a convenience access to some of the RDF resource objects
+ // that are commonly used with this data source. This way, users don't need to
+ // call GetResource repeatedly.
+ for (var resourceId in this.resources) {
+ if (this.resources.hasOwnProperty(resourceId)) {
+ var resource = this.resources[resourceId];
+ // Since the inner "resources" object is often stored in the prototype of
+ // the derived classes, it is shared by all the instances of the data
+ // source created from the same prototype, and the translation from URL to
+ // RDF resource may have been already done.
+ if (typeof resource == "string") {
+ this.resources[resourceId] = this._rdf.GetResource(resource);
+ }
+ }
+ }
+
+ // Store a reference to the wrapped object.
+ this._wrappedObject = aInnerDataSource;
+}
+
+DataSourceWrapper.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIRDFDataSource,
+ ]),
+
+ /**
+ * Collection of RDF resource objects that form the common subjects and the
+ * vocabulary of this RDF data source.
+ *
+ * Derived classes usually override this property in their prototype, defining
+ * the resource URLs as strings. The strings are converted to actual RDF
+ * resources as soon as the first instance of the data source is constructed.
+ *
+ * The original resource URLs can be retrieved using the ValueUTF8 property of
+ * the resource objects.
+ */
+ resources: {},
+
+ /**
+ * Returns the value of the literal to which the given property points.
+ */
+ getLiteralValue: function(aSource, aProperty) {
+ return this.GetTarget(aSource, aProperty, true).
+ QueryInterface(Ci.nsIRDFLiteral).Value;
+ },
+
+ /**
+ * Replaces the literal to which the given property points.
+ */
+ replaceLiteral: function(aSource, aProperty, aNewValue) {
+ // Find the RDF nodes to be modified, assuming that the required assertion
+ // already exists in the data source.
+ var oldRdfLiteral = this.GetTarget(aSource, aProperty, true);
+ var newRdfLiteral = this._rdf.GetLiteral(aNewValue);
+ // Execute the change.
+ this.Change(aSource, aProperty, oldRdfLiteral, newRdfLiteral);
+ },
+
+ // nsIRDFDataSource
+ Assert: function(aSource, aProperty, aTarget, aTruthValue) {
+ // Should return NS_RDF_ASSERTION_REJECTED, but it is a success code.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // nsIRDFDataSource
+ Change: function(aSource, aProperty, aOldTarget, aNewTarget) {
+ // Should return NS_RDF_ASSERTION_REJECTED, but it is a success code.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // nsIRDFDataSource
+ Move: function(aOldSource, aNewSource, aProperty, aTarget) {
+ // Should return NS_RDF_ASSERTION_REJECTED, but it is a success code.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // nsIRDFDataSource
+ Unassert: function(aSource, aProperty, aTarget) {
+ // Should return NS_RDF_ASSERTION_REJECTED, but it is a success code.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Returns an RDF literal containing either "true" or "false".
+ */
+ _rdfBool: function(aBooleanValue) {
+ return this._rdf.GetLiteral(aBooleanValue ? "true" : "false");
+ },
+
+ /**
+ * Makes an RDF sequence associated with the wrapped data source.
+ */
+ _rdfSequence: function(aResource) {
+ return Cc["@mozilla.org/rdf/container-utils;1"]
+ .getService(Ci.nsIRDFContainerUtils).MakeSeq(this._wrappedObject,
+ aResource);
+ },
+
+ /**
+ * RDF data source that is wrapped by this object.
+ */
+ _wrappedObject: null,
+
+ /**
+ * Reference to the global RDF service, provided for convenience.
+ */
+ _rdf: Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService),
+}
diff --git a/src/chrome/content/general/HtmlSourceFragment.js b/src/chrome/content/general/HtmlSourceFragment.js
new file mode 100644
index 0000000..fa17435
--- /dev/null
+++ b/src/chrome/content/general/HtmlSourceFragment.js
@@ -0,0 +1,94 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Provides parsing of an HTML source file into significant fragments.
+ *
+ * This class derives from SourceFragment. See the SourceFragment documentation
+ * for details.
+ */
+function HtmlSourceFragment(aSourceData, aOptions) {
+ SourceFragment.call(this, aSourceData, aOptions);
+
+ // Parse the provided data immediately.
+ this.parse();
+}
+
+HtmlSourceFragment.prototype = {
+ __proto__: SourceFragment.prototype,
+
+ // SourceFragment
+ _executeParse: function(aAddFn) {
+ this._sourceData.replace(
+ /*
+ * The regular expression below is composed of the following parts:
+ *
+ * aBefore ( [\w\W]*? )
+ *
+ * Captures all the characters, including newlines, that are present
+ * before the text recognized by the following expressions.
+ *
+ * Parsing expressions group ( (?:<...>|<...>|$) )
+ *
+ * This non-captured group follows aBefore and contains the actual parsing
+ * expressions. The end of the string is matched explicitly in order for
+ * the aBefore group to capture the characters after the last part of the
+ * string that is recognized by the parsing expressions.
+ *
+ * aStylesheet ( ","gmi");
+ var urlRe = new RegExp("url\\((\\s*([\"']?)\\s*)"+found+"(\\s*\\2\\s*)\\)","g");
+ var replaceFunc = function(all, match, offset) {
+ return all.replace(urlRe, "url($1"+savePathURL+"$3)");
+ };
+ data = data.replace(re, replaceFunc);
+ } else if(uri.type == "import") {
+ // Fix all instances where this url is found in an import rule
+ var re = new RegExp("","gmi");
+ var noURLImportRe = new RegExp("(@import\\s*([\"'])\\s*)"+found+"(\\s*\\2)","g");
+ var urlImportRe = new RegExp("(@import\\s+url\\(\\s*([\"']?)\\s*)"+found+"(\\s*\\2\\s*)\\)","g");
+ var replaceFunc = function(all, match, offset) {
+ all = all.replace(noURLImportRe, "$1"+savePathURL+"$3");
+ all = all.replace(urlImportRe , "$1"+savePathURL+"$3)");
+ return all;
+ };
+ data = data.replace(re, replaceFunc);
+ }
+ }
+
+ // Fix anchors to point to absolute location instead of relative
+ if(this._options['rewriteLinks']) {
+ // TODO: See if adding a negative lookahead for the https?: would improve performance
+ var replaceFunc = function() {
+ var match = /^([^:]+):/.exec(arguments[0]);
+ if(match && match[1] != 'http' && match[1] != 'https')
+ return arguments[0];
+ else
+ return arguments[1]+arguments[2]+me._uri.resolve(arguments[3])+arguments[2];
+ }
+ data = data.replace(/(]+href=)(["'])([^"']+)\2/igm, replaceFunc);
+ }
+
+ // Save adjusted file
+ this._fileSaver.saveURIContents(download.uri, data, download.charset);
+ } else if(download.contentType == "text/css") {
+ // Fix all URLs in this stylesheet
+ for(var n = 0; n < this._uris.length; n++) {
+ var uri = this._uris[n];
+
+ // Skip empty urls or ones that aren't for external CSS files
+ if(!uri.extractedURI || uri.type == 'index' || uri.where != "extcss") continue;
+
+ var found = this._regexEscape(uri.extractedURI);
+ var savePathURL = this._fileSaver.documentPath(uri, download.uri);
+ if(uri.type == "css") {
+ // Fix url functions in CSS
+ var re = new RegExp("url\\((\\s*([\"']?)\\s*)"+found+"(\\s*\\2\\s*)\\)","g");
+ data = data.replace(re,"url($1"+savePathURL+"$3)");
+ } else if(uri.type == "import") {
+ // Fix all instances where this url is found in an import rule
+ var noURLImportRe = new RegExp("(@import\\s*([\"'])\\s*)"+found+"(\\s*\\2)","g");
+ var urlImportRe = new RegExp("(@import\\s+url\\(\\s*([\"']?)\\s*)"+found+"(\\s*\\2\\s*)\\)","g");
+ data = data.replace(noURLImportRe, "$1"+savePathURL+"$3");
+ data = data.replace(urlImportRe , "$1"+savePathURL+"$3)");
+ }
+ }
+
+ // Save adjusted stylesheet
+ this._fileSaver.saveURIContents(download.uri, data, download.charset);
+ } else if(/^text\//.test(download.contentType) || download.contentType == 'application/x-javascript') {
+ // Had problems with nsWebBrowserPersist and text files, so for now I'll do the saving
+ this._fileSaver.saveURIContents(download.uri, data, download.charset);
+ } else if(download.contentType != "") {
+ // Something we aren't processing so use the file saver's saveURI, because it always works
+ this._fileSaver.saveURI(download.uri);
+ } else {
+ this._warnings.push('Missing contentType: '+download.uri);
+ }
+
+ download.contents = ""; // For some small clean up
+
+ this._currentDownloadIndex++;
+};
+
+/**
+ * Called when a save is completed
+ * @function _saveDone
+ * @param {scPageSaver.scURI} uri - The uri of the file that was just saved
+ * @param {Boolean} success - Whether or not the save was successful
+ */
+scPageSaver.prototype._saveDone = function(uri, success) {
+ // Do not execute the callback if the process has been canceled
+ if (this._hasFinished) {
+ return;
+ }
+
+ if(!success) {
+ if(uri.type == 'index') {
+ this._errors.push('Failed to write main file');
+ this.cancel(0);
+ return;
+ } else {
+ this._warnings.push('Could not save: '+uri);
+ }
+ }
+
+ this._prepareToProcessNextURI();
+}
+
+/**
+ * Cleans up and calls callback. Called when finished downloading and processing.
+ * @function _finished
+ */
+scPageSaver.prototype._finished = function() {
+ // If the operation finished, no action is required
+ if (this._hasFinished) {
+ return;
+ }
+
+ this._hasFinished = true;
+
+ if(this._timers.process) this._timers.process.finish = new Date();
+
+ var nsResult = this._errors.length == 0 ? Components.results.NS_OK : Components.results.NS_ERROR_FAILURE;
+
+ if(this._callback) {
+ this._callback(this, nsResult, {warnings: this._warnings, errors: this._errors, timers: this._timers});
+ }
+
+ if(this._listener) this._listener.onStateChange(null, null, scPageSaver.webProgress.STATE_STOP | scPageSaver.webProgress.STATE_IS_NETWORK, nsResult);
+
+ this._listener = null;
+ this._fileSaver = null;
+ this._fileProvider = null;
+ this._callback = null;
+}
+
+/**
+ * Escapes a string for insertion into a regex for recognition of escaped URLs.
+ * @function {String} _regexEscape
+ * @param {String} str - The string to escape
+ * @return The escaped string
+ */
+scPageSaver.prototype._regexEscape = function(str) {
+ return str.replace(/([?+$|./()\[\]^*])/g,"\\$1").replace(/ /g, "(?: |%20)").replace(/&/g, "&(?:amp;)?").replace(/"/g, '(?:"|")').replace(/'/g, "(?:'|')").replace(//g, "(?:>|>)");
+};
+//}
+
+
+/**
+ * Default file saver component.
+ * Is responsible for calculating replacement URLs in the documents and saving
+ * files and urls.
+ * Saving is sequential, so it's not necessary to structure code to handle parallel
+ * saves.
+ * @class scPageSaver.scDefaultFileSaver
+ *///{
+/**
+ * The function to call when a save has completed. Called with the uri of the
+ * saved file and the success of the save as a boolean.
+ * @property {Function} callback
+ */
+/**
+ * The target URI of the entire save. Used by nsITransfer to link to the download
+ * location and other things.
+ * @property {Function} targetURI
+ */
+/**
+ * Creates a file saver object
+ * @constructor scDefaultFileSaver
+ * @param {nsIFile} file - The ouput file for the HTML
+ * @param {nsIFile} dataPath - Optional support folder for data files
+ */
+scPageSaver.scDefaultFileSaver = function(file, dataPath) {
+ this._saveMap = {};
+
+ // Initialize target file
+ this._file = file;
+
+ // Initialize data folder
+ if (!dataPath) {
+ var nameWithoutExtension = file.leafName.replace(/\.[^.]*$/,"");
+ var stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService).createBundle("chrome://global/locale/contentAreaCommands.properties");
+ var folderName = stringBundle.formatStringFromName("filesFolder", [nameWithoutExtension], 1);
+ this._dataFolder = file.clone();
+ this._dataFolder.leafName = folderName;
+ } else {
+ this._dataFolder = dataPath.clone();
+ }
+
+ // Delete and re-create data folder so that it's clean
+ if(this._dataFolder.exists()) this._dataFolder.remove(true);
+
+ // Define the target URI property for the listener
+ this.targetURI = scPageSaver.nsIIOService.newFileURI(file);
+}
+
+/**
+ * Returns the path string for the given scURI based on the relative URI. In
+ * addition ensures the path is unique for the URI by creating the file ahead of
+ * time and checking that it doesn't interfere with any other URIs.
+ * @function {String} documentPath
+ * @param {scPageSaver.scURI} uri - The URI to generate the path for
+ * @param {scPageSaver.scURI} relativeURI - The URI to generate the path relative to
+ */
+scPageSaver.scDefaultFileSaver.prototype.documentPath = function(uri, relativeURI) {
+ if(uri.type == 'index') throw new Error('Not supposed to need document path for main page');
+
+ var saveKey = uri.toString();
+
+ // Determine the base file name to use first and cache it if it's not cached
+ if(typeof this._saveMap[saveKey] == 'undefined') {
+ var fileName = uri.uri.path.split('/').pop();
+ fileName = fileName.replace(/\?.*$/,"");
+ fileName = fileName.replace(/[\"\*\:\?\<\>\|\\]+/g,"");
+ if(fileName.length > 50) fileName = fileName.slice(0, 25)+fileName.slice(-25);
+ if(fileName == "") fileName = "unnamed";
+
+ /* Here we must check if the file can be saved to disk with the chosen
+ * name. One case where the file cannot be saved is when the name
+ * conflicts with one of another file that must be saved. Note that
+ * whether two names collide is dependent on the underlying filesystem:
+ * for example, on FAT on Windows two file names that differ only in
+ * case conflict with each other, while on ext2 on Linux this conflict
+ * does not occur.
+ */
+ // Build a new nsIFile corresponding to the file name to be saved
+ var actualFileOnDisk = this._dataFolder.clone();
+ if(!this._dataFolder.exists()) this._dataFolder.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
+ actualFileOnDisk.append(fileName);
+
+ // Since the file is not actually saved until later, we must create a placeholder
+ actualFileOnDisk.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
+
+ // Find out which unique name has been used
+ fileName = actualFileOnDisk.leafName;
+
+ // Save to save map
+ this._saveMap[saveKey] = fileName;
+ }
+
+ if(relativeURI.type == 'index') {
+ return (encodeURIComponent(this._dataFolder.leafName)+'/'+encodeURIComponent(this._saveMap[saveKey]));
+ } else {
+ return (encodeURIComponent(this._saveMap[saveKey]));
+ }
+}
+
+/**
+ * Returns the file object corresponding to the location where the given scURI
+ * should be saved.
+ * @function {nsIFile} documentLocalFile
+ * @param {scPageSaver.scURI} uri - The URI to generate the file object for
+ */
+scPageSaver.scDefaultFileSaver.prototype.documentLocalFile = function(uri) {
+ var file = null;
+ if(uri.type == 'index') {
+ file = this._file;
+ } else {
+ var file = this._dataFolder.clone();
+ if(typeof this._saveMap[uri.toString()] == 'undefined') this.documentPath(uri, {type:null}); // Force saveMap to be populated
+ file.append(this._saveMap[uri.toString()]);
+ }
+ return file;
+}
+
+/**
+ * Saves the contents of the given uri to disk using the given charset if valid
+ * @function saveURIContents
+ * @param {scPageSaver.scURI} uri - The uri for the file being saved
+ * @param {String} contents - The contents of the file in UTF-8
+ * @param {String} charset - The character set to use when saving the file
+ */
+scPageSaver.scDefaultFileSaver.prototype.saveURIContents = function(uri, contents, charset) {
+ // Get the file object that we're saving to
+ var file = this.documentLocalFile(uri);
+
+ // Write the file to disk
+ var failed = false;
+ var foStream = Components.classes['@mozilla.org/network/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream);
+ var flags = 0x02 | 0x08 | 0x20;
+ if(!charset) charset = scPageSaver.DEFAULT_WRITE_CHARSET;
+ try {
+ foStream.init(file, flags, 0644, 0);
+ var os = Components.classes["@mozilla.org/intl/converter-output-stream;1"].createInstance(Components.interfaces.nsIConverterOutputStream);
+ os.init(foStream, charset, 4096, "?".charCodeAt(0)); // Write to file converting all bad characters to "?"
+ os.writeString(contents);
+ os.close();
+ } catch(e) {
+ this.notifyURIFailed(uri);
+ failed = true;
+ }
+ foStream.close();
+
+ // Notify page saver that the save is done and whether it succeeded or not
+ this.callback(uri, !failed);
+}
+
+/**
+ * Downloads and saves the given uri to disk. Called for binary data like images
+ * or swfs it uses nsIWebBrowserPersist.
+ * @function saveURI
+ * @param {scPageSaver.scURI} uri - The uri for the file being saved
+ */
+scPageSaver.scDefaultFileSaver.prototype.saveURI = function(uri) {
+ this._currentURI = uri;
+ var file = this.documentLocalFile(uri);
+
+ this._persist = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Components.interfaces.nsIWebBrowserPersist);
+ this._persist.progressListener = new scPageSaver.scPersistListener(this);
+ try {
+ var fileURI = uri.toString().replace(/#.*$/, "");
+ var channel = scPageSaver.nsIIOService.newChannel(fileURI, "", null);
+
+ channel.loadFlags |= scPageSaver.nsIRequest.LOAD_FROM_CACHE;
+ channel.loadFlags |= scPageSaver.nsIRequest.VALIDATE_NEVER;
+
+ this._persist.saveChannel(channel, file);
+ } catch(e) {
+ this.notifyURIFailed(uri);
+ this._currentURI = null;
+ this._persist = null;
+ this.callback(uri, false);
+ }
+}
+
+/**
+ * Called instead of the save functions if the download of the given URI failed.
+ * @function notifyURIFailed
+ * @param {scPageSaver.scURI} uri - The uri whose download failed
+ */
+scPageSaver.scDefaultFileSaver.prototype.notifyURIFailed = function(uri) {
+ var file = this.documentLocalFile(uri);
+
+ // Remove the file that was created as a placeholder, if possible
+ try {
+ file.remove(false);
+ } catch(e) { }
+}
+
+/**
+ * Called by the scPersistListener when the save is done
+ * @function saveURIDone
+ */
+scPageSaver.scDefaultFileSaver.prototype.saveURIDone = function() {
+ var uri = this._currentURI;
+
+ // Unset variables
+ this._currentURI = null;
+ this._persist = null;
+
+ // Notify saver that we're done
+ this.callback(uri, true);
+}
+//}
+
+
+/**
+ * Default file provider component.
+ * Is responsible for downloading URIs and determining content type and character
+ * set of the downloaded file, as well as converting the contents to unicode.
+ * @class scPageSaver.scDefaultFileProvider
+ *///{
+/**
+ * The function to call when a download has completed. Called with the download
+ * object that was completed.
+ * @property {Function} callback
+ */
+/**
+ * Creates a file provider object
+ * @constructor scDefaultFileProvider
+ */
+scPageSaver.scDefaultFileProvider = function() {
+ this._downloads = [];
+}
+
+/**
+ * Creates a download object for the given URI and returns it.
+ * @function {scPageSaver.scDownload} createDownload
+ * @param {scPageSaver.scURI} uri - The uri to download
+ */
+scPageSaver.scDefaultFileProvider.prototype.createDownload = function(uri) {
+ var download = new scPageSaver.scDownload(uri, this);
+ this._downloads.push(download);
+ return download;
+}
+
+/**
+ * Called by the scDownload when the download is completed, it calls the download
+ * done callback with the proper data.
+ * @function downloadDone
+ * @param {scPageSaver.scDownload} download - The download that was completed
+ */
+scPageSaver.scDefaultFileProvider.prototype.downloadDone = function(download) {
+ for(var i = 0; i < this._downloads.length; i++) this._downloads.splice(i--, 1);
+ this.callback(download);
+}
+
+/**
+ * Cancels all active downloads
+ * @function cancel
+ */
+scPageSaver.scDefaultFileProvider.prototype.cancel = function() {
+ for(var i = 0; i < this._downloads.length; i++) {
+ this._downloads[i].stop();
+ }
+}
+//}
+
+
+/**
+ * Simple URI data storage class
+ * @class scPageSaver.scURI
+ *///{
+/**
+ * Creates a URI object.
+ * @constructor scURI
+ * @param {String} extractedURI - The URI extracted from the document
+ * @param {String or nsIURI} base - The base URI - used to resolve extracted URI against
+ * @param {String} type - The type of place where the URL was extracted from, like an attribute, import rule, style rule, etc.
+ * @param {String} where - The type of file it came from - "base" or "extcss"
+ */
+scPageSaver.scURI = function(extractedURI, base, type, where) {
+ var uriString = "";
+ if(extractedURI.indexOf("http") == 0) {
+ uriString = extractedURI;
+ } else if(base && !(base.resolve)) {
+ uriString = scPageSaver.nsIIOService.newURI(base, null, null).resolve(extractedURI);
+ } else if (base.resolve) {
+ uriString = base.resolve(extractedURI);
+ }
+
+ this.uri = scPageSaver.nsIIOService.newURI(uriString, null, null);
+ this.extractedURI = extractedURI || "";
+ this.type = type;
+ this.where = where;
+ this.dupe = false;
+};
+
+/**
+ * Tests of the path is the URI object is the same as the given one.
+ * @function {Boolean} isDupe
+ * @param {scPageSaver.scURI} compare - The object to compare against
+ * @return Whether they have the same path
+ */
+scPageSaver.scURI.prototype.isDupe = function(compare) {
+ try {
+ // Compare the two URLs intelligently, based on their scheme
+ return this.uri.equals(compare.uri);
+ } catch(e) {
+ // If the URLs cannot be compared, for example if one of them is an
+ // invalid file:// URL, compare their string version
+ return (this.toString() == compare.toString());
+ }
+}
+
+/**
+ * Tests if both URI objects are exact dupes, coming from the same location, with
+ * the same type, and with the same path.
+ * @function {Boolean} isExactDupe
+ * @param {scPageSaver.scURI} compare - The object to compare against
+ * @return Whether they are exactly the same
+ */
+scPageSaver.scURI.prototype.isExactDupe = function(compare) {
+ return (this.isDupe(compare) && this.where == compare.where && this.type == compare.type && this.extractedURI == compare.extractedURI);
+}
+
+/**
+ * Returns a string representation of the object
+ * @function {String} toString
+ * @return The string representation of the URI
+ */
+scPageSaver.scURI.prototype.toString = function() {
+ if(typeof this._string == 'undefined') {
+ if(!this.uri) {
+ this._string = false;
+ } else if(this.uri.path.indexOf("/") != 0) {
+ this._string = this.uri.prePath+"/"+this.uri.path;
+ } else {
+ this._string = this.uri.prePath+""+this.uri.path;
+ }
+ }
+ return this._string;
+};
+
+/**
+ * Comparison function passed to the sort method for scURI objects
+ * @function {static int} compare
+ * @param {scPageSaver.scURI} a - The first object
+ * @param {scPageSaver.scURI} b - The second object
+ * @return Ordering int for sort
+ */
+scPageSaver.scURI.compare = function(a,b) {
+ if (a.toString() < b.toString()) return -1;
+ if (a.toString() > b.toString()) return 1;
+ if (a.type == 'index') return -1;
+ return 0;
+};
+//}
+
+
+/**
+ * Download data storage class
+ * @class scPageSaver.scDownload
+ *///{
+/**
+ * Creates a download object.
+ * @constructor scDownload
+ * @param {scPageSaver.scURI} uri - The URI for the download
+ * @param {FileProvider} fileProvider - The creating file provider
+ */
+scPageSaver.scDownload = function(uri, fileProvider) {
+ this.contents = "";
+ this.contentType = "";
+ this.charset = "";
+ this.uri = uri;
+ this._fileProvider = fileProvider;
+}
+
+/**
+ * Starts the download.
+ * @function start
+ */
+scPageSaver.scDownload.prototype.start = function() {
+ // Create unichar stream loader and load channel (for getting from cache)
+ var fileURI = this.uri.toString().replace(/#.*$/, "");
+ try {
+ this._loader = Components.classes["@mozilla.org/network/unichar-stream-loader;1"].createInstance(Components.interfaces.nsIUnicharStreamLoader);
+ this._channel = scPageSaver.nsIIOService.newChannel(fileURI, "", null);
+ } catch(e) {
+ this._done(true);
+ return;
+ }
+
+ this._channel.loadFlags |= scPageSaver.nsIRequest.LOAD_FROM_CACHE;
+ this._channel.loadFlags |= scPageSaver.nsIRequest.VALIDATE_NEVER;
+
+ // Set post data if it can be gotten
+ try {
+ var sessionHistory = getWebNavigation().sessionHistory;
+ var entry = sessionHistory.getEntryAtIndex(sessionHistory.index, false);
+ entry = entry.QueryInterface(Components.interfaces.nsISHEntry);
+ if(entry.postData) {
+ var inputStream = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
+ inputStream.setData(entry.postData, entry.postData.length);
+ var uploadChannel = this._channel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ uploadChannel.setUploadStream(inputStream, "application/x-www-form-urlencoded", -1);
+ this._channel.QueryInterface(Components.interfaces.nsIHttpChannel).requestMethod = "POST";
+ }
+ } catch (e) {}
+
+ try {
+ this._loader.init(new scPageSaver.scDownload.UnicharObserver(this), null);
+ this._channel.asyncOpen(this._loader, null);
+ } catch(e) {
+ this._done(true);
+ }
+};
+
+/**
+ * Cancels the download if it's active.
+ * @function stop
+ */
+scPageSaver.scDownload.prototype.stop = function() {
+ if(this._channel) {
+ this._channel.cancel(Components.results.NS_BINDING_ABORTED);
+ }
+ this._channel = null;
+ this._loader = null;
+ this._fileProvider = null;
+ this.failed = true;
+ this.contents = null;
+ this.contentType = null;
+ this.charset = null;
+}
+
+/**
+ * Called when the downloading is done. Cleans up, and calls callback.
+ * @function _done
+ * @param {optional Boolean} failed - Whether done is being called after a failure or not. Defaults to false.
+ */
+scPageSaver.scDownload.prototype._done = function(failed) {
+ if(typeof failed == 'undefined') failed = false;
+ this._channel = null;
+ this._loader = null;
+ this.failed = failed;
+ this._fileProvider.downloadDone(this);
+ this._fileProvider = null;
+};
+
+
+/**
+ * Download Observer which converts contents to unicode.
+ * @class scPageSaver.scDownload.UnicharObserver
+ *///{
+scPageSaver.scDownload.UnicharObserver = function (download) {
+ this._download = download;
+ this._charset = null;
+}
+scPageSaver.scDownload.UnicharObserver.prototype.onDetermineCharset = function (loader, context, firstSegment, length) {
+ if(this._download.charset) {
+ this._charset = this._download.charset;
+ } else {
+ var channel = null;
+ if (loader) channel = loader.channel;
+ if (channel) this._charset = channel.contentCharset;
+ if (!this._charset || this._charset.length == 0) this._charset = scPageSaver.DEFAULT_CHARSET;
+ }
+ return this._charset;
+}
+scPageSaver.scDownload.UnicharObserver.prototype.onStreamComplete = function (loader, context, status, unicharData) {
+ switch (status) {
+ case Components.results.NS_OK:
+ var str = "";
+ try {
+ if (unicharData && unicharData.readString) {
+ var str_ = {};
+ while (unicharData.readString(-1, str_)) str += str_.value;
+ } else if (unicharData) {
+ // Firefox 6 and above
+ str = unicharData;
+ }
+ } catch (e) {
+ this._download._done(true);
+ return;
+ }
+
+ this._download.contents = str;
+ this._download.charset = this._charset;
+ if(loader.channel)
+ this._download.contentType = loader.channel.contentType;
+
+ this._download._done();
+ break;
+ default:
+ // Download failed
+ this._download._done(true);
+ break;
+ }
+};
+//}
+//}
+
+
+/**
+ * nsIWebBrowserPersist listener
+ * @class scPageSaver.scPersistListener
+ *///{
+scPageSaver.scPersistListener = function(fileSaver) {
+ this._fileSaver = fileSaver;
+}
+scPageSaver.scPersistListener.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Components.interfaces.nsIWebProgressListener)) {
+ return this;
+ }
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+};
+scPageSaver.scPersistListener.prototype.onStateChange = function(webProgress, request, stateFlags, status) {
+ if(stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP && stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK) {
+ this._fileSaver.saveURIDone();
+ this._fileSaver = null;
+ }
+};
+scPageSaver.scPersistListener.prototype.onProgressChange = function() {}
+scPageSaver.scPersistListener.prototype.onLocationChange = function() {}
+scPageSaver.scPersistListener.prototype.onStatusChange = function() {}
+scPageSaver.scPersistListener.prototype.onSecurityChange = function() {}
+//}
diff --git a/src/chrome/content/saving/ExactPersist.js b/src/chrome/content/saving/ExactPersist.js
new file mode 100644
index 0000000..7f8fb4d
--- /dev/null
+++ b/src/chrome/content/saving/ExactPersist.js
@@ -0,0 +1,196 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This object implements nsIWebBrowserPersist, and allows displaying the
+ * current download progress and status in the browser's download window.
+ */
+function ExactPersist() {
+
+}
+
+ExactPersist.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsICancelable,
+ Ci.nsIWebBrowserPersist,
+ ]),
+
+ // nsICancelable
+ cancel: function(aReason) {
+ this.result = aReason;
+ if (this._persistJob) {
+ this._persistJob.cancel(aReason);
+ }
+ },
+
+ // nsIWebBrowserPersist
+ persistFlags: 0,
+
+ // nsIWebBrowserPersist
+ currentState: Ci.nsIWebBrowserPersist.PERSIST_STATE_READY,
+
+ // nsIWebBrowserPersist
+ result: Cr.NS_OK,
+
+ // nsIWebBrowserPersist
+ progressListener: null,
+
+ // nsIWebBrowserPersist
+ saveURI: function(aURI, aCacheKey, aReferrer, aPostData, aExtraHeaders,
+ aFile, aPrivacyContext) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // nsIWebBrowserPersist
+ saveChannel: function(aChannel, aFile) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // nsIWebBrowserPersist
+ saveDocument: function(aDocument, aFile, aDataPath, aOutputContentType,
+ aEncodingFlags, aWrapColumn) {
+ // Pass exceptions to the progress listener.
+ try {
+ // Operation in progress.
+ this.currentState = Ci.nsIWebBrowserPersist.PERSIST_STATE_SAVING;
+ if (this.progressListener) {
+ this.progressListener.onStateChange(null, null,
+ Ci.nsIWebProgressListener.STATE_START |
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK, Cr.NS_OK);
+ }
+
+ // Find the local file to save to.
+ var targetFile = aFile.QueryInterface(Ci.nsIFileURL).file;
+
+ // Create a job to save the given document, and listen to its events.
+ var persistJob = new ExactPersistJob(this, aDocument, targetFile,
+ aDataPath, this.saveWithMedia, this.saveWithContentLocation);
+
+ // Store a reference to the PersistBundle object for the save operation.
+ this.persistBundle = persistJob.bundle;
+
+ // When saving a page that was extracted from an archive, use the
+ // information from the original archive to save the page correctly.
+ var originalPage = ArchiveCache.pageFromUri(aDocument.documentURIObject);
+
+ // Before the job is started, change the content location of the main
+ // document to reflect the desired location. This ensures that references
+ // to the main document in saved files are handled correctly.
+ persistJob.setResourceLocation(this.persistBundle.resources[0],
+ (originalPage && originalPage.originalUrl) || aDocument.documentURI);
+
+ // Save the given document.
+ persistJob.start();
+
+ // If the start succeeded, keep a reference to the save job to allow
+ // stopping it.
+ this._persistJob = persistJob;
+ } catch(e) {
+ Cu.reportError(e);
+ // Preserve the result code of XPCOM exceptions.
+ if (e instanceof Ci.nsIXPCException) {
+ this.result = e.result;
+ } else {
+ this.result = Cr.NS_ERROR_FAILURE;
+ }
+ // Report that the download is finished to the listener.
+ this._onComplete();
+ }
+ },
+
+ // nsIWebBrowserPersist
+ cancelSave: function() {
+ this.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ /**
+ * If set to true, objects and media files will be included when saving.
+ */
+ saveWithMedia: false,
+
+ /**
+ * If set to true, the page will be saved for inclusion in an MHTML file.
+ */
+ saveWithContentLocation: false,
+
+ /**
+ * PersistBundle object referencing the resources that have been saved.
+ */
+ persistBundle: null,
+
+ // JobEventListener
+ onJobProgressChange: function(aJob, aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
+ // Simply propagate the event to our listener.
+ if (this.progressListener) {
+ this.progressListener.onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ }
+ },
+
+ // JobEventListener
+ onJobComplete: function(aJob, aResult) {
+ this.result = aResult;
+ this._onComplete();
+ },
+
+ // JobEventListener
+ onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {
+ // Propagate this download event unaltered.
+ if (this.progressListener) {
+ this.progressListener.onStatusChange(aWebProgress, aRequest, aStatus,
+ aMessage);
+ }
+ },
+
+ _onComplete: function() {
+ // Never report the finished condition more than once.
+ if (this.currentState != Ci.nsIWebBrowserPersist.PERSIST_STATE_FINISHED) {
+ // Operation completed.
+ this.currentState = Ci.nsIWebBrowserPersist.PERSIST_STATE_FINISHED;
+ // Signal success or failure in the archiving process.
+ if (this.progressListener) {
+ this.progressListener.onStateChange(null, null,
+ Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK, this.result);
+ }
+ }
+ },
+
+ _persistJob: null,
+}
diff --git a/src/chrome/content/saving/ExactPersistInitialJob.js b/src/chrome/content/saving/ExactPersistInitialJob.js
new file mode 100644
index 0000000..9f33ac7
--- /dev/null
+++ b/src/chrome/content/saving/ExactPersistInitialJob.js
@@ -0,0 +1,70 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Ensures the selected save location is ready to accept the downloaded files.
+ *
+ * This class derives from Job. See the Job documentation for details.
+ *
+ * @param aTargetDataFolder
+ * The support folder to be prepared for data files.
+ */
+function ExactPersistInitialJob(aEventListener, aTargetDataFolder) {
+ Job.call(this, aEventListener);
+ this._targetDataFolder = aTargetDataFolder;
+}
+
+ExactPersistInitialJob.prototype = {
+ __proto__: Job.prototype,
+
+ // Job
+ _executeStart: function() {
+ // Delete the folder where the additional files will be saved.
+ if (this._targetDataFolder.exists()) {
+ this._targetDataFolder.remove(true);
+ }
+ // This job completed its execution.
+ this._notifyCompletion();
+ },
+
+ // Job
+ _executeCancel: function(aReason) {
+ // No special action is required since this object works synchronously.
+ },
+
+ _targetDataFolder: null,
+}
diff --git a/src/chrome/content/saving/ExactPersistJob.js b/src/chrome/content/saving/ExactPersistJob.js
new file mode 100644
index 0000000..88a8788
--- /dev/null
+++ b/src/chrome/content/saving/ExactPersistJob.js
@@ -0,0 +1,371 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+/**
+ * Manages the saving process of all the resources required to render a
+ * document, providing a single progress indication.
+ *
+ * This class derives from JobRunner. See the JobRunner documentation for
+ * details.
+ *
+ * The saving process starts immediately with a reference collection phase. The
+ * provided DOM document is examined, and a child job object is created for any
+ * reference to additional content that should be saved. The type of the child
+ * job object depends on the type of the referenced resource that has been
+ * found. The following types of referenced resources are recognized:
+ * - Previously parsed DOM document (handled by ExactPersistParsedJob)
+ * - Previously parsed CSS stylesheet (handled by ExactPersistParsedJob)
+ * - Resource required for rendering (handled by ExactPersistUnparsedJob)
+ * - Resource not required for rendering (not handled by a save job)
+ *
+ * During this phase, the PersistBundle object of this job is populated with
+ * one PersistResource object for each unique referenced resource, regardless of
+ * whether the resource should be saved locally or not. At the same time, each
+ * ExactPersistParsedJob object builds a list of ExactPersistReference objects,
+ * that associates every reference with its target PersistResource.
+ *
+ * At this point, this save job is ready to start. The first child jobs to be
+ * executed are those for unparsed resources, since a download failure is not
+ * necessarily fatal, but may affect how the references in parsed resources are
+ * updated later.
+ *
+ * When all the unparsed resources have been saved or skipped, the parsed
+ * resources are saved by the ExactPersistParsedJob objects. When a parsed
+ * resource is saved, all the web references that it contains are updated to
+ * point to either the locally saved file, if present, or the original absolute
+ * location of the target resource.
+ *
+ * Note that if the structure of a document involved in a save operation changes
+ * while the operation is in progress, the web references found in newly created
+ * DOM nodes or stylesheet rules will not be updated correctly.
+ *
+ * @param aDocument
+ * The document to be saved, which will be inspected to find additional
+ * related resources.
+ * @param aTargetFile
+ * The nsIFile where the main document will be saved.
+ * @param aTargetDataFolder
+ * The nsIFile of the support folder for data files.
+ */
+function ExactPersistJob(aEventListener, aDocument, aTargetFile,
+ aTargetDataFolder, aSaveWithMedia, aSaveWithContentLocation) {
+ // Never save resources in parallel. This is necessary because unparsed jobs
+ // must be completed before parsed jobs can be started.
+ JobRunner.call(this, aEventListener, false);
+ this.saveWithMedia = aSaveWithMedia;
+ this.saveWithContentLocation = aSaveWithContentLocation;
+
+ // Initialize the state of the object.
+ this.bundle = new PersistBundle();
+ this.folder = new PersistFolder(aTargetDataFolder);
+
+ // Initialize other member variables explicitly.
+ this._parsedJobs = [];
+
+ // Determine if the window from which the document is being saved is private.
+ var isPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
+
+ // If the collection phase succeeds and this job is started, the first thing
+ // to do is to delete any existing support folder for data files.
+ this._addJob(new ExactPersistInitialJob(this, aTargetDataFolder));
+
+ // Find the comparable target URI for the document, by removing the hash part.
+ // This step is required to ensure that the comparisons with other resource
+ // references in the PersistBundle object work correctly.
+ var referenceUri = aDocument.documentURIObject.clone();
+ try {
+ // If the URI has URL syntax, remove the hash part.
+ referenceUri.QueryInterface(Ci.nsIURL).ref = "";
+ } catch (e) {
+ // In case of errors, use the original URI.
+ }
+ // Create a new job for the document and recursively create the other jobs.
+ this.createJobForReference({saveLinkedDomDocument: aDocument}, referenceUri);
+
+ // At this point, all the parsed jobs have been created and added to the
+ // _parsedJobs array. Create an unparsed job for all the resources that should
+ // be saved and don't have a parsed job already associated with them.
+ for (var [, resource] in Iterator(this.bundle.resources)) {
+ if (resource.needsUnparsedJob && !resource.hasParsedJob) {
+ // Create a new object for saving the contents of the resource.
+ var job = new ExactPersistUnparsedJob(this, resource, isPrivate);
+ // Add the job to the list of the ones to be started.
+ this._addJob(job);
+ }
+ }
+
+ // Now that all the resources have been added to the PersistBundle object and
+ // the unparsed jobs have been added to the job list, it is time to add the
+ // parsed jobs at the end of the job list too.
+ for (var [curIndex, job] in Iterator(this._parsedJobs)) {
+ // Add the job to the list of the ones to be started.
+ this._addJob(job);
+ // Set the local file name for the parsed resource. The local file name for
+ // unparsed resources will be determined once the download has started,
+ // since their content type is not known in advance, and the MIME media type
+ // may affect the file extension that is actually used.
+ if (!curIndex) {
+ // This is the first parsed job, corresponding to the main document.
+ job.resource.file = aTargetFile;
+ } else {
+ // This is one of the additional files.
+ this.folder.addUnique(job.resource);
+ }
+ }
+}
+
+ExactPersistJob.prototype = {
+ __proto__: JobRunner.prototype,
+
+ /**
+ * If set to true, objects and media files will be included when saving.
+ */
+ saveWithMedia: false,
+
+ /**
+ * If set to true, the page will be saved for inclusion in an MHTML file.
+ */
+ saveWithContentLocation: false,
+
+ /**
+ * PersistBundle object containing all the resources for this save operation.
+ */
+ bundle: null,
+
+ /**
+ * PersistFolder object for the resources in addition to the main document.
+ */
+ folder: null,
+
+ /**
+ * Registers the given ExactPersistReference object with this save job, and
+ * creates a new child job to save the referenced resource if necessary.
+ *
+ * This function is usually called by the child job objects to create a new
+ * parsed job or prepare an unparsed job, but is also called to create the
+ * first job that saves the main document.
+ *
+ * @param aReference
+ * ExactPersistReference object to be registered with this save job.
+ * The object will be updated with the PersistResource object
+ * corresponding to the target.
+ * @param aReferenceUri
+ * nsIURI object pointing to the resource to be saved. This URI usually
+ * corresponds to the target of the reference, without the hash part.
+ */
+ createJobForReference: function(aReference, aReferenceUri) {
+ // Determine if we are about to save a modified version of the resource.
+ var saveModified =
+ aReference.saveLinkedDomDocument ||
+ aReference.saveLinkedCssStyleSheet ||
+ aReference.saveEmptyScriptType;
+ // Get the actual target resource object for the reference.
+ aReference.resource = this._getResourceForUri(aReferenceUri, saveModified);
+ // Execute the appropriate save action, if required.
+ if (saveModified) {
+ // Indicate that the resource is being saved by a parsed job.
+ aReference.resource.hasParsedJob = true;
+ // Create the actual parsed job.
+ var job = new ExactPersistParsedJob(this, aReference.resource);
+ // Add the parsed job to the list of those to be run after the unparsed
+ // jobs. The first job in this list corresponds to the main document.
+ this._parsedJobs.push(job);
+ // After the job has been added to the list, initialize it and inspect the
+ // referenced document or stylesheet to find additional jobs recursively.
+ if (aReference.saveLinkedDomDocument) {
+ job.initFromDomDocument(aReference.saveLinkedDomDocument);
+ } else if (aReference.saveLinkedCssStyleSheet) {
+ job.initFromCssStyleSheet(aReference.sourceDomDocument,
+ aReference.saveLinkedCssStyleSheet, aReference.targetUri,
+ aReference.saveLinkedFileCharacterSetHint);
+ } else if (aReference.saveEmptyScriptType) {
+ job.initFromEmptyScript(aReference.sourceDomDocument,
+ aReference.saveEmptyScriptType);
+ }
+ } else if (aReference.saveLinkedResource) {
+ // Do not create unparsed save jobs for locations that have side-effects
+ // when accessed, like "javascript:" or "mailto:" addresses.
+ if (!Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil).
+ URIChainHasFlags(aReferenceUri,
+ Ci.nsIProtocolHandler.URI_NON_PERSISTABLE)) {
+ // Do not create unparsed save jobs for resources that have not been
+ // actually loaded to display the document.
+ var loadedUriSpecs = aReference.sourceDomDocument.loadedUriSpecs;
+ if (loadedUriSpecs && loadedUriSpecs[aReferenceUri.spec]) {
+ // The resource will be saved by an unparsed job, unless a parsed job
+ // gets associated with the resource meanwhile.
+ aReference.resource.needsUnparsedJob = true;
+ } else {
+ // Store a special resource location instead of the original URI.
+ aReference.originalResourceNotLoaded = true;
+ }
+ }
+ }
+ },
+
+ /**
+ * Modifies the contentLocation property of the provided PersistResource
+ * object, setting it to the specified location. The specified URI is
+ * appropriately escaped before the property is initialized or updated.
+ */
+ setResourceLocation: function(aResource, aUriSpec) {
+ // If the content location of the referenced resources will be used to find
+ // them inside an MHTML archive, we must ensure that the references in the
+ // output are identical to the "Content-Location" headers, octet by octet.
+ // Since the HTML and XHTML serializers do aggressive escaping of URIs if
+ // they appear in attributes named "src" or "href", potentially making the
+ // strings different even if they contain only valid ASCII characters like
+ // the tilde ("~"), we must do the same aggressive escaping beforehand.
+ if (this.saveWithContentLocation) {
+ // Ensure that all the ASCII and international characters different from
+ // "%#;/?:@&=+$,[]" are URI-escaped using the UTF-8 character set. In this
+ // case, however, the character set is generally not relevant, as the URI
+ // specification is unlikely to contain international characters except
+ // when overriding the location of the main document. For more information
+ // on this escaping method, see the "EscapeURI" function in
+ //
+ // (retrieved 2009-12-24).
+ var textToSubUri = this._textToSubURI;
+ aResource.contentLocation = aUriSpec.replace(/[^%#;\/?:@&=+$,\[\]]+/g,
+ function(aPart) textToSubUri.ConvertAndEscape("utf-8", aPart));
+ } else {
+ // Additional escaping is not required.
+ aResource.contentLocation = aUriSpec;
+ }
+ },
+
+ /**
+ * Returns a reference to the PersistResource object corresponding to the
+ * specified parameters, from the PersistBundle associated with this job. If a
+ * corresponding object is not already available, a new PersistResource object
+ * will be created and added to the PersistBundle.
+ *
+ * @param aUri
+ * nsIURI object for the resource to be retrieved.
+ * @param aModified
+ * If true, indicates that a potentially modified version of the
+ * original resource will be saved in place of the original resource.
+ * This ensures that a unique reference is returned even if a modified
+ * resource with the same original URI already exists in the
+ * PersistBundle.
+ */
+ _getResourceForUri: function(aUri, aModified) {
+ // This function implements the logic that allow plain links to documents
+ // that are being saved locally to be updated to one of their saved
+ // versions, while ensuring that differently modified versions of the same
+ // original document get saved to different files.
+ var resource;
+ if (aModified) {
+ // We are about to save the modified contents of the resource originally
+ // coming from the specified URI. If a reference to an unmodified resource
+ // for the given URI already exists, it means that links to the resource
+ // were encountered previously. Since those links must be updated to point
+ // to the locally modified version of the resource, we reuse the
+ // corresponding resource object if possible. Conversely, we don't reuse
+ // resource objects corresponding to other modified resources, since we
+ // assume that every parsed version of the same original resource is
+ // unique.
+ resource = this.bundle.getResourceByOriginalUri(aUri);
+ } else {
+ // We are processing a reference to the web resource corresponding to the
+ // content originally located at the given URI. If we already encountered
+ // the resource, we can point to it even if we have a modified version.
+ resource = this.bundle.getResourceByReferenceUri(aUri);
+ }
+ // If no resource has been found, create a new one and add it to the bundle.
+ if (!resource) {
+ resource = this._createResourceForUri(aUri);
+ }
+ // If we are about to save a modified version of the resource, indicate this
+ // by generating a unique URI for the resource. This also ensures that the
+ // resource object will not be reused when saving a differently modified
+ // version of the resource retrieved from the same original URI.
+ if (aModified) {
+ this._setUniqueResourceLocation(resource);
+ }
+ // Return the existing or new resource object.
+ return resource;
+ },
+
+ /**
+ * Creates a new PersistResource object associated with the provided URI, and
+ * adds it to the PersistBundle object associated with this save operation.
+ */
+ _createResourceForUri: function(aUri) {
+ var resource = new PersistResource();
+ resource.referenceUri = aUri;
+ resource.originalUri = aUri;
+ this.setResourceLocation(resource, aUri.spec);
+ this.bundle.resources.push(resource);
+ this.bundle.addResourceToIndex(resource);
+ return resource;
+ },
+
+ /**
+ * Modifies the provided PersistResource object, indicating that it contains
+ * parsed content that may have been modified, and as such its unique URI does
+ * not correspond to the location the content was originally retrieved from.
+ * This allows multiple modified versions of the same original content to be
+ * present in the same web archive.
+ */
+ _setUniqueResourceLocation: function(aResource) {
+ // Generate a unique URI by prepending a random prefix to the original
+ // location, for example "urn:snapshot-A6B7C8D9:http://www.example.com/".
+ var randomHexString = Math.floor(Math.random() * 0x100000000).toString(16);
+ var uniquePrefix = "urn:snapshot-" +
+ ("0000000" + randomHexString.toUpperCase()).slice(-8) + ":";
+ var uniqueUri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newURI(uniquePrefix +
+ aResource.referenceUri.spec, null, null);
+ // Modify the properties of the provided resource object.
+ this.bundle.removeResourceFromIndex(aResource);
+ aResource.originalUri = uniqueUri;
+ this.setResourceLocation(aResource, uniqueUri.spec);
+ this.bundle.addResourceToIndex(aResource);
+ },
+
+ /**
+ * Array of ExactPersistParsedJob objects that will be run after the other
+ * jobs are completed.
+ */
+ _parsedJobs: [],
+
+ _textToSubURI: Cc["@mozilla.org/intl/texttosuburi;1"].
+ getService(Ci.nsITextToSubURI),
+}
diff --git a/src/chrome/content/saving/ExactPersistParsedJob.js b/src/chrome/content/saving/ExactPersistParsedJob.js
new file mode 100644
index 0000000..60af52a
--- /dev/null
+++ b/src/chrome/content/saving/ExactPersistParsedJob.js
@@ -0,0 +1,1047 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+k * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Archive Format.
+ *
+ * The Initial Developer of the Original Code is
+ * Paolo Amadini .
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This object implements the logic required to fix web references in parsed
+ * content documents that are being saved to another location.
+ *
+ * For every content document that is processed, the list of web addresses
+ * referenced in the document is collected. Each collected entry contains, along
+ * with the address itself, the in-memory reference to the DOM attribute or
+ * source fragment that will need modification when the new address for the
+ * resource is determined.
+ *
+ * The actual in-memory modification of the addresses generally happens after
+ * the resource has already been saved locally, since until then it is not known
+ * whether the save operation succeeded. Depending on the result of the save
+ * operation, the reference is replaced with a local relative URL or a full
+ * remote URI.
+ *
+ * This object supports DOM documents and CSS stylesheets, and can also be used
+ * to generate empty scripts.
+ *
+ * This class derives from Job. See the Job documentation for details.
+ *
+ * @param aResource
+ * PersistResource object associated with the document or other resource
+ * type to be saved.
+ */
+function ExactPersistParsedJob(aEventListener, aResource) {
+ Job.call(this, aEventListener);
+ this.resource = aResource;
+
+ // Initialize the unique identifier for this save job.
+ this._uniqueId = "job" + Math.floor(Math.random() * 1000000000);
+
+ // Initialize other member variables explicitly.
+ this.references = [];
+}
+
+ExactPersistParsedJob.prototype = {
+ __proto__: Job.prototype,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIDocumentEncoderNodeFixup,
+ ]),
+
+ /**
+ * PersistResource object associated with this parsed document.
+ */
+ resource: null,
+
+ /**
+ * String containing the character set for URIs in the current document, or
+ * null if no character set has been explicitly specified.
+ */
+ characterSet: null,
+
+ /**
+ * nsIURI object with the base URI that is used by default to resolve relative
+ * references in the document associated with this object. For DOM documents,
+ * this value matches the baseURI property, while for other resources this is
+ * equal to the original location of the resource.
+ */
+ baseUri: null,
+
+ /**
+ * Array of ExactPersistReference objects that represent all the references
+ * contained in the document associated with this object.
+ */
+ references: [],
+
+ /**
+ * Finds web references in the given DOM document. The document will then be
+ * saved when the job is started.
+ */
+ initFromDomDocument: function(aDocument) {
+ this._sourceDomDocument = aDocument;
+
+ // Set the properties required to parse the document properly.
+ this.characterSet = aDocument.characterSet;
+ this.baseUri = aDocument.baseURIObject;
+
+ // Set the properties required to save the resource properly.
+ this.resource.mimeType = aDocument.contentType;
+ this.resource.charset = aDocument.characterSet;
+
+ // Scan the document for web references and store the content to be saved.
+ this._document = aDocument;
+ this._scanDomDocument();
+ },
+
+ /**
+ * Finds web references in the given CSS stylesheet. The stylesheet will then
+ * be saved when the job is started.
+ */
+ initFromCssStyleSheet: function(aDocument, aStyleSheet, aStyleSheetUri,
+ aCharacterSet) {
+ this._sourceDomDocument = aDocument;
+
+ // Set the properties required to parse the stylesheet properly.
+ this.characterSet = aCharacterSet || null;
+ this.baseUri = aStyleSheetUri;
+
+ // If the stylesheet begins with a "@charset" rule, use the character set
+ // specified in the rule to parse the URIs contained in the stylesheet,
+ // instead of the one specified in the referencing document. If a different
+ // character set was specified in the "Content-Type" header when the file
+ // was downloaded, it is possible that the character set is still detected
+ // erroneously. For more information on the character set detection
+ // procedure that the browser should use for external stylesheets, see
+ //
+ // (retrieved 2009-12-22).
+ if (aStyleSheet.cssRules.length > 0) {
+ var firstRule = aStyleSheet.cssRules[0];
+ if (firstRule.type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
+ this.characterSet = firstRule.encoding;
+ }
+ }
+
+ // Set the properties required to save the resource properly.
+ this.resource.mimeType = "text/css";
+
+ // Scan the stylesheet for web references and store the content to be saved.
+ this._targetFragment = this._scanCssStyleSheet(aStyleSheet);
+ },
+
+ /**
+ * Saves an empty script when the job is started.
+ */
+ initFromEmptyScript: function(aDocument, aScriptType) {
+ this._sourceDomDocument = aDocument;
+
+ // Set the properties required to save the resource properly.
+ this.resource.mimeType = aScriptType;
+
+ // Store the actual content to be saved.
+ this._targetFragment = this._getEmptyScript(aScriptType);
+ },
+
+ /**
+ * Unique identifier used to distinguish this job from others that may be
+ * running at the same time. This value is used as an entry name in the
+ * exactPersistData property with which the involved DOM nodes are augmented.
+ */
+ _uniqueId: "",
+
+ /**
+ * This function performs all the operations required to create a new
+ * ExactPersistReference object, cross-referencing it with its source DOM node
+ * if required, and finally adding it to the references list for the document
+ * associated with this object.
+ *
+ * @param aProperties
+ * Object containing the initial values of some of the properties of
+ * the ExactPersistReference object to be created. Other properties are
+ * set automatically starting from the provided values. See the
+ * ExactPersistReference object for details.
+ */
+ _createReference: function(aProperties) {
+ // Handle the case where an URI value should be read from a DOM attribute
+ // without further processing. This is the most common way for web
+ // references to be specified in DOM documents. If the source DOM attribute
+ // does not contain an URI, at this point the value has been already
+ // processed, and the targetFragment property has been populated.
+ if (!aProperties.targetFragment && aProperties.sourceAttribute) {
+ // Read the target URI string from the attribute.
+ aProperties.targetUriSpec = aProperties.sourceDomNode.getAttribute(
+ aProperties.sourceAttribute);
+ // If the attribute is empty or missing, no reference should be created,
+ // unless we are saving a linked document.
+ if (!aProperties.targetUriSpec) {
+ if (!aProperties.saveLinkedDomDocument) {
+ return;
+ }
+ // If we are saving a linked document and the containing element has no
+ // source attribute, use an URI that results in a relevant file name.
+ aProperties.targetUriSpec = "http://generated.test/generated-content";
+ }
+ }
+
+ // Create and initialize the reference object.
+ aProperties.sourceDomDocument = this._sourceDomDocument;
+ var reference = new ExactPersistReference(this, aProperties);
+ // If the resource is supposed to have a target URI, but the corresponding
+ // absolute URI couldn't be resolved, ignore the reference and leave the
+ // unresolvable URI unaltered in the source file. If a parsed document is
+ // associated with the source element, it will not be processed.
+ if (reference.targetUriSpec && !reference.targetUri) {
+ return;
+ }
+ // The reference is valid and will be processed.
+ this.references.push(reference);
+
+ // If required, cross-reference the original DOM node with the associated
+ // ExactPersistReference object. This allows for a very fast lookup of the
+ // reference during the node fixup phase that is executed later.
+ if (reference.sourceDomNode) {
+ // Get a reference to the exactPersistData property of the DOM node, or
+ // augment the node with the property if it doesn't exist already. Since
+ // this property is set on an XPCNativeWrapper, it will not be available
+ // to the content documents.
+ var exactPersistData = reference.sourceDomNode.exactPersistData;
+ if (!exactPersistData) {
+ exactPersistData = {};
+ reference.sourceDomNode.exactPersistData = exactPersistData;
+ }
+ // Get a reference to the object, specific to this save job, that contains
+ // the list of references for the DOM node. If the object does not exist,
+ // a new object is created and the property is set accordingly.
+ var jobPersistData = exactPersistData[this._uniqueId];
+ if (!jobPersistData) {
+ jobPersistData = {
+ attributeReferences: [],
+ replaceChildReference: null,
+ };
+ exactPersistData[this._uniqueId] = jobPersistData;
+ }
+ // Finally, add the reference to the object.
+ if (reference.sourceAttribute) {
+ // This reference applies to a specific attribute.
+ jobPersistData.attributeReferences.push(reference);
+ } else {
+ // This reference requires the single child of this node to be replaced.
+ jobPersistData.replaceChildReference = reference;
+ }
+ }
+
+ // If this reference has a target web resource, continue with the process
+ // that determines how the target should be handled in the save operation.
+ if (!reference.targetUri) {
+ return;
+ }
+ // Remove the hash part of the target URI before comparing it with the URI
+ // of the current document to determine if the target is the same file.
+ var referenceUri = reference.targetUri.clone();
+ try {
+ // If the URI has URL syntax, remove the hash part.
+ referenceUri.QueryInterface(Ci.nsIURL).ref = "";
+ } catch (e) {
+ // In case of errors, use the original URI.
+ }
+ if (this._checkUriEquality(referenceUri, this.resource.referenceUri)) {
+ // Ensure that, if the target of the reference is the same document it's
+ // contained in, the reference won't point to another document. This
+ // could happen if differently modified versions of the same document are
+ // present in the persist bundle, as they would have been retrieved from
+ // the same original location. For references to the same document, no
+ // save action is required, since the target resource is being saved now.
+ reference.resource = this.resource;
+ return;
+ }
+ // Ensure that the reference gets associated with a PeristsResource object,
+ // and initialize a new save job for the target if required.
+ this._eventListener.createJobForReference(reference, referenceUri);
+ },
+
+ /**
+ * This support function returns true if the provided nsIURI objects point to
+ * the same resource, or false otherwise.
+ */
+ _checkUriEquality: function(aFirstUri, aSecondUri) {
+ // If one of the arguments is null, no match is found.
+ if (!aFirstUri || !aSecondUri) {
+ return false;
+ }
+ // Compare the two URIs intelligently, based on their scheme.
+ try {
+ return aFirstUri.equals(aSecondUri);
+ } catch(e) {
+ // If the URIs cannot be compared, for example if one of them is an
+ // invalid "file://" URL, compare their string version.
+ return (aFirstUri.spec == aSecondUri.spec);
+ }
+ },
+
+ /**
+ * Yields each node found in the DOM document associated with this job, that
+ * matches the given HTML element and attribute names. The element name can be
+ * "*" to indicate that any element is allowed, and the attribute name can
+ * evaluate to false to indicate that no specific attribute is required.
+ */
+ _htmlNodesGenerator: function(aElementName, aAttributeName) {
+ // Find all the nodes that correspond to the given HTML element. If an
+ // attribute name is specified, return only the elements that contain the
+ // named attribute.
+ var xpathExpression = "//" + aElementName +
+ (aAttributeName ? "[@" + aAttributeName + "]" : "");
+ // Execute the expression to find the resulting nodes, in any order.
+ var xpathResult = this._document.evaluate(xpathExpression, this._document,
+ null, Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
+ // Iterate over the resulting nodes.
+ var curNode;
+ while ((curNode = xpathResult.iterateNext())) {
+ yield curNode;
+ }
+ },
+
+ /**
+ * Yields each "" element found under the specified "