/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * * Copyright (c) 2008, Mozilla Corporation * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the Mozilla Corporation nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Contributor(s): * Josh Aas * Michael Ventnor * * ***** END LICENSE BLOCK ***** */ #include "nptest_platform.h" #include "npapi.h" #include #include #ifdef MOZ_X11 #include #include #endif #include #include #include #include "mozilla/NullPtr.h" #include "mozilla/IntentionalCrash.h" using namespace std; struct _PlatformData { #ifdef MOZ_X11 Display* display; Visual* visual; Colormap colormap; #endif GtkWidget* plug; }; bool pluginSupportsWindowMode() { return true; } bool pluginSupportsWindowlessMode() { return true; } bool pluginSupportsAsyncBitmapDrawing() { return false; } NPError pluginInstanceInit(InstanceData* instanceData) { #ifdef MOZ_X11 instanceData->platformData = static_cast (NPN_MemAlloc(sizeof(PlatformData))); if (!instanceData->platformData) return NPERR_OUT_OF_MEMORY_ERROR; instanceData->platformData->display = nullptr; instanceData->platformData->visual = nullptr; instanceData->platformData->colormap = None; instanceData->platformData->plug = nullptr; return NPERR_NO_ERROR; #else // we only support X11 here, since thats what the plugin system uses return NPERR_INCOMPATIBLE_VERSION_ERROR; #endif } void pluginInstanceShutdown(InstanceData* instanceData) { if (instanceData->hasWidget) { Window window = reinterpret_cast(instanceData->window.window); if (window != None) { // This window XID should still be valid. // See bug 429604 and bug 454756. XWindowAttributes attributes; if (!XGetWindowAttributes(instanceData->platformData->display, window, &attributes)) g_error("XGetWindowAttributes failed at plugin instance shutdown"); } } GtkWidget* plug = instanceData->platformData->plug; if (plug) { instanceData->platformData->plug = 0; if (instanceData->cleanupWidget) { // Default/tidy behavior gtk_widget_destroy(plug); } else { // Flash Player style: let the GtkPlug destroy itself on disconnect. g_signal_handlers_disconnect_matched(plug, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, instanceData); } } NPN_MemFree(instanceData->platformData); instanceData->platformData = 0; } static void SetCairoRGBA(cairo_t* cairoWindow, uint32_t rgba) { float b = (rgba & 0xFF) / 255.0; float g = ((rgba & 0xFF00) >> 8) / 255.0; float r = ((rgba & 0xFF0000) >> 16) / 255.0; float a = ((rgba & 0xFF000000) >> 24) / 255.0; cairo_set_source_rgba(cairoWindow, r, g, b, a); } static void pluginDrawSolid(InstanceData* instanceData, GdkDrawable* gdkWindow, int x, int y, int width, int height) { cairo_t* cairoWindow = gdk_cairo_create(gdkWindow); if (!instanceData->hasWidget) { NPRect* clip = &instanceData->window.clipRect; cairo_rectangle(cairoWindow, clip->left, clip->top, clip->right - clip->left, clip->bottom - clip->top); cairo_clip(cairoWindow); } GdkRectangle windowRect = { x, y, width, height }; gdk_cairo_rectangle(cairoWindow, &windowRect); SetCairoRGBA(cairoWindow, instanceData->scriptableObject->drawColor); cairo_fill(cairoWindow); cairo_destroy(cairoWindow); } static void pluginDrawWindow(InstanceData* instanceData, GdkDrawable* gdkWindow, const GdkRectangle& invalidRect) { NPWindow& window = instanceData->window; // When we have a widget, window.x/y are meaningless since our // widget is always positioned correctly and we just draw into it at 0,0 int x = instanceData->hasWidget ? 0 : window.x; int y = instanceData->hasWidget ? 0 : window.y; int width = window.width; int height = window.height; notifyDidPaint(instanceData); if (instanceData->scriptableObject->drawMode == DM_SOLID_COLOR) { // drawing a solid color for reftests pluginDrawSolid(instanceData, gdkWindow, invalidRect.x, invalidRect.y, invalidRect.width, invalidRect.height); return; } NPP npp = instanceData->npp; if (!npp) return; const char* uaString = NPN_UserAgent(npp); if (!uaString) return; GdkGC* gdkContext = gdk_gc_new(gdkWindow); if (!gdkContext) return; if (!instanceData->hasWidget) { NPRect* clip = &window.clipRect; GdkRectangle gdkClip = { clip->left, clip->top, clip->right - clip->left, clip->bottom - clip->top }; gdk_gc_set_clip_rectangle(gdkContext, &gdkClip); } // draw a grey background for the plugin frame GdkColor grey; grey.red = grey.blue = grey.green = 32767; gdk_gc_set_rgb_fg_color(gdkContext, &grey); gdk_draw_rectangle(gdkWindow, gdkContext, TRUE, x, y, width, height); // draw a 3-pixel-thick black frame around the plugin GdkColor black; black.red = black.green = black.blue = 0; gdk_gc_set_rgb_fg_color(gdkContext, &black); gdk_gc_set_line_attributes(gdkContext, 3, GDK_LINE_SOLID, GDK_CAP_NOT_LAST, GDK_JOIN_MITER); gdk_draw_rectangle(gdkWindow, gdkContext, FALSE, x + 1, y + 1, width - 3, height - 3); // paint the UA string PangoContext* pangoContext = gdk_pango_context_get(); PangoLayout* pangoTextLayout = pango_layout_new(pangoContext); pango_layout_set_width(pangoTextLayout, (width - 10) * PANGO_SCALE); pango_layout_set_text(pangoTextLayout, uaString, -1); gdk_draw_layout(gdkWindow, gdkContext, x + 5, y + 5, pangoTextLayout); g_object_unref(pangoTextLayout); g_object_unref(gdkContext); } static gboolean ExposeWidget(GtkWidget* widget, GdkEventExpose* event, gpointer user_data) { InstanceData* instanceData = static_cast(user_data); pluginDrawWindow(instanceData, event->window, event->area); return TRUE; } static gboolean MotionEvent(GtkWidget* widget, GdkEventMotion* event, gpointer user_data) { InstanceData* instanceData = static_cast(user_data); instanceData->lastMouseX = event->x; instanceData->lastMouseY = event->y; return TRUE; } static gboolean ButtonEvent(GtkWidget* widget, GdkEventButton* event, gpointer user_data) { InstanceData* instanceData = static_cast(user_data); instanceData->lastMouseX = event->x; instanceData->lastMouseY = event->y; if (event->type == GDK_BUTTON_RELEASE) { instanceData->mouseUpEventCount++; } return TRUE; } static gboolean DeleteWidget(GtkWidget* widget, GdkEvent* event, gpointer user_data) { InstanceData* instanceData = static_cast(user_data); // Some plugins do not expect the plug to be removed from the socket before // the plugin instance is destroyed. e.g. bug 485125 if (instanceData->platformData->plug) g_error("plug removed"); // this aborts return FALSE; } void pluginDoSetWindow(InstanceData* instanceData, NPWindow* newWindow) { instanceData->window = *newWindow; #ifdef MOZ_X11 NPSetWindowCallbackStruct *ws_info = static_cast(newWindow->ws_info); instanceData->platformData->display = ws_info->display; instanceData->platformData->visual = ws_info->visual; instanceData->platformData->colormap = ws_info->colormap; #endif } void pluginWidgetInit(InstanceData* instanceData, void* oldWindow) { #ifdef MOZ_X11 GtkWidget* oldPlug = instanceData->platformData->plug; if (oldPlug) { instanceData->platformData->plug = 0; gtk_widget_destroy(oldPlug); } GdkNativeWindow nativeWinId = reinterpret_cast(instanceData->window.window); /* create a GtkPlug container */ GtkWidget* plug = gtk_plug_new(nativeWinId); // Test for bugs 539138 and 561308 if (!plug->window) g_error("Plug has no window"); // aborts /* make sure the widget is capable of receiving focus */ GTK_WIDGET_SET_FLAGS (GTK_WIDGET(plug), GTK_CAN_FOCUS); /* all the events that our widget wants to receive */ gtk_widget_add_events(plug, GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); g_signal_connect(plug, "expose-event", G_CALLBACK(ExposeWidget), instanceData); g_signal_connect(plug, "motion_notify_event", G_CALLBACK(MotionEvent), instanceData); g_signal_connect(plug, "button_press_event", G_CALLBACK(ButtonEvent), instanceData); g_signal_connect(plug, "button_release_event", G_CALLBACK(ButtonEvent), instanceData); g_signal_connect(plug, "delete-event", G_CALLBACK(DeleteWidget), instanceData); gtk_widget_show(plug); instanceData->platformData->plug = plug; #endif } int16_t pluginHandleEvent(InstanceData* instanceData, void* event) { #ifdef MOZ_X11 XEvent* nsEvent = (XEvent*)event; switch (nsEvent->type) { case GraphicsExpose: { const XGraphicsExposeEvent& expose = nsEvent->xgraphicsexpose; NPWindow& window = instanceData->window; window.window = (void*)(expose.drawable); GdkNativeWindow nativeWinId = reinterpret_cast(window.window); GdkDisplay* gdkDisplay = gdk_x11_lookup_xdisplay(expose.display); if (!gdkDisplay) { g_warning("Display not opened by GDK"); return 0; } // gdk_pixmap_foreign_new() doesn't check whether a GdkPixmap already // exists, so check first. // https://bugzilla.gnome.org/show_bug.cgi?id=590690 GdkPixmap* gdkDrawable = GDK_DRAWABLE(gdk_pixmap_lookup_for_display(gdkDisplay, nativeWinId)); // If there is no existing GdkPixmap or it doesn't have a colormap then // create our own. if (gdkDrawable) { GdkColormap* gdkColormap = gdk_drawable_get_colormap(gdkDrawable); if (!gdkColormap) { g_warning("No GdkColormap on GdkPixmap"); return 0; } if (gdk_x11_colormap_get_xcolormap(gdkColormap) != instanceData->platformData->colormap) { g_warning("wrong Colormap"); return 0; } if (gdk_x11_visual_get_xvisual(gdk_colormap_get_visual(gdkColormap)) != instanceData->platformData->visual) { g_warning("wrong Visual"); return 0; } g_object_ref(gdkDrawable); } else { gdkDrawable = GDK_DRAWABLE(gdk_pixmap_foreign_new_for_display(gdkDisplay, nativeWinId)); VisualID visualID = instanceData->platformData->visual->visualid; GdkVisual* gdkVisual = gdk_x11_screen_lookup_visual(gdk_drawable_get_screen(gdkDrawable), visualID); GdkColormap* gdkColormap = gdk_x11_colormap_foreign_new(gdkVisual, instanceData->platformData->colormap); gdk_drawable_set_colormap(gdkDrawable, gdkColormap); g_object_unref(gdkColormap); } const NPRect& clip = window.clipRect; if (expose.x < clip.left || expose.y < clip.top || expose.x + expose.width > clip.right || expose.y + expose.height > clip.bottom) { g_warning("expose rectangle (x=%d,y=%d,w=%d,h=%d) not in clip rectangle (l=%d,t=%d,r=%d,b=%d)", expose.x, expose.y, expose.width, expose.height, clip.left, clip.top, clip.right, clip.bottom); return 0; } if (expose.x < window.x || expose.y < window.y || expose.x + expose.width > window.x + int32_t(window.width) || expose.y + expose.height > window.y + int32_t(window.height)) { g_warning("expose rectangle (x=%d,y=%d,w=%d,h=%d) not in plugin rectangle (x=%d,y=%d,w=%d,h=%d)", expose.x, expose.y, expose.width, expose.height, window.x, window.y, window.width, window.height); return 0; } GdkRectangle invalidRect = { expose.x, expose.y, expose.width, expose.height }; pluginDrawWindow(instanceData, gdkDrawable, invalidRect); g_object_unref(gdkDrawable); break; } case MotionNotify: { XMotionEvent* motion = &nsEvent->xmotion; instanceData->lastMouseX = motion->x; instanceData->lastMouseY = motion->y; break; } case ButtonPress: case ButtonRelease: { XButtonEvent* button = &nsEvent->xbutton; instanceData->lastMouseX = button->x; instanceData->lastMouseY = button->y; if (nsEvent->type == ButtonRelease) { instanceData->mouseUpEventCount++; } break; } default: break; } #endif return 0; } int32_t pluginGetEdge(InstanceData* instanceData, RectEdge edge) { if (!instanceData->hasWidget) return NPTEST_INT32_ERROR; GtkWidget* plug = instanceData->platformData->plug; if (!plug) return NPTEST_INT32_ERROR; GdkWindow* plugWnd = plug->window; if (!plugWnd) return NPTEST_INT32_ERROR; GdkWindow* toplevelGdk = 0; #ifdef MOZ_X11 Window toplevel = 0; NPN_GetValue(instanceData->npp, NPNVnetscapeWindow, &toplevel); if (!toplevel) return NPTEST_INT32_ERROR; toplevelGdk = gdk_window_foreign_new(toplevel); #endif if (!toplevelGdk) return NPTEST_INT32_ERROR; GdkRectangle toplevelFrameExtents; gdk_window_get_frame_extents(toplevelGdk, &toplevelFrameExtents); g_object_unref(toplevelGdk); gint pluginWidth, pluginHeight; gdk_drawable_get_size(GDK_DRAWABLE(plugWnd), &pluginWidth, &pluginHeight); gint pluginOriginX, pluginOriginY; gdk_window_get_origin(plugWnd, &pluginOriginX, &pluginOriginY); gint pluginX = pluginOriginX - toplevelFrameExtents.x; gint pluginY = pluginOriginY - toplevelFrameExtents.y; switch (edge) { case EDGE_LEFT: return pluginX; case EDGE_TOP: return pluginY; case EDGE_RIGHT: return pluginX + pluginWidth; case EDGE_BOTTOM: return pluginY + pluginHeight; } return NPTEST_INT32_ERROR; } #ifdef MOZ_X11 static void intersectWithShapeRects(Display* display, Window window, int kind, GdkRegion* region) { int count = -1, order; XRectangle* shapeRects = XShapeGetRectangles(display, window, kind, &count, &order); // The documentation says that shapeRects will be nullptr when the // extension is not supported. Unfortunately XShapeGetRectangles // also returns nullptr when the region is empty, so we can't treat // nullptr as failure. I hope this way is OK. if (count < 0) return; GdkRegion* shapeRegion = gdk_region_new(); if (!shapeRegion) { XFree(shapeRects); return; } for (int i = 0; i < count; ++i) { XRectangle* r = &shapeRects[i]; GdkRectangle rect = { r->x, r->y, r->width, r->height }; gdk_region_union_with_rect(shapeRegion, &rect); } XFree(shapeRects); gdk_region_intersect(region, shapeRegion); gdk_region_destroy(shapeRegion); } #endif static GdkRegion* computeClipRegion(InstanceData* instanceData) { if (!instanceData->hasWidget) return 0; GtkWidget* plug = instanceData->platformData->plug; if (!plug) return 0; GdkWindow* plugWnd = plug->window; if (!plugWnd) return 0; gint plugWidth, plugHeight; gdk_drawable_get_size(GDK_DRAWABLE(plugWnd), &plugWidth, &plugHeight); GdkRectangle pluginRect = { 0, 0, plugWidth, plugHeight }; GdkRegion* region = gdk_region_rectangle(&pluginRect); if (!region) return 0; int pluginX = 0, pluginY = 0; #ifdef MOZ_X11 Display* display = GDK_WINDOW_XDISPLAY(plugWnd); Window window = GDK_WINDOW_XWINDOW(plugWnd); Window toplevel = 0; NPN_GetValue(instanceData->npp, NPNVnetscapeWindow, &toplevel); if (!toplevel) return 0; for (;;) { Window root; int x, y; unsigned int width, height, border_width, depth; if (!XGetGeometry(display, window, &root, &x, &y, &width, &height, &border_width, &depth)) { gdk_region_destroy(region); return 0; } GdkRectangle windowRect = { 0, 0, static_cast(width), static_cast(height) }; GdkRegion* windowRgn = gdk_region_rectangle(&windowRect); if (!windowRgn) { gdk_region_destroy(region); return 0; } intersectWithShapeRects(display, window, ShapeBounding, windowRgn); intersectWithShapeRects(display, window, ShapeClip, windowRgn); gdk_region_offset(windowRgn, -pluginX, -pluginY); gdk_region_intersect(region, windowRgn); gdk_region_destroy(windowRgn); // Stop now if we've reached the toplevel. Stopping here means // clipping performed by the toplevel window is taken into account. if (window == toplevel) break; Window parent; Window* children; unsigned int nchildren; if (!XQueryTree(display, window, &root, &parent, &children, &nchildren)) { gdk_region_destroy(region); return 0; } XFree(children); pluginX += x; pluginY += y; window = parent; } #endif // pluginX and pluginY are now relative to the toplevel. Make them // relative to the window frame top-left. GdkWindow* toplevelGdk = gdk_window_foreign_new(window); if (!toplevelGdk) return 0; GdkRectangle toplevelFrameExtents; gdk_window_get_frame_extents(toplevelGdk, &toplevelFrameExtents); gint toplevelOriginX, toplevelOriginY; gdk_window_get_origin(toplevelGdk, &toplevelOriginX, &toplevelOriginY); g_object_unref(toplevelGdk); pluginX += toplevelOriginX - toplevelFrameExtents.x; pluginY += toplevelOriginY - toplevelFrameExtents.y; gdk_region_offset(region, pluginX, pluginY); return region; } int32_t pluginGetClipRegionRectCount(InstanceData* instanceData) { GdkRegion* region = computeClipRegion(instanceData); if (!region) return NPTEST_INT32_ERROR; GdkRectangle* rects; gint nrects; gdk_region_get_rectangles(region, &rects, &nrects); gdk_region_destroy(region); g_free(rects); return nrects; } int32_t pluginGetClipRegionRectEdge(InstanceData* instanceData, int32_t rectIndex, RectEdge edge) { GdkRegion* region = computeClipRegion(instanceData); if (!region) return NPTEST_INT32_ERROR; GdkRectangle* rects; gint nrects; gdk_region_get_rectangles(region, &rects, &nrects); gdk_region_destroy(region); if (rectIndex >= nrects) { g_free(rects); return NPTEST_INT32_ERROR; } GdkRectangle rect = rects[rectIndex]; g_free(rects); switch (edge) { case EDGE_LEFT: return rect.x; case EDGE_TOP: return rect.y; case EDGE_RIGHT: return rect.x + rect.width; case EDGE_BOTTOM: return rect.y + rect.height; } return NPTEST_INT32_ERROR; } void pluginDoInternalConsistencyCheck(InstanceData* instanceData, string& error) { } string pluginGetClipboardText(InstanceData* instanceData) { GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); // XXX this is a BAD WAY to interact with GtkClipboard. We use this // deprecated interface only to test nested event loop handling. gchar* text = gtk_clipboard_wait_for_text(cb); string retText = text ? text : ""; g_free(text); return retText; } //----------------------------------------------------------------------------- // NB: this test is quite gross in that it's not only // nondeterministic, but dependent on the guts of the nested glib // event loop handling code in PluginModule. We first sleep long // enough to make sure that the "detection timer" will be pending when // we enter the nested glib loop, then similarly for the "process browser // events" timer. Then we "schedule" the crasher thread to run at about the // same time we expect that the PluginModule "process browser events" task // will run. If all goes well, the plugin process will crash and generate the // XPCOM "plugin crashed" task, and the browser will run that task while still // in the "process some events" loop. static void* CrasherThread(void* data) { // Give the parent thread a chance to send the message. usleep(200); // Exit (without running atexit hooks) rather than crashing with a signal // so as to make timing more reliable. The process terminates immediately // rather than waiting for a thread in the parent process to attach and // generate a minidump. _exit(1); // not reached return(nullptr); } bool pluginCrashInNestedLoop(InstanceData* instanceData) { // wait at least long enough for nested loop detector task to be pending ... sleep(1); // Run the nested loop detector by processing all events that are waiting. bool found_event = false; while (g_main_context_iteration(nullptr, FALSE)) { found_event = true; } if (!found_event) { g_warning("DetectNestedEventLoop did not fire"); return true; // trigger a test failure } // wait at least long enough for the "process browser events" task to be // pending ... sleep(1); // we'll be crashing soon, note that fact now to avoid messing with // timing too much mozilla::NoteIntentionalCrash("plugin"); // schedule the crasher thread ... pthread_t crasherThread; if (0 != pthread_create(&crasherThread, nullptr, CrasherThread, nullptr)) { g_warning("Failed to create thread"); return true; // trigger a test failure } // .. and hope it crashes at about the same time as the "process browser // events" task (that should run in this loop) is being processed in the // parent. found_event = false; while (g_main_context_iteration(nullptr, FALSE)) { found_event = true; } if (found_event) { g_warning("Should have crashed in ProcessBrowserEvents"); } else { g_warning("ProcessBrowserEvents did not fire"); } // if we get here without crashing, then we'll trigger a test failure return true; } static int SleepThenDie(Display* display) { mozilla::NoteIntentionalCrash("plugin"); fprintf(stderr, "[testplugin:%d] SleepThenDie: sleeping\n", getpid()); sleep(1); fprintf(stderr, "[testplugin:%d] SleepThenDie: dying\n", getpid()); _exit(1); } bool pluginDestroySharedGfxStuff(InstanceData* instanceData) { // Closing the X socket results in the gdk error handler being // invoked, which exit()s us. We want to give the parent process a // little while to do whatever it wanted to do, so steal the IO // handler from gdk and set up our own that delays seppuku. XSetIOErrorHandler(SleepThenDie); close(ConnectionNumber(GDK_DISPLAY())); return true; }