Unreviewed, rolling out r244627.
[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
61 typedef AVMetadataItem AVMetadataItemType;
62 SOFT_LINK_FRAMEWORK_OPTIONAL(AVFoundation)
63 SOFT_LINK_CLASS(AVFoundation, AVMetadataItem)
64 #define AVMetadataItem getAVMetadataItemClass()
65
66 namespace WebCore {
67 using namespace PAL;
68
69 #if PLATFORM(IOS_FAMILY)
70 static JSValue *jsValueWithValueInContext(id, JSContext *);
71 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *, JSContext *);
72 #endif
73
74 static String quickTimePluginReplacementScript()
75 {
76     static NeverDestroyed<String> script(QuickTimePluginReplacementJavaScript, sizeof(QuickTimePluginReplacementJavaScript));
77     return script;
78 }
79
80 void QuickTimePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
81 {
82     registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL, isEnabledBySettings));
83 }
84
85 Ref<PluginReplacement> QuickTimePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
86 {
87     return adoptRef(*new QuickTimePluginReplacement(plugin, paramNames, paramValues));
88 }
89
90 bool QuickTimePluginReplacement::supportsMimeType(const String& mimeType)
91 {
92     static const auto typeHash = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
93         "application/vnd.apple.mpegurl", "application/x-mpegurl", "audio/3gpp", "audio/3gpp2", "audio/aac", "audio/aiff",
94         "audio/amr", "audio/basic", "audio/mp3", "audio/mp4", "audio/mpeg", "audio/mpeg3", "audio/mpegurl", "audio/scpls",
95         "audio/wav", "audio/x-aac", "audio/x-aiff", "audio/x-caf", "audio/x-m4a", "audio/x-m4b", "audio/x-m4p",
96         "audio/x-m4r", "audio/x-mp3", "audio/x-mpeg", "audio/x-mpeg3", "audio/x-mpegurl", "audio/x-scpls", "audio/x-wav",
97         "video/3gpp", "video/3gpp2", "video/mp4", "video/quicktime", "video/x-m4v"
98     });
99     return typeHash.get().contains(mimeType);
100 }
101
102 bool QuickTimePluginReplacement::supportsFileExtension(const String& extension)
103 {
104     static const auto extensionSet = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
105         "3g2", "3gp", "3gp2", "3gpp", "aac", "adts", "aif", "aifc", "aiff", "AMR", "au", "bwf", "caf", "cdda", "m3u",
106         "m3u8", "m4a", "m4b", "m4p", "m4r", "m4v", "mov", "mp3", "mp3", "mp4", "mpeg", "mpg", "mqv", "pls", "qt",
107         "snd", "swa", "ts", "ulw", "wav"
108     });
109     return extensionSet.get().contains(extension);
110 }
111
112 bool QuickTimePluginReplacement::isEnabledBySettings(const Settings& settings)
113 {
114     return settings.quickTimePluginReplacementEnabled();
115 }
116
117 QuickTimePluginReplacement::QuickTimePluginReplacement(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
118     : m_parentElement(&plugin)
119     , m_names(paramNames)
120     , m_values(paramValues)
121 {
122 }
123
124 QuickTimePluginReplacement::~QuickTimePluginReplacement()
125 {
126     // FIXME: Why is it useful to null out pointers in an object that is being destroyed?
127     m_parentElement = nullptr;
128     m_scriptObject = nullptr;
129     m_mediaElement = nullptr;
130 }
131
132 RenderPtr<RenderElement> QuickTimePluginReplacement::createElementRenderer(HTMLPlugInElement& plugin, RenderStyle&& style, const RenderTreePosition& insertionPosition)
133 {
134     ASSERT_UNUSED(plugin, m_parentElement == &plugin);
135
136     if (m_mediaElement)
137         return m_mediaElement->createElementRenderer(WTFMove(style), insertionPosition);
138
139     return nullptr;
140 }
141
142 DOMWrapperWorld& QuickTimePluginReplacement::isolatedWorld()
143 {
144     static DOMWrapperWorld& isolatedWorld = DOMWrapperWorld::create(commonVM()).leakRef();
145     return isolatedWorld;
146 }
147
148 bool QuickTimePluginReplacement::ensureReplacementScriptInjected()
149 {
150     if (!m_parentElement->document().frame())
151         return false;
152     
153     DOMWrapperWorld& world = isolatedWorld();
154     ScriptController& scriptController = m_parentElement->document().frame()->script();
155     JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
156     JSC::VM& vm = globalObject->vm();
157     JSC::JSLockHolder lock(vm);
158     auto scope = DECLARE_CATCH_SCOPE(vm);
159     JSC::ExecState* exec = globalObject->globalExec();
160     
161     JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier::fromString(exec, "createPluginReplacement"));
162     if (replacementFunction.isFunction(vm))
163         return true;
164     
165     scriptController.evaluateInWorld(ScriptSourceCode(quickTimePluginReplacementScript()), world);
166     if (UNLIKELY(scope.exception())) {
167         LOG(Plugins, "%p - Exception when evaluating QuickTime plugin replacement script", this);
168         scope.clearException();
169         return false;
170     }
171     
172     return true;
173 }
174
175 bool QuickTimePluginReplacement::installReplacement(ShadowRoot& root)
176 {
177     if (!ensureReplacementScriptInjected())
178         return false;
179
180     if (!m_parentElement->document().frame())
181         return false;
182
183     DOMWrapperWorld& world = isolatedWorld();
184     ScriptController& scriptController = m_parentElement->document().frame()->script();
185     JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
186     JSC::VM& vm = globalObject->vm();
187     JSC::JSLockHolder lock(vm);
188     auto scope = DECLARE_CATCH_SCOPE(vm);
189     JSC::ExecState* exec = globalObject->globalExec();
190
191     // Lookup the "createPluginReplacement" function.
192     JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier::fromString(exec, "createPluginReplacement"));
193     if (replacementFunction.isUndefinedOrNull())
194         return false;
195     JSC::JSObject* replacementObject = replacementFunction.toObject(exec);
196     scope.assertNoException();
197     JSC::CallData callData;
198     JSC::CallType callType = replacementObject->methodTable(vm)->getCallData(replacementObject, callData);
199     if (callType == JSC::CallType::None)
200         return false;
201
202     JSC::MarkedArgumentBuffer argList;
203     argList.append(toJS(exec, globalObject, &root));
204     argList.append(toJS(exec, globalObject, m_parentElement));
205     argList.append(toJS(exec, globalObject, this));
206     argList.append(toJS<IDLSequence<IDLNullable<IDLDOMString>>>(*exec, *globalObject, m_names));
207     argList.append(toJS<IDLSequence<IDLNullable<IDLDOMString>>>(*exec, *globalObject, m_values));
208     ASSERT(!argList.hasOverflowed());
209     JSC::JSValue replacement = call(exec, replacementObject, callType, callData, globalObject, argList);
210     if (UNLIKELY(scope.exception())) {
211         scope.clearException();
212         return false;
213     }
214
215     // Get the <video> created to replace the plug-in.
216     JSC::JSValue value = replacement.get(exec, JSC::Identifier::fromString(exec, "video"));
217     if (!scope.exception() && !value.isUndefinedOrNull())
218         m_mediaElement = JSHTMLVideoElement::toWrapped(vm, value);
219
220     if (!m_mediaElement) {
221         LOG(Plugins, "%p - Failed to find <video> element created by QuickTime plugin replacement script.", this);
222         scope.clearException();
223         return false;
224     }
225
226     // Get the scripting interface.
227     value = replacement.get(exec, JSC::Identifier::fromString(exec, "scriptObject"));
228     if (!scope.exception() && !value.isUndefinedOrNull()) {
229         m_scriptObject = value.toObject(exec);
230         scope.assertNoException();
231     }
232
233     if (!m_scriptObject) {
234         LOG(Plugins, "%p - Failed to find script object created by QuickTime plugin replacement.", this);
235         scope.clearException();
236         return false;
237     }
238
239     return true;
240 }
241
242 unsigned long long QuickTimePluginReplacement::movieSize() const
243 {
244     if (m_mediaElement)
245         return m_mediaElement->fileSize();
246
247     return 0;
248 }
249
250 void QuickTimePluginReplacement::postEvent(const String& eventName)
251 {
252     Ref<HTMLPlugInElement> protect(*m_parentElement);
253     Ref<Event> event = Event::create(eventName, Event::CanBubble::No, Event::IsCancelable::Yes);
254     m_parentElement->dispatchEvent(event);
255 }
256
257 #if PLATFORM(IOS_FAMILY)
258
259 static JSValue *jsValueWithDataInContext(NSData *data, const String& mimeType, JSContext *context)
260 {
261     Vector<char> base64Data;
262     base64Encode([data bytes], [data length], base64Data);
263
264     String data64;
265     if (!mimeType.isEmpty())
266         data64 = "data:" + mimeType + ";base64," + base64Data;
267     else
268         data64 = "data:text/plain;base64," + base64Data;
269
270     return [JSValue valueWithObject:(id)data64.createCFString().get() inContext:context];
271 }
272
273 static JSValue *jsValueWithArrayInContext(NSArray *array, JSContext *context)
274 {
275     JSValueRef exception = 0;
276     JSValue *result = [JSValue valueWithNewArrayInContext:context];
277     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
278     if (exception)
279         return [JSValue valueWithUndefinedInContext:context];
280
281     NSUInteger count = [array count];
282     for (NSUInteger i = 0; i < count; ++i) {
283         JSValue *value = jsValueWithValueInContext([array objectAtIndex:i], context);
284         if (!value)
285             continue;
286
287         JSObjectSetPropertyAtIndex([context JSGlobalContextRef], resultObject, (unsigned)i, [value JSValueRef], &exception);
288         if (exception)
289             continue;
290     }
291
292     return result;
293 }
294
295 static JSValue *jsValueWithDictionaryInContext(NSDictionary *dictionary, JSContext *context)
296 {
297     JSValueRef exception = 0;
298     JSValue *result = [JSValue valueWithNewObjectInContext:context];
299     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
300     if (exception)
301         return [JSValue valueWithUndefinedInContext:context];
302
303     for (id key in [dictionary keyEnumerator]) {
304         if (![key isKindOfClass:[NSString class]])
305             continue;
306
307         JSValue *value = jsValueWithValueInContext([dictionary objectForKey:key], context);
308         if (!value)
309             continue;
310
311         auto name = OpaqueJSString::tryCreate(key);
312         JSObjectSetProperty([context JSGlobalContextRef], resultObject, name.get(), [value JSValueRef], 0, &exception);
313         if (exception)
314             continue;
315     }
316
317     return result;
318 }
319
320 static JSValue *jsValueWithValueInContext(id value, JSContext *context)
321 {
322     if ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])
323         return [JSValue valueWithObject:value inContext:context];
324     else if ([value isKindOfClass:[NSLocale class]])
325         return [JSValue valueWithObject:[value localeIdentifier] inContext:context];
326     else if ([value isKindOfClass:[NSDictionary class]])
327         return jsValueWithDictionaryInContext(value, context);
328     else if ([value isKindOfClass:[NSArray class]])
329         return jsValueWithArrayInContext(value, context);
330     else if ([value isKindOfClass:[NSData class]])
331         return jsValueWithDataInContext(value, emptyString(), context);
332     else if ([value isKindOfClass:[AVMetadataItem class]])
333         return jsValueWithAVMetadataItemInContext(value, context);
334
335     return nil;
336 }
337
338 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *item, JSContext *context)
339 {
340     NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:[item extraAttributes]];
341
342     if (item.keySpace)
343         [dictionary setObject:item.keySpace forKey:@"keyspace"];
344
345     if (item.key)
346         [dictionary setObject:item.key forKey:@"key"];
347
348     if (item.locale)
349         [dictionary setObject:item.locale forKey:@"locale"];
350
351     if (CMTIME_IS_VALID(item.time)) {
352         if (auto timeDictionary = adoptCF(PAL::CMTimeCopyAsDictionary(item.time, kCFAllocatorDefault)))
353             [dictionary setObject:(__bridge NSDictionary *)timeDictionary.get() forKey:@"timestamp"];
354     }
355     
356     if (item.value) {
357         id value = item.value;
358         NSString *mimeType = [[item extraAttributes] objectForKey:@"MIMEtype"];
359         if ([value isKindOfClass:[NSData class]] && mimeType) {
360             Vector<char> base64Data;
361             base64Encode([value bytes], [value length], base64Data);
362             String data64 = "data:" + String(mimeType) + ";base64," + base64Data;
363             [dictionary setObject:(__bridge NSString *)data64.createCFString().get() forKey:@"value"];
364         } else
365             [dictionary setObject:value forKey:@"value"];
366     }
367
368     return jsValueWithDictionaryInContext(dictionary, context);
369 }
370
371 #endif
372
373 JSC::JSValue JSQuickTimePluginReplacement::timedMetaData(JSC::ExecState& state) const
374 {
375 #if PLATFORM(IOS_FAMILY)
376     HTMLVideoElement* parent = wrapped().parentElement();
377     if (!parent || !parent->player())
378         return JSC::jsNull();
379
380     Frame* frame = parent->document().frame();
381     if (!frame)
382         return JSC::jsNull();
383
384     NSArray *metaData = parent->player()->timedMetadata();
385     if (!metaData)
386         return JSC::jsNull();
387
388     JSContext *jsContext = frame->script().javaScriptContext();
389     JSValue *metaDataValue = jsValueWithValueInContext(metaData, jsContext);
390
391     return toJS(&state, [metaDataValue JSValueRef]);
392 #else
393     UNUSED_PARAM(state);
394     return JSC::jsNull();
395 #endif
396 }
397
398 JSC::JSValue JSQuickTimePluginReplacement::accessLog(JSC::ExecState& state) const
399 {
400 #if PLATFORM(IOS_FAMILY)
401     HTMLVideoElement* parent = wrapped().parentElement();
402     if (!parent || !parent->player())
403         return JSC::jsNull();
404
405     Frame* frame = parent->document().frame();
406     if (!frame)
407         return JSC::jsNull();
408
409     JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
410     String accessLogString = parent->player()->accessLog();
411     [dictionary setValue:static_cast<NSString *>(accessLogString) forProperty:(NSString *)CFSTR("extendedLog")];
412
413     return toJS(&state, [dictionary JSValueRef]);
414 #else
415     UNUSED_PARAM(state);
416     return JSC::jsNull();
417 #endif
418 }
419
420 JSC::JSValue JSQuickTimePluginReplacement::errorLog(JSC::ExecState& state) const
421 {
422 #if PLATFORM(IOS_FAMILY)
423     HTMLVideoElement* parent = wrapped().parentElement();
424     if (!parent || !parent->player())
425         return JSC::jsNull();
426
427     Frame* frame = parent->document().frame();
428     if (!frame)
429         return JSC::jsNull();
430
431     JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
432     String errorLogString = parent->player()->errorLog();
433     [dictionary setValue:static_cast<NSString *>(errorLogString) forProperty:(NSString *)CFSTR("extendedLog")];
434
435     return toJS(&state, [dictionary JSValueRef]);
436 #else
437     UNUSED_PARAM(state);
438     return JSC::jsNull();
439 #endif
440 }
441
442 }
443
444 #endif