1/*
2 * Copyright (C) 2005, 2006 Apple Computer, 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 *
8 * 1.  Redistributions of source code must retain the above copyright
9 *     notice, this list of conditions and the following disclaimer.
10 * 2.  Redistributions in binary form must reproduce the above copyright
11 *     notice, this list of conditions and the following disclaimer in the
12 *     documentation and/or other materials provided with the distribution.
13 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 *     its contributors may be used to endorse or promote products derived
15 *     from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29
30#import "WebPluginController.h"
31
32#import "DOMNodeInternal.h"
33#import "WebDataSourceInternal.h"
34#import "WebFrameInternal.h"
35#import "WebFrameView.h"
36#import "WebHTMLViewPrivate.h"
37#import "WebKitErrorsPrivate.h"
38#import "WebKitLogging.h"
39#import "WebNSObjectExtras.h"
40#import "WebNSURLExtras.h"
41#import "WebNSViewExtras.h"
42#import "WebPlugin.h"
43#import "WebPluginContainer.h"
44#import "WebPluginContainerCheck.h"
45#import "WebPluginPackage.h"
46#import "WebPluginPrivate.h"
47#import "WebPluginViewFactory.h"
48#import "WebUIDelegate.h"
49#import "WebViewInternal.h"
50#import <Foundation/NSURLRequest.h>
51#import <WebCore/DocumentLoader.h>
52#import <WebCore/Frame.h>
53#import <WebCore/FrameLoader.h>
54#import <WebCore/HTMLMediaElement.h>
55#import <WebCore/HTMLNames.h>
56#import <WebCore/MediaPlayerProxy.h>
57#import <WebCore/PlatformString.h>
58#import <WebCore/ResourceRequest.h>
59#import <WebCore/ScriptController.h>
60#import <WebCore/WebCoreURLResponse.h>
61#import <objc/objc-runtime.h>
62#import <runtime/JSLock.h>
63
64using namespace WebCore;
65using namespace HTMLNames;
66
67@interface NSView (PluginSecrets)
68- (void)setContainingWindow:(NSWindow *)w;
69@end
70
71// For compatibility only.
72@interface NSObject (OldPluginAPI)
73+ (NSView *)pluginViewWithArguments:(NSDictionary *)arguments;
74@end
75
76@interface NSView (OldPluginAPI)
77- (void)pluginInitialize;
78- (void)pluginStart;
79- (void)pluginStop;
80- (void)pluginDestroy;
81@end
82
83static bool isKindOfClass(id, NSString* className);
84static void installFlip4MacPlugInWorkaroundIfNecessary();
85
86
87static NSMutableSet *pluginViews = nil;
88
89@implementation WebPluginController
90
91+ (NSView *)plugInViewWithArguments:(NSDictionary *)arguments fromPluginPackage:(WebPluginPackage *)pluginPackage
92{
93    [pluginPackage load];
94    Class viewFactory = [pluginPackage viewFactory];
95
96    NSView *view = nil;
97
98    if ([viewFactory respondsToSelector:@selector(plugInViewWithArguments:)]) {
99        JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
100        view = [viewFactory plugInViewWithArguments:arguments];
101    } else if ([viewFactory respondsToSelector:@selector(pluginViewWithArguments:)]) {
102        JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
103        view = [viewFactory pluginViewWithArguments:arguments];
104    }
105
106    if (view == nil) {
107        return nil;
108    }
109
110    if (pluginViews == nil) {
111        pluginViews = [[NSMutableSet alloc] init];
112    }
113    [pluginViews addObject:view];
114
115    return view;
116}
117
118+ (BOOL)isPlugInView:(NSView *)view
119{
120    return [pluginViews containsObject:view];
121}
122
123- (id)initWithDocumentView:(NSView *)view
124{
125    [super init];
126    _documentView = view;
127    _views = [[NSMutableArray alloc] init];
128    _checksInProgress = (NSMutableSet *)CFMakeCollectable(CFSetCreateMutable(NULL, 0, NULL));
129    return self;
130}
131
132- (void)setDataSource:(WebDataSource *)dataSource
133{
134    _dataSource = dataSource;
135}
136
137- (void)dealloc
138{
139    [_views release];
140    [_checksInProgress release];
141#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
142    [_viewsNotInDocument release];
143#endif
144    [super dealloc];
145}
146
147- (void)stopOnePlugin:(NSView *)view
148{
149    if ([view respondsToSelector:@selector(webPlugInStop)]) {
150        JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
151        [view webPlugInStop];
152    } else if ([view respondsToSelector:@selector(pluginStop)]) {
153        JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
154        [view pluginStop];
155    }
156}
157
158- (void)destroyOnePlugin:(NSView *)view
159{
160    if ([view respondsToSelector:@selector(webPlugInDestroy)]) {
161        JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
162        [view webPlugInDestroy];
163    } else if ([view respondsToSelector:@selector(pluginDestroy)]) {
164        JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
165        [view pluginDestroy];
166    }
167}
168
169- (void)startAllPlugins
170{
171    if (_started)
172        return;
173
174    if ([_views count] > 0)
175        LOG(Plugins, "starting WebKit plugins : %@", [_views description]);
176
177    int count = [_views count];
178    for (int i = 0; i < count; i++) {
179        id aView = [_views objectAtIndex:i];
180        if ([aView respondsToSelector:@selector(webPlugInStart)]) {
181            JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
182            [aView webPlugInStart];
183        } else if ([aView respondsToSelector:@selector(pluginStart)]) {
184            JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
185            [aView pluginStart];
186        }
187    }
188    _started = YES;
189}
190
191- (void)stopAllPlugins
192{
193    if (!_started)
194        return;
195
196    if ([_views count] > 0) {
197        LOG(Plugins, "stopping WebKit plugins: %@", [_views description]);
198    }
199
200    int viewsCount = [_views count];
201    for (int i = 0; i < viewsCount; i++)
202        [self stopOnePlugin:[_views objectAtIndex:i]];
203
204#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
205    int viewsNotInDocumentCount = [_viewsNotInDocument count];
206    for (int i = 0; i < viewsNotInDocumentCount; i++)
207        [self stopOnePlugin:[_viewsNotInDocument objectAtIndex:i]];
208#endif
209
210    _started = NO;
211}
212
213#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
214- (void)pluginViewCreated:(NSView *)view
215{
216    if (!_viewsNotInDocument)
217        _viewsNotInDocument= [[NSMutableArray alloc] init];
218    if (![_viewsNotInDocument containsObject:view])
219        [_viewsNotInDocument addObject:view];
220}
221
222+ (void)pluginViewHidden:(NSView *)view
223{
224    [pluginViews removeObject:view];
225}
226#endif
227
228- (void)addPlugin:(NSView *)view
229{
230    if (!_documentView) {
231        LOG_ERROR("can't add a plug-in to a defunct WebPluginController");
232        return;
233    }
234
235    if (![_views containsObject:view]) {
236        [_views addObject:view];
237        [[_documentView _webView] addPluginInstanceView:view];
238
239#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
240        if ([_viewsNotInDocument containsObject:view])
241            [_viewsNotInDocument removeObject:view];
242#endif
243
244        BOOL oldDefersCallbacks = [[self webView] defersCallbacks];
245        if (!oldDefersCallbacks)
246            [[self webView] setDefersCallbacks:YES];
247
248        if (isKindOfClass(view, @"WmvPlugin"))
249            installFlip4MacPlugInWorkaroundIfNecessary();
250
251        LOG(Plugins, "initializing plug-in %@", view);
252        if ([view respondsToSelector:@selector(webPlugInInitialize)]) {
253            JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
254            [view webPlugInInitialize];
255        } else if ([view respondsToSelector:@selector(pluginInitialize)]) {
256            JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
257            [view pluginInitialize];
258        }
259
260        if (!oldDefersCallbacks)
261            [[self webView] setDefersCallbacks:NO];
262
263        if (_started) {
264            LOG(Plugins, "starting plug-in %@", view);
265            if ([view respondsToSelector:@selector(webPlugInStart)]) {
266                JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
267                [view webPlugInStart];
268            } else if ([view respondsToSelector:@selector(pluginStart)]) {
269                JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
270                [view pluginStart];
271            }
272
273            if ([view respondsToSelector:@selector(setContainingWindow:)]) {
274                JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly);
275                [view setContainingWindow:[_documentView window]];
276            }
277        }
278    }
279}
280
281- (void)destroyPlugin:(NSView *)view
282{
283#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
284    if ([_views containsObject:view] || [_viewsNotInDocument containsObject:view]) {
285#else
286    if ([_views containsObject:view]) {
287#endif
288        if (_started)
289            [self stopOnePlugin:view];
290        [self destroyOnePlugin:view];
291
292#if ENABLE(NETSCAPE_PLUGIN_API)
293        if (Frame* frame = core([self webFrame]))
294            frame->script()->cleanupScriptObjectsForPlugin(self);
295#endif
296
297        [pluginViews removeObject:view];
298        [[_documentView _webView] removePluginInstanceView:view];
299        [_views removeObject:view];
300#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
301        [_viewsNotInDocument removeObject:view];
302#endif
303    }
304}
305
306- (void)_webPluginContainerCancelCheckIfAllowedToLoadRequest:(id)checkIdentifier
307{
308    [checkIdentifier cancel];
309    [_checksInProgress removeObject:checkIdentifier];
310}
311
312static void cancelOutstandingCheck(const void *item, void *context)
313{
314    [(id)item cancel];
315}
316
317- (void)_cancelOutstandingChecks
318{
319    if (_checksInProgress) {
320        CFSetApplyFunction((CFSetRef)_checksInProgress, cancelOutstandingCheck, NULL);
321        [_checksInProgress release];
322        _checksInProgress = nil;
323    }
324}
325
326- (void)destroyAllPlugins
327{
328    [self stopAllPlugins];
329
330    if ([_views count] > 0) {
331        LOG(Plugins, "destroying WebKit plugins: %@", [_views description]);
332    }
333
334    [self _cancelOutstandingChecks];
335
336    int viewsCount = [_views count];
337    for (int i = 0; i < viewsCount; i++) {
338        id aView = [_views objectAtIndex:i];
339        [self destroyOnePlugin:aView];
340
341#if ENABLE(NETSCAPE_PLUGIN_API)
342        if (Frame* frame = core([self webFrame]))
343            frame->script()->cleanupScriptObjectsForPlugin(self);
344#endif
345
346        [pluginViews removeObject:aView];
347        [[_documentView _webView] removePluginInstanceView:aView];
348    }
349
350#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
351    int viewsNotInDocumentCount = [_viewsNotInDocument count];
352    for (int i = 0; i < viewsNotInDocumentCount; i++)
353        [self destroyOnePlugin:[_viewsNotInDocument objectAtIndex:i]];
354#endif
355
356    [_views makeObjectsPerformSelector:@selector(removeFromSuperviewWithoutNeedingDisplay)];
357    [_views release];
358    _views = nil;
359
360    _documentView = nil;
361}
362
363- (id)_webPluginContainerCheckIfAllowedToLoadRequest:(NSURLRequest *)request inFrame:(NSString *)target resultObject:(id)obj selector:(SEL)selector
364{
365    WebPluginContainerCheck *check = [WebPluginContainerCheck checkWithRequest:request target:target resultObject:obj selector:selector controller:self contextInfo:nil];
366    [_checksInProgress addObject:check];
367    [check start];
368
369    return check;
370}
371
372- (void)webPlugInContainerLoadRequest:(NSURLRequest *)request inFrame:(NSString *)target
373{
374    if (!request) {
375        LOG_ERROR("nil URL passed");
376        return;
377    }
378    if (!_documentView) {
379        LOG_ERROR("could not load URL %@ because plug-in has already been destroyed", request);
380        return;
381    }
382    WebFrame *frame = [_dataSource webFrame];
383    if (!frame) {
384        LOG_ERROR("could not load URL %@ because plug-in has already been stopped", request);
385        return;
386    }
387    if (!target) {
388        target = @"_top";
389    }
390    NSString *JSString = [[request URL] _webkit_scriptIfJavaScriptURL];
391    if (JSString) {
392        if ([frame findFrameNamed:target] != frame) {
393            LOG_ERROR("JavaScript requests can only be made on the frame that contains the plug-in");
394            return;
395        }
396        [frame _stringByEvaluatingJavaScriptFromString:JSString];
397    } else {
398        if (!request) {
399            LOG_ERROR("could not load URL %@", [request URL]);
400            return;
401        }
402        core(frame)->loader()->load(request, target, false);
403    }
404}
405
406- (void)webPlugInContainerShowStatus:(NSString *)message
407{
408    if (!message)
409        message = @"";
410
411    WebView *v = [_dataSource _webView];
412    [[v _UIDelegateForwarder] webView:v setStatusText:message];
413}
414
415// For compatibility only.
416- (void)showStatus:(NSString *)message
417{
418    [self webPlugInContainerShowStatus:message];
419}
420
421- (NSColor *)webPlugInContainerSelectionColor
422{
423    bool primary = true;
424    if (Frame* frame = core([self webFrame]))
425        primary = frame->selection()->isFocusedAndActive();
426    return primary ? [NSColor selectedTextBackgroundColor] : [NSColor secondarySelectedControlColor];
427}
428
429// For compatibility only.
430- (NSColor *)selectionColor
431{
432    return [self webPlugInContainerSelectionColor];
433}
434
435- (WebFrame *)webFrame
436{
437    return [_dataSource webFrame];
438}
439
440- (WebView *)webView
441{
442    return [[self webFrame] webView];
443}
444
445- (NSString *)URLPolicyCheckReferrer
446{
447    NSURL *responseURL = [[[[self webFrame] _dataSource] response] URL];
448    ASSERT(responseURL);
449    return [responseURL _web_originalDataAsString];
450}
451
452- (void)pluginView:(NSView *)pluginView receivedResponse:(NSURLResponse *)response
453{
454    if ([pluginView respondsToSelector:@selector(webPlugInMainResourceDidReceiveResponse:)])
455        [pluginView webPlugInMainResourceDidReceiveResponse:response];
456    else {
457        // Cancel the load since this plug-in does its own loading.
458        // FIXME: See <rdar://problem/4258008> for a problem with this.
459        NSError *error = [[NSError alloc] _initWithPluginErrorCode:WebKitErrorPlugInWillHandleLoad
460                                                        contentURL:[response URL]
461                                                     pluginPageURL:nil
462                                                        pluginName:nil // FIXME: Get this from somewhere
463                                                          MIMEType:[response MIMEType]];
464        [_dataSource _documentLoader]->cancelMainResourceLoad(error);
465        [error release];
466    }
467}
468
469- (void)pluginView:(NSView *)pluginView receivedData:(NSData *)data
470{
471    if ([pluginView respondsToSelector:@selector(webPlugInMainResourceDidReceiveData:)])
472        [pluginView webPlugInMainResourceDidReceiveData:data];
473}
474
475- (void)pluginView:(NSView *)pluginView receivedError:(NSError *)error
476{
477    if ([pluginView respondsToSelector:@selector(webPlugInMainResourceDidFailWithError:)])
478        [pluginView webPlugInMainResourceDidFailWithError:error];
479}
480
481- (void)pluginViewFinishedLoading:(NSView *)pluginView
482{
483    if ([pluginView respondsToSelector:@selector(webPlugInMainResourceDidFinishLoading)])
484        [pluginView webPlugInMainResourceDidFinishLoading];
485}
486
487#if ENABLE(PLUGIN_PROXY_FOR_VIDEO)
488static WebCore::HTMLMediaElement* mediaProxyClient(DOMElement* element)
489{
490    if (!element) {
491        LOG_ERROR("nil element passed");
492        return nil;
493    }
494
495    Element* node = core(element);
496    if (!node || (!node->hasTagName(HTMLNames::videoTag) && !node->hasTagName(HTMLNames::audioTag))) {
497        LOG_ERROR("invalid media element passed");
498        return nil;
499    }
500
501    return static_cast<WebCore::HTMLMediaElement*>(node);
502}
503
504- (void)_webPluginContainerSetMediaPlayerProxy:(WebMediaPlayerProxy *)proxy forElement:(DOMElement *)element
505{
506    WebCore::HTMLMediaElement* client = mediaProxyClient(element);
507    if (client)
508        client->setMediaPlayerProxy(proxy);
509}
510
511- (void)_webPluginContainerPostMediaPlayerNotification:(int)notification forElement:(DOMElement *)element
512{
513    WebCore::HTMLMediaElement* client = mediaProxyClient(element);
514    if (client)
515        client->deliverNotification((MediaPlayerProxyNotificationType)notification);
516}
517#endif
518
519@end
520
521static bool isKindOfClass(id object, NSString *className)
522{
523    Class cls = NSClassFromString(className);
524
525    if (!cls)
526        return false;
527
528    return [object isKindOfClass:cls];
529}
530
531
532// Existing versions of the Flip4Mac WebKit plug-in have an object lifetime bug related to an NSAlert that is
533// used to notify the user about updates to the plug-in. This bug can result in Safari crashing if the page
534// containing the plug-in navigates while the alert is displayed (<rdar://problem/7313430>).
535//
536// The gist of the bug is thus: Flip4Mac sets an instance of the TSUpdateCheck class as the modal delegate of the
537// NSAlert instance. This TSUpdateCheck instance itself has a delegate. The delegate is set to the WmvPlugin
538// instance which is the NSView subclass that is exposed to WebKit as the plug-in view. Since this relationship
539// is that of delegates the TSUpdateCheck does not retain the WmvPlugin. This leads to a bug if the WmvPlugin
540// instance is destroyed before the TSUpdateCheck instance as the TSUpdateCheck instance will be left with a
541// pointer to a stale object. This will happen if a page containing the Flip4Mac plug-in triggers a navigation
542// while the update sheet is visible as the WmvPlugin instance is removed from the view hierarchy and there are
543// no other references to keep the object alive.
544//
545// We work around this bug by patching the following two messages:
546//
547// 1) -[NSAlert beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:]
548// 2) -[TSUpdateCheck alertDidEnd:returnCode:contextInfo:]
549//
550// Our override of 1) detects whether it is Flip4Mac's update sheet triggering the alert by checking whether the
551// modal delegate is an instance of TSUpdateCheck. If it is, it retains the modal delegate's delegate.
552//
553// Our override of 2) then autoreleases the delegate, balancing the retain we added in 1).
554//
555// These two overrides have the effect of ensuring that the WmvPlugin instance will always outlive the TSUpdateCheck
556// instance, preventing the TSUpdateCheck instance from accessing a stale delegate pointer and crashing the application.
557
558
559typedef void (*beginSheetModalForWindowIMP)(id, SEL, NSWindow *, id, SEL, void*);
560static beginSheetModalForWindowIMP original_NSAlert_beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_;
561
562typedef void (*alertDidEndIMP)(id, SEL, NSAlert *, NSInteger, void*);
563static alertDidEndIMP original_TSUpdateCheck_alertDidEnd_returnCode_contextInfo_;
564
565static void WebKit_TSUpdateCheck_alertDidEnd_returnCode_contextInfo_(id object, SEL selector, NSAlert *alert, NSInteger returnCode, void* contextInfo)
566{
567    [[object delegate] autorelease];
568
569    original_TSUpdateCheck_alertDidEnd_returnCode_contextInfo_(object, selector, alert, returnCode, contextInfo);
570}
571
572static void WebKit_NSAlert_beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_(id object, SEL selector, NSWindow *window, id modalDelegate, SEL didEndSelector, void* contextInfo)
573{
574    if (isKindOfClass(modalDelegate, @"TSUpdateCheck"))
575        [[modalDelegate delegate] retain];
576
577    original_NSAlert_beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_(object, selector, window, modalDelegate, didEndSelector, contextInfo);
578}
579
580static void installFlip4MacPlugInWorkaroundIfNecessary()
581{
582    static bool hasInstalledFlip4MacPlugInWorkaround;
583    if (!hasInstalledFlip4MacPlugInWorkaround) {
584        Class TSUpdateCheck = objc_lookUpClass("TSUpdateCheck");
585        if (!TSUpdateCheck)
586            return;
587
588        Method methodToPatch = class_getInstanceMethod(TSUpdateCheck, @selector(alertDidEnd:returnCode:contextInfo:));
589        if (!methodToPatch)
590            return;
591
592        IMP originalMethod = method_setImplementation(methodToPatch, reinterpret_cast<IMP>(WebKit_TSUpdateCheck_alertDidEnd_returnCode_contextInfo_));
593        original_TSUpdateCheck_alertDidEnd_returnCode_contextInfo_ = reinterpret_cast<alertDidEndIMP>(originalMethod);
594
595        methodToPatch = class_getInstanceMethod(objc_getRequiredClass("NSAlert"), @selector(beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:));
596        originalMethod = method_setImplementation(methodToPatch, reinterpret_cast<IMP>(WebKit_NSAlert_beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_));
597        original_NSAlert_beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_ = reinterpret_cast<beginSheetModalForWindowIMP>(originalMethod);
598
599        hasInstalledFlip4MacPlugInWorkaround = true;
600    }
601}
602