aa8b052d1245eaecfcfe5c28e304b7efeeb4a034
[WebKit-https.git] / Source / WebCore / Modules / plugins / QuickTimePluginReplacement.mm
1 /*
2  * Copyright (C) 2013-2015 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 "Event.h"
33 #import "HTMLPlugInElement.h"
34 #import "HTMLVideoElement.h"
35 #import "JSDOMBinding.h"
36 #import "JSDOMGlobalObject.h"
37 #import "JSHTMLVideoElement.h"
38 #import "JSQuickTimePluginReplacement.h"
39 #import "Logging.h"
40 #import "MainFrame.h"
41 #import "Page.h"
42 #import "RenderElement.h"
43 #import "ScriptController.h"
44 #import "ScriptSourceCode.h"
45 #import "UserAgentScripts.h"
46 #import <objc/runtime.h>
47 #import <AVFoundation/AVFoundation.h>
48 #import <Foundation/NSString.h>
49 #import <JavaScriptCore/JavaScriptCore.h>
50 #import <JavaScriptCore/APICast.h>
51 #import <wtf/text/Base64.h>
52
53 #import "CoreMediaSoftLink.h"
54
55 typedef AVMetadataItem AVMetadataItemType;
56 SOFT_LINK_FRAMEWORK_OPTIONAL(AVFoundation)
57 SOFT_LINK_CLASS(AVFoundation, AVMetadataItem)
58 #define AVMetadataItem getAVMetadataItemClass()
59
60 namespace WebCore {
61
62 #if PLATFORM(IOS)
63 static JSValue *jsValueWithValueInContext(id, JSContext *);
64 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *, JSContext *);
65 #endif
66
67 static String quickTimePluginReplacementScript()
68 {
69     DEPRECATED_DEFINE_STATIC_LOCAL(String, script, (QuickTimePluginReplacementJavaScript, sizeof(QuickTimePluginReplacementJavaScript)));
70     return script;
71 }
72
73 void QuickTimePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
74 {
75     registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL));
76 }
77
78 PassRefPtr<PluginReplacement> QuickTimePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
79 {
80     return adoptRef(new QuickTimePluginReplacement(plugin, paramNames, paramValues));
81 }
82
83 bool QuickTimePluginReplacement::supportsMimeType(const String& mimeType)
84 {
85     static const char* types[] = {
86         "application/vnd.apple.mpegurl", "application/x-mpegurl", "audio/3gpp", "audio/3gpp2", "audio/aac", "audio/aiff",
87         "audio/amr", "audio/basic", "audio/mp3", "audio/mp4", "audio/mpeg", "audio/mpeg3", "audio/mpegurl", "audio/scpls",
88         "audio/wav", "audio/x-aac", "audio/x-aiff", "audio/x-caf", "audio/x-m4a", "audio/x-m4b", "audio/x-m4p",
89         "audio/x-m4r", "audio/x-mp3", "audio/x-mpeg", "audio/x-mpeg3", "audio/x-mpegurl", "audio/x-scpls", "audio/x-wav",
90         "video/3gpp", "video/3gpp2", "video/mp4", "video/quicktime", "video/x-m4v"
91     };
92     DEPRECATED_DEFINE_STATIC_LOCAL(HashSet<String>, typeHash, ());
93     if (!typeHash.size()) {
94         for (size_t i = 0; i < WTF_ARRAY_LENGTH(types); ++i)
95             typeHash.add(types[i]);
96     }
97
98     return typeHash.contains(mimeType);
99 }
100
101 bool QuickTimePluginReplacement::supportsFileExtension(const String& extension)
102 {
103     static const char* extensions[] = {
104         "3g2", "3gp", "3gp2", "3gpp", "aac", "adts", "aif", "aifc", "aiff", "AMR", "au", "bwf", "caf", "cdda", "m3u",
105         "m3u8", "m4a", "m4b", "m4p", "m4r", "m4v", "mov", "mp3", "mp3", "mp4", "mpeg", "mpg", "mqv", "pls", "qt",
106         "snd", "swa", "ts", "ulw", "wav"
107     };
108     DEPRECATED_DEFINE_STATIC_LOCAL(HashSet<String>, extensionHash, ());
109     if (!extensionHash.size()) {
110         for (size_t i = 0; i < WTF_ARRAY_LENGTH(extensions); ++i)
111             extensionHash.add(extensions[i]);
112     }
113
114     return extensionHash.contains(extension);
115 }
116
117 QuickTimePluginReplacement::QuickTimePluginReplacement(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
118     :PluginReplacement()
119     , m_parentElement(&plugin)
120     , m_names(paramNames)
121     , m_values(paramValues)
122     , m_scriptObject(nullptr)
123 {
124 }
125
126 QuickTimePluginReplacement::~QuickTimePluginReplacement()
127 {
128     m_parentElement = nullptr;
129     m_scriptObject = nullptr;
130     m_mediaElement = nullptr;
131 }
132
133 RenderPtr<RenderElement> QuickTimePluginReplacement::createElementRenderer(HTMLPlugInElement& plugin, Ref<RenderStyle>&& style, const RenderTreePosition& insertionPosition)
134 {
135     ASSERT_UNUSED(plugin, m_parentElement == &plugin);
136
137     if (m_mediaElement)
138         return m_mediaElement->createElementRenderer(WTFMove(style), insertionPosition);
139
140     return nullptr;
141 }
142
143 DOMWrapperWorld& QuickTimePluginReplacement::isolatedWorld()
144 {
145     static DOMWrapperWorld& isolatedWorld = DOMWrapperWorld::create(JSDOMWindow::commonVM()).leakRef();
146     return isolatedWorld;
147 }
148
149 bool QuickTimePluginReplacement::ensureReplacementScriptInjected()
150 {
151     if (!m_parentElement->document().frame())
152         return false;
153     
154     DOMWrapperWorld& world = isolatedWorld();
155     ScriptController& scriptController = m_parentElement->document().frame()->script();
156     JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
157     JSC::ExecState* exec = globalObject->globalExec();
158     JSC::JSLockHolder lock(exec);
159     
160     JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier::fromString(exec, "createPluginReplacement"));
161     if (replacementFunction.isFunction())
162         return true;
163     
164     scriptController.evaluateInWorld(ScriptSourceCode(quickTimePluginReplacementScript()), world);
165     if (exec->hadException()) {
166         LOG(Plugins, "%p - Exception when evaluating QuickTime plugin replacement script", this);
167         exec->clearException();
168         return false;
169     }
170     
171     return true;
172 }
173
174 bool QuickTimePluginReplacement::installReplacement(ShadowRoot* root)
175 {
176     if (!ensureReplacementScriptInjected())
177         return false;
178
179     if (!m_parentElement->document().frame())
180         return false;
181
182     DOMWrapperWorld& world = isolatedWorld();
183     ScriptController& scriptController = m_parentElement->document().frame()->script();
184     JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
185     JSC::ExecState* exec = globalObject->globalExec();
186     JSC::JSLockHolder lock(exec);
187     
188     // Lookup the "createPluginReplacement" function.
189     JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier::fromString(exec, "createPluginReplacement"));
190     if (replacementFunction.isUndefinedOrNull())
191         return false;
192     JSC::JSObject* replacementObject = replacementFunction.toObject(exec);
193     JSC::CallData callData;
194     JSC::CallType callType = replacementObject->methodTable()->getCallData(replacementObject, callData);
195     if (callType == JSC::CallTypeNone)
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<String>(exec, globalObject, m_names));
203     argList.append(toJS<String>(exec, globalObject, m_values));
204     JSC::JSValue replacement = call(exec, replacementObject, callType, callData, globalObject, argList);
205     if (exec->hadException()) {
206         exec->clearException();
207         return false;
208     }
209
210     // Get the <video> created to replace the plug-in.
211     JSC::JSValue value = replacement.get(exec, JSC::Identifier::fromString(exec, "video"));
212     if (!exec->hadException() && !value.isUndefinedOrNull())
213         m_mediaElement = JSHTMLVideoElement::toWrapped(value);
214
215     if (!m_mediaElement) {
216         LOG(Plugins, "%p - Failed to find <video> element created by QuickTime plugin replacement script.", this);
217         exec->clearException();
218         return false;
219     }
220
221     // Get the scripting interface.
222     value = replacement.get(exec, JSC::Identifier::fromString(exec, "scriptObject"));
223     if (!exec->hadException() && !value.isUndefinedOrNull())
224         m_scriptObject = value.toObject(exec);
225
226     if (!m_scriptObject) {
227         LOG(Plugins, "%p - Failed to find script object created by QuickTime plugin replacement.", this);
228         exec->clearException();
229         return false;
230     }
231
232     return true;
233 }
234
235 unsigned long long QuickTimePluginReplacement::movieSize() const
236 {
237     if (m_mediaElement)
238         return m_mediaElement->fileSize();
239
240     return 0;
241 }
242
243 void QuickTimePluginReplacement::postEvent(const String& eventName)
244 {
245     Ref<HTMLPlugInElement> protect(*m_parentElement);
246     Ref<Event> event = Event::create(eventName, false, true);
247     m_parentElement->dispatchEvent(event);
248 }
249
250 #if PLATFORM(IOS)
251 static JSValue *jsValueWithDataInContext(NSData *data, const String& mimeType, JSContext *context)
252 {
253     Vector<char> base64Data;
254     base64Encode([data bytes], [data length], base64Data);
255
256     String data64;
257     if (!mimeType.isEmpty())
258         data64 = "data:" + mimeType + ";base64," + base64Data;
259     else
260         data64 = "data:text/plain;base64," + base64Data;
261
262     return [JSValue valueWithObject:(id)data64.createCFString().get() inContext:context];
263 }
264
265 static JSValue *jsValueWithArrayInContext(NSArray *array, JSContext *context)
266 {
267     JSValueRef exception = 0;
268     JSValue *result = [JSValue valueWithNewArrayInContext:context];
269     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
270     if (exception)
271         return [JSValue valueWithUndefinedInContext:context];
272
273     NSUInteger count = [array count];
274     for (NSUInteger i = 0; i < count; ++i) {
275         JSValue *value = jsValueWithValueInContext([array objectAtIndex:i], context);
276         if (!value)
277             continue;
278
279         JSObjectSetPropertyAtIndex([context JSGlobalContextRef], resultObject, (unsigned)i, [value JSValueRef], &exception);
280         if (exception)
281             continue;
282     }
283
284     return result;
285 }
286
287
288 static JSValue *jsValueWithDictionaryInContext(NSDictionary *dictionary, JSContext *context)
289 {
290     JSValueRef exception = 0;
291     JSValue *result = [JSValue valueWithNewObjectInContext:context];
292     JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
293     if (exception)
294         return [JSValue valueWithUndefinedInContext:context];
295
296     for (id key in [dictionary keyEnumerator]) {
297         if (![key isKindOfClass:[NSString class]])
298             continue;
299
300         JSValue *value = jsValueWithValueInContext([dictionary objectForKey:key], context);
301         if (!value)
302             continue;
303
304         JSStringRef name = JSStringCreateWithCFString((CFStringRef)key);
305         JSObjectSetProperty([context JSGlobalContextRef], resultObject, name, [value JSValueRef], 0, &exception);
306         if (exception)
307             continue;
308     }
309
310     return result;
311 }
312
313 static JSValue *jsValueWithValueInContext(id value, JSContext *context)
314 {
315     if ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])
316         return [JSValue valueWithObject:value inContext:context];
317     else if ([value isKindOfClass:[NSLocale class]])
318         return [JSValue valueWithObject:[value localeIdentifier] inContext:context];
319     else if ([value isKindOfClass:[NSDictionary class]])
320         return jsValueWithDictionaryInContext(value, context);
321     else if ([value isKindOfClass:[NSArray class]])
322         return jsValueWithArrayInContext(value, context);
323     else if ([value isKindOfClass:[NSData class]])
324         return jsValueWithDataInContext(value, emptyString(), context);
325     else if ([value isKindOfClass:[AVMetadataItem class]])
326         return jsValueWithAVMetadataItemInContext(value, context);
327
328     return nil;
329 }
330
331 static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *item, JSContext *context)
332 {
333     NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:[item extraAttributes]];
334
335     if (item.keySpace)
336         [dictionary setObject:item.keySpace forKey:@"keyspace"];
337
338     if (item.key)
339         [dictionary setObject:item.key forKey:@"key"];
340
341     if (item.locale)
342         [dictionary setObject:item.locale forKey:@"locale"];
343
344     if (CMTIME_IS_VALID(item.time)) {
345         CFDictionaryRef timeDict = CMTimeCopyAsDictionary(item.time, kCFAllocatorDefault);
346
347         if (timeDict) {
348             [dictionary setObject:(id)timeDict forKey:@"timestamp"];
349             CFRelease(timeDict);
350         }
351     }
352     
353     if (item.value) {
354         id value = item.value;
355         NSString *mimeType = [[item extraAttributes] objectForKey:@"MIMEtype"];
356         if ([value isKindOfClass:[NSData class]] && mimeType) {
357             Vector<char> base64Data;
358             base64Encode([value bytes], [value length], base64Data);
359             String data64 = "data:" + String(mimeType) + ";base64," + base64Data;
360             [dictionary setObject:(id)data64.createCFString().get() forKey:@"value"];
361         } else
362             [dictionary setObject:value forKey:@"value"];
363     }
364
365     return jsValueWithDictionaryInContext(dictionary, context);
366 }
367 #endif
368
369 JSC::JSValue JSQuickTimePluginReplacement::timedMetaData(JSC::ExecState& state) const
370 {
371 #if PLATFORM(IOS)
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)
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)
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