// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.omaha; import android.text.TextUtils; import android.util.Log; import org.chromium.chrome.browser.omaha.XMLParser.Node; /** * Parses XML responses from the Omaha Update Server. * * Expects XML formatted like: * <?xml version="1.0" encoding="UTF-8"?> * <daystart elapsed_seconds="65524"/> * <app appid="{appid}" status="ok"> * <updatecheck status="ok"> * <urls> * <url codebase="https://market.android.com/details?id=com.google.android.apps.chrome/"/> * </urls> * <manifest version="0.16.4130.199"> * <packages> * <package hash="0" name="dummy.apk" required="true" size="0"/> * </packages> * <actions> * <action event="install" run="dummy.apk"/> * <action event="postinstall"/> * </actions> * </manifest> * </updatecheck> * <ping status="ok"/> * </app> * </response> * * The appid is dependent on the variant of Chrome that is running. */ public class ResponseParser { private static final String TAG = "ResponseParser"; // Tags that we care to parse from the response. private static final String TAG_APP = "app"; private static final String TAG_DAYSTART = "daystart"; private static final String TAG_EVENT = "event"; private static final String TAG_MANIFEST = "manifest"; private static final String TAG_PING = "ping"; private static final String TAG_RESPONSE = "response"; private static final String TAG_UPDATECHECK = "updatecheck"; private static final String TAG_URL = "url"; private static final String TAG_URLS = "urls"; private final String mAppId; private final boolean mExpectInstallEvent; private final boolean mExpectPing; private final boolean mExpectUpdatecheck; private final boolean mStrictParsingMode; private Integer mDaystartSeconds; private String mAppStatus; private String mUpdateStatus; private String mNewVersion; private String mUrl; private boolean mParsedInstallEvent; private boolean mParsedPing; private boolean mParsedUpdatecheck; public ResponseParser(String appId, boolean expectInstallEvent, boolean expectPing, boolean expectUpdatecheck) { this(false, appId, expectInstallEvent, expectPing, expectUpdatecheck); } public ResponseParser(boolean strictParsing, String appId, boolean expectInstallEvent, boolean expectPing, boolean expectUpdatecheck) { mStrictParsingMode = strictParsing; mAppId = appId; mExpectInstallEvent = expectInstallEvent; mExpectPing = expectPing; mExpectUpdatecheck = expectUpdatecheck; } public void parseResponse(String xml) throws RequestFailureException { XMLParser parser = new XMLParser(xml); Node rootNode = parser.getRootNode(); parseRootNode(rootNode); } public int getDaystartSeconds() { if (mDaystartSeconds == null) return 0; return mDaystartSeconds; } public String getNewVersion() { return mNewVersion; } public String getURL() { return mUrl; } public String getAppStatus() { return mAppStatus; } public String getUpdateStatus() { return mUpdateStatus; } private void resetParsedData() { mDaystartSeconds = null; mNewVersion = null; mUrl = null; mUpdateStatus = null; mAppStatus = null; mParsedInstallEvent = false; mParsedPing = false; mParsedUpdatecheck = false; } private boolean logError(Node node, int errorCode) throws RequestFailureException { String errorMessage = "Failed to parse: " + node.tag; if (mStrictParsingMode) throw new RequestFailureException(errorMessage, errorCode); Log.e(TAG, errorMessage); return false; } private void parseRootNode(Node rootNode) throws RequestFailureException { for (int i = 0; i < rootNode.children.size(); ++i) { if (TextUtils.equals(TAG_RESPONSE, rootNode.children.get(i).tag)) { if (parseResponseNode(rootNode.children.get(i))) return; break; } } // The tag was bad; reset all of our state and bail. resetParsedData(); logError(rootNode, RequestFailureException.ERROR_PARSE_ROOT); } private boolean parseResponseNode(Node node) throws RequestFailureException { boolean success = true; String serverType = node.attributes.get("server"); success &= TextUtils.equals("3.0", node.attributes.get("protocol")); if (!TextUtils.equals("prod", serverType)) Log.w(TAG, "Server type: " + serverType); for (int i = 0; i < node.children.size(); ++i) { Node current = node.children.get(i); if (TextUtils.equals(TAG_DAYSTART, current.tag)) { success &= parseDaystartNode(current); } else if (TextUtils.equals(TAG_APP, current.tag)) { success &= parseAppNode(current); } else { Log.w(TAG, "Ignoring unknown child of <" + node.tag + "> : " + current.tag); } } if (!success) { return logError(node, RequestFailureException.ERROR_PARSE_RESPONSE); } else if (mDaystartSeconds == null) { return logError(node, RequestFailureException.ERROR_PARSE_DAYSTART); } else if (mAppStatus == null) { return logError(node, RequestFailureException.ERROR_PARSE_APP); } else if (mExpectInstallEvent != mParsedInstallEvent) { return logError(node, RequestFailureException.ERROR_PARSE_EVENT); } else if (mExpectPing != mParsedPing) { return logError(node, RequestFailureException.ERROR_PARSE_PING); } else if (mExpectUpdatecheck != mParsedUpdatecheck) { return logError(node, RequestFailureException.ERROR_PARSE_UPDATECHECK); } return true; } private boolean parseDaystartNode(Node node) throws RequestFailureException { try { mDaystartSeconds = Integer.parseInt(node.attributes.get("elapsed_seconds")); } catch (NumberFormatException e) { return logError(node, RequestFailureException.ERROR_PARSE_DAYSTART); } return true; } private boolean parseAppNode(Node node) throws RequestFailureException { boolean success = true; success &= TextUtils.equals(mAppId, node.attributes.get("appid")); mAppStatus = node.attributes.get("status"); if (TextUtils.equals("ok", mAppStatus)) { for (int i = 0; i < node.children.size(); ++i) { Node current = node.children.get(i); if (TextUtils.equals(TAG_UPDATECHECK, current.tag)) { success &= parseUpdatecheck(current); } else if (TextUtils.equals(TAG_EVENT, current.tag)) { parseEvent(current); } else if (TextUtils.equals(TAG_PING, current.tag)) { parsePing(current); } } } else if (TextUtils.equals("restricted", mAppStatus)) { // Omaha isn't allowed to get data in this country. Pretend the request was fine. } else { success = false; } if (success) return true; return logError(node, RequestFailureException.ERROR_PARSE_APP); } private boolean parseUpdatecheck(Node node) throws RequestFailureException { boolean success = true; mUpdateStatus = node.attributes.get("status"); if (TextUtils.equals("ok", mUpdateStatus)) { for (int i = 0; i < node.children.size(); ++i) { Node current = node.children.get(i); if (TextUtils.equals(TAG_URLS, current.tag)) { parseUrls(current); } else if (TextUtils.equals(TAG_MANIFEST, current.tag)) { parseManifest(current); } } // Confirm all the tags we expected to see were parsed properly. if (mUrl == null) { return logError(node, RequestFailureException.ERROR_PARSE_URLS); } else if (mNewVersion == null) { return logError(node, RequestFailureException.ERROR_PARSE_MANIFEST); } } else if (TextUtils.equals("noupdate", mUpdateStatus)) { // No update is available. Don't bother searching for other attributes. } else if (mUpdateStatus != null && mUpdateStatus.startsWith("error")) { Log.w(TAG, "Ignoring error status for " + node.tag + ": " + mUpdateStatus); } else { Log.w(TAG, "Ignoring unknown status for " + node.tag + ": " + mUpdateStatus); } mParsedUpdatecheck = true; return true; } private void parsePing(Node node) { if (TextUtils.equals("ok", node.attributes.get("status"))) mParsedPing = true; } private void parseEvent(Node node) { if (TextUtils.equals("ok", node.attributes.get("status"))) mParsedInstallEvent = true; } private void parseUrls(Node node) { for (int i = 0; i < node.children.size(); ++i) { Node current = node.children.get(i); if (TextUtils.equals(TAG_URL, current.tag)) parseUrl(current); } } private void parseUrl(Node node) { String url = node.attributes.get("codebase"); if (url == null) return; // The URL gets a "/" tacked onto it by the server. Remove it. if (url.endsWith("/")) url = url.substring(0, url.length() - 1); mUrl = url; } private void parseManifest(Node node) { mNewVersion = node.attributes.get("version"); } }