[iOS] Upstream PLATFORM(IOS) changes to Source/WebKit/
[WebKit-https.git] / Source / WebKit / mac / Misc / WebNSURLExtras.mm
1 /*
2  * Copyright (C) 2005, 2007, 2008, 2009 Apple Inc. All rights reserved.
3  * Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer. 
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution. 
14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission. 
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #import "WebNSURLExtras.h"
31
32 #import "WebKitNSStringExtras.h"
33 #import "WebLocalizableStrings.h"
34 #import "WebNSDataExtras.h"
35 #import "WebSystemInterface.h"
36 #import <Foundation/NSURLRequest.h>
37 #import <WebCore/URL.h>
38 #import <WebCore/LoaderNSURLExtras.h>
39 #import <WebCore/WebCoreNSURLExtras.h>
40 #import <WebKitSystemInterface.h>
41 #import <wtf/Assertions.h>
42 #import <wtf/ObjcRuntimeExtras.h>
43 #import <unicode/uchar.h>
44 #import <unicode/uscript.h>
45
46 #if PLATFORM(IOS)
47 #import <Foundation/NSString_NSURLExtras.h>
48 #endif
49
50 using namespace WebCore;
51 using namespace WTF;
52
53 #if PLATFORM(IOS)
54 // Fake URL scheme.
55 #define WebDataProtocolScheme @"webkit-fake-url"
56 #endif
57
58 #define URL_BYTES_BUFFER_LENGTH 2048
59
60 @implementation NSURL (WebNSURLExtras)
61
62 + (NSURL *)_web_URLWithUserTypedString:(NSString *)string relativeToURL:(NSURL *)URL
63 {
64     return URLWithUserTypedString(string, URL);
65 }
66
67 + (NSURL *)_web_URLWithUserTypedString:(NSString *)string
68 {
69     return URLWithUserTypedString(string, nil);
70 }
71
72 + (NSURL *)_web_URLWithDataAsString:(NSString *)string
73 {
74     if (string == nil) {
75         return nil;
76     }
77     return [self _web_URLWithDataAsString:string relativeToURL:nil];
78 }
79
80 + (NSURL *)_web_URLWithDataAsString:(NSString *)string relativeToURL:(NSURL *)baseURL
81 {
82     if (string == nil) {
83         return nil;
84     }
85     string = [string _webkit_stringByTrimmingWhitespace];
86     NSData *data = [string dataUsingEncoding:NSISOLatin1StringEncoding];
87     return URLWithData(data, baseURL);
88 }
89
90 + (NSURL *)_web_URLWithData:(NSData *)data
91 {
92     return URLWithData(data, nil);
93 }      
94
95 + (NSURL *)_web_URLWithData:(NSData *)data relativeToURL:(NSURL *)baseURL
96 {
97     return URLWithData(data, baseURL);
98 }
99
100 - (NSData *)_web_originalData
101 {
102     return originalURLData(self);
103 }
104
105 - (NSString *)_web_originalDataAsString
106 {
107     return [[[NSString alloc] initWithData:originalURLData(self) encoding:NSISOLatin1StringEncoding] autorelease];
108 }
109
110 - (NSString *)_web_userVisibleString
111 {
112     return userVisibleString(self);
113 }
114
115 - (BOOL)_web_isEmpty
116 {
117     if (!CFURLGetBaseURL((CFURLRef)self))
118         return CFURLGetBytes((CFURLRef)self, NULL, 0) == 0;
119     return [originalURLData(self) length] == 0;
120 }
121
122 - (const char *)_web_URLCString
123 {
124     NSMutableData *data = [NSMutableData data];
125     [data appendData:originalURLData(self)];
126     [data appendBytes:"\0" length:1];
127     return (const char *)[data bytes];
128  }
129
130 - (NSURL *)_webkit_canonicalize
131 {    
132     NSURLRequest *request = [[NSURLRequest alloc] initWithURL:self];
133     Class concreteClass = WKNSURLProtocolClassForRequest(request);
134     if (!concreteClass) {
135         [request release];
136         return self;
137     }
138     
139     // This applies NSURL's concept of canonicalization, but not URL's concept. It would
140     // make sense to apply both, but when we tried that it caused a performance degradation
141     // (see 5315926). It might make sense to apply only the URL concept and not the NSURL
142     // concept, but it's too risky to make that change for WebKit 3.0.
143     NSURLRequest *newRequest = [concreteClass canonicalRequestForRequest:request];
144     NSURL *newURL = [newRequest URL]; 
145     NSURL *result = [[newURL retain] autorelease]; 
146     [request release];
147     
148     return result;
149 }
150
151 - (NSURL *)_web_URLByTruncatingOneCharacterBeforeComponent:(CFURLComponentType)component
152 {
153     return URLByTruncatingOneCharacterBeforeComponent(self, component);
154 }
155
156 - (NSURL *)_webkit_URLByRemovingFragment
157 {
158     return URLByTruncatingOneCharacterBeforeComponent(self, kCFURLComponentFragment);
159 }
160
161 - (NSURL *)_webkit_URLByRemovingResourceSpecifier
162 {
163     return URLByTruncatingOneCharacterBeforeComponent(self, kCFURLComponentResourceSpecifier);
164 }
165
166 - (NSURL *)_web_URLByRemovingUserInfo
167 {
168     return URLByRemovingUserInfo(self);
169 }
170
171 - (BOOL)_webkit_isJavaScriptURL
172 {
173     return [[self _web_originalDataAsString] _webkit_isJavaScriptURL];
174 }
175
176 - (NSString *)_webkit_scriptIfJavaScriptURL
177 {
178     return [[self absoluteString] _webkit_scriptIfJavaScriptURL];
179 }
180
181 - (BOOL)_webkit_isFileURL
182 {    
183     return [[self _web_originalDataAsString] _webkit_isFileURL];
184 }
185
186 - (BOOL)_webkit_isFTPDirectoryURL
187 {
188     return [[self _web_originalDataAsString] _webkit_isFTPDirectoryURL];
189 }
190
191 - (BOOL)_webkit_shouldLoadAsEmptyDocument
192 {
193     return [[self _web_originalDataAsString] _webkit_hasCaseInsensitivePrefix:@"about:"] || [self _web_isEmpty];
194 }
195
196 - (NSURL *)_web_URLWithLowercasedScheme
197 {
198     CFRange range;
199     CFURLGetByteRangeForComponent((CFURLRef)self, kCFURLComponentScheme, &range);
200     if (range.location == kCFNotFound) {
201         return self;
202     }
203     
204     UInt8 static_buffer[URL_BYTES_BUFFER_LENGTH];
205     UInt8 *buffer = static_buffer;
206     CFIndex bytesFilled = CFURLGetBytes((CFURLRef)self, buffer, URL_BYTES_BUFFER_LENGTH);
207     if (bytesFilled == -1) {
208         CFIndex bytesToAllocate = CFURLGetBytes((CFURLRef)self, NULL, 0);
209         buffer = static_cast<UInt8 *>(malloc(bytesToAllocate));
210         bytesFilled = CFURLGetBytes((CFURLRef)self, buffer, bytesToAllocate);
211         ASSERT(bytesFilled == bytesToAllocate);
212     }
213     
214     int i;
215     BOOL changed = NO;
216     for (i = 0; i < range.length; ++i) {
217         char c = buffer[range.location + i];
218         char lower = toASCIILower(c);
219         if (c != lower) {
220             buffer[range.location + i] = lower;
221             changed = YES;
222         }
223     }
224     
225     NSURL *result = changed
226         ? CFBridgingRelease(CFURLCreateAbsoluteURLWithBytes(NULL, buffer, bytesFilled, kCFStringEncodingUTF8, nil, YES))
227         : self;
228
229     if (buffer != static_buffer) {
230         free(buffer);
231     }
232     
233     return result;
234 }
235
236
237 -(NSData *)_web_schemeSeparatorWithoutColon
238 {
239     NSData *result = nil;
240     CFRange rangeWithSeparators;
241     CFRange range = CFURLGetByteRangeForComponent((CFURLRef)self, kCFURLComponentScheme, &rangeWithSeparators);
242     if (rangeWithSeparators.location != kCFNotFound) {
243         NSString *absoluteString = [self absoluteString];
244         NSRange separatorsRange = NSMakeRange(range.location + range.length + 1, rangeWithSeparators.length - range.length - 1);
245         if (separatorsRange.location + separatorsRange.length <= [absoluteString length]) {
246             NSString *slashes = [absoluteString substringWithRange:separatorsRange];
247             result = [slashes dataUsingEncoding:NSISOLatin1StringEncoding];
248         }
249     }
250     return result;
251 }
252
253 -(NSData *)_web_dataForURLComponentType:(CFURLComponentType)componentType
254 {
255     return dataForURLComponentType(self, componentType);
256 }
257
258 -(NSData *)_web_schemeData
259 {
260     return dataForURLComponentType(self, kCFURLComponentScheme);
261 }
262
263 -(NSData *)_web_hostData
264 {
265     NSData *result = dataForURLComponentType(self, kCFURLComponentHost);
266     NSData *scheme = [self _web_schemeData];
267     // Take off localhost for file
268     if ([scheme _web_isCaseInsensitiveEqualToCString:"file"]) {
269         return ([result _web_isCaseInsensitiveEqualToCString:"localhost"]) ? nil : result;
270     }
271     return result;
272 }
273
274 - (NSString *)_web_hostString
275 {
276     return [[[NSString alloc] initWithData:[self _web_hostData] encoding:NSUTF8StringEncoding] autorelease];
277 }
278
279 - (NSString *)_webkit_suggestedFilenameWithMIMEType:(NSString *)MIMEType
280 {
281     return suggestedFilenameWithMIMEType(self, MIMEType);
282 }
283
284 - (NSURL *)_webkit_URLFromURLOrSchemelessFileURL
285 {
286     if ([self scheme])
287         return self;
288
289     return [NSURL URLWithString:[@"file:" stringByAppendingString:[self absoluteString]]];
290 }
291
292 #if PLATFORM(IOS)
293 static inline NSURL *createYouTubeURL(NSString *videoID, NSString *timeID)
294 {
295     // This method creates a youtube: URL, which is an internal format that is passed to YouTube.app.
296     // If the format of these internal links is changed below, then the app probably needs to be updated as well.
297
298     ASSERT(videoID && [videoID length] > 0 && ![videoID isEqualToString:@"/"]);
299
300     if ([timeID length])
301         return [NSURL URLWithString:[NSString stringWithFormat:@"youtube:%@?t=%@", videoID, timeID]];
302     return [NSURL URLWithString:[NSString stringWithFormat:@"youtube:%@", videoID]];
303 }
304
305 - (NSURL *)_webkit_youTubeURL
306 {
307     // Bail out early if we aren't even on www.youtube.com or youtube.com.
308     NSString *scheme = [[self scheme] lowercaseString];
309     if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"])
310         return nil;
311
312     NSString *hostName = [[self host] lowercaseString];
313     BOOL isYouTubeMobileWebAppURL = [hostName isEqualToString:@"m.youtube.com"];
314     BOOL isYouTubeShortenedURL = [hostName isEqualToString:@"youtu.be"];
315     if (!isYouTubeMobileWebAppURL
316         && !isYouTubeShortenedURL
317         && ![hostName isEqualToString:@"www.youtube.com"]
318         && ![hostName isEqualToString:@"youtube.com"])
319         return nil;
320
321     NSString *path = [self path];
322
323     // Short URL of the form: http://youtu.be/v1d301D
324     if (isYouTubeShortenedURL) {
325         NSString *videoID = [path lastPathComponent];
326         if (videoID && ![videoID isEqualToString:@"/"])
327             return createYouTubeURL(videoID, nil);
328         return nil;
329     }
330
331     NSString *query = [self query];
332     NSString *fragment = [self fragment];
333
334     // On the YouTube mobile web app, the path and query string are put into the
335     // fragment so that one web page is only ever loaded (see <rdar://problem/9550639>).
336     if (isYouTubeMobileWebAppURL) {
337         NSRange range = [fragment rangeOfString:@"?"];
338         if (range.location == NSNotFound) {
339             path = fragment;
340             query = nil;
341         } else {
342             path = [fragment substringToIndex:range.location];
343             query = [fragment substringFromIndex:(range.location + 1)];
344         }
345         fragment = nil;
346     }
347
348     if ([[path lowercaseString] isEqualToString:@"/watch"]) {
349         if ([query length]) {
350             NSString *videoID = [[query _webkit_queryKeysAndValues] objectForKey:@"v"];
351             if (videoID) {
352                 NSDictionary *fragmentDict = [[self fragment] _webkit_queryKeysAndValues];
353                 NSString *timeID = [fragmentDict objectForKey:@"t"];
354                 return createYouTubeURL(videoID, timeID);
355             }
356         }
357
358         // May be a new-style link (see <rdar://problem/7733692>).
359         if ([fragment hasPrefix:@"!"]) {
360             query = [fragment substringFromIndex:1];
361
362             if ([query length]) {
363                 NSDictionary *queryDict = [query _webkit_queryKeysAndValues];
364                 NSString *videoID = [queryDict objectForKey:@"v"];
365                 if (videoID) {
366                     NSString *timeID = [queryDict objectForKey:@"t"];
367                     return createYouTubeURL(videoID, timeID);
368                 }
369             }
370         }
371     } else if ([path _web_hasCaseInsensitivePrefix:@"/v/"] || [path _web_hasCaseInsensitivePrefix:@"/e/"]) {
372         NSString *videoID = [path lastPathComponent];
373
374         // These URLs are funny - they don't have a ? for the first query parameter.
375         // Strip all characters after and including '&' to remove extraneous parameters after the video ID.
376         NSRange ampersand = [videoID rangeOfString:@"&"];
377         if (ampersand.location != NSNotFound)
378             videoID = [videoID substringToIndex:ampersand.location];
379
380         if (videoID)
381             return createYouTubeURL(videoID, nil);
382     }
383
384     return nil;
385 }
386
387 + (NSURL *)uniqueURLWithRelativePart:(NSString *)relativePart
388 {
389     CFUUIDRef UUIDRef = CFUUIDCreate(kCFAllocatorDefault);
390     NSString *UUIDString = (NSString *)CFUUIDCreateString(kCFAllocatorDefault, UUIDRef);
391     CFRelease(UUIDRef);
392     NSURL *URL = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@/%@", WebDataProtocolScheme, UUIDString, relativePart]];
393     CFRelease(UUIDString);
394     
395     return URL;
396 }
397
398 #endif // PLATFORM(IOS)
399 @end
400
401 @implementation NSString (WebNSURLExtras)
402
403 - (BOOL)_web_isUserVisibleURL
404 {
405     return isUserVisibleURL(self);
406 }
407
408 - (BOOL)_webkit_isJavaScriptURL
409 {
410     return [self _webkit_hasCaseInsensitivePrefix:@"javascript:"];
411 }
412
413 - (BOOL)_webkit_isFileURL
414 {
415     return [self rangeOfString:@"file:" options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound;
416 }
417
418 - (NSString *)_webkit_stringByReplacingValidPercentEscapes
419 {
420     return decodeURLEscapeSequences(self);
421 }
422
423 - (NSString *)_webkit_scriptIfJavaScriptURL
424 {
425     if (![self _webkit_isJavaScriptURL]) {
426         return nil;
427     }
428     return [[self substringFromIndex:11] _webkit_stringByReplacingValidPercentEscapes];
429 }
430
431 - (BOOL)_webkit_isFTPDirectoryURL
432 {
433     int length = [self length];
434     if (length < 5) {  // 5 is length of "ftp:/"
435         return NO;
436     }
437     unichar lastChar = [self characterAtIndex:length - 1];
438     return lastChar == '/' && [self _webkit_hasCaseInsensitivePrefix:@"ftp:"];
439 }
440
441 - (BOOL)_web_hostNameNeedsDecodingWithRange:(NSRange)range
442 {
443     return hostNameNeedsDecodingWithRange(self, range);
444 }
445
446 - (BOOL)_web_hostNameNeedsEncodingWithRange:(NSRange)range
447 {
448     return hostNameNeedsEncodingWithRange(self, range);
449 }
450
451 - (NSString *)_web_decodeHostNameWithRange:(NSRange)range
452 {
453     return decodeHostNameWithRange(self, range);
454 }
455
456 - (NSString *)_web_encodeHostNameWithRange:(NSRange)range
457 {
458     return encodeHostNameWithRange(self, range);
459 }
460
461 - (NSString *)_web_decodeHostName
462 {
463     return decodeHostName(self);
464 }
465
466 - (NSString *)_web_encodeHostName
467 {
468     return encodeHostName(self);
469 }
470
471 -(NSRange)_webkit_rangeOfURLScheme
472 {
473     NSRange colon = [self rangeOfString:@":"];
474     if (colon.location != NSNotFound && colon.location > 0) {
475         NSRange scheme = {0, colon.location};
476         static NSCharacterSet *InverseSchemeCharacterSet = nil;
477         if (!InverseSchemeCharacterSet) {
478             /*
479              This stuff is very expensive.  10-15 msec on a 2x1.2GHz.  If not cached it swamps
480              everything else when adding items to the autocomplete DB.  Makes me wonder if we
481              even need to enforce the character set here.
482             */
483             NSString *acceptableCharacters = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+.-";
484             InverseSchemeCharacterSet = [[[NSCharacterSet characterSetWithCharactersInString:acceptableCharacters] invertedSet] retain];
485         }
486         NSRange illegals = [self rangeOfCharacterFromSet:InverseSchemeCharacterSet options:0 range:scheme];
487         if (illegals.location == NSNotFound)
488             return scheme;
489     }
490     return NSMakeRange(NSNotFound, 0);
491 }
492
493 -(BOOL)_webkit_looksLikeAbsoluteURL
494 {
495     // Trim whitespace because _web_URLWithString allows whitespace.
496     return [[self _webkit_stringByTrimmingWhitespace] _webkit_rangeOfURLScheme].location != NSNotFound;
497 }
498
499 - (NSString *)_webkit_URLFragment
500 {
501     NSRange fragmentRange;
502     
503     fragmentRange = [self rangeOfString:@"#" options:NSLiteralSearch];
504     if (fragmentRange.location == NSNotFound)
505         return nil;
506     return [self substringFromIndex:fragmentRange.location + 1];
507 }
508
509 #if PLATFORM(IOS)
510
511 - (NSString *)_webkit_unescapedQueryValue
512 {
513     NSMutableString *string = [[[self stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] mutableCopy] autorelease];
514     if (!string) // If we failed to decode the URL as UTF8, fall back to Latin1
515         string = [[[self stringByReplacingPercentEscapesUsingEncoding:NSISOLatin1StringEncoding] mutableCopy] autorelease];
516     [string replaceOccurrencesOfString:@"+" withString:@" " options:NSLiteralSearch range:NSMakeRange(0, [string length])];
517     return string;
518 }
519
520 - (NSDictionary *)_webkit_queryKeysAndValues
521 {
522     unsigned queryLength = [self length];
523     if (!queryLength)
524         return nil;
525     
526     NSMutableDictionary *queryKeysAndValues = nil;
527     NSRange equalSearchRange = NSMakeRange(0, queryLength);
528     
529     while (equalSearchRange.location < queryLength - 1 && equalSearchRange.length) {
530         
531         // Search for "=".
532         NSRange equalRange = [self rangeOfString:@"=" options:NSLiteralSearch range:equalSearchRange];
533         if (equalRange.location == NSNotFound)
534             break;
535         
536         unsigned indexAfterEqual = equalRange.location + 1;
537         if (indexAfterEqual > queryLength - 1)
538             break;
539                         
540         // Get the key before the "=".
541         NSRange keyRange = NSMakeRange(equalSearchRange.location, equalRange.location - equalSearchRange.location);
542         
543         // Seach for the ampersand.
544         NSRange ampersandSearchRange = NSMakeRange(indexAfterEqual, queryLength - indexAfterEqual);
545         NSRange ampersandRange = [self rangeOfString:@"&" options:NSLiteralSearch range:ampersandSearchRange];
546         
547         // Get the value after the "=", before the ampersand.
548         NSRange valueRange;
549         if (ampersandRange.location != NSNotFound)
550             valueRange = NSMakeRange(indexAfterEqual, ampersandRange.location - indexAfterEqual);
551         else 
552             valueRange = NSMakeRange(indexAfterEqual, queryLength - indexAfterEqual);
553                 
554         // Save the key and the value.
555         if (keyRange.length && valueRange.length) {
556             if (queryKeysAndValues == nil)
557                 queryKeysAndValues = [NSMutableDictionary dictionary];
558             NSString *key = [[self substringWithRange:keyRange] lowercaseString];
559             NSString *value = [[self substringWithRange:valueRange] _webkit_unescapedQueryValue];
560             if ([key length] && [value length])
561                 [queryKeysAndValues setObject:value forKey:key];
562         }
563         
564         // At the end.
565         if (ampersandRange.location == NSNotFound)
566             break;
567         
568         // Continue searching after the ampersand.
569         unsigned indexAfterAmpersand = ampersandRange.location + 1;
570         equalSearchRange = NSMakeRange(indexAfterAmpersand, queryLength - indexAfterAmpersand);
571     }
572     
573     return queryKeysAndValues;
574 }
575
576 #endif // PLATFORM(IOS)
577
578 @end