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