MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


BYOB: Build Your Own Browser, Part 2

by Andrew Anderson
05/28/2004

Editor's note: Back in a distant time and galaxy, we ran a nifty article titled Build Your Own Browser where Andrew Anderson showed you the basics of WebKit. It was an introduction to a multipart series (some of the TalkBacks in that first article bemoaned its simplicity, wanting to get to the more advanced stuff). Then, to add fuel to the conversation, Andrew got hit with a big project and the second installment was delayed. Now I have both Parts 2 and 3 in hand. Today's installment picks up where we left off previously, plus includes a discussion about Safari and WebKit security. Next Friday I'll run Part 3, where Andrew shows you how to create a preference window and content eliminator.

The wait is over and the second installment of "Build Your Own Browser" (BYOB) is finally here. The first installment focused on using Interface Builder to build a simple browser without using any code. The browser was functional but lacked many of the features that are available in modern web browsers. The next two installments will make our browser more powerful by using some of the more advanced aspects of WebKit to integrate three new features into our browser:

This week's installment will cover multi-window capabilities. Both installments differ from the previous article in one significant way. While they do run through tutorials for all three new features similarly to the first article, each leaves hints and tips on how to add additional features, instead of explicitly defining them. This is both to keep the series to a readable length and also to allow readers the ability to develop their own unique features. I've also included some information on WebKit security issues and other sites, which might be of interest for browser developers. I'm going to start with that because it's topical at the moment, then get back to adding features to our fledgling browser.

Safari and WebKit Security

Ask any users of Mac OS X and they will say that "OS X is way more secure than Windows," and then they will tell you that they've never had a virus, Trojan horse, or other security problem with their OS X machine. While OS X is more secure than Windows (Richard Forno has an in-depth article on it ), it is naive to assume that "more secure" is the same as secure, or even worse "more secure" is sufficient to protect your computer. In today's networked world, where your Mac OS X machine is connected 24/7 to the Internet, security is the exception, not the rule.

Web browsers are notorious for having security vulnerabilities that can wreck havoc on your system. The problem is that browser users want whiz-bang features like JavaScript, MIME handlers, Plugins, and Java, but these features have the potential to open up any number of security holes and vulnerabilities. Sometimes the vulnerabilities are because of stupid design or implementation; more often than not though, they are the result of some sort of complex interaction between scripting languages, helper applications, and browsers that even the best developer could be excused for not anticipating.

Our simple browser is more secure than most browsers because it does not implement a lot of functionality that can break. As we add more features to our browser though, we need to keep track of not only our own code, but also any security issues in WebKit. Sounds easy enough, right? Nope, the problem is that users, not developers, find most security problems and users don't know what WebKit is. Since Safari is based on WebKit, the easiest way to keep track of WebKit security issues is to stay informed of Safari security advisories.

Safari security advisories in the past have been few and far between, but a few have surfaced lately, including a recent serious issue involving Safari's interaction with Apple's Help system. It ends up that, given the correct parameters, you can get Safari to open Help and then Help will open any specified application. It is easy to think that this bug is a problem with Safari and Help, but really this affects any browser setup that supports AppleScript and the Help system, including anything based on WebKit.

In its current state, our browser does not suffer from this problem because we have not added support for Help (via its MIME type and/or the Download delegate), but if you plan on expanding the browser, you need to watch out for issues like this one. Fortunately, once Apple caught wind of this bug, it fixed it quickly (although there is some debate on how quickly), and the fix is available in the latest security updates.

Apple's fixes are available for 10.2 and 10.3.

For more information on the bug and a simple script that exploits it, check out the overview at François Nonnenmacher's site.

OK, now with that bit of business out of the way, let's get back to enhancing our basic browser.

Multi Windows

Our application is pretty limited at this point; basically, it's a single window in a single frame. And while it can load complex pages and lay them out correctly, it leaves plenty to be desired in terms of functionality. One important piece of functionality that's missing is the ability to open multiple windows at the same time.

In order to achieve this feat, our browser is going to have to be a Cocoa Document-based application. Currently our project is a simple Cocoa Application, which means that we will have to convert our old project into a Cocoa Document-based application. The easiest and most elegant way to do this is to create a new project that is Document-based and copy the relevant parts of the old project to it.

Start by launching Xcode and creating a new "Cocoa Document-based application" project. When Xcode is finished creating the project, the next step is to add the WebKit framework. To do this, go to the Other Frameworks folder under Frameworks folder in the main project section of the new application. Go to the Projects menu bar and choose Add Framework. In the dialog that pops up, find the WebKit framework and add it to the project. The WebKit framework generally resides in "/System/Library/Frameworks/WebKit" and is surprisingly named "WebKit.framework". If you can't find it, check out the original article for instructions on downloading and installing the framework.

Changing the UI

Now it's time to add the original user interface from the old project to the new project. Since our original project was not a document-based application, the user interface was built in MainMenu.nib. In the new project, the user interface for the browser functionality will be built in MyDocument.nib. MyDocument.nib is the file that Cocoa uses to create a new user interface each time a request is made for one. To get the new browser to function like the previous version we need to copy the UI elements in MainMenu.nib to MyDocument.nib.

Copying the UI elements between MainMenu.nib and MyDocument.nib is fairly easy, load both files in Interface Builder and copy the contents from MainMenu.nib to MyDocument.nib. The easiest way to do this is to load both the original and the new project in XCode. Once they are both loaded, double click MainMenu.nib from the original project and then MyDocument.nib from the new project, which will load both NIB files in Interface Builder. Once in Interface Builder, go to MainMenu.nib, select all the UI elements, copy them, then switch windows to MyDocument.nib, and paste the new elements into the window. The last step is to save MyDocument.nib and close MainMenu.nib.

After the UI is built, we need to add connections so the code that we'll develop later will be able to access the UI pieces. When dealing with Cocoa Document-based applications, the connections are made between the "MyDocument" window instance and the "File's Owner" instance. "File's Owner" is a proxy for the "MyDocument" object that corresponds to individual instances of the class. The need to make a total of three connections between "File's Owner" and "MyDocument": two outlets, one for the webView and one for the URL text line; and one action from the URL text line to alter the document for when the value is changed.

To start, we need to choose the MyDocument class in the Classes panel of the Interface Builder control panel (it is a subclass of NSObject). Once MyDocument is chosen, go to the "Classes" pulldown menu and choose "Add Outlet to MyDocument." An information window will pop up; make sure that it is on the "Outlets" tab and add two outlets: "webView" with type "WebView" and "urlString" with type "id." Next switch to the actions tab and add an action named "connectURL."

Now that the outlets and actions are set up, it's time to connect them to the "File's Owner" instance. To connect the outlets, go to the "Instances" tab of MyDocuments.nib, choose the "File's Owner" instance and Ctrl-drag a line to the text field on the user interface. The info window will appear on the connections page, where you should select "urlString" under the "Outlets" tab and click connect. Next, do the same thing to connect the WebView instance to the webView outlet.

The last step is to disconnect the WebView's takeStringURLFrom action from the edit field and reconnect the edit field to the connectURL action that we created before in "File's Owner". To disconnect from takeStringURLFrom, choose the text input field and Ctrl-drag a line to the WebView on "MyDocument"; when information window pops up, choose the "Target/Actions" tab, then click on "takeStringFromURL" and click on the disconnect button. To reconnect the text input field to the "File's Owner" instance, Ctrl-drag a line from the text input field to the "File's Owner" instance, choose the "Targets/Actions" tab, and click on connect.

Once that is all hooked up, save the NIB file and quit Interface Builder. It's time to edit some code!

Changing the Code

There are five pieces of code that need to be added: first, code to make it so the MyDocument class can access the UI elements that we connected to it; second, code the "connectURL" method to handle processing connections entered in the URL text field; third, code to handle JavaScript pop-up windows; fourth, code to handle setting a default home page to load when a new browser window is loaded; and finally, code to handle updating the URL line when pages get forwarded automatically by the WebView object. This sounds like a lot of code to be adding and updating, but it's really not. And the code that will be added is simple and straightforward to understand.

Connecting the UI Elements

The first step is to edit MyDocuments class header file, MyDocument.h, located in the "Classes" subdirectory of the project in Xcode.

If you look at the file now it is pretty empty and should look something like:


#import <Cocoa/Cocoa.h>

@interface MyDocument : NSDocument
{
}
@end

The first line to add is:


#import <WebKit/WebView.h>

after the line "#import <Cocoa/Cocoa.h>". This file gives the Objective-C compiler the information it needs to access the fields and methods of the WebView object.

Next add:


IBOutlet id urlString;
IBOutlet WebView *webView;

between the { and } brackets after the "@interface" statement. These statements are for the outlets that were set up in the NIB file.

The last line to add is:


- (IBAction)connectURL:(id)sender;

after the closing bracket (that is, / }) of the "@interface" declaration and before the "@end" statement. This statement is for the action that was set up in the NIB file.

In the end, the file should look like:


#import <Cocoa/Cocoa.h>

#import <WebKit/WebView.h>

@interface MyDocument : NSDocument
{
    IBOutlet id urlString;
    IBOutlet WebView *webView;
}
- (IBAction)connectURL:(id)sender;
@end

Adding "connectURL"

The next step is to add the connectURL method to MyDocument's implementation file, MyDocument.m. When you open this file, you'll notice that it contains a number of predefined method prototypes. (These methods will be useful to use later in the article, but for the time being ignore these methods.) Add the "connectURL" method:


- (IBAction)connectURL:(id)sender{
    [urlString setStringValue:[sender stringValue]];
    [[webView mainFrame] loadRequest:
	[NSURLRequest requestWithURL:
	    [NSURL URLWithString:
		[sender stringValue]]]];
}

after these methods and before the "@end" statement.

This method is pretty simple; the first line gets the string value from urlString; the second sends that value to the WebView instance to load the site with that string.

You may wonder why we changed this method if for all intents and purposes it does the same that it did before we disconnected the urlString field from the webView object in the NIB. In the old version, the developer maintained no control over how the connection was made, so whatever was entered on the URL line was sent directly to the WebView object.

This creates a problem because when a user enters "www.macdevcenter.com" in the URL line nothing happens, because it lacked a protocol statement ("http://"). We will not implement it here, but "connectURL" can be used to check for a protocol (by doing a string search for ":"), and if one is not present then connectURL can add a default protocol. It is interesting to check out what Safari does when a protocol and a sub-domain are left out on the URL line.

Once "connectURL" is added, the browser now supports basic multi-window operations. If you test it out at this point, you can create new browser windows from the "File/New" menu, but you will not be able to create new windows using JavaScript requests.

JavaScript Windows

Now that we have a basic multi-window browser working, it's time to open up Pandora's box and add support for JavaScript window requests. These requests will be handled using Objective-C's delegation mechanism. The delegation mechanism allows Objective-C classes to be modified without having to subclass them. A class implements a delegate for another class by first altering the delegating class that it will implement its delegation methods and then implementing those methods. This is the extent of our explanation of delegation methods but if you want more information, check out this article at developer.apple.com.

JavaScript window requests are handled by WebKit's UIDelegate. We need to alter the WebView instance, webView, so that the MyDocument object will be the UIDelegate by adding two lines of code:


[webView setUIDelegate:self]; 
[webView setGroupName:@"MyDocument"];

somewhere within MyDocument. One of the methods that Xcode predefined for us in MyDocument is windowControllerDidLoadNib. This method is called after an individual instance of MyDocument is created and the NIB file for it is loaded. Since this method is called at the creation of the MyDocument object, and after the WebView was created by loading the NIB file, the windowControllerDidLoadNib method is an appropriate place to set up our UIDelegate (as well as other delegates later).

The next step is to add an accessor method to MyDocument so we can access the private webView object. First, add the prototype for the method:


- (id)webView;

after the prototype for the "connectURL" method. Then at the end of the implementation file add the actual method:


 - (id)webView
{
    return webView;
}

Once that is done, it is time to implement the two methods that WebView's UIDelegates uses when JavaScript makes new window requests: createWebViewWithRequest and showWebView methods.


- (WebView *)webView:(WebView *)sender 
	    createWebViewWithRequest:
	    (NSURLRequest *)request
{
    id myDocument = [
	    [NSDocumentController 
		sharedDocumentController] 
	    openUntitledDocumentOfType:
		@"DocumentType" 
	    display:YES];

    [[[myDocument webView] mainFrame] 
		loadRequest:request];
    
    return [myDocument webView];
}

- (void)webViewShow:(WebView *)sender
{
    id myDocument = [[NSDocumentController 
			sharedDocumentController] 
			documentForWindow:
			    [sender window]];
    [myDocument showWindows];
}

The first method is called when a new window request is made and it creates a new MyDocument and returns its WebView object so the WebView object can process the URL request. The second method looks up the window for the MyDocument object and displays it on the screen.

Default Home Pages

If you were to run the code the way it is now, every time you created a new window, other than a JavaScript pop-up, the window would come up blank since it has not been told what page to load. To fix this we will add support for default home pages.

The first thing that needs to be done to add support for default home pages is to add method prototypes for accessors methods for our default home page variable to "MyDocument.h":


(void)setDefaultHomepage:(NSString*);
-(NSString *)getDefaultHomepage;

The next step is to add a static variable to MyDocument.m that holds the value for the default home page. At the top of the file, before the "@implementation" statement, add:


static NSString *defaultHP =@"http://www.macdevcenter.com";

We're using a static here and not a class instance variable because we want all instances of MyDocument to use the same default home page. Since Objective C does not support class variables, we need to implement it with a static variable in the class file.

The next step is to make the webView object load the default home page we specified. We will add the code to do this to "windowControllerDidLoadNib", since this method is called after the proper objects are created and before the window is displayed. The code to add this is fairly simple:


[urlString setStringValue:defaultHP];
[[webView mainFrame] loadRequest:
	[NSURLRequest requestWithURL:
	[NSURL URLWithString:defaultHP]]];

The first statement sets the value of the URL line on the UI to the default page, while the second tells the webView to load the default page.

The final step is to add the accessor methods for the home page at the end of the implementation file:


-(void)setDefaultHomepage:(NSString*)homepage{
    defaultHP = homepage;
}

-(NSString *)getDefaultHomepage{
    return defaultHP;
}

Making the URL Line Reactive

The last step for this section is to make sure that the URL line is updated when webView sends it update information. This needs to happen when a request is forwarded from one URL to another. To get the URL line to react to these, we need to use WebView's frameLoadDelegate. This delegate is called when a frame is loaded and gives information on how and what is loaded. Setting up the frameLoadDelegate is similar to setting up the UIDelegate, the first step is to tell the webView object that MyDocument will be the delegate in the windowControllerDidLoadNib method:


[webView setFrameLoadDelegate:self];

Then we add the method that the frameLoadDelegate will call when it loads the actual frame, namely:


- (void)webView:(WebView *)sender 
		didStartProvisionalLoadForFrame:
		(WebFrame *)frame

{
    // Only report feedback for the main frame.
    if (frame == [sender mainFrame]){
        NSString *url = [[[
			  [frame provisionalDataSource] 
			  request] URL] absoluteString];
        
	[urlString setStringValue:url];
    }
}

This method checks to see that this is the main frame of the window and that it sets the URL text input string to the current URL being loaded.

Once that method is added, the multi-window capabilities are now complete.

Final Thoughts

There are a number of resources available on the web for simple low-code/no-code browsers, resources for more advanced browsing applications are few and far between. Apple's site provides an article called Displaying Web Content, which provides guidance on some of WebKit's features, and on all the classes that are available.

One other interesting resource I found is the Trailblazer project at University of Illinois-Urbana Champaign. Trailblazer is a project that uses WebKit but is really more about a revolutionary way to display a user's browsing history

In next week's article, we'll tackle adding a preference window and a content eliminator. Until then, happy coding.

Andrew Anderson is a software developer and consultant in Chicago who's been using and programming on the Mac as long as he can remember.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.