blob: 8b999171868054a86b23f1f9100bda426bf49cc6 [file] [log] [blame]
// Copyright 2019 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 "third_party/blink/renderer/modules/nfc/ndef_record.h"
#include "services/device/public/mojom/nfc.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer_view.h"
#include "third_party/blink/renderer/bindings/modules/v8/string_or_array_buffer_or_array_buffer_view_or_ndef_message_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_ndef_record_init.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_data_view.h"
#include "third_party/blink/renderer/modules/nfc/ndef_message.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
namespace blink {
using NDEFRecordDataSource =
StringOrArrayBufferOrArrayBufferViewOrNDEFMessageInit;
namespace {
WTF::Vector<uint8_t> GetUTF8DataFromString(const String& string) {
StringUTF8Adaptor utf8_string(string);
WTF::Vector<uint8_t> data;
data.Append(utf8_string.data(), utf8_string.size());
return data;
}
bool IsBufferSource(const NDEFRecordDataSource& data) {
return data.IsArrayBuffer() || data.IsArrayBufferView();
}
bool GetBytesOfBufferSource(const NDEFRecordDataSource& buffer_source,
WTF::Vector<uint8_t>* target,
ExceptionState& exception_state) {
DCHECK(IsBufferSource(buffer_source));
uint8_t* data;
size_t data_length;
if (buffer_source.IsArrayBuffer()) {
DOMArrayBuffer* array_buffer = buffer_source.GetAsArrayBuffer();
data = reinterpret_cast<uint8_t*>(array_buffer->Data());
data_length = array_buffer->ByteLength();
} else if (buffer_source.IsArrayBufferView()) {
const DOMArrayBufferView* array_buffer_view =
buffer_source.GetAsArrayBufferView().Get();
data = reinterpret_cast<uint8_t*>(array_buffer_view->BaseAddress());
data_length = array_buffer_view->byteLength();
} else {
NOTREACHED();
return false;
}
wtf_size_t checked_length;
if (!base::CheckedNumeric<wtf_size_t>(data_length)
.AssignIfValid(&checked_length)) {
exception_state.ThrowRangeError(
"The provided buffer source exceeds the maximum supported length");
return false;
}
target->Append(data, checked_length);
return true;
}
// https://w3c.github.io/web-nfc/#dfn-validate-external-type
// Validates |input| as an external type.
bool IsValidExternalType(const String& input) {
static const String kOtherCharsForCustomType(":!()+,-=@;$_*'.");
// Ensure |input| is an ASCII string.
if (!input.ContainsOnlyASCIIOrEmpty())
return false;
// As all characters in |input| is ASCII, limiting its length within 255 just
// limits the length of its utf-8 encoded bytes we finally write into the
// record payload.
if (input.IsEmpty() || input.length() > 255)
return false;
// Finds the first occurrence of ':'.
wtf_size_t colon_index = input.find(':');
if (colon_index == kNotFound)
return false;
// Validates the domain (the part before ':').
String domain = input.Left(colon_index);
if (domain.IsEmpty())
return false;
// TODO(https://crbug.com/520391): Validate |domain|.
// Validates the type (the part after ':').
String type = input.Substring(colon_index + 1);
if (type.IsEmpty())
return false;
for (wtf_size_t i = 0; i < type.length(); i++) {
if (!IsASCIIAlphanumeric(type[i]) &&
!kOtherCharsForCustomType.Contains(type[i])) {
return false;
}
}
return true;
}
// https://w3c.github.io/web-nfc/#dfn-validate-local-type
// Validates |input| as an local type.
bool IsValidLocalType(const String& input) {
// Ensure |input| is an ASCII string.
if (!input.ContainsOnlyASCIIOrEmpty())
return false;
// The prefix ':' will be omitted when we actually write the record type into
// the nfc tag. We're taking it into consideration for validating the length
// here.
if (input.length() < 2 || input.length() > 256)
return false;
if (input[0] != ':')
return false;
if (!IsASCIILower(input[1]) && !IsASCIIDigit(input[1]))
return false;
// TODO(https://crbug.com/520391): Validate |input| is not equal to the record
// type of any NDEF record defined in its containing NDEF message.
return true;
}
String getDocumentLanguage(const ExecutionContext* execution_context) {
String document_language;
if (execution_context) {
Element* document_element =
To<LocalDOMWindow>(execution_context)->document()->documentElement();
if (document_element) {
document_language = document_element->getAttribute(html_names::kLangAttr);
}
if (document_language.IsEmpty()) {
document_language = "en";
}
}
return document_language;
}
static NDEFRecord* CreateTextRecord(const ExecutionContext* execution_context,
const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#mapping-string-to-ndef
if (!record.hasData() ||
!(record.data().IsString() || IsBufferSource(record.data()))) {
exception_state.ThrowTypeError(
"The data for 'text' NDEFRecords must be a String or a BufferSource.");
return nullptr;
}
// Set language to lang if it exists, or the document element's lang
// attribute, or 'en'.
String language;
if (record.hasLang()) {
language = record.lang();
} else {
language = getDocumentLanguage(execution_context);
}
// Bits 0 to 5 define the length of the language tag
// https://w3c.github.io/web-nfc/#text-record
if (language.length() > 63) {
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"Lang length cannot be stored in 6 bit.");
return nullptr;
}
auto& data = record.data();
// TODO(crbug.com/1070871): Use encodingOr("utf-8").
String encoding_label = record.hasEncoding() ? record.encoding() : "utf-8";
WTF::Vector<uint8_t> bytes;
if (data.IsString()) {
if (encoding_label != "utf-8") {
exception_state.ThrowTypeError(
"A DOMString data source is always encoded as \"utf-8\" so other "
"encodings are not allowed.");
return nullptr;
}
StringUTF8Adaptor utf8_string(data.GetAsString());
bytes.Append(utf8_string.data(), utf8_string.size());
} else {
DCHECK(IsBufferSource(data));
if (encoding_label != "utf-8" && encoding_label != "utf-16" &&
encoding_label != "utf-16be" && encoding_label != "utf-16le") {
exception_state.ThrowTypeError(
"Encoding must be either \"utf-8\", \"utf-16\", \"utf-16be\", or "
"\"utf-16le\".");
return nullptr;
}
if (!GetBytesOfBufferSource(data, &bytes, exception_state)) {
return nullptr;
}
}
return MakeGarbageCollected<NDEFRecord>(id, encoding_label, language,
std::move(bytes));
}
// Create a 'url' record or an 'absolute-url' record.
static NDEFRecord* CreateUrlRecord(const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#mapping-url-to-ndef
if (!record.hasData() || !record.data().IsString()) {
exception_state.ThrowTypeError(
"The data for url NDEFRecord must be a String.");
return nullptr;
}
// No need to check mediaType according to the spec.
String url = record.data().GetAsString();
if (!KURL(NullURL(), url).IsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"Cannot parse data for url record.");
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kStandardized,
record.recordType(), id, GetUTF8DataFromString(url));
}
static NDEFRecord* CreateMimeRecord(const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#mapping-binary-data-to-ndef
if (!record.hasData() || !IsBufferSource(record.data())) {
exception_state.ThrowTypeError(
"The data for 'mime' NDEFRecord must be a BufferSource.");
return nullptr;
}
// ExtractMIMETypeFromMediaType() ignores parameters of the MIME type.
String mime_type;
if (record.hasMediaType() && !record.mediaType().IsEmpty()) {
mime_type = ExtractMIMETypeFromMediaType(AtomicString(record.mediaType()));
} else {
mime_type = "application/octet-stream";
}
WTF::Vector<uint8_t> bytes;
if (!GetBytesOfBufferSource(record.data(), &bytes, exception_state)) {
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(id, mime_type, bytes);
}
static NDEFRecord* CreateUnknownRecord(const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
if (!record.hasData() || !IsBufferSource(record.data())) {
exception_state.ThrowTypeError(
"The data for 'unknown' NDEFRecord must be a BufferSource.");
return nullptr;
}
WTF::Vector<uint8_t> bytes;
if (!GetBytesOfBufferSource(record.data(), &bytes, exception_state)) {
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kStandardized, "unknown",
id, bytes);
}
static NDEFRecord* CreateSmartPosterRecord(
const ExecutionContext* execution_context,
const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#dfn-map-smart-poster-to-ndef
if (!record.hasData() || !record.data().IsNDEFMessageInit()) {
exception_state.ThrowTypeError(
"The data for 'smart-poster' NDEFRecord must be an NDEFMessageInit.");
return nullptr;
}
NDEFMessage* payload_message = NDEFMessage::CreateAsPayloadOfSmartPoster(
execution_context, record.data().GetAsNDEFMessageInit(), exception_state);
if (exception_state.HadException())
return nullptr;
DCHECK(payload_message);
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kStandardized,
"smart-poster", id, payload_message);
}
static NDEFRecord* CreateExternalRecord(
const ExecutionContext* execution_context,
const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
const String& record_type = record.recordType();
// https://w3c.github.io/web-nfc/#dfn-map-external-data-to-ndef
if (record.hasData() && IsBufferSource(record.data())) {
WTF::Vector<uint8_t> bytes;
if (!GetBytesOfBufferSource(record.data(), &bytes, exception_state)) {
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kExternal, record_type,
id, bytes);
} else if (record.hasData() && record.data().IsNDEFMessageInit()) {
NDEFMessage* payload_message = NDEFMessage::Create(
execution_context, record.data().GetAsNDEFMessageInit(),
exception_state, /*is_embedded=*/true);
if (exception_state.HadException())
return nullptr;
DCHECK(payload_message);
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kExternal, record_type,
id, payload_message);
}
exception_state.ThrowTypeError(
"The data for external type NDEFRecord must be a BufferSource or an "
"NDEFMessageInit.");
return nullptr;
}
static NDEFRecord* CreateLocalRecord(const ExecutionContext* execution_context,
const String& id,
const NDEFRecordInit& record,
ExceptionState& exception_state) {
const String& record_type = record.recordType();
// https://w3c.github.io/web-nfc/#dfn-map-local-type-to-ndef
if (record.hasData() && IsBufferSource(record.data())) {
WTF::Vector<uint8_t> bytes;
if (!GetBytesOfBufferSource(record.data(), &bytes, exception_state)) {
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kLocal, record_type, id,
bytes);
} else if (record.hasData() && record.data().IsNDEFMessageInit()) {
NDEFMessage* payload_message = NDEFMessage::Create(
execution_context, record.data().GetAsNDEFMessageInit(),
exception_state, /*is_embedded=*/true);
if (exception_state.HadException())
return nullptr;
DCHECK(payload_message);
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kLocal, record_type, id,
payload_message);
}
exception_state.ThrowTypeError(
"The data for local type NDEFRecord must be a BufferSource or an "
"NDEFMessageInit.");
return nullptr;
}
} // namespace
// static
NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context,
const NDEFRecordInit* record,
ExceptionState& exception_state,
bool is_embedded) {
// https://w3c.github.io/web-nfc/#creating-ndef-record
const String& record_type = record->recordType();
// https://w3c.github.io/web-nfc/#dom-ndefrecordinit-mediatype
if (record->hasMediaType() && record_type != "mime") {
exception_state.ThrowTypeError(
"NDEFRecordInit#mediaType is only applicable for 'mime' records.");
return nullptr;
}
// https://w3c.github.io/web-nfc/#dfn-map-empty-record-to-ndef
if (record->hasId() && record_type == "empty") {
exception_state.ThrowTypeError(
"NDEFRecordInit#id is not applicable for 'empty' records.");
return nullptr;
}
// TODO(crbug.com/1070871): Use IdOr(String()).
String id;
if (record->hasId())
id = record->id();
if (record_type == "empty") {
// https://w3c.github.io/web-nfc/#mapping-empty-record-to-ndef
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kStandardized,
record_type, /*id=*/String(), WTF::Vector<uint8_t>());
} else if (record_type == "text") {
return CreateTextRecord(execution_context, id, *record, exception_state);
} else if (record_type == "url" || record_type == "absolute-url") {
return CreateUrlRecord(id, *record, exception_state);
} else if (record_type == "mime") {
return CreateMimeRecord(id, *record, exception_state);
} else if (record_type == "unknown") {
return CreateUnknownRecord(id, *record, exception_state);
} else if (record_type == "smart-poster") {
return CreateSmartPosterRecord(execution_context, id, *record,
exception_state);
} else if (IsValidExternalType(record_type)) {
return CreateExternalRecord(execution_context, id, *record,
exception_state);
} else if (IsValidLocalType(record_type)) {
if (!is_embedded) {
exception_state.ThrowTypeError(
"Local type records are only supposed to be embedded in the payload "
"of another record (smart-poster, external, or local).");
return nullptr;
}
return CreateLocalRecord(execution_context, id, *record, exception_state);
}
exception_state.ThrowTypeError("Invalid NDEFRecord type.");
return nullptr;
}
NDEFRecord::NDEFRecord(device::mojom::blink::NDEFRecordTypeCategory category,
const String& record_type,
const String& id,
WTF::Vector<uint8_t> data)
: category_(category),
record_type_(record_type),
id_(id),
payload_data_(std::move(data)) {
DCHECK_EQ(
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal,
IsValidLocalType(record_type_));
}
NDEFRecord::NDEFRecord(device::mojom::blink::NDEFRecordTypeCategory category,
const String& record_type,
const String& id,
NDEFMessage* payload_message)
: category_(category),
record_type_(record_type),
id_(id),
payload_message_(payload_message) {
DCHECK(record_type_ == "smart-poster" ||
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal ||
category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal);
DCHECK_EQ(
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal,
IsValidLocalType(record_type_));
}
NDEFRecord::NDEFRecord(const String& id,
const String& encoding,
const String& lang,
WTF::Vector<uint8_t> data)
: category_(device::mojom::blink::NDEFRecordTypeCategory::kStandardized),
record_type_("text"),
id_(id),
encoding_(encoding),
lang_(lang),
payload_data_(std::move(data)) {}
NDEFRecord::NDEFRecord(const ExecutionContext* execution_context,
const String& text)
: category_(device::mojom::blink::NDEFRecordTypeCategory::kStandardized),
record_type_("text"),
encoding_("utf-8"),
lang_(getDocumentLanguage(execution_context)),
payload_data_(GetUTF8DataFromString(text)) {}
NDEFRecord::NDEFRecord(const String& id,
const String& media_type,
WTF::Vector<uint8_t> data)
: category_(device::mojom::blink::NDEFRecordTypeCategory::kStandardized),
record_type_("mime"),
id_(id),
media_type_(media_type),
payload_data_(std::move(data)) {}
// Even if |record| is for a local type record, here we do not validate if it's
// in the context of a parent record but just expose to JS as is.
NDEFRecord::NDEFRecord(const device::mojom::blink::NDEFRecord& record)
: category_(record.category),
record_type_(record.record_type),
id_(record.id),
media_type_(record.media_type),
encoding_(record.encoding),
lang_(record.lang),
payload_data_(record.data),
payload_message_(
record.payload_message
? MakeGarbageCollected<NDEFMessage>(*record.payload_message)
: nullptr) {
DCHECK_NE(record_type_ == "mime", media_type_.IsNull());
DCHECK_EQ(
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal,
IsValidLocalType(record_type_));
}
const String& NDEFRecord::mediaType() const {
DCHECK_NE(record_type_ == "mime", media_type_.IsNull());
return media_type_;
}
DOMDataView* NDEFRecord::data() const {
// Step 4 in https://w3c.github.io/web-nfc/#dfn-parse-an-ndef-record
if (record_type_ == "empty") {
DCHECK(payload_data_.IsEmpty());
return nullptr;
}
DOMArrayBuffer* dom_buffer =
DOMArrayBuffer::Create(payload_data_.data(), payload_data_.size());
return DOMDataView::Create(dom_buffer, 0, payload_data_.size());
}
// https://w3c.github.io/web-nfc/#dfn-convert-ndefrecord-data-bytes
base::Optional<HeapVector<Member<NDEFRecord>>> NDEFRecord::toRecords(
ExceptionState& exception_state) const {
if (record_type_ != "smart-poster" &&
category_ != device::mojom::blink::NDEFRecordTypeCategory::kExternal &&
category_ != device::mojom::blink::NDEFRecordTypeCategory::kLocal) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Only {smart-poster, external, local} type records could have a ndef "
"message as payload.");
return base::nullopt;
}
if (!payload_message_)
return base::nullopt;
return payload_message_->records();
}
void NDEFRecord::Trace(Visitor* visitor) const {
visitor->Trace(payload_message_);
ScriptWrappable::Trace(visitor);
}
} // namespace blink