Good Old Drupal
Talk on Services at Drupalcamp Stockholm 2009
I just held a presentation about services and my plans for services 3.x at Drupalcamp in Stockholm. Everything went well except that I was so nervous about running out of time that I teared through the presentation in 20 minutes. But here are the slides if you missed something.
Check out the english version over at slideshare too
Check out my previous blog post A future for Services - 3.x? for more in-depth information about my views on the future of services.
Attached files:A future for Services - 3.x?
Services is a module that makes it easy to provide web services using Drupal. What services doesn't do is to allow the definition of separate API:s. This is something we need to fix if we want to turn services into a real web-service framework.
Consider the following: Today all installed services are always available on all installed servers and they all have to use the same authentication method.
This is what makes services practically useless as a building block for other modules, and what makes is nigh on impossible for API:s to coexist in one Drupal install. This worked fine when services only was used for integration with a single Flash/Js/external application; when it was installed by the developer/admin to solve one specific task.
Now we're nearing a point where both blog api and pingback implementors are looking to leverage services for their modules. This is great, as our goal must be that everybody implementing any kind of web service should look to services (core inclusion on the far horizon). Unfortunately it's not possible with the way services is constructed today.
The proposed solution
What I propose, and have gotten quite far with implementing, is to introduce the concept of separate endpoints. Endpoints allows modules and administrators to define separate API:s. By this I mean that each endpoint will provide the following information:
- which services should be exposed
- with what server and on which path they should be exposed
- how should the clients authenticate themselves
The endpoints can be customized completely independently of each other. Endpoints can also be created directly through the administrative interface. The plan is to use ctools for the endpoint information, which will give us exportables for API:s, correct handling of loading of definitions from code and database, overrides et.c.
I'm really exited about the possibilities that this provides, and how it ties in with features. I see it as absolutely essential for services continued development, and not as a good-to-have extra. That is not to say that it needs to be implemented exactly as I've done it. But we need the core functionality.
The current state of 3.x
I'll illustrate with some screenshots from a site running the endpoints-branch from github:

This is the new (when say new throughout the post I mean "my proposed new") first-page for services admin. It lists the services endpoints in the installation and some brief info about the path, server and authentication module that's used, and it lists the active services for the endpoint. The services that are listed may be fully or partially enabled.

This is a shot of the edit endpoint page. Here you can choose the path for the endpoint and which server and authentication module that should be used in the endpoint. Here you can see that the oauth module has gotten a new configuration which is OAuth context, described pretty well in the form: "The OAuth contexts provides a scope for consumers and authorizations and have their own authorization levels. Different services endpoints may share OAuth contexts and thereby allow the use of consumers and tokens across the services endpoint boundraries."

This page allows you to select which services and controllers will be exposed at the endpoint, and allows the authentication module to supply options for each controller.

What I like the most is the ability to define a services endpoints in code. Here we have the conglomerate module that I'm currently working on that defines it's own endpoint. This way modules can expose their own API:s and own authentication settings without disrupting each other.

The same goes for oauth_common, where modules now can define their own oauth contexts. Both these are perfect candidates for chaos tools integration, and ties in perfectly with the features-way of thinking.
Endpoint branches:
Co-Maintainers Wanted!
The list of modules that I maintain has become quite long, and in the beginning of next year I'll have a little daughter (if the nurse guessed right on the gender). So the time that I have for being a good maintainer will be very limited.
If you feel that you'd like to help maintain any of the following modules, I would be very grateful!
Modules not on DO
Experience of using git, or the willingness to learn, is kind of a requirement, as all my development is done with git. The alternative is a patch-based workflow.
Dynamic vhost configuration
I've recently tried out a new way define my virtual hosts on my development machine. I've always had a configuration file in my home dir (that gets included from httpd.conf) that looks something like this: <!--break--> NameVirtualHost *:80 ServerName drupal.local DocumentRoot /Users/hugowett/Sites/drupal/public_html AllowOverride All Order deny,allow Allow from all
<VirtualHost *:80>
ServerName somesite.local
DocumentRoot /Users/hugowett/Sites/somesite/public_html
<Directory /Users/hugowett/Sites/somesite/public_html >
AllowOverride All
Order deny,allow
Allow from all
</Directory>
</VirtualHost>
Each time I started a new project I would create the project directory, add a new VirtualHost entry and add the domain to my /etc/hosts-file. Then I just needed to restart apache and everything worked.
Now I've defined a dynamic virtual host instead. This way I just need to create a project directory according to the /Users/hugowett/Projects/{$domain_name}/public_html pattern and add the domain to my hosts file. No Apache configuration changes or restarts required.
# get the server name from the Host: header
UseCanonicalName Off
# include the server name in the filenames used to satisfy requests
VirtualDocumentRoot /Users/hugowett/Projects/%0/public_html
<Directory "/Users/hugowett/Projects">
AllowOverride All
Order allow,deny
Allow from all
</Directory>
Unfortunately you will have to edit Drupal's .htaccess file for this to work with clean urls. Uncomment the RewriteBase / directive as we are running Drupal from a virtual document root from now on.
# If your site is running in a VirtualDocumentRoot at http://example.com/,
# uncomment the following line:
# RewriteBase /
The only thing that's missing now is to configure bind for my mac so that I can add a .dev zone with a *.dev wildcard record. Any hints on how to do this would be greatly appreciated. Take a look here to configure bind on you mac, don't use your IP for the A-records on MacBook though, use 127.0.0.1 instead.
Convenience scripts for Solr
This is a set of convenience scripts for daily drupal use of solr in a dev environment. When you use the apachesolr module in many projects it becomes somewhat boring to set up solr again and again. And when you have a whole team doing the same thing, this script becomes a time-saver.
This little project lives at github (as usual): http://github.com/hugowetterberg/solr_nightly. Clone or download and execute get-nightly from the terminal (when inside the solr_nightly directory) to download the latest nightly and set it up with the drupal-specific configuration files.
$ ./get-nightly
Then execute start to get solr up and running.
$ ./start
Now your drupal install should be able to use solr through the apachesolr module. Press ctrl+c to quit solr. If you want to clear out all files that's been created for the search index and other runtime files (to use it with another drupal install, or just to start over), run:
$ ./reset
Remember to delete the index from the drupal admin when you have a site that expects that there is a index. Otherwise your nodes won't be re-added.
Re-run the get-nightly script if you want to download the nightly again.
Code lost
If you're anything like me you probably misplace some code every once in a while. I have a drupal module that I work on in both spare and work time, and I use it in many projects. I sat down just now and started coding on a feature when I realized that I've already done it somewhere else - but where? Somehow I had to find it, and being a programmer, doing it manually isn't an option. So here follows a quick, osx only, tip on how to sift through folders by combining spotlight searches with your own logic.
<!--break-->
Fortunately I knew that I had deleted a certain file in the new version, so checking for candidates would be pretty easy. The following snippet could act as a sort of boilerplate code for your own search and rescue missions. It finds a certain folder through spotlight, checks for a condition that makes it a likely candidate for being what you want, and then it fires it up in gitx (if it's a git repo), or just tells you about the candidate by writing it's path to the terminal.
#!/usr/bin/env php
<?php
$modules = split("\n", shell_exec("mdfind oauth_common kind:folder"));
foreach ($modules as $p) {
if (!empty($p)) {
if (!file_exists($p . '/includes/OAuthCommonHooks.inc')) {
if (file_exists($p . '/.git')) {
chdir($p);
shell_exec("gitx --all");
}
else {
print "Candidate: $p\n";
}
}
}
}
Unfortunately the code stayed lost, but this snippet lives on and I will probably use it again and maybe it will help somebody else.
Caching the results of your functions
Sometimes when you use info-hooks to collect information from a arbitrary number of modules, or perform some other expensive or "unknown cost" operation, you'll start to feel the need to cache your results. If your function will be called several times during one request caching in a static variable will take off some of the load, combining this with the Drupal cache could help even more.
Caveats: you probably know this already, but as with all caching it's only effective/sensible to cache stuff if the cost to calculate the results from scratch exceeds the cost of retrieving the results. This is especially important if the database backend is used for caching. If APC or another fast, memory based caching backend is in place, the list of stuff that could benefit from caching grows a bit longer. The other consideration is hit-rate - will your function really be called often enough for it to be necessary for it to be cached? If your cache is invalidated regularly because of writes this becomes even more of an issue, if the cached never is used, or at least not used enough to justify the cost of the actual caching, you will just have written unnecessary code, or in the worst case unnecessary code that just makes your website slower.
Now, the reason for this post: sample code & potential best practice here at Good Old. The following snippet collects information through module_invoke_all() and then lets other modules alter the very same info in a subsequent drupal_alter(). The results is cached in both statically and in the Drupal cache:
<!--break-->
The danger of id numbers as keys in PHP arrays
Putting id numbers as the key in an array can look like a pretty smart decision at first - but it certainly has its pitfalls.
PHP considers numeric keys as array indexes - if you eg. do an array_merge() on an array the index will be rebuilt and start from 0 again - no matter if your keys really was an index or actually was something else like id numbers.
<!--break-->
Take an example like this:
$nodes = array(
13 => 'Python - beauty of nature or geeks?',
25 => 'Interesting win in football',
11 => 'BREAKING: Mona Lisa stopped smiling',
);
$imported_nodes = array(
26 => 'Greenland\'s best beaches: We\'ve the list',
27 => 'Motorist in crash with ideology',
);
$save = array_merge($nodes, $imported_nodes);I would expect _$save_ to contain a bunch of node id:s linked to node titles - but only the titles and the order of the titles are preserved, the index has been rebuilt. A var_dump() gives us this:
array(5) {
[0]=>
string(35) "Python - beauty of nature or geeks?"
[1]=>
string(27) "Interesting win in football"
[2]=>
string(35) "BREAKING: Mona Lisa stopped smiling"
[3]=>
string(40) "Greenland's best beaches: We've the list"
[4]=>
string(31) "Motorist in crash with ideology"
}A solution to this would be to use a foreach-loop instead of array_merge() to merge the two arrays because then the index wouldn't be rebuilt:
$save = $nodes;
foreach ($imported_nodes as $key => $value) {
$save[$key] = $value;
}I ran across into this behavior repeatedly when I was working with some tree-generation for an upcoming patch to the Drupal module Domain Relationships. In Drupal when you use the form element type 'checkboxes' the key and value returned by the form are the same. If you instead add a separate checkbox for each value you can define the key and value separately.
When I was modifying some form elements defined in the Domain Access module as 'checkboxes' and replaced them with several 'checkbox' elements I forgot to define the keys. I made sure to define the correct return values, since that's what should matter, but it didn't work. It became apparent that the Domain Access module actually wasn't checking the returned value - it was checking the key and it's not alone. It's often more convenient - but I had (although accidentally) reset my keys through a couple of array_merge() and they wasn't linked to domain id:s anymore.
That it's easy to reset an arrays index isn't the only reason to think twice about storing id:s in it. Another reason in at least Drupal is that individual form elements (which are array values) might be modified independently from it's key through eg. an #after_build. All such independent modifications becomes unable to modify the value in an array if the value used in eg. a submit handler is the key and not the value defined in eg. #return_value.
Using numeric keys in associative arrays should be used with caution - they are easily lost in the code and depending on them can result in some weird errors - especially if you're working with third party code. If you have to base your keys on numeric values they should be prefixed with a string (the # character is a prime candidate for this) to force PHP to always treat the array as associative. Also remember that storing necessary values as keys ties the array values to the array making it harder to alter them independently.
How to integrate a iPhone application with Drupal
This is a guide to integrating a iPhone application with Drupal using the services module with the REST server and OAuth authentication. <!--break--> First off we have some prerequisites (except those required by Drupal):
- A Mac, yep, this is about iPhone development, no cruddy windows machines here. Feel free to do the drupal bit on any *nix system though.
- The iPhone SDK
- Git
- If you don't have it, install using port, fink, tarball or whatever takes your fancy
You can get by without git, just download the zip- or tar-balls off github and place them in the same places as the clones or submodules.
Getting the basics up and runningYou don't have to use Good Old Drupal. It's basically just the official Drupal release with some added modules and themes in sites/all. But it's what we use at the office, and because of git it makes upgrading all our sites a breeze.
$ mkdir drupal_iphone
$ cd drupal_iphone
$ git clone git://github.com/hugowetterberg/goodold_drupal.git public_html
Pre-Drupal install stuff that you all should be familiar with:
$ mkdir logs
$ cd public_html/sites/default
$ cp default.settings.php settings.php
$ mkdir themes modules files
$ chmod a+w settings.php files
The reason that I create a logs directory is that my vhost setup always looks like this:
<VirtualHost *:80>
DocumentRoot "/Users/hugowett/Projects/drupal_iphone/public_html"
ServerName drupaliphone.local
ErrorLog "../logs"
<Directory "./">
AllowOverride All
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
...and Apache becomes cranky if it's log directory doesn't exist. Needless to say, you need to set up the site so that Apache knows about it (add vhost, restart et cetera) and add a hosts entry to get your dev-domain working locally. I use Hoster to manage my hosts file, but manual editing of the /etc/hosts file works just as well.
Install Drupal as usual and remove write permissions from settings.php.
$ chmod a-w settings.php
Installing modules
There are some basic modules that we should enable to get a web-service that our iPhone app can talk to. First return to public_html, then add the following modules:
// Services (with the REST-patch applied)
$ git submodule add git://github.com/hugowetterberg/services.git sites/default/modules/services
// The rest server
$ git submodule add git://github.com/hugowetterberg/rest_server.git sites/default/modules/rest_server
// OAuth
$ git submodule add git://github.com/hugowetterberg/oauth_common.git sites/default/modules/oauth_common
$ git submodule add git://github.com/hugowetterberg/services_oauth.git sites/default/modules/services_oauth
$ git submodule add git://github.com/hugowetterberg/inputstream.git sites/default/modules/inputstream
// Utility modules
$ git submodule add git://github.com/hugowetterberg/query_builder.git sites/default/modules/query_builder
$ git submodule add git://github.com/hugowetterberg/services_oop.git sites/default/modules/services_oop
Go to admin/build/modules and enable the "OAuth Authentication", "Node Resource", "Query Builder" and "REST Server" modules, this will cause all the required modules to be installed.
ConfigurationGo to admin/settings/oauth/authorizations and delete all the default authorization levels EXCEPT the "read" authorization, we don't need very fine-grained authorization levels for this test.
Go to admin/build/services/settings and set "OAuth authentication" as the authorization module. Then click the "Authentication" tab and set #retrieve and #index to require "Read access", the others should require "Full access".
If you want to try the REST server out a bit in your browser you could set "Required authentication" to "None" for #retrieve and #index. That will disable all of services access checking (Drupal's permission system will still be in effect though). Then create two or more story nodes with random content and check out out the following urls:
- Fetching a resource
- services/rest/node/1.yaml
- services/rest/node/2.yaml
- Using the index
- services/rest/node.yaml
- services/rest/node.yaml?sort_field=created&sort_order=ASC
- services/rest/node.yaml?nid=1
- services/rest/node.yaml?created=1251269200:
- services/rest/node.yaml?created=1251269100:1251269200
- services/rest/node.yaml?fields=nid,title,teaser
- services/rest/node.yaml?fields=nid,title,created,comment_count
- Getting some documentation of the available index operations
- services/rest/node.yaml?__describe
The syntax for a range criteria is "FROM:TO" and ranges can be left open as either "FROM:" or ":TO".
Remember to restore the "Required authentication" to "Consumer key and access token" if you want to restrict access to the read operations.
Create a application entry for the App
Go to user/1/applications/add and give the app a name that suits your iPhone application, maybe "iPhone client". Skip the callback and application type stuff.
The iPhone applicationCreate a iPhone project in Xcode, use the "Window-based application" template. Place it wherever you want, I'll place mine in the folder ~/Documents/CocoaDev and call it "DrupalApp".
If you're feeling very insecure about objective-c (maybe never coded c?), the iPhone and/or Xcode I suggest that you brew a pot of tea, make yourself confortable in a sofa, put the MacBook thingy in your knee and start watching the excellent Stanford lectures on iPhone Development.
Then you need to read this article. It details how to set up a project, and Xcode, to link statically to another project. But in this case we're not going to link to cocos2D, but to a library that I developed for use with my application: HBaseLibrary.
Clone the library to whatever location you choose to place it (Choose? What? Why? Read the article!). I place my shared libraries in ~/Documents/CocoaDev/Libraries, so that's what I'll use in my example here:
$ git clone git://github.com/hugowetterberg/HBaseLibrary.git ~/Documents/CocoaDev/Libraries/HBaseLibrary
Now use your newfound skills (What? Found? Where? Read the article!) to set up "DrupalApp" so that it links to the "HBaseLibrary". Once you've done that, run "DrupalApp" in the simulator to make sure that all the basic stuff is working before we actually start coding.
Creating the interfaceThe first thing we want our app to do is to get authorized by the user. So lets create a view for that. Right-click on Resources and add a new file.

Choose the View XIB template and call your XIB "AuthorizationView.xib". Then double click on it to open it up in Interface Builder. Drag two buttons and a label to the view and play around with the styling, positioning et cetera.
This is what we want to attain:

One button for requesting authorization and one for resetting the authorization. And a label with a cheerful message that we can display when the process is finished. The only slightly tricky thing here is how to get the pin-striped background. Select the view and choose the "groupTableViewBackgroundColor" from the iPhone SDK palette.

All done? Great, that's the V in MVC, now let's move on to the C. Save and go back to Xcode. Right-click on Classes in the "Groups & Files" pane and add a UIViewController subclass called "AuthorizationViewController", if Xcode gives you the option to create a xib at the same time you should just uncheck that, as we have one already. Now you have a .h and a .m file for your controller. The .h (header) file contains the @interface definition for your class, which is the specification for what you'll implement in the .m-file.
Now we should add IBOutlets for the objects in the view that the controller needs to know about, and IBActions for the events that we want to receive notifications of. I have made comments in the code describing the different parts that I'm adding to the header file, that's easier than trying to refer to stuff before we have a common vocabulary.
@interface AuthorizationViewController : UIViewController {
// These are instance variables that will hold pointers
// to interface elements.
UIButton *authorizeButton;
UIButton *resetButton;
UILabel *doinGreatLabel;
}
// Declaration of properties, this is the same as declaring
// - (UIButton)authorizeButton;
// - (void)setAuthorizeButton:(UIButton *);
// ...but a more semantically cohesive declaration. There are
// other benefits, like the ability to @synthesize properties,
// and some memory-management assistance.
// see http://www.google.com/search?q=objective+c+properties
// for more details. IBOutlet is a keyword
// that's removed by the preprocessor (never compiled)
// and is used to tell Interface Builder that it can
// assign objects to them.
@property (retain) IBOutlet UIButton *authorizeButton;
@property (retain) IBOutlet UIButton *resetButton;
@property (retain) IBOutlet UILabel *doinGreatLabel;
// Class methods, the "-"-sign denotes a instance method
// the "+"-sign would be used for class methods. IBAction
// is just a alias for void, but it works just like IBOutlet
// and lets Interface Builder know that it can set the method
// as a target for actions.
- (IBAction)requestAuthorization;
- (IBAction)resetAuthorization;
@end
Then switch to the .m-file, use the cmd-opt-up shortcut (
), or all the manual switching will drive you insane.
The .m file contains a lot of placeholder stuff that's either empty or completely commented out. The -init(WithNibName) method is the equivalent of a constructor in objective-c. Although you shouldn't do too much there in controller classes, as the view isn't ready on init. Another reason is that your view object might be released if the iPhone is short on memory and if your view isn't currently displayed. So all setup relating to the view (or any of the IBOutlet variables) should be done in -viewDidLoad.
Add #import "AuthorizationManager.h" under the other import, so that we can get to the shared authorization manager. Then add a private @interface so that we can declare some internal methods. It's not necessary to do this, but you'll get warnings like this: 'AuthorizationViewController' may not respond to '-updateInterface', when compiling if you don't.
#import "AuthorizationViewController.h"
#import "AuthorizationManager.h"
@interface AuthorizationViewController (Private)
- (void)updateInterfaceAnimated:(BOOL)animated;
@end
Uncomment the -viewDidLoad method and add the following:
- (void)viewDidLoad {
[super viewDidLoad];
[self updateInterfaceAnimated:FALSE];
}
- (void)updateInterfaceAnimated:(BOOL)animated {
AuthorizationManager *manager = [AuthorizationManager sharedManager];
// Disable the authorize button if we already have access
authorizeButton.enabled = ![manager hasAccess];
// Change the title of the authorize button if
// we have a request token.
if ([manager requestToken]) {
[authorizeButton setTitle:@"Verify authorization" forState:UIControlStateNormal];
}
// Adding some animation code here, just to show you
// how easy it is to get good-looking animations
// on the iPhone.
if (animated) {
[UIView beginAnimations:@"showSuccessLabel" context:nil];
[UIView setAnimationDuration:0.3];
}
if ([manager hasAccess]) {
authorizeButton.alpha = 0.0;
doinGreatLabel.alpha = 1.0;
}
else {
authorizeButton.alpha = 1.0;
doinGreatLabel.alpha = 0.0;
}
if (animated) {
[UIView commitAnimations];
}
}
What we've done so far is to define what types of interface elements our controller can handle, and what is should do with them. But we still have to hook up the actual elements in our xib to the outlets and actions in the controller. Open up AuthorizationView.xib and select "File's Owner" and, select the (i) section of the inspector, and write the name of the view controller class in the Class field.

Then open up the connections section in the inspector and start connecting the outlets to the interface elements. Connect the requestAuthorization resetAuthorization actions to the "Touch up inside" events of the respective buttons. Also make sure that the view outlet is connected to the top level view.

Now we're going to make our view show up when we start the application. If you run it in the simulator now everything will (hopefully) compile but you'll still only get the blank white screen. To do something about that, add a IBOutlet (instance variable & property) for a UITabBarController in your app delegate header file (DrupalAppAppDelegate.h). Yep, this is a test. Take a look at the AuthorizationViewController to see how it should be done.
Then open up the MainWindow.xib and add a "Tab Bar Controller" as a top level object. Select the "Drupal App App Delegate" and connect it's tabBarController outlet to the "Tab Bar Controller".
Now it's time to hook up our AuthorizationViewController to the Tab Bar Controller. Open up "DrupalAppAppDelegate.m" and add the following:
#import "DrupalAppAppDelegate.h"
#import "AuthorizationViewController.h"
@implementation DrupalAppAppDelegate
@synthesize window, tabBarController;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Create a instance of our controller, specifying the name of our xib
AuthorizationViewController *auth = [[[AuthorizationViewController alloc] initWithNibName:@"AuthorizationView" bundle:nil] autorelease];
// Create an array of controllers, just the auth controller for now
NSArray *controllers = [[[NSArray alloc] initWithObjects:auth, nil] autorelease];
// Pass the controller array to the tab bar controller
[tabBarController setViewControllers:controllers];
// Add the tab bar view to our window
[window addSubview:tabBarController.view];
[window makeKeyAndVisible];
}
If you run the app in the simulator now you should see the authorization view and a empty tab bar. Let's add some stuff in the AuthorizationViewController implementation that tells the tab bar how to show the authorization view. Uncomment the -initWithNibName function
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
self.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"Authorization" image:[UIImage imageNamed:@"info.png"] tag:0];
}
return self;
}
Download this info image and add it to your project (The Resources folder is a good place to put it). Many of the interface element images in the iPhone are, like this image, purely alpha-channel oriented. Adding images to your project will automatically make them available to be referenced in code like this and selected for use in interface builder.
The authorization process
The first step is to set up the shared authorization manager. Add the following code to the -applicationDidFinishLaunching method in the app delegate. Set it up before you instantiate the AuthorizationViewController. Remember to #import the AuthorizationManager and OAConsumer headers.
// Create a shared authorization manager
NSURL *baseUrl = [NSURL URLWithString:@"http://drupaliphone.local"];
OAConsumer *consumer = [[OAConsumer alloc] initWithKey:@"92dyZAANYnepnjG69suXV775rKDgnhzi" secret:@"ffRTh8pP9gfWSFpu3NJt6j5v6j4ENZSf"];
AuthorizationManager *manager = [[[AuthorizationManager alloc] initWithConsumer:consumer baseUrl:baseUrl] autorelease];
[AuthorizationManager setSharedManager:[manager autorelease]];
The key and secret that I've used in this sample are the ones that I got when I created the iPhone application in Drupal, they won't work for you. Go to user/1/applications and copy the consumer key and secret into your code.
Now open AuthorizationViewController.m, it's time to implement those action methods that we defined in the header. You might have noticed the warnings when compiling, and the fact that the application crashes when you touch any of the buttons in the authorization view. I can recommend two ways of getting the method stubs: 1. copy them from the header file, 2. Write "-re" and press the shortcut key opt+esc to bring up autocompletion options.
- (void)requestAuthorization {
AuthorizationManager *manager = [AuthorizationManager sharedManager];
// Verify access if we got a access token
if ([manager requestToken]) {
[manager getAccessToken];
[self updateInterfaceAnimated:YES];
}
else {
// Get the request token
// If all goes well we'll be booted into Safari
// so we don't have to bother with UI updates
[manager getRequestToken];
}
}
- (void)resetAuthorization {
AuthorizationManager *manager = [AuthorizationManager sharedManager];
[manager resetAuthorization];
[self updateInterfaceAnimated:YES];
}
Great! You should now have a fully working OAuth workflow. Try it out by starting up your app in the simulator and pressing "Request authorization". Then log in and approve the authorization request. Start up your app and press the "Verify authorization button". If all went well you should see the "Great! You're authorized text" and the app should persist the authorization between launches.
The table view and the REST client
Now we're going to create a NodeListController. Right-click on "Classes" and add a UITableViewController subclass just as you did with the AuthorizationViewController.

Open the header file for the NodeListController add a #import for RESTClient. A RESTClient and a NSArray instance variable and a init method that takes a rest client in addition to the standard style argument.
#import "RESTClient.h"
@interface NodeListController : UITableViewController {
RESTClient *client;
NSArray *nodes;
}
- (id)initWithStyle:(UITableViewStyle)style restClient:(RESTClient *)aClient;
@end
Then switch to the implementation file and uncomment the init function and add a rest client parameter as per the header definition. Assign the client parameter to the instance variable and take care to retain it, otherwise it'll get deallocated by the release pool.
- (id)initWithStyle:(UITableViewStyle)style restClient:(RESTClient *)aClient {
if (self = [super initWithStyle:style]) {
client = [aClient retain];
// Set some info for the tab bar, we'll use one of the built in items this time
self.tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:UITabBarSystemItemMostRecent tag:0];
// The only parameters we need is some sorting stuff,
// probably redundant, but it showcases parameter sending
NSMutableDictionary *params = [[[NSMutableDictionary alloc] init] autorelease];
[params setObject:@"created" forKey:@"sort_field"];
[params setObject:@"DESC" forKey:@"sort_order"];
// Tell the rest client to load the node index
// We provide two selectors for callbacks, one
// for success and one for failure
[client getResourceAsync:[NSURL URLWithString:@"http://drupaliphone.local/services/rest/node.json"]
method:@"GET"
parameters:params
target:self
selector:@selector(restRequest:nodesLoaded:)
failSelector:@selector(restRequest:nodesFailedToLoad:)];
}
return self;
}
Now we'll implement the methods we referenced in the selector.
- (void)restRequest:(id)request nodesLoaded:(NSArray *)loadedNodes {
// Store and retain the results
nodes = [loadedNodes retain];
// ...and tell the table view to load the new data
[self.tableView reloadData];
}
- (void)restRequest:(id)request nodesFailedToLoad:(NSError *)error {
// Tell the user about the failure
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Failed to load nodes"
message:[error localizedDescription]
delegate:nil cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alert show];
[alert release];
}
There are some more methods we need to implement. There are already placeholders that you can use as a starting point further down in the code. The first method is used to tell the table view how many rows we have data for. The second is for creating custom table cells for displaying our data.
// We'll pass the node count if they've been
// loaded, otherwise we'll return zero.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
int count = 0;
if (nodes) {
count = [nodes count];
}
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"Node cell";
// Cells are reused to increase performance
// when scrolling a table view.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
// Create a new cell if we didn't get one to reuse
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
}
// Get the node and set the label text for the cell
NSDictionary *node = [nodes objectAtIndex:indexPath.row];
cell.textLabel.text = [node objectForKey:@"title"];
return cell;
}
Finally we need to set up the rest client in the application delegate. The REST client is a part of the HBaseLibrary. It uses a delegate to implement it's authentication process, and there's already a delegate implemented to integrate with the AuthorizationManager. Instantiate the RESTClient in -applicationDidFinishLaunching, after the AuthorizationManager has been created. Our old code for the authorization controller instantiation is included, and the controller array initialization has been changed to include the node list controller.
// Create the REST client
RESTClient *client = [[[RESTClient alloc] init] autorelease];
OAuthRESTClientDelegate *oauthDelegate = [[OAuthRESTClientDelegate alloc] initWithAuthorizationManager:manager];
client.delegate = [oauthDelegate autorelease];
// Create a instance of our controller, specifying the name of our xib
AuthorizationViewController *auth = [[[AuthorizationViewController alloc] initWithNibName:@"AuthorizationView" bundle:nil] autorelease];
// Create a instance of our node list controller
NodeListController *nodeList = [[[NodeListController alloc] initWithStyle:UITableViewStylePlain restClient:client] autorelease];
// Create an array of controllers
NSArray *controllers = [[[NSArray alloc] initWithObjects:nodeList, auth, nil] autorelease];
// Pass the controller array to the tab bar controller
[tabBarController setViewControllers:controllers];
// Select the authorization view if we haven't been authorized
if (![manager hasAccess]) {
[tabBarController setSelectedIndex:1];
}
That's it. It's a simple application, but it integrates properly with Drupal using OAuth and REST, hopefully it'll get you started. There will be a part 2 of this guide, that will add support for write operations (node creation and updating) and maybe some image uploading as well.
The code and database dumpThere are three repositories that accompanies this blog post:
- The iPhone application
- The Drupal site
- The blog post and database dump
Remember to to run git submodule update --init when you check out the drupal repo, many of the used drupal modules are included as submodules.
New Drupal module for DRY CSS
I've created a new Drupal module, cssdry, for writing DRY (don't repeat yourself) CSS. The module leverages a CSSProcessor class written by Allain Lalonde, with some alterations to support CSS2 attribute selector and relative url rewriting.
<!--break-->
The easiest way to demonstrate it's functionality is by example. So here we go.
oEmbed, easier embedding for Drupal
Fighting seagulls
A long standing issue on sites is how to treat the embedding of different content on your site in a user friendly and standard compliant way.
Usually YouTube gives you an embed-code with an object-tag and a nested embed-tag which your not-that-html-knowledgeable users should try to insert into their content - bypassing a wysiwyg etc.
With oEmbed no embed-codes needs to be pasted in anymore. It's an open standard for the automatic embedding of content using just the ordinary url to the resource.
The standard is natively supported by Flickr, Vimeo, Hulu, Qik etc, and since a week ago also Scribd. Many popular sites that don't support oEmbed natively are supported by third party initiatives like oohEmbed (which is released as open source).
Our oEmbed module for Drupal, which I've now released on Drupal.org, supports both embedding of other sites content into a Drupal site and embedding of the Drupal site's content on other oEmbed consuming sites.
This release is contains the base features needed, but there's still much work I'm planning on getting done on it. Among other things the module could make use of a better admin interface to make it easy to add support for new oEmbed sites without hacking the code. It could also be interesting to try and make use of the auto discovery for oEmbed, with security in mind of course, to discover new embeddable content.
An example of a Flickr-image embedded with the oEmbed module can be seen at the top of this entry - embedded by just pasting http://www.flickr.com/photos/wetterberg/3478520059/ into the textarea. It can of course be styled to your own needs.
The module is production ready - we're using it on a couple of live sites. Download it and replace your current URL-filter in your input filter with the oEmbed filter.
