mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-08-29 10:17:35 +00:00
the trailing while loop reuses the regex variable, which by then points at the angular brace pattern whenever stripAngular is YES. that pattern has two capture groups while the loop substitutes $1@im****$3, so NSRegularExpression raises NSInvalidArgumentException and the NS_HANDLER swallows it. give the @import pattern its own variable and use it in both the replacement and the loop. the replacement also used the template of the block above, which dropped the whole style element instead of masking the @import in place, so test_stringWithoutHTMLInjection failed. it passes again, and a case with stripAngular YES covers the loop.
1106 lines
38 KiB
Objective-C
1106 lines
38 KiB
Objective-C
/* NSString+Utilities.m - this file is part of SOGo
|
|
*
|
|
* Copyright (C) 2006-2022 Inverse inc.
|
|
*
|
|
* This file is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This file is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
|
|
#import <EOControl/EOQualifier.h>
|
|
|
|
#import <NGExtensions/NSDictionary+misc.h>
|
|
#import <NGExtensions/NSObject+Logs.h>
|
|
#import <NGExtensions/NGBase64Coding.h>
|
|
|
|
#import <NGMime/NGMimeHeaderFieldGenerator.h>
|
|
#import <SBJson/SBJsonParser.h>
|
|
|
|
#import "NSArray+Utilities.h"
|
|
#import "NSDictionary+URL.h"
|
|
|
|
#import "NSString+Utilities.h"
|
|
|
|
static NSMutableCharacterSet *urlNonEndingChars = nil;
|
|
static NSMutableCharacterSet *urlAfterEndingChars = nil;
|
|
static NSMutableCharacterSet *schemaStartChars = nil;
|
|
static NSMutableCharacterSet *emailStartChars = nil;
|
|
|
|
static NSMutableCharacterSet *jsonEscapingChars = NULL;
|
|
static NSString **cssEscapingStrings = NULL;
|
|
static unichar *cssEscapingCharacters = NULL;
|
|
static int cssEscapingCount;
|
|
|
|
@implementation NSString (SOGoURLExtension)
|
|
|
|
- (NSString *) composeURLWithAction: (NSString *) action
|
|
parameters: (NSDictionary *) urlParameters
|
|
andHash: (BOOL) useHash
|
|
{
|
|
NSMutableString *completeURL;
|
|
|
|
completeURL = [NSMutableString new];
|
|
[completeURL autorelease];
|
|
|
|
[completeURL appendString: [self urlWithoutParameters]];
|
|
if (![completeURL hasSuffix: @"/"])
|
|
[completeURL appendString: @"/"];
|
|
[completeURL appendString: action];
|
|
if (urlParameters)
|
|
[completeURL appendString: [urlParameters asURLParameters]];
|
|
if (useHash)
|
|
[completeURL appendString: @"#"];
|
|
|
|
return completeURL;
|
|
}
|
|
|
|
- (NSString *) hostlessURL
|
|
{
|
|
NSString *newURL;
|
|
NSRange hostR, locationR;
|
|
|
|
if ([self hasPrefix: @"/"])
|
|
{
|
|
newURL = [self copy];
|
|
[newURL autorelease];
|
|
}
|
|
else
|
|
{
|
|
hostR = [self rangeOfString: @"://"];
|
|
locationR = [[self substringFromIndex: (hostR.location + hostR.length)] rangeOfString: @"/"];
|
|
if(locationR.location != NSNotFound)
|
|
newURL = [self substringFromIndex: (hostR.location + hostR.length + locationR.location)];
|
|
else
|
|
newURL = @"";
|
|
}
|
|
|
|
return newURL;
|
|
}
|
|
|
|
- (NSString *) urlWithoutParameters;
|
|
{
|
|
NSRange r;
|
|
NSString *newUrl;
|
|
|
|
r = [self rangeOfString:@"?" options: NSBackwardsSearch];
|
|
if (r.length > 0)
|
|
newUrl = [self substringToIndex: NSMaxRange (r) - 1];
|
|
else
|
|
newUrl = self;
|
|
|
|
return newUrl;
|
|
}
|
|
|
|
- (NSRange) _rangeOfURLInRange: (NSRange) refRange
|
|
withPrefixChars: (NSCharacterSet *) startChars
|
|
{
|
|
int start, length;
|
|
NSRange workRange;
|
|
|
|
start = refRange.location;
|
|
if (start > 0)
|
|
start--; // Start with the character before the refRange
|
|
while (start > -1
|
|
&& [startChars characterIsMember:
|
|
[self characterAtIndex: start]])
|
|
start--;
|
|
start++;
|
|
|
|
length = [self length];
|
|
// In [UIxMailPartTextViewer flatContentAsString], we first escape HTML entities and then
|
|
// add URLs. Therefore, the brackets (inequality signs <>) have been encoded at this point.
|
|
if (length > (start + 4)
|
|
&& [[self substringWithRange: NSMakeRange (start, 4)] compare: @"<"] == NSOrderedSame)
|
|
start += 4;
|
|
|
|
length -= start;
|
|
workRange = [self rangeOfCharacterFromSet: urlAfterEndingChars
|
|
options: NSLiteralSearch range: NSMakeRange (start, length)];
|
|
if (workRange.location != NSNotFound)
|
|
length = workRange.location - start;
|
|
while
|
|
(length > 0
|
|
&& [urlNonEndingChars characterIsMember:
|
|
[self characterAtIndex: (start + length - 1)]])
|
|
length--;
|
|
|
|
// Remove trailing ">"
|
|
if (([self length] >= start + length + 1)
|
|
&& [[self substringWithRange: NSMakeRange (start, length + 1)] hasSuffix: @">"])
|
|
length -= 3;
|
|
|
|
return NSMakeRange (start, length);
|
|
}
|
|
|
|
- (void) _handleURLs: (NSMutableString *) selfCopy
|
|
textToMatch: (NSString *) match
|
|
urlPrefixChars: (NSCharacterSet *) startChars
|
|
prefix: (NSString *) prefix
|
|
inRanges: (NSMutableArray *) ranges
|
|
{
|
|
NSEnumerator *enumRanges;
|
|
NSMutableArray *newRanges;
|
|
NSRange matchRange, currentUrlRange, rest;
|
|
NSRange *rangePtr;
|
|
NSString *urlText, *newUrlText;
|
|
unsigned int length, matchLength, offset;
|
|
|
|
newRanges = [NSMutableArray array];
|
|
matchLength = [match length];
|
|
|
|
matchRange = [selfCopy rangeOfString: match];
|
|
while (matchRange.location != NSNotFound)
|
|
{
|
|
currentUrlRange = [selfCopy _rangeOfURLInRange: matchRange
|
|
withPrefixChars: startChars];
|
|
if (![ranges hasRangeIntersection: currentUrlRange])
|
|
if (currentUrlRange.length > matchLength)
|
|
[newRanges addNonNSObject: ¤tUrlRange
|
|
withSize: sizeof (NSRange)
|
|
copy: YES];
|
|
rest.location = NSMaxRange (currentUrlRange);
|
|
length = [selfCopy length];
|
|
rest.length = length - rest.location;
|
|
matchRange = [selfCopy rangeOfString: match
|
|
options: 0 range: rest];
|
|
}
|
|
|
|
// Make the substitutions, keep track of the new offset
|
|
offset = 0;
|
|
enumRanges = [newRanges objectEnumerator];
|
|
while ((rangePtr = [[enumRanges nextObject] pointerValue]))
|
|
{
|
|
rangePtr->location += offset;
|
|
urlText = [selfCopy substringFromRange: *rangePtr];
|
|
//If we try to detect mail, check if the first char is @, meaning this is not an email
|
|
if([prefix isEqualToString: @"mailto:"] && [urlText characterAtIndex: 0] == '@')
|
|
continue;
|
|
newUrlText = [NSString stringWithFormat: @"<a rel=\"noopener\" href=\"%@%@\">%@</a>",
|
|
([urlText hasPrefix: prefix]? @"" : prefix),
|
|
urlText, urlText];
|
|
[selfCopy replaceCharactersInRange: *rangePtr
|
|
withString: newUrlText];
|
|
offset += ([newUrlText length] - [urlText length]);
|
|
|
|
// Add range for further substitutions
|
|
currentUrlRange = NSMakeRange (rangePtr->location, [newUrlText length]);
|
|
[ranges addNonNSObject: ¤tUrlRange
|
|
withSize: sizeof (NSRange)
|
|
copy: YES];
|
|
}
|
|
[newRanges freeNonNSObjects];
|
|
}
|
|
|
|
- (NSString *) stringByDetectingURLs
|
|
{
|
|
NSMutableString *selfCopy;
|
|
NSMutableArray *ranges;
|
|
|
|
if (!urlNonEndingChars)
|
|
{
|
|
urlNonEndingChars = [NSMutableCharacterSet new];
|
|
[urlNonEndingChars addCharactersInString: @"=,.:;&()>\t \r\n"];
|
|
}
|
|
if (!urlAfterEndingChars)
|
|
{
|
|
urlAfterEndingChars = [NSMutableCharacterSet new];
|
|
[urlAfterEndingChars addCharactersInString: @"()[]\t \r\n"];
|
|
}
|
|
if (!schemaStartChars)
|
|
{
|
|
schemaStartChars = [NSMutableCharacterSet new];
|
|
[schemaStartChars addCharactersInString: @"abcdefghijklmnopqrstuvwxyz"
|
|
@"ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
|
}
|
|
if (!emailStartChars)
|
|
{
|
|
emailStartChars = [NSMutableCharacterSet new];
|
|
[emailStartChars addCharactersInString: @"abcdefghijklmnopqrstuvwxyz"
|
|
@"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
@"01234567890"
|
|
@"!#$%&'*+-/=?^`{|}~."];
|
|
}
|
|
|
|
ranges = [NSMutableArray array];
|
|
selfCopy = [NSMutableString stringWithString: self];
|
|
[self _handleURLs: selfCopy
|
|
textToMatch: @"://"
|
|
urlPrefixChars: schemaStartChars
|
|
prefix: @""
|
|
inRanges: ranges];
|
|
[self _handleURLs: selfCopy
|
|
textToMatch: @"@"
|
|
urlPrefixChars: emailStartChars
|
|
prefix: @"mailto:"
|
|
inRanges: ranges];
|
|
[ranges freeNonNSObjects];
|
|
|
|
return selfCopy;
|
|
}
|
|
|
|
- (NSString *) asSafeJSString
|
|
{
|
|
NSRange esc;
|
|
|
|
if (!jsonEscapingChars)
|
|
{
|
|
jsonEscapingChars = [[NSMutableCharacterSet characterSetWithRange: NSMakeRange(0,32)] retain];
|
|
[jsonEscapingChars addCharactersInString: @"\"\\"];
|
|
}
|
|
|
|
esc = [self rangeOfCharacterFromSet: jsonEscapingChars];
|
|
if (!esc.length)
|
|
{
|
|
// No special chars
|
|
return self;
|
|
}
|
|
else
|
|
{
|
|
NSMutableString *representation;
|
|
NSUInteger length, i;
|
|
unichar uc;
|
|
|
|
representation = [NSMutableString string];
|
|
|
|
length = [self length];
|
|
for (i = 0; i < length; i++)
|
|
{
|
|
uc = [self characterAtIndex:i];
|
|
switch (uc)
|
|
{
|
|
case '"': [representation appendString:@"\\\""]; break;
|
|
case '\\': [representation appendString:@"\\\\"]; break;
|
|
case '\t': [representation appendString:@"\\t"]; break;
|
|
case '\n': [representation appendString:@"\\n"]; break;
|
|
case '\r': [representation appendString:@"\\r"]; break;
|
|
case '\b': [representation appendString:@"\\b"]; break;
|
|
case '\f': [representation appendString:@"\\f"]; break;
|
|
default:
|
|
if (uc < 0x20)
|
|
[representation appendFormat:@"\\u%04x", uc];
|
|
else
|
|
[representation appendFormat: @"%C", uc];
|
|
break;
|
|
}
|
|
}
|
|
return representation;
|
|
}
|
|
}
|
|
|
|
- (NSString *) doubleQuotedString
|
|
{
|
|
return [NSString stringWithFormat: @"\"%@\"", [self asSafeJSString]];
|
|
}
|
|
|
|
//
|
|
// See http://www.hackcraft.net/xmlUnicode/
|
|
//
|
|
// XML1.0 and XML1.1 allow different characters in different contexts,
|
|
// but for the most part I will only describe the XML1.0 usage, XML1.1
|
|
// usage is analogous. The first definition that is relevant here is
|
|
// that of a Char: [2] Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] |
|
|
// [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character,
|
|
// excluding the surrogate blocks, FFFE, and FFFF. */
|
|
//
|
|
// This defines which characters can be used in an XML1.0 document. It
|
|
// is clearly very liberal, banning only some of the control
|
|
// characters and the noncharacters U+FFFE and U+FFFF. Indeed it is
|
|
// somewhat too liberal in my view since it allows other noncharacters
|
|
// (the code points from U+FDD0 to U+FDEF inclusive and the last 2
|
|
// code points in each plane, from U+1FFFE & U+1FFFF through to
|
|
// U+10FFFE & U+10FFFF, are noncharacters) but the production quoted
|
|
// above allows them.
|
|
//
|
|
- (NSString *) safeString
|
|
{
|
|
NSData *data;
|
|
NSString *s;
|
|
|
|
const wchar_t *buf;
|
|
wchar_t *start, c;
|
|
int len, i, j;
|
|
|
|
data = [self dataUsingEncoding: NSUTF32LittleEndianStringEncoding];
|
|
len = [data length];
|
|
buf = [data bytes];
|
|
start = (wchar_t *)calloc(len, sizeof(wchar_t));
|
|
|
|
for (i = 0, j = 0; i < len/4; i++)
|
|
{
|
|
c = buf[i];
|
|
|
|
if (c == 0x0 ||
|
|
c == 0x9 ||
|
|
c == 0xA ||
|
|
(c >= 0x20 && c < 0x300) || // Skip combining diacritical marks
|
|
(c > 0x36F && c < 0xD7FF) ||
|
|
(c >= 0xE000 && c < 0xFE00) || // Skip variation selectors
|
|
(c > 0xFE0F && c <= 0xFFFD) ||
|
|
(c >= (wchar_t)0x10000 && c <= (wchar_t)0x10FFFF))
|
|
{
|
|
start[j] = c;
|
|
j++;
|
|
}
|
|
}
|
|
|
|
s = [[NSString alloc] initWithBytesNoCopy: start length: j*sizeof(wchar_t) encoding: NSUTF32LittleEndianStringEncoding freeWhenDone: YES];
|
|
|
|
return AUTORELEASE(s);
|
|
}
|
|
|
|
- (NSString *) safeStringByEscapingXMLString
|
|
{
|
|
return [self safeStringByEscapingXMLString: NO];
|
|
}
|
|
//
|
|
// This is a copy from NSString+XMLEscaping.m from SOPE.
|
|
// The difference here is that we use wchar_t instead of unichar.
|
|
// This is needed to get the rigth numeric character reference.
|
|
// e.g. SMILING FACE WITH OPEN MOUTH
|
|
// ok: wchar_t -> 😃 wrong: unichar -> � �
|
|
//
|
|
// We avoid naming it like the one in SOPE since if the ActiveSync
|
|
// bundle is loaded, it'll overwrite the one provided by SOPE.
|
|
//
|
|
- (NSString *) safeStringByEscapingXMLString: (BOOL) encodeCR
|
|
{
|
|
NSData *data;
|
|
|
|
register unsigned i, len, j;
|
|
register wchar_t *buf;
|
|
const wchar_t *chars;
|
|
unsigned escapeCount;
|
|
|
|
if ([self length] == 0) return @"";
|
|
|
|
data = [self dataUsingEncoding: NSUTF32LittleEndianStringEncoding];
|
|
chars = [data bytes];
|
|
len = [data length]/4;
|
|
|
|
/* check for characters to escape ... */
|
|
for (i = 0, escapeCount = 0; i < len; i++)
|
|
{
|
|
switch (chars[i]) {
|
|
case '&': case '"': case '<': case '>': case '\r':
|
|
escapeCount++;
|
|
break;
|
|
default:
|
|
if (chars[i] < 0x20 || chars[i] > 127)
|
|
escapeCount++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* nothing to escape ... */
|
|
if (escapeCount == 0 )
|
|
return [[self copy] autorelease];
|
|
|
|
buf = calloc((len + 5) + (escapeCount * 16), sizeof(wchar_t));
|
|
for (i = 0, j = 0; i < len; i++)
|
|
{
|
|
switch (chars[i])
|
|
{
|
|
/* escape special chars */
|
|
case '&':
|
|
buf[j] = '&'; j++; buf[j] = 'a'; j++; buf[j] = 'm'; j++;
|
|
buf[j] = 'p'; j++; buf[j] = ';'; j++;
|
|
break;
|
|
case '"':
|
|
buf[j] = '&'; j++; buf[j] = 'q'; j++; buf[j] = 'u'; j++;
|
|
buf[j] = 'o'; j++; buf[j] = 't'; j++; buf[j] = ';'; j++;
|
|
break;
|
|
case '<':
|
|
buf[j] = '&'; j++; buf[j] = 'l'; j++; buf[j] = 't'; j++;
|
|
buf[j] = ';'; j++;
|
|
break;
|
|
case '>':
|
|
buf[j] = '&'; j++; buf[j] = 'g'; j++; buf[j] = 't'; j++;
|
|
buf[j] = ';'; j++;
|
|
break;
|
|
case '\r':
|
|
if (encodeCR) // falls back to default if we don't encode
|
|
{
|
|
buf[j] = '&'; j++; buf[j] = '#'; j++; buf[j] = '1'; j++;
|
|
buf[j] = '3'; j++; buf[j] = ';'; j++;
|
|
break;
|
|
}
|
|
default:
|
|
/* escape big chars */
|
|
if (chars[i] > 127)
|
|
{
|
|
unsigned char nbuf[32];
|
|
unsigned int k;
|
|
|
|
sprintf((char *)nbuf, "&#%i;", (int)chars[i]);
|
|
for (k = 0; nbuf[k] != '\0'; k++)
|
|
{
|
|
buf[j] = nbuf[k];
|
|
j++;
|
|
}
|
|
}
|
|
else if (chars[i] == 0x9 || chars[i] == 0xA || chars[i] == 0xD || chars[i] >= 0x20)
|
|
{ // ignore any unsupported control character
|
|
/* nothing to escape */
|
|
buf[j] = chars[i];
|
|
j++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
self = [[NSString alloc] initWithBytesNoCopy: buf
|
|
length: (j*sizeof(wchar_t))
|
|
encoding: NSUTF32LittleEndianStringEncoding
|
|
freeWhenDone: YES];
|
|
return [self autorelease];
|
|
}
|
|
|
|
- (NSString *) jsonRepresentation
|
|
{
|
|
NSString *cleanedString;
|
|
|
|
// Escape double quotes and remove control characters
|
|
cleanedString = [[self safeString] doubleQuotedString];
|
|
return cleanedString;
|
|
}
|
|
|
|
- (void) _setupCSSEscaping
|
|
{
|
|
NSArray *strings, *characters;
|
|
int count;
|
|
|
|
strings = [NSArray arrayWithObjects: @"_U_", @"_D_", @"_H_", @"_A_", @"_S_",
|
|
@"_C_", @"_SC_",
|
|
@"_CO_", @"_SP_", @"_SQ_", @"_DQ_",
|
|
@"_LP_", @"_RP_", @"_LS_", @"_RS_", @"_LC_", @"_RC_",
|
|
@"_AM_", @"_P_", @"_DS_", nil];
|
|
[strings retain];
|
|
cssEscapingStrings = [strings asPointersOfObjects];
|
|
|
|
characters = [NSArray arrayWithObjects: @"_", @".", @"#", @"@", @"*",
|
|
@":", @";",
|
|
@",", @" ", @"'", @"\"",
|
|
@"(", @")", @"[", @"]", @"{", @"}",
|
|
@"&", @"+", @"$", nil];
|
|
cssEscapingCount = [strings count];
|
|
cssEscapingCharacters = NSZoneMalloc (NULL,
|
|
(cssEscapingCount + 1)
|
|
* sizeof (unichar));
|
|
for (count = 0; count < cssEscapingCount; count++)
|
|
*(cssEscapingCharacters + count) = [[characters objectAtIndex: count] characterAtIndex: 0];
|
|
*(cssEscapingCharacters + cssEscapingCount) = 0;
|
|
}
|
|
|
|
- (int) _cssCharacterIndex: (unichar) character
|
|
{
|
|
int idx, count;
|
|
|
|
idx = -1;
|
|
for (count = 0; idx == -1 && count < cssEscapingCount; count++)
|
|
if (*(cssEscapingCharacters + count) == character)
|
|
idx = count;
|
|
|
|
return idx;
|
|
}
|
|
|
|
- (NSString *) asCSSIdentifier
|
|
{
|
|
NSCharacterSet *numericSet;
|
|
NSMutableString *cssIdentifier;
|
|
unichar currentChar;
|
|
int count, max, idx;
|
|
|
|
if (!cssEscapingStrings)
|
|
[self _setupCSSEscaping];
|
|
|
|
cssIdentifier = [NSMutableString string];
|
|
numericSet = [NSCharacterSet decimalDigitCharacterSet];
|
|
max = [self length];
|
|
|
|
if (max > 0)
|
|
{
|
|
if ([numericSet characterIsMember: [self characterAtIndex: 0]])
|
|
// A CSS identifier can't start with a digit; we add an underscore
|
|
[cssIdentifier appendString: @"_"];
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
currentChar = [self characterAtIndex: count];
|
|
idx = [self _cssCharacterIndex: currentChar];
|
|
if (idx > -1)
|
|
[cssIdentifier appendString: cssEscapingStrings[idx]];
|
|
else
|
|
[cssIdentifier appendFormat: @"%C", currentChar];
|
|
}
|
|
}
|
|
|
|
return cssIdentifier;
|
|
}
|
|
|
|
- (int) _cssStringIndex: (NSString *) string
|
|
{
|
|
int idx, count;
|
|
|
|
idx = -1;
|
|
for (count = 0; idx == -1 && count < cssEscapingCount; count++)
|
|
if ([string hasPrefix: *(cssEscapingStrings + count)])
|
|
idx = count;
|
|
|
|
return idx;
|
|
}
|
|
|
|
- (NSString *) fromCSSIdentifier
|
|
{
|
|
NSCharacterSet *numericSet;
|
|
NSMutableString *newString;
|
|
NSString *currentString;
|
|
int count, length, max, idx;
|
|
unichar currentChar;
|
|
|
|
if (!cssEscapingStrings)
|
|
[self _setupCSSEscaping];
|
|
|
|
numericSet = [NSCharacterSet decimalDigitCharacterSet];
|
|
newString = [NSMutableString string];
|
|
max = [self length];
|
|
count = 0;
|
|
|
|
if (max > 0
|
|
&& [self characterAtIndex: 0] == '_'
|
|
&& [numericSet characterIsMember: [self characterAtIndex: 1]])
|
|
{
|
|
/* If the identifier starts with an underscore followed by a digit,
|
|
we remove the underscore */
|
|
count = 1;
|
|
}
|
|
|
|
for (; count < max - 2; count++)
|
|
{
|
|
currentChar = [self characterAtIndex: count];
|
|
if (currentChar == '_')
|
|
{
|
|
/* The difficulty here is that most escaping strings are 3 chars
|
|
long except one. Therefore we must juggle a little bit with the
|
|
lengths in order to avoid an overflow exception. */
|
|
length = 4;
|
|
if (count + length > max)
|
|
length = max - count;
|
|
currentString = [self substringFromRange: NSMakeRange (count, length)];
|
|
idx = [self _cssStringIndex: currentString];
|
|
if (idx > -1)
|
|
{
|
|
[newString appendFormat: @"%C", cssEscapingCharacters[idx]];
|
|
count += [cssEscapingStrings[idx] length] - 1;
|
|
}
|
|
else
|
|
[newString appendFormat: @"%C", currentChar];
|
|
}
|
|
else
|
|
[newString appendFormat: @"%C", currentChar];
|
|
}
|
|
currentString = [self substringFromRange: NSMakeRange (count, max - count)];
|
|
[newString appendString: currentString];
|
|
|
|
return newString;
|
|
}
|
|
|
|
|
|
- (NSString *) mailDomain
|
|
{
|
|
NSArray *mailSeparated;
|
|
|
|
mailSeparated = [self componentsSeparatedByString: @"@"];
|
|
if([mailSeparated count] == 2)
|
|
return [mailSeparated objectAtIndex: 1];
|
|
[self logWithFormat: @"Error while extracting domain from : %@", self];
|
|
return nil;
|
|
}
|
|
|
|
- (NSString *) pureEMailAddress
|
|
{
|
|
NSString *pureAddress;
|
|
NSRange delimiter;
|
|
|
|
delimiter = [self rangeOfString: @"<"];
|
|
if (delimiter.location == NSNotFound)
|
|
pureAddress = self;
|
|
else
|
|
{
|
|
pureAddress = [self substringFromIndex: NSMaxRange (delimiter)];
|
|
delimiter = [pureAddress rangeOfString: @">"];
|
|
if (delimiter.location != NSNotFound)
|
|
pureAddress = [pureAddress substringToIndex: delimiter.location];
|
|
}
|
|
|
|
return pureAddress;
|
|
}
|
|
|
|
- (NSString *) asQPSubjectString: (NSString *) encoding
|
|
{
|
|
return [NGMimeHeaderFieldGenerator encodeQuotedPrintableText: self];
|
|
}
|
|
|
|
- (BOOL) caseInsensitiveMatches: (NSString *) match
|
|
{
|
|
EOQualifier *sq;
|
|
NSString *format;
|
|
|
|
format = [NSString stringWithFormat:
|
|
@"(description isCaseInsensitiveLike: '%@')",
|
|
match];
|
|
sq = [EOQualifier qualifierWithQualifierFormat: format];
|
|
|
|
return [(id<EOQualifierEvaluation>)sq evaluateWithObject: self];
|
|
}
|
|
|
|
//
|
|
// To decompose the DN extracted from a SSL certificate
|
|
// using the XN_FLAG_SEP_MULTILINE flag.
|
|
//
|
|
- (NSArray *) componentsFromMultilineDN
|
|
{
|
|
NSArray *pair;
|
|
NSEnumerator *componentsEnum, *rdnComponentsEnum;
|
|
NSMutableArray *components;
|
|
NSString *component, *pairString;
|
|
|
|
components = [NSMutableArray array];
|
|
componentsEnum = [[self componentsSeparatedByString: @"\n"] objectEnumerator];
|
|
while (( component = [componentsEnum nextObject] ))
|
|
{
|
|
rdnComponentsEnum = [[component componentsSeparatedByString: @" + "] objectEnumerator];
|
|
while (( pairString = [rdnComponentsEnum nextObject] ))
|
|
{
|
|
pair = [pairString componentsSeparatedByString: @"="];
|
|
if ([pair count] == 2)
|
|
[components addObject: [NSArray arrayWithObjects:
|
|
[pair objectAtIndex: 0],
|
|
[pair objectAtIndex: 1], nil]];
|
|
}
|
|
}
|
|
|
|
return components;
|
|
}
|
|
|
|
#if LIB_FOUNDATION_LIBRARY
|
|
- (BOOL) boolValue
|
|
{
|
|
return !([self isEqualToString: @"0"]
|
|
|| [self isEqualToString: @"NO"]);
|
|
}
|
|
#endif
|
|
|
|
- (int) timeValue
|
|
{
|
|
int time;
|
|
NSInteger i;
|
|
|
|
if ([self length] > 0)
|
|
{
|
|
i = [self rangeOfString: @":"].location;
|
|
if (i == NSNotFound)
|
|
time = [self intValue];
|
|
else
|
|
time = [[self substringToIndex: i] intValue];
|
|
}
|
|
else
|
|
time = -1;
|
|
|
|
return time;
|
|
}
|
|
|
|
- (BOOL) isJSONString
|
|
{
|
|
NSDictionary *jsonData;
|
|
|
|
#warning this method is a quick and dirty way of detecting the file-format
|
|
jsonData = [self objectFromJSONString];
|
|
|
|
return (jsonData != nil);
|
|
}
|
|
|
|
- (id) objectFromJSONString
|
|
{
|
|
SBJsonParser *parser;
|
|
NSArray *object;
|
|
NSError *error;
|
|
NSString *unescaped, *json;
|
|
|
|
object = nil;
|
|
|
|
if ([self length] > 0)
|
|
{
|
|
parser = [SBJsonParser new];
|
|
[parser autorelease];
|
|
error = nil;
|
|
|
|
/* Parse it this way so we can parse simple values, like "null" */
|
|
json = [NSString stringWithFormat: @"[%@]", self];
|
|
object = [parser objectWithString: json
|
|
error: &error];
|
|
if (error)
|
|
{
|
|
[self errorWithFormat: @"json parser: %@,"
|
|
@" attempting once more after unescaping...", error];
|
|
unescaped = [json stringByReplacingString: @"\\\\"
|
|
withString: @"\\"];
|
|
object = [parser objectWithString: unescaped
|
|
error: &error];
|
|
if (error)
|
|
{
|
|
[self errorWithFormat: @"total failure. Original string is: %@", self];
|
|
object = nil;
|
|
}
|
|
else
|
|
[self logWithFormat: @"initial object deserialized successfully!"];
|
|
}
|
|
}
|
|
|
|
return [object objectAtIndex: 0];
|
|
}
|
|
|
|
- (NSString *) asSafeSQLString
|
|
{
|
|
return [[self stringByReplacingString: @"\\" withString: @"\\\\"]
|
|
stringByReplacingString: @"'" withString: @"\\'"];
|
|
}
|
|
|
|
- (NSString *) asSafeSQLLikeString
|
|
{
|
|
return [[self asSafeSQLString] stringByReplacingString: @"\%" withString: @"\\%"];
|
|
}
|
|
|
|
- (NSUInteger) countOccurrencesOfString: (NSString *) substring
|
|
{
|
|
NSRange matchRange, substrRange;
|
|
BOOL done = NO;
|
|
NSUInteger selfLen, substrLen, count = 0;
|
|
|
|
selfLen = [self length];
|
|
substrLen = [substring length];
|
|
|
|
matchRange = NSMakeRange (0, selfLen);
|
|
while (!done && matchRange.length > 0)
|
|
{
|
|
substrRange = [self rangeOfString: substring options: 0 range: matchRange];
|
|
if (substrRange.location == NSNotFound)
|
|
done = YES;
|
|
else
|
|
{
|
|
count++;
|
|
matchRange.location = substrRange.location + 1;
|
|
if (matchRange.location + substrLen > selfLen)
|
|
done = YES;
|
|
else
|
|
matchRange.length = selfLen - matchRange.location;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
- (NSString *) stringByReplacingPrefix: (NSString *) oldPrefix
|
|
withPrefix: (NSString *) newPrefix
|
|
{
|
|
NSUInteger oldPrefixLength;
|
|
NSString *newString;
|
|
|
|
if (![self hasPrefix: oldPrefix])
|
|
[NSException raise: NSInvalidArgumentException
|
|
format: @"string does not have the specified prefix"];
|
|
|
|
oldPrefixLength = [oldPrefix length];
|
|
newString = [NSString stringWithFormat: @"%@%@",
|
|
newPrefix,
|
|
[self substringFromIndex: oldPrefixLength]];
|
|
|
|
return newString;
|
|
}
|
|
|
|
- (NSString *) encryptWithKey: (NSString *) theKey
|
|
{
|
|
NSMutableData *encryptedPassword;
|
|
NSMutableString *key;
|
|
NSString *result;
|
|
NSUInteger i, passLength, theKeyLength, keyLength;
|
|
unichar p, k, e;
|
|
|
|
if ([theKey length] > 0)
|
|
{
|
|
// The length of the key must be greater (or equal) than
|
|
// the length of the password
|
|
key = [NSMutableString string];
|
|
keyLength = 0;
|
|
|
|
passLength = [self length];
|
|
theKeyLength = [theKey length];
|
|
while (keyLength < passLength)
|
|
{
|
|
[key appendString: theKey];
|
|
keyLength += theKeyLength;
|
|
}
|
|
|
|
encryptedPassword = [NSMutableData data];
|
|
for (i = 0; i < passLength; i++)
|
|
{
|
|
p = [self characterAtIndex: i];
|
|
k = [key characterAtIndex: i];
|
|
e = p ^ k;
|
|
[encryptedPassword appendBytes: (void *)&e length: 2];
|
|
}
|
|
|
|
result = [encryptedPassword stringByEncodingBase64];
|
|
}
|
|
else
|
|
result = nil;
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSString *) decryptWithKey: (NSString *) theKey
|
|
{
|
|
NSMutableString *result;
|
|
NSMutableString *key;
|
|
NSData *decoded;
|
|
unichar *decryptedPassword;
|
|
NSUInteger i, theKeyLength, keyLength, decodedLength;
|
|
unichar p, k;
|
|
|
|
if ([theKey length] > 0)
|
|
{
|
|
decoded = [self dataByDecodingBase64];
|
|
decryptedPassword = (unichar *)[decoded bytes];
|
|
|
|
// The length of the key must be greater (or equal) than
|
|
// the length of the password
|
|
key = [NSMutableString string];
|
|
keyLength = 0;
|
|
decodedLength = ([decoded length] / 2); /* 1 unichar = 2 bytes/char */
|
|
theKeyLength = [theKey length];
|
|
|
|
while (keyLength < decodedLength)
|
|
{
|
|
[key appendString: theKey];
|
|
keyLength += theKeyLength;
|
|
}
|
|
|
|
result = [NSMutableString string];
|
|
for (i = 0; i < decodedLength; i++)
|
|
{
|
|
k = [key characterAtIndex: i];
|
|
p = decryptedPassword[i] ^ k;
|
|
[result appendFormat: @"%C", p];
|
|
}
|
|
}
|
|
else
|
|
result = nil;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Remove all HTML tags except for <a> </a>
|
|
* @return A clean string
|
|
*/
|
|
- (NSString *)removeHTMLTagsExceptAnchorTags {
|
|
NSError *error;
|
|
NSRegularExpression *regex;
|
|
NSString *stringWithoutHTML;
|
|
|
|
error = nil;
|
|
|
|
regex = [NSRegularExpression regularExpressionWithPattern: @"<(?!a|\\/a\\b)[^>]*>" options: NSRegularExpressionCaseInsensitive error: &error];
|
|
stringWithoutHTML = [regex stringByReplacingMatchesInString: self options: 0 range: NSMakeRange(0, [self length]) withTemplate:@""];
|
|
|
|
if (error) {
|
|
[self logWithFormat: @"Error while removing tags : %@", [error localizedDescription]];
|
|
return self;
|
|
}
|
|
|
|
return stringWithoutHTML;
|
|
}
|
|
|
|
/**
|
|
* Get the safe string avoiding HTML injection
|
|
* @param stripHTMLCode Remove all HTML code from content
|
|
* @return A safe string
|
|
*/
|
|
- (NSString *) stringWithoutHTMLInjection: (BOOL)stripHTMLCode stripAngular: (BOOL)stripAngular
|
|
{
|
|
NSString *result, *text, *newResult;
|
|
NSScanner *theScanner;
|
|
NSError *error;
|
|
NSUInteger numberOfMatches;
|
|
NSRegularExpression *regex, *importRegex;
|
|
|
|
text = nil;
|
|
error = nil;
|
|
result = [NSString stringWithString: self];
|
|
regex = nil;
|
|
importRegex = nil;
|
|
|
|
NS_DURING
|
|
{
|
|
if (stripHTMLCode) {
|
|
// Author : https://www.codercrunch.com/question/1251681838/how-remove-html-tags-string-ios
|
|
theScanner = [NSScanner scannerWithString: result];
|
|
while ([theScanner isAtEnd] == NO) {
|
|
// find start of tag
|
|
[theScanner scanUpToString: @"<" intoString: NULL];
|
|
// find end of tag
|
|
[theScanner scanUpToString: @">" intoString: &text];
|
|
|
|
// Check that text is not <xxx@xx.net
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"^<[a-zA-Z.\\-_]+@[a-zA-Z.\\-_]+.[a-zA-Z]+$"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
numberOfMatches = 0;
|
|
|
|
if (text && !error) {
|
|
numberOfMatches = [regex numberOfMatchesInString: text
|
|
options:0
|
|
range:NSMakeRange(0, [text length])];
|
|
if (0 == numberOfMatches) {
|
|
// replace the found tag with a space
|
|
//(you can filter multi-spaces out later if you wish)
|
|
newResult = [result stringByReplacingOccurrencesOfString:
|
|
[NSString stringWithFormat: @"%@>", text]
|
|
withString: @" "];
|
|
result = [NSString stringWithString: newResult];
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Clean XSS
|
|
// Examples of injection : https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html#xss-locator-polygot
|
|
|
|
// Remove javascript:
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"j[\\s\\u200B	

\\\\0]*a[\\s\\u200B	

\\\\0]*v[\\s\\u200B	

\\\\0]*a[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*:"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove vbscript:
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"v[\\s\\u200B	

\\\\0]*b[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*:"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove livescript:
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"l[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*v[\\s\\u200B	

\\\\0]*e[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*:"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove <script
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"<[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"<scr***"];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove </script
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"<[\\s\\u200B	

\\\\0]*/[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"</scr***"];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove <iframe
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"<[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*f[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*a[\\s\\u200B	

\\\\0]*m[\\s\\u200B	

\\\\0]*e"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"<ifr***"];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove <form
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"<[\\s\\u200B	

\\\\0]*f[\\s\\u200B	

\\\\0]*o[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*m"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"<for*"];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove </form
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"<[\\s\\u200B	

\\\\0]*/[\\s\\u200B	

\\\\0]*f[\\s\\u200B	

\\\\0]*o[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*m"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"</for*"];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove onload
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"onload="
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onl***="];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove onmouseover
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"onmouseover="
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onmouseo***="];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove onrepeat
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"onrepeat="
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onrep***="];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove onerror
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"onerror="
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onerr***="];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
// Remove @import css (in style tags)
|
|
importRegex = [NSRegularExpression regularExpressionWithPattern:@"(<[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*y[\\s\\u200B	

\\\\0]*l[\\s\\u200B	

\\\\0]*e.*)([\\s\\u200B	

\\\\0]*@[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*m[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*o[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*t)(.*<[\\s\\u200B	

\\\\0]*\\/[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*y[\\s\\u200B	

\\\\0]*l[\\s\\u200B	

\\\\0]*e[\\s\\u200B	

\\\\0]*>)"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [importRegex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"$1@im****$3"];
|
|
result = [NSString stringWithString: newResult];
|
|
|
|
|
|
if(stripAngular) {
|
|
// Remove {{ and }} as they are interprated by angularJS with no way of escaping
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\{\\{)|({{)"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"{\\\\{"];
|
|
result = [NSString stringWithString: newResult];
|
|
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\}\\})|(}})"
|
|
options: NSRegularExpressionCaseInsensitive error:&error];
|
|
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"}/}"];
|
|
result = [NSString stringWithString: newResult];
|
|
}
|
|
|
|
newResult = result;
|
|
while([importRegex numberOfMatchesInString:newResult options:0 range:NSMakeRange(0, [newResult length])] > 0) {
|
|
newResult = [importRegex stringByReplacingMatchesInString:newResult options:0 range:NSMakeRange(0, [newResult length]) withTemplate:@"$1@im****$3"];
|
|
}
|
|
result = [NSString stringWithString: newResult];
|
|
}
|
|
}
|
|
NS_HANDLER
|
|
{
|
|
[self logWithFormat: @"Error while stripping HTML injection : %@", [localException name]];
|
|
}
|
|
NS_ENDHANDLER;
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSString *) cleanInvalidHTMLTags {
|
|
// Clean HTML invalid tags as reported in https://bugs.sogo.nu/view.php?id=5755
|
|
NSString *s;
|
|
|
|
s = [NSString stringWithString: self];
|
|
s = [s stringByReplacingOccurrencesOfString:@"<!-->" withString:@""];
|
|
s = [s stringByReplacingOccurrencesOfString:@"<!--<!" withString:@"<!--"];
|
|
|
|
return s;
|
|
}
|
|
|
|
@end
|