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
55SOFT_LINK_FRAMEWORK_OPTIONAL(CoreMedia)
56SOFT_LINK(CoreMedia, CMTimeCopyAsDictionary, CFDictionaryRef, (CMTime time, CFAllocatorRef allocator), (time, allocator))
57
58typedef AVMetadataItem AVMetadataItemType;
59SOFT_LINK_FRAMEWORK_OPTIONAL(AVFoundation)
60SOFT_LINK_CLASS(AVFoundation, AVMetadataItem)
61#define AVMetadataItem getAVMetadataItemClass()
62
63namespace WebCore {
64
65#if PLATFORM(IOS)
66static JSValue *jsValueWithValueInContext(id, JSContext *);
67static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *, JSContext *);
68#endif
69
70static String quickTimePluginReplacementScript()
71{
72    DEPRECATED_DEFINE_STATIC_LOCAL(String, script, (QuickTimePluginReplacementJavaScript, sizeof(QuickTimePluginReplacementJavaScript)));
73    return script;
74}
75
76void QuickTimePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
77{
78    registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL));
79}
80
81PassRefPtr<PluginReplacement> QuickTimePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
82{
83    return adoptRef(new QuickTimePluginReplacement(plugin, paramNames, paramValues));
84}
85
86bool 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
104bool 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
120QuickTimePluginReplacement::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
129QuickTimePluginReplacement::~QuickTimePluginReplacement()
130{
131    m_parentElement = nullptr;
132    m_scriptObject = nullptr;
133    m_mediaElement = nullptr;
134}
135
136RenderPtr<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(WTF::move(style));
142
143    return nullptr;
144}
145
146DOMWrapperWorld& QuickTimePluginReplacement::isolatedWorld()
147{
148    static DOMWrapperWorld& isolatedWorld = *DOMWrapperWorld::create(JSDOMWindow::commonVM()).leakRef();
149    return isolatedWorld;
150}
151
152bool 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    JSC::JSLockHolder lock(exec);
163
164    JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier(exec, "createPluginReplacement"));
165    if (replacementFunction.isFunction())
166        return true;
167
168    scriptController.evaluateInWorld(ScriptSourceCode(quickTimePluginReplacementScript()), world);
169    if (exec->hadException()) {
170        LOG(Plugins, "%p - Exception when evaluating QuickTime plugin replacement script", this);
171        exec->clearException();
172        return false;
173    }
174
175    return true;
176}
177
178bool QuickTimePluginReplacement::installReplacement(ShadowRoot* root)
179{
180    Page* page = m_parentElement->document().page();
181
182    if (!ensureReplacementScriptInjected())
183        return false;
184
185    DOMWrapperWorld& world = isolatedWorld();
186    ScriptController& scriptController = page->mainFrame().script();
187    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
188    JSC::ExecState* exec = globalObject->globalExec();
189    JSC::JSLockHolder lock(exec);
190
191    // Lookup the "createPluginReplacement" function.
192    JSC::JSValue replacementFunction = globalObject->get(exec, JSC::Identifier(exec, "createPluginReplacement"));
193    if (replacementFunction.isUndefinedOrNull())
194        return false;
195    JSC::JSObject* replacementObject = replacementFunction.toObject(exec);
196    JSC::CallData callData;
197    JSC::CallType callType = replacementObject->methodTable()->getCallData(replacementObject, callData);
198    if (callType == JSC::CallTypeNone)
199        return false;
200
201    JSC::MarkedArgumentBuffer argList;
202    argList.append(toJS(exec, globalObject, root));
203    argList.append(toJS(exec, globalObject, m_parentElement));
204    argList.append(toJS(exec, globalObject, this));
205    argList.append(toJS<String>(exec, globalObject, m_names));
206    argList.append(toJS<String>(exec, globalObject, m_values));
207    JSC::JSValue replacement = call(exec, replacementObject, callType, callData, globalObject, argList);
208    if (exec->hadException()) {
209        exec->clearException();
210        return false;
211    }
212
213    // Get the <video> created to replace the plug-in.
214    JSC::JSValue value = replacement.get(exec, JSC::Identifier(exec, "video"));
215    if (!exec->hadException() && !value.isUndefinedOrNull())
216        m_mediaElement = toHTMLVideoElement(value);
217
218    if (!m_mediaElement) {
219        LOG(Plugins, "%p - Failed to find <video> element created by QuickTime plugin replacement script.", this);
220        exec->clearException();
221        return false;
222    }
223
224    // Get the scripting interface.
225    value = replacement.get(exec, JSC::Identifier(exec, "scriptObject"));
226    if (!exec->hadException() && !value.isUndefinedOrNull())
227        m_scriptObject = value.toObject(exec);
228
229    if (!m_scriptObject) {
230        LOG(Plugins, "%p - Failed to find script object created by QuickTime plugin replacement.", this);
231        exec->clearException();
232        return false;
233    }
234
235    return true;
236}
237
238unsigned long long QuickTimePluginReplacement::movieSize() const
239{
240    if (m_mediaElement)
241        return m_mediaElement->fileSize();
242
243    return 0;
244}
245
246void QuickTimePluginReplacement::postEvent(const String& eventName)
247{
248    Ref<HTMLPlugInElement> protect(*m_parentElement);
249    RefPtr<Event> event = Event::create(eventName, false, true);
250    m_parentElement->dispatchEvent(event.get());
251}
252
253#if PLATFORM(IOS)
254static JSValue *jsValueWithDataInContext(NSData *data, const String& mimeType, JSContext *context)
255{
256    Vector<char> base64Data;
257    base64Encode([data bytes], [data length], base64Data);
258
259    String data64;
260    if (!mimeType.isEmpty())
261        data64 = "data:" + mimeType + ";base64," + base64Data;
262    else
263        data64 = "data:text/plain;base64," + base64Data;
264
265    return [JSValue valueWithObject:(id)data64.createCFString().get() inContext:context];
266}
267
268static JSValue *jsValueWithArrayInContext(NSArray *array, JSContext *context)
269{
270    JSValueRef exception = 0;
271    JSValue *result = [JSValue valueWithNewArrayInContext:context];
272    JSObjectRef resultObject = JSValueToObject([context JSGlobalContextRef], [result JSValueRef], &exception);
273    if (exception)
274        return [JSValue valueWithUndefinedInContext:context];
275
276    NSUInteger count = [array count];
277    for (NSUInteger i = 0; i < count; ++i) {
278        JSValue *value = jsValueWithValueInContext([array objectAtIndex:i], context);
279        if (!value)
280            continue;
281
282        JSObjectSetPropertyAtIndex([context JSGlobalContextRef], resultObject, (unsigned)i, [value JSValueRef], &exception);
283        if (exception)
284            continue;
285    }
286
287    return result;
288}
289
290
291static 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        JSStringRef name = JSStringCreateWithCFString((CFStringRef)key);
308        JSObjectSetProperty([context JSGlobalContextRef], resultObject, name, [value JSValueRef], 0, &exception);
309        if (exception)
310            continue;
311    }
312
313    return result;
314}
315
316static 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:[AVMetadataItem class]])
329        return jsValueWithAVMetadataItemInContext(value, context);
330
331    return nil;
332}
333
334static JSValue *jsValueWithAVMetadataItemInContext(AVMetadataItemType *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        CFDictionaryRef timeDict = CMTimeCopyAsDictionary(item.time, kCFAllocatorDefault);
349
350        if (timeDict) {
351            [dictionary setObject:(id)timeDict forKey:@"timestamp"];
352            CFRelease(timeDict);
353        }
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:(id)data64.createCFString().get() forKey:@"value"];
364        } else
365            [dictionary setObject:value forKey:@"value"];
366    }
367
368    return jsValueWithDictionaryInContext(dictionary, context);
369}
370#endif
371
372JSC::JSValue JSQuickTimePluginReplacement::timedMetaData(JSC::ExecState* exec) const
373{
374#if PLATFORM(IOS)
375    HTMLVideoElement* parent = impl().parentElement();
376    if (!parent || !parent->player())
377        return JSC::jsNull();
378
379    Frame* frame = parent->document().frame();
380    if (!frame)
381        return JSC::jsNull();
382
383    NSArray *metaData = parent->player()->timedMetadata();
384    if (!metaData)
385        return JSC::jsNull();
386
387    JSContext *jsContext = frame->script().javaScriptContext();
388    JSValue *metaDataValue = jsValueWithValueInContext(metaData, jsContext);
389
390    return toJS(exec, [metaDataValue JSValueRef]);
391#else
392    UNUSED_PARAM(exec);
393    return JSC::jsNull();
394#endif
395}
396
397JSC::JSValue JSQuickTimePluginReplacement::accessLog(JSC::ExecState* exec) const
398{
399#if PLATFORM(IOS)
400    HTMLVideoElement* parent = impl().parentElement();
401    if (!parent || !parent->player())
402        return JSC::jsNull();
403
404    Frame* frame = parent->document().frame();
405    if (!frame)
406        return JSC::jsNull();
407
408    JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
409    String accessLogString = parent->player()->accessLog();
410    [dictionary setValue:static_cast<NSString *>(accessLogString) forProperty:(NSString *)CFSTR("extendedLog")];
411
412    return toJS(exec, [dictionary JSValueRef]);
413#else
414    UNUSED_PARAM(exec);
415    return JSC::jsNull();
416#endif
417}
418
419JSC::JSValue JSQuickTimePluginReplacement::errorLog(JSC::ExecState* exec) const
420{
421#if PLATFORM(IOS)
422    HTMLVideoElement* parent = impl().parentElement();
423    if (!parent || !parent->player())
424        return JSC::jsNull();
425
426    Frame* frame = parent->document().frame();
427    if (!frame)
428        return JSC::jsNull();
429
430    JSValue *dictionary = [JSValue valueWithNewObjectInContext:frame->script().javaScriptContext()];
431    String errorLogString = parent->player()->errorLog();
432    [dictionary setValue:static_cast<NSString *>(errorLogString) forProperty:(NSString *)CFSTR("extendedLog")];
433
434    return toJS(exec, [dictionary JSValueRef]);
435#else
436    UNUSED_PARAM(exec);
437    return JSC::jsNull();
438#endif
439}
440
441}
442
443#endif
444