1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.service.adselection; 18 19 import android.annotation.NonNull; 20 import android.net.Uri; 21 22 import com.android.adservices.LoggerFactory; 23 import com.android.adservices.service.Flags; 24 import com.android.internal.annotations.VisibleForTesting; 25 26 import java.util.HashSet; 27 import java.util.Objects; 28 import java.util.Set; 29 import java.util.regex.Matcher; 30 import java.util.regex.Pattern; 31 import java.util.stream.Collectors; 32 33 /** 34 * Generates JS scripts given prebuilt URIs. 35 * 36 * <p>Prebuilt URIs are in '{@code ad-selection-prebuilt://<use-case>/<name>?<query-param>}' 37 * 38 * <p> 39 */ 40 public class PrebuiltLogicGenerator { 41 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 42 // TODO (b/271055928): Investigate abstracting use cases to their own classes with different 43 // flavors 44 @VisibleForTesting 45 static final String UNKNOWN_PREBUILT_IDENTIFIER = "Unknown prebuilt identifier: '%s'!"; 46 47 @VisibleForTesting 48 static final String MISSING_PREBUILT_PARAMS = "Missing prebuilt URI query params: '%s'!"; 49 50 @VisibleForTesting 51 static final String PREBUILT_FEATURE_IS_DISABLED = "Prebuilt Uri feature is disabled!"; 52 53 @VisibleForTesting 54 static final String UNRECOGNIZED_PREBUILT_PARAMS = 55 "Unrecognized prebuilt URI query params: '%s'!"; 56 57 @VisibleForTesting 58 public static final String AD_SELECTION_PREBUILT_SCHEMA = "ad-selection-prebuilt"; 59 60 @VisibleForTesting public static final String AD_SELECTION_USE_CASE = "ad-selection"; 61 62 @VisibleForTesting 63 public static final String AD_SELECTION_HIGHEST_BID_WINS = "highest-bid-wins"; 64 65 @VisibleForTesting 66 static final String AD_SELECTION_HIGHEST_BID_WINS_JS = 67 "//From prebuilts AD_SELECTION_HIGHEST_BID_WINS_JS\n" 68 + "function scoreAd(ad, bid, auction_config, seller_signals," 69 + " trusted_scoring_signals,\n" 70 + " contextual_signal, user_signal, custom_audience_signal) {\n" 71 + " return {'status': 0, 'score': bid };\n" 72 + "}\n" 73 + "function reportResult(ad_selection_config, render_uri, bid," 74 + " contextual_signals) {\n" 75 + " // Add the address of your reporting server here\n" 76 + " let reporting_address = '${reportingUrl}';\n" 77 + " return {'status': 0, 'results': {'signals_for_buyer':" 78 + " '{\"signals_for_buyer\" : 1}'\n" 79 + " , 'reporting_uri': reporting_address + '?render_uri='\n" 80 + " + render_uri + '?bid=' + bid }};\n" 81 + "}"; 82 83 public static final String AD_SELECTION_FROM_OUTCOMES_USE_CASE = "ad-selection-from-outcomes"; 84 85 public static final String AD_OUTCOME_SELECTION_WATERFALL_MEDIATION_TRUNCATION = 86 "waterfall-mediation-truncation"; 87 88 @VisibleForTesting 89 static final String AD_OUTCOME_SELECTION_WATERFALL_MEDIATION_TRUNCATION_JS = 90 "function selectOutcome(outcomes, selection_signals) {\n" 91 + " const outcome_1p = outcomes[0];\n" 92 + " const bid_floor = selection_signals.${bidFloor};\n" 93 + " return {'status': 0, 'result': (outcome_1p.bid >= bid_floor) ?" 94 + " outcome_1p : null};\n" 95 + "}"; 96 97 @VisibleForTesting static final String NAMED_PARAM_TEMPLATE = "\\$\\{%s\\}"; 98 private static final Pattern PARAM_IDENTIFIER_REGEX_PATTERN = 99 Pattern.compile(String.format(NAMED_PARAM_TEMPLATE, "(.*?)")); 100 @NonNull private final Flags mFlags; 101 PrebuiltLogicGenerator(@onNull Flags flags)102 public PrebuiltLogicGenerator(@NonNull Flags flags) { 103 Objects.requireNonNull(flags); 104 105 mFlags = flags; 106 } 107 108 /** 109 * Returns true if the given URI is in FLEDGE Ad Selection Prebuilt format 110 * 111 * @param decisionUri URI to check 112 * @return true if prebuilt URI, otherwise false 113 */ isPrebuiltUri(Uri decisionUri)114 public boolean isPrebuiltUri(Uri decisionUri) { 115 String scheme = decisionUri.getScheme(); 116 boolean isPrebuilt = Objects.nonNull(scheme) && scheme.equals(AD_SELECTION_PREBUILT_SCHEMA); 117 sLogger.v("Checking if URI %s is of prebuilt schema: %s", decisionUri, isPrebuilt); 118 sLogger.v("Prebuilt enabled flag: %s", mFlags.getFledgeAdSelectionPrebuiltUriEnabled()); 119 if (isPrebuilt && !mFlags.getFledgeAdSelectionPrebuiltUriEnabled()) { 120 sLogger.e(PREBUILT_FEATURE_IS_DISABLED); 121 throw new IllegalArgumentException(PREBUILT_FEATURE_IS_DISABLED); 122 } 123 return isPrebuilt; 124 } 125 126 /** 127 * Returns the generated JS script from a valid prebuilt URI. 128 * 129 * @param prebuiltUri valid prebuilt URI. {@link IllegalArgumentException} is thrown if the 130 * given URI is not valid or supported. 131 * @return JS script 132 */ jsScriptFromPrebuiltUri(Uri prebuiltUri)133 public String jsScriptFromPrebuiltUri(Uri prebuiltUri) { 134 135 if (!isPrebuiltUri(prebuiltUri)) { 136 String err = String.format(UNKNOWN_PREBUILT_IDENTIFIER, prebuiltUri); 137 sLogger.e(err); 138 throw new IllegalArgumentException(err); 139 } 140 sLogger.v("Prebuilt enabled: %s", mFlags.getFledgeAdSelectionPrebuiltUriEnabled()); 141 if (!mFlags.getFledgeAdSelectionPrebuiltUriEnabled()) { 142 sLogger.e(PREBUILT_FEATURE_IS_DISABLED); 143 throw new IllegalArgumentException(PREBUILT_FEATURE_IS_DISABLED); 144 } 145 sLogger.v("Generating JS for URI: %s", prebuiltUri); 146 String jsTemplate = 147 getPrebuiltJsScriptTemplate(prebuiltUri.getHost(), prebuiltUri.getPath()); 148 sLogger.v("Template found for URI %s:%n%s", prebuiltUri, jsTemplate); 149 150 Set<String> requiredParams = calculateRequiredParameters(jsTemplate); 151 Set<String> queryParams = prebuiltUri.getQueryParameterNames(); 152 sLogger.v("Required parameters are calculated: %s", requiredParams); 153 sLogger.v("Query parameters are calculated: %s", queryParams); 154 155 crossValidateRequiredAndQueryParams(requiredParams, queryParams); 156 157 for (String param : requiredParams) { 158 if (!prebuiltUri.getQueryParameterNames().contains(param)) { 159 String err = String.format(MISSING_PREBUILT_PARAMS, param); 160 sLogger.e(err); 161 throw new IllegalArgumentException(err); 162 } 163 164 jsTemplate = 165 jsTemplate.replaceAll( 166 String.format(NAMED_PARAM_TEMPLATE, param), 167 prebuiltUri.getQueryParameter(param)); 168 } 169 sLogger.i("Final prebuilt JS is generated:%n%s", jsTemplate); 170 171 return jsTemplate; 172 } 173 calculateRequiredParameters(String jsTemplate)174 private Set<String> calculateRequiredParameters(String jsTemplate) { 175 Set<String> requiredParameters = new HashSet<>(); 176 Matcher matcher = PARAM_IDENTIFIER_REGEX_PATTERN.matcher(jsTemplate); 177 while (matcher.find()) { 178 requiredParameters.add(matcher.group(1)); 179 } 180 return requiredParameters; 181 } 182 getPrebuiltJsScriptTemplate(String prebuiltUseCase, String prebuiltName)183 private String getPrebuiltJsScriptTemplate(String prebuiltUseCase, String prebuiltName) { 184 sLogger.v("Use case is '%s', prebuilt name is '%s'.", prebuiltUseCase, prebuiltName); 185 switch (prebuiltUseCase) { 186 case AD_SELECTION_USE_CASE: 187 sLogger.v("Use case matched with %s", AD_SELECTION_USE_CASE); 188 return getPrebuiltJsScriptTemplateForAdSelection(prebuiltName); 189 case AD_SELECTION_FROM_OUTCOMES_USE_CASE: 190 sLogger.v("Use case matched with %s", AD_SELECTION_FROM_OUTCOMES_USE_CASE); 191 return getPrebuiltJsScriptTemplateForAdSelectionFromOutcome(prebuiltName); 192 default: 193 String err = String.format(UNKNOWN_PREBUILT_IDENTIFIER, prebuiltUseCase); 194 sLogger.e(err); 195 throw new IllegalArgumentException(err); 196 } 197 } 198 getPrebuiltJsScriptTemplateForAdSelection(String prebuiltName)199 private String getPrebuiltJsScriptTemplateForAdSelection(String prebuiltName) { 200 switch (prebuiltName) { 201 case "/" + AD_SELECTION_HIGHEST_BID_WINS + "/": 202 sLogger.v("Prebuilt use case matched with %s", AD_SELECTION_HIGHEST_BID_WINS); 203 return AD_SELECTION_HIGHEST_BID_WINS_JS; 204 default: 205 String err = String.format(UNKNOWN_PREBUILT_IDENTIFIER, prebuiltName); 206 sLogger.e(err); 207 throw new IllegalArgumentException(err); 208 } 209 } 210 getPrebuiltJsScriptTemplateForAdSelectionFromOutcome(String prebuiltName)211 private String getPrebuiltJsScriptTemplateForAdSelectionFromOutcome(String prebuiltName) { 212 switch (prebuiltName) { 213 case "/" + AD_OUTCOME_SELECTION_WATERFALL_MEDIATION_TRUNCATION + "/": 214 sLogger.v( 215 "Prebuilt use case matched with %s", 216 AD_OUTCOME_SELECTION_WATERFALL_MEDIATION_TRUNCATION); 217 return AD_OUTCOME_SELECTION_WATERFALL_MEDIATION_TRUNCATION_JS; 218 default: 219 String err = String.format(UNKNOWN_PREBUILT_IDENTIFIER, prebuiltName); 220 sLogger.e(err); 221 throw new IllegalArgumentException(err); 222 } 223 } 224 crossValidateRequiredAndQueryParams( Set<String> requiredParams, Set<String> queryParams)225 private void crossValidateRequiredAndQueryParams( 226 Set<String> requiredParams, Set<String> queryParams) { 227 Set<String> erroneousParams; 228 if (!(erroneousParams = 229 requiredParams.stream() 230 .filter(p -> !queryParams.contains(p)) 231 .collect(Collectors.toSet())) 232 .isEmpty()) { 233 String err = String.format(MISSING_PREBUILT_PARAMS, erroneousParams); 234 sLogger.e(err); 235 throw new IllegalArgumentException(err); 236 } 237 if (!(erroneousParams = 238 queryParams.stream() 239 .filter(p -> !requiredParams.contains(p)) 240 .collect(Collectors.toSet())) 241 .isEmpty()) { 242 String err = String.format(UNRECOGNIZED_PREBUILT_PARAMS, erroneousParams); 243 sLogger.e(err); 244 throw new IllegalArgumentException(err); 245 } 246 } 247 } 248