[GTK][WPE] Need a function to convert internal URI to display ("pretty") URI
[WebKit-https.git] / Source / WTF / wtf / cocoa / NSURLExtras.mm
1 /*
2  * Copyright (C) 2005, 2007, 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import "config.h"
30 #import <Foundation/Foundation.h>
31 #import "NSURLExtras.h"
32
33 #import "CFURLExtras.h"
34 #import "URLParser.h"
35 #import <wtf/Function.h>
36 #import <wtf/HexNumber.h>
37 #import <wtf/ObjCRuntimeExtras.h>
38 #import <wtf/RetainPtr.h>
39 #import <wtf/URLHelpers.h>
40 #import <wtf/Vector.h>
41
42 #define URL_BYTES_BUFFER_LENGTH 2048
43
44 namespace WTF {
45
46 using namespace URLHelpers;
47
48 static BOOL readIDNScriptWhiteListFile(NSString *filename)
49 {
50     if (!filename)
51         return NO;
52
53     FILE *file = fopen([filename fileSystemRepresentation], "r");
54     if (!file)
55         return NO;
56     
57     // Read a word at a time.
58     // Allow comments, starting with # character to the end of the line.
59     while (1) {
60         // Skip a comment if present.
61         if (fscanf(file, " #%*[^\n\r]%*[\n\r]") == EOF)
62             break;
63         
64         // Read a script name if present.
65         char word[33];
66         int result = fscanf(file, " %32[^# \t\n\r]%*[^# \t\n\r] ", word);
67         if (result == EOF)
68             break;
69         
70         if (result == 1) {
71             // Got a word, map to script code and put it into the array.
72             whiteListIDNScript(word);
73         }
74     }
75     fclose(file);
76     return YES;
77 }
78
79 namespace URLHelpers {
80
81 void loadIDNScriptWhiteList()
82 {
83     static dispatch_once_t flag;
84     dispatch_once(&flag, ^{
85         // Read white list from library.
86         NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
87         int numDirs = [dirs count];
88         for (int i = 0; i < numDirs; i++) {
89             if (readIDNScriptWhiteListFile([[dirs objectAtIndex:i] stringByAppendingPathComponent:@"IDNScriptWhiteList.txt"]))
90                 return;
91         }
92         initializeDefaultIDNScriptWhiteList();
93     });
94 }
95
96 } // namespace URLHelpers
97     
98 static String decodePercentEscapes(const String& string)
99 {
100     NSString *substring = (NSString *)string;
101     substring = CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapes(nullptr, (CFStringRef)substring, CFSTR("")));
102
103     if (!substring)
104         return string;
105
106     return (String)substring;
107 }
108
109 NSString *decodeHostName(NSString *string)
110 {
111     std::optional<String> host = mapHostName(string, std::nullopt);
112     if (!host)
113         return nil;
114     return !*host ? string : (NSString *)*host;
115 }
116
117 NSString *encodeHostName(NSString *string)
118 {
119     std::optional<String> host = mapHostName(string, decodePercentEscapes);
120     if (!host)
121         return nil;
122     return !*host ? string : (NSString *)*host;
123 }
124
125 static RetainPtr<NSString> stringByTrimmingWhitespace(NSString *string)
126 {
127     auto trimmed = adoptNS([string mutableCopy]);
128     CFStringTrimWhitespace((__bridge CFMutableStringRef)trimmed.get());
129     return trimmed;
130 }
131
132 NSURL *URLByTruncatingOneCharacterBeforeComponent(NSURL *URL, CFURLComponentType component)
133 {
134     if (!URL)
135         return nil;
136     
137     CFRange fragRg = CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, component, nullptr);
138     if (fragRg.location == kCFNotFound)
139         return URL;
140
141     Vector<UInt8, URL_BYTES_BUFFER_LENGTH> urlBytes(URL_BYTES_BUFFER_LENGTH);
142     CFIndex numBytes = CFURLGetBytes((__bridge CFURLRef)URL, urlBytes.data(), urlBytes.size());
143     if (numBytes == -1) {
144         numBytes = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
145         urlBytes.grow(numBytes);
146         CFURLGetBytes((__bridge CFURLRef)URL, urlBytes.data(), numBytes);
147     }
148
149     CFURLRef result = CFURLCreateWithBytes(nullptr, urlBytes.data(), fragRg.location - 1, kCFStringEncodingUTF8, nullptr);
150     if (!result)
151         result = CFURLCreateWithBytes(nullptr, urlBytes.data(), fragRg.location - 1, kCFStringEncodingISOLatin1, nullptr);
152         
153     return result ? CFBridgingRelease(result) : URL;
154 }
155
156 static NSURL *URLByRemovingResourceSpecifier(NSURL *URL)
157 {
158     return URLByTruncatingOneCharacterBeforeComponent(URL, kCFURLComponentResourceSpecifier);
159 }
160
161 NSURL *URLWithData(NSData *data, NSURL *baseURL)
162 {
163     if (!data)
164         return nil;
165     
166     NSURL *result = nil;
167     size_t length = [data length];
168     if (length > 0) {
169         // work around <rdar://4470771>: CFURLCreateAbsoluteURLWithBytes(.., TRUE) doesn't remove non-path components.
170         baseURL = URLByRemovingResourceSpecifier(baseURL);
171         
172         const UInt8 *bytes = static_cast<const UInt8*>([data bytes]);
173         
174         // CFURLCreateAbsoluteURLWithBytes would complain to console if we passed a path to it.
175         if (bytes[0] == '/' && !baseURL)
176             return nil;
177         
178         // NOTE: We use UTF-8 here since this encoding is used when computing strings when returning URL components
179         // (e.g calls to NSURL -path). However, this function is not tolerant of illegal UTF-8 sequences, which
180         // could either be a malformed string or bytes in a different encoding, like shift-jis, so we fall back
181         // onto using ISO Latin 1 in those cases.
182         result = CFBridgingRelease(CFURLCreateAbsoluteURLWithBytes(nullptr, bytes, length, kCFStringEncodingUTF8, (__bridge CFURLRef)baseURL, YES));
183         if (!result)
184             result = CFBridgingRelease(CFURLCreateAbsoluteURLWithBytes(nullptr, bytes, length, kCFStringEncodingISOLatin1, (__bridge CFURLRef)baseURL, YES));
185     } else
186         result = [NSURL URLWithString:@""];
187
188     return result;
189 }
190 static NSData *dataWithUserTypedString(NSString *string)
191 {
192     NSData *userTypedData = [string dataUsingEncoding:NSUTF8StringEncoding];
193     ASSERT(userTypedData);
194     
195     const UInt8* inBytes = static_cast<const UInt8 *>([userTypedData bytes]);
196     int inLength = [userTypedData length];
197     if (!inLength)
198         return nil;
199     
200     char* outBytes = static_cast<char *>(malloc(inLength * 3)); // large enough to %-escape every character
201     char* p = outBytes;
202     int outLength = 0;
203     for (int i = 0; i < inLength; i++) {
204         UInt8 c = inBytes[i];
205         if (c <= 0x20 || c >= 0x7f) {
206             *p++ = '%';
207             *p++ = upperNibbleToASCIIHexDigit(c);
208             *p++ = lowerNibbleToASCIIHexDigit(c);
209             outLength += 3;
210         } else {
211             *p++ = c;
212             outLength++;
213         }
214     }
215     
216     return [NSData dataWithBytesNoCopy:outBytes length:outLength]; // adopts outBytes
217 }
218
219 NSURL *URLWithUserTypedString(NSString *string, NSURL *nsURL)
220 {
221     if (!string)
222         return nil;
223
224     auto mappedString = mapHostNames(stringByTrimmingWhitespace(string).get(), decodePercentEscapes);
225     if (!mappedString)
226         return nil;
227
228     // Let's check whether the URL is bogus.
229     URL url { URL { nsURL }, mappedString };
230     if (!url.createCFURL())
231         return nil;
232
233     // FIXME: https://bugs.webkit.org/show_bug.cgi?id=186057
234     // We should be able to use url.createCFURL instead of using directly CFURL parsing routines.
235     NSData *data = dataWithUserTypedString(mappedString);
236     if (!data)
237         return [NSURL URLWithString:@""];
238
239     return URLWithData(data, nsURL);
240 }
241
242 NSURL *URLWithUserTypedStringDeprecated(NSString *string, NSURL *URL)
243 {
244     if (!string)
245         return nil;
246
247     NSURL *result = URLWithUserTypedString(string, URL);
248     if (!result) {
249         NSData *resultData = dataWithUserTypedString(string);
250         if (!resultData)
251             return [NSURL URLWithString:@""];
252         result = URLWithData(resultData, URL);
253     }
254
255     return result;
256 }
257
258 static BOOL hasQuestionMarkOnlyQueryString(NSURL *URL)
259 {
260     CFRange rangeWithSeparators;
261     CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, kCFURLComponentQuery, &rangeWithSeparators);
262     if (rangeWithSeparators.location != kCFNotFound && rangeWithSeparators.length == 1)
263         return YES;
264
265     return NO;
266 }
267
268 NSData *dataForURLComponentType(NSURL *URL, CFURLComponentType componentType)
269 {
270     Vector<UInt8, URL_BYTES_BUFFER_LENGTH> allBytesBuffer(URL_BYTES_BUFFER_LENGTH);
271     CFIndex bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, allBytesBuffer.data(), allBytesBuffer.size());
272     if (bytesFilled == -1) {
273         CFIndex bytesToAllocate = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
274         allBytesBuffer.grow(bytesToAllocate);
275         bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, allBytesBuffer.data(), bytesToAllocate);
276     }
277     
278     const CFURLComponentType completeURL = (CFURLComponentType)-1;
279     CFRange range;
280     if (componentType != completeURL) {
281         range = CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, componentType, nullptr);
282         if (range.location == kCFNotFound)
283             return nil;
284     } else {
285         range.location = 0;
286         range.length = bytesFilled;
287     }
288     
289     NSData *componentData = [NSData dataWithBytes:allBytesBuffer.data() + range.location length:range.length]; 
290     
291     const unsigned char *bytes = static_cast<const unsigned char *>([componentData bytes]);
292     NSMutableData *resultData = [NSMutableData data];
293     // NOTE: add leading '?' to query strings non-zero length query strings.
294     // NOTE: retain question-mark only query strings.
295     if (componentType == kCFURLComponentQuery) {
296         if (range.length > 0 || hasQuestionMarkOnlyQueryString(URL))
297             [resultData appendBytes:"?" length:1];    
298     }
299     for (int i = 0; i < range.length; i++) {
300         unsigned char c = bytes[i];
301         if (c <= 0x20 || c >= 0x7f) {
302             char escaped[3];
303             escaped[0] = '%';
304             escaped[1] = upperNibbleToASCIIHexDigit(c);
305             escaped[2] = lowerNibbleToASCIIHexDigit(c);
306             [resultData appendBytes:escaped length:3];    
307         } else {
308             char b[1];
309             b[0] = c;
310             [resultData appendBytes:b length:1];    
311         }               
312     }
313     
314     return resultData;
315 }
316
317 static NSURL *URLByRemovingComponentAndSubsequentCharacter(NSURL *URL, CFURLComponentType component)
318 {
319     CFRange range = CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, component, 0);
320     if (range.location == kCFNotFound)
321         return URL;
322     
323     // Remove one subsequent character.
324     range.length++;
325
326     Vector<UInt8, URL_BYTES_BUFFER_LENGTH> buffer(URL_BYTES_BUFFER_LENGTH);
327     CFIndex numBytes = CFURLGetBytes((__bridge CFURLRef)URL, buffer.data(), buffer.size());
328     if (numBytes == -1) {
329         numBytes = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
330         buffer.grow(numBytes);
331         CFURLGetBytes((__bridge CFURLRef)URL, buffer.data(), numBytes);
332     }
333     UInt8* urlBytes = buffer.data();
334         
335     if (numBytes < range.location)
336         return URL;
337     if (numBytes < range.location + range.length)
338         range.length = numBytes - range.location;
339         
340     memmove(urlBytes + range.location, urlBytes + range.location + range.length, numBytes - range.location + range.length);
341     
342     CFURLRef result = CFURLCreateWithBytes(nullptr, urlBytes, numBytes - range.length, kCFStringEncodingUTF8, nullptr);
343     if (!result)
344         result = CFURLCreateWithBytes(nullptr, urlBytes, numBytes - range.length, kCFStringEncodingISOLatin1, nullptr);
345                 
346     return result ? CFBridgingRelease(result) : URL;
347 }
348
349 NSURL *URLByRemovingUserInfo(NSURL *URL)
350 {
351     return URLByRemovingComponentAndSubsequentCharacter(URL, kCFURLComponentUserInfo);
352 }
353
354 NSData *originalURLData(NSURL *URL)
355 {
356     UInt8 *buffer = (UInt8 *)malloc(URL_BYTES_BUFFER_LENGTH);
357     CFIndex bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, buffer, URL_BYTES_BUFFER_LENGTH);
358     if (bytesFilled == -1) {
359         CFIndex bytesToAllocate = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
360         buffer = (UInt8 *)realloc(buffer, bytesToAllocate);
361         bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, buffer, bytesToAllocate);
362         ASSERT(bytesFilled == bytesToAllocate);
363     }
364     
365     // buffer is adopted by the NSData
366     NSData *data = [NSData dataWithBytesNoCopy:buffer length:bytesFilled freeWhenDone:YES];
367     
368     NSURL *baseURL = (__bridge NSURL *)CFURLGetBaseURL((__bridge CFURLRef)URL);
369     if (baseURL)
370         return originalURLData(URLWithData(data, baseURL));
371     return data;
372 }
373
374 NSString *userVisibleString(NSURL *URL)
375 {
376     NSData *data = originalURLData(URL);
377     CString string(static_cast<const char*>([data bytes]), [data length]);
378     return userVisibleURL(string);
379 }
380
381 BOOL isUserVisibleURL(NSString *string)
382 {
383     BOOL valid = YES;
384     // get buffer
385     
386     char static_buffer[1024];
387     const char *p;
388     BOOL success = CFStringGetCString((__bridge CFStringRef)string, static_buffer, 1023, kCFStringEncodingUTF8);
389     p = success ? static_buffer : [string UTF8String];
390     
391     int length = strlen(p);
392     
393     // check for characters <= 0x20 or >=0x7f, %-escape sequences of %7f, and xn--, these
394     // are the things that will lead _web_userVisibleString to actually change things.
395     for (int i = 0; i < length; i++) {
396         unsigned char c = p[i];
397         // escape control characters, space, and delete
398         if (c <= 0x20 || c == 0x7f) {
399             valid = NO;
400             break;
401         } else if (c == '%' && (i + 1 < length && isASCIIHexDigit(p[i + 1])) && i + 2 < length && isASCIIHexDigit(p[i + 2])) {
402             auto u = toASCIIHexValue(p[i + 1], p[i + 2]);
403             if (u > 0x7f) {
404                 valid = NO;
405                 break;
406             }
407             i += 2;
408         } else {
409             // Check for "xn--" in an efficient, non-case-sensitive, way.
410             if (c == '-' && i >= 3 && (p[i - 3] | 0x20) == 'x' && (p[i - 2] | 0x20) == 'n' && p[i - 1] == '-') {
411                 valid = NO;
412                 break;
413             }
414         }
415     }
416     
417     return valid;
418 }
419
420 NSRange rangeOfURLScheme(NSString *string)
421 {
422     NSRange colon = [string rangeOfString:@":"];
423     if (colon.location != NSNotFound && colon.location > 0) {
424         NSRange scheme = {0, colon.location};
425         /*
426          This stuff is very expensive.  10-15 msec on a 2x1.2GHz.  If not cached it swamps
427          everything else when adding items to the autocomplete DB.  Makes me wonder if we
428          even need to enforce the character set here.
429          */
430         NSString *acceptableCharacters = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+.-";
431         static NeverDestroyed<RetainPtr<NSCharacterSet>> InverseSchemeCharacterSet([[NSCharacterSet characterSetWithCharactersInString:acceptableCharacters] invertedSet]);
432         NSRange illegals = [string rangeOfCharacterFromSet:InverseSchemeCharacterSet.get().get() options:0 range:scheme];
433         if (illegals.location == NSNotFound)
434             return scheme;
435     }
436     return NSMakeRange(NSNotFound, 0);
437 }
438
439 BOOL looksLikeAbsoluteURL(NSString *string)
440 {
441     // Trim whitespace because _web_URLWithString allows whitespace.
442     return rangeOfURLScheme(stringByTrimmingWhitespace(string).get()).location != NSNotFound;
443 }
444
445 } // namespace WebCore