blob: a0f0e4cae0bd113e970f6a6bc71a7d9e3fc9c6bd [file] [log] [blame]
// Copyright 2020 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.
#include "chrome/updater/tag.h"
#include <map>
#include <utility>
#include "base/no_destructor.h"
#include "base/optional.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chrome/updater/lib_util.h"
#include "chrome/updater/util.h"
namespace updater {
namespace tagging {
namespace {
// The name of the bundle being installed. If not specified, the first app's
// appname is used.
constexpr base::StringPiece kTagArgBundleName = "bundlename";
// The language of the product the user is installing.
constexpr base::StringPiece kTagArgLanguage = "lang";
// Flag denoting that the user is flighting a new test feature.
constexpr base::StringPiece kTagArgFlighting = "flighting";
// Flag denoting that the user has agreed to provide usage stats, crashreports
// etc.
constexpr base::StringPiece kTagArgUsageStats = "usagestats";
// A unique value for this installation session. It can be used to follow the
// progress from the website to installation completion.
constexpr base::StringPiece kTagArgInstallationId = "iid";
// The Brand Code used for branding.
// If a brand value already exists on the system, it should be ignored.
// This value is used to set the initial brand for the updater and the client
// app.
constexpr base::StringPiece kTagArgBrandCode = "brand";
// The Client ID used for branding.
// If a client value already exists on the system, it should be ignored.
// This value is used to set the initial client for the updater and the client
// app.
constexpr base::StringPiece kTagArgClientId = "client";
// A set of experiment labels used to track installs that are included in
// experiments. Use "experiments" for per-app arguments; use "omahaexperiments"
// for updater-specific labels.
constexpr base::StringPiece kAppArgExperimentLabels = "experiments";
constexpr base::StringPiece kTagArgOmahaExperimentLabels = "omahaexperiments";
// A referral ID used for tracking referrals.
constexpr base::StringPiece kTagArgReferralId = "referral";
// Tells the updater what ap value to set in the registry.
constexpr base::StringPiece kAppArgAdditionalParameters = "ap";
// Indicates which browser to restart on successful install.
constexpr base::StringPiece kTagArgBrowserType = "browser";
// The list of arguments that are needed for a meta-installer, to
// indicate which application is being installed. These are stamped
// inside the meta-installer binary.
constexpr base::StringPiece kTagArgAppId = "appguid";
constexpr base::StringPiece kAppArgAppName = "appname";
constexpr base::StringPiece kAppArgNeedsAdmin = "needsadmin";
constexpr base::StringPiece kAppArgInstallDataIndex = "installdataindex";
constexpr base::StringPiece kAppArgUntrustedData = "untrusteddata";
// This switch allows extra data to be communicated to the application
// installer. The extra data needs to be URL-encoded. The data will be decoded
// and written to the file, that is then passed in the command line to the
// application installer in the form "/installerdata=blah.dat". One per
// application.
constexpr base::StringPiece kAppArgInstallerData = "installerdata";
// Character that is disallowed from appearing in the tag.
constexpr char kDisallowedCharInTag = '/';
base::Optional<AppArgs::NeedsAdmin> ParseNeedsAdminEnum(base::StringPiece str) {
if (base::EqualsCaseInsensitiveASCII("false", str))
return AppArgs::NeedsAdmin::kNo;
if (base::EqualsCaseInsensitiveASCII("true", str))
return AppArgs::NeedsAdmin::kYes;
if (base::EqualsCaseInsensitiveASCII("prefers", str))
return AppArgs::NeedsAdmin::kPrefers;
return base::nullopt;
}
// Returns base::nullopt if parsing failed.
base::Optional<bool> ParseBool(base::StringPiece str) {
if (base::EqualsCaseInsensitiveASCII("false", str))
return false;
if (base::EqualsCaseInsensitiveASCII("true", str))
return true;
return base::nullopt;
}
// A custom comparator functor class for the parse tables.
using ParseTableCompare = CaseInsensitiveASCIICompare<std::string>;
namespace global_attributes {
ErrorCode ParseBundleName(base::StringPiece value, TagArgs* args) {
value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL);
if (value.empty())
return ErrorCode::kGlobal_BundleNameCannotBeWhitespace;
args->bundle_name = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseInstallationId(base::StringPiece value, TagArgs* args) {
args->installation_id = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseBrandCode(base::StringPiece value, TagArgs* args) {
args->brand_code = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseClientId(base::StringPiece value, TagArgs* args) {
args->client_id = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseOmahaExperimentLabels(base::StringPiece value, TagArgs* args) {
value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL);
if (value.empty())
return ErrorCode::kGlobal_ExperimentLabelsCannotBeWhitespace;
args->experiment_labels = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseReferralId(base::StringPiece value, TagArgs* args) {
args->referral_id = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseBrowserType(base::StringPiece value, TagArgs* args) {
int browser_type = 0;
if (!base::StringToInt(value, &browser_type))
return ErrorCode::kGlobal_BrowserTypeIsInvalid;
if (browser_type < 0)
return ErrorCode::kGlobal_BrowserTypeIsInvalid;
args->browser_type =
(browser_type < static_cast<int>(TagArgs::BrowserType::kMax))
? TagArgs::BrowserType(browser_type)
: TagArgs::BrowserType::kUnknown;
return ErrorCode::kSuccess;
}
ErrorCode ParseLanguage(base::StringPiece value, TagArgs* args) {
// Even if we don't support the language, we want to pass it to the
// installer. Omaha will pick its language later. See http://b/1336966.
args->language = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseFlighting(base::StringPiece value, TagArgs* args) {
const base::Optional<bool> flighting = ParseBool(value);
if (!flighting.has_value())
return ErrorCode::kGlobal_FlightingValueIsNotABoolean;
args->flighting = flighting.value();
return ErrorCode::kSuccess;
}
ErrorCode ParseUsageStats(base::StringPiece value, TagArgs* args) {
int tristate = 0;
if (!base::StringToInt(value, &tristate))
return ErrorCode::kGlobal_UsageStatsValueIsInvalid;
if (tristate == 0) {
args->usage_stats_enable = false;
} else if (tristate == 1) {
args->usage_stats_enable = true;
} else if (tristate == 2) {
args->usage_stats_enable = base::nullopt;
} else {
return ErrorCode::kGlobal_UsageStatsValueIsInvalid;
}
return ErrorCode::kSuccess;
}
// Parses an app ID and adds it to the list of apps in |args|, if valid.
ErrorCode ParseAppId(base::StringPiece value, TagArgs* args) {
if (!base::IsStringASCII(value))
return ErrorCode::kApp_AppIdIsNotValid;
args->apps.push_back(AppArgs(value));
return ErrorCode::kSuccess;
}
// |value| must not be empty.
// |args| must not be null.
using ParseGlobalAttributeFunPtr = ErrorCode (*)(base::StringPiece value,
TagArgs* args);
using GlobalParseTable =
std::map<base::StringPiece, ParseGlobalAttributeFunPtr, ParseTableCompare>;
const GlobalParseTable& GetTable() {
static const base::NoDestructor<GlobalParseTable> instance{{
{kTagArgBundleName, &ParseBundleName},
{kTagArgInstallationId, &ParseInstallationId},
{kTagArgBrandCode, &ParseBrandCode},
{kTagArgClientId, &ParseClientId},
{kTagArgOmahaExperimentLabels, &ParseOmahaExperimentLabels},
{kTagArgReferralId, &ParseReferralId},
{kTagArgBrowserType, &ParseBrowserType},
{kTagArgLanguage, &ParseLanguage},
{kTagArgFlighting, &ParseFlighting},
{kTagArgUsageStats, &ParseUsageStats},
{kTagArgAppId, &ParseAppId},
}};
return *instance;
}
} // namespace global_attributes
namespace app_attributes {
ErrorCode ParseAdditionalParameters(base::StringPiece value, AppArgs* args) {
args->ap = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseExperimentLabels(base::StringPiece value, AppArgs* args) {
value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL);
if (value.empty())
return ErrorCode::kApp_ExperimentLabelsCannotBeWhitespace;
args->experiment_labels = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseAppName(base::StringPiece value, AppArgs* args) {
value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL);
if (value.empty())
return ErrorCode::kApp_AppNameCannotBeWhitespace;
args->app_name = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseNeedsAdmin(base::StringPiece value, AppArgs* args) {
const auto needs_admin = ParseNeedsAdminEnum(value);
if (!needs_admin.has_value())
return ErrorCode::kApp_NeedsAdminValueIsInvalid;
args->needs_admin = needs_admin.value();
return ErrorCode::kSuccess;
}
ErrorCode ParseInstallDataIndex(base::StringPiece value, AppArgs* args) {
args->install_data_index = value.as_string();
return ErrorCode::kSuccess;
}
ErrorCode ParseUntrustedData(base::StringPiece value, AppArgs* args) {
args->untrusted_data = value.as_string();
return ErrorCode::kSuccess;
}
// |value| must not be empty.
// |args| must not be null.
using ParseAppAttributeFunPtr = ErrorCode (*)(base::StringPiece value,
AppArgs* args);
using AppParseTable =
std::map<base::StringPiece, ParseAppAttributeFunPtr, ParseTableCompare>;
const AppParseTable& GetTable() {
static const base::NoDestructor<AppParseTable> instance{{
{kAppArgAdditionalParameters, &ParseAdditionalParameters},
{kAppArgExperimentLabels, &ParseExperimentLabels},
{kAppArgAppName, &ParseAppName},
{kAppArgNeedsAdmin, &ParseNeedsAdmin},
{kAppArgInstallDataIndex, &ParseInstallDataIndex},
{kAppArgUntrustedData, &ParseUntrustedData},
}};
return *instance;
}
} // namespace app_attributes
namespace installer_data_attributes {
// Search for the given appid specified by |value| in |args->apps| and write its
// index to |current_app_index|.
ErrorCode FindAppIdInTagArgs(base::StringPiece value,
TagArgs* args,
base::Optional<size_t>* current_app_index) {
if (!base::IsStringASCII(value))
return ErrorCode::kApp_AppIdIsNotValid;
// Find the app in the existing list.
for (size_t i = 0; i < args->apps.size(); i++) {
if (base::EqualsCaseInsensitiveASCII(args->apps[i].app_id, value)) {
*current_app_index = i;
}
}
if (!current_app_index->has_value())
return ErrorCode::kAppInstallerData_AppIdNotFound;
return ErrorCode::kSuccess;
}
ErrorCode ParseInstallerData(base::StringPiece value,
TagArgs* args,
base::Optional<size_t>* current_app_index) {
if (!current_app_index->has_value())
return ErrorCode::
kAppInstallerData_InstallerDataCannotBeSpecifiedBeforeAppId;
args->apps[current_app_index->value()].encoded_installer_data =
value.as_string();
return ErrorCode::kSuccess;
}
// |value| must not be empty.
// |args| must not be null.
// |current_app_index| is an in/out parameter. It stores the index of the
// current app and nullopt if no app has been set yet. Writing to it will set
// the index for future calls to these functions.
using ParseInstallerDataAttributeFunPtr =
ErrorCode (*)(base::StringPiece value,
TagArgs* args,
base::Optional<size_t>* current_app_index);
using InstallerDataParseTable = std::map<base::StringPiece,
ParseInstallerDataAttributeFunPtr,
ParseTableCompare>;
const InstallerDataParseTable& GetTable() {
static const base::NoDestructor<InstallerDataParseTable> instance{{
{kTagArgAppId, &FindAppIdInTagArgs},
{kAppArgInstallerData, &ParseInstallerData},
}};
return *instance;
}
} // namespace installer_data_attributes
namespace query_string {
// An attribute in a metainstaller tag or app installer data args string.
// - The first value is the "name" of the attribute.
// - The second value is the "value" of the attribute.
using Attribute = std::pair<base::StringPiece, std::string>;
// Splits |query_string| into |Attribute|s. Attribute values will be unescaped
// if |unescape_value| is true.
//
// Ownership follows the same rules as |base::SplitStringPiece|.
std::vector<Attribute> Split(base::StringPiece query_string,
bool unescape_value = true) {
std::vector<Attribute> attributes;
for (const auto& attribute_string :
base::SplitStringPiece(query_string, "&", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
size_t separate_pos = attribute_string.find_first_of("=");
if (separate_pos == base::StringPiece::npos) {
// Add a name-only attribute.
base::StringPiece name = base::TrimWhitespaceASCII(
attribute_string, base::TrimPositions::TRIM_ALL);
attributes.emplace_back(name, "");
} else {
base::StringPiece name =
base::TrimWhitespaceASCII(attribute_string.substr(0, separate_pos),
base::TrimPositions::TRIM_ALL);
base::StringPiece value =
base::TrimWhitespaceASCII(attribute_string.substr(separate_pos + 1),
base::TrimPositions::TRIM_ALL);
attributes.emplace_back(name, unescape_value
? updater::UnescapeURLComponent(value)
: value.as_string());
}
}
return attributes;
}
} // namespace query_string
// Parses global and app-specific attributes from |tag|.
ErrorCode ParseTag(base::StringPiece tag, TagArgs* args) {
const auto& global_func_lookup_table = global_attributes::GetTable();
const auto& app_func_lookup_table = app_attributes::GetTable();
for (const auto& attribute : query_string::Split(tag)) {
// Attribute names are only ASCII, so no i18n case folding needed.
const base::StringPiece name = attribute.first;
const base::StringPiece value = attribute.second;
if (global_func_lookup_table.find(name) != global_func_lookup_table.end()) {
if (value.empty())
return ErrorCode::kAttributeMustHaveValue;
const ErrorCode result = global_func_lookup_table.at(name)(value, args);
if (result != ErrorCode::kSuccess)
return result;
} else if (app_func_lookup_table.find(name) !=
app_func_lookup_table.end()) {
if (args->apps.empty())
return ErrorCode::kApp_AppIdNotSpecified;
if (value.empty())
return ErrorCode::kAttributeMustHaveValue;
AppArgs* current_app = &args->apps.back();
const ErrorCode result =
app_func_lookup_table.at(name)(value, current_app);
if (result != ErrorCode::kSuccess)
return result;
} else {
return ErrorCode::kUnrecognizedName;
}
}
// The bundle name inherits the first app's name, if not set.
if (args->bundle_name.empty() && !args->apps.empty())
args->bundle_name = args->apps[0].app_name;
return ErrorCode::kSuccess;
}
// Parses app-specific installer data from |app_installer_data_args|.
ErrorCode ParseAppInstallerDataArgs(base::StringPiece app_installer_data_args,
TagArgs* args) {
// The currently tracked app index to apply installer data to.
base::Optional<size_t> current_app_index;
// Installer data is assumed to be URL-encoded, so we don't unescape it.
bool unescape_value = false;
for (const auto& attribute :
query_string::Split(app_installer_data_args, unescape_value)) {
const base::StringPiece name = attribute.first;
const base::StringPiece value = attribute.second;
if (value.empty())
return ErrorCode::kAttributeMustHaveValue;
const auto& func_lookup_table = installer_data_attributes::GetTable();
if (func_lookup_table.find(name) == func_lookup_table.end())
return ErrorCode::kUnrecognizedName;
const ErrorCode result =
func_lookup_table.at(name)(value, args, &current_app_index);
if (result != ErrorCode::kSuccess)
return result;
}
return ErrorCode::kSuccess;
}
// Checks that |args| does not contain |kDisallowedCharInTag|.
bool IsValidArgs(base::StringPiece args) {
return !base::Contains(args, kDisallowedCharInTag);
}
} // namespace
AppArgs::AppArgs(base::StringPiece app_id)
: app_id(base::ToLowerASCII(app_id)) {
CHECK(!app_id.empty());
}
AppArgs::~AppArgs() = default;
AppArgs::AppArgs(AppArgs& other) = default;
AppArgs& AppArgs::operator=(AppArgs& other) = default;
AppArgs::AppArgs(AppArgs&& other) = default;
AppArgs& AppArgs::operator=(AppArgs&& other) = default;
TagArgs::TagArgs() = default;
TagArgs::~TagArgs() = default;
TagArgs::TagArgs(TagArgs& other) = default;
TagArgs& TagArgs::operator=(TagArgs& other) = default;
TagArgs::TagArgs(TagArgs&& other) = default;
TagArgs& TagArgs::operator=(TagArgs&& other) = default;
ErrorCode Parse(base::StringPiece tag,
base::Optional<base::StringPiece> app_installer_data_args,
TagArgs* args) {
if (!IsValidArgs(tag))
return ErrorCode::kTagIsInvalid;
const ErrorCode result = ParseTag(tag, args);
if (result != ErrorCode::kSuccess)
return result;
if (!app_installer_data_args.has_value())
return ErrorCode::kSuccess;
if (!IsValidArgs(app_installer_data_args.value()))
return ErrorCode::kTagIsInvalid;
return ParseAppInstallerDataArgs(app_installer_data_args.value(), args);
}
} // namespace tagging
} // namespace updater