| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 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 "remoting/host/plugin/host_script_object.h" | 5 #include "remoting/host/plugin/host_script_object.h" |
| 6 #include "remoting/host/plugin/daemon_controller.h" |
| 6 | 7 |
| 7 #include "base/bind.h" | 8 #include "base/bind.h" |
| 8 #include "base/message_loop.h" | 9 #include "base/message_loop.h" |
| 9 #include "base/message_loop_proxy.h" | 10 #include "base/message_loop_proxy.h" |
| 10 #include "base/sys_string_conversions.h" | 11 #include "base/sys_string_conversions.h" |
| 11 #include "base/threading/platform_thread.h" | 12 #include "base/threading/platform_thread.h" |
| 12 #include "base/utf_string_conversions.h" | 13 #include "base/utf_string_conversions.h" |
| 13 #include "remoting/jingle_glue/xmpp_signal_strategy.h" | 14 #include "remoting/jingle_glue/xmpp_signal_strategy.h" |
| 14 #include "remoting/base/auth_token_util.h" | 15 #include "remoting/base/auth_token_util.h" |
| 15 #include "remoting/host/chromoting_host.h" | 16 #include "remoting/host/chromoting_host.h" |
| 16 #include "remoting/host/chromoting_host_context.h" | 17 #include "remoting/host/chromoting_host_context.h" |
| 17 #include "remoting/host/desktop_environment.h" | 18 #include "remoting/host/desktop_environment.h" |
| 18 #include "remoting/host/host_key_pair.h" | 19 #include "remoting/host/host_key_pair.h" |
| 19 #include "remoting/host/host_secret.h" | 20 #include "remoting/host/host_secret.h" |
| 20 #include "remoting/host/it2me_host_user_interface.h" | 21 #include "remoting/host/it2me_host_user_interface.h" |
| 21 #include "remoting/host/plugin/host_log_handler.h" | 22 #include "remoting/host/plugin/host_log_handler.h" |
| 22 #include "remoting/host/plugin/policy_hack/nat_policy.h" | 23 #include "remoting/host/plugin/policy_hack/nat_policy.h" |
| 23 #include "remoting/host/register_support_host_request.h" | 24 #include "remoting/host/register_support_host_request.h" |
| 24 #include "remoting/protocol/it2me_host_authenticator_factory.h" | 25 #include "remoting/protocol/it2me_host_authenticator_factory.h" |
| 25 | 26 |
| 26 namespace remoting { | 27 namespace remoting { |
| 27 | 28 |
| 28 // Supported Javascript interface: | |
| 29 // readonly attribute string accessCode; | |
| 30 // readonly attribute int accessCodeLifetime; | |
| 31 // readonly attribute string client; | |
| 32 // readonly attribute int state; | |
| 33 // | |
| 34 // state: { | |
| 35 // DISCONNECTED, | |
| 36 // STARTING, | |
| 37 // REQUESTED_ACCESS_CODE, | |
| 38 // RECEIVED_ACCESS_CODE, | |
| 39 // CONNECTED, | |
| 40 // DISCONNECTING, | |
| 41 // ERROR, | |
| 42 // } | |
| 43 // | |
| 44 // attribute Function void logDebugInfo(string); | |
| 45 // attribute Function void onNatTraversalPolicyChanged(boolean); | |
| 46 // attribute Function void onStateChanged(state); | |
| 47 // | |
| 48 // // The |auth_service_with_token| parameter should be in the format | |
| 49 // // "auth_service:auth_token". An example would be "oauth2:1/2a3912vd". | |
| 50 // void connect(string uid, string auth_service_with_token); | |
| 51 // void disconnect(); | |
| 52 // void localize(string (*localize_func)(string,...)); | |
| 53 | |
| 54 namespace { | 29 namespace { |
| 55 | 30 |
| 56 const char* kAttrNameAccessCode = "accessCode"; | 31 const char* kAttrNameAccessCode = "accessCode"; |
| 57 const char* kAttrNameAccessCodeLifetime = "accessCodeLifetime"; | 32 const char* kAttrNameAccessCodeLifetime = "accessCodeLifetime"; |
| 58 const char* kAttrNameClient = "client"; | 33 const char* kAttrNameClient = "client"; |
| 34 const char* kAttrNameDaemonState = "daemonState"; |
| 59 const char* kAttrNameState = "state"; | 35 const char* kAttrNameState = "state"; |
| 60 const char* kAttrNameLogDebugInfo = "logDebugInfo"; | 36 const char* kAttrNameLogDebugInfo = "logDebugInfo"; |
| 61 const char* kAttrNameOnNatTraversalPolicyChanged = | 37 const char* kAttrNameOnNatTraversalPolicyChanged = |
| 62 "onNatTraversalPolicyChanged"; | 38 "onNatTraversalPolicyChanged"; |
| 63 const char* kAttrNameOnStateChanged = "onStateChanged"; | 39 const char* kAttrNameOnStateChanged = "onStateChanged"; |
| 64 const char* kFuncNameConnect = "connect"; | 40 const char* kFuncNameConnect = "connect"; |
| 65 const char* kFuncNameDisconnect = "disconnect"; | 41 const char* kFuncNameDisconnect = "disconnect"; |
| 66 const char* kFuncNameLocalize = "localize"; | 42 const char* kFuncNameLocalize = "localize"; |
| 43 const char* kFuncNameSetDaemonPin = "setDaemonPin"; |
| 44 const char* kFuncNameStartDaemon = "startDaemon"; |
| 45 const char* kFuncNameStopDaemon = "stopDaemon"; |
| 67 | 46 |
| 68 // States. | 47 // States. |
| 69 const char* kAttrNameDisconnected = "DISCONNECTED"; | 48 const char* kAttrNameDisconnected = "DISCONNECTED"; |
| 70 const char* kAttrNameStarting = "STARTING"; | 49 const char* kAttrNameStarting = "STARTING"; |
| 71 const char* kAttrNameRequestedAccessCode = "REQUESTED_ACCESS_CODE"; | 50 const char* kAttrNameRequestedAccessCode = "REQUESTED_ACCESS_CODE"; |
| 72 const char* kAttrNameReceivedAccessCode = "RECEIVED_ACCESS_CODE"; | 51 const char* kAttrNameReceivedAccessCode = "RECEIVED_ACCESS_CODE"; |
| 73 const char* kAttrNameConnected = "CONNECTED"; | 52 const char* kAttrNameConnected = "CONNECTED"; |
| 74 const char* kAttrNameDisconnecting = "DISCONNECTING"; | 53 const char* kAttrNameDisconnecting = "DISCONNECTING"; |
| 75 const char* kAttrNameError = "ERROR"; | 54 const char* kAttrNameError = "ERROR"; |
| 76 | 55 |
| 77 const int kMaxLoginAttempts = 5; | 56 const int kMaxLoginAttempts = 5; |
| 78 | 57 |
| 79 } // namespace | 58 } // namespace |
| 80 | 59 |
| 81 HostNPScriptObject::HostNPScriptObject( | 60 HostNPScriptObject::HostNPScriptObject( |
| 82 NPP plugin, | 61 NPP plugin, |
| 83 NPObject* parent, | 62 NPObject* parent, |
| 84 PluginMessageLoopProxy::Delegate* plugin_thread_delegate) | 63 PluginMessageLoopProxy::Delegate* plugin_thread_delegate) |
| 85 : plugin_(plugin), | 64 : plugin_(plugin), |
| 86 parent_(parent), | 65 parent_(parent), |
| 87 state_(kDisconnected), | 66 state_(kDisconnected), |
| 88 np_thread_id_(base::PlatformThread::CurrentId()), | 67 np_thread_id_(base::PlatformThread::CurrentId()), |
| 89 plugin_message_loop_proxy_( | 68 plugin_message_loop_proxy_( |
| 90 new PluginMessageLoopProxy(plugin_thread_delegate)), | 69 new PluginMessageLoopProxy(plugin_thread_delegate)), |
| 91 host_context_(plugin_message_loop_proxy_), | 70 host_context_(plugin_message_loop_proxy_), |
| 92 failed_login_attempts_(0), | 71 failed_login_attempts_(0), |
| 72 daemon_controller_(DaemonController::Create()), |
| 93 disconnected_event_(true, false), | 73 disconnected_event_(true, false), |
| 94 am_currently_logging_(false), | 74 am_currently_logging_(false), |
| 95 nat_traversal_enabled_(false), | 75 nat_traversal_enabled_(false), |
| 96 policy_received_(false) { | 76 policy_received_(false) { |
| 97 } | 77 } |
| 98 | 78 |
| 99 HostNPScriptObject::~HostNPScriptObject() { | 79 HostNPScriptObject::~HostNPScriptObject() { |
| 100 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | 80 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); |
| 101 | 81 |
| 102 // Shutdown It2MeHostUserInterface first so that it doesn't try to post | 82 // Shutdown It2MeHostUserInterface first so that it doesn't try to post |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 140 base::Bind(&HostNPScriptObject::OnNatPolicyUpdate, | 120 base::Bind(&HostNPScriptObject::OnNatPolicyUpdate, |
| 141 base::Unretained(this))); | 121 base::Unretained(this))); |
| 142 return true; | 122 return true; |
| 143 } | 123 } |
| 144 | 124 |
| 145 bool HostNPScriptObject::HasMethod(const std::string& method_name) { | 125 bool HostNPScriptObject::HasMethod(const std::string& method_name) { |
| 146 VLOG(2) << "HasMethod " << method_name; | 126 VLOG(2) << "HasMethod " << method_name; |
| 147 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | 127 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); |
| 148 return (method_name == kFuncNameConnect || | 128 return (method_name == kFuncNameConnect || |
| 149 method_name == kFuncNameDisconnect || | 129 method_name == kFuncNameDisconnect || |
| 150 method_name == kFuncNameLocalize); | 130 method_name == kFuncNameLocalize || |
| 131 method_name == kFuncNameSetDaemonPin || |
| 132 method_name == kFuncNameStartDaemon || |
| 133 method_name == kFuncNameStopDaemon); |
| 151 } | 134 } |
| 152 | 135 |
| 153 bool HostNPScriptObject::InvokeDefault(const NPVariant* args, | 136 bool HostNPScriptObject::InvokeDefault(const NPVariant* args, |
| 154 uint32_t argCount, | 137 uint32_t argCount, |
| 155 NPVariant* result) { | 138 NPVariant* result) { |
| 156 VLOG(2) << "InvokeDefault"; | 139 VLOG(2) << "InvokeDefault"; |
| 157 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | 140 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); |
| 158 SetException("exception during default invocation"); | 141 SetException("exception during default invocation"); |
| 159 return false; | 142 return false; |
| 160 } | 143 } |
| 161 | 144 |
| 162 bool HostNPScriptObject::Invoke(const std::string& method_name, | 145 bool HostNPScriptObject::Invoke(const std::string& method_name, |
| 163 const NPVariant* args, | 146 const NPVariant* args, |
| 164 uint32_t argCount, | 147 uint32_t argCount, |
| 165 NPVariant* result) { | 148 NPVariant* result) { |
| 166 VLOG(2) << "Invoke " << method_name; | 149 VLOG(2) << "Invoke " << method_name; |
| 167 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | 150 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); |
| 168 if (method_name == kFuncNameConnect) { | 151 if (method_name == kFuncNameConnect) { |
| 169 return Connect(args, argCount, result); | 152 return Connect(args, argCount, result); |
| 170 } else if (method_name == kFuncNameDisconnect) { | 153 } else if (method_name == kFuncNameDisconnect) { |
| 171 return Disconnect(args, argCount, result); | 154 return Disconnect(args, argCount, result); |
| 172 } else if (method_name == kFuncNameLocalize) { | 155 } else if (method_name == kFuncNameLocalize) { |
| 173 return Localize(args, argCount, result); | 156 return Localize(args, argCount, result); |
| 157 } else if (method_name == kFuncNameSetDaemonPin) { |
| 158 return SetDaemonPin(args, argCount, result); |
| 159 } else if (method_name == kFuncNameStartDaemon) { |
| 160 return StartDaemon(args, argCount, result); |
| 161 } else if (method_name == kFuncNameStopDaemon) { |
| 162 return StopDaemon(args, argCount, result); |
| 174 } else { | 163 } else { |
| 175 SetException("Invoke: unknown method " + method_name); | 164 SetException("Invoke: unknown method " + method_name); |
| 176 return false; | 165 return false; |
| 177 } | 166 } |
| 178 } | 167 } |
| 179 | 168 |
| 180 bool HostNPScriptObject::HasProperty(const std::string& property_name) { | 169 bool HostNPScriptObject::HasProperty(const std::string& property_name) { |
| 181 VLOG(2) << "HasProperty " << property_name; | 170 VLOG(2) << "HasProperty " << property_name; |
| 182 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | 171 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); |
| 183 return (property_name == kAttrNameAccessCode || | 172 return (property_name == kAttrNameAccessCode || |
| 184 property_name == kAttrNameAccessCodeLifetime || | 173 property_name == kAttrNameAccessCodeLifetime || |
| 185 property_name == kAttrNameClient || | 174 property_name == kAttrNameClient || |
| 175 property_name == kAttrNameDaemonState || |
| 186 property_name == kAttrNameState || | 176 property_name == kAttrNameState || |
| 187 property_name == kAttrNameLogDebugInfo || | 177 property_name == kAttrNameLogDebugInfo || |
| 188 property_name == kAttrNameOnNatTraversalPolicyChanged || | 178 property_name == kAttrNameOnNatTraversalPolicyChanged || |
| 189 property_name == kAttrNameOnStateChanged || | 179 property_name == kAttrNameOnStateChanged || |
| 190 property_name == kAttrNameDisconnected || | 180 property_name == kAttrNameDisconnected || |
| 191 property_name == kAttrNameStarting || | 181 property_name == kAttrNameStarting || |
| 192 property_name == kAttrNameRequestedAccessCode || | 182 property_name == kAttrNameRequestedAccessCode || |
| 193 property_name == kAttrNameReceivedAccessCode || | 183 property_name == kAttrNameReceivedAccessCode || |
| 194 property_name == kAttrNameConnected || | 184 property_name == kAttrNameConnected || |
| 195 property_name == kAttrNameDisconnecting || | 185 property_name == kAttrNameDisconnecting || |
| (...skipping 25 matching lines...) Expand all Loading... |
| 221 base::AutoLock auto_lock(access_code_lock_); | 211 base::AutoLock auto_lock(access_code_lock_); |
| 222 *result = NPVariantFromString(access_code_); | 212 *result = NPVariantFromString(access_code_); |
| 223 return true; | 213 return true; |
| 224 } else if (property_name == kAttrNameAccessCodeLifetime) { | 214 } else if (property_name == kAttrNameAccessCodeLifetime) { |
| 225 base::AutoLock auto_lock(access_code_lock_); | 215 base::AutoLock auto_lock(access_code_lock_); |
| 226 INT32_TO_NPVARIANT(access_code_lifetime_.InSeconds(), *result); | 216 INT32_TO_NPVARIANT(access_code_lifetime_.InSeconds(), *result); |
| 227 return true; | 217 return true; |
| 228 } else if (property_name == kAttrNameClient) { | 218 } else if (property_name == kAttrNameClient) { |
| 229 *result = NPVariantFromString(client_username_); | 219 *result = NPVariantFromString(client_username_); |
| 230 return true; | 220 return true; |
| 221 } else if (property_name == kAttrNameDaemonState) { |
| 222 INT32_TO_NPVARIANT(daemon_controller_->GetState(), *result); |
| 223 return true; |
| 231 } else if (property_name == kAttrNameDisconnected) { | 224 } else if (property_name == kAttrNameDisconnected) { |
| 232 INT32_TO_NPVARIANT(kDisconnected, *result); | 225 INT32_TO_NPVARIANT(kDisconnected, *result); |
| 233 return true; | 226 return true; |
| 234 } else if (property_name == kAttrNameStarting) { | 227 } else if (property_name == kAttrNameStarting) { |
| 235 INT32_TO_NPVARIANT(kStarting, *result); | 228 INT32_TO_NPVARIANT(kStarting, *result); |
| 236 return true; | 229 return true; |
| 237 } else if (property_name == kAttrNameRequestedAccessCode) { | 230 } else if (property_name == kAttrNameRequestedAccessCode) { |
| 238 INT32_TO_NPVARIANT(kRequestedAccessCode, *result); | 231 INT32_TO_NPVARIANT(kRequestedAccessCode, *result); |
| 239 return true; | 232 return true; |
| 240 } else if (property_name == kAttrNameReceivedAccessCode) { | 233 } else if (property_name == kAttrNameReceivedAccessCode) { |
| (...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 313 } | 306 } |
| 314 | 307 |
| 315 bool HostNPScriptObject::Enumerate(std::vector<std::string>* values) { | 308 bool HostNPScriptObject::Enumerate(std::vector<std::string>* values) { |
| 316 VLOG(2) << "Enumerate"; | 309 VLOG(2) << "Enumerate"; |
| 317 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | 310 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); |
| 318 const char* entries[] = { | 311 const char* entries[] = { |
| 319 kAttrNameAccessCode, | 312 kAttrNameAccessCode, |
| 320 kAttrNameState, | 313 kAttrNameState, |
| 321 kAttrNameLogDebugInfo, | 314 kAttrNameLogDebugInfo, |
| 322 kAttrNameOnStateChanged, | 315 kAttrNameOnStateChanged, |
| 323 kFuncNameConnect, | |
| 324 kFuncNameDisconnect, | |
| 325 kFuncNameLocalize, | |
| 326 kAttrNameDisconnected, | 316 kAttrNameDisconnected, |
| 327 kAttrNameStarting, | 317 kAttrNameStarting, |
| 328 kAttrNameRequestedAccessCode, | 318 kAttrNameRequestedAccessCode, |
| 329 kAttrNameReceivedAccessCode, | 319 kAttrNameReceivedAccessCode, |
| 330 kAttrNameConnected, | 320 kAttrNameConnected, |
| 331 kAttrNameDisconnecting, | 321 kAttrNameDisconnecting, |
| 332 kAttrNameError | 322 kAttrNameError, |
| 323 kFuncNameConnect, |
| 324 kFuncNameDisconnect, |
| 325 kFuncNameLocalize, |
| 326 kFuncNameSetDaemonPin, |
| 327 kFuncNameStartDaemon, |
| 328 kFuncNameStopDaemon |
| 333 }; | 329 }; |
| 334 for (size_t i = 0; i < arraysize(entries); ++i) { | 330 for (size_t i = 0; i < arraysize(entries); ++i) { |
| 335 values->push_back(entries[i]); | 331 values->push_back(entries[i]); |
| 336 } | 332 } |
| 337 return true; | 333 return true; |
| 338 } | 334 } |
| 339 | 335 |
| 340 void HostNPScriptObject::OnAccessDenied(const std::string& jid) { | 336 void HostNPScriptObject::OnAccessDenied(const std::string& jid) { |
| 341 DCHECK(host_context_.network_message_loop()->BelongsToCurrentThread()); | 337 DCHECK(host_context_.network_message_loop()->BelongsToCurrentThread()); |
| 342 | 338 |
| (...skipping 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 559 if (NPVARIANT_IS_OBJECT(args[0])) { | 555 if (NPVARIANT_IS_OBJECT(args[0])) { |
| 560 ScopedRefNPObject localize_func(NPVARIANT_TO_OBJECT(args[0])); | 556 ScopedRefNPObject localize_func(NPVARIANT_TO_OBJECT(args[0])); |
| 561 LocalizeStrings(localize_func.get()); | 557 LocalizeStrings(localize_func.get()); |
| 562 return true; | 558 return true; |
| 563 } else { | 559 } else { |
| 564 SetException("localize: unexpected type for argument 1"); | 560 SetException("localize: unexpected type for argument 1"); |
| 565 return false; | 561 return false; |
| 566 } | 562 } |
| 567 } | 563 } |
| 568 | 564 |
| 565 bool HostNPScriptObject::SetDaemonPin(const NPVariant* args, |
| 566 uint32_t arg_count, |
| 567 NPVariant* result) { |
| 568 if (arg_count != 1) { |
| 569 SetException("startDaemon: bad number of arguments"); |
| 570 return false; |
| 571 } |
| 572 if (NPVARIANT_IS_STRING(args[0])) { |
| 573 bool set_pin_result = |
| 574 daemon_controller_->SetPin(StringFromNPVariant(args[0])); |
| 575 BOOLEAN_TO_NPVARIANT(set_pin_result, *result); |
| 576 return true; |
| 577 } else { |
| 578 SetException("startDaemon: unexpected type for argument 1"); |
| 579 return false; |
| 580 } |
| 581 } |
| 582 |
| 583 bool HostNPScriptObject::StartDaemon(const NPVariant* args, |
| 584 uint32_t arg_count, |
| 585 NPVariant* result) { |
| 586 if (arg_count != 0) { |
| 587 SetException("startDaemon: bad number of arguments"); |
| 588 return false; |
| 589 } |
| 590 bool start_result = daemon_controller_->Start(); |
| 591 BOOLEAN_TO_NPVARIANT(start_result, *result); |
| 592 return true; |
| 593 } |
| 594 |
| 595 bool HostNPScriptObject::StopDaemon(const NPVariant* args, |
| 596 uint32_t arg_count, |
| 597 NPVariant* result) { |
| 598 if (arg_count != 0) { |
| 599 SetException("startDaemon: bad number of arguments"); |
| 600 return false; |
| 601 } |
| 602 bool stop_result = daemon_controller_->Stop(); |
| 603 BOOLEAN_TO_NPVARIANT(stop_result, *result); |
| 604 return true; |
| 605 } |
| 606 |
| 569 void HostNPScriptObject::DisconnectInternal() { | 607 void HostNPScriptObject::DisconnectInternal() { |
| 570 if (!host_context_.network_message_loop()->BelongsToCurrentThread()) { | 608 if (!host_context_.network_message_loop()->BelongsToCurrentThread()) { |
| 571 host_context_.network_message_loop()->PostTask( | 609 host_context_.network_message_loop()->PostTask( |
| 572 FROM_HERE, base::Bind(&HostNPScriptObject::DisconnectInternal, | 610 FROM_HERE, base::Bind(&HostNPScriptObject::DisconnectInternal, |
| 573 base::Unretained(this))); | 611 base::Unretained(this))); |
| 574 return; | 612 return; |
| 575 } | 613 } |
| 576 | 614 |
| 577 switch (state_) { | 615 switch (state_) { |
| 578 case kDisconnected: | 616 case kDisconnected: |
| (...skipping 257 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 836 uint32_t argCount) { | 874 uint32_t argCount) { |
| 837 NPVariant np_result; | 875 NPVariant np_result; |
| 838 bool is_good = g_npnetscape_funcs->invokeDefault(plugin_, func, args, | 876 bool is_good = g_npnetscape_funcs->invokeDefault(plugin_, func, args, |
| 839 argCount, &np_result); | 877 argCount, &np_result); |
| 840 if (is_good) | 878 if (is_good) |
| 841 g_npnetscape_funcs->releasevariantvalue(&np_result); | 879 g_npnetscape_funcs->releasevariantvalue(&np_result); |
| 842 return is_good; | 880 return is_good; |
| 843 } | 881 } |
| 844 | 882 |
| 845 } // namespace remoting | 883 } // namespace remoting |
| OLD | NEW |