Streamline JSRetainPtr, fix leaks of JSString and JSGlobalContext
[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)
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)
258 static JSValue *jsValueWithDataInContext(NSData *data, const String& mimeType, JSContext *context)
259 {
260     Vector<char> base64Data;
261     base64Encode([data bytes], [data length], base64Data);
262
263     String data64;
264     if (!mimeType.isEmpty())
265         data64 = "data:" + mimeType + ";base64," + base64Data;
266     else
267         data64 = "data:text/plain;base64," + base64Data;
268
269     return [JSValue valueWithObject:(id)data64.createCFString().get() inContext:context];
270 }
271
272 static JSValue *jsValueWithArrayInContext(NSArray *array, JSContext *context)
273 {
274     JSValueRef exception = 0;
275     JSValue *result = [JSValue valueWithNewArrayInContext:context];
276     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
277     if (exception)
278         return [JSValue valueWithUndefinedInContext:context];
279
280     NSUInteger count = [array count];
281     for (NSUInteger i = 0; i < count; ++i) {
282         JSValue *value = jsValueWithValueInContext([array objectAtIndex:i], context);
283         if (!value)
284             continue;
285
286         JSObjectSetPropertyAtIndex([context JSGlobalContextRef], resultObject, (unsigned)i, [value JSValueRef], &exception);
287         if (exception)
288             continue;
289     }
290
291     return result;
292 }
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         JSStringRef name = JSStringCreateWithCFString((__bridge CFStringRef)key);
312         JSObjectSetProperty([context JSGlobalContextRef], resultObject, name, [value JSValueRef], 0, &exception);
313         JSStringRelease(name);
314         if (exception)
315             continue;
316     }
317
318     return result;
319 }
320
321 static JSValue *jsValueWithValueInContext(id value, JSContext *context)
322 {
323     if ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])
324         return [JSValue valueWithObject:value inContext:context];
325     else if ([value isKindOfClass:[NSLocale class]])
326         return [JSValue valueWithObject:[value localeIdentifier] inContext:context];
327     else if ([value isKindOfClass:[NSDictionary class]])
328         return jsValueWithDictionaryInContext(value, context);
329     else if ([value isKindOfClass:[NSArray class]])
330         return jsValueWithArrayInContext(value, context);
331     else if ([value isKindOfClass:[NSData class]])
332         return jsValueWithDataInContext(value, emptyString(), context);
333     else if ([value isKindOfClass:[AVMetadataItem class]])
334         return jsValueWithAVMetadataItemInContext(value, context);
335
336     return nil;
337 }
338
339 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *item, JSContext *context)
340 {
341     NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:[item extraAttributes]];
342
343     if (item.keySpace)
344         [dictionary setObject:item.keySpace forKey:@"keyspace"];
345
346     if (item.key)
347         [dictionary setObject:item.key forKey:@"key"];
348
349     if (item.locale)
350         [dictionary setObject:item.locale forKey:@"locale"];
351
352     if (CMTIME_IS_VALID(item.time)) {
353         CFDictionaryRef timeDict = PAL::CMTimeCopyAsDictionary(item.time, kCFAllocatorDefault);
354
355         if (timeDict) {
356             [dictionary setObject:(id)timeDict forKey:@"timestamp"];
357             CFRelease(timeDict);
358         }
359     }
360     
361     if (item.value) {
362         id value = item.value;
363         NSString *mimeType = [[item extraAttributes] objectForKey:@"MIMEtype"];
364         if ([value isKindOfClass:[NSData class]] && mimeType) {
365             Vector<char> base64Data;
366             base64Encode([value bytes], [value length], base64Data);
367             String data64 = "data:" + String(mimeType) + ";base64," + base64Data;
368             [dictionary setObject:(id)data64.createCFString().get() forKey:@"value"];
369         } else
370             [dictionary setObject:value forKey:@"value"];
371     }
372
373     return jsValueWithDictionaryInContext(dictionary, context);
374 }
375 #endif
376
377 JSC::JSValue JSQuickTimePluginReplacement::timedMetaData(JSC::ExecState& state) const
378 {
379 #if PLATFORM(IOS)
380     HTMLVideoElement* parent = wrapped().parentElement();
381     if (!parent || !parent->player())
382         return JSC::jsNull();
383
384     Frame* frame = parent->document().frame();
385     if (!frame)
386         return JSC::jsNull();
387
388     NSArray *metaData = parent->player()->timedMetadata();
389     if (!metaData)
390         return JSC::jsNull();
391
392     JSContext *jsContext = frame->script().javaScriptContext();
393     JSValue *metaDataValue = jsValueWithValueInContext(metaData, jsContext);
394     
395     return toJS(&state, [metaDataValue JSValueRef]);
396 #else
397     UNUSED_PARAM(state);
398     return JSC::jsNull();
399 #endif
400 }
401
402 JSC::JSValue JSQuickTimePluginReplacement::accessLog(JSC::ExecState& state) const
403 {
404 #if PLATFORM(IOS)
405     HTMLVideoElement* parent = wrapped().parentElement();
406     if (!parent || !parent->player())
407         return JSC::jsNull();
408
409     Frame* frame = parent->document().frame();
410     if (!frame)
411         return JSC::jsNull();
412
413     JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
414     String accessLogString = parent->player()->accessLog();
415     [dictionary setValue:static_cast<NSString *>(accessLogString) forProperty:(NSString *)CFSTR("extendedLog")];
416
417     return toJS(&state, [dictionary JSValueRef]);
418 #else
419     UNUSED_PARAM(state);
420     return JSC::jsNull();
421 #endif
422 }
423
424 JSC::JSValue JSQuickTimePluginReplacement::errorLog(JSC::ExecState& state) const
425 {
426 #if PLATFORM(IOS)
427     HTMLVideoElement* parent = wrapped().parentElement();
428     if (!parent || !parent->player())
429         return JSC::jsNull();
430
431     Frame* frame = parent->document().frame();
432     if (!frame)
433         return JSC::jsNull();
434
435     JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
436     String errorLogString = parent->player()->errorLog();
437     [dictionary setValue:static_cast<NSString *>(errorLogString) forProperty:(NSString *)CFSTR("extendedLog")];
438
439     return toJS(&state, [dictionary JSValueRef]);
440 #else
441     UNUSED_PARAM(state);
442     return JSC::jsNull();
443 #endif
444 }
445
446 }
447
448 #endif