📜 ⬆️ ⬇️

Safari plugin? Easily!

Today, I finally got tired of running Firefox every time I need to quickly pull out an XPath site for an element (there’s a nice XPather extension for this), and I decided to look at how to inject my code into a Cocoa application.


The general principle is simple: Objective-C runtime allows you to get a pointer to any class from anywhere on the program and perform all sorts of tricks with it. Taking the SIMBL manual as a reference , I began to develop a bundle.

The task was as follows: add an item to the context menu of Safari, upon activation of which an XPath selection will be made for the element under the cursor. Using F-Script Anywhere, I literally found out in a minute that the implementation of WebUIDelegate (including the webView method: contextMenuItemsForElement: defaultMenuItems :) in the target browser corresponds to the BrowserWebView class. This means that it is necessary to replace the methods, for which we will use the DTRenameSelector function:
  1. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  2. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  3. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  4. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  5. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  6. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  7. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  8. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  9. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }
  10. BOOL DTRenameSelector ( Class _class, SEL _oldSelector, SEL _newSelector ) { Method method = nil ; // First, look for the methods method = class_getInstanceMethod ( _class, _oldSelector ) ; if ( method == nil ) return NO ; method - >method_name = _newSelector; return YES ; }

Now you can make a stub for our "fake" selector:
  1. static NSArray * webView_contextMenuItemsForElement_defaultMenuItems_ ( id self, SEL _cmd,
  2. id * sender, NSDictionary * element, NSArray * defaultMenuItems )
  3. {
  4. return [ self
  5. _sxp_orig_webView : sender
  6. contextMenuItemsForElement : element
  7. defaultMenuItems : defaultMenuItems ] ;
  8. }

Since this is a C function, the first two arguments of the method (self and _cmd) must be specified explicitly. Internally, the function calls the real selector, which we will need to rename at boot. Runtime promises us to execute the + load method when initializing any class, which we will use:
  1. static SXPPlugin * Plugin = nil ;
  2. @implementation SXPPlugin
  3. + ( SXPPlugin * ) sharedInstance
  4. {
  5. @synchronized ( self ) {
  6. if ( ! Plugin )
  7. Plugin = [ [ SXPPlugin alloc ] init ] ;
  8. }
  9. return plugin;
  10. }
  11. - ( void ) swizzle
  12. {
  13. Class BrowserWebView = objc_getClass ( "BrowserWebView" ) ;
  14. if ( BrowserWebView ) {
  15. class_addMethod (
  16. BrowserWebView,
  17. @selector ( _sxp_fake_webView : contextMenuItemsForElement : defaultMenuItems :) ,
  18. ( IMP ) webView_contextMenuItemsForElement_defaultMenuItems_,
  19. "@@: @@@" ) ;
  20. DTRenameSelector (
  21. BrowserWebView,
  22. @selector ( webView : contextMenuItemsForElement : defaultMenuItems :) ,
  23. @selector ( _sxp_orig_webView : contextMenuItemsForElement : defaultMenuItems :) ) ;
  24. DTRenameSelector (
  25. BrowserWebView,
  26. @selector ( _sxp_fake_webView : contextMenuItemsForElement : defaultMenuItems :) ,
  27. @selector ( webView : contextMenuItemsForElement : defaultMenuItems :) ) ;
  28. } else {
  29. NSLog ( @ "Failed to get BrowserWebView class" ) ;
  30. }
  31. }
  32. + ( void ) load
  33. {
  34. SXPPlugin * plugin = [ SXPPlugin sharedInstance ] ;
  35. [ plugin swizzle ] ;
  36. }
  37. @end

What is going on here? As soon as our bundle is loaded into Safari, the + [SXPPlugin load] method will work, which, firstly, will create the first (and last) instance of itself, and, secondly, will replace the method in BrowserWebView, by adding a dummy selector (_sxp_fake) , rename the present to (_sxp_orig) and then install the fictitious in its place. In fact, _sxp_orig here points not to the code inside Safari, but to another extension that was loaded earlier - Speed ​​Download. And after SXPPlugin, 1Password will still be loaded, and it will also be inserted into the chain in the same way (if all of this would have to be unloaded, then the most important thing is to keep order). By the way, the "@@: @@@" for the selector description means that the selector returns id (first @) and accepts (id, SEL, id, id, id).
')
Everything, at this stage you can assemble a bundle, hook it to the SIMBL or directly to InputManagers (but this is not so convenient) and look at how Safari continues to work :)

We continue to increase the functionality, we need our own element in this menu:
  1. - ( id ) init
  2. {
  3. if ( ( self = [ super init ] ) ) {
  4. // init menu
  5. NSMenu * m = [ [ NSMenu alloc ] initWithTitle : @ "XPath" ] ;
  6. NSMenuItem * mi = [ [ NSMenuItem alloc ]
  7. initWithTitle : @ "XPath for node"
  8. action : @selector ( onMenu :)
  9. keyEquivalent : @ "" ] ;
  10. [ mi setTarget : self ] ;
  11. [ m addItem : mi ] ;
  12. [ mi release ] ;
  13. mi = [ [ NSMenuItem alloc ]
  14. initWithTitle : @ "Show browser"
  15. action : @selector ( onMenuBrowser :)
  16. keyEquivalent : @ "" ] ;
  17. [ mi setTarget : self ] ;
  18. [ m addItem : mi ] ;
  19. [ mi release ] ;
  20. _myMenuItem = [ [ NSMenuItem alloc ]
  21. initWithTitle : @ "XPath"
  22. action : nil
  23. keyEquivalent : @ "" ] ;
  24. [ _myMenuItem setSubmenu : m ] ;
  25. [ m release ] ;
  26. [ _myMenuItem setEnabled : YES ] ;
  27. }
  28. return self;
  29. }

In the constructor, we create the “XPath” menu item, which has a submenu with the “XPath for node” and “Show browser” elements (both elements are linked to actions on the main class).

Add a menu to the class description: NSMenuItem *_myMenuItem; and make it available via the property: @property (readonly) NSMenuItem *myMenuItem; . Now we can get to this menu from our function (which is executed in the context of BrowserWebView):
  1. static NSArray * webView_contextMenuItemsForElement_defaultMenuItems_ ( id self, SEL _cmd,
  2. id * sender, NSDictionary * element, NSArray * defaultMenuItems )
  3. {
  4. [ SXPPlugin sharedInstance ] .ctx = element;
  5. NSArray * itms = [ self
  6. _sxp_orig_webView : sender
  7. contextMenuItemsForElement : element
  8. defaultMenuItems : defaultMenuItems ] ;
  9. NSMutableArray * itms2 = [ NSMutableArray arrayWithArray : itms ] ;
  10. [ itms2 addObject : [ NSMenuItem separatorItem ] ] ;
  11. [ itms2 addObject : [ [ SXPPlugin sharedInstance ] myMenuItem ] ] ;
  12. return itms2;
  13. }

Do not pay attention to the fourth line, it will be further. And everything else is obvious - we get a list of the current menu items, add a separator and our menu there, return above.
Menu

Now we start to do more interesting things. To begin with, a break in programming and a few minutes at Interface Builder, in which the main interface will be created:
Main UI
The XPath will be displayed in the top line, and the result of the selection will be displayed in the bottom list [NSBundle loadNibNamed:@"XPathBrowser" owner:self]; everything we need on the main class, and in init we add the xib load: [NSBundle loadNibNamed:@"XPathBrowser" owner:self]; .

Now, actually, we unleash XPath. To know where to spin it in the interceptor, we save the dictionary "element", it is in it all the information we need.
  1. - ( void ) onMenu : ( id ) sender
  2. {
  3. [ window makeKeyAndOrderFront : self ] ;
  4. NSString * xp = [ self xpathForNode : [ _ctx objectForKey : @ "WebElementDOMNode" ] ] ;
  5. [ xpathField setStringValue : xp ] ;
  6. [ self onEvaluate : self ] ;
  7. }
  8. - ( void ) onMenuBrowser : ( id ) sender
  9. {
  10. [ window makeKeyAndOrderFront : self ] ;
  11. }

An instance of the DOMNode class, which was clicked, is associated with the WebElementDOMNode key. The xpathForNode code: I will not give you here, it is quite cumbersome (those who wish can see it in git ), the principle of operation is the following: unwind the parent node to the very top. If a node has an id attribute, then it is added to the XPath, if the parent node has several nodes of the same type (and they do not have an id), then the node index is used.

But there is not enough XPath to count, it is necessary to execute it and get a result. To do this, you could use an NSXMLDocument and feed it data from the current frame, but this is not as interesting as the opportunity to get nodes from JavaScript:
  1. - ( void ) onEvaluate : ( id ) sender
  2. {
  3. NSString * xp = [ xpathField stringValue ] ;
  4. WebFrame * frame = [ _ctx objectForKey : @ "WebElementFrame" ] ;
  5. WebView * view = [ frame webView ] ;
  6. NSString * js = [ NSString
  7. stringWithFormat : @ "document.evaluate ( \" % @ \ " , document, null, XPathResult.ANY_TYPE, null)" , xp ] ;
  8. id o = [ [ view windowScriptObject ] evaluateWebScript : js ] ;
  9. [ _nodes release ] ;
  10. _nodes = nil ;
  11. if ( ! [ [ o class ] isEqual : [ WebUndefined class ] ] ) {
  12. NSMutableArray * nodes = [ NSMutableArray array ] ;
  13. id n = [ o iterateNext ] ;
  14. while ( n ) {
  15. [ nodes addObject : [ self dictForNode : n ] ] ;
  16. n = [ o iterateNext ] ;
  17. }
  18. _nodes = [ nodes retain ] ;
  19. } ;
  20. [ outlineView reloadData ] ;
  21. }

Initially, NSOutlineView received DOM * objects directly, but due to the intricacies of the self-leaving nodes (do you have to explicitly retain them?), I rebuild the tree in the nodes array.

I ignore the potential injection of JS, if you have access to a browser, then performing JS on the frame can be much simpler. And the quotation mark seems to be not valid in XPath at all.

On the method -dictForNode: you can also look at git. Its task is to expand the DOM tree into a set of NSDictionary / NSArray structures, from which the outline view will build the list.

Final result:


I also have an alternative project, where I tried to write the same thing on python (although XPath did a wonderful binding to libxml / libxslt - lxml there). But it turned out that the lack of a full-featured browser is still inconvenient.

Source Code (MIT): http://git.hackndev.com/?p=farcaller/safarixpath.git;a=summary

Patches, bug fixes, ideas are welcome.

Update : thanks for the tip, moved to Mac OS X and iPhone Development

Update : at the request of workers, added support for XPath sampling of both the DOM tree and the source HTML document. It can mess up with encodings, there is an unobvious transformation, if it falls in HTML mode - I really ask for the original file for preparation (with utf8 and cp1251 it seems to work). In addition, the NSXMLDocument breaks something on some pages altogether, the problem is being investigated.

At the same time prefix DOM-parser, it now supports functions (count, name, etc.). NSXMLDocument does not know how.

SIMBL-bundle for the latest Safari: http://farcaller.net/stuff/SafariXPath.bundle-1.1.zip

Source: https://habr.com/ru/post/65368/


All Articles