Chromium Code Reviews| Index: third_party/WebKit/Source/modules/nfc/NFC.cpp |
| diff --git a/third_party/WebKit/Source/modules/nfc/NFC.cpp b/third_party/WebKit/Source/modules/nfc/NFC.cpp |
| index ab1afe7641ec86f18eefccadcbbaa5899af69c3b..bc72ad1caa4e828e900dc0fde06e049ec16b7b58 100644 |
| --- a/third_party/WebKit/Source/modules/nfc/NFC.cpp |
| +++ b/third_party/WebKit/Source/modules/nfc/NFC.cpp |
| @@ -4,17 +4,464 @@ |
| #include "modules/nfc/NFC.h" |
| +#include "bindings/core/v8/JSONValuesForV8.h" |
| #include "bindings/core/v8/ScriptPromiseResolver.h" |
| +#include "bindings/core/v8/V8ArrayBuffer.h" |
| +#include "core/dom/DOMArrayBuffer.h" |
| #include "core/dom/DOMException.h" |
| #include "core/dom/ExceptionCode.h" |
| +#include "core/frame/LocalDOMWindow.h" |
| +#include "modules/nfc/NFCError.h" |
| #include "modules/nfc/NFCMessage.h" |
| #include "modules/nfc/NFCPushOptions.h" |
| +#include "platform/mojo/MojoHelper.h" |
| +#include "public/platform/ServiceRegistry.h" |
| + |
| +namespace { |
| +const char kJsonMimePrefix[] = "application/"; |
| +const char kJsonMimeType[] = "application/json"; |
| +const char kOpaqueMimeType[] = "application/octet-stream"; |
| +const char kPlainTextMimeType[] = "text/plain"; |
| +const char kPlainTextMimePrefix[] = "text/"; |
| +const char kCharSetUTF8[] = ";charset=UTF-8"; |
| +} // anonymous namespace |
| + |
| +// Mojo type converters |
| +namespace mojo { |
| + |
| +using device::wtf::NFCMessage; |
| +using device::wtf::NFCMessagePtr; |
| +using device::wtf::NFCRecord; |
| +using device::wtf::NFCRecordPtr; |
| +using device::wtf::NFCRecordType; |
| +using device::wtf::NFCPushOptions; |
| +using device::wtf::NFCPushOptionsPtr; |
| +using device::wtf::NFCPushTarget; |
| + |
| +NFCPushTarget toNFCPushTarget(const WTF::String& target) |
| +{ |
| + if (target == "tag") |
| + return NFCPushTarget::TAG; |
| + |
| + if (target == "peer") |
| + return NFCPushTarget::PEER; |
| + |
| + return NFCPushTarget::ANY; |
| +} |
| + |
| +NFCRecordType toNFCRecordType(const WTF::String& recordType) |
| +{ |
| + if (recordType == "empty") |
| + return NFCRecordType::EMPTY; |
| + |
| + if (recordType == "text") |
| + return NFCRecordType::TEXT; |
| + |
| + if (recordType == "url") |
| + return NFCRecordType::URL; |
| + |
| + if (recordType == "json") |
| + return NFCRecordType::JSON; |
| + |
| + if (recordType == "opaque") |
| + return NFCRecordType::OPAQUE_RECORD; |
| + |
| + ASSERT_NOT_REACHED(); |
| + return NFCRecordType::EMPTY; |
| +} |
| + |
| +// https://w3c.github.io/web-nfc/#creating-web-nfc-message Step 2.1 |
| +// If NFCRecord type is not provided, deduct NFCRecord type from JS data type: |
| +// String or Number => 'text' record |
| +// ArrayBuffer => 'opaque' record |
| +// JSON serializable Object => 'json' record |
| +NFCRecordType deductRecordTypeFromDataType(const blink::NFCRecord& record) |
| +{ |
| + if (record.hasData()) { |
| + v8::Local<v8::Value> value = record.data().v8Value(); |
| + |
| + if (value->IsString() |
| + || (value->IsNumber() && !std::isnan(value.As<v8::Number>()->Value()))) { |
| + return NFCRecordType::TEXT; |
| + } |
| + |
| + if (value->IsObject() && !value->IsArrayBuffer()) { |
| + return NFCRecordType::JSON; |
| + } |
| + |
| + if (value->IsArrayBuffer()) { |
| + return NFCRecordType::OPAQUE_RECORD; |
| + } |
| + } |
| + |
| + return NFCRecordType::EMPTY; |
| +} |
| + |
| +void setMediaType(NFCRecordPtr& recordPtr, const WTF::String& recordMediaType, const WTF::String& defaultMediaType) |
| +{ |
| + recordPtr->mediaType = recordMediaType.isEmpty() ? defaultMediaType : recordMediaType; |
| +} |
| + |
| +template <> |
| +struct TypeConverter<mojo::WTFArray<uint8_t>, WTF::String> { |
| + static mojo::WTFArray<uint8_t> Convert(const WTF::String& string) |
| + { |
| + WTF::CString utf8String = string.utf8(); |
| + mojo::WTFArray<uint8_t> array(utf8String.length()); |
| + memcpy(&array.front(), utf8String.data(), utf8String.length()); |
| + return array; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<mojo::WTFArray<uint8_t>, blink::DOMArrayBuffer*> { |
| + static mojo::WTFArray<uint8_t> Convert(blink::DOMArrayBuffer* buffer) |
| + { |
| + mojo::WTFArray<uint8_t> array(buffer->byteLength()); |
| + memcpy(&array.front(), buffer->data(), buffer->byteLength()); |
| + return array; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCRecordPtr, WTF::String> { |
| + static NFCRecordPtr Convert(const WTF::String& string) |
| + { |
| + NFCRecordPtr record = NFCRecord::New(); |
| + record->recordType = NFCRecordType::TEXT; |
| + record->mediaType = kPlainTextMimeType; |
| + record->mediaType.append(kCharSetUTF8); |
| + record->data = mojo::WTFArray<uint8_t>::From(string); |
| + return record; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCRecordPtr, blink::DOMArrayBuffer*> { |
| + static NFCRecordPtr Convert(blink::DOMArrayBuffer* buffer) |
| + { |
| + NFCRecordPtr record = NFCRecord::New(); |
| + record->recordType = NFCRecordType::OPAQUE_RECORD; |
| + record->mediaType = kOpaqueMimeType; |
| + record->data = mojo::WTFArray<uint8_t>::From(buffer); |
| + return record; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCMessagePtr, WTF::String> { |
| + static NFCMessagePtr Convert(const WTF::String& string) |
| + { |
| + NFCMessagePtr message = NFCMessage::New(); |
| + message->data = mojo::WTFArray<NFCRecordPtr>::New(1); |
| + message->data[0] = NFCRecord::From(string); |
| + return message; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<mojo::WTFArray<uint8_t>, blink::ScriptValue> { |
| + static mojo::WTFArray<uint8_t> Convert(const blink::ScriptValue& scriptValue) |
| + { |
| + v8::Local<v8::Value> value = scriptValue.v8Value(); |
| + |
| + if (value->IsNumber()) |
| + return mojo::WTFArray<uint8_t>::From(WTF::String::number(value.As<v8::Number>()->Value())); |
| + |
| + if (value->IsString()) { |
| + blink::V8StringResource<> stringResource = value; |
| + if (stringResource.prepare()) |
| + return mojo::WTFArray<uint8_t>::From<WTF::String>(stringResource); |
| + } |
| + |
| + if (value->IsObject() && !value->IsArrayBuffer()) { |
| + RefPtr<blink::JSONValue> jsonResult = blink::toJSONValue(scriptValue.context(), value); |
| + if (jsonResult && (jsonResult->getType() == blink::JSONValue::TypeObject)) |
| + return mojo::WTFArray<uint8_t>::From(jsonResult->toJSONString()); |
| + } |
| + |
| + if (value->IsArrayBuffer()) |
| + return mojo::WTFArray<uint8_t>::From(blink::V8ArrayBuffer::toImpl(value.As<v8::Object>())); |
| + |
| + return nullptr; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCRecordPtr, blink::NFCRecord> { |
| + static NFCRecordPtr Convert(const blink::NFCRecord& record) |
| + { |
| + NFCRecordPtr recordPtr = NFCRecord::New(); |
| + |
| + if (record.hasRecordType()) |
| + recordPtr->recordType = toNFCRecordType(record.recordType()); |
| + else |
| + recordPtr->recordType = deductRecordTypeFromDataType(record); |
| + |
| + // If record type is "empty", no need to set media type or data. |
| + // https://w3c.github.io/web-nfc/#creating-web-nfc-message |
| + if (recordPtr->recordType == NFCRecordType::EMPTY) |
| + return recordPtr; |
| + |
| + switch (recordPtr->recordType) { |
| + case NFCRecordType::TEXT: |
| + case NFCRecordType::URL: |
| + setMediaType(recordPtr, record.mediaType(), kPlainTextMimeType); |
| + recordPtr->mediaType.append(kCharSetUTF8); |
| + break; |
| + case NFCRecordType::JSON: |
| + setMediaType(recordPtr, record.mediaType(), kJsonMimeType); |
| + break; |
| + case NFCRecordType::OPAQUE_RECORD: |
| + setMediaType(recordPtr, record.mediaType(), kOpaqueMimeType); |
| + break; |
| + default: |
| + ASSERT_NOT_REACHED(); |
| + break; |
| + } |
| + |
| + recordPtr->data = mojo::WTFArray<uint8_t>::From(record.data()); |
| + |
| + // If JS object cannot be converted to uint8_t array, return null, |
| + // interrupt NFCMessage conversion algorithm and reject promise with |
| + // SyntaxError exception. |
| + if (recordPtr->data.is_null()) |
| + return nullptr; |
| + |
| + return recordPtr; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCMessagePtr, blink::NFCMessage> { |
| + static NFCMessagePtr Convert(const blink::NFCMessage& message) |
| + { |
| + NFCMessagePtr messagePtr = NFCMessage::New(); |
| + messagePtr->url = message.url(); |
| + messagePtr->data.resize(message.data().size()); |
| + for (size_t i = 0; i < message.data().size(); ++i) { |
| + NFCRecordPtr record = NFCRecord::From(message.data()[i]); |
| + if (record.is_null()) |
| + return nullptr; |
| + |
| + messagePtr->data[i] = std::move(record); |
| + } |
| + messagePtr->data = mojo::WTFArray<NFCRecordPtr>::From(message.data()); |
| + return messagePtr; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCMessagePtr, blink::DOMArrayBuffer*> { |
| + static NFCMessagePtr Convert(blink::DOMArrayBuffer* buffer) |
| + { |
| + NFCMessagePtr message = NFCMessage::New(); |
| + message->data = mojo::WTFArray<NFCRecordPtr>::New(1); |
| + message->data[0] = NFCRecord::From(buffer); |
| + return message; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCMessagePtr, blink::NFCPushMessage> { |
| + static NFCMessagePtr Convert(const blink::NFCPushMessage& message) |
| + { |
| + if (message.isString()) |
| + return NFCMessage::From(message.getAsString()); |
| + |
| + if (message.isNFCMessage()) |
| + return NFCMessage::From(message.getAsNFCMessage()); |
| + |
| + if (message.isArrayBuffer()) |
| + return NFCMessage::From(message.getAsArrayBuffer()); |
| + |
| + ASSERT_NOT_REACHED(); |
| + return nullptr; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<NFCPushOptionsPtr, blink::NFCPushOptions> { |
| + static NFCPushOptionsPtr Convert(const blink::NFCPushOptions& pushOptions) |
| + { |
| + // https://w3c.github.io/web-nfc/#the-nfcpushoptions-dictionary |
| + // Default values for NFCPushOptions dictionary are: |
| + // target = 'any', timeout = Infinity, ignoreRead = true |
| + NFCPushOptionsPtr pushOptionsPtr = NFCPushOptions::New(); |
| + |
| + if (pushOptions.hasTarget()) |
| + pushOptionsPtr->target = toNFCPushTarget(pushOptions.target()); |
| + else |
| + pushOptionsPtr->target = NFCPushTarget::ANY; |
| + |
| + if (pushOptions.hasTimeout()) |
| + pushOptionsPtr->timeout = pushOptions.timeout(); |
| + else |
| + pushOptionsPtr->timeout = std::numeric_limits<double>::infinity(); |
| + |
| + if (pushOptions.hasIgnoreRead()) |
| + pushOptionsPtr->ignoreRead = pushOptions.ignoreRead(); |
| + else |
| + pushOptionsPtr->ignoreRead = true; |
| + |
| + return pushOptionsPtr; |
| + } |
| +}; |
| + |
| +} // namespace mojo |
| namespace blink { |
| + |
| +namespace { |
| + |
| +bool isValidTextRecord(const NFCRecord& record) |
| +{ |
| + v8::Local<v8::Value> value = record.data().v8Value(); |
| + if (!value->IsString() && !(value->IsNumber() && !std::isnan(value.As<v8::Number>()->Value()))) |
| + return false; |
| + |
| + if (record.hasMediaType() && !record.mediaType().startsWith(kPlainTextMimePrefix, TextCaseInsensitive)) |
| + return false; |
| + |
| + return true; |
| +} |
| + |
| +bool isValidURLRecord(const NFCRecord& record) |
| +{ |
| + if (!record.data().v8Value()->IsString()) |
| + return false; |
| + |
| + return isValidTextRecord(record); |
| +} |
| + |
| +bool isValidJSONRecord(const NFCRecord& record) |
| +{ |
| + v8::Local<v8::Value> value = record.data().v8Value(); |
| + if (!value->IsObject() || value->IsArrayBuffer()) |
| + return false; |
| + |
| + if (record.hasMediaType() && !record.mediaType().startsWith(kJsonMimePrefix, TextCaseInsensitive)) |
| + return false; |
| + |
| + return true; |
| +} |
| + |
| +bool isValidOpaqueRecord(const NFCRecord& record) |
| +{ |
| + return record.data().v8Value()->IsArrayBuffer(); |
| +} |
| + |
| +bool isValidNFCRecord(const NFCRecord& record) |
| +{ |
| + device::wtf::NFCRecordType type; |
| + if (record.hasRecordType()) { |
| + type = mojo::toNFCRecordType(record.recordType()); |
| + } else { |
| + type = mojo::deductRecordTypeFromDataType(record); |
| + |
| + // https://w3c.github.io/web-nfc/#creating-web-nfc-message |
| + // If NFCRecord.recordType is not set and record type cannot be deducted |
| + // from NFCRecord.data, reject promise with SyntaxError. |
| + if (type == device::wtf::NFCRecordType::EMPTY) |
| + return false; |
| + } |
| + |
| + // Non-empty records must have data. |
| + if (!record.hasData() && (type != device::wtf::NFCRecordType::EMPTY)) |
| + return false; |
| + |
| + switch (type) { |
| + case device::wtf::NFCRecordType::TEXT: |
| + return isValidTextRecord(record); |
| + case device::wtf::NFCRecordType::URL: |
| + return isValidURLRecord(record); |
| + case device::wtf::NFCRecordType::JSON: |
| + return isValidJSONRecord(record); |
| + case device::wtf::NFCRecordType::OPAQUE_RECORD: |
| + return isValidOpaqueRecord(record); |
| + case device::wtf::NFCRecordType::EMPTY: |
| + return !record.hasData() && record.mediaType().isEmpty(); |
| + } |
| + |
| + ASSERT_NOT_REACHED(); |
| + return false; |
| +} |
| + |
| +DOMException* isValidNFCRecordArray(const HeapVector<NFCRecord>& records) |
| +{ |
| + // https://w3c.github.io/web-nfc/#the-push-method |
| + // If NFCMessage.data is empty, reject promise with SyntaxError |
| + if (records.isEmpty()) |
| + return DOMException::create(SyntaxError); |
| + |
| + for (const auto& record : records) { |
| + if (!isValidNFCRecord(record)) |
| + return DOMException::create(SyntaxError); |
| + } |
| + |
| + return nullptr; |
| +} |
| + |
| +DOMException* isValidNFCPushMessage(const NFCPushMessage& message) |
| +{ |
| + if (!message.isNFCMessage() && !message.isString() && !message.isArrayBuffer()) |
| + return DOMException::create(TypeMismatchError); |
| + |
| + if (message.isNFCMessage()) { |
| + if (!message.getAsNFCMessage().hasData()) |
| + return DOMException::create(TypeMismatchError); |
| + |
| + return isValidNFCRecordArray(message.getAsNFCMessage().data()); |
| + } |
| + |
| + return nullptr; |
| +} |
| + |
| +bool setURL(const String& origin, device::wtf::NFCMessagePtr& message) |
| +{ |
| + KURL originURL(ParsedURLString, origin); |
| + |
| + if (!message->url.isEmpty() && originURL.canSetPathname()) { |
| + originURL.setPath(message->url); |
| + } |
| + |
| + message->url = originURL; |
| + return originURL.isValid(); |
| +} |
| + |
| +} // anonymous namespace |
| + |
| +using NFCMojoCallback = mojo::Callback<void(device::wtf::NFCErrorPtr)>; |
| +class NFCCallback final : public NFCMojoCallback::Runnable { |
|
Reilly Grant (use Gerrit)
2016/04/25 20:12:54
I've added a createBaseCallback() function in Mojo
shalamov
2016/04/26 13:17:50
Done.
|
| + WTF_MAKE_NONCOPYABLE(NFCCallback); |
| +public: |
| + using NFCCallbackPromiseAdapter = CallbackPromiseAdapter<void, NFCError>; |
| + explicit NFCCallback(ScriptPromiseResolver* resolver) |
| + : m_promiseAdapter(adoptPtr(new NFCCallbackPromiseAdapter(resolver))) { } |
| + |
| + ~NFCCallback() override |
| + { |
| + // If NFCService is not available or disappears when NFC HW is disabled, |
| + // reject promise with NotSupportedError exception. |
| + m_promiseAdapter->onError(device::wtf::NFCErrorType::NOT_SUPPORTED); |
| + } |
| + |
| + void Run(device::wtf::NFCErrorPtr error) override |
| + { |
| + if (error.is_null()) |
| + m_promiseAdapter->onSuccess(); |
| + else |
| + m_promiseAdapter->onError(error->error_type); |
| + } |
| + |
| +private: |
| + OwnPtr<NFCCallbackPromiseAdapter> m_promiseAdapter; |
| +}; |
| + |
| NFC::NFC(LocalFrame* frame) |
| - : LocalFrameLifecycleObserver(frame) |
| - , PageLifecycleObserver(frame ? frame->page() : 0) |
| + : PageLifecycleObserver(frame ? frame->page() : 0), |
| + m_client(this) |
| { |
| } |
| @@ -24,16 +471,50 @@ NFC* NFC::create(LocalFrame* frame) |
| return nfc; |
| } |
| -ScriptPromise NFC::push(ScriptState* scriptState, const NFCPushMessage& records, const NFCPushOptions& options) |
| +NFC::~NFC() = default; |
| + |
| +// https://w3c.github.io/web-nfc/#writing-or-pushing-content |
| +ScriptPromise NFC::push(ScriptState* scriptState, const NFCPushMessage& pushMessage, const NFCPushOptions& options) |
| { |
| - // TODO(shalamov): To be implemented. |
| - return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError)); |
| + String errorMessage; |
| + if (!scriptState->getExecutionContext()->isSecureContext(errorMessage)) { |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(SecurityError, errorMessage)); |
| + } |
| + |
| + DOMException* exception = isValidNFCPushMessage(pushMessage); |
| + if (exception) |
| + return ScriptPromise::rejectWithDOMException(scriptState, exception); |
| + |
| + if (!Initialize(scriptState->domWindow()->frame()->serviceRegistry())) |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError)); |
| + |
| + device::wtf::NFCMessagePtr message = device::wtf::NFCMessage::From(pushMessage); |
| + if (!message) |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(SyntaxError)); |
| + |
| + if (!setURL(scriptState->getExecutionContext()->getSecurityOrigin()->toString(), message)) |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(SyntaxError)); |
| + |
| + ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState); |
| + m_nfc->Push(std::move(message), device::wtf::NFCPushOptions::From(options), device::wtf::NFC::PushCallback(new NFCCallback(resolver))); |
| + |
| + return resolver->promise(); |
| } |
| ScriptPromise NFC::cancelPush(ScriptState* scriptState, const String& target) |
| { |
| - // TODO(shalamov): To be implemented. |
| - return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError)); |
| + String errorMessage; |
| + if (!scriptState->getExecutionContext()->isSecureContext(errorMessage)) { |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(SecurityError, errorMessage)); |
| + } |
| + |
| + if (!Initialize(scriptState->domWindow()->frame()->serviceRegistry())) |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError)); |
| + |
| + ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState); |
| + m_nfc->CancelPush(mojo::toNFCPushTarget(target), device::wtf::NFC::CancelPushCallback(new NFCCallback(resolver))); |
| + |
| + return resolver->promise(); |
| } |
| ScriptPromise NFC::watch(ScriptState* scriptState, MessageCallback* callback, const NFCWatchOptions& options) |
| @@ -54,21 +535,51 @@ ScriptPromise NFC::cancelWatch(ScriptState* scriptState) |
| return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError)); |
| } |
| -void NFC::willDetachFrameHost() |
| -{ |
| - // TODO(shalamov): To be implemented. |
| -} |
| - |
| void NFC::pageVisibilityChanged() |
| { |
| - // TODO(shalamov): To be implemented. When visibility is lost, |
| + // If service is not initialized, there cannot be any pending NFC activities |
| + if (!m_nfc) |
| + return; |
| + |
| // NFC operations should be suspended. |
| // https://w3c.github.io/web-nfc/#nfc-suspended |
| + if (page()->visibilityState() == PageVisibilityStateVisible) |
| + m_nfc->ResumeNFCOperations(); |
| + else |
| + m_nfc->SuspendNFCOperations(); |
| +} |
| + |
| +void NFC::OnWatch(uint32_t id, device::wtf::NFCMessagePtr message) |
| +{ |
| +} |
| + |
| +bool NFC::Initialize(ServiceRegistry* registry) |
| +{ |
| + DCHECK(registry); |
| + |
| + if (m_nfc) |
| + return true; |
| + |
| + registry->connectToRemoteService(mojo::GetProxy(&m_nfc)); |
| + |
| + if (m_nfc) { |
| + m_nfc.set_connection_error_handler(sameThreadBindForMojo(&NFC::OnError, this)); |
| + m_nfc->SetClient(m_client.CreateInterfacePtrAndBind()); |
| + } |
| + |
| + return m_nfc; |
| +} |
| + |
| +void NFC::OnError() |
| +{ |
| + // todo(shalamov): clear map that contains nfc watch callbacks |
| + if (m_client.is_bound()) |
| + m_client.Close(); |
| + m_nfc.reset(); |
| } |
| DEFINE_TRACE(NFC) |
| { |
| - LocalFrameLifecycleObserver::trace(visitor); |
| PageLifecycleObserver::trace(visitor); |
| } |