1 // Copyright 2014 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 "content/browser/appcache/view_appcache_internals_job.h" 6 7 #include <algorithm> 8 #include <string> 9 10 #include "base/base64.h" 11 #include "base/bind.h" 12 #include "base/format_macros.h" 13 #include "base/i18n/time_formatting.h" 14 #include "base/logging.h" 15 #include "base/memory/weak_ptr.h" 16 #include "base/strings/string_number_conversions.h" 17 #include "base/strings/string_util.h" 18 #include "base/strings/stringprintf.h" 19 #include "base/strings/utf_string_conversions.h" 20 #include "net/base/escape.h" 21 #include "net/base/io_buffer.h" 22 #include "net/base/net_errors.h" 23 #include "net/http/http_response_headers.h" 24 #include "net/url_request/url_request.h" 25 #include "net/url_request/url_request_simple_job.h" 26 #include "net/url_request/view_cache_helper.h" 27 #include "webkit/browser/appcache/appcache.h" 28 #include "webkit/browser/appcache/appcache_group.h" 29 #include "webkit/browser/appcache/appcache_policy.h" 30 #include "webkit/browser/appcache/appcache_response.h" 31 #include "webkit/browser/appcache/appcache_service_impl.h" 32 #include "webkit/browser/appcache/appcache_storage.h" 33 34 using appcache::AppCacheGroup; 35 using appcache::AppCacheInfo; 36 using appcache::AppCacheInfoCollection; 37 using appcache::AppCacheInfoVector; 38 using appcache::AppCacheServiceImpl; 39 using appcache::AppCacheStorage; 40 using appcache::AppCacheStorageReference; 41 using appcache::AppCacheResourceInfo; 42 using appcache::AppCacheResourceInfoVector; 43 using appcache::AppCacheResponseInfo; 44 using appcache::AppCacheResponseReader; 45 46 namespace content { 47 namespace { 48 49 const char kErrorMessage[] = "Error in retrieving Application Caches."; 50 const char kEmptyAppCachesMessage[] = "No available Application Caches."; 51 const char kManifestNotFoundMessage[] = "Manifest not found."; 52 const char kManifest[] = "Manifest: "; 53 const char kSize[] = "Size: "; 54 const char kCreationTime[] = "Creation Time: "; 55 const char kLastAccessTime[] = "Last Access Time: "; 56 const char kLastUpdateTime[] = "Last Update Time: "; 57 const char kFormattedDisabledAppCacheMsg[] = 58 "<b><i><font color=\"FF0000\">" 59 "This Application Cache is disabled by policy.</font></i></b><br/>"; 60 const char kRemoveCacheLabel[] = "Remove"; 61 const char kViewCacheLabel[] = "View Entries"; 62 const char kRemoveCacheCommand[] = "remove-cache"; 63 const char kViewCacheCommand[] = "view-cache"; 64 const char kViewEntryCommand[] = "view-entry"; 65 66 void EmitPageStart(std::string* out) { 67 out->append( 68 "<!DOCTYPE HTML>\n" 69 "<html><title>AppCache Internals</title>\n" 70 "<meta http-equiv=\"Content-Security-Policy\"" 71 " content=\"object-src 'none'; script-src 'none'\">\n" 72 "<style>\n" 73 "body { font-family: sans-serif; font-size: 0.8em; }\n" 74 "tt, code, pre { font-family: WebKitHack, monospace; }\n" 75 "form { display: inline; }\n" 76 ".subsection_body { margin: 10px 0 10px 2em; }\n" 77 ".subsection_title { font-weight: bold; }\n" 78 "</style>\n" 79 "</head><body>\n"); 80 } 81 82 void EmitPageEnd(std::string* out) { 83 out->append("</body></html>\n"); 84 } 85 86 void EmitListItem(const std::string& label, 87 const std::string& data, 88 std::string* out) { 89 out->append("<li>"); 90 out->append(net::EscapeForHTML(label)); 91 out->append(net::EscapeForHTML(data)); 92 out->append("</li>\n"); 93 } 94 95 void EmitAnchor(const std::string& url, const std::string& text, 96 std::string* out) { 97 out->append("<a href=\""); 98 out->append(net::EscapeForHTML(url)); 99 out->append("\">"); 100 out->append(net::EscapeForHTML(text)); 101 out->append("</a>"); 102 } 103 104 void EmitCommandAnchor(const char* label, 105 const GURL& base_url, 106 const char* command, 107 const char* param, 108 std::string* out) { 109 std::string query(command); 110 query.push_back('='); 111 query.append(param); 112 GURL::Replacements replacements; 113 replacements.SetQuery(query.data(), url::Component(0, query.length())); 114 GURL command_url = base_url.ReplaceComponents(replacements); 115 EmitAnchor(command_url.spec(), label, out); 116 } 117 118 void EmitAppCacheInfo(const GURL& base_url, 119 AppCacheServiceImpl* service, 120 const AppCacheInfo* info, 121 std::string* out) { 122 std::string manifest_url_base64; 123 base::Base64Encode(info->manifest_url.spec(), &manifest_url_base64); 124 125 out->append("\n<p>"); 126 out->append(kManifest); 127 EmitAnchor(info->manifest_url.spec(), info->manifest_url.spec(), out); 128 out->append("<br/>\n"); 129 if (!service->appcache_policy()->CanLoadAppCache( 130 info->manifest_url, info->manifest_url)) { 131 out->append(kFormattedDisabledAppCacheMsg); 132 } 133 out->append("\n<br/>\n"); 134 EmitCommandAnchor(kRemoveCacheLabel, base_url, 135 kRemoveCacheCommand, manifest_url_base64.c_str(), out); 136 out->append(" "); 137 EmitCommandAnchor(kViewCacheLabel, base_url, 138 kViewCacheCommand, manifest_url_base64.c_str(), out); 139 out->append("\n<br/>\n"); 140 out->append("<ul>"); 141 EmitListItem( 142 kSize, 143 base::UTF16ToUTF8(FormatBytesUnlocalized(info->size)), 144 out); 145 EmitListItem( 146 kCreationTime, 147 base::UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->creation_time)), 148 out); 149 EmitListItem( 150 kLastUpdateTime, 151 base::UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->last_update_time)), 152 out); 153 EmitListItem( 154 kLastAccessTime, 155 base::UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->last_access_time)), 156 out); 157 out->append("</ul></p></br>\n"); 158 } 159 160 void EmitAppCacheInfoVector( 161 const GURL& base_url, 162 AppCacheServiceImpl* service, 163 const AppCacheInfoVector& appcaches, 164 std::string* out) { 165 for (std::vector<AppCacheInfo>::const_iterator info = 166 appcaches.begin(); 167 info != appcaches.end(); ++info) { 168 EmitAppCacheInfo(base_url, service, &(*info), out); 169 } 170 } 171 172 void EmitTableData(const std::string& data, bool align_right, bool bold, 173 std::string* out) { 174 if (align_right) 175 out->append("<td align='right'>"); 176 else 177 out->append("<td>"); 178 if (bold) 179 out->append("<b>"); 180 out->append(data); 181 if (bold) 182 out->append("</b>"); 183 out->append("</td>"); 184 } 185 186 std::string FormFlagsString(const AppCacheResourceInfo& info) { 187 std::string str; 188 if (info.is_manifest) 189 str.append("Manifest, "); 190 if (info.is_master) 191 str.append("Master, "); 192 if (info.is_intercept) 193 str.append("Intercept, "); 194 if (info.is_fallback) 195 str.append("Fallback, "); 196 if (info.is_explicit) 197 str.append("Explicit, "); 198 if (info.is_foreign) 199 str.append("Foreign, "); 200 return str; 201 } 202 203 std::string FormViewEntryAnchor(const GURL& base_url, 204 const GURL& manifest_url, const GURL& entry_url, 205 int64 response_id, 206 int64 group_id) { 207 std::string manifest_url_base64; 208 std::string entry_url_base64; 209 std::string response_id_string; 210 std::string group_id_string; 211 base::Base64Encode(manifest_url.spec(), &manifest_url_base64); 212 base::Base64Encode(entry_url.spec(), &entry_url_base64); 213 response_id_string = base::Int64ToString(response_id); 214 group_id_string = base::Int64ToString(group_id); 215 216 std::string query(kViewEntryCommand); 217 query.push_back('='); 218 query.append(manifest_url_base64); 219 query.push_back('|'); 220 query.append(entry_url_base64); 221 query.push_back('|'); 222 query.append(response_id_string); 223 query.push_back('|'); 224 query.append(group_id_string); 225 226 GURL::Replacements replacements; 227 replacements.SetQuery(query.data(), url::Component(0, query.length())); 228 GURL view_entry_url = base_url.ReplaceComponents(replacements); 229 230 std::string anchor; 231 EmitAnchor(view_entry_url.spec(), entry_url.spec(), &anchor); 232 return anchor; 233 } 234 235 void EmitAppCacheResourceInfoVector( 236 const GURL& base_url, 237 const GURL& manifest_url, 238 const AppCacheResourceInfoVector& resource_infos, 239 int64 group_id, 240 std::string* out) { 241 out->append("<table border='0'>\n"); 242 out->append("<tr>"); 243 EmitTableData("Flags", false, true, out); 244 EmitTableData("URL", false, true, out); 245 EmitTableData("Size (headers and data)", true, true, out); 246 out->append("</tr>\n"); 247 for (AppCacheResourceInfoVector::const_iterator 248 iter = resource_infos.begin(); 249 iter != resource_infos.end(); ++iter) { 250 out->append("<tr>"); 251 EmitTableData(FormFlagsString(*iter), false, false, out); 252 EmitTableData(FormViewEntryAnchor(base_url, manifest_url, 253 iter->url, iter->response_id, 254 group_id), 255 false, false, out); 256 EmitTableData(base::UTF16ToUTF8(FormatBytesUnlocalized(iter->size)), 257 true, false, out); 258 out->append("</tr>\n"); 259 } 260 out->append("</table>\n"); 261 } 262 263 void EmitResponseHeaders(net::HttpResponseHeaders* headers, std::string* out) { 264 out->append("<hr><pre>"); 265 out->append(net::EscapeForHTML(headers->GetStatusLine())); 266 out->push_back('\n'); 267 268 void* iter = NULL; 269 std::string name, value; 270 while (headers->EnumerateHeaderLines(&iter, &name, &value)) { 271 out->append(net::EscapeForHTML(name)); 272 out->append(": "); 273 out->append(net::EscapeForHTML(value)); 274 out->push_back('\n'); 275 } 276 out->append("</pre>"); 277 } 278 279 void EmitHexDump(const char *buf, size_t buf_len, size_t total_len, 280 std::string* out) { 281 out->append("<hr><pre>"); 282 base::StringAppendF(out, "Showing %d of %d bytes\n\n", 283 static_cast<int>(buf_len), static_cast<int>(total_len)); 284 net::ViewCacheHelper::HexDump(buf, buf_len, out); 285 if (buf_len < total_len) 286 out->append("\nNote: data is truncated..."); 287 out->append("</pre>"); 288 } 289 290 GURL DecodeBase64URL(const std::string& base64) { 291 std::string url; 292 base::Base64Decode(base64, &url); 293 return GURL(url); 294 } 295 296 bool ParseQuery(const std::string& query, 297 std::string* command, std::string* value) { 298 size_t position = query.find("="); 299 if (position == std::string::npos) 300 return false; 301 *command = query.substr(0, position); 302 *value = query.substr(position + 1); 303 return !command->empty() && !value->empty(); 304 } 305 306 bool SortByManifestUrl(const AppCacheInfo& lhs, 307 const AppCacheInfo& rhs) { 308 return lhs.manifest_url.spec() < rhs.manifest_url.spec(); 309 } 310 311 bool SortByResourceUrl(const AppCacheResourceInfo& lhs, 312 const AppCacheResourceInfo& rhs) { 313 return lhs.url.spec() < rhs.url.spec(); 314 } 315 316 GURL ClearQuery(const GURL& url) { 317 GURL::Replacements replacements; 318 replacements.ClearQuery(); 319 return url.ReplaceComponents(replacements); 320 } 321 322 // Simple base class for the job subclasses defined here. 323 class BaseInternalsJob : public net::URLRequestSimpleJob, 324 public AppCacheServiceImpl::Observer { 325 protected: 326 BaseInternalsJob(net::URLRequest* request, 327 net::NetworkDelegate* network_delegate, 328 AppCacheServiceImpl* service) 329 : URLRequestSimpleJob(request, network_delegate), 330 appcache_service_(service), 331 appcache_storage_(service->storage()) { 332 appcache_service_->AddObserver(this); 333 } 334 335 virtual ~BaseInternalsJob() { 336 appcache_service_->RemoveObserver(this); 337 } 338 339 virtual void OnServiceReinitialized( 340 AppCacheStorageReference* old_storage_ref) OVERRIDE { 341 if (old_storage_ref->storage() == appcache_storage_) 342 disabled_storage_reference_ = old_storage_ref; 343 } 344 345 AppCacheServiceImpl* appcache_service_; 346 AppCacheStorage* appcache_storage_; 347 scoped_refptr<AppCacheStorageReference> disabled_storage_reference_; 348 }; 349 350 // Job that lists all appcaches in the system. 351 class MainPageJob : public BaseInternalsJob { 352 public: 353 MainPageJob(net::URLRequest* request, 354 net::NetworkDelegate* network_delegate, 355 AppCacheServiceImpl* service) 356 : BaseInternalsJob(request, network_delegate, service), 357 weak_factory_(this) { 358 } 359 360 virtual void Start() OVERRIDE { 361 DCHECK(request_); 362 info_collection_ = new AppCacheInfoCollection; 363 appcache_service_->GetAllAppCacheInfo( 364 info_collection_.get(), 365 base::Bind(&MainPageJob::OnGotInfoComplete, 366 weak_factory_.GetWeakPtr())); 367 } 368 369 // Produces a page containing the listing 370 virtual int GetData(std::string* mime_type, 371 std::string* charset, 372 std::string* out, 373 const net::CompletionCallback& callback) const OVERRIDE { 374 mime_type->assign("text/html"); 375 charset->assign("UTF-8"); 376 377 out->clear(); 378 EmitPageStart(out); 379 if (!info_collection_.get()) { 380 out->append(kErrorMessage); 381 } else if (info_collection_->infos_by_origin.empty()) { 382 out->append(kEmptyAppCachesMessage); 383 } else { 384 typedef std::map<GURL, AppCacheInfoVector> InfoByOrigin; 385 AppCacheInfoVector appcaches; 386 for (InfoByOrigin::const_iterator origin = 387 info_collection_->infos_by_origin.begin(); 388 origin != info_collection_->infos_by_origin.end(); ++origin) { 389 appcaches.insert(appcaches.end(), 390 origin->second.begin(), origin->second.end()); 391 } 392 std::sort(appcaches.begin(), appcaches.end(), SortByManifestUrl); 393 394 GURL base_url = ClearQuery(request_->url()); 395 EmitAppCacheInfoVector(base_url, appcache_service_, appcaches, out); 396 } 397 EmitPageEnd(out); 398 return net::OK; 399 } 400 401 private: 402 virtual ~MainPageJob() {} 403 404 void OnGotInfoComplete(int rv) { 405 if (rv != net::OK) 406 info_collection_ = NULL; 407 StartAsync(); 408 } 409 410 scoped_refptr<AppCacheInfoCollection> info_collection_; 411 base::WeakPtrFactory<MainPageJob> weak_factory_; 412 DISALLOW_COPY_AND_ASSIGN(MainPageJob); 413 }; 414 415 // Job that redirects back to the main appcache internals page. 416 class RedirectToMainPageJob : public BaseInternalsJob { 417 public: 418 RedirectToMainPageJob(net::URLRequest* request, 419 net::NetworkDelegate* network_delegate, 420 AppCacheServiceImpl* service) 421 : BaseInternalsJob(request, network_delegate, service) {} 422 423 virtual int GetData(std::string* mime_type, 424 std::string* charset, 425 std::string* data, 426 const net::CompletionCallback& callback) const OVERRIDE { 427 return net::OK; // IsRedirectResponse induces a redirect. 428 } 429 430 virtual bool IsRedirectResponse(GURL* location, 431 int* http_status_code) OVERRIDE { 432 *location = ClearQuery(request_->url()); 433 *http_status_code = 307; 434 return true; 435 } 436 437 protected: 438 virtual ~RedirectToMainPageJob() {} 439 }; 440 441 // Job that removes an appcache and then redirects back to the main page. 442 class RemoveAppCacheJob : public RedirectToMainPageJob { 443 public: 444 RemoveAppCacheJob( 445 net::URLRequest* request, 446 net::NetworkDelegate* network_delegate, 447 AppCacheServiceImpl* service, 448 const GURL& manifest_url) 449 : RedirectToMainPageJob(request, network_delegate, service), 450 manifest_url_(manifest_url), 451 weak_factory_(this) { 452 } 453 454 virtual void Start() OVERRIDE { 455 DCHECK(request_); 456 457 appcache_service_->DeleteAppCacheGroup( 458 manifest_url_,base::Bind(&RemoveAppCacheJob::OnDeleteAppCacheComplete, 459 weak_factory_.GetWeakPtr())); 460 } 461 462 private: 463 virtual ~RemoveAppCacheJob() {} 464 465 void OnDeleteAppCacheComplete(int rv) { 466 StartAsync(); // Causes the base class to redirect. 467 } 468 469 GURL manifest_url_; 470 base::WeakPtrFactory<RemoveAppCacheJob> weak_factory_; 471 }; 472 473 474 // Job shows the details of a particular manifest url. 475 class ViewAppCacheJob : public BaseInternalsJob, 476 public AppCacheStorage::Delegate { 477 public: 478 ViewAppCacheJob( 479 net::URLRequest* request, 480 net::NetworkDelegate* network_delegate, 481 AppCacheServiceImpl* service, 482 const GURL& manifest_url) 483 : BaseInternalsJob(request, network_delegate, service), 484 manifest_url_(manifest_url) {} 485 486 virtual void Start() OVERRIDE { 487 DCHECK(request_); 488 appcache_storage_->LoadOrCreateGroup(manifest_url_, this); 489 } 490 491 // Produces a page containing the entries listing. 492 virtual int GetData(std::string* mime_type, 493 std::string* charset, 494 std::string* out, 495 const net::CompletionCallback& callback) const OVERRIDE { 496 mime_type->assign("text/html"); 497 charset->assign("UTF-8"); 498 out->clear(); 499 EmitPageStart(out); 500 if (appcache_info_.manifest_url.is_empty()) { 501 out->append(kManifestNotFoundMessage); 502 } else { 503 GURL base_url = ClearQuery(request_->url()); 504 EmitAppCacheInfo(base_url, appcache_service_, &appcache_info_, out); 505 EmitAppCacheResourceInfoVector(base_url, 506 manifest_url_, 507 resource_infos_, 508 appcache_info_.group_id, 509 out); 510 } 511 EmitPageEnd(out); 512 return net::OK; 513 } 514 515 private: 516 virtual ~ViewAppCacheJob() { 517 appcache_storage_->CancelDelegateCallbacks(this); 518 } 519 520 // AppCacheStorage::Delegate override 521 virtual void OnGroupLoaded( 522 AppCacheGroup* group, const GURL& manifest_url) OVERRIDE { 523 DCHECK_EQ(manifest_url_, manifest_url); 524 if (group && group->newest_complete_cache()) { 525 appcache_info_.manifest_url = manifest_url; 526 appcache_info_.group_id = group->group_id(); 527 appcache_info_.size = group->newest_complete_cache()->cache_size(); 528 appcache_info_.creation_time = group->creation_time(); 529 appcache_info_.last_update_time = 530 group->newest_complete_cache()->update_time(); 531 appcache_info_.last_access_time = base::Time::Now(); 532 group->newest_complete_cache()->ToResourceInfoVector(&resource_infos_); 533 std::sort(resource_infos_.begin(), resource_infos_.end(), 534 SortByResourceUrl); 535 } 536 StartAsync(); 537 } 538 539 GURL manifest_url_; 540 AppCacheInfo appcache_info_; 541 AppCacheResourceInfoVector resource_infos_; 542 DISALLOW_COPY_AND_ASSIGN(ViewAppCacheJob); 543 }; 544 545 // Job that shows the details of a particular cached resource. 546 class ViewEntryJob : public BaseInternalsJob, 547 public AppCacheStorage::Delegate { 548 public: 549 ViewEntryJob( 550 net::URLRequest* request, 551 net::NetworkDelegate* network_delegate, 552 AppCacheServiceImpl* service, 553 const GURL& manifest_url, 554 const GURL& entry_url, 555 int64 response_id, int64 group_id) 556 : BaseInternalsJob(request, network_delegate, service), 557 manifest_url_(manifest_url), entry_url_(entry_url), 558 response_id_(response_id), group_id_(group_id), amount_read_(0) { 559 } 560 561 virtual void Start() OVERRIDE { 562 DCHECK(request_); 563 appcache_storage_->LoadResponseInfo( 564 manifest_url_, group_id_, response_id_, this); 565 } 566 567 // Produces a page containing the response headers and data. 568 virtual int GetData(std::string* mime_type, 569 std::string* charset, 570 std::string* out, 571 const net::CompletionCallback& callback) const OVERRIDE { 572 mime_type->assign("text/html"); 573 charset->assign("UTF-8"); 574 out->clear(); 575 EmitPageStart(out); 576 EmitAnchor(entry_url_.spec(), entry_url_.spec(), out); 577 out->append("<br/>\n"); 578 if (response_info_.get()) { 579 if (response_info_->http_response_info()) 580 EmitResponseHeaders(response_info_->http_response_info()->headers.get(), 581 out); 582 else 583 out->append("Failed to read response headers.<br>"); 584 585 if (response_data_.get()) { 586 EmitHexDump(response_data_->data(), 587 amount_read_, 588 response_info_->response_data_size(), 589 out); 590 } else { 591 out->append("Failed to read response data.<br>"); 592 } 593 } else { 594 out->append("Failed to read response headers and data.<br>"); 595 } 596 EmitPageEnd(out); 597 return net::OK; 598 } 599 600 private: 601 virtual ~ViewEntryJob() { 602 appcache_storage_->CancelDelegateCallbacks(this); 603 } 604 605 virtual void OnResponseInfoLoaded( 606 AppCacheResponseInfo* response_info, int64 response_id) OVERRIDE { 607 if (!response_info) { 608 StartAsync(); 609 return; 610 } 611 response_info_ = response_info; 612 613 // Read the response data, truncating if its too large. 614 const int64 kLimit = 100 * 1000; 615 int64 amount_to_read = 616 std::min(kLimit, response_info->response_data_size()); 617 response_data_ = new net::IOBuffer(amount_to_read); 618 619 reader_.reset(appcache_storage_->CreateResponseReader( 620 manifest_url_, group_id_, response_id_)); 621 reader_->ReadData( 622 response_data_.get(), 623 amount_to_read, 624 base::Bind(&ViewEntryJob::OnReadComplete, base::Unretained(this))); 625 } 626 627 void OnReadComplete(int result) { 628 reader_.reset(); 629 amount_read_ = result; 630 if (result < 0) 631 response_data_ = NULL; 632 StartAsync(); 633 } 634 635 GURL manifest_url_; 636 GURL entry_url_; 637 int64 response_id_; 638 int64 group_id_; 639 scoped_refptr<AppCacheResponseInfo> response_info_; 640 scoped_refptr<net::IOBuffer> response_data_; 641 int amount_read_; 642 scoped_ptr<AppCacheResponseReader> reader_; 643 }; 644 645 } // namespace 646 647 net::URLRequestJob* ViewAppCacheInternalsJobFactory::CreateJobForRequest( 648 net::URLRequest* request, 649 net::NetworkDelegate* network_delegate, 650 AppCacheServiceImpl* service) { 651 if (!request->url().has_query()) 652 return new MainPageJob(request, network_delegate, service); 653 654 std::string command; 655 std::string param; 656 ParseQuery(request->url().query(), &command, ¶m); 657 658 if (command == kRemoveCacheCommand) 659 return new RemoveAppCacheJob(request, network_delegate, service, 660 DecodeBase64URL(param)); 661 662 if (command == kViewCacheCommand) 663 return new ViewAppCacheJob(request, network_delegate, service, 664 DecodeBase64URL(param)); 665 666 std::vector<std::string> tokens; 667 int64 response_id; 668 int64 group_id; 669 if (command == kViewEntryCommand && Tokenize(param, "|", &tokens) == 4u && 670 base::StringToInt64(tokens[2], &response_id) && 671 base::StringToInt64(tokens[3], &group_id)) { 672 return new ViewEntryJob(request, network_delegate, service, 673 DecodeBase64URL(tokens[0]), // manifest url 674 DecodeBase64URL(tokens[1]), // entry url 675 response_id, group_id); 676 } 677 678 return new RedirectToMainPageJob(request, network_delegate, service); 679 } 680 681 } // namespace content 682