42ea43957c80f3f8b99bbc599e741a729548fd09
[WebKit-https.git] / Source / WebCore / Modules / plugins / QuickTimePluginReplacement.mm
1 /*
2  * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #import "config.h"
27
28 #if ENABLE(MEDIA_CONTROLS_SCRIPT)
29
30 #import "QuickTimePluginReplacement.h"
31
32 #import "CommonVM.h"
33 #import "Event.h"
34 #import "Frame.h"
35 #import "HTMLPlugInElement.h"
36 #import "HTMLVideoElement.h"
37 #import "JSDOMBinding.h"
38 #import "JSDOMConvertNullable.h"
39 #import "JSDOMConvertSequences.h"
40 #import "JSDOMConvertStrings.h"
41 #import "JSDOMGlobalObject.h"
42 #import "JSHTMLVideoElement.h"
43 #import "JSQuickTimePluginReplacement.h"
44 #import "Logging.h"
45 #import "RenderElement.h"
46 #import "ScriptController.h"
47 #import "ScriptSourceCode.h"
48 #import "Settings.h"
49 #import "ShadowRoot.h"
50 #import "UserAgentScripts.h"
51 #import <AVFoundation/AVMetadataItem.h>
52 #import <Foundation/NSString.h>
53 #import <JavaScriptCore/APICast.h>
54 #import <JavaScriptCore/CatchScope.h>
55 #import <JavaScriptCore/JavaScriptCore.h>
56 #import <objc/runtime.h>
57 #import <wtf/text/Base64.h>
58
59 #import <pal/cf/CoreMediaSoftLink.h>
60 #import <pal/cocoa/AVFoundationSoftLink.h>
61
62 namespace WebCore {
63 using namespace PAL;
64
65 #if PLATFORM(IOS_FAMILY)
66 static JSValue *jsValueWithValueInContext(id, JSContext *);
67 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItem *, JSContext *);
68 #endif
69
70 static String quickTimePluginReplacementScript()
71 {
72     static NeverDestroyed<String> script(QuickTimePluginReplacementJavaScript, sizeof(QuickTimePluginReplacementJavaScript));
73     return script;
74 }
75
76 void QuickTimePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
77 {
78     registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL, isEnabledBySettings));
79 }
80
81 Ref<PluginReplacement> QuickTimePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
82 {
83     return adoptRef(*new QuickTimePluginReplacement(plugin, paramNames, paramValues));
84 }
85
86 bool QuickTimePluginReplacement::supportsMimeType(const String& mimeType)
87 {
88     static const auto typeHash = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
89         "application/vnd.apple.mpegurl", "application/x-mpegurl", "audio/3gpp", "audio/3gpp2", "audio/aac", "audio/aiff",
90         "audio/amr", "audio/basic", "audio/mp3", "audio/mp4", "audio/mpeg", "audio/mpeg3", "audio/mpegurl", "audio/scpls",
91         "audio/wav", "audio/x-aac", "audio/x-aiff", "audio/x-caf", "audio/x-m4a", "audio/x-m4b", "audio/x-m4p",
92         "audio/x-m4r", "audio/x-mp3", "audio/x-mpeg", "audio/x-mpeg3", "audio/x-mpegurl", "audio/x-scpls", "audio/x-wav",
93         "video/3gpp", "video/3gpp2", "video/mp4", "video/quicktime", "video/x-m4v"
94     });
95     return typeHash.get().contains(mimeType);
96 }
97
98 bool QuickTimePluginReplacement::supportsFileExtension(const String& extension)
99 {
100     static const auto extensionSet = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
101         "3g2", "3gp", "3gp2", "3gpp", "aac", "adts", "aif", "aifc", "aiff", "AMR", "au", "bwf", "caf", "cdda", "m3u",
102         "m3u8", "m4a", "m4b", "m4p", "m4r", "m4v", "mov", "mp3", "mp3", "mp4", "mpeg", "mpg", "mqv", "pls", "qt",
103         "snd", "swa", "ts", "ulw", "wav"
104     });
105     return extensionSet.get().contains(extension);
106 }
107
108 bool QuickTimePluginReplacement::isEnabledBySettings(const Settings& settings)
109 {
110     return settings.quickTimePluginReplacementEnabled();
111 }
112
113 QuickTimePluginReplacement::QuickTimePluginReplacement(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
114     : m_parentElement(&plugin)
115     , m_names(paramNames)
116     , m_values(paramValues)
117 {
118 }
119
120 QuickTimePluginReplacement::~QuickTimePluginReplacement()
121 {
122     // FIXME: Why is it useful to null out pointers in an object that is being destroyed?
123     m_parentElement = nullptr;
124     m_scriptObject = nullptr;
125     m_mediaElement = nullptr;
126 }
127
128 RenderPtr<RenderElement> QuickTimePluginReplacement::createElementRenderer(HTMLPlugInElement& plugin, RenderStyle&& style, const RenderTreePosition& insertionPosition)
129 {
130     ASSERT_UNUSED(plugin, m_parentElement == &plugin);
131
132     if (m_mediaElement)
133         return m_mediaElement->createElementRenderer(WTFMove(style), insertionPosition);
134
135     return nullptr;
136 }
137
138 DOMWrapperWorld& QuickTimePluginReplacement::isolatedWorld()
139 {
140     static DOMWrapperWorld& isolatedWorld = DOMWrapperWorld::create(commonVM()).leakRef();
141     return isolatedWorld;
142 }
143
144 bool QuickTimePluginReplacement::ensureReplacementScriptInjected()
145 {
146     if (!m_parentElement->document().frame())
147         return false;
148     
149     DOMWrapperWorld& world = isolatedWorld();
150     ScriptController& scriptController = m_parentElement->document().frame()->script();
151     JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
152     JSC::VM& vm = globalObject->vm();
153     JSC::JSLockHolder lock(vm);
154     auto scope = DECLARE_CATCH_SCOPE(vm);
155     JSC::ExecState* exec = globalObject->globalExec();
156     
157     JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier::fromString(exec, "createPluginReplacement"));
158     if (replacementFunction.isFunction(vm))
159         return true;
160     
161     scriptController.evaluateInWorld(ScriptSourceCode(quickTimePluginReplacementScript()), world);
162     if (UNLIKELY(scope.exception())) {
163         LOG(Plugins, "%p - Exception when evaluating QuickTime plugin replacement script", this);
164         scope.clearException();
165         return false;
166     }
167     
168     return true;
169 }
170
171 bool QuickTimePluginReplacement::installReplacement(ShadowRoot& root)
172 {
173     if (!ensureReplacementScriptInjected())
174         return false;
175
176     if (!m_parentElement->document().frame())
177         return false;
178
179     DOMWrapperWorld& world = isolatedWorld();
180     ScriptController& scriptController = m_parentElement->document().frame()->script();
181     JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
182     JSC::VM& vm = globalObject->vm();
183     JSC::JSLockHolder lock(vm);
184     auto scope = DECLARE_CATCH_SCOPE(vm);
185     JSC::ExecState* exec = globalObject->globalExec();
186
187     // Lookup the "createPluginReplacement" function.
188     JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier::fromString(exec, "createPluginReplacement"));
189     if (replacementFunction.isUndefinedOrNull())
190         return false;
191     JSC::JSObject* replacementObject = replacementFunction.toObject(exec);
192     scope.assertNoException();
193     JSC::CallData callData;
194     JSC::CallType callType = replacementObject->methodTable(vm)->getCallData(replacementObject, callData);
195     if (callType == JSC::CallType::None)
196         return false;
197
198     JSC::MarkedArgumentBuffer argList;
199     argList.append(toJS(exec, globalObject, &root));
200     argList.append(toJS(exec, globalObject, m_parentElement));
201     argList.append(toJS(exec, globalObject, this));
202     argList.append(toJS<IDLSequence<IDLNullable<IDLDOMString>>>(*exec, *globalObject, m_names));
203     argList.append(toJS<IDLSequence<IDLNullable<IDLDOMString>>>(*exec, *globalObject, m_values));
204     ASSERT(!argList.hasOverflowed());
205     JSC::JSValue replacement = call(exec, replacementObject, callType, callData, globalObject, argList);
206     if (UNLIKELY(scope.exception())) {
207         scope.clearException();
208         return false;
209     }
210
211     // Get the <video> created to replace the plug-in.
212     JSC::JSValue value = replacement.get(exec, JSC::Identifier::fromString(exec, "video"));
213     if (!scope.exception() && !value.isUndefinedOrNull())
214         m_mediaElement = JSHTMLVideoElement::toWrapped(vm, value);
215
216     if (!m_mediaElement) {
217         LOG(Plugins, "%p - Failed to find <video> element created by QuickTime plugin replacement script.", this);
218         scope.clearException();
219         return false;
220     }
221
222     // Get the scripting interface.
223     value = replacement.get(exec, JSC::Identifier::fromString(exec, "scriptObject"));
224     if (!scope.exception() && !value.isUndefinedOrNull()) {
225         m_scriptObject = value.toObject(exec);
226         scope.assertNoException();
227     }
228
229     if (!m_scriptObject) {
230         LOG(Plugins, "%p - Failed to find script object created by QuickTime plugin replacement.", this);
231         scope.clearException();
232         return false;
233     }
234
235     return true;
236 }
237
238 unsigned long long QuickTimePluginReplacement::movieSize() const
239 {
240     if (m_mediaElement)
241         return m_mediaElement->fileSize();
242
243     return 0;
244 }
245
246 void QuickTimePluginReplacement::postEvent(const String& eventName)
247 {
248     Ref<HTMLPlugInElement> protect(*m_parentElement);
249     Ref<Event> event = Event::create(eventName, Event::CanBubble::No, Event::IsCancelable::Yes);
250     m_parentElement->dispatchEvent(event);
251 }
252
253 #if PLATFORM(IOS_FAMILY)
254
255 static JSValue *jsValueWithDataInContext(NSData *data, const String& mimeType, JSContext *context)
256 {
257     Vector<char> base64Data;
258     base64Encode([data bytes], [data length], base64Data);
259
260     String data64;
261     if (!mimeType.isEmpty())
262         data64 = "data:" + mimeType + ";base64," + base64Data;
263     else
264         data64 = "data:text/plain;base64," + base64Data;
265
266     return [JSValue valueWithObject:(id)data64.createCFString().get() inContext:context];
267 }
268
269 static JSValue *jsValueWithArrayInContext(NSArray *array, JSContext *context)
270 {
271     JSValueRef exception = 0;
272     JSValue *result = [JSValue valueWithNewArrayInContext:context];
273     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
274     if (exception)
275         return [JSValue valueWithUndefinedInContext:context];
276
277     NSUInteger count = [array count];
278     for (NSUInteger i = 0; i < count; ++i) {
279         JSValue *value = jsValueWithValueInContext([array objectAtIndex:i], context);
280         if (!value)
281             continue;
282
283         JSObjectSetPropertyAtIndex([context JSGlobalContextRef], resultObject, (unsigned)i, [value JSValueRef], &exception);
284         if (exception)
285             continue;
286     }
287
288     return result;
289 }
290
291 static JSValue *jsValueWithDictionaryInContext(NSDictionary *dictionary, JSContext *context)
292 {
293     JSValueRef exception = 0;
294     JSValue *result = [JSValue valueWithNewObjectInContext:context];
295     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
296     if (exception)
297         return [JSValue valueWithUndefinedInContext:context];
298
299     for (id key in [dictionary keyEnumerator]) {
300         if (![key isKindOfClass:[NSString class]])
301             continue;
302
303         JSValue *value = jsValueWithValueInContext([dictionary objectForKey:key], context);
304         if (!value)
305             continue;
306
307         auto name = OpaqueJSString::tryCreate(key);
308         JSObjectSetProperty([context JSGlobalContextRef], resultObject, name.get(), [value JSValueRef], 0, &exception);
309         if (exception)
310             continue;
311     }
312
313     return result;
314 }
315
316 static JSValue *jsValueWithValueInContext(id value, JSContext *context)
317 {
318     if ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])
319         return [JSValue valueWithObject:value inContext:context];
320     else if ([value isKindOfClass:[NSLocale class]])
321         return [JSValue valueWithObject:[value localeIdentifier] inContext:context];
322     else if ([value isKindOfClass:[NSDictionary class]])
323         return jsValueWithDictionaryInContext(value, context);
324     else if ([value isKindOfClass:[NSArray class]])
325         return jsValueWithArrayInContext(value, context);
326     else if ([value isKindOfClass:[NSData class]])
327         return jsValueWithDataInContext(value, emptyString(), context);
328     else if ([value isKindOfClass:PAL::getAVMetadataItemClass()])
329         return jsValueWithAVMetadataItemInContext(value, context);
330
331     return nil;
332 }
333
334 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItem *item, JSContext *context)
335 {
336     NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:[item extraAttributes]];
337
338     if (item.keySpace)
339         [dictionary setObject:item.keySpace forKey:@"keyspace"];
340
341     if (item.key)
342         [dictionary setObject:item.key forKey:@"key"];
343
344     if (item.locale)
345         [dictionary setObject:item.locale forKey:@"locale"];
346
347     if (CMTIME_IS_VALID(item.time)) {
348         if (auto timeDictionary = adoptCF(PAL::CMTimeCopyAsDictionary(item.time, kCFAllocatorDefault)))
349             [dictionary setObject:(__bridge NSDictionary *)timeDictionary.get() forKey:@"timestamp"];
350     }
351     
352     if (item.value) {
353         id value = item.value;
354         NSString *mimeType = [[item extraAttributes] objectForKey:@"MIMEtype"];
355         if ([value isKindOfClass:[NSData class]] && mimeType) {
356             Vector<char> base64Data;
357             base64Encode([value bytes], [value length], base64Data);
358             String data64 = "data:" + String(mimeType) + ";base64," + base64Data;
359             [dictionary setObject:(__bridge NSString *)data64.createCFString().get() forKey:@"value"];
360         } else
361             [dictionary setObject:value forKey:@"value"];
362     }
363
364     return jsValueWithDictionaryInContext(dictionary, context);
365 }
366
367 #endif
368
369 JSC::JSValue JSQuickTimePluginReplacement::timedMetaData(JSC::ExecState& state) const
370 {
371 #if PLATFORM(IOS_FAMILY)
372     HTMLVideoElement* parent = wrapped().parentElement();
373     if (!parent || !parent->player())
374         return JSC::jsNull();
375
376     Frame* frame = parent->document().frame();
377     if (!frame)
378         return JSC::jsNull();
379
380     NSArray *metaData = parent->player()->timedMetadata();
381     if (!metaData)
382         return JSC::jsNull();
383
384     JSContext *jsContext = frame->script().javaScriptContext();
385     JSValue *metaDataValue = jsValueWithValueInContext(metaData, jsContext);
386
387     return toJS(&state, [metaDataValue JSValueRef]);
388 #else
389     UNUSED_PARAM(state);
390     return JSC::jsNull();
391 #endif
392 }
393
394 JSC::JSValue JSQuickTimePluginReplacement::accessLog(JSC::ExecState& state) const
395 {
396 #if PLATFORM(IOS_FAMILY)
397     HTMLVideoElement* parent = wrapped().parentElement();
398     if (!parent || !parent->player())
399         return JSC::jsNull();
400
401     Frame* frame = parent->document().frame();
402     if (!frame)
403         return JSC::jsNull();
404
405     JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
406     String accessLogString = parent->player()->accessLog();
407     [dictionary setValue:static_cast<NSString *>(accessLogString) forProperty:(NSString *)CFSTR("extendedLog")];
408
409     return toJS(&state, [dictionary JSValueRef]);
410 #else
411     UNUSED_PARAM(state);
412     return JSC::jsNull();
413 #endif
414 }
415
416 JSC::JSValue JSQuickTimePluginReplacement::errorLog(JSC::ExecState& state) const
417 {
418 #if PLATFORM(IOS_FAMILY)
419     HTMLVideoElement* parent = wrapped().parentElement();
420     if (!parent || !parent->player())
421         return JSC::jsNull();
422
423     Frame* frame = parent->document().frame();
424     if (!frame)
425         return JSC::jsNull();
426
427     JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
428     String errorLogString = parent->player()->errorLog();
429     [dictionary setValue:static_cast<NSString *>(errorLogString) forProperty:(NSString *)CFSTR("extendedLog")];
430
431     return toJS(&state, [dictionary JSValueRef]);
432 #else
433     UNUSED_PARAM(state);
434     return JSC::jsNull();
435 #endif
436 }
437
438 }
439
440 #endif