Bug 1005818 - Part 1: Load a widget as an app if the |src| is in the |widgetPages|. r=fabrice, sr=sicking
☠☠ backed out by 03acff0b4f82 ☠ ☠
authorJunior Hsu <juhsu@mozilla.com>
Thu, 03 Jul 2014 13:47:09 +0800
changeset 200260 e020d647d6d371b4da16dcb6f07b2b4c5d7add23
parent 200259 6ae6e5032735c1189800783ddc7d777a5ed8a7df
child 200261 e71a3cac1b3dd3d77ce23cf7edb37f627bb8020b
push id47860
push userryanvm@gmail.com
push dateTue, 19 Aug 2014 12:42:37 +0000
treeherdermozilla-inbound@bb91698edd20 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice, sicking
bugs1005818
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1005818 - Part 1: Load a widget as an app if the |src| is in the |widgetPages|. r=fabrice, sr=sicking 1. Add permission |embed-widgets| and Element attribute |mozwidget| 2. Add |hasWidgetPage| in /mozIApplication.idl 3. Check permission |embed-widgets| and the |src| is in the |widgetPages| when |GetAppManifest| 4. Add test case 5. Should enable preference |dom.enable_widgets|
content/base/public/nsIFrameLoader.idl
content/base/src/nsGkAtomList.h
content/html/content/src/nsGenericHTMLFrameElement.cpp
content/html/content/src/nsGenericHTMLFrameElement.h
dom/apps/src/AppsUtils.jsm
dom/apps/src/PermissionsTable.jsm
dom/apps/src/Webapps.jsm
dom/apps/tests/file_app.sjs
dom/apps/tests/file_widget_app.template.html
dom/apps/tests/file_widget_app.template.webapp
dom/apps/tests/mochitest.ini
dom/apps/tests/test_widget.html
dom/interfaces/apps/mozIApplication.idl
dom/interfaces/html/nsIMozBrowserFrame.idl
--- a/content/base/public/nsIFrameLoader.idl
+++ b/content/base/public/nsIFrameLoader.idl
@@ -283,16 +283,17 @@ interface nsIFrameLoader : nsISupports
    * The notion of "visibility" here is separate from the notion of a
    * window/docshell's visibility.  This field is mostly here so that we can
    * have a notion of visibility in the parent process when frames are OOP.
    */
   [infallible] attribute boolean visible;
 
   /**
    * Find out whether the owner content really is a browser or app frame
+   * Especially, a widget frame is regarded as an app frame.
    */
   readonly attribute boolean ownerIsBrowserOrAppFrame;
 };
 
 %{C++
 class nsFrameLoader;
 %}
 
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -86,16 +86,17 @@ GK_ATOM(alternate, "alternate")
 GK_ATOM(always, "always")
 GK_ATOM(ancestor, "ancestor")
 GK_ATOM(ancestorOrSelf, "ancestor-or-self")
 GK_ATOM(anchor, "anchor")
 GK_ATOM(_and, "and")
 GK_ATOM(anonid, "anonid")
 GK_ATOM(any, "any")
 GK_ATOM(mozapp, "mozapp")
+GK_ATOM(mozwidget, "mozwidget")
 GK_ATOM(applet, "applet")
 GK_ATOM(applyImports, "apply-imports")
 GK_ATOM(applyTemplates, "apply-templates")
 GK_ATOM(mozapptype, "mozapptype")
 GK_ATOM(apz, "apz")
 GK_ATOM(archive, "archive")
 GK_ATOM(area, "area")
 GK_ATOM(arrow, "arrow")
--- a/content/html/content/src/nsGenericHTMLFrameElement.cpp
+++ b/content/html/content/src/nsGenericHTMLFrameElement.cpp
@@ -327,55 +327,113 @@ nsGenericHTMLFrameElement::GetIsExpectin
   if (!nsIMozBrowserFrame::GetReallyIsApp()) {
     return NS_OK;
   }
 
   *aOut = HasAttr(kNameSpaceID_None, nsGkAtoms::expectingSystemMessage);
   return NS_OK;
 }
 
+/** Get manifest url of app or widget
+ * @param AppType: nsGkAtoms::mozapp or nsGkAtoms::mozwidget
+ */
+void nsGenericHTMLFrameElement::GetManifestURLByType(nsIAtom *aAppType,
+                                                     nsAString& aManifestURL)
+{
+  aManifestURL.Truncate();
+
+  if (aAppType != nsGkAtoms::mozapp && aAppType != nsGkAtoms::mozwidget) {
+    return;
+  }
+
+  nsAutoString manifestURL;
+  GetAttr(kNameSpaceID_None, aAppType, manifestURL);
+  if (manifestURL.IsEmpty()) {
+    return;
+  }
+
+  // Check permission.
+  nsCOMPtr<nsIPermissionManager> permMgr = services::GetPermissionManager();
+  NS_ENSURE_TRUE_VOID(permMgr);
+  nsIPrincipal *principal = NodePrincipal();
+  const char* aPermissionType = (aAppType == nsGkAtoms::mozapp) ? "embed-apps"
+                                                                : "embed-widgets";
+  uint32_t permission = nsIPermissionManager::DENY_ACTION;
+  nsresult rv = permMgr->TestPermissionFromPrincipal(principal,
+                                                     aPermissionType,
+                                                     &permission);
+  NS_ENSURE_SUCCESS_VOID(rv);
+  if (permission != nsIPermissionManager::ALLOW_ACTION) {
+    return;
+  }
+
+  nsCOMPtr<nsIAppsService> appsService = do_GetService(APPS_SERVICE_CONTRACTID);
+  NS_ENSURE_TRUE_VOID(appsService);
+
+  nsCOMPtr<mozIApplication> app;
+  appsService->GetAppByManifestURL(manifestURL, getter_AddRefs(app));
+
+  if (!app) {
+    return;
+  }
+
+  bool hasWidgetPage = false;
+  nsAutoString src;
+  if (aAppType == nsGkAtoms::mozwidget) {
+    GetAttr(kNameSpaceID_None, nsGkAtoms::src, src);
+    nsresult rv = app->HasWidgetPage(src, &hasWidgetPage);
+
+    if (!NS_SUCCEEDED(rv) || !hasWidgetPage) {
+      return;
+    }
+  }
+
+  aManifestURL.Assign(manifestURL);
+}
+
 NS_IMETHODIMP
 nsGenericHTMLFrameElement::GetAppManifestURL(nsAString& aOut)
 {
   aOut.Truncate();
 
   // At the moment, you can't be an app without being a browser.
   if (!nsIMozBrowserFrame::GetReallyIsBrowserOrApp()) {
     return NS_OK;
   }
 
-  // Check permission.
-  nsIPrincipal *principal = NodePrincipal();
-  nsCOMPtr<nsIPermissionManager> permMgr =
-    services::GetPermissionManager();
-  NS_ENSURE_TRUE(permMgr, NS_OK);
+  nsAutoString appManifestURL;
+  nsAutoString widgetManifestURL;
+
+  GetManifestURLByType(nsGkAtoms::mozapp, appManifestURL);
+
+  if (Preferences::GetBool("dom.enable_widgets")) {
+    GetManifestURLByType(nsGkAtoms::mozwidget, widgetManifestURL);
+  }
 
-  uint32_t permission = nsIPermissionManager::DENY_ACTION;
-  nsresult rv = permMgr->TestPermissionFromPrincipal(principal,
-                                                     "embed-apps",
-                                                     &permission);
-  NS_ENSURE_SUCCESS(rv, NS_OK);
-  if (permission != nsIPermissionManager::ALLOW_ACTION) {
+  bool isApp = !appManifestURL.IsEmpty();
+  bool isWidget = !widgetManifestURL.IsEmpty();
+
+  if (!isApp && !isWidget) {
+    // No valid case to get manifest
+    return NS_OK;
+  }
+
+  if (isApp && isWidget) {
+    NS_WARNING("Can not simultaneously be mozapp and mozwidget");
     return NS_OK;
   }
 
   nsAutoString manifestURL;
-  GetAttr(kNameSpaceID_None, nsGkAtoms::mozapp, manifestURL);
-  if (manifestURL.IsEmpty()) {
-    return NS_OK;
+  if (isApp) {
+    manifestURL.Assign(appManifestURL);
+  } else if (isWidget) {
+    manifestURL.Assign(widgetManifestURL);
   }
 
-  nsCOMPtr<nsIAppsService> appsService = do_GetService(APPS_SERVICE_CONTRACTID);
-  NS_ENSURE_TRUE(appsService, NS_OK);
-
-  nsCOMPtr<mozIApplication> app;
-  appsService->GetAppByManifestURL(manifestURL, getter_AddRefs(app));
-  if (app) {
-    aOut.Assign(manifestURL);
-  }
+  aOut.Assign(manifestURL);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsGenericHTMLFrameElement::DisallowCreateFrameLoader()
 {
   MOZ_ASSERT(!mFrameLoader);
--- a/content/html/content/src/nsGenericHTMLFrameElement.h
+++ b/content/html/content/src/nsGenericHTMLFrameElement.h
@@ -83,11 +83,14 @@ public:
 
 protected:
   virtual ~nsGenericHTMLFrameElement() {}
 
   virtual mozilla::dom::Element* ThisFrameElement() MOZ_OVERRIDE
   {
     return this;
   }
+
+private:
+  void GetManifestURLByType(nsIAtom *aAppType, nsAString& aOut);
 };
 
 #endif // nsGenericHTMLFrameElement_h
--- a/dom/apps/src/AppsUtils.jsm
+++ b/dom/apps/src/AppsUtils.jsm
@@ -52,16 +52,20 @@ mozIApplication.prototype = {
     // permission.
     let principal = secMan.getAppCodebasePrincipal(uri, this.localId,
                                                    /*mozbrowser*/false);
     let perm = Services.perms.testExactPermissionFromPrincipal(principal,
                                                                aPermission);
     return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
   },
 
+  hasWidgetPage: function(aPageURL) {
+    return this.widgetPages.indexOf(aPageURL) != -1;
+  },
+
   QueryInterface: function(aIID) {
     if (aIID.equals(Ci.mozIApplication) ||
         aIID.equals(Ci.nsISupports))
       return this;
     throw Cr.NS_ERROR_NO_INTERFACE;
   }
 }
 
@@ -95,16 +99,17 @@ function _setAppProperties(aObj, aApp) {
   aObj.packageHash = aApp.packageHash;
   aObj.staged = aApp.staged;
   aObj.installerAppId = aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID;
   aObj.installerIsBrowser = !!aApp.installerIsBrowser;
   aObj.storeId = aApp.storeId || "";
   aObj.storeVersion = aApp.storeVersion || 0;
   aObj.role = aApp.role || "";
   aObj.redirects = aApp.redirects;
+  aObj.widgetPages = aApp.widgetPages || [];
   aObj.kind = aApp.kind;
 }
 
 this.AppsUtils = {
   // Clones a app, without the manifest.
   cloneAppObject: function(aApp) {
     let obj = {};
     _setAppProperties(obj, aApp);
@@ -673,16 +678,20 @@ ManifestHelper.prototype = {
   get orientation() {
     return this._localeProp("orientation");
   },
 
   get package_path() {
     return this._localeProp("package_path");
   },
 
+  get widgetPages() {
+    return this._localeProp("widgetPages");
+  },
+
   get size() {
     return this._manifest["size"] || 0;
   },
 
   get permissions() {
     if (this._manifest.permissions) {
       return this._manifest.permissions;
     }
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -227,16 +227,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "embed-apps": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
+                           "embed-widgets": {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
                            "storage": {
                              app: ALLOW_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION,
                              substitute: [
                                "indexedDB-unlimited",
                                "default-persistent-storage"
                              ]
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -257,16 +257,20 @@ this.DOMApplicationRegistry = {
           app.storeVersion = 0;
         }
 
         // Default role to "".
         if (app.role === undefined) {
           app.role = "";
         }
 
+        if (app.widgetPages === undefined) {
+          app.widgetPages = [];
+        }
+
         // At startup we can't be downloading, and the $TMP directory
         // will be empty so we can't just apply a staged update.
         app.downloading = false;
         app.readyToApplyDownload = false;
       }
     });
   },
 
@@ -316,16 +320,25 @@ this.DOMApplicationRegistry = {
           isAbsoluteURI(redirect.from) &&
           !isAbsoluteURI(redirect.to)) {
         res.push(redirect);
       }
     }
     return res.length > 0 ? res : null;
   },
 
+  _saveWidgetsFullPath: function(aManifest, aDestApp) {
+    if (aManifest.widgetPages) {
+      aDestApp.widgetPages = aManifest.widgetPages.map(aManifest.resolveURL,
+                                                       aManifest/* thisArg */);
+    } else {
+      aDestApp.widgetPages = [];
+    }
+  },
+
   // Registers all the activities and system messages.
   registerAppsHandlers: Task.async(function*(aRunUpdate) {
     this.notifyAppsRegistryStart();
     let ids = [];
     for (let id in this.webapps) {
       ids.push({ id: id });
     }
     if (supportSystemMessages()) {
@@ -340,16 +353,20 @@ this.DOMApplicationRegistry = {
           // If we can't load the manifest, we probably have a corrupted
           // registry. We delete the app since we can't do anything with it.
           delete this.webapps[aResult.id];
           return;
         }
         let app = this.webapps[aResult.id];
         app.csp = aResult.manifest.csp || "";
         app.role = aResult.manifest.role || "";
+
+        let localeManifest = new ManifestHelper(aResult.manifest, app.origin, app.manifestURL);
+        this._saveWidgetsFullPath(localeManifest, app);
+
         if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
           app.redirects = this.sanitizeRedirects(aResult.redirects);
         }
         if (app.origin.startsWith("app://")) {
           app.kind = this.kPackaged;
         } else {
           // Hosted apps, can be appcached or not.
           app.kind = aResult.manifest.appcache_path ? this.kHostedAppcache
@@ -979,16 +996,18 @@ this.DOMApplicationRegistry = {
         }
 
         let localeManifest =
           new ManifestHelper(manifest, app.origin, app.manifestURL);
 
         app.name = manifest.name;
         app.csp = manifest.csp || "";
         app.role = localeManifest.role;
+        this._saveWidgetsFullPath(localeManifest, app);
+
         if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
           app.redirects = this.sanitizeRedirects(manifest.redirects);
         }
         if (app.origin.startsWith("app://")) {
           app.kind = this.kPackaged;
         } else {
           // Hosted apps, can be appcached or not.
           app.kind = aResult.manifest.appcache_path ? this.kHostedAppcache
@@ -2009,16 +2028,18 @@ this.DOMApplicationRegistry = {
 
     let manifest =
       new ManifestHelper(aNewManifest, aApp.origin, aApp.manifestURL);
     // A package is available: set downloadAvailable to fire the matching
     // event.
     aApp.downloadAvailable = true;
     aApp.downloadSize = manifest.size;
     aApp.updateManifest = aNewManifest;
+    this._saveWidgetsFullPath(manifest, aApp);
+
     yield this._saveApps();
 
     this.broadcastMessage("Webapps:UpdateState", {
       app: aApp,
       id: aApp.id
     });
     this.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadavailable",
@@ -2067,16 +2088,17 @@ this.DOMApplicationRegistry = {
       }
 
       this.updateDataStore(this.webapps[aId].localId, aApp.origin,
                            aApp.manifestURL, aApp.manifest);
 
       aApp.name = aNewManifest.name;
       aApp.csp = manifest.csp || "";
       aApp.role = manifest.role || "";
+      this._saveWidgetsFullPath(manifest, aApp);
       aApp.updateTime = Date.now();
     } else {
       manifest =
         new ManifestHelper(aOldManifest, aApp.origin, aApp.manifestURL);
     }
 
     // Update the registry.
     this.webapps[aId] = aApp;
@@ -2466,16 +2488,17 @@ this.DOMApplicationRegistry = {
       throw Error("Unknown app kind: " + appObject.kind);
     }
 
     appObject.localId = aLocalId;
     appObject.basePath = OS.Path.dirname(this.appsFile);
     appObject.name = aManifest.name;
     appObject.csp = aLocaleManifest.csp || "";
     appObject.role = aLocaleManifest.role;
+    this._saveWidgetsFullPath(aLocaleManifest, appObject);
     appObject.installerAppId = aData.appId;
     appObject.installerIsBrowser = aData.isBrowser;
 
     return appObject;
   },
 
   _writeManifestFile: function(aId, aIsPackage, aJsonManifest) {
     debug("_writeManifestFile");
@@ -2501,16 +2524,19 @@ this.DOMApplicationRegistry = {
         (aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) {
       return;
     }
 
     app.name = aManifest.name;
 
     app.csp = aManifest.csp || "";
 
+    let aLocaleManifest = new ManifestHelper(aManifest, app.origin, app.manifestURL);
+    this._saveWidgetsFullPath(aLocaleManifest, app);
+
     app.appStatus = AppsUtils.getAppManifestStatus(aManifest);
 
     app.removable = true;
 
     // Reuse the app ID if the scheme is "app".
     let uri = Services.io.newURI(app.origin, null, null);
     if (uri.scheme == "app") {
       app.id = uri.host;
--- a/dom/apps/tests/file_app.sjs
+++ b/dom/apps/tests/file_app.sjs
@@ -1,25 +1,25 @@
 var gBasePath = "tests/dom/apps/tests/";
 var gAppTemplatePath = "tests/dom/apps/tests/file_app.template.html";
 var gAppcacheTemplatePath = "tests/dom/apps/tests/file_cached_app.template.appcache";
+var gWidgetTemplatePath = "tests/dom/apps/tests/file_widget_app.template.html";
 var gDefaultIcon = "default_icon";
 
 function makeResource(templatePath, version, apptype) {
   let icon = getState('icon') || gDefaultIcon;
   var res = readTemplate(templatePath).replace(/VERSIONTOKEN/g, version)
                                       .replace(/APPTYPETOKEN/g, apptype)
                                       .replace(/ICONTOKEN/g, icon);
 
   // Hack - This is necessary to make the tests pass, but hbambas says it
   // shouldn't be necessary. Comment it out and watch the tests fail.
   if (templatePath == gAppTemplatePath && apptype == 'cached') {
     res = res.replace('<html>', '<html manifest="file_app.sjs?apptype=cached&getappcache=true">');
   }
-
   return res;
 }
 
 function handleRequest(request, response) {
   var query = getQuery(request);
 
   // If this is a version update, update state and return.
   if ("setVersion" in query) {
@@ -41,17 +41,17 @@ function handleRequest(request, response
     response.setHeader("Content-Type", "text/html", false);
     response.setHeader("Access-Control-Allow-Origin", "*", false);
     response.write('OK');
     return;
   }
 
   // Get the app type.
   var apptype = query.apptype;
-  if (apptype != 'hosted' && apptype != 'cached')
+  if (apptype != 'hosted' && apptype != 'cached' && apptype != 'widget')
     throw "Invalid app type: " + apptype;
 
   // Get the version from server state and handle the etag.
   var version = Number(getState('version'));
   var etag = getEtag(request, version);
   dump("Server Etag: " + etag + "\n");
 
   if (etagMatches(request, etag)) {
@@ -78,17 +78,22 @@ function handleRequest(request, response
   //
   // NB: Among other reasons, we use the same sjs file here so that the version
   //     state is shared.
   if (apptype == 'cached' && 'getappcache' in query) {
     response.setHeader("Content-Type", "text/cache-manifest", false);
     response.write(makeResource(gAppcacheTemplatePath, version, apptype));
     return;
   }
-
+  else if (apptype == 'widget')
+  {
+    response.setHeader("Content-Type", "text/html", false);
+    response.write(makeResource(gWidgetTemplatePath, version, apptype));
+    return;
+  }
   // Generate the app.
   response.setHeader("Content-Type", "text/html", false);
   response.write(makeResource(gAppTemplatePath, version, apptype));
 }
 
 function getEtag(request, version) {
   return request.queryString.replace(/&/g, '-').replace(/=/g, '-') + '-' + version;
 }
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_widget_app.template.html
@@ -0,0 +1,68 @@
+<html>
+<head>
+<script>
+
+function sendMessage(msg) {
+  alert(msg);
+}
+
+function ok(p, msg) {
+  if (p)
+    sendMessage("OK: " + msg);
+  else
+    sendMessage("KO: " + msg);
+}
+
+function is(a, b, msg) {
+  if (a == b)
+    sendMessage("OK: " + a + " == " + b + " - " + msg);
+  else
+    sendMessage("KO: " + a + " != " + b + " - " + msg);
+}
+
+function installed(p) {
+  if (p)
+    sendMessage("IS_INSTALLED");
+  else
+    sendMessage("NOT_INSTALLED");
+}
+
+function finish() {
+  sendMessage("VERSION: MyWebApp vVERSIONTOKEN");
+  sendMessage("DONE");
+}
+
+function cbError() {
+  ok(false, "Error callback invoked");
+  finish();
+}
+
+function go() {
+  ok(true, "Launched app");
+  var request = window.navigator.mozApps.getSelf();
+  request.onsuccess = function() {
+    var widget = request.result;
+    ok(widget,"Should be a widget");
+    checkWidget(widget);
+  }
+  request.onerror = cbError;
+}
+
+function checkWidget(widget) {
+  // If the widget is installed, |widget| will be non-null. If it is, verify its state.
+  installed(!!widget);
+  if (widget) {
+    var widgetName = "Really Rapid Release (APPTYPETOKEN)";
+    var manifest = SpecialPowers.wrap(widget.manifest);
+    is(manifest.name, widgetName, "Manifest name should be correct");
+    is(widget.origin, "http://test", "Widget origin should be correct");
+    is(widget.installOrigin, "http://mochi.test:8888", "Install origin should be correct");
+  }
+  finish();
+}
+
+</script>
+</head>
+<body onload="go();">
+App Body. Version: VERSIONTOKEN
+</body></html>
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/file_widget_app.template.webapp
@@ -0,0 +1,11 @@
+{
+  "name": "Really Rapid Release (widget)",
+  "description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
+  "launch_path": "/tests/dom/apps/tests/file_app.sjs?apptype=widget",
+  "icons": {
+    "128": "ICONTOKEN"
+  },
+  "widgetPages": [
+    "/tests/dom/apps/tests/file_app.sjs?apptype=widget"
+  ]
+}
--- a/dom/apps/tests/mochitest.ini
+++ b/dom/apps/tests/mochitest.ini
@@ -5,16 +5,18 @@ support-files =
   file_app.sjs
   file_app.template.html
   file_cached_app.template.appcache
   file_cached_app.template.webapp
   file_hosted_app.template.webapp
   file_packaged_app.sjs
   file_packaged_app.template.html
   file_packaged_app.template.webapp
+  file_widget_app.template.webapp
+  file_widget_app.template.html
   signed_app.sjs
   signed_app_template.webapp
   signed/*
   test_packaged_app_common.js
   marketplace/*
   pkg_install_iframe.html
 
 [test_app_update.html]
@@ -23,8 +25,9 @@ support-files =
 [test_install_receipts.html]
 [test_marketplace_pkg_install.html]
 skip-if = buildapp == "b2g" || toolkit == "android" # see bug 989806
 [test_packaged_app_install.html]
 [test_packaged_app_update.html]
 [test_receipt_operations.html]
 [test_signed_pkg_install.html]
 [test_uninstall_errors.html]
+[test_widget.html]
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/test_widget.html
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - basic operation on a readonly db</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  var gWidgetManifestURL = 'http://test/tests/dom/apps/tests/file_app.sjs?apptype=widget&getmanifest=true';
+  var gApp;
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function installApp() {
+    var request = navigator.mozApps.install(gWidgetManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      gApp = request.result;
+
+      runTest();
+    }
+  }
+
+  function uninstallApp() {
+    // Uninstall the app.
+    var request = navigator.mozApps.mgmt.uninstall(gApp);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      // All done.
+      info("All done");
+      runTest();
+    }
+  }
+
+  function testApp() {
+    var ifr = document.createElement('iframe');
+    ifr.setAttribute('mozbrowser', 'true');
+    ifr.setAttribute('mozwidget', gApp.manifestURL);
+    ifr.setAttribute('src', gApp.origin+gApp.manifest.launch_path);
+
+    var domParent = document.getElementById('container');
+
+    // Set us up to listen for messages from the app.
+    var listener = function(e) {
+      var message = e.detail.message;
+      if (/^OK/.exec(message)) {
+        ok(true, "Message from widget: " + message);
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from widget: " + message);
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Message from widget complete");
+        ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(ifr);
+        runTest();
+      }
+    }
+
+    // This event is triggered when the app calls "alert".
+    ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
+    domParent.appendChild(ifr);
+  }
+
+  var tests = [
+    // Permissions
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "browser", "allow": 1, "context": document },
+         { "type": "embed-widgets", "allow": 1, "context": document },
+         { "type": "webapps-manage", "allow": 1, "context": document }], runTest);
+    },
+
+    // Preferences
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.mozBrowserFramesEnabled", true],
+                                         ["dom.enable_widgets", true]]}, runTest);
+    },
+
+    function() {
+      SpecialPowers.setAllAppsLaunchable(true);
+      runTest();
+    },
+
+    // No confirmation needed when an app is installed
+    function() {
+      SpecialPowers.autoConfirmAppInstall(runTest);
+    },
+
+    // Installing the app
+    installApp,
+
+    // Run tests in app
+    testApp,
+
+    // Uninstall the app
+    uninstallApp
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  if (SpecialPowers.isMainProcess()) {
+    SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTest();
+  </script>
+</body>
+</html>
--- a/dom/interfaces/apps/mozIApplication.idl
+++ b/dom/interfaces/apps/mozIApplication.idl
@@ -6,22 +6,28 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "domstubs.idl"
 
 /**
  * We expose Gecko-internal helpers related to "web apps" through this
  * sub-interface.
  */
-[scriptable, uuid(7bd62430-c374-49eb-be1b-ce821a180360)]
+[scriptable, uuid(1d856b11-ac29-47d3-bd52-a86e3d45fcf4)]
 interface mozIApplication: nsISupports
 {
   /* Return true if this app has |permission|. */
   boolean hasPermission(in string permission);
 
+  /**
+   *  Return true if this app can be a widget and
+   *  its |widgetPages| contains |page|
+   */
+  boolean hasWidgetPage(in DOMString pageURL);
+
   /* Application status as defined in nsIPrincipal. */
   readonly attribute unsigned short appStatus;
 
   /* Returns the uuid of the app. */
   readonly attribute DOMString id;
 
   /* Returns the origin of the app. */
   readonly attribute DOMString origin;
--- a/dom/interfaces/html/nsIMozBrowserFrame.idl
+++ b/dom/interfaces/html/nsIMozBrowserFrame.idl
@@ -20,33 +20,37 @@ interface nsIMozBrowserFrame : nsIDOMMoz
    * may have to pass various security checks.
    */
   [infallible] readonly attribute boolean reallyIsBrowserOrApp;
 
   /**
    * Gets whether this frame really is an app frame.
    *
    * In order to really be an app frame, this frame must really be a browser
-   * frame (this requirement will go away eventually), and the frame's mozapp
-   * attribute must point to the manifest of a valid app.
+   * frame (this requirement will go away eventually), and must satisfy one
+   * and only one of the following conditions:
+   * 1. the frame's mozapp attribute must point to the manifest of a valid app
+   * 2. the frame's mozwidget attribute must point to the manifest of a valid
+   * app, and the src should be in the |widgetPages| specified by the manifest.
    */
   [infallible] readonly attribute boolean reallyIsApp;
 
   /**
    * This corresponds to the expecting-system-message attribute, which tells us
    * whether we should expect that this frame will receive a system message once
    * it starts up.
    *
    * It's the embedder's job to set this attribute on a frame.  Its presence
    * might cause us to increase the priority of the frame's process.
    */
   [infallible] readonly attribute boolean isExpectingSystemMessage;
 
   /**
-   * Gets this frame's app manifest URL, if the frame really is an app frame.
+   * Gets this frame's app manifest URL or widget manifest URL, if the frame
+   * really is an app frame.
    * Otherwise, returns the empty string.
    *
    * This method is guaranteed not to fail.
    */
   readonly attribute AString appManifestURL;
 
   /**
    * Normally, a frame tries to create its frame loader when its src is