summaryrefslogtreecommitdiffstats
path: root/chrome/browser/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/extensions')
-rw-r--r--chrome/browser/extensions/extension_bookmarks_module.cc313
-rw-r--r--chrome/browser/extensions/extension_bookmarks_module.h34
-rw-r--r--chrome/browser/extensions/extension_function.cc2
-rw-r--r--chrome/browser/extensions/extension_function.h11
-rw-r--r--chrome/browser/extensions/extension_function_dispatcher.cc13
-rw-r--r--chrome/browser/extensions/extension_test_api.cc11
-rw-r--r--chrome/browser/extensions/extension_test_api.h6
-rw-r--r--chrome/browser/extensions/extensions_quota_service.cc148
-rw-r--r--chrome/browser/extensions/extensions_quota_service.h195
-rw-r--r--chrome/browser/extensions/extensions_quota_service_unittest.cc308
-rw-r--r--chrome/browser/extensions/extensions_service.h6
11 files changed, 1010 insertions, 37 deletions
diff --git a/chrome/browser/extensions/extension_bookmarks_module.cc b/chrome/browser/extensions/extension_bookmarks_module.cc
index 0029702..9ae6ff9 100644
--- a/chrome/browser/extensions/extension_bookmarks_module.cc
+++ b/chrome/browser/extensions/extension_bookmarks_module.cc
@@ -5,6 +5,8 @@
#include "chrome/browser/extensions/extension_bookmarks_module.h"
#include "base/json/json_writer.h"
+#include "base/sha1.h"
+#include "base/stl_util-inl.h"
#include "base/string_util.h"
#include "chrome/browser/bookmarks/bookmark_codec.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
@@ -12,6 +14,7 @@
#include "chrome/browser/browser_list.h"
#include "chrome/browser/extensions/extension_bookmarks_module_constants.h"
#include "chrome/browser/extensions/extension_message_service.h"
+#include "chrome/browser/extensions/extensions_quota_service.h"
#include "chrome/browser/profile.h"
#include "chrome/common/notification_service.h"
#include "chrome/common/pref_names.h"
@@ -19,6 +22,14 @@
namespace keys = extension_bookmarks_module_constants;
+using base::TimeDelta;
+typedef QuotaLimitHeuristic::Bucket Bucket;
+typedef QuotaLimitHeuristic::Config Config;
+typedef QuotaLimitHeuristic::BucketList BucketList;
+typedef ExtensionsQuotaService::TimedLimit TimedLimit;
+typedef ExtensionsQuotaService::SustainedLimit SustainedLimit;
+typedef QuotaLimitHeuristic::BucketMapper BucketMapper;
+
// Helper functions.
class ExtensionBookmarks {
public:
@@ -360,32 +371,59 @@ bool SearchBookmarksFunction::RunImpl() {
return true;
}
+// static
+bool RemoveBookmarkFunction::ExtractIds(const Value* args,
+ std::list<int64>* ids, bool* invalid_id) {
+ std::string id_string;
+ if (args->IsType(Value::TYPE_STRING) &&
+ args->GetAsString(&id_string)) {
+ int64 id;
+ if (StringToInt64(id_string, &id))
+ ids->push_back(id);
+ else
+ *invalid_id = true;
+ } else {
+ if (!args->IsType(Value::TYPE_LIST))
+ return false;
+ const ListValue* ids_list = static_cast<const ListValue*>(args);
+ size_t count = ids_list->GetSize();
+ if (count <= 0)
+ return false;
+ for (size_t i = 0; i < count; ++i) {
+ if (!ids_list->GetString(i, &id_string))
+ return false;
+ int64 id;
+ if (StringToInt64(id_string, &id)) {
+ ids->push_back(id);
+ } else {
+ *invalid_id = true;
+ break;
+ }
+ }
+ }
+ return true;
+}
+
bool RemoveBookmarkFunction::RunImpl() {
+ std::list<int64> ids;
+ bool invalid_id = false;
+ EXTENSION_FUNCTION_VALIDATE(ExtractIds(args_.get(), &ids, &invalid_id));
+ if (invalid_id) {
+ error_ = keys::kInvalidIdError;
+ return false;
+ }
bool recursive = false;
if (name() == RemoveTreeBookmarkFunction::function_name())
recursive = true;
BookmarkModel* model = profile()->GetBookmarkModel();
- int64 id;
- std::string id_string;
- if (args_->IsType(Value::TYPE_STRING) &&
- args_->GetAsString(&id_string) &&
- StringToInt64(id_string, &id)) {
- return ExtensionBookmarks::RemoveNode(model, id, recursive, &error_);
- } else {
- EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_LIST));
- const ListValue* ids = args_as_list();
- size_t count = ids->GetSize();
- EXTENSION_FUNCTION_VALIDATE(count > 0);
- for (size_t i = 0; i < count; ++i) {
- EXTENSION_FUNCTION_VALIDATE(ids->GetString(i, &id_string));
- if (!GetBookmarkIdAsInt64(id_string, &id))
- return false;
- if (!ExtensionBookmarks::RemoveNode(model, id, recursive, &error_))
- return false;
- }
- return true;
+ size_t count = ids.size();
+ EXTENSION_FUNCTION_VALIDATE(count > 0);
+ for (std::list<int64>::iterator it = ids.begin(); it != ids.end(); ++it) {
+ if (!ExtensionBookmarks::RemoveNode(model, *it, recursive, &error_))
+ return false;
}
+ return true;
}
bool CreateBookmarkFunction::RunImpl() {
@@ -452,19 +490,30 @@ bool CreateBookmarkFunction::RunImpl() {
return true;
}
+// static
+bool MoveBookmarkFunction::ExtractIds(const Value* args,
+ std::list<int64>* ids,
+ bool* invalid_id) {
+ // For now, Move accepts ID parameters in the same way as an Update.
+ return UpdateBookmarkFunction::ExtractIds(args, ids, invalid_id);
+}
+
bool MoveBookmarkFunction::RunImpl() {
- EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_LIST));
- const ListValue* args = args_as_list();
- int64 id;
- std::string id_string;
- EXTENSION_FUNCTION_VALIDATE(args->GetString(0, &id_string));
- if (!GetBookmarkIdAsInt64(id_string, &id))
+ std::list<int64> ids;
+ bool invalid_id = false;
+ EXTENSION_FUNCTION_VALIDATE(ExtractIds(args_.get(), &ids, &invalid_id));
+ if (invalid_id) {
+ error_ = keys::kInvalidIdError;
return false;
+ }
+ EXTENSION_FUNCTION_VALIDATE(ids.size() == 1);
+ const ListValue* args = args_as_list();
+
DictionaryValue* destination;
EXTENSION_FUNCTION_VALIDATE(args->GetDictionary(1, &destination));
BookmarkModel* model = profile()->GetBookmarkModel();
- const BookmarkNode* node = model->GetNodeByID(id);
+ const BookmarkNode* node = model->GetNodeByID(ids.front());
if (!node) {
error_ = keys::kNoNodeError;
return false;
@@ -520,21 +569,42 @@ bool MoveBookmarkFunction::RunImpl() {
return true;
}
-bool UpdateBookmarkFunction::RunImpl() {
- EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_LIST));
- const ListValue* args = args_as_list();
- int64 id;
+// static
+bool UpdateBookmarkFunction::ExtractIds(const Value* args,
+ std::list<int64>* ids,
+ bool* invalid_id) {
std::string id_string;
- EXTENSION_FUNCTION_VALIDATE(args->GetString(0, &id_string));
- if (!GetBookmarkIdAsInt64(id_string, &id))
+ if (!args->IsType(Value::TYPE_LIST))
+ return false;
+ const ListValue* args_list = static_cast<const ListValue*>(args);
+ if (!args_list->GetString(0, &id_string))
return false;
+ int64 id;
+ if (StringToInt64(id_string, &id))
+ ids->push_back(id);
+ else
+ *invalid_id = true;
+ return true;
+}
+
+bool UpdateBookmarkFunction::RunImpl() {
+ std::list<int64> ids;
+ bool invalid_id = false;
+ EXTENSION_FUNCTION_VALIDATE(ExtractIds(args_.get(), &ids, &invalid_id));
+ if (invalid_id) {
+ error_ = keys::kInvalidIdError;
+ return false;
+ }
+ EXTENSION_FUNCTION_VALIDATE(ids.size() == 1);
+
+ const ListValue* args = args_as_list();
DictionaryValue* updates;
EXTENSION_FUNCTION_VALIDATE(args->GetDictionary(1, &updates));
std::wstring title;
updates->GetString(keys::kTitleKey, &title); // Optional (empty is clear).
BookmarkModel* model = profile()->GetBookmarkModel();
- const BookmarkNode* node = model->GetNodeByID(id);
+ const BookmarkNode* node = model->GetNodeByID(ids.front());
if (!node) {
error_ = keys::kNoNodeError;
return false;
@@ -552,3 +622,180 @@ bool UpdateBookmarkFunction::RunImpl() {
return true;
}
+
+// Mapper superclass for BookmarkFunctions.
+template <typename BucketIdType>
+class BookmarkBucketMapper : public BucketMapper {
+ public:
+ virtual ~BookmarkBucketMapper() { STLDeleteValues(&buckets_); }
+ protected:
+ Bucket* GetBucket(const BucketIdType& id) {
+ Bucket* b = buckets_[id];
+ if (b == NULL) {
+ b = new Bucket();
+ buckets_[id] = b;
+ }
+ return b;
+ }
+ private:
+ std::map<BucketIdType, Bucket*> buckets_;
+};
+
+// Mapper for 'bookmarks.create'. Maps "same input to bookmarks.create" to a
+// unique bucket.
+class CreateBookmarkBucketMapper : public BookmarkBucketMapper<std::string> {
+ public:
+ explicit CreateBookmarkBucketMapper(Profile* profile) : profile_(profile) {}
+ // TODO(tim): This should share code with CreateBookmarkFunction::RunImpl,
+ // but I can't figure out a good way to do that with all the macros.
+ virtual void GetBucketsForArgs(const Value* args, BucketList* buckets) {
+ if (!args->IsType(Value::TYPE_DICTIONARY))
+ return;
+
+ std::string parent_id;
+ const DictionaryValue* json = static_cast<const DictionaryValue*>(args);
+ if (json->HasKey(keys::kParentIdKey)) {
+ if (!json->GetString(keys::kParentIdKey, &parent_id))
+ return;
+ }
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetNodeByID(StringToInt64(parent_id));
+ if (!parent)
+ return;
+
+ std::string bucket_id = WideToUTF8(parent->GetTitle());
+ std::wstring title;
+ json->GetString(keys::kTitleKey, &title);
+ std::string url_string;
+ json->GetString(keys::kUrlKey, &url_string);
+
+ bucket_id += WideToUTF8(title);
+ bucket_id += url_string;
+ // 20 bytes (SHA1 hash length) is very likely less than most of the
+ // |bucket_id| strings we construct here, so we hash it to save space.
+ buckets->push_back(GetBucket(base::SHA1HashString(bucket_id)));
+ }
+ private:
+ Profile* profile_;
+};
+
+// Mapper for 'bookmarks.remove'.
+class RemoveBookmarksBucketMapper : public BookmarkBucketMapper<std::string> {
+ public:
+ explicit RemoveBookmarksBucketMapper(Profile* profile) : profile_(profile) {}
+ virtual void GetBucketsForArgs(const Value* args, BucketList* buckets) {
+ typedef std::list<int64> IdList;
+ IdList ids;
+ bool invalid_id = false;
+ if (!RemoveBookmarkFunction::ExtractIds(args, &ids, &invalid_id) ||
+ invalid_id) {
+ return;
+ }
+
+ for (IdList::iterator it = ids.begin(); it != ids.end(); ++it) {
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ const BookmarkNode* node = model->GetNodeByID(*it);
+ if (!node || !node->GetParent())
+ return;
+
+ std::string bucket_id;
+ bucket_id += WideToUTF8(node->GetParent()->GetTitle());
+ bucket_id += WideToUTF8(node->GetTitle());
+ bucket_id += node->GetURL().spec();
+ buckets->push_back(GetBucket(base::SHA1HashString(bucket_id)));
+ }
+ }
+ private:
+ Profile* profile_;
+};
+
+// Mapper for any bookmark function accepting bookmark IDs as parameters, where
+// a distinct ID corresponds to a single item in terms of quota limiting. This
+// is inappropriate for bookmarks.remove, for example, since repeated removals
+// of the same item will actually have a different ID each time.
+template <class FunctionType>
+class BookmarkIdMapper : public BookmarkBucketMapper<int64> {
+ public:
+ typedef std::list<int64> IdList;
+ virtual void GetBucketsForArgs(const Value* args, BucketList* buckets) {
+ IdList ids;
+ bool invalid_id = false;
+ if (!FunctionType::ExtractIds(args, &ids, &invalid_id) || invalid_id)
+ return;
+ for (IdList::iterator it = ids.begin(); it != ids.end(); ++it)
+ buckets->push_back(GetBucket(*it));
+ }
+};
+
+// Builds heuristics for all BookmarkFunctions using specialized BucketMappers.
+class BookmarksQuotaLimitFactory {
+ public:
+ // For id-based bookmark functions.
+ template <class FunctionType>
+ static void Build(QuotaLimitHeuristics* heuristics) {
+ BuildWithMappers(heuristics, new BookmarkIdMapper<FunctionType>(),
+ new BookmarkIdMapper<FunctionType>());
+ }
+
+ // For bookmarks.create.
+ static void BuildForCreate(QuotaLimitHeuristics* heuristics,
+ Profile* profile) {
+ BuildWithMappers(heuristics, new CreateBookmarkBucketMapper(profile),
+ new CreateBookmarkBucketMapper(profile));
+ }
+
+ // For bookmarks.remove.
+ static void BuildForRemove(QuotaLimitHeuristics* heuristics,
+ Profile* profile) {
+ BuildWithMappers(heuristics, new RemoveBookmarksBucketMapper(profile),
+ new RemoveBookmarksBucketMapper(profile));
+ }
+
+ private:
+ static void BuildWithMappers(QuotaLimitHeuristics* heuristics,
+ BucketMapper* short_mapper, BucketMapper* long_mapper) {
+ TimedLimit* timed = new TimedLimit(kLongLimitConfig, long_mapper);
+ // A max of two operations per minute, sustained over 10 minutes.
+ SustainedLimit* sustained = new SustainedLimit(TimeDelta::FromMinutes(10),
+ kShortLimitConfig, short_mapper);
+ heuristics->push_back(timed);
+ heuristics->push_back(sustained);
+ }
+
+ // The quota configurations used for all BookmarkFunctions.
+ static const Config kShortLimitConfig;
+ static const Config kLongLimitConfig;
+
+ DISALLOW_IMPLICIT_CONSTRUCTORS(BookmarksQuotaLimitFactory);
+};
+
+const Config BookmarksQuotaLimitFactory::kShortLimitConfig = {
+ 2, // 2 tokens per interval.
+ TimeDelta::FromMinutes(1) // 1 minute long refill interval.
+};
+
+const Config BookmarksQuotaLimitFactory::kLongLimitConfig = {
+ 100, // 100 tokens per interval.
+ TimeDelta::FromHours(1) // 1 hour long refill interval.
+};
+
+// And finally, building the individual heuristics for each function.
+void RemoveBookmarkFunction::GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ BookmarksQuotaLimitFactory::BuildForRemove(heuristics, profile());
+}
+
+void MoveBookmarkFunction::GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ BookmarksQuotaLimitFactory::Build<MoveBookmarkFunction>(heuristics);
+}
+
+void UpdateBookmarkFunction::GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ BookmarksQuotaLimitFactory::Build<UpdateBookmarkFunction>(heuristics);
+};
+
+void CreateBookmarkFunction::GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ BookmarksQuotaLimitFactory::BuildForCreate(heuristics, profile());
+}
diff --git a/chrome/browser/extensions/extension_bookmarks_module.h b/chrome/browser/extensions/extension_bookmarks_module.h
index acaf91d..42b780c 100644
--- a/chrome/browser/extensions/extension_bookmarks_module.h
+++ b/chrome/browser/extensions/extension_bookmarks_module.h
@@ -5,6 +5,7 @@
#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_BOOKMARKS_MODULE_H_
#define CHROME_BROWSER_EXTENSIONS_EXTENSION_BOOKMARKS_MODULE_H_
+#include <list>
#include <string>
#include "base/singleton.h"
@@ -64,6 +65,7 @@ class ExtensionBookmarkEventRouter : public BookmarkModelObserver {
class BookmarksFunction : public AsyncExtensionFunction,
public NotificationObserver {
+ public:
virtual void Run();
virtual bool RunImpl() = 0;
@@ -81,27 +83,43 @@ class BookmarksFunction : public AsyncExtensionFunction,
};
class GetBookmarksFunction : public BookmarksFunction {
+ public:
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.get")
};
class GetBookmarkChildrenFunction : public BookmarksFunction {
+ public:
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.getChildren")
};
class GetBookmarkTreeFunction : public BookmarksFunction {
+ public:
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.getTree")
};
class SearchBookmarksFunction : public BookmarksFunction {
+ public:
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.search")
};
class RemoveBookmarkFunction : public BookmarksFunction {
+ public:
+ // Returns true on successful parse and sets invalid_id to true if conversion
+ // from id string to int64 failed.
+ static bool ExtractIds(const Value* args, std::list<int64>* ids,
+ bool* invalid_id);
virtual bool RunImpl();
+ virtual void GetQuotaLimitHeuristics(
+ std::list<QuotaLimitHeuristic*>* heuristics) const;
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.remove")
};
@@ -110,17 +128,33 @@ class RemoveTreeBookmarkFunction : public RemoveBookmarkFunction {
};
class CreateBookmarkFunction : public BookmarksFunction {
+ public:
+ virtual void GetQuotaLimitHeuristics(
+ std::list<QuotaLimitHeuristic*>* heuristics) const;
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.create")
};
class MoveBookmarkFunction : public BookmarksFunction {
+ public:
+ static bool ExtractIds(const Value* args, std::list<int64>* ids,
+ bool* invalid_id);
+ virtual void GetQuotaLimitHeuristics(
+ std::list<QuotaLimitHeuristic*>* heuristics) const;
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.move")
};
class UpdateBookmarkFunction : public BookmarksFunction {
+ public:
+ static bool ExtractIds(const Value* args, std::list<int64>* ids,
+ bool* invalid_id);
+ virtual void GetQuotaLimitHeuristics(
+ std::list<QuotaLimitHeuristic*>* heuristics) const;
virtual bool RunImpl();
+ private:
DECLARE_EXTENSION_FUNCTION_NAME("bookmarks.update")
};
diff --git a/chrome/browser/extensions/extension_function.cc b/chrome/browser/extensions/extension_function.cc
index 27ce5eb..006fe99 100644
--- a/chrome/browser/extensions/extension_function.cc
+++ b/chrome/browser/extensions/extension_function.cc
@@ -36,7 +36,7 @@ std::string AsyncExtensionFunction::extension_id() {
return dispatcher()->extension_id();
}
-Profile* AsyncExtensionFunction::profile() {
+Profile* AsyncExtensionFunction::profile() const {
DCHECK(dispatcher());
return dispatcher()->profile();
}
diff --git a/chrome/browser/extensions/extension_function.h b/chrome/browser/extensions/extension_function.h
index be74f36..6db6443 100644
--- a/chrome/browser/extensions/extension_function.h
+++ b/chrome/browser/extensions/extension_function.h
@@ -6,6 +6,7 @@
#define CHROME_BROWSER_EXTENSIONS_EXTENSION_FUNCTION_H_
#include <string>
+#include <list>
#include "base/values.h"
#include "base/scoped_ptr.h"
@@ -14,6 +15,7 @@
class ExtensionFunctionDispatcher;
class Profile;
+class QuotaLimitHeuristic;
#define EXTENSION_FUNCTION_VALIDATE(test) do { \
if (!(test)) { \
@@ -48,10 +50,15 @@ class ExtensionFunction : public base::RefCounted<ExtensionFunction> {
// Retrieves any error string from the function.
virtual const std::string GetError() = 0;
+ // Returns a quota limit heuristic suitable for this function.
+ // No quota limiting by default.
+ virtual void GetQuotaLimitHeuristics(
+ std::list<QuotaLimitHeuristic*>* heuristics) const {}
+
void set_dispatcher_peer(ExtensionFunctionDispatcher::Peer* peer) {
peer_ = peer;
}
- ExtensionFunctionDispatcher* dispatcher() {
+ ExtensionFunctionDispatcher* dispatcher() const {
return peer_->dispatcher_;
}
@@ -134,7 +141,7 @@ class AsyncExtensionFunction : public ExtensionFunction {
// Note: After Run() returns, dispatcher() can be NULL. Since these getters
// rely on dispatcher(), make sure it is valid before using them.
std::string extension_id();
- Profile* profile();
+ Profile* profile() const;
// The arguments to the API. Only non-null if argument were specified.
scoped_ptr<Value> args_;
diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc
index 3a99e37..071c6f1 100644
--- a/chrome/browser/extensions/extension_function_dispatcher.cc
+++ b/chrome/browser/extensions/extension_function_dispatcher.cc
@@ -24,6 +24,7 @@
#include "chrome/browser/extensions/extension_tabs_module_constants.h"
#include "chrome/browser/extensions/extension_test_api.h"
#include "chrome/browser/extensions/extension_toolstrip_api.h"
+#include "chrome/browser/extensions/extensions_quota_service.h"
#include "chrome/browser/extensions/extensions_service.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/renderer_host/render_process_host.h"
@@ -149,6 +150,7 @@ void FactoryRegistry::ResetFunctions() {
RegisterFunction<ExtensionTestPassFunction>();
RegisterFunction<ExtensionTestFailFunction>();
RegisterFunction<ExtensionTestLogFunction>();
+ RegisterFunction<ExtensionTestQuotaResetFunction>();
}
void FactoryRegistry::GetAllNames(std::vector<std::string>* names) {
@@ -281,7 +283,16 @@ void ExtensionFunctionDispatcher::HandleRequest(const std::string& name,
function->SetArgs(args);
function->set_request_id(request_id);
function->set_has_callback(has_callback);
- function->Run();
+
+ ExtensionsService* service = profile()->GetExtensionsService();
+ DCHECK(service);
+ ExtensionsQuotaService* quota = service->quota_service();
+ if (quota->Assess(extension_id(), function, args, base::TimeTicks::Now())) {
+ function->Run();
+ } else {
+ render_view_host_->SendExtensionResponse(function->request_id(), false,
+ std::string(), QuotaLimitHeuristic::kGenericOverQuotaError);
+ }
}
void ExtensionFunctionDispatcher::SendResponse(ExtensionFunction* function,
diff --git a/chrome/browser/extensions/extension_test_api.cc b/chrome/browser/extensions/extension_test_api.cc
index e3354e4..ac0a2eb 100644
--- a/chrome/browser/extensions/extension_test_api.cc
+++ b/chrome/browser/extensions/extension_test_api.cc
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "chrome/browser/profile.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/extensions/extensions_quota_service.h"
#include "chrome/browser/extensions/extension_test_api.h"
#include "chrome/common/notification_service.h"
@@ -30,3 +33,11 @@ bool ExtensionTestLogFunction::RunImpl() {
LOG(INFO) << message;
return true;
}
+
+bool ExtensionTestQuotaResetFunction::RunImpl() {
+ ExtensionsService* service = profile()->GetExtensionsService();
+ ExtensionsQuotaService* quota = service->quota_service();
+ quota->Purge();
+ quota->violators_.clear();
+ return true;
+}
diff --git a/chrome/browser/extensions/extension_test_api.h b/chrome/browser/extensions/extension_test_api.h
index 79adf13..3b89e53 100644
--- a/chrome/browser/extensions/extension_test_api.h
+++ b/chrome/browser/extensions/extension_test_api.h
@@ -25,4 +25,10 @@ class ExtensionTestLogFunction : public SyncExtensionFunction {
DECLARE_EXTENSION_FUNCTION_NAME("test.log")
};
+class ExtensionTestQuotaResetFunction : public SyncExtensionFunction {
+ ~ExtensionTestQuotaResetFunction() {}
+ virtual bool RunImpl();
+ DECLARE_EXTENSION_FUNCTION_NAME("test.resetQuota")
+};
+
#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_TEST_API_H_
diff --git a/chrome/browser/extensions/extensions_quota_service.cc b/chrome/browser/extensions/extensions_quota_service.cc
new file mode 100644
index 0000000..9005d8f
--- /dev/null
+++ b/chrome/browser/extensions/extensions_quota_service.cc
@@ -0,0 +1,148 @@
+// Copyright (c) 2009 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/browser/extensions/extensions_quota_service.h"
+
+#include "base/message_loop.h"
+#include "base/stl_util-inl.h"
+#include "chrome/browser/extensions/extension_function.h"
+
+// If the browser stays open long enough, we reset state once a day.
+// Whatever this value is, it should be an order of magnitude longer than
+// the longest interval in any of the QuotaLimitHeuristics in use.
+static const int kPurgeIntervalInDays = 1;
+
+const char QuotaLimitHeuristic::kGenericOverQuotaError[] =
+ "This request exceeds available quota.";
+
+ExtensionsQuotaService::ExtensionsQuotaService() {
+ if (MessageLoop::current() != NULL) { // Null in unit tests.
+ purge_timer_.Start(base::TimeDelta::FromDays(kPurgeIntervalInDays),
+ this, &ExtensionsQuotaService::Purge);
+ }
+}
+
+ExtensionsQuotaService::~ExtensionsQuotaService() {
+ purge_timer_.Stop();
+ Purge();
+}
+
+bool ExtensionsQuotaService::Assess(const std::string& extension_id,
+ ExtensionFunction* function, const Value* args,
+ const base::TimeTicks& event_time) {
+ // Lookup function list for extension.
+ FunctionHeuristicsMap& functions = function_heuristics_[extension_id];
+
+ // Lookup heuristics for function, create if necessary.
+ QuotaLimitHeuristics& heuristics = functions[function->name()];
+ if (heuristics.empty())
+ function->GetQuotaLimitHeuristics(&heuristics);
+
+ if (heuristics.empty())
+ return true; // No heuristic implies no limit.
+
+ if (violators_.find(extension_id) != violators_.end())
+ return false; // Repeat offender.
+
+ bool global_decision = true;
+ for (QuotaLimitHeuristics::iterator heuristic = heuristics.begin();
+ heuristic != heuristics.end(); ++heuristic) {
+ // Apply heuristic to each item (bucket).
+ global_decision = (*heuristic)->ApplyToArgs(args, event_time) &&
+ global_decision;
+ }
+
+ if (!global_decision) {
+ PurgeFunctionHeuristicsMap(&functions);
+ function_heuristics_.erase(extension_id);
+ violators_.insert(extension_id);
+ }
+ return global_decision;
+}
+
+void ExtensionsQuotaService::PurgeFunctionHeuristicsMap(
+ FunctionHeuristicsMap* map) {
+ FunctionHeuristicsMap::iterator heuristics = map->begin();
+ while (heuristics != map->end()) {
+ STLDeleteElements(&heuristics->second);
+ map->erase(heuristics++);
+ }
+}
+
+void ExtensionsQuotaService::Purge() {
+ std::map<std::string, FunctionHeuristicsMap>::iterator it =
+ function_heuristics_.begin();
+ for (; it != function_heuristics_.end(); function_heuristics_.erase(it++))
+ PurgeFunctionHeuristicsMap(&it->second);
+}
+
+void QuotaLimitHeuristic::Bucket::Reset(const Config& config,
+ const base::TimeTicks& start) {
+ num_tokens_ = config.refill_token_count;
+ expiration_ = start + config.refill_interval;
+}
+
+bool QuotaLimitHeuristic::ApplyToArgs(const Value* args,
+ const base::TimeTicks& event_time) {
+ BucketList buckets;
+ bucket_mapper_->GetBucketsForArgs(args, &buckets);
+ for (BucketList::iterator i = buckets.begin(); i != buckets.end(); ++i) {
+ if ((*i)->expiration().is_null()) // A brand new bucket.
+ (*i)->Reset(config_, event_time);
+ if (!Apply(*i, event_time))
+ return false; // It only takes one to spoil it for everyone.
+ }
+ return true;
+}
+
+ExtensionsQuotaService::SustainedLimit::SustainedLimit(
+ const base::TimeDelta& sustain, const Config& config, BucketMapper* map)
+ : QuotaLimitHeuristic(config, map),
+ repeat_exhaustion_allowance_(sustain.InSeconds() /
+ config.refill_interval.InSeconds()),
+ num_available_repeat_exhaustions_(repeat_exhaustion_allowance_) {
+}
+
+bool ExtensionsQuotaService::TimedLimit::Apply(Bucket* bucket,
+ const base::TimeTicks& event_time) {
+ if (event_time > bucket->expiration())
+ bucket->Reset(config(), event_time);
+
+ return bucket->DeductToken();
+}
+
+bool ExtensionsQuotaService::SustainedLimit::Apply(Bucket* bucket,
+ const base::TimeTicks& event_time) {
+ if (event_time > bucket->expiration()) {
+ // We reset state for this item and start over again if this request breaks
+ // the bad cycle that was previously being tracked. This occurs if the
+ // state in the bucket expired recently (it has been long enough since the
+ // event that we don't care about the last event), but the bucket still has
+ // tokens (so pressure was not sustained over that time), OR we are more
+ // than 1 full refill interval away from the last event (so even if we used
+ // up all the tokens in the last bucket, nothing happened in the entire
+ // next refill interval, so it doesn't matter).
+ if (bucket->has_tokens() || event_time > bucket->expiration() +
+ config().refill_interval) {
+ bucket->Reset(config(), event_time);
+ num_available_repeat_exhaustions_ = repeat_exhaustion_allowance_;
+ } else if (--num_available_repeat_exhaustions_ > 0) {
+ // The last interval was saturated with requests, and this is the first
+ // event in the next interval. If this happens
+ // repeat_exhaustion_allowance_ times, it's a violation. Reset the bucket
+ // state to start timing from the end of the last interval (and we'll
+ // deduct the token below) so we can detect this each time it happens.
+ bucket->Reset(config(), bucket->expiration());
+ } else {
+ // No allowances left; this request is a violation.
+ return false;
+ }
+ }
+
+ // We can go negative since we check has_tokens when we get to *next* bucket,
+ // and for the small interval all that matters is whether we used up all the
+ // tokens (which is true if num_tokens_ <= 0).
+ bucket->DeductToken();
+ return true;
+}
diff --git a/chrome/browser/extensions/extensions_quota_service.h b/chrome/browser/extensions/extensions_quota_service.h
new file mode 100644
index 0000000..f68a763
--- /dev/null
+++ b/chrome/browser/extensions/extensions_quota_service.h
@@ -0,0 +1,195 @@
+// Copyright (c) 2009 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.
+
+// The ExtensionsQuotaService uses heuristics to limit abusive requests
+// made by extensions. In this model 'items' (e.g individual bookmarks) are
+// represented by a 'Bucket' that holds state for that item for one single
+// interval of time. The interval of time is defined as 'how long we need to
+// watch an item (for a particular heuristic) before making a decision about
+// quota violations'. A heuristic is two functions: one mapping input
+// arguments to a unique Bucket (the BucketMapper), and another to determine
+// if a new request involving such an item at a given time is a violation.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSIONS_QUOTA_SERVICE_H_
+#define CHROME_BROWSER_EXTENSIONS_EXTENSIONS_QUOTA_SERVICE_H_
+
+#include <list>
+#include <map>
+#include <string>
+
+#include "base/hash_tables.h"
+#include "base/scoped_ptr.h"
+#include "base/time.h"
+#include "base/timer.h"
+#include "base/values.h"
+
+class ExtensionFunction;
+class QuotaLimitHeuristic;
+typedef std::list<QuotaLimitHeuristic*> QuotaLimitHeuristics;
+
+class ExtensionsQuotaService {
+ public:
+ // Some concrete heuristics (declared below) that ExtensionFunctions can
+ // use to help the service make decisions about quota violations.
+ class TimedLimit;
+ class SustainedLimit;
+
+ ExtensionsQuotaService();
+ ~ExtensionsQuotaService();
+
+ // Decide whether the invocation of |function| with argument |args| by the
+ // extension specified by |extension_id| results in a quota limit violation.
+ // Returns true if the request is fine and can proceed, false if the request
+ // should be throttled and an error returned to the extension.
+ bool Assess(const std::string& extension_id, ExtensionFunction* function,
+ const Value* args, const base::TimeTicks& event_time);
+ private:
+ friend class ExtensionTestQuotaResetFunction;
+ typedef std::map<std::string, QuotaLimitHeuristics> FunctionHeuristicsMap;
+
+ // Purge resets all accumulated data (except |violators_|) as if the service
+ // was just created. Called periodically so we don't consume an unbounded
+ // amount of memory while tracking quota. Yes, this could mean an extension
+ // gets away with murder if it is timed right, but the extensions we are
+ // trying to limit are ones that consistently violate, so we'll converge
+ // to the correct set.
+ void Purge();
+ void PurgeFunctionHeuristicsMap(FunctionHeuristicsMap* map);
+ base::RepeatingTimer<ExtensionsQuotaService> purge_timer_;
+
+ // Our quota tracking state for extensions that have invoked quota limited
+ // functions. Each extension is treated separately, so extension ids are the
+ // key for the mapping. As an extension invokes functions, the map keeps
+ // track of which functions it has invoked and the heuristics for each one.
+ // Each heuristic will be evaluated and ANDed together to get a final answer.
+ std::map<std::string, FunctionHeuristicsMap> function_heuristics_;
+
+ // For now, as soon as an extension violates quota, we don't allow it to
+ // make any more requests to quota limited functions. This provides a quick
+ // lookup for these extensions that is only stored in memory.
+ base::hash_set<std::string> violators_;
+
+ DISALLOW_COPY_AND_ASSIGN(ExtensionsQuotaService);
+};
+
+// A QuotaLimitHeuristic is two things: 1, A heuristic to map extension
+// function arguments to corresponding Buckets for each input arg, and 2) a
+// heuristic for determining if a new event involving a particular item
+// (represented by its Bucket) constitutes a quota violation.
+class QuotaLimitHeuristic {
+ public:
+ // Parameters to configure the amount of tokens allotted to individual
+ // Bucket objects (see Below) and how often they are replenished.
+ struct Config {
+ // The maximum number of tokens a bucket can contain, and is refilled to
+ // every epoch.
+ int64 refill_token_count;
+
+ // Specifies how frequently the bucket is logically refilled with tokens.
+ base::TimeDelta refill_interval;
+ };
+
+ // A Bucket is how the heuristic portrays an individual item (since quota
+ // limits are per item) and all associated state for an item that needs to
+ // carry through multiple calls to Apply. It "holds" tokens, which are
+ // debited and credited in response to new events involving the item being
+ // being represented. For convenience, instead of actually periodically
+ // refilling buckets they are just 'Reset' on-demand (e.g. when new events
+ // come in). So, a bucket has an expiration to denote it has becomes stale.
+ class Bucket {
+ public:
+ explicit Bucket() : num_tokens_(0) {}
+ // Removes a token from this bucket, and returns true if the bucket had
+ // any tokens in the first place.
+ bool DeductToken() { return num_tokens_-- > 0; }
+
+ // Returns true if this bucket has tokens to deduct.
+ bool has_tokens() const { return num_tokens_ > 0; }
+
+ // Reset this bucket to specification (from internal configuration), to be
+ // valid from |start| until the first refill interval elapses and it needs
+ // to be reset again.
+ void Reset(const Config& config, const base::TimeTicks& start);
+
+ // The time at which the token count and next expiration should be reset,
+ // via a call to Reset.
+ const base::TimeTicks& expiration() { return expiration_; }
+ private:
+ base::TimeTicks expiration_;
+ int64 num_tokens_;
+ DISALLOW_COPY_AND_ASSIGN(Bucket);
+ };
+ typedef std::list<Bucket*> BucketList;
+
+ // A generic error message for quota violating requests.
+ static const char kGenericOverQuotaError[];
+
+ // A helper interface to retrieve the bucket corresponding to |args| from
+ // the set of buckets (which is typically stored in the BucketMapper itself)
+ // for this QuotaLimitHeuristic.
+ class BucketMapper {
+ public:
+ // In most cases, this should simply extract item IDs from the arguments
+ // (e.g for bookmark operations involving an existing item). If a problem
+ // occurs while parsing |args|, the function aborts - buckets may be non-
+ // empty). The expectation is that invalid args and associated errors are
+ // handled by the ExtensionFunction itself so we don't concern ourselves.
+ virtual void GetBucketsForArgs(const Value* args, BucketList* buckets) = 0;
+ };
+
+ // Ownership of |mapper| is given to the new QuotaLimitHeuristic.
+ explicit QuotaLimitHeuristic(const Config& config, BucketMapper* map)
+ : config_(config), bucket_mapper_(map) {}
+ virtual ~QuotaLimitHeuristic() {}
+
+ // Determines if sufficient quota exists (according to the Apply
+ // implementation of a derived class) to perform an operation with |args|,
+ // based on the history of similar operations with similar arguments (which
+ // is retrieved using the BucketMapper).
+ bool ApplyToArgs(const Value* args, const base::TimeTicks& event_time);
+
+ protected:
+ const Config& config() { return config_; }
+
+ // Determine if the new event occurring at |event_time| involving |bucket|
+ // constitutes a quota violation according to this heuristic.
+ virtual bool Apply(Bucket* bucket, const base::TimeTicks& event_time) = 0;
+
+ private:
+ friend class QuotaLimitHeuristicTest;
+
+ const Config config_;
+
+ // The mapper used in Map. Cannot be NULL.
+ scoped_ptr<BucketMapper> bucket_mapper_;
+
+ DISALLOW_COPY_AND_ASSIGN(QuotaLimitHeuristic);
+};
+
+// A simple per-item heuristic to limit the number of events that can occur in
+// a given period of time; e.g "no more than 100 events in an hour".
+class ExtensionsQuotaService::TimedLimit : public QuotaLimitHeuristic {
+ public:
+ explicit TimedLimit(const Config& config, BucketMapper* map)
+ : QuotaLimitHeuristic(config, map) {}
+ virtual bool Apply(Bucket* bucket, const base::TimeTicks& event_time);
+};
+
+// A per-item heuristic to limit the number of events that can occur in a
+// period of time over a sustained longer interval. E.g "no more than two
+// events per minute, sustained over 10 minutes".
+class ExtensionsQuotaService::SustainedLimit : public QuotaLimitHeuristic {
+ public:
+ SustainedLimit(const base::TimeDelta& sustain,
+ const Config& config,
+ BucketMapper* map);
+ virtual bool Apply(Bucket* bucket, const base::TimeTicks& event_time);
+ private:
+ // Specifies how long exhaustion of buckets is allowed to continue before
+ // denying requests.
+ const int64 repeat_exhaustion_allowance_;
+ int64 num_available_repeat_exhaustions_;
+};
+
+#endif // CHROME_BROWSER_EXTENSIONS_EXTENSIONS_QUOTA_SERVICE_H_
diff --git a/chrome/browser/extensions/extensions_quota_service_unittest.cc b/chrome/browser/extensions/extensions_quota_service_unittest.cc
new file mode 100644
index 0000000..fae8d41
--- /dev/null
+++ b/chrome/browser/extensions/extensions_quota_service_unittest.cc
@@ -0,0 +1,308 @@
+// Copyright (c) 2009 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 "base/stl_util-inl.h"
+#include "base/string_util.h"
+#include "chrome/browser/extensions/extension_function.h"
+#include "chrome/browser/extensions/extensions_quota_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::TimeDelta;
+using base::TimeTicks;
+
+typedef QuotaLimitHeuristic::Bucket Bucket;
+typedef QuotaLimitHeuristic::Config Config;
+typedef QuotaLimitHeuristic::BucketList BucketList;
+typedef ExtensionsQuotaService::TimedLimit TimedLimit;
+typedef ExtensionsQuotaService::SustainedLimit SustainedLimit;
+
+static const Config kFrozenConfig = { 0, TimeDelta::FromDays(0) };
+static const Config k2PerMinute = { 2, TimeDelta::FromMinutes(1) };
+static const Config k20PerHour = { 20, TimeDelta::FromHours(1) };
+static const TimeTicks kStartTime = TimeTicks();
+static const TimeTicks k1MinuteAfterStart =
+ kStartTime + TimeDelta::FromMinutes(1);
+
+namespace {
+class Mapper : public QuotaLimitHeuristic::BucketMapper {
+ public:
+ Mapper() {}
+ virtual ~Mapper() { STLDeleteValues(&buckets_); }
+ virtual void GetBucketsForArgs(const Value* args, BucketList* buckets) {
+ const ListValue* v = static_cast<const ListValue*>(args);
+ for (size_t i = 0; i < v->GetSize(); i++) {
+ int id;
+ ASSERT_TRUE(v->GetInteger(i, &id));
+ Bucket* bucket = buckets_[id];
+ if (bucket == NULL) {
+ bucket = new Bucket();
+ buckets_[id] = bucket;
+ }
+ buckets->push_back(bucket);
+ }
+ }
+ private:
+ typedef std::map<int, Bucket*> BucketMap;
+ BucketMap buckets_;
+ DISALLOW_COPY_AND_ASSIGN(Mapper);
+};
+
+class MockMapper : public QuotaLimitHeuristic::BucketMapper {
+ public:
+ virtual void GetBucketsForArgs(const Value* args, BucketList* buckets) {}
+};
+
+class MockFunction : public ExtensionFunction {
+ public:
+ explicit MockFunction(const std::string& name) { set_name(name); }
+ virtual void SetArgs(const Value* args) {}
+ virtual const std::string GetError() { return std::string(); }
+ virtual const std::string GetResult() { return std::string(); }
+ virtual void Run() {}
+};
+
+class TimedLimitMockFunction : public MockFunction {
+ public:
+ explicit TimedLimitMockFunction(const std::string& name)
+ : MockFunction(name) {}
+ virtual void GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ heuristics->push_back(new TimedLimit(k2PerMinute, new Mapper()));
+ }
+};
+
+class ChainedLimitsMockFunction : public MockFunction {
+ public:
+ explicit ChainedLimitsMockFunction(const std::string& name)
+ : MockFunction(name) {}
+ virtual void GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ // No more than 2 per minute sustained over 5 minutes.
+ heuristics->push_back(new SustainedLimit(TimeDelta::FromMinutes(5),
+ k2PerMinute, new Mapper()));
+ // No more than 20 per hour.
+ heuristics->push_back(new TimedLimit(k20PerHour, new Mapper()));
+ }
+};
+
+class FrozenMockFunction : public MockFunction {
+ public:
+ explicit FrozenMockFunction(const std::string& name) : MockFunction(name) {}
+ virtual void GetQuotaLimitHeuristics(
+ QuotaLimitHeuristics* heuristics) const {
+ heuristics->push_back(new TimedLimit(kFrozenConfig, new Mapper()));
+ }
+};
+} // namespace
+
+class ExtensionsQuotaServiceTest : public testing::Test {
+ public:
+ ExtensionsQuotaServiceTest()
+ : extension_a_("a"), extension_b_("b"), extension_c_("c") {}
+ virtual void SetUp() {
+ service_.reset(new ExtensionsQuotaService());
+ }
+ virtual void TearDown() {
+ service_.reset();
+ }
+ protected:
+ std::string extension_a_;
+ std::string extension_b_;
+ std::string extension_c_;
+ scoped_ptr<ExtensionsQuotaService> service_;
+};
+
+class QuotaLimitHeuristicTest : public testing::Test {
+ public:
+ static void DoMoreThan2PerMinuteFor5Minutes(const TimeTicks& start_time,
+ QuotaLimitHeuristic* lim,
+ Bucket* b,
+ int an_unexhausted_minute) {
+ for (int i = 0; i < 5; i++) {
+ // Perform one operation in each minute.
+ int m = i * 60;
+ EXPECT_TRUE(lim->Apply(b, start_time + TimeDelta::FromSeconds(10 + m)));
+ EXPECT_TRUE(b->has_tokens());
+
+ if (i == an_unexhausted_minute)
+ continue; // Don't exhaust all tokens this minute.
+
+ EXPECT_TRUE(lim->Apply(b, start_time + TimeDelta::FromSeconds(15 + m)));
+ EXPECT_FALSE(b->has_tokens());
+
+ // These are OK because we haven't exhausted all buckets.
+ EXPECT_TRUE(lim->Apply(b, start_time + TimeDelta::FromSeconds(20 + m)));
+ EXPECT_FALSE(b->has_tokens());
+ EXPECT_TRUE(lim->Apply(b, start_time + TimeDelta::FromSeconds(50 + m)));
+ EXPECT_FALSE(b->has_tokens());
+ }
+ }
+};
+
+TEST_F(QuotaLimitHeuristicTest, Timed) {
+ TimedLimit lim(k2PerMinute, new MockMapper());
+ Bucket b;
+
+ b.Reset(k2PerMinute, kStartTime);
+ EXPECT_TRUE(lim.Apply(&b, kStartTime));
+ EXPECT_TRUE(b.has_tokens());
+ EXPECT_TRUE(lim.Apply(&b, kStartTime + TimeDelta::FromSeconds(30)));
+ EXPECT_FALSE(b.has_tokens());
+ EXPECT_FALSE(lim.Apply(&b, k1MinuteAfterStart));
+
+ b.Reset(k2PerMinute, kStartTime);
+ EXPECT_TRUE(lim.Apply(&b, k1MinuteAfterStart - TimeDelta::FromSeconds(1)));
+ EXPECT_TRUE(lim.Apply(&b, k1MinuteAfterStart));
+ EXPECT_TRUE(lim.Apply(&b, k1MinuteAfterStart + TimeDelta::FromSeconds(1)));
+ EXPECT_TRUE(lim.Apply(&b, k1MinuteAfterStart + TimeDelta::FromSeconds(2)));
+ EXPECT_FALSE(lim.Apply(&b, k1MinuteAfterStart + TimeDelta::FromSeconds(3)));
+}
+
+TEST_F(QuotaLimitHeuristicTest, Sustained) {
+ SustainedLimit lim(TimeDelta::FromMinutes(5), k2PerMinute, new MockMapper());
+ Bucket bucket;
+
+ bucket.Reset(k2PerMinute, kStartTime);
+ DoMoreThan2PerMinuteFor5Minutes(kStartTime, &lim, &bucket, -1);
+ // This straw breaks the camel's back.
+ EXPECT_FALSE(lim.Apply(&bucket, kStartTime + TimeDelta::FromMinutes(6)));
+
+ // The heuristic resets itself on a safe request.
+ EXPECT_TRUE(lim.Apply(&bucket, kStartTime + TimeDelta::FromDays(1)));
+
+ // Do the same as above except don't exhaust final bucket.
+ bucket.Reset(k2PerMinute, kStartTime);
+ DoMoreThan2PerMinuteFor5Minutes(kStartTime, &lim, &bucket, -1);
+ EXPECT_TRUE(lim.Apply(&bucket, kStartTime + TimeDelta::FromMinutes(7)));
+
+ // Do the same as above except don't exhaust the 3rd (w.l.o.g) bucket.
+ bucket.Reset(k2PerMinute, kStartTime);
+ DoMoreThan2PerMinuteFor5Minutes(kStartTime, &lim, &bucket, 3);
+ // If the 3rd bucket were exhausted, this would fail (see first test).
+ EXPECT_TRUE(lim.Apply(&bucket, kStartTime + TimeDelta::FromMinutes(6)));
+}
+
+TEST_F(ExtensionsQuotaServiceTest, NoHeuristic) {
+ scoped_refptr<MockFunction> f(new MockFunction("foo"));
+ ListValue args;
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &args, kStartTime));
+}
+
+TEST_F(ExtensionsQuotaServiceTest, FrozenHeuristic) {
+ scoped_refptr<MockFunction> f(new FrozenMockFunction("foo"));
+ ListValue args;
+ args.Append(new FundamentalValue(1));
+ EXPECT_FALSE(service_->Assess(extension_a_, f, &args, kStartTime));
+}
+
+TEST_F(ExtensionsQuotaServiceTest, SingleHeuristic) {
+ scoped_refptr<MockFunction> f(new TimedLimitMockFunction("foo"));
+ ListValue args;
+ args.Append(new FundamentalValue(1));
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &args, kStartTime));
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(10)));
+ EXPECT_FALSE(service_->Assess(extension_a_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(15)));
+
+ ListValue args2;
+ args2.Append(new FundamentalValue(1));
+ args2.Append(new FundamentalValue(2));
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args2, kStartTime));
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args2,
+ kStartTime + TimeDelta::FromSeconds(10)));
+
+ TimeDelta peace = TimeDelta::FromMinutes(30);
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args, kStartTime + peace));
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args,
+ kStartTime + peace + TimeDelta::FromSeconds(10)));
+ EXPECT_FALSE(service_->Assess(extension_b_, f, &args2,
+ kStartTime + peace + TimeDelta::FromSeconds(15)));
+
+ // Test that items are independent.
+ ListValue args3;
+ args3.Append(new FundamentalValue(3));
+ EXPECT_TRUE(service_->Assess(extension_c_, f, &args, kStartTime));
+ EXPECT_TRUE(service_->Assess(extension_c_, f, &args3,
+ kStartTime + TimeDelta::FromSeconds(10)));
+ EXPECT_TRUE(service_->Assess(extension_c_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(15)));
+ EXPECT_TRUE(service_->Assess(extension_c_, f, &args3,
+ kStartTime + TimeDelta::FromSeconds(20)));
+ EXPECT_FALSE(service_->Assess(extension_c_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(25)));
+ EXPECT_FALSE(service_->Assess(extension_c_, f, &args3,
+ kStartTime + TimeDelta::FromSeconds(30)));
+}
+
+TEST_F(ExtensionsQuotaServiceTest, ChainedHeuristics) {
+ scoped_refptr<MockFunction> f(new ChainedLimitsMockFunction("foo"));
+ ListValue args;
+ args.Append(new FundamentalValue(1));
+
+ // First, test that the low limit can be avoided but the higher one is hit.
+ // One event per minute for 20 minutes comes in under the sustained limit,
+ // but is equal to the timed limit.
+ for (int i = 0; i < 20; i++) {
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(10 + i * 60)));
+ }
+
+ // This will bring us to 21 events in an hour, which is a violation.
+ EXPECT_FALSE(service_->Assess(extension_a_, f, &args,
+ kStartTime + TimeDelta::FromMinutes(30)));
+
+ // Now, check that we can still hit the lower limit.
+ for (int i = 0; i < 5; i++) {
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(10 + i * 60)));
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(15 + i * 60)));
+ EXPECT_TRUE(service_->Assess(extension_b_, f, &args,
+ kStartTime + TimeDelta::FromSeconds(20 + i * 60)));
+ }
+
+ EXPECT_FALSE(service_->Assess(extension_b_, f, &args,
+ kStartTime + TimeDelta::FromMinutes(6)));
+}
+
+TEST_F(ExtensionsQuotaServiceTest, MultipleFunctionsDontInterfere) {
+ scoped_refptr<MockFunction> f(new TimedLimitMockFunction("foo"));
+ scoped_refptr<MockFunction> g(new TimedLimitMockFunction("bar"));
+
+ ListValue args_f;
+ ListValue args_g;
+ args_f.Append(new FundamentalValue(1));
+ args_g.Append(new FundamentalValue(2));
+
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &args_f, kStartTime));
+ EXPECT_TRUE(service_->Assess(extension_a_, g, &args_g, kStartTime));
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &args_f,
+ kStartTime + TimeDelta::FromSeconds(10)));
+ EXPECT_TRUE(service_->Assess(extension_a_, g, &args_g,
+ kStartTime + TimeDelta::FromSeconds(10)));
+ EXPECT_FALSE(service_->Assess(extension_a_, f, &args_f,
+ kStartTime + TimeDelta::FromSeconds(15)));
+ EXPECT_FALSE(service_->Assess(extension_a_, g, &args_g,
+ kStartTime + TimeDelta::FromSeconds(15)));
+}
+
+TEST_F(ExtensionsQuotaServiceTest, ViolatorsWillBeViolators) {
+ scoped_refptr<MockFunction> f(new TimedLimitMockFunction("foo"));
+ scoped_refptr<MockFunction> g(new TimedLimitMockFunction("bar"));
+ ListValue arg;
+ arg.Append(new FundamentalValue(1));
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &arg, kStartTime));
+ EXPECT_TRUE(service_->Assess(extension_a_, f, &arg,
+ kStartTime + TimeDelta::FromSeconds(10)));
+ EXPECT_FALSE(service_->Assess(extension_a_, f, &arg,
+ kStartTime + TimeDelta::FromSeconds(15)));
+
+ // We don't allow this extension to use quota limited functions even if they
+ // wait a while.
+ EXPECT_FALSE(service_->Assess(extension_a_, f, &arg,
+ kStartTime + TimeDelta::FromDays(1)));
+ EXPECT_FALSE(service_->Assess(extension_a_, g, &arg,
+ kStartTime + TimeDelta::FromDays(1)));
+}
diff --git a/chrome/browser/extensions/extensions_service.h b/chrome/browser/extensions/extensions_service.h
index 910a396..5b0252d 100644
--- a/chrome/browser/extensions/extensions_service.h
+++ b/chrome/browser/extensions/extensions_service.h
@@ -20,6 +20,7 @@
#include "chrome/browser/chrome_thread.h"
#include "chrome/browser/extensions/extension_prefs.h"
#include "chrome/browser/extensions/extension_process_manager.h"
+#include "chrome/browser/extensions/extensions_quota_service.h"
#include "chrome/browser/extensions/external_extension_provider.h"
#include "chrome/browser/extensions/sandboxed_extension_unpacker.h"
#include "chrome/browser/privacy_blacklist/blacklist_manager.h"
@@ -217,6 +218,8 @@ class ExtensionsService
// Note that this may return NULL if autoupdate is not turned on.
ExtensionUpdater* updater() { return updater_.get(); }
+ ExtensionsQuotaService* quota_service() { return &quota_service_; }
+
// Notify the frontend that there was an error loading an extension.
// This method is public because ExtensionsServiceBackend can post to here.
void ReportExtensionLoadError(const FilePath& extension_path,
@@ -285,6 +288,9 @@ class ExtensionsService
// The backend that will do IO on behalf of this instance.
scoped_refptr<ExtensionsServiceBackend> backend_;
+ // Used by dispatchers to limit API quota for individual extensions.
+ ExtensionsQuotaService quota_service_;
+
// Is the service ready to go?
bool ready_;