Commit 755211fe authored by kalman@chromium.org's avatar kalman@chromium.org

Refactor guest view availability to be API based not permission based.

This is a step towards allowing WebUI to directly embed guest views. 

BUG=386838
R=fsamuel@chromium.org

Review URL: https://codereview.chromium.org/426593007

Cr-Commit-Position: refs/heads/master@{#288407}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@288407 0039d316-1c4b-4281-b951-d872f2087c98
parent 14522ce4
...@@ -35,7 +35,7 @@ bool GuestViewInternalCreateGuestFunction::RunAsync() { ...@@ -35,7 +35,7 @@ bool GuestViewInternalCreateGuestFunction::RunAsync() {
this); this);
guest_view_manager->CreateGuest(view_type, guest_view_manager->CreateGuest(view_type,
extension_id(), extension_id(),
render_view_host()->GetProcess()->GetID(), GetAssociatedWebContents(),
*create_params, *create_params,
callback); callback);
......
...@@ -6,6 +6,10 @@ ...@@ -6,6 +6,10 @@
namespace appview { namespace appview {
// API namespace for the embedder.
const char kEmbedderAPINamespace[] = "appViewEmbedderInternal";
// Parameters/properties on events.
const char kAppID[] = "appId"; const char kAppID[] = "appId";
const char kEmbedderID[] ="embedderId"; const char kEmbedderID[] ="embedderId";
const char kGuestInstanceID[] = "guestInstanceId"; const char kGuestInstanceID[] = "guestInstanceId";
......
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
namespace appview { namespace appview {
// API namespace for the *embedder*. The embedder and guest use different APIs.
extern const char kEmbedderAPINamespace[];
// Parameters/properties on events. // Parameters/properties on events.
extern const char kAppID[]; extern const char kAppID[];
extern const char kEmbedderID[]; extern const char kEmbedderID[];
......
...@@ -17,13 +17,11 @@ ...@@ -17,13 +17,11 @@
#include "extensions/browser/api/app_runtime/app_runtime_api.h" #include "extensions/browser/api/app_runtime/app_runtime_api.h"
#include "extensions/browser/event_router.h" #include "extensions/browser/event_router.h"
#include "extensions/browser/extension_host.h" #include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h" #include "extensions/browser/extension_system.h"
#include "extensions/browser/lazy_background_task_queue.h" #include "extensions/browser/lazy_background_task_queue.h"
#include "extensions/browser/view_type_utils.h" #include "extensions/browser/view_type_utils.h"
#include "extensions/common/api/app_runtime.h" #include "extensions/common/api/app_runtime.h"
#include "extensions/common/extension_messages.h" #include "extensions/common/extension_messages.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ipc/ipc_message_macros.h" #include "ipc/ipc_message_macros.h"
namespace app_runtime = extensions::core_api::app_runtime; namespace app_runtime = extensions::core_api::app_runtime;
...@@ -137,16 +135,8 @@ bool AppViewGuest::HandleContextMenu(const content::ContextMenuParams& params) { ...@@ -137,16 +135,8 @@ bool AppViewGuest::HandleContextMenu(const content::ContextMenuParams& params) {
return true; return true;
} }
bool AppViewGuest::CanEmbedderUseGuestView( const char* AppViewGuest::GetAPINamespace() {
const std::string& embedder_extension_id) { return appview::kEmbedderAPINamespace;
const extensions::Extension* embedder_extension =
extensions::ExtensionRegistry::Get(browser_context())
->enabled_extensions()
.GetByID(embedder_extension_id);
if (!embedder_extension)
return false;
return embedder_extension->permissions_data()->HasAPIPermission(
extensions::APIPermission::kAppView);
} }
void AppViewGuest::CreateWebContents( void AppViewGuest::CreateWebContents(
......
...@@ -47,8 +47,7 @@ class AppViewGuest : public GuestView<AppViewGuest>, ...@@ -47,8 +47,7 @@ class AppViewGuest : public GuestView<AppViewGuest>,
const content::ContextMenuParams& params) OVERRIDE; const content::ContextMenuParams& params) OVERRIDE;
// GuestViewBase implementation. // GuestViewBase implementation.
virtual bool CanEmbedderUseGuestView( virtual const char* GetAPINamespace() OVERRIDE;
const std::string& embedder_extension_id) OVERRIDE;
virtual void CreateWebContents( virtual void CreateWebContents(
const std::string& embedder_extension_id, const std::string& embedder_extension_id,
int embedder_render_process_id, int embedder_render_process_id,
......
...@@ -6,6 +6,9 @@ ...@@ -6,6 +6,9 @@
namespace extensionoptions { namespace extensionoptions {
// API namespace.
extern const char kAPINamespace[] = "extensionOptionsInternal";
// Attributes. // Attributes.
const char kAttributeAutoSize[] = "autosize"; const char kAttributeAutoSize[] = "autosize";
const char kAttributeMaxHeight[] = "maxheight"; const char kAttributeMaxHeight[] = "maxheight";
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
namespace extensionoptions { namespace extensionoptions {
// API namespace.
extern const char kAPINamespace[];
// Attributes. // Attributes.
extern const char kAttributeAutoSize[]; extern const char kAttributeAutoSize[];
extern const char kAttributeMaxHeight[]; extern const char kAttributeMaxHeight[];
......
...@@ -36,16 +36,8 @@ ExtensionOptionsGuest::ExtensionOptionsGuest( ...@@ -36,16 +36,8 @@ ExtensionOptionsGuest::ExtensionOptionsGuest(
ExtensionOptionsGuest::~ExtensionOptionsGuest() { ExtensionOptionsGuest::~ExtensionOptionsGuest() {
} }
bool ExtensionOptionsGuest::CanEmbedderUseGuestView( const char* ExtensionOptionsGuest::GetAPINamespace() {
const std::string& embedder_extension_id) { return extensionoptions::kAPINamespace;
const extensions::Extension* embedder_extension =
extensions::ExtensionRegistry::Get(browser_context())
->enabled_extensions()
.GetByID(embedder_extension_id);
if (!embedder_extension)
return false;
return embedder_extension->permissions_data()->HasAPIPermission(
extensions::APIPermission::kEmbeddedExtensionOptions);
} }
// static // static
......
...@@ -23,8 +23,7 @@ class ExtensionOptionsGuest ...@@ -23,8 +23,7 @@ class ExtensionOptionsGuest
int guest_instance_id); int guest_instance_id);
// GuestViewBase implementation. // GuestViewBase implementation.
virtual bool CanEmbedderUseGuestView( virtual const char* GetAPINamespace() OVERRIDE;
const std::string& embedder_extension_id) OVERRIDE;
virtual void CreateWebContents( virtual void CreateWebContents(
const std::string& embedder_extension_id, const std::string& embedder_extension_id,
int embedder_render_process_id, int embedder_render_process_id,
......
...@@ -18,6 +18,10 @@ ...@@ -18,6 +18,10 @@
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h" #include "content/public/common/url_constants.h"
#include "extensions/browser/event_router.h" #include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/process_map.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_provider.h"
#include "third_party/WebKit/public/web/WebInputEvent.h" #include "third_party/WebKit/public/web/WebInputEvent.h"
using content::WebContents; using content::WebContents;
...@@ -97,27 +101,48 @@ GuestViewBase::GuestViewBase(content::BrowserContext* browser_context, ...@@ -97,27 +101,48 @@ GuestViewBase::GuestViewBase(content::BrowserContext* browser_context,
weak_ptr_factory_(this) { weak_ptr_factory_(this) {
} }
void GuestViewBase::Init( void GuestViewBase::Init(const std::string& embedder_extension_id,
const std::string& embedder_extension_id, content::WebContents* embedder_web_contents,
int embedder_render_process_id, const base::DictionaryValue& create_params,
const base::DictionaryValue& create_params, const WebContentsCreatedCallback& callback) {
const WebContentsCreatedCallback& callback) {
if (initialized_) if (initialized_)
return; return;
initialized_ = true; initialized_ = true;
if (!CanEmbedderUseGuestView(embedder_extension_id)) { extensions::Feature* feature =
extensions::FeatureProvider::GetAPIFeatures()->GetFeature(
GetAPINamespace());
CHECK(feature);
extensions::ProcessMap* process_map =
extensions::ProcessMap::Get(browser_context());
CHECK(process_map);
const extensions::Extension* embedder_extension =
extensions::ExtensionRegistry::Get(browser_context_)
->enabled_extensions()
.GetByID(embedder_extension_id);
int embedder_process_id =
embedder_web_contents->GetRenderProcessHost()->GetID();
extensions::Feature::Availability availability =
feature->IsAvailableToContext(
embedder_extension,
process_map->GetMostLikelyContextType(embedder_extension,
embedder_process_id),
embedder_web_contents->GetLastCommittedURL());
if (!availability.is_available()) {
callback.Run(NULL); callback.Run(NULL);
return; return;
} }
CreateWebContents(embedder_extension_id, CreateWebContents(embedder_extension_id,
embedder_render_process_id, embedder_process_id,
create_params, create_params,
base::Bind(&GuestViewBase::CompleteInit, base::Bind(&GuestViewBase::CompleteInit,
AsWeakPtr(), AsWeakPtr(),
embedder_extension_id, embedder_extension_id,
embedder_render_process_id, embedder_process_id,
callback)); callback));
} }
......
...@@ -141,11 +141,13 @@ class GuestViewBase : public content::BrowserPluginGuestDelegate, ...@@ -141,11 +141,13 @@ class GuestViewBase : public content::BrowserPluginGuestDelegate,
// to destruction. // to destruction.
virtual void WillDestroy() {} virtual void WillDestroy() {}
// This method is to be implemented by the derived class. It determines // This method is to be implemented by the derived class. Access to guest
// whether the guest view type of the derived class can be used by the // views are determined by the availability of the internal extension API
// provided embedder extension ID. // used to implement the guest view.
virtual bool CanEmbedderUseGuestView( //
const std::string& embedder_extension_id) = 0; // This should be the name of the API as it appears in the _api_features.json
// file.
virtual const char* GetAPINamespace() = 0;
// This method is to be implemented by the derived class. Given a set of // This method is to be implemented by the derived class. Given a set of
// initialization parameters, a concrete subclass of GuestViewBase can // initialization parameters, a concrete subclass of GuestViewBase can
...@@ -161,7 +163,7 @@ class GuestViewBase : public content::BrowserPluginGuestDelegate, ...@@ -161,7 +163,7 @@ class GuestViewBase : public content::BrowserPluginGuestDelegate,
// This creates a WebContents and initializes |this| GuestViewBase to use the // This creates a WebContents and initializes |this| GuestViewBase to use the
// newly created WebContents. // newly created WebContents.
void Init(const std::string& embedder_extension_id, void Init(const std::string& embedder_extension_id,
int embedder_render_process_id, content::WebContents* embedder_web_contents,
const base::DictionaryValue& create_params, const base::DictionaryValue& create_params,
const WebContentsCreatedCallback& callback); const WebContentsCreatedCallback& callback);
......
...@@ -62,12 +62,11 @@ int GuestViewManager::GetNextInstanceID() { ...@@ -62,12 +62,11 @@ int GuestViewManager::GetNextInstanceID() {
return ++current_instance_id_; return ++current_instance_id_;
} }
void GuestViewManager::CreateGuest( void GuestViewManager::CreateGuest(const std::string& view_type,
const std::string& view_type, const std::string& embedder_extension_id,
const std::string& embedder_extension_id, content::WebContents* embedder_web_contents,
int embedder_render_process_id, const base::DictionaryValue& create_params,
const base::DictionaryValue& create_params, const WebContentsCreatedCallback& callback) {
const WebContentsCreatedCallback& callback) {
int guest_instance_id = GetNextInstanceID(); int guest_instance_id = GetNextInstanceID();
GuestViewBase* guest = GuestViewBase* guest =
GuestViewBase::Create(context_, guest_instance_id, view_type); GuestViewBase::Create(context_, guest_instance_id, view_type);
...@@ -75,10 +74,8 @@ void GuestViewManager::CreateGuest( ...@@ -75,10 +74,8 @@ void GuestViewManager::CreateGuest(
callback.Run(NULL); callback.Run(NULL);
return; return;
} }
guest->Init(embedder_extension_id, guest->Init(
embedder_render_process_id, embedder_extension_id, embedder_web_contents, create_params, callback);
create_params,
callback);
} }
content::WebContents* GuestViewManager::CreateGuestWithWebContentsParams( content::WebContents* GuestViewManager::CreateGuestWithWebContentsParams(
......
...@@ -20,6 +20,7 @@ class GURL; ...@@ -20,6 +20,7 @@ class GURL;
namespace content { namespace content {
class BrowserContext; class BrowserContext;
class WebContents;
} // namespace content } // namespace content
class GuestViewManager : public content::BrowserPluginGuestManager, class GuestViewManager : public content::BrowserPluginGuestManager,
...@@ -48,12 +49,11 @@ class GuestViewManager : public content::BrowserPluginGuestManager, ...@@ -48,12 +49,11 @@ class GuestViewManager : public content::BrowserPluginGuestManager,
typedef base::Callback<void(content::WebContents*)> typedef base::Callback<void(content::WebContents*)>
WebContentsCreatedCallback; WebContentsCreatedCallback;
void CreateGuest( void CreateGuest(const std::string& view_type,
const std::string& view_type, const std::string& embedder_extension_id,
const std::string& embedder_extension_id, content::WebContents* embedder_web_contents,
int embedder_render_process_id, const base::DictionaryValue& create_params,
const base::DictionaryValue& create_params, const WebContentsCreatedCallback& callback);
const WebContentsCreatedCallback& callback);
content::WebContents* CreateGuestWithWebContentsParams( content::WebContents* CreateGuestWithWebContentsParams(
const std::string& view_type, const std::string& view_type,
......
...@@ -13,6 +13,9 @@ const char kAttributeMaxWidth[] = "maxwidth"; ...@@ -13,6 +13,9 @@ const char kAttributeMaxWidth[] = "maxwidth";
const char kAttributeMinHeight[] = "minheight"; const char kAttributeMinHeight[] = "minheight";
const char kAttributeMinWidth[] = "minwidth"; const char kAttributeMinWidth[] = "minwidth";
// API namespace.
const char kAPINamespace[] = "webViewInternal";
// Events. // Events.
const char kEventClose[] = "webViewInternal.onClose"; const char kEventClose[] = "webViewInternal.onClose";
const char kEventConsoleMessage[] = "webViewInternal.onConsoleMessage"; const char kEventConsoleMessage[] = "webViewInternal.onConsoleMessage";
......
...@@ -16,6 +16,10 @@ extern const char kAttributeMaxWidth[]; ...@@ -16,6 +16,10 @@ extern const char kAttributeMaxWidth[];
extern const char kAttributeMinHeight[]; extern const char kAttributeMinHeight[];
extern const char kAttributeMinWidth[]; extern const char kAttributeMinWidth[];
// API namespace.
// TODO(kalman): Consolidate this with the other API constants.
extern const char kAPINamespace[];
// Events. // Events.
extern const char kEventClose[]; extern const char kEventClose[];
extern const char kEventConsoleMessage[]; extern const char kEventConsoleMessage[];
......
...@@ -48,10 +48,8 @@ ...@@ -48,10 +48,8 @@
#include "content/public/common/result_codes.h" #include "content/public/common/result_codes.h"
#include "content/public/common/stop_find_action.h" #include "content/public/common/stop_find_action.h"
#include "content/public/common/url_constants.h" #include "content/public/common/url_constants.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h" #include "extensions/browser/extension_system.h"
#include "extensions/common/constants.h" #include "extensions/common/constants.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ipc/ipc_message_macros.h" #include "ipc/ipc_message_macros.h"
#include "net/base/escape.h" #include "net/base/escape.h"
#include "net/base/net_errors.h" #include "net/base/net_errors.h"
...@@ -226,16 +224,8 @@ scoped_ptr<base::ListValue> WebViewGuest::MenuModelToValue( ...@@ -226,16 +224,8 @@ scoped_ptr<base::ListValue> WebViewGuest::MenuModelToValue(
return items.Pass(); return items.Pass();
} }
bool WebViewGuest::CanEmbedderUseGuestView( const char* WebViewGuest::GetAPINamespace() {
const std::string& embedder_extension_id) { return webview::kAPINamespace;
const extensions::Extension* embedder_extension =
extensions::ExtensionRegistry::Get(browser_context())
->enabled_extensions()
.GetByID(embedder_extension_id);
if (!embedder_extension)
return false;
return embedder_extension->permissions_data()->HasAPIPermission(
extensions::APIPermission::kWebView);
} }
void WebViewGuest::CreateWebContents( void WebViewGuest::CreateWebContents(
...@@ -570,13 +560,13 @@ void WebViewGuest::CreateNewGuestWebViewWindow( ...@@ -570,13 +560,13 @@ void WebViewGuest::CreateNewGuestWebViewWindow(
base::DictionaryValue create_params; base::DictionaryValue create_params;
create_params.SetString(webview::kStoragePartitionId, storage_partition_id); create_params.SetString(webview::kStoragePartitionId, storage_partition_id);
guest_manager->CreateGuest( guest_manager->CreateGuest(WebViewGuest::Type,
WebViewGuest::Type, embedder_extension_id(),
embedder_extension_id(), embedder_web_contents(),
embedder_web_contents()->GetRenderProcessHost()->GetID(), create_params,
create_params, base::Bind(&WebViewGuest::NewGuestWebViewCallback,
base::Bind(&WebViewGuest::NewGuestWebViewCallback, base::Unretained(this),
base::Unretained(this), params)); params));
} }
void WebViewGuest::NewGuestWebViewCallback( void WebViewGuest::NewGuestWebViewCallback(
......
...@@ -83,8 +83,7 @@ class WebViewGuest : public GuestView<WebViewGuest>, ...@@ -83,8 +83,7 @@ class WebViewGuest : public GuestView<WebViewGuest>,
void SetZoom(double zoom_factor); void SetZoom(double zoom_factor);
// GuestViewBase implementation. // GuestViewBase implementation.
virtual bool CanEmbedderUseGuestView( virtual const char* GetAPINamespace() OVERRIDE;
const std::string& embedder_extension_id) OVERRIDE;
virtual void CreateWebContents( virtual void CreateWebContents(
const std::string& embedder_extension_id, const std::string& embedder_extension_id,
int embedder_render_process_id, int embedder_render_process_id,
......
...@@ -56,6 +56,14 @@ ...@@ -56,6 +56,14 @@
// Any webpage can use the app API. // Any webpage can use the app API.
"matches": ["<all_urls>"] "matches": ["<all_urls>"]
}, },
// The API for the *embedder* of appview. Appview has both an embedder and
// guest API, which are different.
"appViewEmbedderInternal": {
"internal": true,
"contexts": ["blessed_extension"],
"dependencies": ["permission:appview"]
},
// TODO(fsamuel,kalman): Rename this appViewGuestInternal.
"appViewInternal": { "appViewInternal": {
"internal": true, "internal": true,
"channel": "dev", "channel": "dev",
......
...@@ -539,7 +539,8 @@ void EventRouter::DispatchEventToProcess(const std::string& extension_id, ...@@ -539,7 +539,8 @@ void EventRouter::DispatchEventToProcess(const std::string& extension_id,
BrowserContext* listener_context = process->GetBrowserContext(); BrowserContext* listener_context = process->GetBrowserContext();
ProcessMap* process_map = ProcessMap::Get(listener_context); ProcessMap* process_map = ProcessMap::Get(listener_context);
// TODO(kalman): Convert this method to use ProcessMap::GuessContextType. // TODO(kalman): Convert this method to use
// ProcessMap::GetMostLikelyContextType.
const Extension* extension = const Extension* extension =
ExtensionRegistry::Get(browser_context_)->enabled_extensions().GetByID( ExtensionRegistry::Get(browser_context_)->enabled_extensions().GetByID(
......
...@@ -455,7 +455,7 @@ ExtensionFunction* ExtensionFunctionDispatcher::CreateExtensionFunction( ...@@ -455,7 +455,7 @@ ExtensionFunction* ExtensionFunctionDispatcher::CreateExtensionFunction(
function->set_response_callback(callback); function->set_response_callback(callback);
function->set_source_tab_id(params.source_tab_id); function->set_source_tab_id(params.source_tab_id);
function->set_source_context_type( function->set_source_context_type(
process_map.GuessContextType(extension, requesting_process_id)); process_map.GetMostLikelyContextType(extension, requesting_process_id));
return function; return function;
} }
......
...@@ -123,8 +123,9 @@ std::set<std::string> ProcessMap::GetExtensionsInProcess(int process_id) const { ...@@ -123,8 +123,9 @@ std::set<std::string> ProcessMap::GetExtensionsInProcess(int process_id) const {
return result; return result;
} }
Feature::Context ProcessMap::GuessContextType(const Extension* extension, Feature::Context ProcessMap::GetMostLikelyContextType(
int process_id) const { const Extension* extension,
int process_id) const {
// WARNING: This logic must match Dispatcher::ClassifyJavaScriptContext, as // WARNING: This logic must match Dispatcher::ClassifyJavaScriptContext, as
// much as possible. // much as possible.
......
...@@ -95,24 +95,36 @@ class ProcessMap : public KeyedService { ...@@ -95,24 +95,36 @@ class ProcessMap : public KeyedService {
std::set<std::string> GetExtensionsInProcess(int process_id) const; std::set<std::string> GetExtensionsInProcess(int process_id) const;
// Guesses the most permissive context type for the process with ID // Gets the most likely context type for the process with ID |process_id|
// |process_id|. Context types are renderer (JavaScript) concepts but the // which hosts Extension |extension|, if any (may be NULL). Context types are
// browser can do a decent job in guessing what the process hosts. // renderer (JavaScript) concepts but the browser can do a decent job in
// guessing what the process hosts.
// //
// |extension| is the funky part - unfortunately we need to trust the
// caller of this method to be correct that indeed the context does feature
// an extension. This matters for iframes, where an extension could be
// hosted in another extension's process (privilege level needs to be
// downgraded) or in a web page's process (privilege level needs to be
// upgraded).
//
// The latter of these is slightly problematic from a security perspective;
// if a web page renderer gets owned it could try to pretend it's an
// extension and get access to some unprivileged APIs. Luckly, when OOP
// iframes lauch, it won't be an issue.
//
// Anyhow, the expected behaviour is:
// - For hosted app processes, this will be blessed_web_page. // - For hosted app processes, this will be blessed_web_page.
// - For other extension processes, this will be blessed_extension. // - For other extension processes, this will be blessed_extension.
// - For WebUI processes, this will be a webui. // - For WebUI processes, this will be a webui.
// - For anything else we have the choice of unblessed_extension or // - For any other extension we have the choice of unblessed_extension or
// content_script. Since content scripts are more common, guess that. // content_script. Since content scripts are more common, guess that.
// We *could* in theory track which web processes have extension frames // We *could* in theory track which web processes have extension frames
// in them, and those would be unblessed_extension, but we don't at the // in them, and those would be unblessed_extension, but we don't at the
// moment, and once OOP iframes exist then there won't even be such a // moment, and once OOP iframes exist then there won't even be such a
// thing as an unblessed_extension context. // thing as an unblessed_extension context.
// // - For anything else, web_page.
// |extension| isn't used to upgrade the process trust level, but rather used Feature::Context GetMostLikelyContextType(const Extension* extension,
// as a tiebreaker if a process is found to contain multiple extensions. int process_id) const;
Feature::Context GuessContextType(const Extension* extension,
int process_id) const;
private: private:
struct Item; struct Item;
......
...@@ -1174,7 +1174,7 @@ Feature::Context Dispatcher::ClassifyJavaScriptContext( ...@@ -1174,7 +1174,7 @@ Feature::Context Dispatcher::ClassifyJavaScriptContext(
int extension_group, int extension_group,
const GURL& url, const GURL& url,
const blink::WebSecurityOrigin& origin) { const blink::WebSecurityOrigin& origin) {
// WARNING: This logic must match ProcessMap::GuessContextType, as much as // WARNING: This logic must match ProcessMap::GetContextType, as much as
// possible. // possible.
DCHECK_GE(extension_group, 0); DCHECK_GE(extension_group, 0);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment