| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "chrome/browser/installable/installable_checker.h" |
| 6 |
| 7 #include "base/bind.h" |
| 8 #include "base/strings/string_util.h" |
| 9 #include "chrome/browser/manifest/manifest_icon_downloader.h" |
| 10 #include "chrome/browser/manifest/manifest_icon_selector.h" |
| 11 #include "chrome/browser/profiles/profile.h" |
| 12 #include "content/public/browser/browser_context.h" |
| 13 #include "content/public/browser/browser_thread.h" |
| 14 #include "content/public/browser/navigation_handle.h" |
| 15 #include "content/public/browser/service_worker_context.h" |
| 16 #include "content/public/browser/storage_partition.h" |
| 17 #include "third_party/WebKit/public/platform/WebDisplayMode.h" |
| 18 |
| 19 namespace { |
| 20 |
| 21 const int kIconSizeUnset = -1; |
| 22 |
| 23 const char kPngExtension[] = ".png"; |
| 24 |
| 25 // This constant is the icon size on Android (48dp) multiplied by the scale |
| 26 // factor of a Nexus 5 device (3). For mobile and desktop platforms, a 144px |
| 27 // icon is an approximate, appropriate lower bound. |
| 28 // TODO(dominickn): consolidate this constant with minimum_icon_size_in_dp |
| 29 // across platforms. |
| 30 const int kIconMinimumSizeInPx = 144; |
| 31 |
| 32 // Returns true if |manifest| specifies a PNG icon that is at least 144x144px |
| 33 // (or has size "any"). |
| 34 bool DoesManifestContainRequiredIcon(const content::Manifest& manifest) { |
| 35 for (const auto& icon : manifest.icons) { |
| 36 // The type field is optional. If it isn't present, fall back on checking |
| 37 // the src extension, and allow the icon if the extension ends with png. |
| 38 if (!base::EqualsASCII(icon.type.string(), "image/png") && |
| 39 !(icon.type.is_null() && |
| 40 base::EndsWith(icon.src.ExtractFileName(), kPngExtension, |
| 41 base::CompareCase::INSENSITIVE_ASCII))) |
| 42 continue; |
| 43 |
| 44 for (const auto& size : icon.sizes) { |
| 45 if (size.IsEmpty()) // "any" |
| 46 return true; |
| 47 if (size.width() >= kIconMinimumSizeInPx && |
| 48 size.height() >= kIconMinimumSizeInPx) { |
| 49 return true; |
| 50 } |
| 51 } |
| 52 } |
| 53 |
| 54 return false; |
| 55 } |
| 56 |
| 57 } // anonymous namespace |
| 58 |
| 59 DEFINE_WEB_CONTENTS_USER_DATA_KEY(installable::InstallableChecker); |
| 60 |
| 61 namespace installable { |
| 62 |
| 63 InstallableParams::InstallableParams() |
| 64 : ideal_icon_size_in_dp(kIconSizeUnset), |
| 65 minimum_icon_size_in_dp(kIconSizeUnset), |
| 66 check_valid_webapp_manifest(false), |
| 67 check_service_worker(false), |
| 68 check_valid_icon(false) { } |
| 69 |
| 70 InstallableChecker::InstallableChecker(content::WebContents* web_contents) |
| 71 : content::WebContentsObserver(web_contents), |
| 72 status_(DORMANT), |
| 73 processing_error_(NO_ERROR), |
| 74 manifest_error_(NO_ERROR), |
| 75 valid_webapp_manifest_error_(NO_ERROR), |
| 76 service_worker_error_(NO_ERROR), |
| 77 icon_error_(NO_ERROR), |
| 78 ideal_icon_size_in_dp_(kIconSizeUnset), |
| 79 minimum_icon_size_in_dp_(kIconSizeUnset), |
| 80 has_valid_webapp_manifest_(false), |
| 81 has_service_worker_(false), |
| 82 weak_factory_(this) {} |
| 83 |
| 84 InstallableChecker::~InstallableChecker() { } |
| 85 |
| 86 bool InstallableChecker::IsManifestValidForWebApp( |
| 87 const content::Manifest& manifest) { |
| 88 if (manifest.IsEmpty()) { |
| 89 valid_webapp_manifest_error_ = MANIFEST_EMPTY; |
| 90 return false; |
| 91 } |
| 92 |
| 93 if (!manifest.start_url.is_valid()) { |
| 94 valid_webapp_manifest_error_ = START_URL_NOT_VALID; |
| 95 return false; |
| 96 } |
| 97 |
| 98 if ((manifest.name.is_null() || manifest.name.string().empty()) && |
| 99 (manifest.short_name.is_null() || manifest.short_name.string().empty())) { |
| 100 valid_webapp_manifest_error_ = MANIFEST_MISSING_NAME_OR_SHORT_NAME; |
| 101 return false; |
| 102 } |
| 103 |
| 104 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be |
| 105 // accepted. If we accept it today, it would fallback to "browser" and make |
| 106 // this check moot. See https://crbug.com/604390. |
| 107 if (manifest.display != blink::WebDisplayModeStandalone && |
| 108 manifest.display != blink::WebDisplayModeFullscreen) { |
| 109 valid_webapp_manifest_error_ = MANIFEST_DISPLAY_NOT_SUPPORTED; |
| 110 return false; |
| 111 } |
| 112 |
| 113 if (!DoesManifestContainRequiredIcon(manifest)) { |
| 114 valid_webapp_manifest_error_ = MANIFEST_MISSING_SUITABLE_ICON; |
| 115 return false; |
| 116 } |
| 117 |
| 118 return true; |
| 119 } |
| 120 |
| 121 void InstallableChecker::Start(const InstallableParams& params, |
| 122 const InstallableCallback& callback) { |
| 123 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| 124 |
| 125 // Always reset processing_error_, as it records events like navigation which |
| 126 // are outside of fetching/validating resources. |
| 127 processing_error_ = NO_ERROR; |
| 128 |
| 129 // If we've already working on a task, or running callbacks, add the new task |
| 130 // to the pending list and return. It will be dealt with once the current work |
| 131 // completes. We keep the pending list separate in case this call was |
| 132 // initiated during an InstallableCallback invocation, which modifies the |
| 133 // tasks_ list. |
| 134 if (IsActive() || HasFlag(RUNNING_CALLBACKS)) { |
| 135 pending_tasks_.push_back({params, callback}); |
| 136 return; |
| 137 } |
| 138 |
| 139 tasks_.push_back({params, callback}); |
| 140 SetFlag(STARTED); |
| 141 StartTask(); |
| 142 } |
| 143 |
| 144 void InstallableChecker::Cancel() { |
| 145 // Clear the STARTED flag to signal that we should stop work immediately. |
| 146 // Callers of this method should immediately call back to FetchResource() or |
| 147 // Start(), which will terminate the current task and run callbacks. |
| 148 ClearFlag(STARTED); |
| 149 } |
| 150 |
| 151 bool InstallableChecker::DoesIconSizeMatch( |
| 152 const InstallableParams& params) const { |
| 153 return (ideal_icon_size_in_dp_ == params.ideal_icon_size_in_dp) && |
| 154 (minimum_icon_size_in_dp_ == params.minimum_icon_size_in_dp); |
| 155 } |
| 156 |
| 157 ErrorCode InstallableChecker::GetErrorCode(const InstallableParams& params) { |
| 158 if (processing_error_ != NO_ERROR) |
| 159 return processing_error_; |
| 160 if (manifest_error_ != NO_ERROR) |
| 161 return manifest_error_; |
| 162 if (params.check_valid_webapp_manifest && |
| 163 valid_webapp_manifest_error_ != NO_ERROR) { |
| 164 return valid_webapp_manifest_error_; |
| 165 } |
| 166 if (params.check_service_worker && service_worker_error_ != NO_ERROR) |
| 167 return service_worker_error_; |
| 168 if (params.check_valid_icon && icon_error_ != NO_ERROR) |
| 169 return icon_error_; |
| 170 |
| 171 return NO_ERROR; |
| 172 } |
| 173 |
| 174 content::WebContents* InstallableChecker::GetWebContents() { |
| 175 content::WebContents* contents = web_contents(); |
| 176 if (!contents || contents->IsBeingDestroyed()) |
| 177 return nullptr; |
| 178 return contents; |
| 179 } |
| 180 |
| 181 bool InstallableChecker::IsComplete(const InstallableParams& params) const { |
| 182 // Returns true if for all resources: |
| 183 // a. the params did not request it, OR |
| 184 // b. the resource has been retrieved. |
| 185 return (HasFlag(MANIFEST_FETCHED)) && |
| 186 (!params.check_valid_webapp_manifest || HasFlag(MANIFEST_VALIDATED)) && |
| 187 (!params.check_service_worker || HasFlag(SERVICE_WORKER_CHECKED)) && |
| 188 (!params.check_valid_icon || |
| 189 (HasFlag(ICON_FETCHED) && DoesIconSizeMatch(params))); |
| 190 } |
| 191 |
| 192 bool InstallableChecker::IsRunning(content::WebContents* web_contents) { |
| 193 if (!web_contents) { |
| 194 processing_error_ = RENDERER_EXITING; |
| 195 return false; |
| 196 } |
| 197 |
| 198 if (!IsActive()) { |
| 199 processing_error_ = USER_NAVIGATED; |
| 200 return false; |
| 201 } |
| 202 |
| 203 return true; |
| 204 } |
| 205 |
| 206 void InstallableChecker::Reset() { |
| 207 status_ = DORMANT; |
| 208 processing_error_ = NO_ERROR; |
| 209 manifest_error_ = NO_ERROR; |
| 210 valid_webapp_manifest_error_ = NO_ERROR; |
| 211 service_worker_error_ = NO_ERROR; |
| 212 icon_error_ = NO_ERROR; |
| 213 |
| 214 ideal_icon_size_in_dp_ = kIconSizeUnset; |
| 215 minimum_icon_size_in_dp_ = kIconSizeUnset; |
| 216 |
| 217 tasks_.clear(); |
| 218 pending_tasks_.clear(); |
| 219 |
| 220 manifest_url_ = GURL(); |
| 221 manifest_ = content::Manifest(); |
| 222 icon_url_ = GURL(); |
| 223 icon_.reset(nullptr); |
| 224 has_valid_webapp_manifest_ = false; |
| 225 has_service_worker_ = false; |
| 226 } |
| 227 |
| 228 void InstallableChecker::RunCallbacks() { |
| 229 // Post a callback and delete it from the list of tasks if: |
| 230 // - the STARTED status bit is missing. This means that we've either finished |
| 231 // all possible checks, or we have canceled the pipeline |
| 232 // - the params that the callback was started with have been satisfied. |
| 233 // We run through the entire tasks_ vector here since we may have completed |
| 234 // other tasks while working on the current one. For example, we may have |
| 235 // initiated a full check on page load for app banners. Whilst that runs, |
| 236 // multiple other requests may come in and be queued. When the app banner task |
| 237 // completes, the queued requests will be moved to the tasks_ vector, and as |
| 238 // we have already fetched all resources, we can dispatch their callbacks |
| 239 // immediately. |
| 240 SetFlag(RUNNING_CALLBACKS); |
| 241 for (auto it = tasks_.begin(); it != tasks_.end();) { |
| 242 const InstallableParams& params = it->first; |
| 243 if (!IsActive() || IsComplete(params)) { |
| 244 InstallableResult result = { |
| 245 GetErrorCode(params), manifest_url_, manifest_, |
| 246 params.check_valid_icon ? icon_url_ : GURL::EmptyGURL(), |
| 247 params.check_valid_icon ? icon_.get() : nullptr, |
| 248 params.check_valid_webapp_manifest ? has_valid_webapp_manifest_ |
| 249 : false, |
| 250 params.check_service_worker ? has_service_worker_ : false}; |
| 251 // We must run this directly to guarantee the callback gets consistent |
| 252 // results. If we PostTask, a second Task may begin that invalidates the |
| 253 // icon object before the callback gets a chance to use it. |
| 254 it->second.Run(result); |
| 255 it = tasks_.erase(it); |
| 256 } else { |
| 257 ++it; |
| 258 } |
| 259 } |
| 260 ClearFlag(RUNNING_CALLBACKS); |
| 261 } |
| 262 |
| 263 void InstallableChecker::StartTask() { |
| 264 RunCallbacks(); |
| 265 |
| 266 if (!pending_tasks_.empty()) { |
| 267 // Shift any pending tasks to the end of tasks_. |
| 268 tasks_.insert(tasks_.end(), pending_tasks_.begin(), pending_tasks_.end()); |
| 269 pending_tasks_.clear(); |
| 270 } |
| 271 |
| 272 // If there's nothing to do, exit. Resources remain cached so any future calls |
| 273 // won't re-fetch anything that has already been retrieved. |
| 274 if (tasks_.empty()) { |
| 275 ClearFlag(STARTED); |
| 276 return; |
| 277 } |
| 278 |
| 279 // If we are requesting an icon, and the requested size differs from any |
| 280 // previously fetched (or if we haven't yet fetched an icon), reset the icon |
| 281 // state. This has no impact on the other resources, so don't reset them. |
| 282 const InstallableParams& params = tasks_[0].first; |
| 283 if (params.check_valid_icon && !DoesIconSizeMatch(params)) { |
| 284 ideal_icon_size_in_dp_ = params.ideal_icon_size_in_dp; |
| 285 minimum_icon_size_in_dp_ = params.minimum_icon_size_in_dp; |
| 286 icon_url_ = GURL(); |
| 287 icon_.reset(nullptr); |
| 288 icon_error_ = NO_ERROR; |
| 289 ClearFlag(ICON_FETCHED); |
| 290 } |
| 291 |
| 292 FetchResource(); |
| 293 } |
| 294 |
| 295 void InstallableChecker::FetchResource() { |
| 296 DCHECK(!tasks_.empty()); |
| 297 const InstallableParams& params = tasks_[0].first; |
| 298 |
| 299 // Cancel if there is an error code for any resource requested by params. |
| 300 if (GetErrorCode(params) != NO_ERROR) |
| 301 Cancel(); |
| 302 |
| 303 // If not active, then Cancel() has been called. Go straight back to |
| 304 // StartTask, which will clear the current task. Otherwise, if we fall through |
| 305 // to the else case, we've fetched everything necessary for this task, so |
| 306 // call StartTask to run its callback and start the next task. |
| 307 if (!IsActive()) |
| 308 StartTask(); |
| 309 else if (!HasFlag(MANIFEST_FETCHED)) |
| 310 FetchManifest(); |
| 311 else if (params.check_valid_webapp_manifest && !HasFlag(MANIFEST_VALIDATED)) |
| 312 CheckValidWebappManifest(); |
| 313 else if (params.check_service_worker && !HasFlag(SERVICE_WORKER_CHECKED)) |
| 314 CheckServiceWorker(); |
| 315 else if (params.check_valid_icon && !HasFlag(ICON_FETCHED)) |
| 316 ExtractAndFetchBestIcon(); |
| 317 else |
| 318 StartTask(); |
| 319 } |
| 320 |
| 321 void InstallableChecker::FetchManifest() { |
| 322 DCHECK(!HasFlag(MANIFEST_FETCHED)); |
| 323 DCHECK(manifest_.IsEmpty()); |
| 324 DCHECK(manifest_url_.is_empty()); |
| 325 |
| 326 content::WebContents* web_contents = GetWebContents(); |
| 327 DCHECK(web_contents); |
| 328 |
| 329 web_contents->GetManifest(base::Bind(&InstallableChecker::OnDidGetManifest, |
| 330 weak_factory_.GetWeakPtr())); |
| 331 } |
| 332 |
| 333 void InstallableChecker::OnDidGetManifest(const GURL& manifest_url, |
| 334 const content::Manifest& manifest) { |
| 335 if (!IsRunning(GetWebContents())) |
| 336 Cancel(); |
| 337 else if (manifest_url.is_empty()) |
| 338 manifest_error_ = NO_MANIFEST; |
| 339 else if (manifest.IsEmpty()) |
| 340 manifest_error_ = MANIFEST_EMPTY; |
| 341 |
| 342 manifest_url_ = manifest_url; |
| 343 manifest_ = manifest; |
| 344 |
| 345 SetFlag(MANIFEST_FETCHED); |
| 346 FetchResource(); |
| 347 } |
| 348 |
| 349 void InstallableChecker::CheckValidWebappManifest() { |
| 350 DCHECK(!HasFlag(MANIFEST_VALIDATED)); |
| 351 DCHECK(!manifest_.IsEmpty()); |
| 352 |
| 353 has_valid_webapp_manifest_ = IsManifestValidForWebApp(manifest_); |
| 354 if (!has_valid_webapp_manifest_) |
| 355 Cancel(); |
| 356 |
| 357 SetFlag(MANIFEST_VALIDATED); |
| 358 FetchResource(); |
| 359 } |
| 360 |
| 361 void InstallableChecker::CheckServiceWorker() { |
| 362 DCHECK(!HasFlag(SERVICE_WORKER_CHECKED)); |
| 363 DCHECK(!manifest_.IsEmpty()); |
| 364 |
| 365 content::WebContents* web_contents = GetWebContents(); |
| 366 |
| 367 // Check to see if there is a single service worker controlling this page |
| 368 // and the manifest's start url. |
| 369 Profile* profile = |
| 370 Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| 371 content::StoragePartition* storage_partition = |
| 372 content::BrowserContext::GetStoragePartition( |
| 373 profile, web_contents->GetSiteInstance()); |
| 374 DCHECK(storage_partition); |
| 375 |
| 376 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( |
| 377 web_contents->GetLastCommittedURL(), manifest_.start_url, |
| 378 base::Bind(&InstallableChecker::OnDidCheckHasServiceWorker, |
| 379 weak_factory_.GetWeakPtr())); |
| 380 } |
| 381 |
| 382 void InstallableChecker::OnDidCheckHasServiceWorker(bool has_service_worker) { |
| 383 if (!IsRunning(GetWebContents())) |
| 384 Cancel(); |
| 385 |
| 386 has_service_worker_ = has_service_worker; |
| 387 if (!has_service_worker) |
| 388 service_worker_error_ = NO_MATCHING_SERVICE_WORKER; |
| 389 |
| 390 SetFlag(SERVICE_WORKER_CHECKED); |
| 391 FetchResource(); |
| 392 } |
| 393 |
| 394 void InstallableChecker::ExtractAndFetchBestIcon() { |
| 395 // icon_url_ and icon_ should have both been reset if this method is called. |
| 396 DCHECK(!HasFlag(ICON_FETCHED)); |
| 397 DCHECK(!manifest_.IsEmpty()); |
| 398 DCHECK(icon_url_.is_empty()); |
| 399 DCHECK(icon_.get() == nullptr); |
| 400 DCHECK_GT(ideal_icon_size_in_dp_, 0); |
| 401 DCHECK_GT(minimum_icon_size_in_dp_, 0); |
| 402 |
| 403 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( |
| 404 manifest_.icons, ideal_icon_size_in_dp_, minimum_icon_size_in_dp_); |
| 405 |
| 406 // First, check the icon URL. If it exists, see if the ManifestIconDownloader |
| 407 // is able to download it. If it can, it will call OnAppIconFetched. Any |
| 408 // conditional failures will set the ICON_FETCHED bit and fall through back to |
| 409 // FetchResource. |
| 410 if (icon_url.is_empty()) { |
| 411 icon_error_ = NO_ACCEPTABLE_ICON; |
| 412 } else { |
| 413 bool can_download_icon = ManifestIconDownloader::Download( |
| 414 GetWebContents(), icon_url, ideal_icon_size_in_dp_, |
| 415 minimum_icon_size_in_dp_, |
| 416 base::Bind(&InstallableChecker::OnAppIconFetched, |
| 417 weak_factory_.GetWeakPtr(), icon_url)); |
| 418 if (can_download_icon) |
| 419 return; |
| 420 icon_error_ = CANNOT_DOWNLOAD_ICON; |
| 421 } |
| 422 |
| 423 SetFlag(ICON_FETCHED); |
| 424 FetchResource(); |
| 425 } |
| 426 |
| 427 void InstallableChecker::OnAppIconFetched(const GURL icon_url, |
| 428 const SkBitmap& bitmap) { |
| 429 if (!IsRunning(GetWebContents())) { |
| 430 Cancel(); |
| 431 } else if (bitmap.drawsNothing()) { |
| 432 icon_error_ = NO_ICON_AVAILABLE; |
| 433 } else { |
| 434 icon_url_ = icon_url; |
| 435 icon_.reset(new SkBitmap(bitmap)); |
| 436 } |
| 437 |
| 438 SetFlag(ICON_FETCHED); |
| 439 FetchResource(); |
| 440 } |
| 441 |
| 442 void InstallableChecker::DidFinishNavigation( |
| 443 content::NavigationHandle* handle) { |
| 444 if (handle->IsInMainFrame() && handle->HasCommitted() && |
| 445 !handle->IsErrorPage() && !handle->IsSamePage()) { |
| 446 Reset(); |
| 447 } |
| 448 } |
| 449 |
| 450 void InstallableChecker::WebContentsDestroyed() { |
| 451 Reset(); |
| 452 Observe(nullptr); |
| 453 } |
| 454 |
| 455 // static |
| 456 int InstallableChecker::GetMinimumIconSizeInPx() { |
| 457 return kIconMinimumSizeInPx; |
| 458 } |
| 459 |
| 460 } // namespace installable |
| OLD | NEW |