web_resource_service.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2011 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/web_resource/web_resource_service.h"
6
7#include <string>
8
9#include "base/command_line.h"
10#include "base/file_path.h"
11#include "base/string_util.h"
12#include "base/string_number_conversions.h"
13#include "base/threading/thread_restrictions.h"
14#include "base/time.h"
15#include "base/utf_string_conversions.h"
16#include "base/values.h"
17#include "chrome/browser/browser_process.h"
18#include "chrome/browser/browser_thread.h"
19#include "chrome/browser/extensions/extension_service.h"
20#include "chrome/browser/platform_util.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/browser/sync/sync_ui_util.h"
23#include "chrome/common/chrome_switches.h"
24#include "chrome/common/extensions/extension.h"
25#include "chrome/common/net/url_fetcher.h"
26#include "chrome/common/notification_service.h"
27#include "chrome/common/notification_type.h"
28#include "chrome/common/pref_names.h"
29#include "googleurl/src/gurl.h"
30#include "net/base/load_flags.h"
31#include "net/url_request/url_request_status.h"
32
33namespace {
34
35// Delay on first fetch so we don't interfere with startup.
36static const int kStartResourceFetchDelay = 5000;
37
38// Delay between calls to update the cache (48 hours).
39static const int kCacheUpdateDelay = 48 * 60 * 60 * 1000;
40
41// Users are randomly assigned to one of kNTPPromoGroupSize buckets, in order
42// to be able to roll out promos slowly, or display different promos to
43// different groups.
44static const int kNTPPromoGroupSize = 16;
45
46// Maximum number of hours for each time slice (4 weeks).
47static const int kMaxTimeSliceHours = 24 * 7 * 4;
48
49// Used to determine which build type should be shown a given promo.
50enum BuildType {
51  DEV_BUILD = 1,
52  BETA_BUILD = 1 << 1,
53  STABLE_BUILD = 1 << 2,
54};
55
56}  // namespace
57
58const char* WebResourceService::kCurrentTipPrefName = "current_tip";
59const char* WebResourceService::kTipCachePrefName = "tips";
60
61class WebResourceService::WebResourceFetcher
62    : public URLFetcher::Delegate {
63 public:
64  explicit WebResourceFetcher(WebResourceService* web_resource_service) :
65      ALLOW_THIS_IN_INITIALIZER_LIST(fetcher_factory_(this)),
66      web_resource_service_(web_resource_service) {
67  }
68
69  // Delay initial load of resource data into cache so as not to interfere
70  // with startup time.
71  void StartAfterDelay(int64 delay_ms) {
72    MessageLoop::current()->PostDelayedTask(FROM_HERE,
73      fetcher_factory_.NewRunnableMethod(&WebResourceFetcher::StartFetch),
74                                         delay_ms);
75  }
76
77  // Initializes the fetching of data from the resource server.  Data
78  // load calls OnURLFetchComplete.
79  void StartFetch() {
80    // Balanced in OnURLFetchComplete.
81    web_resource_service_->AddRef();
82    // First, put our next cache load on the MessageLoop.
83    MessageLoop::current()->PostDelayedTask(FROM_HERE,
84        fetcher_factory_.NewRunnableMethod(&WebResourceFetcher::StartFetch),
85            web_resource_service_->cache_update_delay());
86    // If we are still fetching data, exit.
87    if (web_resource_service_->in_fetch_)
88      return;
89    else
90      web_resource_service_->in_fetch_ = true;
91
92    std::string locale = g_browser_process->GetApplicationLocale();
93    std::string web_resource_server = kDefaultWebResourceServer;
94    web_resource_server.append(locale);
95
96    url_fetcher_.reset(new URLFetcher(GURL(
97        web_resource_server),
98        URLFetcher::GET, this));
99    // Do not let url fetcher affect existing state in profile (by setting
100    // cookies, for example.
101    url_fetcher_->set_load_flags(net::LOAD_DISABLE_CACHE |
102      net::LOAD_DO_NOT_SAVE_COOKIES);
103    URLRequestContextGetter* url_request_context_getter =
104        web_resource_service_->profile()->GetRequestContext();
105    url_fetcher_->set_request_context(url_request_context_getter);
106    url_fetcher_->Start();
107  }
108
109  // From URLFetcher::Delegate.
110  void OnURLFetchComplete(const URLFetcher* source,
111                          const GURL& url,
112                          const net::URLRequestStatus& status,
113                          int response_code,
114                          const ResponseCookies& cookies,
115                          const std::string& data) {
116    // Delete the URLFetcher when this function exits.
117    scoped_ptr<URLFetcher> clean_up_fetcher(url_fetcher_.release());
118
119    // Don't parse data if attempt to download was unsuccessful.
120    // Stop loading new web resource data, and silently exit.
121    if (!status.is_success() || (response_code != 200))
122      return;
123
124    web_resource_service_->UpdateResourceCache(data);
125    web_resource_service_->Release();
126  }
127
128 private:
129  // So that we can delay our start so as not to affect start-up time; also,
130  // so that we can schedule future cache updates.
131  ScopedRunnableMethodFactory<WebResourceFetcher> fetcher_factory_;
132
133  // The tool that fetches the url data from the server.
134  scoped_ptr<URLFetcher> url_fetcher_;
135
136  // Our owner and creator. Ref counted.
137  WebResourceService* web_resource_service_;
138};
139
140// This class coordinates a web resource unpack and parse task which is run in
141// a separate process.  Results are sent back to this class and routed to
142// the WebResourceService.
143class WebResourceService::UnpackerClient
144    : public UtilityProcessHost::Client {
145 public:
146  UnpackerClient(WebResourceService* web_resource_service,
147                 const std::string& json_data)
148    : web_resource_service_(web_resource_service),
149      json_data_(json_data), got_response_(false) {
150  }
151
152  void Start() {
153    AddRef();  // balanced in Cleanup.
154
155    // If we don't have a resource_dispatcher_host_, assume we're in
156    // a test and run the unpacker directly in-process.
157    bool use_utility_process =
158        web_resource_service_->resource_dispatcher_host_ != NULL &&
159        !CommandLine::ForCurrentProcess()->HasSwitch(switches::kSingleProcess);
160    if (use_utility_process) {
161      BrowserThread::ID thread_id;
162      CHECK(BrowserThread::GetCurrentThreadIdentifier(&thread_id));
163      BrowserThread::PostTask(
164          BrowserThread::IO, FROM_HERE,
165          NewRunnableMethod(this, &UnpackerClient::StartProcessOnIOThread,
166                            web_resource_service_->resource_dispatcher_host_,
167                            thread_id));
168    } else {
169      WebResourceUnpacker unpacker(json_data_);
170      if (unpacker.Run()) {
171        OnUnpackWebResourceSucceeded(*unpacker.parsed_json());
172      } else {
173        OnUnpackWebResourceFailed(unpacker.error_message());
174      }
175    }
176  }
177
178 private:
179  ~UnpackerClient() {}
180
181  // UtilityProcessHost::Client
182  virtual void OnProcessCrashed(int exit_code) {
183    if (got_response_)
184      return;
185
186    OnUnpackWebResourceFailed(
187        "Chrome crashed while trying to retrieve web resources.");
188  }
189
190  virtual void OnUnpackWebResourceSucceeded(
191      const DictionaryValue& parsed_json) {
192    web_resource_service_->OnWebResourceUnpacked(parsed_json);
193    Cleanup();
194  }
195
196  virtual void OnUnpackWebResourceFailed(const std::string& error_message) {
197    web_resource_service_->EndFetch();
198    Cleanup();
199  }
200
201  // Release reference and set got_response_.
202  void Cleanup() {
203    if (got_response_)
204      return;
205
206    got_response_ = true;
207    Release();
208  }
209
210  void StartProcessOnIOThread(ResourceDispatcherHost* rdh,
211                              BrowserThread::ID thread_id) {
212    UtilityProcessHost* host = new UtilityProcessHost(rdh, this, thread_id);
213    // TODO(mrc): get proper file path when we start using web resources
214    // that need to be unpacked.
215    host->StartWebResourceUnpacker(json_data_);
216  }
217
218  scoped_refptr<WebResourceService> web_resource_service_;
219
220  // Holds raw JSON string.
221  const std::string& json_data_;
222
223  // True if we got a response from the utility process and have cleaned up
224  // already.
225  bool got_response_;
226};
227
228// Server for dynamically loaded NTP HTML elements. TODO(mirandac): append
229// locale for future usage, when we're serving localizable strings.
230const char* WebResourceService::kDefaultWebResourceServer =
231    "https://www.google.com/support/chrome/bin/topic/1142433/inproduct?hl=";
232
233WebResourceService::WebResourceService(Profile* profile)
234    : prefs_(profile->GetPrefs()),
235      profile_(profile),
236      ALLOW_THIS_IN_INITIALIZER_LIST(service_factory_(this)),
237      in_fetch_(false),
238      web_resource_update_scheduled_(false) {
239  Init();
240}
241
242WebResourceService::~WebResourceService() { }
243
244void WebResourceService::Init() {
245  cache_update_delay_ = kCacheUpdateDelay;
246  resource_dispatcher_host_ = g_browser_process->resource_dispatcher_host();
247  web_resource_fetcher_.reset(new WebResourceFetcher(this));
248  prefs_->RegisterStringPref(prefs::kNTPWebResourceCacheUpdate, "0");
249  prefs_->RegisterDoublePref(prefs::kNTPCustomLogoStart, 0);
250  prefs_->RegisterDoublePref(prefs::kNTPCustomLogoEnd, 0);
251  prefs_->RegisterDoublePref(prefs::kNTPPromoStart, 0);
252  prefs_->RegisterDoublePref(prefs::kNTPPromoEnd, 0);
253  prefs_->RegisterStringPref(prefs::kNTPPromoLine, std::string());
254  prefs_->RegisterBooleanPref(prefs::kNTPPromoClosed, false);
255  prefs_->RegisterIntegerPref(prefs::kNTPPromoGroup, -1);
256  prefs_->RegisterIntegerPref(prefs::kNTPPromoBuild,
257                              DEV_BUILD | BETA_BUILD | STABLE_BUILD);
258  prefs_->RegisterIntegerPref(prefs::kNTPPromoGroupTimeSlice, 0);
259
260  // If the promo start is in the future, set a notification task to invalidate
261  // the NTP cache at the time of the promo start.
262  double promo_start = prefs_->GetDouble(prefs::kNTPPromoStart);
263  double promo_end = prefs_->GetDouble(prefs::kNTPPromoEnd);
264  ScheduleNotification(promo_start, promo_end);
265}
266
267void WebResourceService::EndFetch() {
268  in_fetch_ = false;
269}
270
271void WebResourceService::OnWebResourceUnpacked(
272  const DictionaryValue& parsed_json) {
273  UnpackLogoSignal(parsed_json);
274  UnpackPromoSignal(parsed_json);
275  EndFetch();
276}
277
278void WebResourceService::WebResourceStateChange() {
279  web_resource_update_scheduled_ = false;
280  NotificationService* service = NotificationService::current();
281  service->Notify(NotificationType::WEB_RESOURCE_STATE_CHANGED,
282                  Source<WebResourceService>(this),
283                  NotificationService::NoDetails());
284}
285
286void WebResourceService::ScheduleNotification(double promo_start,
287                                              double promo_end) {
288  if (promo_start > 0 && promo_end > 0 && !web_resource_update_scheduled_) {
289    int64 ms_until_start =
290        static_cast<int64>((base::Time::FromDoubleT(
291            promo_start) - base::Time::Now()).InMilliseconds());
292    int64 ms_until_end =
293        static_cast<int64>((base::Time::FromDoubleT(
294            promo_end) - base::Time::Now()).InMilliseconds());
295    if (ms_until_start > 0) {
296      web_resource_update_scheduled_ = true;
297      MessageLoop::current()->PostDelayedTask(FROM_HERE,
298          service_factory_.NewRunnableMethod(
299              &WebResourceService::WebResourceStateChange),
300              ms_until_start);
301    }
302    if (ms_until_end > 0) {
303      web_resource_update_scheduled_ = true;
304      MessageLoop::current()->PostDelayedTask(FROM_HERE,
305          service_factory_.NewRunnableMethod(
306              &WebResourceService::WebResourceStateChange),
307              ms_until_end);
308      if (ms_until_start <= 0) {
309        // Notify immediately if time is between start and end.
310        WebResourceStateChange();
311      }
312    }
313  }
314}
315
316void WebResourceService::StartAfterDelay() {
317  int64 delay = kStartResourceFetchDelay;
318  // Check whether we have ever put a value in the web resource cache;
319  // if so, pull it out and see if it's time to update again.
320  if (prefs_->HasPrefPath(prefs::kNTPWebResourceCacheUpdate)) {
321    std::string last_update_pref =
322        prefs_->GetString(prefs::kNTPWebResourceCacheUpdate);
323    if (!last_update_pref.empty()) {
324      double last_update_value;
325      base::StringToDouble(last_update_pref, &last_update_value);
326      int64 ms_until_update = cache_update_delay_ -
327          static_cast<int64>((base::Time::Now() - base::Time::FromDoubleT(
328          last_update_value)).InMilliseconds());
329      delay = ms_until_update > cache_update_delay_ ?
330          cache_update_delay_ : (ms_until_update < kStartResourceFetchDelay ?
331                                kStartResourceFetchDelay : ms_until_update);
332    }
333  }
334  // Start fetch and wait for UpdateResourceCache.
335  web_resource_fetcher_->StartAfterDelay(delay);
336}
337
338void WebResourceService::UpdateResourceCache(const std::string& json_data) {
339  UnpackerClient* client = new UnpackerClient(this, json_data);
340  client->Start();
341
342  // Set cache update time in preferences.
343  prefs_->SetString(prefs::kNTPWebResourceCacheUpdate,
344      base::DoubleToString(base::Time::Now().ToDoubleT()));
345}
346
347void WebResourceService::UnpackTips(const DictionaryValue& parsed_json) {
348  // Get dictionary of cached preferences.
349  web_resource_cache_ =
350      prefs_->GetMutableDictionary(prefs::kNTPWebResourceCache);
351
352  // The list of individual tips.
353  ListValue* tip_holder = new ListValue();
354  web_resource_cache_->Set(WebResourceService::kTipCachePrefName, tip_holder);
355
356  DictionaryValue* topic_dict;
357  ListValue* answer_list;
358  std::string topic_id;
359  std::string answer_id;
360  std::string inproduct;
361  int tip_counter = 0;
362
363  if (parsed_json.GetDictionary("topic", &topic_dict)) {
364    if (topic_dict->GetString("topic_id", &topic_id))
365      web_resource_cache_->SetString("topic_id", topic_id);
366    if (topic_dict->GetList("answers", &answer_list)) {
367      for (ListValue::const_iterator tip_iter = answer_list->begin();
368           tip_iter != answer_list->end(); ++tip_iter) {
369        if (!(*tip_iter)->IsType(Value::TYPE_DICTIONARY))
370          continue;
371        DictionaryValue* a_dic =
372            static_cast<DictionaryValue*>(*tip_iter);
373        if (a_dic->GetString("inproduct", &inproduct)) {
374          tip_holder->Append(Value::CreateStringValue(inproduct));
375        }
376        tip_counter++;
377      }
378      // If tips exist, set current index to 0.
379      if (!inproduct.empty()) {
380        web_resource_cache_->SetInteger(
381          WebResourceService::kCurrentTipPrefName, 0);
382      }
383    }
384  }
385}
386
387void WebResourceService::UnpackPromoSignal(const DictionaryValue& parsed_json) {
388  DictionaryValue* topic_dict;
389  ListValue* answer_list;
390  double old_promo_start = 0;
391  double old_promo_end = 0;
392  double promo_start = 0;
393  double promo_end = 0;
394
395  // Check for preexisting start and end values.
396  if (prefs_->HasPrefPath(prefs::kNTPPromoStart) &&
397      prefs_->HasPrefPath(prefs::kNTPPromoEnd)) {
398    old_promo_start = prefs_->GetDouble(prefs::kNTPPromoStart);
399    old_promo_end = prefs_->GetDouble(prefs::kNTPPromoEnd);
400  }
401
402  // Check for newly received start and end values.
403  if (parsed_json.GetDictionary("topic", &topic_dict)) {
404    if (topic_dict->GetList("answers", &answer_list)) {
405      std::string promo_start_string = "";
406      std::string promo_end_string = "";
407      std::string promo_string = "";
408      std::string promo_build = "";
409      int promo_build_type = 0;
410      int time_slice_hrs = 0;
411      for (ListValue::const_iterator tip_iter = answer_list->begin();
412           tip_iter != answer_list->end(); ++tip_iter) {
413        if (!(*tip_iter)->IsType(Value::TYPE_DICTIONARY))
414          continue;
415        DictionaryValue* a_dic =
416            static_cast<DictionaryValue*>(*tip_iter);
417        std::string promo_signal;
418        if (a_dic->GetString("name", &promo_signal)) {
419          if (promo_signal == "promo_start") {
420            a_dic->GetString("question", &promo_build);
421            size_t split = promo_build.find(":");
422            if (split != std::string::npos &&
423                base::StringToInt(promo_build.substr(0, split),
424                                  &promo_build_type) &&
425                base::StringToInt(promo_build.substr(split+1),
426                                  &time_slice_hrs) &&
427                promo_build_type >= 0 &&
428                promo_build_type <= (DEV_BUILD | BETA_BUILD | STABLE_BUILD) &&
429                time_slice_hrs >= 0 &&
430                time_slice_hrs <= kMaxTimeSliceHours) {
431              prefs_->SetInteger(prefs::kNTPPromoBuild, promo_build_type);
432              prefs_->SetInteger(prefs::kNTPPromoGroupTimeSlice,
433                                 time_slice_hrs);
434            } else {
435              // If no time data or bad time data are set, show promo on all
436              // builds with no time slicing.
437              prefs_->SetInteger(prefs::kNTPPromoBuild,
438                                 DEV_BUILD | BETA_BUILD | STABLE_BUILD);
439              prefs_->SetInteger(prefs::kNTPPromoGroupTimeSlice, 0);
440            }
441            a_dic->GetString("inproduct", &promo_start_string);
442            a_dic->GetString("tooltip", &promo_string);
443            prefs_->SetString(prefs::kNTPPromoLine, promo_string);
444            srand(static_cast<uint32>(time(NULL)));
445            prefs_->SetInteger(prefs::kNTPPromoGroup,
446                               rand() % kNTPPromoGroupSize);
447          } else if (promo_signal == "promo_end") {
448            a_dic->GetString("inproduct", &promo_end_string);
449          }
450        }
451      }
452      if (!promo_start_string.empty() &&
453          promo_start_string.length() > 0 &&
454          !promo_end_string.empty() &&
455          promo_end_string.length() > 0) {
456        base::Time start_time;
457        base::Time end_time;
458        if (base::Time::FromString(
459                ASCIIToWide(promo_start_string).c_str(), &start_time) &&
460            base::Time::FromString(
461                ASCIIToWide(promo_end_string).c_str(), &end_time)) {
462          // Add group time slice, adjusted from hours to seconds.
463          promo_start = start_time.ToDoubleT() +
464              (prefs_->FindPreference(prefs::kNTPPromoGroup) ?
465                  prefs_->GetInteger(prefs::kNTPPromoGroup) *
466                      time_slice_hrs * 60 * 60 : 0);
467          promo_end = end_time.ToDoubleT();
468        }
469      }
470    }
471  }
472
473  // If start or end times have changed, trigger a new web resource
474  // notification, so that the logo on the NTP is updated. This check is
475  // outside the reading of the web resource data, because the absence of
476  // dates counts as a triggering change if there were dates before.
477  // Also reset the promo closed preference, to signal a new promo.
478  if (!(old_promo_start == promo_start) ||
479      !(old_promo_end == promo_end)) {
480    prefs_->SetDouble(prefs::kNTPPromoStart, promo_start);
481    prefs_->SetDouble(prefs::kNTPPromoEnd, promo_end);
482    prefs_->SetBoolean(prefs::kNTPPromoClosed, false);
483    ScheduleNotification(promo_start, promo_end);
484  }
485}
486
487void WebResourceService::UnpackLogoSignal(const DictionaryValue& parsed_json) {
488  DictionaryValue* topic_dict;
489  ListValue* answer_list;
490  double old_logo_start = 0;
491  double old_logo_end = 0;
492  double logo_start = 0;
493  double logo_end = 0;
494
495  // Check for preexisting start and end values.
496  if (prefs_->HasPrefPath(prefs::kNTPCustomLogoStart) &&
497      prefs_->HasPrefPath(prefs::kNTPCustomLogoEnd)) {
498    old_logo_start = prefs_->GetDouble(prefs::kNTPCustomLogoStart);
499    old_logo_end = prefs_->GetDouble(prefs::kNTPCustomLogoEnd);
500  }
501
502  // Check for newly received start and end values.
503  if (parsed_json.GetDictionary("topic", &topic_dict)) {
504    if (topic_dict->GetList("answers", &answer_list)) {
505      std::string logo_start_string = "";
506      std::string logo_end_string = "";
507      for (ListValue::const_iterator tip_iter = answer_list->begin();
508           tip_iter != answer_list->end(); ++tip_iter) {
509        if (!(*tip_iter)->IsType(Value::TYPE_DICTIONARY))
510          continue;
511        DictionaryValue* a_dic =
512            static_cast<DictionaryValue*>(*tip_iter);
513        std::string logo_signal;
514        if (a_dic->GetString("name", &logo_signal)) {
515          if (logo_signal == "custom_logo_start") {
516            a_dic->GetString("inproduct", &logo_start_string);
517          } else if (logo_signal == "custom_logo_end") {
518            a_dic->GetString("inproduct", &logo_end_string);
519          }
520        }
521      }
522      if (!logo_start_string.empty() &&
523          logo_start_string.length() > 0 &&
524          !logo_end_string.empty() &&
525          logo_end_string.length() > 0) {
526        base::Time start_time;
527        base::Time end_time;
528        if (base::Time::FromString(
529                ASCIIToWide(logo_start_string).c_str(), &start_time) &&
530            base::Time::FromString(
531                ASCIIToWide(logo_end_string).c_str(), &end_time)) {
532          logo_start = start_time.ToDoubleT();
533          logo_end = end_time.ToDoubleT();
534        }
535      }
536    }
537  }
538
539  // If logo start or end times have changed, trigger a new web resource
540  // notification, so that the logo on the NTP is updated. This check is
541  // outside the reading of the web resource data, because the absence of
542  // dates counts as a triggering change if there were dates before.
543  if (!(old_logo_start == logo_start) ||
544      !(old_logo_end == logo_end)) {
545    prefs_->SetDouble(prefs::kNTPCustomLogoStart, logo_start);
546    prefs_->SetDouble(prefs::kNTPCustomLogoEnd, logo_end);
547    NotificationService* service = NotificationService::current();
548    service->Notify(NotificationType::WEB_RESOURCE_STATE_CHANGED,
549                    Source<WebResourceService>(this),
550                    NotificationService::NoDetails());
551  }
552}
553
554namespace WebResourceServiceUtil {
555
556bool CanShowPromo(Profile* profile) {
557  bool promo_closed = false;
558  PrefService* prefs = profile->GetPrefs();
559  if (prefs->HasPrefPath(prefs::kNTPPromoClosed))
560    promo_closed = prefs->GetBoolean(prefs::kNTPPromoClosed);
561
562  // Only show if not synced.
563  bool is_synced =
564      (profile->HasProfileSyncService() &&
565          sync_ui_util::GetStatus(
566              profile->GetProfileSyncService()) == sync_ui_util::SYNCED);
567
568  // GetVersionStringModifier hits the registry. See http://crbug.com/70898.
569  base::ThreadRestrictions::ScopedAllowIO allow_io;
570  const std::string channel = platform_util::GetVersionStringModifier();
571  bool is_promo_build = false;
572  if (prefs->HasPrefPath(prefs::kNTPPromoBuild)) {
573    int builds_allowed = prefs->GetInteger(prefs::kNTPPromoBuild);
574    if (channel == "dev") {
575      is_promo_build = (DEV_BUILD & builds_allowed) != 0;
576    } else if (channel == "beta") {
577      is_promo_build = (BETA_BUILD & builds_allowed) != 0;
578    } else if (channel == "stable") {
579      is_promo_build = (STABLE_BUILD & builds_allowed) != 0;
580    } else {
581      is_promo_build = true;
582    }
583  }
584
585  return !promo_closed && !is_synced && is_promo_build;
586}
587
588}  // namespace WebResourceService
589
590