5.2 Read GET and POST variables from forms and URLs
by Marko Riedel
5.2.1 Idea
We add a category to NSMutableDictionary that contains a method that initializes the dictionary with the
variables that were passed to the CGI script.
The procedure is simple.
- Obtain the data from environment variables: if REQUEST_METHOD is POST read CONTENT_LENGTH bytes
from the standard input; if it is GET, use the contents of QUERY_STRING.
- Split the data into key-value pairs. This is done by splitting on the ampersand character &.
The data consists of a sequence of such pairs, where the equal sign = denotes assignment, as
in
- Replace the plus character + by a space and hexadecimal escape sequences of the form %XX by the
corresponding ASCII character in both the key and the value.
5.2.2 Implementation
We start by adding a method to NSString that will carry out the last of the steps that we described above. It will
scan the respective C string and and write characters into a temporary buffer. We allocate the same number of bytes
as in the original string since it can only get shorter.
@implementation NSString (CGI) - parseCGI { int pos, length = [self length]; const char *cstr = [self cString]; NSZone *zone = NSDefaultMallocZone(); char *rbuf, *rcur; NSString *result; rbuf = rcur = NSZoneMalloc(zone, length*sizeof(char));
We use a loop to do the actual processing. A plus character is recorded as a space character. A complete
hexadecimal escape sequence is passed to sscanf for conversion into an unsigned integer, the lowest byte of which is
recorded in the buffer.
for(pos=0; pos<length; pos++){ if(cstr[pos]==’+’
){ *rcur++ = ’_’
; } else if(cstr[pos]==’%’
&& pos+2<length && isxdigit(cstr[pos+1]) && isxdigit(cstr[pos+2])){ char hex[3] = { cstr[pos+1], cstr[pos+2], 0 }; unsigned int dec; sscanf(hex, ”%x”
, &dec); pos += 2; *rcur++ = dec; } else{ *rcur++ = cstr[pos]; } } result = [NSString stringWithCString:rbuf length:rcur-rbuf]; NSZoneFree(zone, rbuf); return result; }
Finally the method converts the data into the result string and frees the temporary buffer.
The method initWithCGI (added to NSMutableString) actutally creates the dictionary. We prepare for this by
reading the process environment and immediately consulting REQUEST_METHOD to determine how to obtain the raw
data. We will scan the data from start to finish and declare three variables to maintain state during
the scan: prev, the first character of the current key-value pair, eqpos, the position of the equal
sign in the current pair, and pos, which gives the first position of the next pair or the end of the
data.
@implementation NSMutableDictionary (CGI) - (id)initWithCGI { NSProcessInfo *procInfo = [NSProcessInfo processInfo]; NSDictionary *env = [procInfo environment]; NSString *method = [env objectForKey:@”REQUEST_METHOD”
]; int length, prev, pos, eqpos; const char *bytes;
We obtain the data first. REQUEST_METHOD tells us how. We raise an exception if the data available on the
standard input do not contain the number of bytes specified in CONTENT_LENGTH (this applies to POST
submissions). We initialize the dictionary by preparing it to hold eight pairs (we could have chosen a different
capacity).
if([method isEqualToString:@”GET”
]){ NSString *str = [env objectForKey:@”QUERY_STRING”
]; length = [str length]; bytes = [str cString]; } else if([method isEqualToString:@”POST”
]){ NSData *data; length = [[env objectForKey:@”CONTENT_LENGTH”
] intValue]; data = [[NSFileHandle fileHandleWithStandardInput] readDataOfLength:length]; if([data length]!=length){ NSString *fmt = @”expected_%d_characters_on_STDIN,_got_%d”
; [NSException raise:NSRangeException format:fmt, length, [data length]]; } bytes = [data bytes]; } else{ [NSException raise:NSInvalidArgumentException format:@”request_method_not_supported:_%@”
, method]; } [self initWithCapacity:8];
Now start the loop. We record the most recent occurence of the equal sign for use in the key-value
split.
for(prev=0, pos=0; pos<length; pos++){ if(bytes[pos]==’=’
){ eqpos = pos; }
An ampersand signals that a complete pair has been scanned, as does having scanned up to the last character of
the string. We compute the length of the key string and the value string and make sure they contain meaningful
values, e.g. the key should not be empty.
if(bytes[pos]==’&’
∣∣ (pos>0 && pos==length-1)){ int klen, vlen; NSString *key, *value; id lookup; klen = eqpos-prev; vlen = pos-eqpos-1+(bytes[pos]==’&’
? 0 : 1); if(!(klen>0 && vlen>=0)){ [NSException raise:NSInvalidArgumentException format:@”malformed_query/content”
]; }
It remains to convert the source bytes into Objective C strings and parse them with parseCGI as discussed
previously.
key = [[NSString stringWithCString:bytes+prev length:klen] parseCGI]; value = [[NSString stringWithCString:bytes+eqpos+1 length:vlen] parseCGI];
Now put the pair into the dictionary. There are three cases.
- The key is not in the dictionary. We add the key-value pair.
- The key is already in the dictionary but it is not an array. We have found a second value for the key,
and replace the object that it refers to by an array containing the two values.
- The key is in the dictionary and refers to an array. We add the new value to the array.
Multiple values for the same key arise when the state of a set of checkboxes is submitted. We store the start
position of the next pair in prev at the end of the body of the loop.
lookup = [self objectForKey:key]; if(lookup==nil){ [self setObject:value forKey:key]; } else if([lookup isKindOfClass: [NSMutableArray class]]==NO){ [self setObject:[NSMutableArray arrayWithObjects:value, lookup, nil] forKey:key]; } else{ [lookup addObject:value]; } prev = pos+1; } } return self; }
The routine returns self, which now contains all pairs that were submitted.
This code was inspired by cgi-lib.pl by Steven E. Brenner.