424 lines
16 KiB
Plaintext
424 lines
16 KiB
Plaintext
/*
|
|
* Copyright (C) 2013-2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#import "config.h"
|
|
|
|
#import "QuickTimePluginReplacement.h"
|
|
|
|
#import "CommonVM.h"
|
|
#import "Event.h"
|
|
#import "Frame.h"
|
|
#import "HTMLPlugInElement.h"
|
|
#import "HTMLVideoElement.h"
|
|
#import "JSDOMBinding.h"
|
|
#import "JSDOMConvertNullable.h"
|
|
#import "JSDOMConvertSequences.h"
|
|
#import "JSDOMConvertStrings.h"
|
|
#import "JSDOMGlobalObject.h"
|
|
#import "JSHTMLVideoElement.h"
|
|
#import "JSQuickTimePluginReplacement.h"
|
|
#import "Logging.h"
|
|
#import "RenderElement.h"
|
|
#import "ScriptController.h"
|
|
#import "ScriptSourceCode.h"
|
|
#import "Settings.h"
|
|
#import "ShadowRoot.h"
|
|
#import "UserAgentScripts.h"
|
|
#import <AVFoundation/AVMetadataItem.h>
|
|
#import <Foundation/NSString.h>
|
|
#import <JavaScriptCore/APICast.h>
|
|
#import <JavaScriptCore/CatchScope.h>
|
|
#import <JavaScriptCore/JavaScriptCore.h>
|
|
#import <objc/runtime.h>
|
|
#import <wtf/text/Base64.h>
|
|
|
|
#import <pal/cf/CoreMediaSoftLink.h>
|
|
#import <pal/cocoa/AVFoundationSoftLink.h>
|
|
|
|
namespace WebCore {
|
|
|
|
#if PLATFORM(IOS_FAMILY)
|
|
static JSValue *jsValueWithValueInContext(id, JSContext *);
|
|
static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItem *, JSContext *);
|
|
#endif
|
|
|
|
static String quickTimePluginReplacementScript()
|
|
{
|
|
static NeverDestroyed<String> script(StringImpl::createWithoutCopying(QuickTimePluginReplacementJavaScript, sizeof(QuickTimePluginReplacementJavaScript)));
|
|
return script;
|
|
}
|
|
|
|
void QuickTimePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
|
|
{
|
|
registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL, isEnabledBySettings));
|
|
}
|
|
|
|
Ref<PluginReplacement> QuickTimePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
|
|
{
|
|
return adoptRef(*new QuickTimePluginReplacement(plugin, paramNames, paramValues));
|
|
}
|
|
|
|
bool QuickTimePluginReplacement::supportsMimeType(const String& mimeType)
|
|
{
|
|
static const auto typeHash = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
|
|
"application/vnd.apple.mpegurl", "application/x-mpegurl", "audio/3gpp", "audio/3gpp2", "audio/aac", "audio/aiff",
|
|
"audio/amr", "audio/basic", "audio/mp3", "audio/mp4", "audio/mpeg", "audio/mpeg3", "audio/mpegurl", "audio/scpls",
|
|
"audio/wav", "audio/x-aac", "audio/x-aiff", "audio/x-caf", "audio/x-m4a", "audio/x-m4b", "audio/x-m4p",
|
|
"audio/x-m4r", "audio/x-mp3", "audio/x-mpeg", "audio/x-mpeg3", "audio/x-mpegurl", "audio/x-scpls", "audio/x-wav",
|
|
"video/3gpp", "video/3gpp2", "video/mp4", "video/quicktime", "video/x-m4v"
|
|
});
|
|
return typeHash.get().contains(mimeType);
|
|
}
|
|
|
|
bool QuickTimePluginReplacement::supportsFileExtension(const String& extension)
|
|
{
|
|
static const auto extensionSet = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
|
|
"3g2", "3gp", "3gp2", "3gpp", "aac", "adts", "aif", "aifc", "aiff", "AMR", "au", "bwf", "caf", "cdda", "m3u",
|
|
"m3u8", "m4a", "m4b", "m4p", "m4r", "m4v", "mov", "mp3", "mp3", "mp4", "mpeg", "mpg", "mqv", "pls", "qt",
|
|
"snd", "swa", "ts", "ulw", "wav"
|
|
});
|
|
return extensionSet.get().contains(extension);
|
|
}
|
|
|
|
bool QuickTimePluginReplacement::isEnabledBySettings(const Settings& settings)
|
|
{
|
|
return settings.quickTimePluginReplacementEnabled();
|
|
}
|
|
|
|
QuickTimePluginReplacement::QuickTimePluginReplacement(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
|
|
: m_parentElement(makeWeakPtr(plugin))
|
|
, m_names(paramNames)
|
|
, m_values(paramValues)
|
|
{
|
|
}
|
|
|
|
QuickTimePluginReplacement::~QuickTimePluginReplacement() = default;
|
|
|
|
RenderPtr<RenderElement> QuickTimePluginReplacement::createElementRenderer(HTMLPlugInElement& plugin, RenderStyle&& style, const RenderTreePosition& insertionPosition)
|
|
{
|
|
ASSERT_UNUSED(plugin, m_parentElement == &plugin);
|
|
|
|
if (m_mediaElement)
|
|
return m_mediaElement->createElementRenderer(WTFMove(style), insertionPosition);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
DOMWrapperWorld& QuickTimePluginReplacement::isolatedWorld()
|
|
{
|
|
static DOMWrapperWorld& isolatedWorld = DOMWrapperWorld::create(commonVM(), DOMWrapperWorld::Type::Internal, "QuickTimePluginReplacement"_s).leakRef();
|
|
return isolatedWorld;
|
|
}
|
|
|
|
bool QuickTimePluginReplacement::ensureReplacementScriptInjected()
|
|
{
|
|
if (!m_parentElement->document().frame())
|
|
return false;
|
|
|
|
DOMWrapperWorld& world = isolatedWorld();
|
|
ScriptController& scriptController = m_parentElement->document().frame()->script();
|
|
JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
|
|
JSC::VM& vm = globalObject->vm();
|
|
JSC::JSLockHolder lock(vm);
|
|
auto scope = DECLARE_CATCH_SCOPE(vm);
|
|
JSC::JSGlobalObject* lexicalGlobalObject = globalObject;
|
|
|
|
JSC::JSValue replacementFunction = globalObject->get(lexicalGlobalObject, JSC::Identifier::fromString(vm, "createPluginReplacement"));
|
|
if (replacementFunction.isCallable(vm))
|
|
return true;
|
|
|
|
scriptController.evaluateInWorldIgnoringException(ScriptSourceCode(quickTimePluginReplacementScript()), world);
|
|
if (UNLIKELY(scope.exception())) {
|
|
LOG(Plugins, "%p - Exception when evaluating QuickTime plugin replacement script", this);
|
|
scope.clearException();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
auto QuickTimePluginReplacement::installReplacement(ShadowRoot& root) -> InstallResult
|
|
{
|
|
if (!ensureReplacementScriptInjected())
|
|
return { false };
|
|
|
|
if (!m_parentElement->document().frame())
|
|
return { false };
|
|
|
|
DOMWrapperWorld& world = isolatedWorld();
|
|
ScriptController& scriptController = m_parentElement->document().frame()->script();
|
|
JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
|
|
JSC::VM& vm = globalObject->vm();
|
|
JSC::JSLockHolder lock(vm);
|
|
auto scope = DECLARE_CATCH_SCOPE(vm);
|
|
JSC::JSGlobalObject* lexicalGlobalObject = globalObject;
|
|
|
|
auto clearExceptionAndReturnFalse = [&] () -> InstallResult {
|
|
scope.clearException();
|
|
return { false };
|
|
};
|
|
|
|
// Lookup the "createPluginReplacement" function.
|
|
JSC::JSValue replacementFunction = globalObject->get(lexicalGlobalObject, JSC::Identifier::fromString(vm, "createPluginReplacement"));
|
|
if (replacementFunction.isUndefinedOrNull())
|
|
return { false };
|
|
JSC::JSObject* replacementObject = replacementFunction.toObject(lexicalGlobalObject);
|
|
RETURN_IF_EXCEPTION(scope, clearExceptionAndReturnFalse());
|
|
|
|
auto callData = getCallData(vm, replacementObject);
|
|
if (callData.type == JSC::CallData::Type::None)
|
|
return { false };
|
|
|
|
JSC::MarkedArgumentBuffer argList;
|
|
argList.append(toJS(lexicalGlobalObject, globalObject, &root));
|
|
argList.append(toJS(lexicalGlobalObject, globalObject, m_parentElement.get()));
|
|
argList.append(toJS(lexicalGlobalObject, globalObject, this));
|
|
argList.append(toJS<IDLSequence<IDLNullable<IDLDOMString>>>(*lexicalGlobalObject, *globalObject, m_names));
|
|
argList.append(toJS<IDLSequence<IDLNullable<IDLDOMString>>>(*lexicalGlobalObject, *globalObject, m_values));
|
|
ASSERT(!argList.hasOverflowed());
|
|
JSC::JSValue replacement = call(lexicalGlobalObject, replacementObject, callData, globalObject, argList);
|
|
RETURN_IF_EXCEPTION(scope, clearExceptionAndReturnFalse());
|
|
|
|
// Get the <video> created to replace the plug-in.
|
|
JSC::JSValue value = replacement.get(lexicalGlobalObject, JSC::Identifier::fromString(vm, "video"));
|
|
if (!scope.exception() && !value.isUndefinedOrNull())
|
|
m_mediaElement = JSHTMLVideoElement::toWrapped(vm, value);
|
|
|
|
if (!m_mediaElement) {
|
|
LOG(Plugins, "%p - Failed to find <video> element created by QuickTime plugin replacement script.", this);
|
|
scope.clearException();
|
|
return { false };
|
|
}
|
|
|
|
// Get the scripting interface.
|
|
value = replacement.get(lexicalGlobalObject, JSC::Identifier::fromString(vm, "scriptObject"));
|
|
if (!value.isObject()) {
|
|
LOG(Plugins, "%p - Failed to find script object created by QuickTime plugin replacement.", this);
|
|
scope.clearException();
|
|
return { false };
|
|
}
|
|
|
|
return { true, value };
|
|
}
|
|
|
|
unsigned long long QuickTimePluginReplacement::movieSize() const
|
|
{
|
|
if (m_mediaElement)
|
|
return m_mediaElement->fileSize();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void QuickTimePluginReplacement::postEvent(const String& eventName)
|
|
{
|
|
Ref<HTMLPlugInElement> protect(*m_parentElement);
|
|
Ref<Event> event = Event::create(eventName, Event::CanBubble::No, Event::IsCancelable::Yes);
|
|
m_parentElement->dispatchEvent(event);
|
|
}
|
|
|
|
#if PLATFORM(IOS_FAMILY)
|
|
|
|
static JSValue *jsValueWithDataInContext(NSData *data, const String& mimeType, JSContext *context)
|
|
{
|
|
// FIXME: Add makeCFString/makeNSString to avoid unnecessary String allocation.
|
|
String data64;
|
|
if (!mimeType.isEmpty())
|
|
data64 = makeString("data:", mimeType, ";base64,", base64Encoded([data bytes], [data length]));
|
|
else
|
|
data64 = makeString("data:text/plain;base64,", base64Encoded([data bytes], [data length]));
|
|
|
|
return [JSValue valueWithObject:(id)data64.createCFString().get() inContext:context];
|
|
}
|
|
|
|
static JSValue *jsValueWithArrayInContext(NSArray *array, JSContext *context)
|
|
{
|
|
JSValueRef exception = 0;
|
|
JSValue *result = [JSValue valueWithNewArrayInContext:context];
|
|
JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
|
|
if (exception)
|
|
return [JSValue valueWithUndefinedInContext:context];
|
|
|
|
NSUInteger count = [array count];
|
|
for (NSUInteger i = 0; i < count; ++i) {
|
|
JSValue *value = jsValueWithValueInContext([array objectAtIndex:i], context);
|
|
if (!value)
|
|
continue;
|
|
|
|
JSObjectSetPropertyAtIndex([context JSGlobalContextRef], resultObject, (unsigned)i, [value JSValueRef], &exception);
|
|
if (exception)
|
|
continue;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static JSValue *jsValueWithDictionaryInContext(NSDictionary *dictionary, JSContext *context)
|
|
{
|
|
JSValueRef exception = 0;
|
|
JSValue *result = [JSValue valueWithNewObjectInContext:context];
|
|
JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
|
|
if (exception)
|
|
return [JSValue valueWithUndefinedInContext:context];
|
|
|
|
for (id key in [dictionary keyEnumerator]) {
|
|
if (![key isKindOfClass:[NSString class]])
|
|
continue;
|
|
|
|
JSValue *value = jsValueWithValueInContext([dictionary objectForKey:key], context);
|
|
if (!value)
|
|
continue;
|
|
|
|
auto name = OpaqueJSString::tryCreate(key);
|
|
JSObjectSetProperty([context JSGlobalContextRef], resultObject, name.get(), [value JSValueRef], 0, &exception);
|
|
if (exception)
|
|
continue;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static JSValue *jsValueWithValueInContext(id value, JSContext *context)
|
|
{
|
|
if ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])
|
|
return [JSValue valueWithObject:value inContext:context];
|
|
else if ([value isKindOfClass:[NSLocale class]])
|
|
return [JSValue valueWithObject:[value localeIdentifier] inContext:context];
|
|
else if ([value isKindOfClass:[NSDictionary class]])
|
|
return jsValueWithDictionaryInContext(value, context);
|
|
else if ([value isKindOfClass:[NSArray class]])
|
|
return jsValueWithArrayInContext(value, context);
|
|
else if ([value isKindOfClass:[NSData class]])
|
|
return jsValueWithDataInContext(value, emptyString(), context);
|
|
else if ([value isKindOfClass:PAL::getAVMetadataItemClass()])
|
|
return jsValueWithAVMetadataItemInContext(value, context);
|
|
|
|
return nil;
|
|
}
|
|
|
|
static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItem *item, JSContext *context)
|
|
{
|
|
NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:[item extraAttributes]];
|
|
|
|
if (item.keySpace)
|
|
[dictionary setObject:item.keySpace forKey:@"keyspace"];
|
|
|
|
if (item.key)
|
|
[dictionary setObject:item.key forKey:@"key"];
|
|
|
|
if (item.locale)
|
|
[dictionary setObject:item.locale forKey:@"locale"];
|
|
|
|
if (CMTIME_IS_VALID(item.time)) {
|
|
if (auto timeDictionary = adoptCF(PAL::CMTimeCopyAsDictionary(item.time, kCFAllocatorDefault)))
|
|
[dictionary setObject:(__bridge NSDictionary *)timeDictionary.get() forKey:@"timestamp"];
|
|
}
|
|
|
|
if (item.value) {
|
|
id value = item.value;
|
|
NSString *mimeType = [[item extraAttributes] objectForKey:@"MIMEtype"];
|
|
if ([value isKindOfClass:[NSData class]] && mimeType) {
|
|
auto data64 = makeString("data:", String(mimeType), ";base64,", base64Encoded([value bytes], [value length]));
|
|
[dictionary setObject:(__bridge NSString *)data64.createCFString().get() forKey:@"value"];
|
|
} else
|
|
[dictionary setObject:value forKey:@"value"];
|
|
}
|
|
|
|
return jsValueWithDictionaryInContext(dictionary, context);
|
|
}
|
|
|
|
#endif
|
|
|
|
JSC::JSValue JSQuickTimePluginReplacement::timedMetaData(JSC::JSGlobalObject& state) const
|
|
{
|
|
#if PLATFORM(IOS_FAMILY)
|
|
HTMLVideoElement* parent = wrapped().parentElement();
|
|
if (!parent || !parent->player())
|
|
return JSC::jsNull();
|
|
|
|
Frame* frame = parent->document().frame();
|
|
if (!frame)
|
|
return JSC::jsNull();
|
|
|
|
NSArray *metaData = parent->player()->timedMetadata();
|
|
if (!metaData)
|
|
return JSC::jsNull();
|
|
|
|
JSContext *jsContext = frame->script().javaScriptContext();
|
|
JSValue *metaDataValue = jsValueWithValueInContext(metaData, jsContext);
|
|
|
|
return toJS(&state, [metaDataValue JSValueRef]);
|
|
#else
|
|
UNUSED_PARAM(state);
|
|
return JSC::jsNull();
|
|
#endif
|
|
}
|
|
|
|
JSC::JSValue JSQuickTimePluginReplacement::accessLog(JSC::JSGlobalObject& state) const
|
|
{
|
|
#if PLATFORM(IOS_FAMILY)
|
|
HTMLVideoElement* parent = wrapped().parentElement();
|
|
if (!parent || !parent->player())
|
|
return JSC::jsNull();
|
|
|
|
Frame* frame = parent->document().frame();
|
|
if (!frame)
|
|
return JSC::jsNull();
|
|
|
|
JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
|
|
String accessLogString = parent->player()->accessLog();
|
|
[dictionary setValue:static_cast<NSString *>(accessLogString) forProperty:(NSString *)CFSTR("extendedLog")];
|
|
|
|
return toJS(&state, [dictionary JSValueRef]);
|
|
#else
|
|
UNUSED_PARAM(state);
|
|
return JSC::jsNull();
|
|
#endif
|
|
}
|
|
|
|
JSC::JSValue JSQuickTimePluginReplacement::errorLog(JSC::JSGlobalObject& state) const
|
|
{
|
|
#if PLATFORM(IOS_FAMILY)
|
|
HTMLVideoElement* parent = wrapped().parentElement();
|
|
if (!parent || !parent->player())
|
|
return JSC::jsNull();
|
|
|
|
Frame* frame = parent->document().frame();
|
|
if (!frame)
|
|
return JSC::jsNull();
|
|
|
|
JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
|
|
String errorLogString = parent->player()->errorLog();
|
|
[dictionary setValue:static_cast<NSString *>(errorLogString) forProperty:(NSString *)CFSTR("extendedLog")];
|
|
|
|
return toJS(&state, [dictionary JSValueRef]);
|
|
#else
|
|
UNUSED_PARAM(state);
|
|
return JSC::jsNull();
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|