OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/extensions/api/gcd_private/gcd_private_api.h" | 5 #include "chrome/browser/extensions/api/gcd_private/gcd_private_api.h" |
6 | 6 |
7 #include "base/lazy_instance.h" | 7 #include "base/lazy_instance.h" |
8 #include "base/memory/scoped_ptr.h" | 8 #include "base/memory/scoped_ptr.h" |
| 9 #include "base/memory/scoped_vector.h" |
| 10 #include "base/strings/stringprintf.h" |
9 #include "chrome/browser/local_discovery/cloud_device_list.h" | 11 #include "chrome/browser/local_discovery/cloud_device_list.h" |
10 #include "chrome/browser/local_discovery/cloud_print_printer_list.h" | 12 #include "chrome/browser/local_discovery/cloud_print_printer_list.h" |
11 #include "chrome/browser/local_discovery/gcd_constants.h" | 13 #include "chrome/browser/local_discovery/gcd_constants.h" |
12 #include "chrome/browser/local_discovery/privet_device_lister_impl.h" | 14 #include "chrome/browser/local_discovery/privet_device_lister_impl.h" |
| 15 #include "chrome/browser/local_discovery/privet_http_impl.h" |
13 #include "chrome/browser/profiles/profile.h" | 16 #include "chrome/browser/profiles/profile.h" |
14 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h" | 17 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h" |
15 #include "chrome/browser/signin/signin_manager_factory.h" | 18 #include "chrome/browser/signin/signin_manager_factory.h" |
16 #include "components/signin/core/browser/profile_oauth2_token_service.h" | 19 #include "components/signin/core/browser/profile_oauth2_token_service.h" |
17 #include "components/signin/core/browser/signin_manager.h" | 20 #include "components/signin/core/browser/signin_manager.h" |
18 #include "components/signin/core/browser/signin_manager_base.h" | 21 #include "components/signin/core/browser/signin_manager_base.h" |
| 22 #include "net/base/net_util.h" |
19 | 23 |
20 namespace extensions { | 24 namespace extensions { |
21 | 25 |
22 namespace gcd_private = api::gcd_private; | 26 namespace gcd_private = api::gcd_private; |
23 | 27 |
24 namespace { | 28 namespace { |
25 | 29 |
26 const int kNumRequestsNeeded = 2; | 30 const int kNumRequestsNeeded = 2; |
27 | 31 |
28 const char kIDPrefixCloudPrinter[] = "cloudprint:"; | 32 const char kIDPrefixCloudPrinter[] = "cloudprint:"; |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
65 if (!signin_manager) | 69 if (!signin_manager) |
66 return scoped_ptr<local_discovery::GCDApiFlow>(); | 70 return scoped_ptr<local_discovery::GCDApiFlow>(); |
67 return local_discovery::GCDApiFlow::Create( | 71 return local_discovery::GCDApiFlow::Create( |
68 profile->GetRequestContext(), | 72 profile->GetRequestContext(), |
69 token_service, | 73 token_service, |
70 signin_manager->GetAuthenticatedAccountId()); | 74 signin_manager->GetAuthenticatedAccountId()); |
71 } | 75 } |
72 | 76 |
73 } // namespace | 77 } // namespace |
74 | 78 |
| 79 class GcdPrivateRequest : public local_discovery::PrivetV3Session::Request { |
| 80 public: |
| 81 GcdPrivateRequest(const std::string& api, |
| 82 const base::DictionaryValue& input, |
| 83 const GcdPrivateAPI::MessageResponseCallback& callback, |
| 84 GcdPrivateSessionHolder* session_holder); |
| 85 virtual ~GcdPrivateRequest(); |
| 86 |
| 87 // local_discovery::PrivetV3Session::Request implementation. |
| 88 virtual std::string GetName() OVERRIDE; |
| 89 virtual const base::DictionaryValue& GetInput() OVERRIDE; |
| 90 virtual void OnError( |
| 91 local_discovery::PrivetURLFetcher::ErrorType error) OVERRIDE; |
| 92 virtual void OnParsedJson(const base::DictionaryValue& value, |
| 93 bool has_error) OVERRIDE; |
| 94 |
| 95 private: |
| 96 std::string api_; |
| 97 scoped_ptr<base::DictionaryValue> input_; |
| 98 GcdPrivateAPI::MessageResponseCallback callback_; |
| 99 GcdPrivateSessionHolder* session_holder_; |
| 100 }; |
| 101 |
| 102 class GcdPrivateSessionHolder |
| 103 : public local_discovery::PrivetV3Session::Delegate { |
| 104 public: |
| 105 typedef base::Callback<void(api::gcd_private::Status status, |
| 106 const std::string& code, |
| 107 api::gcd_private::ConfirmationType type)> |
| 108 ConfirmationCodeCallback; |
| 109 |
| 110 GcdPrivateSessionHolder(const std::string& ip_address, |
| 111 int port, |
| 112 net::URLRequestContextGetter* request_context); |
| 113 virtual ~GcdPrivateSessionHolder(); |
| 114 |
| 115 void Start(const ConfirmationCodeCallback& callback); |
| 116 |
| 117 void ConfirmCode(const GcdPrivateAPI::SessionEstablishedCallback& callback); |
| 118 |
| 119 void SendMessage(const std::string& api, |
| 120 const base::DictionaryValue& input, |
| 121 GcdPrivateAPI::MessageResponseCallback callback); |
| 122 |
| 123 void DeleteRequest(GcdPrivateRequest* request); |
| 124 |
| 125 private: |
| 126 // local_discovery::PrivetV3Session::Delegate implementation. |
| 127 virtual void OnSetupConfirmationNeeded( |
| 128 const std::string& confirmation_code) OVERRIDE; |
| 129 virtual void OnSessionEstablished() OVERRIDE; |
| 130 virtual void OnCannotEstablishSession() OVERRIDE; |
| 131 |
| 132 scoped_ptr<local_discovery::PrivetHTTPClient> http_client_; |
| 133 scoped_ptr<local_discovery::PrivetV3Session> privet_session_; |
| 134 typedef ScopedVector<GcdPrivateRequest> RequestVector; |
| 135 RequestVector requests_; |
| 136 |
| 137 ConfirmationCodeCallback confirm_callback_; |
| 138 GcdPrivateAPI::SessionEstablishedCallback session_established_callback_; |
| 139 }; |
| 140 |
75 GcdPrivateAPI::GcdPrivateAPI(content::BrowserContext* context) | 141 GcdPrivateAPI::GcdPrivateAPI(content::BrowserContext* context) |
76 : num_device_listeners_(0), browser_context_(context) { | 142 : num_device_listeners_(0), last_session_id_(0), browser_context_(context) { |
77 DCHECK(browser_context_); | 143 DCHECK(browser_context_); |
78 if (EventRouter::Get(context)) { | 144 if (EventRouter::Get(context)) { |
79 EventRouter::Get(context) | 145 EventRouter::Get(context) |
80 ->RegisterObserver(this, gcd_private::OnDeviceStateChanged::kEventName); | 146 ->RegisterObserver(this, gcd_private::OnDeviceStateChanged::kEventName); |
81 EventRouter::Get(context) | 147 EventRouter::Get(context) |
82 ->RegisterObserver(this, gcd_private::OnDeviceRemoved::kEventName); | 148 ->RegisterObserver(this, gcd_private::OnDeviceRemoved::kEventName); |
83 } | 149 } |
84 } | 150 } |
85 | 151 |
86 GcdPrivateAPI::~GcdPrivateAPI() { | 152 GcdPrivateAPI::~GcdPrivateAPI() { |
(...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
170 | 236 |
171 bool GcdPrivateAPI::QueryForDevices() { | 237 bool GcdPrivateAPI::QueryForDevices() { |
172 if (!privet_device_lister_) | 238 if (!privet_device_lister_) |
173 return false; | 239 return false; |
174 | 240 |
175 privet_device_lister_->DiscoverNewDevices(true); | 241 privet_device_lister_->DiscoverNewDevices(true); |
176 | 242 |
177 return true; | 243 return true; |
178 } | 244 } |
179 | 245 |
| 246 void GcdPrivateAPI::EstablishSession(const std::string& ip_address, |
| 247 int port, |
| 248 ConfirmationCodeCallback callback) { |
| 249 int session_id = last_session_id_++; |
| 250 linked_ptr<GcdPrivateSessionHolder> session_handler( |
| 251 new GcdPrivateSessionHolder( |
| 252 ip_address, port, browser_context_->GetRequestContext())); |
| 253 sessions_[session_id] = session_handler; |
| 254 session_handler->Start(base::Bind(callback, session_id)); |
| 255 } |
| 256 |
| 257 void GcdPrivateAPI::ConfirmCode(int session_id, |
| 258 SessionEstablishedCallback callback) { |
| 259 GCDSessionMap::iterator found = sessions_.find(session_id); |
| 260 |
| 261 if (found == sessions_.end()) { |
| 262 callback.Run(gcd_private::STATUS_UNKNOWNSESSIONERROR); |
| 263 return; |
| 264 } |
| 265 |
| 266 found->second->ConfirmCode(callback); |
| 267 } |
| 268 |
| 269 void GcdPrivateAPI::SendMessage(int session_id, |
| 270 const std::string& api, |
| 271 const base::DictionaryValue& input, |
| 272 MessageResponseCallback callback) { |
| 273 GCDSessionMap::iterator found = sessions_.find(session_id); |
| 274 |
| 275 if (found == sessions_.end()) { |
| 276 callback.Run(gcd_private::STATUS_UNKNOWNSESSIONERROR, |
| 277 base::DictionaryValue()); |
| 278 return; |
| 279 } |
| 280 |
| 281 found->second->SendMessage(api, input, callback); |
| 282 } |
| 283 |
| 284 void GcdPrivateAPI::RemoveSession(int session_id) { |
| 285 sessions_.erase(session_id); |
| 286 } |
| 287 |
180 // static | 288 // static |
181 void GcdPrivateAPI::SetGCDApiFlowFactoryForTests( | 289 void GcdPrivateAPI::SetGCDApiFlowFactoryForTests( |
182 GCDApiFlowFactoryForTests* factory) { | 290 GCDApiFlowFactoryForTests* factory) { |
183 g_gcd_api_flow_factory = factory; | 291 g_gcd_api_flow_factory = factory; |
184 } | 292 } |
185 | 293 |
| 294 GcdPrivateRequest::GcdPrivateRequest( |
| 295 const std::string& api, |
| 296 const base::DictionaryValue& input, |
| 297 const GcdPrivateAPI::MessageResponseCallback& callback, |
| 298 GcdPrivateSessionHolder* session_holder) |
| 299 : api_(api), |
| 300 input_(input.DeepCopy()), |
| 301 callback_(callback), |
| 302 session_holder_(session_holder) { |
| 303 } |
| 304 |
| 305 GcdPrivateRequest::~GcdPrivateRequest() { |
| 306 } |
| 307 |
| 308 std::string GcdPrivateRequest::GetName() { |
| 309 return api_; |
| 310 } |
| 311 |
| 312 const base::DictionaryValue& GcdPrivateRequest::GetInput() { |
| 313 return *input_; |
| 314 } |
| 315 |
| 316 void GcdPrivateRequest::OnError( |
| 317 local_discovery::PrivetURLFetcher::ErrorType error) { |
| 318 callback_.Run(gcd_private::STATUS_CONNECTIONERROR, base::DictionaryValue()); |
| 319 |
| 320 session_holder_->DeleteRequest(this); |
| 321 } |
| 322 |
| 323 void GcdPrivateRequest::OnParsedJson(const base::DictionaryValue& value, |
| 324 bool has_error) { |
| 325 callback_.Run(gcd_private::STATUS_SUCCESS, value); |
| 326 |
| 327 session_holder_->DeleteRequest(this); |
| 328 } |
| 329 |
| 330 GcdPrivateSessionHolder::GcdPrivateSessionHolder( |
| 331 const std::string& ip_address, |
| 332 int port, |
| 333 net::URLRequestContextGetter* request_context) { |
| 334 std::string host_string; |
| 335 net::IPAddressNumber address_number; |
| 336 |
| 337 if (net::ParseIPLiteralToNumber(ip_address, &address_number) && |
| 338 address_number.size() == net::kIPv6AddressSize) { |
| 339 host_string = base::StringPrintf("[%s]", ip_address.c_str()); |
| 340 } else { |
| 341 host_string = ip_address; |
| 342 } |
| 343 |
| 344 http_client_.reset(new local_discovery::PrivetHTTPClientImpl( |
| 345 "", net::HostPortPair(host_string, port), request_context)); |
| 346 } |
| 347 |
| 348 GcdPrivateSessionHolder::~GcdPrivateSessionHolder() { |
| 349 } |
| 350 |
| 351 void GcdPrivateSessionHolder::Start(const ConfirmationCodeCallback& callback) { |
| 352 confirm_callback_ = callback; |
| 353 |
| 354 privet_session_.reset( |
| 355 new local_discovery::PrivetV3Session(http_client_.Pass(), this)); |
| 356 privet_session_->Start(); |
| 357 } |
| 358 |
| 359 void GcdPrivateSessionHolder::ConfirmCode( |
| 360 const GcdPrivateAPI::SessionEstablishedCallback& callback) { |
| 361 session_established_callback_ = callback; |
| 362 privet_session_->ConfirmCode(); |
| 363 } |
| 364 |
| 365 void GcdPrivateSessionHolder::SendMessage( |
| 366 const std::string& api, |
| 367 const base::DictionaryValue& input, |
| 368 GcdPrivateAPI::MessageResponseCallback callback) { |
| 369 GcdPrivateRequest* request = |
| 370 new GcdPrivateRequest(api, input, callback, this); |
| 371 requests_.push_back(request); |
| 372 privet_session_->StartRequest(request); |
| 373 } |
| 374 |
| 375 void GcdPrivateSessionHolder::DeleteRequest(GcdPrivateRequest* request) { |
| 376 // TODO(noamsml): Does this need to be optimized? |
| 377 for (RequestVector::iterator i = requests_.begin(); i != requests_.end(); |
| 378 i++) { |
| 379 if (*i == request) { |
| 380 requests_.erase(i); |
| 381 break; |
| 382 } |
| 383 } |
| 384 } |
| 385 |
| 386 void GcdPrivateSessionHolder::OnSetupConfirmationNeeded( |
| 387 const std::string& confirmation_code) { |
| 388 confirm_callback_.Run(gcd_private::STATUS_SUCCESS, |
| 389 confirmation_code, |
| 390 gcd_private::CONFIRMATION_TYPE_DISPLAYCODE); |
| 391 |
| 392 confirm_callback_.Reset(); |
| 393 } |
| 394 |
| 395 void GcdPrivateSessionHolder::OnSessionEstablished() { |
| 396 session_established_callback_.Run(gcd_private::STATUS_SUCCESS); |
| 397 |
| 398 session_established_callback_.Reset(); |
| 399 } |
| 400 |
| 401 void GcdPrivateSessionHolder::OnCannotEstablishSession() { |
| 402 session_established_callback_.Run(gcd_private::STATUS_SESSIONERROR); |
| 403 |
| 404 session_established_callback_.Reset(); |
| 405 } |
| 406 |
186 GcdPrivateGetCloudDeviceListFunction::GcdPrivateGetCloudDeviceListFunction() { | 407 GcdPrivateGetCloudDeviceListFunction::GcdPrivateGetCloudDeviceListFunction() { |
187 } | 408 } |
188 | 409 |
189 GcdPrivateGetCloudDeviceListFunction::~GcdPrivateGetCloudDeviceListFunction() { | 410 GcdPrivateGetCloudDeviceListFunction::~GcdPrivateGetCloudDeviceListFunction() { |
190 } | 411 } |
191 | 412 |
192 bool GcdPrivateGetCloudDeviceListFunction::RunAsync() { | 413 bool GcdPrivateGetCloudDeviceListFunction::RunAsync() { |
193 requests_succeeded_ = 0; | 414 requests_succeeded_ = 0; |
194 requests_failed_ = 0; | 415 requests_failed_ = 0; |
195 | 416 |
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
296 return false; | 517 return false; |
297 } | 518 } |
298 | 519 |
299 GcdPrivateEstablishSessionFunction::GcdPrivateEstablishSessionFunction() { | 520 GcdPrivateEstablishSessionFunction::GcdPrivateEstablishSessionFunction() { |
300 } | 521 } |
301 | 522 |
302 GcdPrivateEstablishSessionFunction::~GcdPrivateEstablishSessionFunction() { | 523 GcdPrivateEstablishSessionFunction::~GcdPrivateEstablishSessionFunction() { |
303 } | 524 } |
304 | 525 |
305 bool GcdPrivateEstablishSessionFunction::RunAsync() { | 526 bool GcdPrivateEstablishSessionFunction::RunAsync() { |
306 return false; | 527 scoped_ptr<gcd_private::EstablishSession::Params> params = |
| 528 gcd_private::EstablishSession::Params::Create(*args_); |
| 529 |
| 530 if (!params) |
| 531 return false; |
| 532 |
| 533 GcdPrivateAPI* gcd_api = |
| 534 BrowserContextKeyedAPIFactory<GcdPrivateAPI>::Get(GetProfile()); |
| 535 |
| 536 if (!gcd_api) |
| 537 return false; |
| 538 |
| 539 gcd_api->EstablishSession( |
| 540 params->ip_address, |
| 541 params->port, |
| 542 base::Bind(&GcdPrivateEstablishSessionFunction::OnConfirmCodeCallback, |
| 543 this)); |
| 544 |
| 545 return true; |
| 546 } |
| 547 |
| 548 void GcdPrivateEstablishSessionFunction::OnConfirmCodeCallback( |
| 549 int session_id, |
| 550 gcd_private::Status status, |
| 551 const std::string& confirm_code, |
| 552 gcd_private::ConfirmationType confirmation_type) { |
| 553 results_ = gcd_private::EstablishSession::Results::Create( |
| 554 session_id, status, confirm_code, confirmation_type); |
| 555 SendResponse(true); |
307 } | 556 } |
308 | 557 |
309 GcdPrivateConfirmCodeFunction::GcdPrivateConfirmCodeFunction() { | 558 GcdPrivateConfirmCodeFunction::GcdPrivateConfirmCodeFunction() { |
310 } | 559 } |
311 | 560 |
312 GcdPrivateConfirmCodeFunction::~GcdPrivateConfirmCodeFunction() { | 561 GcdPrivateConfirmCodeFunction::~GcdPrivateConfirmCodeFunction() { |
313 } | 562 } |
314 | 563 |
315 bool GcdPrivateConfirmCodeFunction::RunAsync() { | 564 bool GcdPrivateConfirmCodeFunction::RunAsync() { |
316 return false; | 565 scoped_ptr<gcd_private::ConfirmCode::Params> params = |
| 566 gcd_private::ConfirmCode::Params::Create(*args_); |
| 567 |
| 568 if (!params) |
| 569 return false; |
| 570 |
| 571 GcdPrivateAPI* gcd_api = |
| 572 BrowserContextKeyedAPIFactory<GcdPrivateAPI>::Get(GetProfile()); |
| 573 |
| 574 if (!gcd_api) |
| 575 return false; |
| 576 |
| 577 gcd_api->ConfirmCode( |
| 578 params->session_id, |
| 579 base::Bind(&GcdPrivateConfirmCodeFunction::OnSessionEstablishedCallback, |
| 580 this)); |
| 581 |
| 582 return true; |
| 583 } |
| 584 |
| 585 void GcdPrivateConfirmCodeFunction::OnSessionEstablishedCallback( |
| 586 api::gcd_private::Status status) { |
| 587 results_ = gcd_private::ConfirmCode::Results::Create(status); |
| 588 SendResponse(true); |
317 } | 589 } |
318 | 590 |
319 GcdPrivateSendMessageFunction::GcdPrivateSendMessageFunction() { | 591 GcdPrivateSendMessageFunction::GcdPrivateSendMessageFunction() { |
320 } | 592 } |
321 | 593 |
322 GcdPrivateSendMessageFunction::~GcdPrivateSendMessageFunction() { | 594 GcdPrivateSendMessageFunction::~GcdPrivateSendMessageFunction() { |
323 } | 595 } |
324 | 596 |
325 bool GcdPrivateSendMessageFunction::RunAsync() { | 597 bool GcdPrivateSendMessageFunction::RunAsync() { |
326 return false; | 598 scoped_ptr<gcd_private::PassMessage::Params> params = |
| 599 gcd_private::PassMessage::Params::Create(*args_); |
| 600 |
| 601 if (!params) |
| 602 return false; |
| 603 |
| 604 GcdPrivateAPI* gcd_api = |
| 605 BrowserContextKeyedAPIFactory<GcdPrivateAPI>::Get(GetProfile()); |
| 606 |
| 607 if (!gcd_api) |
| 608 return false; |
| 609 |
| 610 gcd_api->SendMessage( |
| 611 params->session_id, |
| 612 params->api, |
| 613 params->input.additional_properties, |
| 614 base::Bind(&GcdPrivateSendMessageFunction::OnMessageSentCallback, this)); |
| 615 |
| 616 return true; |
| 617 } |
| 618 |
| 619 void GcdPrivateSendMessageFunction::OnMessageSentCallback( |
| 620 api::gcd_private::Status status, |
| 621 const base::DictionaryValue& value) { |
| 622 gcd_private::PassMessage::Results::Response response; |
| 623 response.additional_properties.MergeDictionary(&value); |
| 624 |
| 625 results_ = gcd_private::PassMessage::Results::Create(status, response); |
| 626 SendResponse(true); |
327 } | 627 } |
328 | 628 |
329 GcdPrivateTerminateSessionFunction::GcdPrivateTerminateSessionFunction() { | 629 GcdPrivateTerminateSessionFunction::GcdPrivateTerminateSessionFunction() { |
330 } | 630 } |
331 | 631 |
332 GcdPrivateTerminateSessionFunction::~GcdPrivateTerminateSessionFunction() { | 632 GcdPrivateTerminateSessionFunction::~GcdPrivateTerminateSessionFunction() { |
333 } | 633 } |
334 | 634 |
335 bool GcdPrivateTerminateSessionFunction::RunAsync() { | 635 bool GcdPrivateTerminateSessionFunction::RunAsync() { |
336 return false; | 636 scoped_ptr<gcd_private::TerminateSession::Params> params = |
| 637 gcd_private::TerminateSession::Params::Create(*args_); |
| 638 |
| 639 if (!params) |
| 640 return false; |
| 641 |
| 642 GcdPrivateAPI* gcd_api = |
| 643 BrowserContextKeyedAPIFactory<GcdPrivateAPI>::Get(GetProfile()); |
| 644 |
| 645 if (!gcd_api) |
| 646 return false; |
| 647 |
| 648 gcd_api->RemoveSession(params->session_id); |
| 649 |
| 650 SendResponse(true); |
| 651 return true; |
337 } | 652 } |
338 | 653 |
339 GcdPrivateGetCommandDefinitionsFunction:: | 654 GcdPrivateGetCommandDefinitionsFunction:: |
340 GcdPrivateGetCommandDefinitionsFunction() { | 655 GcdPrivateGetCommandDefinitionsFunction() { |
341 } | 656 } |
342 | 657 |
343 GcdPrivateGetCommandDefinitionsFunction:: | 658 GcdPrivateGetCommandDefinitionsFunction:: |
344 ~GcdPrivateGetCommandDefinitionsFunction() { | 659 ~GcdPrivateGetCommandDefinitionsFunction() { |
345 } | 660 } |
346 | 661 |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
382 } | 697 } |
383 | 698 |
384 GcdPrivateGetCommandsListFunction::~GcdPrivateGetCommandsListFunction() { | 699 GcdPrivateGetCommandsListFunction::~GcdPrivateGetCommandsListFunction() { |
385 } | 700 } |
386 | 701 |
387 bool GcdPrivateGetCommandsListFunction::RunAsync() { | 702 bool GcdPrivateGetCommandsListFunction::RunAsync() { |
388 return false; | 703 return false; |
389 } | 704 } |
390 | 705 |
391 } // namespace extensions | 706 } // namespace extensions |
OLD | NEW |