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