diff --git a/Changelog.rtf b/Changelog.rtf new file mode 100755 index 0000000..0026005 --- /dev/null +++ b/Changelog.rtf @@ -0,0 +1,52 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\f0\b\fs24 \cf0 Version 1.0 Beta\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\b0 \cf0 - First Release\ +\ +\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\b \cf0 Version 1.0.1 Beta\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\b0 \cf0 - Re-arranged preference window contents\ +- Added "Coalesce Notifications" option\ +- Added about panel\ +\ +\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\b \cf0 Version 1.1 Beta +\b0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural +\cf0 - Implemented Rules\ +- Added tool tips\ +\ +\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\b \cf0 Version 1.1.1 Beta +\b0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural +\cf0 - Errors are now displayed when manual refreshing\ +- The rules window shows its errors in a sheet now and dismisses itself after displaying those\ +\ +\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\b \cf0 Version 1.1.2 Beta\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\b0 \cf0 - Changed some translations for clarification\ +- Added system sleep / wake events to refresh just after the mac wakes up (if automatic refreshing is enabled)\ +- Some changes to the code\ +- Included "Beta" and "Build" identifier in the about panel\ +\ +\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\b \cf0 Version 1.1.3 Beta +\b0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural +\cf0 - Notifications now don't display "... uploaded a video" anymore but only the name of the uploader} \ No newline at end of file diff --git a/ISO8601DateFormatter/ISO8601DateFormatter.h b/ISO8601DateFormatter/ISO8601DateFormatter.h new file mode 100755 index 0000000..af4b57c --- /dev/null +++ b/ISO8601DateFormatter/ISO8601DateFormatter.h @@ -0,0 +1,79 @@ +/*ISO8601DateFormatter.h + * + *Created by Peter Hosey on 2009-04-11. + *Copyright 2009 Peter Hosey. All rights reserved. + */ + +#import + +/*This class converts dates to and from ISO 8601 strings. A good introduction to ISO 8601: + * + *Parsing can be done strictly, or not. When you parse loosely, leading whitespace is ignored, as is anything after the date. + *The loose parser will return an NSDate for this string: @" \t\r\n\f\t 2006-03-02!!!" + *Leading non-whitespace will not be ignored; the string will be rejected, and nil returned. See the README that came with this addition. + * + *The strict parser will only accept a string if the date is the entire string. The above string would be rejected immediately, solely on these grounds. + *Also, the loose parser provides some extensions that the strict parser doesn't. + *For example, the standard says for "-DDD" (an ordinal date in the implied year) that the logical representation (meaning, hierarchically) would be "--DDD", but because that extra hyphen is "superfluous", it was omitted. + *The loose parser will accept the extra hyphen; the strict parser will not. + *A full list of these extensions is in the README file. + */ + +/*The format to either expect or produce. + *Calendar format is YYYY-MM-DD. + *Ordinal format is YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01. + *Week format is YYYY-Www-D, where ww ranges from 1 to 53 (the 'W' is literal) and D ranges from 1 to 7; for example, 2009-W05-07. + */ +enum { + ISO8601DateFormatCalendar, + ISO8601DateFormatOrdinal, + ISO8601DateFormatWeek, +}; +typedef NSUInteger ISO8601DateFormat; + +//The default separator for time values. Currently, this is ':'. +extern unichar ISO8601DefaultTimeSeparatorCharacter; + +@interface ISO8601DateFormatter: NSFormatter +{ + NSString *lastUsedFormatString; + NSDateFormatter *unparsingFormatter; + + NSCalendar *parsingCalendar, *unparsingCalendar; + + NSTimeZone *defaultTimeZone; + ISO8601DateFormat format; + unichar timeSeparator; + BOOL includeTime; + BOOL parsesStrictly; +} + +//Call this if you get a memory warning. ++ (void) purgeGlobalCaches; + +@property(nonatomic, retain) NSTimeZone *defaultTimeZone; + +#pragma mark Parsing + +//As a formatter, this object converts strings to dates. + +@property BOOL parsesStrictly; + +- (NSDateComponents *) dateComponentsFromString:(NSString *)string; +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone; +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange; + +- (NSDate *) dateFromString:(NSString *)string; +- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone; +- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange; + +#pragma mark Unparsing + +@property ISO8601DateFormat format; +@property BOOL includeTime; +@property unichar timeSeparator; + +- (NSString *) stringFromDate:(NSDate *)date; +- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone; + +@end diff --git a/ISO8601DateFormatter/ISO8601DateFormatter.m b/ISO8601DateFormatter/ISO8601DateFormatter.m new file mode 100755 index 0000000..2613ad2 --- /dev/null +++ b/ISO8601DateFormatter/ISO8601DateFormatter.m @@ -0,0 +1,895 @@ +/*ISO8601DateFormatter.m + * + *Created by Peter Hosey on 2009-04-11. + *Copyright 2009 Peter Hosey. All rights reserved. + */ + +#import +#import "ISO8601DateFormatter.h" + +#ifndef DEFAULT_TIME_SEPARATOR +# define DEFAULT_TIME_SEPARATOR ':' +#endif +unichar ISO8601DefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR; + +//Unicode date formats. +#define ISO_CALENDAR_DATE_FORMAT @"yyyy-MM-dd" +//#define ISO_WEEK_DATE_FORMAT @"YYYY-'W'ww-ee" //Doesn't actually work because NSDateComponents counts the weekday starting at 1. +#define ISO_ORDINAL_DATE_FORMAT @"yyyy-DDD" +#define ISO_TIME_FORMAT @"HH:mm:ss" +#define ISO_TIME_WITH_TIMEZONE_FORMAT ISO_TIME_FORMAT @"Z" +//printf formats. +#define ISO_TIMEZONE_UTC_FORMAT @"Z" +#define ISO_TIMEZONE_OFFSET_FORMAT @"%+.2d%.2d" + +@interface ISO8601DateFormatter(UnparsingPrivate) + +- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep; + +- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone; +- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone; + +@end + +static NSMutableDictionary *timeZonesByOffset; + +@implementation ISO8601DateFormatter + ++ (void) initialize { + if (!timeZonesByOffset) { + timeZonesByOffset = [[NSMutableDictionary alloc] init]; + } +} + ++ (void) purgeGlobalCaches { + NSMutableDictionary *oldCache = timeZonesByOffset; + timeZonesByOffset = nil; + [oldCache release]; +} + +- (NSCalendar *) makeCalendarWithDesiredConfiguration { + NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; + calendar.firstWeekday = 2; //Monday + calendar.timeZone = [NSTimeZone defaultTimeZone]; + return calendar; +} + +- (id) init { + if ((self = [super init])) { + parsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; + unparsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; + + format = ISO8601DateFormatCalendar; + timeSeparator = ISO8601DefaultTimeSeparatorCharacter; + includeTime = NO; + parsesStrictly = NO; + } + return self; +} +- (void) dealloc { + [defaultTimeZone release]; + + [unparsingFormatter release]; + [lastUsedFormatString release]; + [parsingCalendar release]; + [unparsingCalendar release]; + + [super dealloc]; +} + +@synthesize defaultTimeZone; +- (void) setDefaultTimeZone:(NSTimeZone *)tz { + if (defaultTimeZone != tz) { + [defaultTimeZone release]; + defaultTimeZone = [tz retain]; + + unparsingCalendar.timeZone = defaultTimeZone; + } +} + +//The following properties are only here because GCC doesn't like @synthesize in category implementations. + +#pragma mark Parsing + +@synthesize parsesStrictly; + +static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits); +static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits); +static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next); +static double read_double(const unsigned char *str, const unsigned char **next); +static BOOL is_leap_year(NSUInteger year); + +/*Valid ISO 8601 date formats: + * + *YYYYMMDD + *YYYY-MM-DD + *YYYY-MM + *YYYY + *YY //century + * //Implied century: YY is 00-99 + * YYMMDD + * YY-MM-DD + * -YYMM + * -YY-MM + * -YY + * //Implied year + * --MMDD + * --MM-DD + * --MM + * //Implied year and month + * ---DD + * //Ordinal dates: DDD is the number of the day in the year (1-366) + *YYYYDDD + *YYYY-DDD + * YYDDD + * YY-DDD + * -DDD + * //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week + *yyyyWwwd + *yyyy-Www-d + *yyyyWww + *yyyy-Www + *yyWwwd + *yy-Www-d + *yyWww + *yy-Www + * //Year of the implied decade + *-yWwwd + *-y-Www-d + *-yWww + *-y-Www + * //Week and day of implied year + * -Wwwd + * -Www-d + * //Week only of implied year + * -Www + * //Day only of implied week + * -W-d + */ + +- (NSDateComponents *) dateComponentsFromString:(NSString *)string { + return [self dateComponentsFromString:string timeZone:NULL]; +} +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone { + return [self dateComponentsFromString:string timeZone:outTimeZone range:NULL]; +} +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange { + NSDate *now = [NSDate date]; + + NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease]; + NSDateComponents *nowComponents = [parsingCalendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now]; + + NSUInteger + //Date + year, + month_or_week = 0U, + day = 0U, + //Time + hour = 0U; + NSTimeInterval + minute = 0.0, + second = 0.0; + //Time zone + NSInteger tz_hour = 0; + NSInteger tz_minute = 0; + + enum { + monthAndDate, + week, + dateOnly + } dateSpecification = monthAndDate; + + BOOL strict = self.parsesStrictly; + unichar timeSep = self.timeSeparator; + + if (strict) timeSep = ISO8601DefaultTimeSeparatorCharacter; + NSAssert(timeSep != '\0', @"Time separator must not be NUL."); + + BOOL isValidDate = ([string length] > 0U); + NSTimeZone *timeZone = nil; + + const unsigned char *ch = (const unsigned char *)[string UTF8String]; + + NSRange range = { 0U, 0U }; + const unsigned char *start_of_date = NULL; + if (strict && isspace(*ch)) { + range.location = NSNotFound; + isValidDate = NO; + } else { + //Skip leading whitespace. + NSUInteger i = 0U; + for(NSUInteger len = strlen((const char *)ch); i < len; ++i) { + if (!isspace(ch[i])) + break; + } + + range.location = i; + ch += i; + start_of_date = ch; + + NSUInteger segment; + NSUInteger num_leading_hyphens = 0U, num_digits = 0U; + + if (*ch == 'T') { + //There is no date here, only a time. Set the date to now; then we'll parse the time. + isValidDate = isdigit(*++ch); + + year = nowComponents.year; + month_or_week = nowComponents.month; + day = nowComponents.day; + } else { + while(*ch == '-') { + ++num_leading_hyphens; + ++ch; + } + + segment = read_segment(ch, &ch, &num_digits); + switch(num_digits) { + case 0: + if (*ch == 'W') { + if ((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) { + year = nowComponents.year; + month_or_week = 1U; + ch += 2; + goto parseDayAfterWeek; + } else if (num_leading_hyphens == 1U) { + year = nowComponents.year; + goto parseWeekAndDay; + } else + isValidDate = NO; + } else + isValidDate = NO; + break; + + case 8: //YYYY MM DD + if (num_leading_hyphens > 0U) + isValidDate = NO; + else { + day = segment % 100U; + segment /= 100U; + month_or_week = segment % 100U; + year = segment / 100U; + } + break; + + case 6: //YYMMDD (implicit century) + if (num_leading_hyphens > 0U) + isValidDate = NO; + else { + day = segment % 100U; + segment /= 100U; + month_or_week = segment % 100U; + year = nowComponents.year; + year -= (year % 100U); + year += segment / 100U; + } + break; + + case 4: + switch(num_leading_hyphens) { + case 0: //YYYY + year = segment; + + if (*ch == '-') ++ch; + + if (!isdigit(*ch)) { + if (*ch == 'W') + goto parseWeekAndDay; + else + month_or_week = day = 1U; + } else { + segment = read_segment(ch, &ch, &num_digits); + switch(num_digits) { + case 4: //MMDD + day = segment % 100U; + month_or_week = segment / 100U; + break; + + case 2: //MM + month_or_week = segment; + + if (*ch == '-') ++ch; + if (!isdigit(*ch)) + day = 1U; + else + day = read_segment(ch, &ch, NULL); + break; + + case 3: //DDD + day = segment % 1000U; + dateSpecification = dateOnly; + if (strict && (day > (365U + is_leap_year(year)))) + isValidDate = NO; + break; + + default: + isValidDate = NO; + } + } + break; + + case 1: //YYMM + month_or_week = segment % 100U; + year = segment / 100U; + + if (*ch == '-') ++ch; + if (!isdigit(*ch)) + day = 1U; + else + day = read_segment(ch, &ch, NULL); + + break; + + case 2: //MMDD + day = segment % 100U; + month_or_week = segment / 100U; + year = nowComponents.year; + + break; + + default: + isValidDate = NO; + } //switch(num_leading_hyphens) (4 digits) + break; + + case 1: + if (strict) { + //Two digits only - never just one. + if (num_leading_hyphens == 1U) { + if (*ch == '-') ++ch; + if (*++ch == 'W') { + year = nowComponents.year; + year -= (year % 10U); + year += segment; + goto parseWeekAndDay; + } else + isValidDate = NO; + } else + isValidDate = NO; + break; + } + case 2: + switch(num_leading_hyphens) { + case 0: + if (*ch == '-') { + //Implicit century + year = nowComponents.year; + year -= (year % 100U); + year += segment; + + if (*++ch == 'W') + goto parseWeekAndDay; + else if (!isdigit(*ch)) { + goto centuryOnly; + } else { + //Get month and/or date. + segment = read_segment_4digits(ch, &ch, &num_digits); + NSLog(@"(%@) parsing month; segment is %lu and ch is %s", string, (unsigned long)segment, ch); + switch(num_digits) { + case 4: //YY-MMDD + day = segment % 100U; + month_or_week = segment / 100U; + break; + + case 1: //YY-M; YY-M-DD (extension) + if (strict) { + isValidDate = NO; + break; + } + case 2: //YY-MM; YY-MM-DD + month_or_week = segment; + if (*ch == '-') { + if (isdigit(*++ch)) + day = read_segment_2digits(ch, &ch); + else + day = 1U; + } else + day = 1U; + break; + + case 3: //Ordinal date. + day = segment; + dateSpecification = dateOnly; + break; + } + } + } else if (*ch == 'W') { + year = nowComponents.year; + year -= (year % 100U); + year += segment; + + parseWeekAndDay: //*ch should be 'W' here. + if (!isdigit(*++ch)) { + //Not really a week-based date; just a year followed by '-W'. + if (strict) + isValidDate = NO; + else + month_or_week = day = 1U; + } else { + month_or_week = read_segment_2digits(ch, &ch); + if (*ch == '-') ++ch; + parseDayAfterWeek: + day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U; + dateSpecification = week; + } + } else { + //Century only. Assume current year. + centuryOnly: + year = segment * 100U + nowComponents.year % 100U; + month_or_week = day = 1U; + } + break; + + case 1:; //-YY; -YY-MM (implicit century) + NSLog(@"(%@) found %lu digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %lu", string, (unsigned long)num_digits, (unsigned long)segment); + NSUInteger current_year = nowComponents.year; + NSUInteger current_century = (current_year % 100U); + year = segment + (current_year - current_century); + if (num_digits == 1U) //implied decade + year += current_century - (current_year % 10U); + + if (*ch == '-') { + ++ch; + month_or_week = read_segment_2digits(ch, &ch); + NSLog(@"(%@) month is %lu", string, (unsigned long)month_or_week); + } + + day = 1U; + break; + + case 2: //--MM; --MM-DD + year = nowComponents.year; + month_or_week = segment; + if (*ch == '-') { + ++ch; + day = read_segment_2digits(ch, &ch); + } + break; + + case 3: //---DD + year = nowComponents.year; + month_or_week = nowComponents.month; + day = segment; + break; + + default: + isValidDate = NO; + } //switch(num_leading_hyphens) (2 digits) + break; + + case 7: //YYYY DDD (ordinal date) + if (num_leading_hyphens > 0U) + isValidDate = NO; + else { + day = segment % 1000U; + year = segment / 1000U; + dateSpecification = dateOnly; + if (strict && (day > (365U + is_leap_year(year)))) + isValidDate = NO; + } + break; + + case 3: //--DDD (ordinal date, implicit year) + //Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen. + if ((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict)) + isValidDate = NO; + else { + day = segment; + year = nowComponents.year; + dateSpecification = dateOnly; + if (strict && (day > (365U + is_leap_year(year)))) + isValidDate = NO; + } + break; + + default: + isValidDate = NO; + } + } + + if (isValidDate) { + if (isspace(*ch) || (*ch == 'T')) ++ch; + + if (isdigit(*ch)) { + hour = read_segment_2digits(ch, &ch); + if (*ch == timeSep) { + ++ch; + if ((timeSep == ',') || (timeSep == '.')) { + //We can't do fractional minutes when '.' is the segment separator. + //Only allow whole minutes and whole seconds. + minute = read_segment_2digits(ch, &ch); + if (*ch == timeSep) { + ++ch; + second = read_segment_2digits(ch, &ch); + } + } else { + //Allow a fractional minute. + //If we don't get a fraction, look for a seconds segment. + //Otherwise, the fraction of a minute is the seconds. + minute = read_double(ch, &ch); + second = modf(minute, &minute); + if (second > DBL_EPSILON) + second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30). + else if (*ch == timeSep) { + ++ch; + second = read_double(ch, &ch); + } + } + } + + if (!strict) { + if (isspace(*ch)) ++ch; + } + + switch(*ch) { + case 'Z': + timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + break; + + case '+': + case '-':; + BOOL negative = (*ch == '-'); + if (isdigit(*++ch)) { + //Read hour offset. + segment = *ch - '0'; + if (isdigit(*++ch)) { + segment *= 10U; + segment += *(ch++) - '0'; + } + tz_hour = (NSInteger)segment; + if (negative) tz_hour = -tz_hour; + + //Optional separator. + if (*ch == timeSep) ++ch; + + if (isdigit(*ch)) { + //Read minute offset. + segment = *ch - '0'; + if (isdigit(*++ch)) { + segment *= 10U; + segment += *ch - '0'; + } + tz_minute = segment; + if (negative) tz_minute = -tz_minute; + } + + NSTimeInterval timeZoneOffset = (tz_hour * 3600) + (tz_minute * 60); + NSNumber *offsetNum = [NSNumber numberWithDouble:timeZoneOffset]; + timeZone = [timeZonesByOffset objectForKey:offsetNum]; + if (!timeZone) { + timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset]; + if (timeZone) + [timeZonesByOffset setObject:timeZone forKey:offsetNum]; + } + } + } + } + } + + if (isValidDate) { + components.year = year; + components.day = day; + components.hour = hour; + components.minute = (NSInteger)minute; + components.second = (NSInteger)second; + + switch(dateSpecification) { + case monthAndDate: + components.month = month_or_week; + break; + + case week:; + //Adapted from . + //This works by converting the week date into an ordinal date, then letting the next case handle it. + NSUInteger prevYear = year - 1U; + NSUInteger YY = prevYear % 100U; + NSUInteger C = prevYear - YY; + NSUInteger G = YY + YY / 4U; + NSUInteger isLeapYear = (((C / 100U) % 4U) * 5U); + NSUInteger Jan1Weekday = (isLeapYear + G) % 7U; + enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ }; + components.day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2)); + + case dateOnly: //An "ordinal date". + break; + } + } + } //if (!(strict && isdigit(ch[0]))) + + if (outRange) { + if (isValidDate) + range.length = ch - start_of_date; + else + range.location = NSNotFound; + + *outRange = range; + } + if (outTimeZone) { + *outTimeZone = timeZone; + } + + return components; +} + +- (NSDate *) dateFromString:(NSString *)string { + return [self dateFromString:string timeZone:NULL]; +} +- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone { + return [self dateFromString:string timeZone:outTimeZone range:NULL]; +} +- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange { + NSTimeZone *timeZone = nil; + NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange]; + if (outTimeZone) + *outTimeZone = timeZone; + parsingCalendar.timeZone = timeZone; + + return [parsingCalendar dateFromComponents:components]; +} + +- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error { + NSDate *date = [self dateFromString:string]; + if (outValue) + *outValue = date; + return (date != nil); +} + +#pragma mark Unparsing + +@synthesize format; +@synthesize includeTime; +@synthesize timeSeparator; + +- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep { + if (timeSep != ':') { + NSMutableString *timeFormatMutable = [[timeFormat mutableCopy] autorelease]; + [timeFormatMutable replaceOccurrencesOfString:@":" + withString:[NSString stringWithCharacters:&timeSep length:1U] + options:NSBackwardsSearch | NSLiteralSearch + range:(NSRange){ 0UL, [timeFormat length] }]; + timeFormat = timeFormatMutable; + } + return timeFormat; +} + +- (NSString *) stringFromDate:(NSDate *)date { + NSTimeZone *timeZone = self.defaultTimeZone; + if (!timeZone) timeZone = [NSTimeZone defaultTimeZone]; + return [self stringFromDate:date timeZone:timeZone]; +} + +- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { + switch (self.format) { + case ISO8601DateFormatCalendar: + return [self stringFromDate:date formatString:ISO_CALENDAR_DATE_FORMAT timeZone:timeZone]; + case ISO8601DateFormatWeek: + return [self weekDateStringForDate:date timeZone:timeZone]; + case ISO8601DateFormatOrdinal: + return [self stringFromDate:date formatString:ISO_ORDINAL_DATE_FORMAT timeZone:timeZone]; + default: + [NSException raise:NSInternalInconsistencyException format:@"self.format was %ld, not calendar (%d), week (%d), or ordinal (%d)", self.format, ISO8601DateFormatCalendar, ISO8601DateFormatWeek, ISO8601DateFormatOrdinal]; + return nil; + } +} + +- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone { + if (includeTime) + dateFormat = [dateFormat stringByAppendingFormat:@"'T'%@", [self replaceColonsInString:ISO_TIME_FORMAT withTimeSeparator:self.timeSeparator]]; + + unparsingCalendar.timeZone = timeZone; + + if (dateFormat != lastUsedFormatString) { + [unparsingFormatter release]; + unparsingFormatter = nil; + + [lastUsedFormatString release]; + lastUsedFormatString = [dateFormat retain]; + } + + if (!unparsingFormatter) { + unparsingFormatter = [[NSDateFormatter alloc] init]; + unparsingFormatter.formatterBehavior = NSDateFormatterBehavior10_4; + unparsingFormatter.dateFormat = dateFormat; + unparsingFormatter.calendar = unparsingCalendar; + } + + NSString *str = [unparsingFormatter stringForObjectValue:date]; + + if (includeTime) { + NSInteger offset = [timeZone secondsFromGMT]; + offset /= 60; //bring down to minutes + if (offset == 0) + str = [str stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT]; + else + str = [str stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT, offset / 60, offset % 60]; + } + + //Undo the change we made earlier + unparsingCalendar.timeZone = self.defaultTimeZone; + + return str; +} + +- (NSString *) stringForObjectValue:(id)value { + NSParameterAssert([value isKindOfClass:[NSDate class]]); + + return [self stringFromDate:(NSDate *)value]; +} + +/*Adapted from: + * Algorithm for Converting Gregorian Dates to ISO 8601 Week Date + * Rick McCarty, 1999 + * http://personal.ecu.edu/mccartyr/ISOwdALG.txt + */ +- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { + unparsingCalendar.timeZone = timeZone; + NSDateComponents *components = [unparsingCalendar components:NSYearCalendarUnit | NSWeekdayCalendarUnit | NSDayCalendarUnit fromDate:date]; + + //Determine the ordinal date. + NSDateComponents *startOfYearComponents = [unparsingCalendar components:NSYearCalendarUnit fromDate:date]; + startOfYearComponents.month = 1; + startOfYearComponents.day = 1; + NSDateComponents *ordinalComponents = [unparsingCalendar components:NSDayCalendarUnit fromDate:[unparsingCalendar dateFromComponents:startOfYearComponents] toDate:date options:0]; + ordinalComponents.day += 1; + + enum { + monday, tuesday, wednesday, thursday, friday, saturday, sunday + }; + enum { + january = 1, february, march, + april, may, june, + july, august, september, + october, november, december + }; + + NSInteger year = components.year; + NSInteger week = 0; + //The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5. + NSInteger dayOfWeek = (components.weekday + 5) % 7; + NSInteger dayOfYear = ordinalComponents.day; + + NSInteger prevYear = year - 1; + + BOOL yearIsLeapYear = is_leap_year(year); + BOOL prevYearIsLeapYear = is_leap_year(prevYear); + + NSInteger YY = prevYear % 100; + NSInteger C = prevYear - YY; + NSInteger G = YY + YY / 4; + NSInteger Jan1Weekday = (((((C / 100) % 4) * 5) + G) % 7); + + NSInteger weekday = ((dayOfYear + Jan1Weekday) - 1) % 7; + + if((dayOfYear <= (7 - Jan1Weekday)) && (Jan1Weekday > thursday)) { + week = 52 + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear)); + --year; + } else { + NSInteger lengthOfYear = 365 + yearIsLeapYear; + if((lengthOfYear - dayOfYear) < (thursday - weekday)) { + ++year; + week = 1; + } else { + NSInteger J = dayOfYear + (sunday - weekday) + Jan1Weekday; + week = J / 7 - (Jan1Weekday > thursday); + } + } + + NSString *timeString; + if(includeTime) { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + unichar timeSep = self.timeSeparator; + if (!timeSep) timeSep = ISO8601DefaultTimeSeparatorCharacter; + formatter.dateFormat = [self replaceColonsInString:ISO_TIME_WITH_TIMEZONE_FORMAT withTimeSeparator:timeSep]; + + timeString = [formatter stringForObjectValue:date]; + + [formatter release]; + } else + timeString = @""; + + return [NSString stringWithFormat:@"%lu-W%02lu-%02lu%@", (unsigned long)year, (unsigned long)week, ((unsigned long)dayOfWeek) + 1U, timeString]; +} + +@end + +static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) { + NSUInteger num_digits = 0U; + NSUInteger value = 0U; + + while(isdigit(*str)) { + value *= 10U; + value += *str - '0'; + ++num_digits; + ++str; + } + + if (next) *next = str; + if (out_num_digits) *out_num_digits = num_digits; + + return value; +} +static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) { + NSUInteger num_digits = 0U; + NSUInteger value = 0U; + + if (isdigit(*str)) { + value += *(str++) - '0'; + ++num_digits; + } + + if (isdigit(*str)) { + value *= 10U; + value += *(str++) - '0'; + ++num_digits; + } + + if (isdigit(*str)) { + value *= 10U; + value += *(str++) - '0'; + ++num_digits; + } + + if (isdigit(*str)) { + value *= 10U; + value += *(str++) - '0'; + ++num_digits; + } + + if (next) *next = str; + if (out_num_digits) *out_num_digits = num_digits; + + return value; +} +static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next) { + NSUInteger value = 0U; + + if (isdigit(*str)) + value += *str - '0'; + + if (isdigit(*++str)) { + value *= 10U; + value += *(str++) - '0'; + } + + if (next) *next = str; + + return value; +} + +//strtod doesn't support ',' as a separator. This does. +static double read_double(const unsigned char *str, const unsigned char **next) { + double value = 0.0; + + if (str) { + NSUInteger int_value = 0; + + while(isdigit(*str)) { + int_value *= 10U; + int_value += (*(str++) - '0'); + } + value = int_value; + + if (((*str == ',') || (*str == '.'))) { + ++str; + + register double multiplier, multiplier_multiplier; + multiplier = multiplier_multiplier = 0.1; + + while(isdigit(*str)) { + value += (*(str++) - '0') * multiplier; + multiplier *= multiplier_multiplier; + } + } + } + + if (next) *next = str; + + return value; +} + +static BOOL is_leap_year(NSUInteger year) { + return \ + ((year % 4U) == 0U) + && (((year % 100U) != 0U) + || ((year % 400U) == 0U)); +} diff --git a/Notifications for YouTube.icns b/Notifications for YouTube.icns new file mode 100755 index 0000000..deb4a68 Binary files /dev/null and b/Notifications for YouTube.icns differ diff --git a/Notifications for YouTube.xcodeproj/project.pbxproj b/Notifications for YouTube.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 6c4bbc8..2618e56 --- a/Notifications for YouTube.xcodeproj/project.pbxproj +++ b/Notifications for YouTube.xcodeproj/project.pbxproj @@ -7,15 +7,62 @@ objects = { /* Begin PBXBuildFile section */ + 3B87C5BD179C1ECE008949FF /* NYTRulesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5BB179C1ECE008949FF /* NYTRulesWindowController.m */; }; + 3B87C5C6179C3B2E008949FF /* NYTAuthentication.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5C5179C3B2E008949FF /* NYTAuthentication.m */; }; + 3B87C5C9179C3C6E008949FF /* NYTUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5C8179C3C6E008949FF /* NYTUtil.m */; }; + 3B87C5CC179C6F59008949FF /* NYTChannelRestriction.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5CB179C6F59008949FF /* NYTChannelRestriction.m */; }; + 3B87C5D2179C81F2008949FF /* ItemCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5D1179C81F2008949FF /* ItemCellView.m */; }; + 3B87C5D3179CAE6F008949FF /* NYTRulesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B87C5D5179CAE6F008949FF /* NYTRulesWindowController.xib */; }; + 3B87C5D9179CB26E008949FF /* RulesPredicates.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3B87C5DB179CB26E008949FF /* RulesPredicates.strings */; }; 3BAF5F6C178C0DAE00087D7C /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */; }; 3BAF5F76178C0DAE00087D7C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F74178C0DAE00087D7C /* InfoPlist.strings */; }; 3BAF5F78178C0DAE00087D7C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F77178C0DAE00087D7C /* main.m */; }; 3BAF5F7C178C0DAE00087D7C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F7A178C0DAE00087D7C /* Credits.rtf */; }; 3BAF5F7F178C0DAE00087D7C /* NYTAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */; }; 3BAF5F82178C0DAE00087D7C /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F80178C0DAE00087D7C /* MainMenu.xib */; }; + 3BAF5F90178C322B00087D7C /* NYTUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F8F178C322B00087D7C /* NYTUser.m */; }; + 3BAF5F92178C3CDD00087D7C /* poweredByYT.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F91178C3CDD00087D7C /* poweredByYT.png */; }; + 3BAF5F94178C3CEA00087D7C /* Notifications for YouTube.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F93178C3CEA00087D7C /* Notifications for YouTube.icns */; }; + 3BAF5F9D178C53F600087D7C /* NYTUpdateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F9C178C53F600087D7C /* NYTUpdateManager.m */; }; + 3BDCBCE517931FA000517427 /* NYTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BDCBCE417931FA000517427 /* NYTVideo.m */; }; + 3BE5037B17905259008808D6 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE5037A17905259008808D6 /* WebKit.framework */; }; + 3BE5037D1790525E008808D6 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE5037C1790525E008808D6 /* Security.framework */; }; + 3BE5037F1790527F008808D6 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE5037E1790527F008808D6 /* SystemConfiguration.framework */; }; + 3BE5038D1790B657008808D6 /* GTMHTTPFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503821790B657008808D6 /* GTMHTTPFetcher.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3BE5038E1790B657008808D6 /* GTMHTTPFetchHistory.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503841790B657008808D6 /* GTMHTTPFetchHistory.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3BE5038F1790B657008808D6 /* GTMOAuth2Authentication.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503861790B657008808D6 /* GTMOAuth2Authentication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3BE503901790B657008808D6 /* GTMOAuth2SignIn.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503881790B657008808D6 /* GTMOAuth2SignIn.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3BE503921790B657008808D6 /* GTMOAuth2WindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE5038B1790B657008808D6 /* GTMOAuth2WindowController.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3BE503961790B678008808D6 /* ISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503951790B678008808D6 /* ISO8601DateFormatter.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3BE5039C1790C5B2008808D6 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BE5039A1790C5B2008808D6 /* Localizable.strings */; }; + 3BE5039E1790C86A008808D6 /* GTMOAuth2Window.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BE503A01790C86A008808D6 /* GTMOAuth2Window.xib */; }; + 3BE64577179A13590086DAA5 /* NumberValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE64576179A13590086DAA5 /* NumberValueTransformer.m */; }; + 3BFF3CBB1795D90D00ACAF58 /* play-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB51795D90D00ACAF58 /* play-icon.png */; }; + 3BFF3CBC1795D90D00ACAF58 /* play-icon-bw.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB61795D90D00ACAF58 /* play-icon-bw.png */; }; + 3BFF3CBD1795D90D00ACAF58 /* play-icon-bw@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB71795D90D00ACAF58 /* play-icon-bw@2x.png */; }; + 3BFF3CBE1795D90D00ACAF58 /* play-icon-s.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB81795D90D00ACAF58 /* play-icon-s.png */; }; + 3BFF3CBF1795D90D00ACAF58 /* play-icon-s@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB91795D90D00ACAF58 /* play-icon-s@2x.png */; }; + 3BFF3CC01795D90D00ACAF58 /* play-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CBA1795D90D00ACAF58 /* play-icon@2x.png */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3B50712317A07EEB00C83EA0 /* Changelog.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Changelog.rtf; sourceTree = ""; }; + 3B61BD22179B283100FA4B3B /* de */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = de; path = de.lproj/MainMenu.xib; sourceTree = ""; }; + 3B61BD23179B28C800FA4B3B /* de */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 3B87C5BA179C1ECE008949FF /* NYTRulesWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTRulesWindowController.h; sourceTree = ""; }; + 3B87C5BB179C1ECE008949FF /* NYTRulesWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTRulesWindowController.m; sourceTree = ""; }; + 3B87C5C4179C3B2E008949FF /* NYTAuthentication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTAuthentication.h; sourceTree = ""; }; + 3B87C5C5179C3B2E008949FF /* NYTAuthentication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTAuthentication.m; sourceTree = ""; }; + 3B87C5C7179C3C6E008949FF /* NYTUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTUtil.h; sourceTree = ""; }; + 3B87C5C8179C3C6E008949FF /* NYTUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTUtil.m; sourceTree = ""; }; + 3B87C5CA179C6F59008949FF /* NYTChannelRestriction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTChannelRestriction.h; sourceTree = ""; }; + 3B87C5CB179C6F59008949FF /* NYTChannelRestriction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTChannelRestriction.m; sourceTree = ""; }; + 3B87C5D0179C81F2008949FF /* ItemCellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemCellView.h; sourceTree = ""; }; + 3B87C5D1179C81F2008949FF /* ItemCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemCellView.m; sourceTree = ""; }; + 3B87C5D4179CAE6F008949FF /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/NYTRulesWindowController.xib; sourceTree = ""; }; + 3B87C5D6179CAE75008949FF /* de */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = de; path = de.lproj/NYTRulesWindowController.xib; sourceTree = ""; }; + 3B87C5DA179CB26E008949FF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/RulesPredicates.strings; sourceTree = ""; }; + 3B87C5DC179CB275008949FF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/RulesPredicates.strings; sourceTree = ""; }; 3BAF5F68178C0DAE00087D7C /* Notifications for YouTube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Notifications for YouTube.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 3BAF5F6E178C0DAE00087D7C /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -27,8 +74,45 @@ 3BAF5F79178C0DAE00087D7C /* Notifications for YouTube-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notifications for YouTube-Prefix.pch"; sourceTree = ""; }; 3BAF5F7B178C0DAE00087D7C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 3BAF5F7D178C0DAE00087D7C /* NYTAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NYTAppDelegate.h; sourceTree = ""; }; - 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NYTAppDelegate.m; sourceTree = ""; }; + 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NYTAppDelegate.m; sourceTree = ""; wrapsLines = 1; }; 3BAF5F81178C0DAE00087D7C /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; + 3BAF5F8E178C322B00087D7C /* NYTUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTUser.h; sourceTree = ""; }; + 3BAF5F8F178C322B00087D7C /* NYTUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTUser.m; sourceTree = ""; }; + 3BAF5F91178C3CDD00087D7C /* poweredByYT.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = poweredByYT.png; sourceTree = ""; }; + 3BAF5F93178C3CEA00087D7C /* Notifications for YouTube.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "Notifications for YouTube.icns"; sourceTree = ""; }; + 3BAF5F9B178C53F600087D7C /* NYTUpdateManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTUpdateManager.h; sourceTree = ""; }; + 3BAF5F9C178C53F600087D7C /* NYTUpdateManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTUpdateManager.m; sourceTree = ""; }; + 3BDCBCE317931FA000517427 /* NYTVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTVideo.h; sourceTree = ""; }; + 3BDCBCE417931FA000517427 /* NYTVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTVideo.m; sourceTree = ""; }; + 3BE5037A17905259008808D6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 3BE5037C1790525E008808D6 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 3BE5037E1790527F008808D6 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 3BE503811790B657008808D6 /* GTMHTTPFetcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetcher.h; sourceTree = ""; }; + 3BE503821790B657008808D6 /* GTMHTTPFetcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetcher.m; sourceTree = ""; }; + 3BE503831790B657008808D6 /* GTMHTTPFetchHistory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetchHistory.h; sourceTree = ""; }; + 3BE503841790B657008808D6 /* GTMHTTPFetchHistory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetchHistory.m; sourceTree = ""; }; + 3BE503851790B657008808D6 /* GTMOAuth2Authentication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2Authentication.h; sourceTree = ""; }; + 3BE503861790B657008808D6 /* GTMOAuth2Authentication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2Authentication.m; sourceTree = ""; }; + 3BE503871790B657008808D6 /* GTMOAuth2SignIn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2SignIn.h; sourceTree = ""; }; + 3BE503881790B657008808D6 /* GTMOAuth2SignIn.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2SignIn.m; sourceTree = ""; }; + 3BE5038A1790B657008808D6 /* GTMOAuth2WindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2WindowController.h; sourceTree = ""; }; + 3BE5038B1790B657008808D6 /* GTMOAuth2WindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2WindowController.m; sourceTree = ""; }; + 3BE5038C1790B657008808D6 /* OAuth2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OAuth2.h; sourceTree = ""; }; + 3BE503941790B678008808D6 /* ISO8601DateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ISO8601DateFormatter.h; sourceTree = ""; }; + 3BE503951790B678008808D6 /* ISO8601DateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ISO8601DateFormatter.m; sourceTree = ""; }; + 3BE503981790C401008808D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; + 3BE503991790C402008808D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = de; path = de.lproj/Credits.rtf; sourceTree = ""; }; + 3BE5039B1790C5B2008808D6 /* en */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 3BE5039F1790C86A008808D6 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/GTMOAuth2Window.xib; sourceTree = ""; }; + 3BE503A11790C870008808D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = de; path = de.lproj/GTMOAuth2Window.xib; sourceTree = ""; }; + 3BE64575179A13590086DAA5 /* NumberValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NumberValueTransformer.h; sourceTree = ""; }; + 3BE64576179A13590086DAA5 /* NumberValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NumberValueTransformer.m; sourceTree = ""; }; + 3BFF3CB51795D90D00ACAF58 /* play-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon.png"; sourceTree = ""; }; + 3BFF3CB61795D90D00ACAF58 /* play-icon-bw.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-bw.png"; sourceTree = ""; }; + 3BFF3CB71795D90D00ACAF58 /* play-icon-bw@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-bw@2x.png"; sourceTree = ""; }; + 3BFF3CB81795D90D00ACAF58 /* play-icon-s.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-s.png"; sourceTree = ""; }; + 3BFF3CB91795D90D00ACAF58 /* play-icon-s@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-s@2x.png"; sourceTree = ""; }; + 3BFF3CBA1795D90D00ACAF58 /* play-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon@2x.png"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -36,6 +120,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3BE5037F1790527F008808D6 /* SystemConfiguration.framework in Frameworks */, + 3BE5037D1790525E008808D6 /* Security.framework in Frameworks */, + 3BE5037B17905259008808D6 /* WebKit.framework in Frameworks */, 3BAF5F6C178C0DAE00087D7C /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -46,6 +133,10 @@ 3BAF5F5F178C0DAE00087D7C = { isa = PBXGroup; children = ( + 3BAF5F93178C3CEA00087D7C /* Notifications for YouTube.icns */, + 3B50712317A07EEB00C83EA0 /* Changelog.rtf */, + 3BE503801790B657008808D6 /* OAuth 2 */, + 3BE503931790B678008808D6 /* ISO8601DateFormatter */, 3BAF5F71178C0DAE00087D7C /* Notifications for YouTube */, 3BAF5F6A178C0DAE00087D7C /* Frameworks */, 3BAF5F69178C0DAE00087D7C /* Products */, @@ -63,6 +154,9 @@ 3BAF5F6A178C0DAE00087D7C /* Frameworks */ = { isa = PBXGroup; children = ( + 3BE5037E1790527F008808D6 /* SystemConfiguration.framework */, + 3BE5037C1790525E008808D6 /* Security.framework */, + 3BE5037A17905259008808D6 /* WebKit.framework */, 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */, 3BAF5F6D178C0DAE00087D7C /* Other Frameworks */, ); @@ -82,9 +176,17 @@ 3BAF5F71178C0DAE00087D7C /* Notifications for YouTube */ = { isa = PBXGroup; children = ( + 3BE64574179A13310086DAA5 /* Customization */, + 3BAF5F9E178C540800087D7C /* Core */, + 3BFF3CC11795D91500ACAF58 /* Resources */, 3BAF5F7D178C0DAE00087D7C /* NYTAppDelegate.h */, 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */, 3BAF5F80178C0DAE00087D7C /* MainMenu.xib */, + 3B87C5CA179C6F59008949FF /* NYTChannelRestriction.h */, + 3B87C5CB179C6F59008949FF /* NYTChannelRestriction.m */, + 3B87C5BA179C1ECE008949FF /* NYTRulesWindowController.h */, + 3B87C5BB179C1ECE008949FF /* NYTRulesWindowController.m */, + 3B87C5D5179CAE6F008949FF /* NYTRulesWindowController.xib */, 3BAF5F72178C0DAE00087D7C /* Supporting Files */, ); path = "Notifications for YouTube"; @@ -102,6 +204,78 @@ name = "Supporting Files"; sourceTree = ""; }; + 3BAF5F9E178C540800087D7C /* Core */ = { + isa = PBXGroup; + children = ( + 3BAF5F8E178C322B00087D7C /* NYTUser.h */, + 3BAF5F8F178C322B00087D7C /* NYTUser.m */, + 3BDCBCE317931FA000517427 /* NYTVideo.h */, + 3BDCBCE417931FA000517427 /* NYTVideo.m */, + 3B87C5C7179C3C6E008949FF /* NYTUtil.h */, + 3B87C5C8179C3C6E008949FF /* NYTUtil.m */, + 3B87C5C4179C3B2E008949FF /* NYTAuthentication.h */, + 3B87C5C5179C3B2E008949FF /* NYTAuthentication.m */, + 3BAF5F9B178C53F600087D7C /* NYTUpdateManager.h */, + 3BAF5F9C178C53F600087D7C /* NYTUpdateManager.m */, + ); + name = Core; + sourceTree = ""; + }; + 3BE503801790B657008808D6 /* OAuth 2 */ = { + isa = PBXGroup; + children = ( + 3BE503811790B657008808D6 /* GTMHTTPFetcher.h */, + 3BE503821790B657008808D6 /* GTMHTTPFetcher.m */, + 3BE503831790B657008808D6 /* GTMHTTPFetchHistory.h */, + 3BE503841790B657008808D6 /* GTMHTTPFetchHistory.m */, + 3BE503851790B657008808D6 /* GTMOAuth2Authentication.h */, + 3BE503861790B657008808D6 /* GTMOAuth2Authentication.m */, + 3BE503871790B657008808D6 /* GTMOAuth2SignIn.h */, + 3BE503881790B657008808D6 /* GTMOAuth2SignIn.m */, + 3BE503A01790C86A008808D6 /* GTMOAuth2Window.xib */, + 3BE5038A1790B657008808D6 /* GTMOAuth2WindowController.h */, + 3BE5038B1790B657008808D6 /* GTMOAuth2WindowController.m */, + 3BE5038C1790B657008808D6 /* OAuth2.h */, + ); + path = "OAuth 2"; + sourceTree = ""; + }; + 3BE503931790B678008808D6 /* ISO8601DateFormatter */ = { + isa = PBXGroup; + children = ( + 3BE503941790B678008808D6 /* ISO8601DateFormatter.h */, + 3BE503951790B678008808D6 /* ISO8601DateFormatter.m */, + ); + path = ISO8601DateFormatter; + sourceTree = ""; + }; + 3BE64574179A13310086DAA5 /* Customization */ = { + isa = PBXGroup; + children = ( + 3BE64575179A13590086DAA5 /* NumberValueTransformer.h */, + 3BE64576179A13590086DAA5 /* NumberValueTransformer.m */, + 3B87C5D0179C81F2008949FF /* ItemCellView.h */, + 3B87C5D1179C81F2008949FF /* ItemCellView.m */, + ); + name = Customization; + sourceTree = ""; + }; + 3BFF3CC11795D91500ACAF58 /* Resources */ = { + isa = PBXGroup; + children = ( + 3BAF5F91178C3CDD00087D7C /* poweredByYT.png */, + 3BFF3CB51795D90D00ACAF58 /* play-icon.png */, + 3BFF3CBA1795D90D00ACAF58 /* play-icon@2x.png */, + 3BFF3CB61795D90D00ACAF58 /* play-icon-bw.png */, + 3BFF3CB71795D90D00ACAF58 /* play-icon-bw@2x.png */, + 3BFF3CB81795D90D00ACAF58 /* play-icon-s.png */, + 3BFF3CB91795D90D00ACAF58 /* play-icon-s@2x.png */, + 3BE5039A1790C5B2008808D6 /* Localizable.strings */, + 3B87C5DB179CB26E008949FF /* RulesPredicates.strings */, + ); + name = Resources; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -138,6 +312,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + de, ); mainGroup = 3BAF5F5F178C0DAE00087D7C; productRefGroup = 3BAF5F69178C0DAE00087D7C /* Products */; @@ -157,6 +332,18 @@ 3BAF5F76178C0DAE00087D7C /* InfoPlist.strings in Resources */, 3BAF5F7C178C0DAE00087D7C /* Credits.rtf in Resources */, 3BAF5F82178C0DAE00087D7C /* MainMenu.xib in Resources */, + 3BAF5F92178C3CDD00087D7C /* poweredByYT.png in Resources */, + 3BAF5F94178C3CEA00087D7C /* Notifications for YouTube.icns in Resources */, + 3BE5039E1790C86A008808D6 /* GTMOAuth2Window.xib in Resources */, + 3BE5039C1790C5B2008808D6 /* Localizable.strings in Resources */, + 3BFF3CBB1795D90D00ACAF58 /* play-icon.png in Resources */, + 3BFF3CBC1795D90D00ACAF58 /* play-icon-bw.png in Resources */, + 3BFF3CBD1795D90D00ACAF58 /* play-icon-bw@2x.png in Resources */, + 3BFF3CBE1795D90D00ACAF58 /* play-icon-s.png in Resources */, + 3BFF3CBF1795D90D00ACAF58 /* play-icon-s@2x.png in Resources */, + 3BFF3CC01795D90D00ACAF58 /* play-icon@2x.png in Resources */, + 3B87C5D3179CAE6F008949FF /* NYTRulesWindowController.xib in Resources */, + 3B87C5D9179CB26E008949FF /* RulesPredicates.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -169,16 +356,50 @@ files = ( 3BAF5F78178C0DAE00087D7C /* main.m in Sources */, 3BAF5F7F178C0DAE00087D7C /* NYTAppDelegate.m in Sources */, + 3BAF5F90178C322B00087D7C /* NYTUser.m in Sources */, + 3BAF5F9D178C53F600087D7C /* NYTUpdateManager.m in Sources */, + 3BE5038D1790B657008808D6 /* GTMHTTPFetcher.m in Sources */, + 3BE5038E1790B657008808D6 /* GTMHTTPFetchHistory.m in Sources */, + 3BE5038F1790B657008808D6 /* GTMOAuth2Authentication.m in Sources */, + 3BE503901790B657008808D6 /* GTMOAuth2SignIn.m in Sources */, + 3BE503921790B657008808D6 /* GTMOAuth2WindowController.m in Sources */, + 3BE503961790B678008808D6 /* ISO8601DateFormatter.m in Sources */, + 3BDCBCE517931FA000517427 /* NYTVideo.m in Sources */, + 3BE64577179A13590086DAA5 /* NumberValueTransformer.m in Sources */, + 3B87C5BD179C1ECE008949FF /* NYTRulesWindowController.m in Sources */, + 3B87C5C6179C3B2E008949FF /* NYTAuthentication.m in Sources */, + 3B87C5C9179C3C6E008949FF /* NYTUtil.m in Sources */, + 3B87C5CC179C6F59008949FF /* NYTChannelRestriction.m in Sources */, + 3B87C5D2179C81F2008949FF /* ItemCellView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ + 3B87C5D5179CAE6F008949FF /* NYTRulesWindowController.xib */ = { + isa = PBXVariantGroup; + children = ( + 3B87C5D4179CAE6F008949FF /* en */, + 3B87C5D6179CAE75008949FF /* de */, + ); + name = NYTRulesWindowController.xib; + sourceTree = ""; + }; + 3B87C5DB179CB26E008949FF /* RulesPredicates.strings */ = { + isa = PBXVariantGroup; + children = ( + 3B87C5DA179CB26E008949FF /* en */, + 3B87C5DC179CB275008949FF /* de */, + ); + name = RulesPredicates.strings; + sourceTree = ""; + }; 3BAF5F74178C0DAE00087D7C /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( 3BAF5F75178C0DAE00087D7C /* en */, + 3BE503981790C401008808D6 /* de */, ); name = InfoPlist.strings; sourceTree = ""; @@ -187,6 +408,7 @@ isa = PBXVariantGroup; children = ( 3BAF5F7B178C0DAE00087D7C /* en */, + 3BE503991790C402008808D6 /* de */, ); name = Credits.rtf; sourceTree = ""; @@ -195,10 +417,29 @@ isa = PBXVariantGroup; children = ( 3BAF5F81178C0DAE00087D7C /* en */, + 3B61BD22179B283100FA4B3B /* de */, ); name = MainMenu.xib; sourceTree = ""; }; + 3BE5039A1790C5B2008808D6 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 3BE5039B1790C5B2008808D6 /* en */, + 3B61BD23179B28C800FA4B3B /* de */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 3BE503A01790C86A008808D6 /* GTMOAuth2Window.xib */ = { + isa = PBXVariantGroup; + children = ( + 3BE5039F1790C86A008808D6 /* en */, + 3BE503A11790C870008808D6 /* de */, + ); + name = GTMOAuth2Window.xib; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -304,6 +545,7 @@ 3BAF5F87178C0DAE00087D7C /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/Notifications for YouTube.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Notifications for YouTube.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..cd53a31 --- /dev/null +++ b/Notifications for YouTube.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Notifications for YouTube.xcodeproj/project.xcworkspace/xcuserdata/kim.xcuserdatad/UserInterfaceState.xcuserstate b/Notifications for YouTube.xcodeproj/project.xcworkspace/xcuserdata/kim.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100755 index 0000000..a3d28fd Binary files /dev/null and b/Notifications for YouTube.xcodeproj/project.xcworkspace/xcuserdata/kim.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Notifications for YouTube.xcodeproj/project.xcworkspace/xcuserdata/kim.xcuserdatad/WorkspaceSettings.xcsettings b/Notifications for YouTube.xcodeproj/project.xcworkspace/xcuserdata/kim.xcuserdatad/WorkspaceSettings.xcsettings new file mode 100755 index 0000000..bfffcfe --- /dev/null +++ b/Notifications for YouTube.xcodeproj/project.xcworkspace/xcuserdata/kim.xcuserdatad/WorkspaceSettings.xcsettings @@ -0,0 +1,10 @@ + + + + + HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges + + SnapshotAutomaticallyBeforeSignificantChanges + + + diff --git a/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist b/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist new file mode 100755 index 0000000..05301bc --- /dev/null +++ b/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist @@ -0,0 +1,5 @@ + + + diff --git a/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcschemes/Notifications for YouTube.xcscheme b/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcschemes/Notifications for YouTube.xcscheme new file mode 100755 index 0000000..6c9991c --- /dev/null +++ b/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcschemes/Notifications for YouTube.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcschemes/xcschememanagement.plist b/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000..4b6e049 --- /dev/null +++ b/Notifications for YouTube.xcodeproj/xcuserdata/kim.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Notifications for YouTube.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + 3BAF5F67178C0DAE00087D7C + + primary + + + + + diff --git a/Notifications for YouTube/ItemCellView.h b/Notifications for YouTube/ItemCellView.h new file mode 100755 index 0000000..6fee8fd --- /dev/null +++ b/Notifications for YouTube/ItemCellView.h @@ -0,0 +1,15 @@ +// +// ItemCellView.h +// LionTableViewTesting +// +// Created by Tomaž Kragelj on 8/29/11. +// Copyright 2011 __MyCompanyName__. All rights reserved. +// + +/* A table cell view that supports a detail text field. This is useful because by default no label but the default text field gets highlighted when the cell gets selected. The item cell view also supports a second text field that toggles between [NSColor windowBackgroundColor] and [NSColor controlShadowColor] for the default and selected state respectively. + */ +@interface ItemCellView : NSTableCellView + +@property (nonatomic) IBOutlet NSTextField *detailTextField; + +@end diff --git a/Notifications for YouTube/ItemCellView.m b/Notifications for YouTube/ItemCellView.m new file mode 100755 index 0000000..cb8b21f --- /dev/null +++ b/Notifications for YouTube/ItemCellView.m @@ -0,0 +1,21 @@ +// +// ItemCellView.m +// LionTableViewTesting +// +// Created by Tomaž Kragelj on 8/29/11. +// Copyright 2011 __MyCompanyName__. All rights reserved. +// + +#import "ItemCellView.h" + +@implementation ItemCellView + +@synthesize detailTextField = _detailTextField; + +- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle { + NSColor *textColor = (backgroundStyle == NSBackgroundStyleDark) ? [NSColor windowBackgroundColor] : [NSColor controlShadowColor]; + self.detailTextField.textColor = textColor; + [super setBackgroundStyle:backgroundStyle]; +} + +@end diff --git a/Notifications for YouTube/NYTAppDelegate.h b/Notifications for YouTube/NYTAppDelegate.h old mode 100644 new mode 100755 index 9006fed..16456a6 --- a/Notifications for YouTube/NYTAppDelegate.h +++ b/Notifications for YouTube/NYTAppDelegate.h @@ -6,10 +6,58 @@ // Copyright (c) 2013 Kim Wittenburg. All rights reserved. // -#import +@interface NYTAppDelegate : NSObject -@interface NYTAppDelegate : NSObject +@property (unsafe_unretained) IBOutlet NSWindow *window; -@property (assign) IBOutlet NSWindow *window; +#pragma mark *** Handling the Status Item *** + +/* The menu attached to the app's status item. There should be very few reason to change the menu's contents. + */ +@property (weak) IBOutlet NSMenu *statusMenu; + +/* This property holds the app's status item if any. Following this definition this property will be nil if there is currently no status item. + */ +@property (readonly, strong) NSStatusItem *statusItem; + +/* Use these methods to set and query the state of the app's status item in a secure and safe way. + */ +- (void)setStatusItemVisible:(BOOL)visible; +- (BOOL)isStatusItemVisible; + +#pragma mark *** Handling Login *** + +/* This property returns YES as long as there is currently a login process running. You may want to observe this property to get notified when the login process finished. This is important because there should be no notifications delivered when the user is about to change. + This property gives no information wether the login process sucessfully finished or was canceled by the user or if an error occured. If you want further information than wether the logging in process is running you must query NYTUpdateManager. + */ +@property (readonly) BOOL loggingIn; + +/* This method is triggered from the user interface. It begins a modal sheet that gives the user the possibility to login and thus permit Notifications for YouTube to access the account data. This method does not try to access the user's keychain to query any existing login data. This is handled during applicationDidFinishLaunching: + */ +- (IBAction)login:(id)sender; + +#pragma mark *** Handling Refreshing *** + +/* Triggers a refresh from the user interface. + ATTENTION: If you attempt to invoke this method on your own note that the sender's window will be used to present errors occured during the update (such as network unavailability). If the sender's window is nil or the sender does not respond to a window property the alerts are displayed application modal. If you do not want any errors to be displayed automatically use NYTUpdateManager. + */ +- (IBAction)performRefresh:(id)sender; + +#pragma mark *** Configure Notifications *** + +/* Begins a sheet that allows the user to edit the rules which notifications should be shown. This method is triggered from the user interface. + */ +- (IBAction)editRules:(id)sender; + +#pragma mark *** Other Actions *** + +/* Show application windows. These methods also make Notifications for YouTube the foremost application. + */ +- (IBAction)showPreferenceWindow:(id)sender; +- (IBAction)showAboutPanel:(id)sender; + +/* Opens the YouTube website. + */ +- (IBAction)browseYouTube:(id)sender; @end diff --git a/Notifications for YouTube/NYTAppDelegate.m b/Notifications for YouTube/NYTAppDelegate.m old mode 100644 new mode 100755 index 8adde1f..586c6d6 --- a/Notifications for YouTube/NYTAppDelegate.m +++ b/Notifications for YouTube/NYTAppDelegate.m @@ -8,11 +8,263 @@ #import "NYTAppDelegate.h" -@implementation NYTAppDelegate +// Core Imports +#import "NYTAuthentication.h" +#import "NYTUpdateManager.h" + +// Other Imports +#import "NumberValueTransformer.h" +#import "NYTRulesWindowController.h" + +@interface NYTAppDelegate () + +// Outlets etc. +@property (weak) IBOutlet NSButton *loginButton; +@property (readwrite, strong) NSStatusItem *statusItem; + +// Login +@property (readwrite) BOOL loggingIn; + +@end + +// Notifications used for KVO +static void* const NYTLoginChangedNotification = @"NYTLoginChangedNotification"; +static void* const NYTUpdateManagerPropertyDidChangeNotification = @"NYTUpdateManagerPropertyDidChangeNotification"; + +@implementation NYTAppDelegate { + NYTRulesWindowController *_rulesController; +} + ++ (void)initialize +{ + [super initialize]; + // Register our value transformers + [NSValueTransformer setValueTransformer:[NumberValueTransformer new] forName:@"NumberValueTransformer"]; +} + +- (void)dealloc +{ + // Make sure this object gets removed from any observed object + NYTUpdateManager *manager = [NYTUpdateManager sharedManager]; + [manager removeObserver:self forKeyPath:@"autoRefreshEnabled"]; + [manager removeObserver:self forKeyPath:@"autoRefreshInterval"]; + [manager removeObserver:self forKeyPath:@"coalescesNotifications"]; + [manager removeObserver:self forKeyPath:@"maximumVideoAge"]; + [[NYTAuthentication sharedAuthentication] removeObserver:self forKeyPath:@"isLoggedIn"]; +} + +#pragma mark - Application Delegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - // Insert code here to initialize your application + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NYTUpdateManager *manager = [NYTUpdateManager sharedManager]; + NYTAuthentication *auth = [NYTAuthentication sharedAuthentication]; + + // Configure the User Defaults + NSDictionary *userDefaults = @{@"autoRefreshEnabled": @YES, @"autoRefreshInterval": @600, @"coalescesNotifications": @YES, @"maximumVideoAge": @3600}; + [defaults registerDefaults:userDefaults]; + + // Configure Notification Centers + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + // Notification Startup + NSUserNotification *startNotification = aNotification.userInfo[NSApplicationLaunchUserNotificationKey]; + if (startNotification) { + [self userNotificationCenter:[NSUserNotificationCenter defaultUserNotificationCenter] didActivateNotification:startNotification]; + } + + // Configure the status item + self.statusItemVisible = YES; + + // Configure the login (and log in). Do so before configuring notifications or auto refresh because this can take a while + [auth addObserver:self forKeyPath:@"isLoggedIn" options:0 context:NYTLoginChangedNotification]; + self.loggingIn = YES; + [auth tryLoginFromKeychainWithCompletionHandler:^{ + self.loggingIn = NO; + // If there is no login in the keychain show the preferences + if (!auth.isLoggedIn) { + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + [self.window makeKeyAndOrderFront:self]; + } + }]; + + // Configure the update manager + manager.maximumVideoAge = [defaults integerForKey:@"maximumVideoAge"]; + manager.coalescesNotifications = [defaults boolForKey:@"coalescesNotifications"]; + manager.autoRefreshInterval = [defaults integerForKey:@"autoRefreshInterval"]; + [manager addObserver:self forKeyPath:@"maximumVideoAge" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification]; + [manager addObserver:self forKeyPath:@"coalescesNotifications" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification]; + [manager addObserver:self forKeyPath:@"autoRefreshInterval" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification]; + + // Configure (Auto) Refresh + manager.autoRefreshEnabled = [defaults boolForKey:@"autoRefreshEnabled"]; + [manager addObserver:self forKeyPath:@"autoRefreshEnabled" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification]; +} + +- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag +{ + // Open the preferences on reopen (if no other window is visible) + if (!flag) { + [self.window makeKeyAndOrderFront:nil]; + } + return YES; +} + +- (void)applicationWillTerminate:(NSNotification *)notification +{ + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +#pragma mark - User Notification Center Delegate + +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + // Show the URL + NSString *videoURL = notification.userInfo[NYTUserNotificationURLKey]; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:videoURL]]; + // Remove the notification + [center removeDeliveredNotification:notification]; +} + +- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification +{ + return YES; +} + +#pragma mark - +#pragma mark *** Handling the Status Item *** + +- (void)setStatusItemVisible:(BOOL)visible +{ + if (visible) { + self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + //NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:@"(4)"]; + //[title addAttribute:NSStrokeWidthAttributeName value:@-5.0 range:NSMakeRange/*(3, 4)*/(1, 1)]; + //self.statusItem.attributedTitle = title; + self.statusItem.image = [NSImage imageNamed:@"play-icon-bw"]; + self.statusItem.alternateImage = [NSImage imageNamed:@"play-icon-s"]; + self.statusItem.menu = self.statusMenu; + self.statusItem.highlightMode = YES; + } else { + [[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem]; + self.statusItem = nil; + } +} + +- (BOOL)isStatusItemVisible +{ + return self.statusItem != nil; +} + +#pragma mark *** Handling Login *** + +- (IBAction)login:(id)sender { + if ([NYTAuthentication sharedAuthentication].isLoggedIn) { + // Logout: Present a sheet giving the following options: Logout, Cancel, Switch user + NSString *title = NSLocalizedString(@"Do you want to log out or switch to another user?", nil); + NSString *switchUser = NSLocalizedString(@"Switch User…", nil); + NSString *logout = NSLocalizedString(@"Logout", nil); + NSString *cancel = NSLocalizedString(@"Cancel", nil); + NSString *msg = NSLocalizedString(@"If you logout the login stored in your keychain will be removed. You would have to log in agin the next time you want to use Notifications for YouTube.", nil); + NSBeginAlertSheet(title, switchUser, logout, cancel, self.window, self, NULL, @selector(sheetDidDismiss:returnCode:contextInfo:), NULL, @"%@", msg); + } else { + [self doLogin]; + } +} + +- (void)sheetDidDismiss:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo +{ + if (returnCode == NSAlertDefaultReturn) { + // Switch User + [self doLogin]; + } else if (returnCode == NSAlertAlternateReturn) { + // Logout: Redirect to Update Manager + [[NYTAuthentication sharedAuthentication] discardLogin]; + } +} + +- (void)doLogin +{ + // Login: Redirect to Update Manager + self.loggingIn = YES; + [[NYTAuthentication sharedAuthentication] beginLoginSheetForWindow:self.window completionHandler:^(NSError *error) { + if (error) { + // Show the error as sheet if possible + if (self.window.isVisible) { + [NSApp presentError:error modalForWindow:self.window delegate:nil didPresentSelector:NULL contextInfo:NULL]; + } else { + [NSApp presentError:error]; + } + } + self.loggingIn = NO; + }]; +} + +#pragma mark *** Handling Refreshing *** + +- (IBAction)performRefresh:(id)sender { + // Refresh with notifications + [[NYTUpdateManager sharedManager] refreshFeedsWithErrorHandler:^(NSError *error) { + // Show the error if the refresh was triggered manually + // Possibly show it in a modal sheet otherwise a modal window + if (error) { + if ([sender respondsToSelector:@selector(window)] && [sender window].isVisible) { + NSAlert *alert = [NSAlert alertWithError:error]; + [alert beginSheetModalForWindow:[sender window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL]; + } else { + [NSApp presentError:error]; + } + } + } notify:YES]; +} + +#pragma mark *** Configure Notifications *** + +- (IBAction)editRules:(id)sender { + _rulesController = [[NYTRulesWindowController alloc] init]; + [NSApp beginSheet:_rulesController.window modalForWindow:self.window modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:NULL]; +} + +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + // For efficiency reasons + _rulesController = nil; +} + +#pragma mark *** Other Actions *** + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == NYTUpdateManagerPropertyDidChangeNotification) { + id value = change[NSKeyValueChangeNewKey]; + [[NSUserDefaults standardUserDefaults] setObject:value forKey:keyPath]; + }else if (context == NYTLoginChangedNotification) { + // Manage the title of the login/logout button based on wether login information is present (using KVO) + if ([NYTAuthentication sharedAuthentication].isLoggedIn) { + self.loginButton.title = NSLocalizedString(@"Logout…", nil); + } else { + self.loginButton.title = NSLocalizedString(@"Login…", nil); + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (IBAction)showPreferenceWindow:(id)sender +{ + // Activate the app first + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + [self.window makeKeyAndOrderFront:sender]; + [self.window makeMainWindow]; +} + +- (IBAction)showAboutPanel:(id)sender { + [NSApp activateIgnoringOtherApps:YES]; + [NSApp orderFrontStandardAboutPanel:sender]; +} + +- (IBAction)browseYouTube:(id)sender { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.youtube.com/"]]; } @end diff --git a/Notifications for YouTube/NYTAuthentication.h b/Notifications for YouTube/NYTAuthentication.h new file mode 100755 index 0000000..084373f --- /dev/null +++ b/Notifications for YouTube/NYTAuthentication.h @@ -0,0 +1,60 @@ +// +// NYTAuthentication.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +@class GTMOAuth2Authentication, NYTUser; + +extern NSString * const DeveloperKey; +extern NSString * const DeveloperKeyHTTPHeaderField; +extern NSString * const ClientID; +extern NSString * const ClientSecret; +extern NSString * const KeychainItemName; + +/* Authentication manages (as the name suggests) authentication. For any operation (login, logout, ...) you should prompt this class. There are also KVO compilant properties that allow you to observe the login and the logged in user. + */ +@interface NYTAuthentication : NSObject + +#pragma mark *** Initialization *** + +/* There is only one instance of this class at runtime. After the first instance was created with any of the initializing methods any further calls to any of them will just return the already created instance. + */ ++ (NYTAuthentication *)sharedAuthentication; +- (id)init; + +#pragma mark *** Login *** + +/* Trys to get all information possible from the keychain item. If any error occurs or there is no keychain item this method does nothing. This method returns immediately. + */ +- (void)tryLoginFromKeychainWithCompletionHandler:(void(^)(void))handler; + +/* Presents a modal sheet for the given window prompting the user to log in to permit Notifications for YouTube the access to his subscriptions. If the login process fails the completion handler will contain an appropriate error object. + */ +- (void)beginLoginSheetForWindow:(NSWindow *)window completionHandler:(void (^)(NSError *))handler; + +#pragma mark *** Logout *** + +/* Discards the current login information and thus logs the user out. This method makes sure that any information of the currently logged in user is discarded (as the keychain entry). Use this as a log out method. You do not have to log out before you log in with a new user. + */ +- (void)discardLogin; + +#pragma mark *** Authentication and Login Information *** + +/* The actual authentication that backs the NYTAuthentication. May be nil if no user is currently logged in. + */ +@property (readonly) GTMOAuth2Authentication *authentication; + +/* Returns YES if there is currently a user logged in. Always prefer this property over a nil check to loggedInUser because the loggedInUser property may not be updated properly at the time you query this property. + If this property is YES the loggedInUser property is set to the user that is currently logged in. + You may want to observe this property to get informed when the user logs in and out. + */ +@property (readonly) BOOL isLoggedIn; + +/* Returns the currently logged in user or nil if there is none. This property can be observed to get notified when the user changes. + */ +@property (readonly) NYTUser *loggedInUser; + +@end diff --git a/Notifications for YouTube/NYTAuthentication.m b/Notifications for YouTube/NYTAuthentication.m new file mode 100755 index 0000000..b5bb847 --- /dev/null +++ b/Notifications for YouTube/NYTAuthentication.m @@ -0,0 +1,172 @@ +// +// NYTAuthentication.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTAuthentication.h" + +// API Imports +#import "OAuth2.h" + +// Core Imports +#import "NYTUser.h" +#import "NYTUtil.h" +#import "NYTUpdateManager.h" + +// Google Access Constants +NSString * const DeveloperKey = @"key=AI39si6ubNmTeFzFTFz27hFn6r-51knz0_CDWvqgyL-nVKBcFWu5C_c46TWQcHTwwK9bOBlrhdlKatyEqjEnJhZd2dMB7UgFhQ"; +NSString * const DeveloperKeyHTTPHeaderField = @"X-GData-Key"; +NSString * const ClientID = @"908431532405.apps.googleusercontent.com"; +NSString * const ClientSecret = @"hKcyoNAzEIuHZaz0pdTUBMBv"; +NSString * const KeychainItemName = @"Notifications for YouTube"; + +@interface NYTAuthentication () + +@property (readwrite) GTMOAuth2Authentication *authentication; +@property (readwrite) NYTUser *loggedInUser; + +@end + +@implementation NYTAuthentication + +#pragma mark *** Initialization *** + +static NYTAuthentication *sharedAuthentication = nil; + ++ (NYTAuthentication *)sharedAuthentication +{ + if (!sharedAuthentication) { + sharedAuthentication = [[self alloc] init]; + } + return sharedAuthentication; +} + +- (id)init +{ + // Allow exactly one instance at all (for any way of initialization) + if (sharedAuthentication) { + return sharedAuthentication; + } + self = [super init]; + if (self) { + sharedAuthentication = self; + } + return self; +} + +#pragma mark *** KVO/KVC Additions to Properties *** + ++ (NSSet *)keyPathsForValuesAffectingIsLoggedIn +{ + return [NSSet setWithObjects:@"authentication", nil]; +} + +#pragma mark *** Login *** + +- (void)tryLoginFromKeychainWithCompletionHandler:(void(^)(void))handler +{ + // Get keychain data + GTMOAuth2Authentication *auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:KeychainItemName clientID:ClientID clientSecret:ClientSecret]; + if (auth.canAuthorize) { + // Possible success (keychain) + // Login (to refresh tokens/validate values) in background + dispatch_queue_t prevQueue = dispatch_get_current_queue(); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Try logging in + NSError *error = [self completeLoginWithAuthentication:auth]; + dispatch_async(prevQueue, ^{ + if (!error) { + // We suceeded so update the authentication object properly + self.authentication = auth; + // Refresh the feeds without notifications to set the 'known' state + [[NYTUpdateManager sharedManager] refreshFeedsWithErrorHandler:NULL notify:NO]; + } + if (handler) { + handler(); + } + }); + }); + } else { + // Failed (keychain) + if (handler) { + handler(); + } + } +} + +- (void)beginLoginSheetForWindow:(NSWindow *)window completionHandler:(void (^)(NSError *))handler +{ + [[NYTUpdateManager sharedManager] pauseAutoRefreshing]; + + // Show the login window + GTMOAuth2WindowController *windowController = [[GTMOAuth2WindowController alloc] initWithScope:@"https://gdata.youtube.com" clientID:ClientID clientSecret:ClientSecret keychainItemName:KeychainItemName resourceBundle:nil]; + [windowController signInSheetModalForWindow:window completionHandler:^(GTMOAuth2Authentication *auth, NSError *error) { + if (error) { + // If an error occured just return + if (handler) { + handler(error); + } + [[NYTUpdateManager sharedManager] resumeAutoRefreshing]; + } else { + // Complete the login asynchronously (because we are very likely currently operating on the main thread) + dispatch_queue_t prevQueue = dispatch_get_current_queue(); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Complete the login + NSError *error = [self completeLoginWithAuthentication:auth]; + + // Handle the error on the caller's queue + dispatch_async(prevQueue, ^{ + if (!error) { + self.authentication = auth; + [[NYTUpdateManager sharedManager] refreshFeedsWithErrorHandler:NULL notify:NO]; + } + if (handler) { + handler(error); + } + [[NYTUpdateManager sharedManager] resumeAutoRefreshing]; + }); + }); + } + }]; +} + +- (NSError *)completeLoginWithAuthentication:(GTMOAuth2Authentication *)auth +{ + // Get the user's data + NSError *error; + static NSString *userPageURL = @"https://gdata.youtube.com/feeds/api/users/default"; + NSXMLDocument *document = LoadXMLDocumentSynchronous(userPageURL, auth, &error); + if (!document) { + return error; + } + + // Parse the data and store them app-friendly + NYTUser *user = [[NYTUser alloc] initWithUserProfilePage:document]; + dispatch_async(dispatch_get_main_queue(), ^{ + // Set on the main queue due to bindings relations + self.loggedInUser = user; + }); + return nil; +} + +#pragma mark *** Logout *** + +- (void)discardLogin +{ + // Also remove from keychain + [GTMOAuth2WindowController removeAuthFromKeychainForName:KeychainItemName]; + self.authentication = nil; + self.loggedInUser = nil; +} + +#pragma mark *** Login Information *** + +- (BOOL)isLoggedIn +{ + return self.authentication != nil && self.authentication.canAuthorize; +} + +@end diff --git a/Notifications for YouTube/NYTChannelRestriction.h b/Notifications for YouTube/NYTChannelRestriction.h new file mode 100755 index 0000000..318f2bd --- /dev/null +++ b/Notifications for YouTube/NYTChannelRestriction.h @@ -0,0 +1,44 @@ +// +// NYTChannelRestrictions.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +@class NYTUser; + +/* A channel restriction contains information about the rules that apply to a specific channel. Like a predicate a restriction eventually evaluates to YES or NO giving information wether a specific video should be included in the notifications or not. + You should be aware that encoding a restriction will NOT encode the associated user. Since every channel restriction belongs to one user it is recommended that you store any restrictions as the value for a key that identifies the user (preferably the user id or the channel id). + */ +@interface NYTChannelRestriction : NSObject + +/* Initializes a restriction. By default a restriction always evaluates to YES. + */ +- (id)init; + +/* The user this restriction belongs to. This property is not necessary but it may make things easier if you, say, want to bind restrictions to a user interface. + */ +@property NYTUser *user; + +/* If YES any other properties are ignored and this restriction just evaluates to NO. + */ +@property BOOL disableAllNotifications; + +/* Specifies the way of interpretation of the predicate. If this is YES a positive predicate evaluation means that the video should be included in the notifications, if this is NO a positive predicate evaluation means that the video should NOT be included. The default value is YES. + */ +@property BOOL positivePredicate; + +/* The predicate that is used to evaluate this restriction. The object this predicate eventually should be evaluated with should be an instance of NYTVideo. + */ +@property NSPredicate *predicate; + +/* A localized string that summarizes this restriction. It can be "Show all", "Restricted" or "Show none". + */ +- (NSString *)localizedRestrictionSummary; + +/* Returns YES if this predicate evaluates to the same value as a newly created restriction would. You can use this method to efficiently store only those restrictions that are NOT the default. + */ +- (BOOL)differsFromDefaultValues; + +@end diff --git a/Notifications for YouTube/NYTChannelRestriction.m b/Notifications for YouTube/NYTChannelRestriction.m new file mode 100755 index 0000000..34d3dac --- /dev/null +++ b/Notifications for YouTube/NYTChannelRestriction.m @@ -0,0 +1,69 @@ +// +// NYTChannelRestrictions.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTChannelRestriction.h" + +// Core Imports +#import "NYTUtil.h" + +@implementation NYTChannelRestriction + ++ (NSSet *)keyPathsForValuesAffectingLocalizedRestrictionSummary +{ + return [NSSet setWithObjects:@"disableAllNotifications", @"predicate", nil]; +} + +- (id)init +{ + self = [super init]; + if (self) { + // Default values + _positivePredicate = YES; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [self init]; + if (self) { + // Do not decode the user + _disableAllNotifications = [aDecoder decodeBoolForKey:@"Disable All Notifications"]; + _predicate = [aDecoder decodeObjectForKey:@"Predicate"]; + _positivePredicate = [aDecoder decodeBoolForKey:@"Positive Predicate"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + // Do not encode the user + [aCoder encodeBool:self.disableAllNotifications forKey:@"Disable All Notifications"]; + [aCoder encodeObject:self.predicate forKey:@"Predicate"]; + [aCoder encodeBool:self.positivePredicate forKey:@"Positive Predicate"]; +} + +- (NSString *)localizedRestrictionSummary +{ + if (self.disableAllNotifications) { + return NSLocalizedString(@"No notifications", nil); + } else { + if (!self.predicate) { + return NSLocalizedString(@"Show all notifications", nil); + } else { + return NSLocalizedString(@"Restrict notifications", nil); + } + } +} + +- (BOOL)differsFromDefaultValues +{ + return self.disableAllNotifications == YES || self.predicate != nil; +} + +@end diff --git a/Notifications for YouTube/NYTRulesWindowController.h b/Notifications for YouTube/NYTRulesWindowController.h new file mode 100755 index 0000000..9bbc323 --- /dev/null +++ b/Notifications for YouTube/NYTRulesWindowController.h @@ -0,0 +1,32 @@ +// +// NYTRulesWindowController.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +/* The rules window controller manages a window that lets the user change rules for notifications. + */ +@interface NYTRulesWindowController : NSWindowController + +/* This property is YES as long as this controller loads the user's subscriptions. The loading process starts immediately after the window has been loaded. + */ +@property (readonly) BOOL loading; + +/* Contains the user's subscriptions (after they have been loaded, before that this property is nil). An entry in this array is an instance of NYTChannelRestriction. + Both of these arrays are fully KVO and KVC compilant. + */ +@property (readonly) NSArray *subscriptions; + +/* Convenience actions for the user triggered from the interface: Enable/Disable all and search. + */ +- (IBAction)enableAllNotifications:(id)sender; +- (IBAction)disableAllNotifications:(id)sender; + +/* Finish the editing of rules in either way. + */ +- (IBAction)save:(id)sender; +- (IBAction)cancel:(id)sender; + +@end diff --git a/Notifications for YouTube/NYTRulesWindowController.m b/Notifications for YouTube/NYTRulesWindowController.m new file mode 100755 index 0000000..444f87d --- /dev/null +++ b/Notifications for YouTube/NYTRulesWindowController.m @@ -0,0 +1,188 @@ +// +// NYTRulesWindowController.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTRulesWindowController.h" + +// Core Imports +#import "NYTUser.h" +#import "NYTUtil.h" +#import "NYTAuthentication.h" + +// Other Imports +#import "NYTChannelRestriction.h" + +@interface NYTRulesWindowController () + +@property (readwrite) BOOL loading; +@property (readwrite) NSArray *subscriptions; + +/* The restrictions previously stored in the user defaults + */ +@property NSDictionary *savedRestrictions; + +@end + +@implementation NYTRulesWindowController + +/* Convenient init + */ +- (id)init +{ + return [self initWithWindowNibName:@"NYTRulesWindowController" owner:self]; +} + +- (void)windowDidLoad +{ + [super windowDidLoad]; + + // Load the subscriptions + [self loadSubscriptions]; +} + +#pragma mark *** Loading *** + +- (void)loadSubscriptions +{ + self.loading = YES; + + // Load in background to not block the main thread + dispatch_queue_t prevQueue = dispatch_get_current_queue(); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Prepare for multiple pages + NSError *error; + NSMutableArray *loadedSubscriptions; + NSUInteger processedResults = 0; + NSUInteger totalResults = 0; + + do { + // We are creating a couple of objects so setup a separate autoreleasepool + @autoreleasepool { + // Load the document + NSString *url = [NSString stringWithFormat:@"https://gdata.youtube.com/feeds/api/users/default/subscriptions?max-results=50&start-index=%li&v=2", processedResults+1]; + NSXMLDocument *document = LoadXMLDocumentSynchronous(url, [NYTAuthentication sharedAuthentication].authentication, &error); + + // Store the entries as app-friendly video objects + if (document) { + + // Set up the collection for collecting the videos + if (processedResults == 0) { + static NSString *const totalResultsPath = @"./feed/openSearch:totalResults"; + NSXMLElement *totalResultsElement = [document nodesForXPath:totalResultsPath error:nil][0]; + totalResults = (NSUInteger) totalResultsElement.stringValue.integerValue; + loadedSubscriptions = [NSMutableArray arrayWithCapacity:totalResults]; + } + + // For each page parse its results and add them to the collection + static NSString *const resultsPerPagePath = @"./feed/openSearch:itemsPerPage"; + NSXMLElement *resultsPerPageElement = [document nodesForXPath:resultsPerPagePath error:nil][0]; + int resultsPerPage = resultsPerPageElement.stringValue.intValue; + + processedResults += resultsPerPage; + NSArray *pageSubscriptions = [self subscriptionsOnPage:document error:&error]; + if (pageSubscriptions) { + [loadedSubscriptions addObjectsFromArray:pageSubscriptions]; + } else { + // Abort on error + [self abortLoadingOnQueue:prevQueue withError:error]; + return; + } + } else { + // Abort on error + [self abortLoadingOnQueue:prevQueue withError:error]; + return; + } + } + } while (processedResults < totalResults); + + // Finish the loading process on the main thread + dispatch_async(prevQueue, ^{ + self.subscriptions = loadedSubscriptions; + self.loading = NO; + }); + }); +} + +- (NSArray *)subscriptionsOnPage:(NSXMLDocument *)document error:(NSError **)error +{ + // Set up the stored restrictions (but just once) + if (!self.savedRestrictions) { + self.savedRestrictions = [[[NSUserDefaults standardUserDefaults] objectForKey:@"Rules"] mutableCopy]; + if (!self.savedRestrictions) { + self.savedRestrictions = [NSMutableDictionary new]; + } + } + + // For every entry parse a subscription + NSArray *entries = [document nodesForXPath:@"./feed/entry" error:nil]; + NSMutableArray *subscriptions = [NSMutableArray arrayWithCapacity:entries.count]; + for (NSXMLNode *entry in entries) { + NYTUser *user = [[NYTUser alloc] initWithSubscriptionEntry:entry]; + + // Have a restriction object available for each subscription. If there is none stored create a default one. + NSData *restrictionData = self.savedRestrictions[user.channelID]; + NYTChannelRestriction *restriction; + if (restrictionData) { + restriction = [NSKeyedUnarchiver unarchiveObjectWithData:restrictionData]; + } else { + restriction = [[NYTChannelRestriction alloc] init]; + } + restriction.user = user; + [subscriptions addObject:restriction]; + } + return subscriptions; +} + +- (void)abortLoadingOnQueue:(dispatch_queue_t)queue withError:(NSError *)error +{ + dispatch_async(queue, ^{ + [NSApp presentError:error modalForWindow:self.window delegate:self didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) contextInfo:NULL]; + }); +} + +- (void)didPresentErrorWithRecovery:(BOOL)didRecover contextInfo:(void *)contextInfo +{ + if (!didRecover) { + [self cancel:nil]; + } +} + +- (IBAction)enableAllNotifications:(id)sender { + // Really enable all (also clearing the predicate) + for (NYTChannelRestriction *restriction in self.subscriptions) { + restriction.disableAllNotifications = NO; + restriction.predicate = nil; + } +} + +- (IBAction)disableAllNotifications:(id)sender { + for (NYTChannelRestriction *restriction in self.subscriptions) { + restriction.disableAllNotifications = YES; + } +} + +- (IBAction)save:(id)sender { + // Encode and save the restrictions that are not default values. + NSMutableDictionary *restrictsToSave = [NSMutableDictionary new]; + for (NYTChannelRestriction *restrictions in self.subscriptions) { + if (restrictions.differsFromDefaultValues) { + restrictsToSave[restrictions.user.channelID] = [NSKeyedArchiver archivedDataWithRootObject:restrictions]; + } + } + [[NSUserDefaults standardUserDefaults] setObject:restrictsToSave forKey:@"Rules"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + // Hide this window + [self cancel:sender]; +} + +- (IBAction)cancel:(id)sender { + // Just hide the window without saving + [self.window orderOut:sender]; + [NSApp endSheet:self.window]; +} +@end diff --git a/Notifications for YouTube/NYTUpdateManager.h b/Notifications for YouTube/NYTUpdateManager.h new file mode 100755 index 0000000..4561854 --- /dev/null +++ b/Notifications for YouTube/NYTUpdateManager.h @@ -0,0 +1,84 @@ +// +// NYTUpdater.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 09.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +/* Defines a key in the userInfo dictionary of an NSUserNotification that can be used to receive an URL (in the form of a string) that represents the video the notification was sent for. + */ +extern NSString * const NYTUserNotificationURLKey; + +@class NYTUser; + +/* The NYTUpdater class is the core class to refresh the upload feeds of the channels defined in the NYTSubscriptionManager singletone class. This class is also responsible for generating appropriate user notifications and presenting them to the user. + If you want to create you own subclass of update manager you must make sure that it's instance is created before the default one gets created. + */ +@interface NYTUpdateManager : NSObject + +#pragma mark *** Obtaining the Shared Instance *** + +/* Use this method to obtain the shared instance. The init message will only be sent once. Any further sharedUpdater or init messages will just return the shared instance. + */ ++ (NYTUpdateManager *)sharedManager; +- (id)init; + +#pragma mark *** Refreshing Feeds *** + +/* A KVO compilant property telling you wether there is currently an update in progress or not. + */ +@property (readonly) BOOL refreshing; + +/* Returns wether a refresh is currently possible. Always query this status before performing a refresh because otherwise your app might crash if there is no user currently logged in. + */ +- (BOOL)canRefresh; + +/* Refreshes the feeds in the background and notifies the handler about any errors occured. If no error occured the handler is NOT invoked. If you want to know when the refresh process finished you can observe on the refreshing property for changes using KVO. + If the update manager is already refreshing this method does nothing and returns immediately. The error handler is NOT invoked. + You can specify a notify flag. If this flag is set to YES the update manger delivers a user notification for each new video. You may want to specify NO to set the current videos as the 'root' position, that is that these videos are 'known'. This is automatically done when the login changes (otherwise there could be a notification for each of the 98 videos in the feed). + */ +- (void)refreshFeedsWithErrorHandler:(void (^)(NSError *))handler notify:(BOOL)flag; + +#pragma mark *** Notifications *** + +/* Note to Notification restrictions: Notifications may also be restricted by the user-defined rules. The rules are stored in the user defaults for the key "Rules". For information on the format of rules see NYTRulesWindowController. + */ + +/* When refreshing feeds notifications will only be sent for feeds that are in the specified date range from now to a date that is offset to the past by this value. + Specify 0 to not prevent notifications by time. Negative values will be evaluated as 0. + */ +@property NSTimeInterval maximumVideoAge; + +/* If set to YES multiple video uploads will be coalesced into a single notification in the form of 'xy new videos' - 'abc, def, ghi and 42 more uploaded new videos'. The NYTUserNotificationURLKey value will not be a video URL but to an URL to the user's subscription video list on YouTube. + */ +@property BOOL coalescesNotifications; + +#pragma mark *** Auto Refresh *** + +/* Enabling and disabling auto refresh: You can turn on and off auto refresh permanently. This means that the auto refresh timer will be stopped and not be started again until auto refresh is re-enabled. While auto refresh is disabled pausing and resuming is still usable. Enabling auto refresh while it is paused will not start auto refresh. Auto refresh will be actually started when the same number of resume messages as pause messages were sent. You can still modify and query the refresh interval while auto refresh is disabled. + While auto refresh is running the timer is also running and firing events. But if canRefresh returns NO auto refresh will not perform a refresh but just do nothing. (It will not stop the timer.) + */ +@property (getter = isAutoRefreshEnabled) BOOL autoRefreshEnabled; + +/* Restarts auto refresh. If auto refresh is currently disabled this method has no effect. Use this method if you want the next auto refresh update to be fired in the amount of seconds specified by the auto refresh interval. + */ +- (void)restartAutoRefresh; + +/* These methods pause and resume the auto refresh timer. The autoRefreshRunning property tells you wether the auto refresh timer is currently running (which means is will be set to NO if auto refresh is paused). + Pausing and resuming auto refresh can be useful if you program enters a critical section during whose execution there should be no refresh (for example during modifying the update settings). Calls of pauseAutoRefreshing and resumeAutoRefreshing can be nested. Every pause message must have a corresponding resume message and vice versa. Auto refreshing will not be resumed until the same amount of resume messages were sent as pause messages were sent before. + Pausing and resuming auto refresh is used by the update manager at several times (for example while the login/logout process is running). Do not suggest wether auto refreshing is currently running or paused based on your code. Always query the current staus by sending a isAutoRefreshRunning message before. + */ +@property (readonly, getter = isAutoRefreshRunning) BOOL autoRefreshRunning; +- (void)pauseAutoRefreshing; +- (void)resumeAutoRefreshing; + +/* Performs an auto refresh. Although this method returns immediately it may not immediately refresh. If this method is invoked while auto refresh is "sleeping" the next time auto refresh will get actually started (as determined by isAutoRefreshRunning) it will trigger a refresh immediately. If auto refresh gets disabled after this method gets invoked or already is disabled by now nothing will happen. + */ +- (void)performAutoRefresh:(id)sender; + +/* The time interval used by auto refresh to refresh the feeds. Specified in seconds. If you change this property the elapsed time is transfered to the new interval. If the new interval is smaller than the elapsed time auto refresh refreshes immediately. Specify 0 to refresh as often as possible (may be very inefficient since Auto Refresh is constructed to refresh over time. + */ +@property (nonatomic) NSTimeInterval autoRefreshInterval; + +@end diff --git a/Notifications for YouTube/NYTUpdateManager.m b/Notifications for YouTube/NYTUpdateManager.m new file mode 100755 index 0000000..9bee0f6 --- /dev/null +++ b/Notifications for YouTube/NYTUpdateManager.m @@ -0,0 +1,436 @@ +// +// NYTUpdater.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 09.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTUpdateManager.h" + +// Core Imports +#import "NYTVideo.h" +#import "NYTUtil.h" +#import "NYTAuthentication.h" + +// Other Imports +#import "NYTChannelRestriction.h" + +/* User notifications sent by this class will contain this key in their userInfo dictionary. The value will be an NSString object representing an URL that can be opened to show the complete contents of the notification. These contents are either a video page or (for coalesced notifications) the user's "my subscriptions" page. + */ +NSString * const NYTUserNotificationURLKey = @"NYTUserNotificationURLKey"; + +@interface NYTUpdateManager () + +// Refreshing +@property (readwrite) BOOL refreshing; + +// Caching Refresh Data +@property NSMutableSet *knownVideos; +@property NSDate *lastRefreshDate; // Only for Auto Refresh + +// Auto Refreshing +@property NSTimer *autoRefreshTimer; +@property BOOL autoRefreshWasDisabled; +@property NSInteger autoRefreshPauseCount; +@property BOOL refreshOnResume; + +@end + +@implementation NYTUpdateManager + +@synthesize autoRefreshEnabled = _autoRefreshEnabled; + +#pragma mark *** Obtaining the Shared Instance *** + +static NYTUpdateManager *sharedManager = nil; + ++ (NYTUpdateManager *)sharedManager +{ + if (!sharedManager) { + sharedManager = [[self alloc] init]; + } + return sharedManager; +} + +- (id)init +{ + // Allow exactly one instance at all (for any way of initialization) + if (sharedManager) { + return sharedManager; + } + self = [super init]; + if (self) { + sharedManager = self; + [[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(systemWillGoToSleep:) name:NSWorkspaceWillSleepNotification object:NULL]; + [[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(systemDidAwakeFromSleep:) name:NSWorkspaceDidWakeNotification object:NULL]; + } + return self; +} + +- (void)dealloc +{ + [self.autoRefreshTimer invalidate]; + [[NSWorkspace sharedWorkspace].notificationCenter removeObserver:self]; +} + +#pragma mark *** Refreshing Feeds *** + +- (BOOL)canRefresh +{ + // If logged in refreshing is possible + return [NYTAuthentication sharedAuthentication].isLoggedIn; +} + +- (void)refreshFeedsWithErrorHandler:(void (^)(NSError *))handler notify:(BOOL)flag +{ + self.refreshOnResume = NO; // Just for sure + if (self.refreshing) { + return; + } + + // Prepare for refreshing + self.refreshing = YES; + [self pauseAutoRefreshing]; + self.lastRefreshDate = [NSDate date]; // Since the refresh date is just for auto refresh it should be changed here to let auto refresh continue even if there was an error + + // Refresh on a background queue + dispatch_queue_t prevQueue = dispatch_get_current_queue(); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Prepare for paged feeds + NSError *error; + NSMutableSet *feedVideos; + NSUInteger processedResults = 0; + NSUInteger totalResults = 0; + + do { + // We are creating a couple of objects so setup a separate autoreleasepool + @autoreleasepool { + // Load the document + NSString *url = [NSString stringWithFormat:@"https://gdata.youtube.com/feeds/api/users/default/newsubscriptionvideos?max-results=50&start-index=%li&v=2", processedResults+1]; + NSXMLDocument *document = LoadXMLDocumentSynchronous(url, [NYTAuthentication sharedAuthentication].authentication, &error); + + // Store the entries as app-friendly video objects + if (document) { + + // Set up the collection for collecting the videos + if (processedResults == 0) { + static NSString *totalResultsPath = @"./feed/openSearch:totalResults"; + NSXMLElement *totalResultsElement = [document nodesForXPath:totalResultsPath error:nil][0]; + totalResults = (NSUInteger) totalResultsElement.stringValue.integerValue; + feedVideos = [NSMutableSet setWithCapacity:totalResults]; + } + + // For each page parse its results and add them to the collection + static NSString *resultsPerPagePath = @"./feed/openSearch:itemsPerPage"; + NSXMLElement *resultsPerPageElement = [document nodesForXPath:resultsPerPagePath error:nil][0]; + int resultsPerPage = resultsPerPageElement.stringValue.intValue; + + processedResults += resultsPerPage; + [feedVideos unionSet:[self videosOnPage:document]]; + } else { + // Abort on error + [self abortRefreshOnQueue:prevQueue withErrorHandler:handler usingError:error]; + return; + } + } + } while (processedResults < totalResults); + + // Deliver user notifications if wanted + if (flag) { + [self notifyForVideos:[self videosForNotifications:feedVideos]]; + } + + // Clean up + self.knownVideos = [NSMutableSet setWithCapacity:feedVideos.count]; + for (NYTVideo *video in feedVideos) { + // For memory efficiency just store the video IDs + [self.knownVideos addObject:video.videoID]; + } + // Resume on the caller's queue + dispatch_async(prevQueue, ^{ + self.refreshing = NO; + [self resumeAutoRefreshing]; + }); + }); +} + +- (void)abortRefreshOnQueue:(dispatch_queue_t)queue withErrorHandler:(void(^)(NSError *))handler usingError:(NSError *)error +{ + dispatch_async(queue, ^{ + if (handler) { + handler(error); + } + self.refreshing = NO; + [self resumeAutoRefreshing]; + }); +} + +- (NSSet *)videosOnPage:(NSXMLDocument *)document +{ + // Get the entries + static NSString *entryPath = @"./feed/entry"; + NSArray *entries = [document nodesForXPath:entryPath error:nil]; + NSMutableSet *videos = [NSMutableSet setWithCapacity:entries.count]; + + // Parse the entries + for (NSXMLNode *entry in entries) { + static NSString *mediaGroupPath = @"./media:group"; + NSXMLNode *mediaGroupNode = [entry nodesForXPath:mediaGroupPath error:nil][0]; + [videos addObject:[[NYTVideo alloc] initWithMediaGroupNode:mediaGroupNode]]; + } + return videos; +} + +#pragma mark - System Notifications + +- (void)systemWillGoToSleep:(NSNotification *)notification +{ + [self pauseAutoRefreshing]; +} + +- (void)systemDidAwakeFromSleep:(NSNotification *)notification +{ + [self resumeAutoRefreshing]; + [self performAutoRefresh:nil]; +} + +#pragma mark *** Notifications *** + +- (NSSet *)videosForNotifications:(NSSet *)allVideos +{ + // Prepare objects that are not dependant on videos + NSMutableSet *acceptedVideos = [[NSMutableSet alloc] init]; + NSDictionary *restrictions = [[NSUserDefaults standardUserDefaults] objectForKey:@"Rules"]; + NSTimeInterval maximumVideoAgeOffset = self.maximumVideoAge <= 0 ? 0 : -self.maximumVideoAge; + NSDate *minimumVideoUploadedDate = maximumVideoAgeOffset == 0 ? [NSDate distantPast] : [NSDate dateWithTimeIntervalSinceNow:maximumVideoAgeOffset]; + + // Enumerate all videos + for (NYTVideo *video in allVideos) { + // Get the video dependant objects + NSTimeInterval timeSinceMinimumUploadedDate = [video.uploadedDate timeIntervalSinceDate:minimumVideoUploadedDate]; + NSData *restrictionData = restrictions[video.uploaderID]; + NYTChannelRestriction *restriction = nil; + if (restrictionData) { + restriction = [NSKeyedUnarchiver unarchiveObjectWithData:restrictionData]; + } + + // Validate the video and accept it, if it passed the validation + if (timeSinceMinimumUploadedDate >= 0 && + ![self.knownVideos containsObject:video.videoID] && + [self acceptVideo:video withRestriction:restriction]) { + [acceptedVideos addObject:video]; + } else { + + } + } + + return acceptedVideos; +} + +- (BOOL)acceptVideo:(NYTVideo *)video withRestriction:(NYTChannelRestriction *)restriction +{ + // For efficiency restrictions are not stored if they only contain default values. The default values accept all videos. + if (!restriction) { + return YES; + } + if (restriction.disableAllNotifications) { + return NO; + } + BOOL predicateResult = [restriction.predicate evaluateWithObject:video]; + return restriction.positivePredicate == predicateResult; +} + +// Convenience macros +#define NYTCoalescedNotificationNumberOfIncludedChannels 3 +#define NYTCoalescedNotificationShouldIncludeAll(varName) (varName.count <= NYTCoalescedNotificationNumberOfIncludedChannels + 1) + +- (void)notifyForVideos:(NSSet *)videos +{ + // Should coalesce + if (videos.count > 1 && self.coalescesNotifications) { + + // Create the notification + NSUserNotification *notification = [[NSUserNotification alloc] init]; + notification.title = [NSString stringWithFormat:NSLocalizedString(@"%li new videos", nil), videos.count]; + + // Create the textual enumeration for the uploaders + NSMutableString *uploadersEnum = @"".mutableCopy; + NSSet *uploaders = [videos valueForKey:@"uploaderDisplayName"]; + NSInteger processed = 0; + for (NSString *uploader in uploaders) { + if (processed == 0) { + // The first uploader is just added as is + [uploadersEnum appendString:uploader]; + } else if (processed < NYTCoalescedNotificationNumberOfIncludedChannels + || NYTCoalescedNotificationShouldIncludeAll(uploaders)) { + if (processed == uploaders.count - 1) { + // This is the last one so offer a special separation text + [uploadersEnum appendFormat:NSLocalizedString(@" and %@", nil), uploader]; + } else { + [uploadersEnum appendFormat:NSLocalizedString(@", %@", nil), uploader]; + } + } else { + break; + } + processed++; + } + // Set the enum for the notification + NSString *informativeText; + if (NYTCoalescedNotificationShouldIncludeAll(uploaders)) { + // There are no "more" uploaders + informativeText = [NSString stringWithFormat:NSLocalizedString(@"New videos from %@", nil), uploadersEnum]; + } else { + // There are 2 or more "more" uploaders + informativeText = [NSString stringWithFormat:NSLocalizedString(@"New videos from %@ and %li more", nil), uploadersEnum, uploaders.count - NYTCoalescedNotificationNumberOfIncludedChannels]; + } + + // Finish the notification and deliver it + notification.informativeText = informativeText; + notification.userInfo = @{NYTUserNotificationURLKey: @"http://www.youtube.com/feed/subscriptions"}; + notification.soundName = NSUserNotificationDefaultSoundName; + dispatch_async(dispatch_get_current_queue(), ^{ + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; + }); + + } else { + for (NYTVideo *video in videos) { + // Configure the notification for every video + NSUserNotification *notification = [[NSUserNotification alloc] init]; + notification.title = video.uploaderDisplayName; + notification.informativeText = video.title; + notification.userInfo = @{NYTUserNotificationURLKey: video.URL}; + notification.deliveryDate = video.uploadedDate; + notification.soundName = NSUserNotificationDefaultSoundName; + // Just to make sure notifications get delivered on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; + }); + } + } +} + +#pragma mark *** Auto Refresh *** + +- (void)performAutoRefresh:(id)sender +{ + if (self.canRefresh && self.isAutoRefreshRunning) { + [self refreshFeedsWithErrorHandler:NULL notify:YES]; + } else { + self.refreshOnResume = YES; // Will be of no matter if auto refresh is disabled + } +} + +- (void)setAutoRefreshEnabled:(BOOL)autoRefreshEnabled +{ + if (autoRefreshEnabled) { + [self enableAutoRefresh]; + } else { + [self disableAutoRefresh]; + } +} + +- (void)enableAutoRefresh +{ + if (!self.autoRefreshEnabled) { + _autoRefreshEnabled = YES; + self.autoRefreshWasDisabled = YES; + [self startAutoRefreshTimerIfAppropriate:NO]; + } +} + +- (void)disableAutoRefresh +{ + if (self.autoRefreshEnabled) { + [self stopAutoRefreshTimer]; + _autoRefreshEnabled = NO; + } +} + +- (BOOL)isAutoRefreshEnabled +{ + return _autoRefreshEnabled; +} + +- (void)restartAutoRefresh +{ + [self stopAutoRefreshTimer]; + [self startAutoRefreshTimerIfAppropriate:YES]; +} + +- (BOOL)isAutoRefreshRunning +{ + return self.autoRefreshTimer != nil; +} + +- (void)pauseAutoRefreshing +{ + self.autoRefreshPauseCount++; + if (self.autoRefreshPauseCount == 1) { + [self stopAutoRefreshTimer]; + } +} + +- (void)resumeAutoRefreshing +{ + self.autoRefreshPauseCount--; + if (self.autoRefreshPauseCount == 0) { + [self startAutoRefreshTimerIfAppropriate:YES]; + } else if (self.autoRefreshPauseCount < 0) { + [NSException raise:@"AutoRefreshUnbalancedPauseException" format:@"Auto Refresh pause-resume messages are not balanced. Each pauseAutoRefreshing must be balanced with exactly one resumeAutoRefreshin message. The last resumeAutoRefreshing message did not have a corresponding pauseAutoRefreshing message."]; + } +} + +- (void)startAutoRefreshTimerIfAppropriate:(BOOL)resume +{ + // If auto refresh should be running right now + if (self.autoRefreshEnabled && self.autoRefreshPauseCount == 0) { + [self.autoRefreshTimer invalidate]; // Just to be sure + // Should we really resume or restart? + BOOL actualResume = resume && !self.autoRefreshWasDisabled; + self.autoRefreshWasDisabled = NO; + + // Auto refresh was paused by sending a pauseAutoRefreshing message and should now be resumed + if (actualResume) { + + // Get the time since the last update + NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:self.lastRefreshDate]; + + // If the current time interval requires an update based on the elapsed time refresh immediately and schedule the timer with the complete interval + if (self.refreshOnResume || elapsed >= self.autoRefreshInterval) { + self.autoRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:self.autoRefreshInterval target:self selector:@selector(performAutoRefresh:) userInfo:nil repeats:YES]; + [self.autoRefreshTimer fire]; // Fire immediately because the elapsed time since pausing the timer exceeds the refresh interval or we are forced to do so + } else { + + // Otherwise set the first fire date to the time point that is constitued by the current time interval and the last refresh date + NSDate *firstFireDate = [NSDate dateWithTimeInterval:self.autoRefreshInterval sinceDate:self.lastRefreshDate]; + self.autoRefreshTimer = [[NSTimer alloc] initWithFireDate:firstFireDate interval:self.autoRefreshInterval target:self selector:@selector(performAutoRefresh:) userInfo:nil repeats:YES]; + [[NSRunLoop currentRunLoop] addTimer:self.autoRefreshTimer forMode:NSDefaultRunLoopMode]; + } + } else { + + // Auto refresh was disabled since the timer was stopped so just start the timer. Refresh on resume does not have an impact here. + self.lastRefreshDate = [NSDate date]; + self.autoRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:self.autoRefreshInterval target:self selector:@selector(performAutoRefresh:) userInfo:nil repeats:YES]; + } + + // Cleanup + self.refreshOnResume = NO; // We already have passed the point where it's important + } +} + +- (void)stopAutoRefreshTimer +{ + [self.autoRefreshTimer invalidate]; + self.autoRefreshTimer = nil; +} + +- (void)setAutoRefreshInterval:(NSTimeInterval)autoRefreshInterval +{ + [self pauseAutoRefreshing]; + _autoRefreshInterval = autoRefreshInterval; + [self resumeAutoRefreshing]; +} + +@end diff --git a/Notifications for YouTube/NYTUser.h b/Notifications for YouTube/NYTUser.h new file mode 100755 index 0000000..7431009 --- /dev/null +++ b/Notifications for YouTube/NYTUser.h @@ -0,0 +1,48 @@ +// +// NYTUser.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 18.06.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +/* A NYTUser represents a user or a channel on YouTube. + */ +@interface NYTUser : NSObject + +#pragma mark *** Properties *** + +/* The display name of the user. Always use this property if you need to display a name to the user. This name is NOT unique. Use the userID or the channelID to uniquely identify a user or a channel respectively. + */ +@property (readonly) NSString *displayName; + +/* Uniquely identifies a user. Every user on youtube has a unique user id. + */ +@property (readonly) NSString *userID; + +/* Uniquely identifies a channel. Not every user has a channel. The channel id is always exactly the user id prefixed by @"UC". + */ +@property (readonly) NSString *channelID; + +/* The textual summary of the channel. This is sometimes referred to as the channel's description. You should display this in combination with the display name to the user to help him identify the channel he wants. + */ +@property (readonly) NSString *summary; + +/* The image URL is a network URL pointing to an image resource that is the user's image. + */ +@property (readonly) NSURL *imageURL; + +#pragma mark *** Initialization *** + +/* Parses a user from its profile page ("https://gdata.youtube.com/feeds/api/users/default"). Initializes displayName, userID, channelID and summary. + */ +- (id)initWithUserProfilePage:(NSXMLNode *)node; + +/* Parses a user from its subscription entry (e.g. "https://gdata.youtube.com/feeds/api/users/default/subscriptions"). Initializes displayNamem, userID, channelID and imageURL + */ +- (id)initWithSubscriptionEntry:(NSXMLNode *)entry; + +- (BOOL)isEqual:(id)object; +- (BOOL)isEqualToUser:(NYTUser *)user; + +@end diff --git a/Notifications for YouTube/NYTUser.m b/Notifications for YouTube/NYTUser.m new file mode 100755 index 0000000..61b6a04 --- /dev/null +++ b/Notifications for YouTube/NYTUser.m @@ -0,0 +1,86 @@ +// +// NYTUser.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 18.06.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTUser.h" + +@implementation NYTUser + +- (id)initWithUserProfilePage:(NSXMLNode *)node +{ + self = [super init]; + if (self) { + static NSString *const displayNamePath = @"./entry/author/name"; + static NSString *const userIDPath = @"./entry/yt:username"; + static NSString *const summaryPath = @"./entry/content"; + NSXMLElement *displayNameElement = [node nodesForXPath:displayNamePath error:nil][0]; + NSXMLElement *userIDElement = [node nodesForXPath:userIDPath error:nil][0]; + NSXMLElement *summaryElement = [node nodesForXPath:summaryPath error:nil][0]; + _displayName = displayNameElement.stringValue; + _userID = userIDElement.stringValue; + _channelID = [NSString stringWithFormat:@"UC%@", _userID]; + _summary = summaryElement.stringValue; + } + return self; +} + +- (id)initWithSubscriptionEntry:(NSXMLNode *)entry +{ + self = [super init]; + if (self) { + static NSString *const displayNamePath = @"./yt:username/@display"; + static NSString *const imagePath = @"./media:thumbnail/@url"; + static NSString *const channelIDPath = @"./yt:channelId"; + NSXMLElement *displayNameElement = [entry nodesForXPath:displayNamePath error:nil][0]; + NSXMLElement *imageElement = [entry nodesForXPath:imagePath error:nil][0]; + NSXMLElement *channelIDElement = [entry nodesForXPath:channelIDPath error:nil][0]; + _displayName = displayNameElement.stringValue; + _imageURL = [NSURL URLWithString:imageElement.stringValue]; + _channelID = channelIDElement.stringValue; + _userID = [_channelID substringFromIndex:2]; + } + return self; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + if (object == nil) { + return NO; + } + if (![object isKindOfClass:[NYTUser class]]) { + return NO; + } + return [self isEqualToUser:object]; +} + +- (BOOL)isEqualToUser:(NYTUser *)user +{ + if (self == user) { + return YES; + } + if (self.userID) { + if (user.userID) { + return [self.userID isEqualToString:user.userID]; + } else { + NSString *otherUserID = [user.channelID substringFromIndex:2]; // Remove the UC from channel ID + return [self.userID isEqualToString:otherUserID]; + } + } else if (self.channelID) { + if (user.channelID) { + return [self.channelID isEqualToString:user.channelID]; + } else { + NSString *userID = [self.channelID substringFromIndex:2]; // Remove the UC from channel ID + return [userID isEqualToString:user.userID]; + } + } + return NO; // Probably won't get here +} + +@end diff --git a/Notifications for YouTube/NYTUtil.h b/Notifications for YouTube/NYTUtil.h new file mode 100755 index 0000000..9046c94 --- /dev/null +++ b/Notifications for YouTube/NYTUtil.h @@ -0,0 +1,17 @@ +// +// NYTUtil.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +@class GTMOAuth2Authentication; + +/* Convenience macro + */ +#define NYTNull [NSNull null] + +/* Loads the contents of an URL synchronously. The URL request is authorized and authenticated for YouTube requests previously. If an error occurs nil is returned and outError is set to an appropriate error pointer. Otherwise outError is left unmodified. + */ +extern NSXMLDocument * LoadXMLDocumentSynchronous(NSString *url, GTMOAuth2Authentication *auth, NSError **outError); \ No newline at end of file diff --git a/Notifications for YouTube/NYTUtil.m b/Notifications for YouTube/NYTUtil.m new file mode 100755 index 0000000..939bd73 --- /dev/null +++ b/Notifications for YouTube/NYTUtil.m @@ -0,0 +1,52 @@ +// +// NYTUtil.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 21.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTUtil.h" + +// API Imports +#import "GTMOAuth2Authentication.h" + +// Core Imports +#import "NYTAuthentication.h" + +extern NSXMLDocument * LoadXMLDocumentSynchronous(NSString *url, GTMOAuth2Authentication *auth, NSError **outError) +{ + // Group to wait for refresh of token (if neccessary) + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + + // Create the request; authenticate and authorize it. + NSURL *actualURL = [NSURL URLWithString:url]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:actualURL]; + [request setValue:DeveloperKey forHTTPHeaderField:DeveloperKeyHTTPHeaderField]; + dispatch_sync(dispatch_get_main_queue(), ^{ + // Seems that authorization must be performed on the main queue + [auth authorizeRequest:request completionHandler:^(NSError *error) { + if (outError != NULL) { + *outError = error; + } + dispatch_group_leave(group); + }]; + }); + + // Wait for authorization + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + if (*outError) { + // The authorization failed + return nil; + } + + // Create the document + NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:outError]; + if (!data) { + return nil; + } + NSXMLDocument *document = [[NSXMLDocument alloc] initWithData:data options:NSXMLDocumentTidyXML error:outError]; + return document; + +} \ No newline at end of file diff --git a/Notifications for YouTube/NYTVideo.h b/Notifications for YouTube/NYTVideo.h new file mode 100755 index 0000000..ee9a513 --- /dev/null +++ b/Notifications for YouTube/NYTVideo.h @@ -0,0 +1,48 @@ +// +// NYTVideo.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 14.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +/* A NYTVideo represents a YouTube video's metadata. + */ +@interface NYTVideo : NSObject + +/* A unique id that identifies the video. + */ +@property (readonly) NSString *videoID; + +/* The channel id of the channel that uploaded the video. See channelID in NYTUser for more details on channel ids. + */ +@property (readonly) NSString *uploaderID; + +/* The date this video was uploaded to YouTube. Precise on 1 second. + */ +@property (readonly) NSDate *uploadedDate; + +/* The title of the video as displayed on the website. Use this to display a video title to a user. Do not use this to uniquely identify a video. use the videoID instead for that purpose. + */ +@property (readonly) NSString *title; + +/* The URL on YouTube that runs the video. Open this URL to display this video on YouTube. + */ +@property (readonly) NSString *URL; + +/* The description of the video as displayed on the website. Display this with the video's title to help the user identify a video. + */ +@property (readonly) NSString *videoDescription; + +/* The display name of the channel that uploaded the video. See displayName in NYTUser for more information. + */ +@property (readonly) NSString *uploaderDisplayName; + +/* Initializes this video by parsing the given node. The node should be a media:group node of a video feed entry. + */ +- (id)initWithMediaGroupNode:(NSXMLNode *)node; + +- (BOOL)isEqual:(id)object; +- (BOOL)isEqualToVideo:(NYTVideo *)video; + +@end diff --git a/Notifications for YouTube/NYTVideo.m b/Notifications for YouTube/NYTVideo.m new file mode 100755 index 0000000..dbd60a3 --- /dev/null +++ b/Notifications for YouTube/NYTVideo.m @@ -0,0 +1,81 @@ +// +// NYTVideo.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 14.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NYTVideo.h" + +// API Imports +#import "ISO8601DateFormatter.h" + +@implementation NYTVideo + +static ISO8601DateFormatter *formatter = nil; + ++ (ISO8601DateFormatter *)dateFormatter +{ + // For efficiency use one instace for the entire app + if (!formatter) { + formatter = [[ISO8601DateFormatter alloc] init]; + formatter.includeTime = YES; + } + return formatter; +} + +- (id)initWithMediaGroupNode:(NSXMLNode *)node +{ + self = [super init]; + if (self) { + static NSString *const videoIDPath = @"./yt:videoid"; + static NSString *const uploaderIDPath = @"./yt:uploaderId"; + static NSString *const uploadedDatePath = @"./yt:uploaded"; + static NSString *const titlePath = @"./media:title"; + static NSString *const urlPath = @"./media:player/@url"; + static NSString *const descriptionPath = @"./media:description"; + static NSString *const uploaderDisplayNamePath = @"./media:credit[@role='uploader']/@yt:display"; + NSXMLElement *videoIDElement = [node nodesForXPath:videoIDPath error:nil][0]; + NSXMLElement *uploaderIDElement = [node nodesForXPath:uploaderIDPath error:nil][0]; + NSXMLElement *uploadedDateElement = [node nodesForXPath:uploadedDatePath error:nil][0]; + NSXMLElement *titleElement = [node nodesForXPath:titlePath error:nil][0]; + NSXMLElement *urlElement = [node nodesForXPath:urlPath error:nil][0]; + NSXMLElement *descriptionElement = [node nodesForXPath:descriptionPath error:nil][0]; + NSXMLElement *uploaderDisplayNameElement = [node nodesForXPath:uploaderDisplayNamePath error:nil][0]; + _videoID = videoIDElement.stringValue; + _uploaderID = uploaderIDElement.stringValue; + _uploadedDate = [[NYTVideo dateFormatter] dateFromString:uploadedDateElement.stringValue]; + _title = titleElement.stringValue; + _URL = urlElement.stringValue; + _videoDescription = descriptionElement.stringValue; + _uploaderDisplayName = uploaderDisplayNameElement.stringValue; + } + return self; +} + +- (NSString *)description +{ + return self.videoID; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + if (object == nil) { + return NO; + } + if (![object isKindOfClass:[NYTVideo class]]) { + return NO; + } + return [self isEqualToVideo:object]; +} + +- (BOOL)isEqualToVideo:(NYTVideo *)video +{ + return [self.videoID isEqualToString:video.videoID]; +} + +@end diff --git a/Notifications for YouTube/Notifications for YouTube-Info.plist b/Notifications for YouTube/Notifications for YouTube-Info.plist old mode 100644 new mode 100755 index 4686c0b..77ced67 --- a/Notifications for YouTube/Notifications for YouTube-Info.plist +++ b/Notifications for YouTube/Notifications for YouTube-Info.plist @@ -4,10 +4,12 @@ CFBundleDevelopmentRegion en + CFBundleDisplayName + Notifications for YouTube CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFile - + Notifications for YouTube CFBundleIdentifier wittenburg.kim.${PRODUCT_NAME:rfc1034identifier} CFBundleInfoDictionaryVersion @@ -17,15 +19,19 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.1.3 Beta CFBundleSignature ???? CFBundleVersion - 1 + Build 42 LSApplicationCategoryType public.app-category.entertainment + LSHasLocalizedDisplayName + LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} + LSUIElement + NSHumanReadableCopyright Copyright © 2013 Kim Wittenburg. All rights reserved. NSMainNibFile diff --git a/Notifications for YouTube/Notifications for YouTube-Prefix.pch b/Notifications for YouTube/Notifications for YouTube-Prefix.pch old mode 100644 new mode 100755 diff --git a/Notifications for YouTube/NumberValueTransformer.h b/Notifications for YouTube/NumberValueTransformer.h new file mode 100755 index 0000000..f5f4b88 --- /dev/null +++ b/Notifications for YouTube/NumberValueTransformer.h @@ -0,0 +1,16 @@ +// +// TimeIntervalValueTransformer.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 20.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import + +/* The number value transformer transforms a number into... a number. Into exactly the same number. You can use this transformer if you get an type error binding a property of the right type. For example if you bind the selected tag of a popup button to an NSTimeInterval property. + This transformer does allow reverse transformation. + */ +@interface NumberValueTransformer : NSValueTransformer + +@end diff --git a/Notifications for YouTube/NumberValueTransformer.m b/Notifications for YouTube/NumberValueTransformer.m new file mode 100755 index 0000000..3ae9a63 --- /dev/null +++ b/Notifications for YouTube/NumberValueTransformer.m @@ -0,0 +1,33 @@ +// +// TimeIntervalValueTransformer.m +// Notifications for YouTube +// +// Created by Kim Wittenburg on 20.07.13. +// Copyright (c) 2013 Kim Wittenburg. All rights reserved. +// + +#import "NumberValueTransformer.h" + +@implementation NumberValueTransformer + ++ (Class)transformedValueClass +{ + return [NSNumber class]; +} + ++ (BOOL)allowsReverseTransformation +{ + return YES; +} + +- (id)transformedValue:(id)value +{ + return value; +} + +- (id)reverseTransformedValue:(id)value +{ + return value; +} + +@end diff --git a/Notifications for YouTube/de.lproj/Credits.rtf b/Notifications for YouTube/de.lproj/Credits.rtf new file mode 100755 index 0000000..181dd8d --- /dev/null +++ b/Notifications for YouTube/de.lproj/Credits.rtf @@ -0,0 +1,19 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\paperw11900\paperh16840\vieww9600\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs24 \cf0 Entwicklung: +\b0 \ + Kim Wittenburg\ +\ + +\b Design: +\b0 \ + Kim Wittenburg\ +\ + +\b Tests: +\b0 \ + Kim Wittenburg} \ No newline at end of file diff --git a/Notifications for YouTube/de.lproj/InfoPlist.strings b/Notifications for YouTube/de.lproj/InfoPlist.strings new file mode 100755 index 0000000..d54e33c --- /dev/null +++ b/Notifications for YouTube/de.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +/* Localized versions of Info.plist keys */ + +"CFBundleDisplayName" = "Benachrichtigungen für YouTube"; +"Notifications for YouTube" = "Benachrichtigungen für YouTube"; \ No newline at end of file diff --git a/Notifications for YouTube/de.lproj/Localizable.strings b/Notifications for YouTube/de.lproj/Localizable.strings new file mode 100755 index 0000000..fb179cd Binary files /dev/null and b/Notifications for YouTube/de.lproj/Localizable.strings differ diff --git a/Notifications for YouTube/de.lproj/MainMenu.xib b/Notifications for YouTube/de.lproj/MainMenu.xib new file mode 100755 index 0000000..f5c8631 --- /dev/null +++ b/Notifications for YouTube/de.lproj/MainMenu.xib @@ -0,0 +1,993 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NumberValueTransformer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NumberValueTransformer + + + + + + + + + + + + + + + + + + (nicht angemeldet) + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Notifications for YouTube/de.lproj/NYTRulesWindowController.xib b/Notifications for YouTube/de.lproj/NYTRulesWindowController.xib new file mode 100755 index 0000000..d700c12 --- /dev/null +++ b/Notifications for YouTube/de.lproj/NYTRulesWindowController.xib @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + predicate + user.displayName contains[cd] $value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + title + + + + + + + + + + title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Notifications for YouTube/de.lproj/RulesPredicates.strings b/Notifications for YouTube/de.lproj/RulesPredicates.strings new file mode 100755 index 0000000..2f839df --- /dev/null +++ b/Notifications for YouTube/de.lproj/RulesPredicates.strings @@ -0,0 +1,16 @@ +/* + RulesPredicates.strings + Notifications for YouTube + + Created by Kim Wittenburg on 22.07.13. + Copyright (c) 2013 Kim Wittenburg. All rights reserved. +*/ + +"%[Any]@ of the following are true" = "Entspricht %[einem]@ der folgenden Kriterien"; +"%[All]@ of the following are true" = "Entspricht %[allen]@ der folgenden Kriterien"; +"%[None]@ of the following are true" = "Entspricht %[keinem]@ der folgenden Kriterien"; +"%[Video Title]@ %[contains]@ %@" = "%1$[Videotitel]@ %2$[enthält]@ %3$@"; +"%[Video Title]@ %[begins with]@ %@" = "%1$[Videotitel]@ %2$[beginnt mit]@ %3$@"; +"%[Video Title]@ %[ends with]@ %@" = "%1$[Videotitel]@ %2$[endet mit]@ %3$@"; +"%[Video Title]@ %[is]@ %@" = "%1$[Videotitel]@ %2$[ist]@ %3$@"; +"%[Video Title]@ %[is not]@ %@" = "%1$[Videotitel]@ %2$[ist nicht]@ %3$@"; \ No newline at end of file diff --git a/Notifications for YouTube/en.lproj/Credits.rtf b/Notifications for YouTube/en.lproj/Credits.rtf old mode 100644 new mode 100755 index 46576ef..7a6ffd3 --- a/Notifications for YouTube/en.lproj/Credits.rtf +++ b/Notifications for YouTube/en.lproj/Credits.rtf @@ -1,29 +1,19 @@ -{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} +{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\paperw9840\paperh8400 -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural +\paperw11900\paperh16840\vieww9600\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 \f0\b\fs24 \cf0 Engineering: \b0 \ - Some people\ + Kim Wittenburg\ \ \b Human Interface Design: \b0 \ - Some other people\ + Kim Wittenburg\ \ \b Testing: \b0 \ - Hopefully not nobody\ -\ - -\b Documentation: -\b0 \ - Whoever\ -\ - -\b With special thanks to: -\b0 \ - Mom\ -} + Kim Wittenburg} \ No newline at end of file diff --git a/Notifications for YouTube/en.lproj/InfoPlist.strings b/Notifications for YouTube/en.lproj/InfoPlist.strings old mode 100644 new mode 100755 index 477b28f..3defd63 --- a/Notifications for YouTube/en.lproj/InfoPlist.strings +++ b/Notifications for YouTube/en.lproj/InfoPlist.strings @@ -1,2 +1,4 @@ /* Localized versions of Info.plist keys */ +"CFBundleDisplayName" = "Notifications for YouTube"; +"Notifications for YouTube" = "Notifications for YouTube"; \ No newline at end of file diff --git a/Notifications for YouTube/en.lproj/Localizable.strings b/Notifications for YouTube/en.lproj/Localizable.strings new file mode 100755 index 0000000..ef8a91f Binary files /dev/null and b/Notifications for YouTube/en.lproj/Localizable.strings differ diff --git a/Notifications for YouTube/en.lproj/MainMenu.xib b/Notifications for YouTube/en.lproj/MainMenu.xib old mode 100644 new mode 100755 index d37c495..a6e4a57 --- a/Notifications for YouTube/en.lproj/MainMenu.xib +++ b/Notifications for YouTube/en.lproj/MainMenu.xib @@ -1,4666 +1,991 @@ - - - - 1080 - 11D50 - 2457 - 1138.32 - 568.00 - - com.apple.InterfaceBuilder.CocoaPlugin - 2457 - - - NSWindowTemplate - NSView - NSMenu - NSMenuItem - NSCustomObject - - - com.apple.InterfaceBuilder.CocoaPlugin - - - PluginDependencyRecalculationVersion - - - - - NSApplication - - - FirstResponder - - - NSApplication - - - AMainMenu - - - - Notifications for YouTube - - 1048576 - 2147483647 - - NSImage - NSMenuCheckmark - - - NSImage - NSMenuMixedState - - submenuAction: - - Notifications for YouTube - - - - About Notifications for YouTube - - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Preferences… - , - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Services - - 1048576 - 2147483647 - - - submenuAction: - - Services - - _NSServicesMenu - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Hide Notifications for YouTube - h - 1048576 - 2147483647 - - - - - - Hide Others - h - 1572864 - 2147483647 - - - - - - Show All - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Quit Notifications for YouTube - q - 1048576 - 2147483647 - - - - - _NSAppleMenu - - - - - File - - 1048576 - 2147483647 - - - submenuAction: - - File - - - - New - n - 1048576 - 2147483647 - - - - - - Open… - o - 1048576 - 2147483647 - - - - - - Open Recent - - 1048576 - 2147483647 - - - submenuAction: - - Open Recent - - - - Clear Menu - - 1048576 - 2147483647 - - - - - _NSRecentDocumentsMenu - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Close - w - 1048576 - 2147483647 - - - - - - Save… - s - 1048576 - 2147483647 - - - - - - Revert to Saved - - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Page Setup... - P - 1179648 - 2147483647 - - - - - - - Print… - p - 1048576 - 2147483647 - - - - - - - - - Edit - - 1048576 - 2147483647 - - - submenuAction: - - Edit - - - - Undo - z - 1048576 - 2147483647 - - - - - - Redo - Z - 1179648 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Cut - x - 1048576 - 2147483647 - - - - - - Copy - c - 1048576 - 2147483647 - - - - - - Paste - v - 1048576 - 2147483647 - - - - - - Paste and Match Style - V - 1572864 - 2147483647 - - - - - - Delete - - 1048576 - 2147483647 - - - - - - Select All - a - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Find - - 1048576 - 2147483647 - - - submenuAction: - - Find - - - - Find… - f - 1048576 - 2147483647 - - - 1 - - - - Find and Replace… - f - 1572864 - 2147483647 - - - 12 - - - - Find Next - g - 1048576 - 2147483647 - - - 2 - - - - Find Previous - G - 1179648 - 2147483647 - - - 3 - - - - Use Selection for Find - e - 1048576 - 2147483647 - - - 7 - - - - Jump to Selection - j - 1048576 - 2147483647 - - - - - - - - - Spelling and Grammar - - 1048576 - 2147483647 - - - submenuAction: - - Spelling and Grammar - - - - Show Spelling and Grammar - : - 1048576 - 2147483647 - - - - - - Check Document Now - ; - 1048576 - 2147483647 - - - - - - YES - YES - - - 2147483647 - - - - - - Check Spelling While Typing - - 1048576 - 2147483647 - - - - - - Check Grammar With Spelling - - 1048576 - 2147483647 - - - - - - Correct Spelling Automatically - - 2147483647 - - - - - - - - - Substitutions - - 1048576 - 2147483647 - - - submenuAction: - - Substitutions - - - - Show Substitutions - - 2147483647 - - - - - - YES - YES - - - 2147483647 - - - - - - Smart Copy/Paste - f - 1048576 - 2147483647 - - - 1 - - - - Smart Quotes - g - 1048576 - 2147483647 - - - 2 - - - - Smart Dashes - - 2147483647 - - - - - - Smart Links - G - 1179648 - 2147483647 - - - 3 - - - - Text Replacement - - 2147483647 - - - - - - - - - Transformations - - 2147483647 - - - submenuAction: - - Transformations - - - - Make Upper Case - - 2147483647 - - - - - - Make Lower Case - - 2147483647 - - - - - - Capitalize - - 2147483647 - - - - - - - - - Speech - - 1048576 - 2147483647 - - - submenuAction: - - Speech - - - - Start Speaking - - 1048576 - 2147483647 - - - - - - Stop Speaking - - 1048576 - 2147483647 - - - - - - - - - - - - Format - - 2147483647 - - - submenuAction: - - Format - - - - Font - - 2147483647 - - - submenuAction: - - Font - - - - Show Fonts - t - 1048576 - 2147483647 - - - - - - Bold - b - 1048576 - 2147483647 - - - 2 - - - - Italic - i - 1048576 - 2147483647 - - - 1 - - - - Underline - u - 1048576 - 2147483647 - - - - - - YES - YES - - - 2147483647 - - - - - - Bigger - + - 1048576 - 2147483647 - - - 3 - - - - Smaller - - - 1048576 - 2147483647 - - - 4 - - - - YES - YES - - - 2147483647 - - - - - - Kern - - 2147483647 - - - submenuAction: - - Kern - - - - Use Default - - 2147483647 - - - - - - Use None - - 2147483647 - - - - - - Tighten - - 2147483647 - - - - - - Loosen - - 2147483647 - - - - - - - - - Ligatures - - 2147483647 - - - submenuAction: - - Ligatures - - - - Use Default - - 2147483647 - - - - - - Use None - - 2147483647 - - - - - - Use All - - 2147483647 - - - - - - - - - Baseline - - 2147483647 - - - submenuAction: - - Baseline - - - - Use Default - - 2147483647 - - - - - - Superscript - - 2147483647 - - - - - - Subscript - - 2147483647 - - - - - - Raise - - 2147483647 - - - - - - Lower - - 2147483647 - - - - - - - - - YES - YES - - - 2147483647 - - - - - - Show Colors - C - 1048576 - 2147483647 - - - - - - YES - YES - - - 2147483647 - - - - - - Copy Style - c - 1572864 - 2147483647 - - - - - - Paste Style - v - 1572864 - 2147483647 - - - - - _NSFontMenu - - - - - Text - - 2147483647 - - - submenuAction: - - Text - - - - Align Left - { - 1048576 - 2147483647 - - - - - - Center - | - 1048576 - 2147483647 - - - - - - Justify - - 2147483647 - - - - - - Align Right - } - 1048576 - 2147483647 - - - - - - YES - YES - - - 2147483647 - - - - - - Writing Direction - - 2147483647 - - - submenuAction: - - Writing Direction - - - - YES - Paragraph - - 2147483647 - - - - - - CURlZmF1bHQ - - 2147483647 - - - - - - CUxlZnQgdG8gUmlnaHQ - - 2147483647 - - - - - - CVJpZ2h0IHRvIExlZnQ - - 2147483647 - - - - - - YES - YES - - - 2147483647 - - - - - - YES - Selection - - 2147483647 - - - - - - CURlZmF1bHQ - - 2147483647 - - - - - - CUxlZnQgdG8gUmlnaHQ - - 2147483647 - - - - - - CVJpZ2h0IHRvIExlZnQ - - 2147483647 - - - - - - - - - YES - YES - - - 2147483647 - - - - - - Show Ruler - - 2147483647 - - - - - - Copy Ruler - c - 1310720 - 2147483647 - - - - - - Paste Ruler - v - 1310720 - 2147483647 - - - - - - - - - - - - View - - 1048576 - 2147483647 - - - submenuAction: - - View - - - - Show Toolbar - t - 1572864 - 2147483647 - - - - - - Customize Toolbar… - - 1048576 - 2147483647 - - - - - - - - - Window - - 1048576 - 2147483647 - - - submenuAction: - - Window - - - - Minimize - m - 1048576 - 2147483647 - - - - - - Zoom - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Bring All to Front - - 1048576 - 2147483647 - - - - - _NSWindowsMenu - - - - - Help - - 2147483647 - - - submenuAction: - - Help - - - - Notifications for YouTube Help - ? - 1048576 - 2147483647 - - - - - _NSHelpMenu - - - - _NSMainMenu - - - 15 - 2 - {{335, 390}, {480, 360}} - 1954021376 - Notifications for YouTube - NSWindow - - - - - 256 - {480, 360} - - {{0, 0}, {2560, 1418}} - {10000000000000, 10000000000000} - YES - - - NYTAppDelegate - - - NSFontManager - - - - - - - terminate: - - - - 449 - - - - orderFrontStandardAboutPanel: - - - - 142 - - - - delegate - - - - 495 - - - - performMiniaturize: - - - - 37 - - - - arrangeInFront: - - - - 39 - - - - print: - - - - 86 - - - - runPageLayout: - - - - 87 - - - - clearRecentDocuments: - - - - 127 - - - - performClose: - - - - 193 - - - - toggleContinuousSpellChecking: - - - - 222 - - - - undo: - - - - 223 - - - - copy: - - - - 224 - - - - checkSpelling: - - - - 225 - - - - paste: - - - - 226 - - - - stopSpeaking: - - - - 227 - - - - cut: - - - - 228 - - - - showGuessPanel: - - - - 230 - - - - redo: - - - - 231 - - - - selectAll: - - - - 232 - - - - startSpeaking: - - - - 233 - - - - delete: - - - - 235 - - - - performZoom: - - - - 240 - - - - performFindPanelAction: - - - - 241 - - - - centerSelectionInVisibleArea: - - - - 245 - - - - toggleGrammarChecking: - - - - 347 - - - - toggleSmartInsertDelete: - - - - 355 - - - - toggleAutomaticQuoteSubstitution: - - - - 356 - - - - toggleAutomaticLinkDetection: - - - - 357 - - - - saveDocument: - - - - 362 - - - - revertDocumentToSaved: - - - - 364 - - - - runToolbarCustomizationPalette: - - - - 365 - - - - toggleToolbarShown: - - - - 366 - - - - hide: - - - - 367 - - - - hideOtherApplications: - - - - 368 - - - - unhideAllApplications: - - - - 370 - - - - newDocument: - - - - 373 - - - - openDocument: - - - - 374 - - - - raiseBaseline: - - - - 426 - - - - lowerBaseline: - - - - 427 - - - - copyFont: - - - - 428 - - - - subscript: - - - - 429 - - - - superscript: - - - - 430 - - - - tightenKerning: - - - - 431 - - - - underline: - - - - 432 - - - - orderFrontColorPanel: - - - - 433 - - - - useAllLigatures: - - - - 434 - - - - loosenKerning: - - - - 435 - - - - pasteFont: - - - - 436 - - - - unscript: - - - - 437 - - - - useStandardKerning: - - - - 438 - - - - useStandardLigatures: - - - - 439 - - - - turnOffLigatures: - - - - 440 - - - - turnOffKerning: - - - - 441 - - - - toggleAutomaticSpellingCorrection: - - - - 456 - - - - orderFrontSubstitutionsPanel: - - - - 458 - - - - toggleAutomaticDashSubstitution: - - - - 461 - - - - toggleAutomaticTextReplacement: - - - - 463 - - - - uppercaseWord: - - - - 464 - - - - capitalizeWord: - - - - 467 - - - - lowercaseWord: - - - - 468 - - - - pasteAsPlainText: - - - - 486 - - - - performFindPanelAction: - - - - 487 - - - - performFindPanelAction: - - - - 488 - - - - performFindPanelAction: - - - - 489 - - - - showHelp: - - - - 493 - - - - alignCenter: - - - - 518 - - - - pasteRuler: - - - - 519 - - - - toggleRuler: - - - - 520 - - - - alignRight: - - - - 521 - - - - copyRuler: - - - - 522 - - - - alignJustified: - - - - 523 - - - - alignLeft: - - - - 524 - - - - makeBaseWritingDirectionNatural: - - - - 525 - - - - makeBaseWritingDirectionLeftToRight: - - - - 526 - - - - makeBaseWritingDirectionRightToLeft: - - - - 527 - - - - makeTextWritingDirectionNatural: - - - - 528 - - - - makeTextWritingDirectionLeftToRight: - - - - 529 - - - - makeTextWritingDirectionRightToLeft: - - - - 530 - - - - performFindPanelAction: - - - - 535 - - - - addFontTrait: - - - - 421 - - - - addFontTrait: - - - - 422 - - - - modifyFont: - - - - 423 - - - - orderFrontFontPanel: - - - - 424 - - - - modifyFont: - - - - 425 - - - - window - - - - 532 - - - - - - 0 - - - - - - -2 - - - File's Owner - - - -1 - - - First Responder - - - -3 - - - Application - - - 29 - - - - - - - - - - - - - - 19 - - - - - - - - 56 - - - - - - - - 217 - - - - - - - - 83 - - - - - - - - 81 - - - - - - - - - - - - - - - - - 75 - - - - - 78 - - - - - 72 - - - - - 82 - - - - - 124 - - - - - - - - 77 - - - - - 73 - - - - - 79 - - - - - 112 - - - - - 74 - - - - - 125 - - - - - - - - 126 - - - - - 205 - - - - - - - - - - - - - - - - - - - - - - 202 - - - - - 198 - - - - - 207 - - - - - 214 - - - - - 199 - - - - - 203 - - - - - 197 - - - - - 206 - - - - - 215 - - - - - 218 - - - - - - - - 216 - - - - - - - - 200 - - - - - - - - - - - - - 219 - - - - - 201 - - - - - 204 - - - - - 220 - - - - - - - - - - - - - 213 - - - - - 210 - - - - - 221 - - - - - 208 - - - - - 209 - - - - - 57 - - - - - - - - - - - - - - - - - - 58 - - - - - 134 - - - - - 150 - - - - - 136 - - - - - 144 - - - - - 129 - - - - - 143 - - - - - 236 - - - - - 131 - - - - - - - - 149 - - - - - 145 - - - - - 130 - - - - - 24 - - - - - - - - - - - 92 - - - - - 5 - - - - - 239 - - - - - 23 - - - - - 295 - - - - - - - - 296 - - - - - - - - - 297 - - - - - 298 - - - - - 211 - - - - - - - - 212 - - - - - - - - - 195 - - - - - 196 - - - - - 346 - - - - - 348 - - - - - - - - 349 - - - - - - - - - - - - - - 350 - - - - - 351 - - - - - 354 - - - - - 371 - - - - - - - - 372 - - - - - 375 - - - - - - - - 376 - - - - - - - - - 377 - - - - - - - - 388 - - - - - - - - - - - - - - - - - - - - - - - 389 - - - - - 390 - - - - - 391 - - - - - 392 - - - - - 393 - - - - - 394 - - - - - 395 - - - - - 396 - - - - - 397 - - - - - - - - 398 - - - - - - - - 399 - - - - - - - - 400 - - - - - 401 - - - - - 402 - - - - - 403 - - - - - 404 - - - - - 405 - - - - - - - - - - - - 406 - - - - - 407 - - - - - 408 - - - - - 409 - - - - - 410 - - - - - 411 - - - - - - - - - - 412 - - - - - 413 - - - - - 414 - - - - - 415 - - - - - - - - - - - 416 - - - - - 417 - - - - - 418 - - - - - 419 - - - - - 420 - - - - - 450 - - - - - - - - 451 - - - - - - - - - - 452 - - - - - 453 - - - - - 454 - - - - - 457 - - - - - 459 - - - - - 460 - - - - - 462 - - - - - 465 - - - - - 466 - - - - - 485 - - - - - 490 - - - - - - - - 491 - - - - - - - - 492 - - - - - 494 - - - - - 496 - - - - - - - - 497 - - - - - - - - - - - - - - - - - 498 - - - - - 499 - - - - - 500 - - - - - 501 - - - - - 502 - - - - - 503 - - - - - - - - 504 - - - - - 505 - - - - - 506 - - - - - 507 - - - - - 508 - - - - - - - - - - - - - - - - 509 - - - - - 510 - - - - - 511 - - - - - 512 - - - - - 513 - - - - - 514 - - - - - 515 - - - - - 516 - - - - - 517 - - - - - 534 - - - - - - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - {{380, 496}, {480, 360}} - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - - - - - 535 - - - - - ABCardController - NSObject - - id - id - id - id - id - id - id - - - - addCardViewField: - id - - - copy: - id - - - cut: - id - - - doDelete: - id - - - find: - id - - - paste: - id - - - saveChanges: - id - - - - ABCardView - NSButton - NSManagedObjectContext - NSSearchField - NSTextField - NSWindow - - - - mCardView - ABCardView - - - mEditButton - NSButton - - - mManagedObjectContext - NSManagedObjectContext - - - mSearchField - NSSearchField - - - mStatusTextField - NSTextField - - - mWindow - NSWindow - - - - IBProjectSource - ./Classes/ABCardController.h - - - - ABCardView - NSView - - id - id - - - - commitAndSave: - id - - - statusImageClicked: - id - - - - NSObjectController - NSImageView - NSView - ABNameFrameView - NSView - NSImage - ABImageView - - - - mBindingsController - NSObjectController - - - mBuddyStatusImage - NSImageView - - - mHeaderView - NSView - - - mNameView - ABNameFrameView - - - mNextKeyView - NSView - - - mUserImage - NSImage - - - mUserImageView - ABImageView - - - - IBProjectSource - ./Classes/ABCardView.h - - - - ABImageView - NSImageView - - id - id - id - id - - - - copy: - id - - - cut: - id - - - delete: - id - - - paste: - id - - - - IBProjectSource - ./Classes/ABImageView.h - - - - DVTBorderedView - DVTLayoutView_ML - - contentView - NSView - - - contentView - - contentView - NSView - - - - IBProjectSource - ./Classes/DVTBorderedView.h - - - - DVTDelayedMenuButton - NSButton - - IBProjectSource - ./Classes/DVTDelayedMenuButton.h - - - - DVTGradientImageButton - NSButton - - IBProjectSource - ./Classes/DVTGradientImageButton.h - - - - DVTImageAndTextCell - NSTextFieldCell - - IBProjectSource - ./Classes/DVTImageAndTextCell.h - - - - DVTImageAndTextColumn - NSTableColumn - - IBProjectSource - ./Classes/DVTImageAndTextColumn.h - - - - DVTLayoutView_ML - NSView - - IBProjectSource - ./Classes/DVTLayoutView_ML.h - - - - DVTOutlineView - NSOutlineView - - IBProjectSource - ./Classes/DVTOutlineView.h - - - - DVTSplitView - NSSplitView - - IBProjectSource - ./Classes/DVTSplitView.h - - - - DVTStackView_ML - DVTLayoutView_ML - - IBProjectSource - ./Classes/DVTStackView_ML.h - - - - DVTTableView - NSTableView - - IBProjectSource - ./Classes/DVTTableView.h - - - - DVTViewController - NSViewController - - IBProjectSource - ./Classes/DVTViewController.h - - - - HFController - NSObject - - selectAll: - id - - - selectAll: - - selectAll: - id - - - - IBProjectSource - ./Classes/HFController.h - - - - HFRepresenterTextView - NSView - - selectAll: - id - - - selectAll: - - selectAll: - id - - - - IBProjectSource - ./Classes/HFRepresenterTextView.h - - - - IBEditor - NSObject - - id - id - id - id - id - - - - changeFont: - id - - - performCopy: - id - - - performCut: - id - - - selectAll: - id - - - sizeSelectionToFit: - id - - - - IBProjectSource - ./Classes/IBEditor.h - - - - IDECapsuleListView - DVTStackView_ML - - dataSource - id - - - dataSource - - dataSource - id - - - - IBProjectSource - ./Classes/IDECapsuleListView.h - - - - IDEDMArrayController - NSArrayController - - IBProjectSource - ./Classes/IDEDMArrayController.h - - - - IDEDMEditor - IDEEditor - - DVTBorderedView - NSView - IDEDMEditorSourceListController - DVTSplitView - - - - bottomToolbarBorderView - DVTBorderedView - - - sourceListSplitViewPane - NSView - - - sourceListViewController - IDEDMEditorSourceListController - - - splitView - DVTSplitView - - - - IBProjectSource - ./Classes/IDEDMEditor.h - - - - IDEDMEditorController - IDEViewController - - IBProjectSource - ./Classes/IDEDMEditorController.h - - - - IDEDMEditorSourceListController - IDEDMEditorController - - DVTBorderedView - IDEDMEditor - DVTImageAndTextColumn - DVTOutlineView - NSTreeController - - - - borderedView - DVTBorderedView - - - parentEditor - IDEDMEditor - - - primaryColumn - DVTImageAndTextColumn - - - sourceListOutlineView - DVTOutlineView - - - sourceListTreeController - NSTreeController - - - - IBProjectSource - ./Classes/IDEDMEditorSourceListController.h - - - - IDEDMHighlightImageAndTextCell - DVTImageAndTextCell - - IBProjectSource - ./Classes/IDEDMHighlightImageAndTextCell.h - - - - IDEDataModelBrowserEditor - IDEDMEditorController - - IDEDataModelPropertiesTableController - IDECapsuleListView - NSArrayController - IDEDataModelPropertiesTableController - IDEDataModelEntityContentsEditor - IDEDataModelPropertiesTableController - - - - attributesTableViewController - IDEDataModelPropertiesTableController - - - capsuleView - IDECapsuleListView - - - entityArrayController - NSArrayController - - - fetchedPropertiesTableViewController - IDEDataModelPropertiesTableController - - - parentEditor - IDEDataModelEntityContentsEditor - - - relationshipsTableViewController - IDEDataModelPropertiesTableController - - - - IBProjectSource - ./Classes/IDEDataModelBrowserEditor.h - - - - IDEDataModelConfigurationEditor - IDEDMEditorController - - IDECapsuleListView - IDEDataModelEditor - IDEDataModelConfigurationTableController - - - - capsuleListView - IDECapsuleListView - - - parentEditor - IDEDataModelEditor - - - tableController - IDEDataModelConfigurationTableController - - - - IBProjectSource - ./Classes/IDEDataModelConfigurationEditor.h - - - - IDEDataModelConfigurationTableController - IDEDMEditorController - - NSArrayController - NSArrayController - IDEDataModelConfigurationEditor - XDTableView - - - - configurationsArrayController - NSArrayController - - - entitiesArrayController - NSArrayController - - - parentEditor - IDEDataModelConfigurationEditor - - - tableView - XDTableView - - - - IBProjectSource - ./Classes/IDEDataModelConfigurationTableController.h - - - - IDEDataModelDiagramEditor - IDEDMEditorController - - XDDiagramView - IDEDataModelEntityContentsEditor - - - - diagramView - XDDiagramView - - - parentEditor - IDEDataModelEntityContentsEditor - - - - IBProjectSource - ./Classes/IDEDataModelDiagramEditor.h - - - - IDEDataModelEditor - IDEDMEditor - - DVTDelayedMenuButton - DVTDelayedMenuButton - NSSegmentedControl - IDEDataModelConfigurationEditor - IDEDataModelEntityContentsEditor - IDEDataModelFetchRequestEditor - NSSegmentedControl - NSTabView - - - - addEntityButton - DVTDelayedMenuButton - - - addPropertyButton - DVTDelayedMenuButton - - - browserDiagramSegmentControl - NSSegmentedControl - - - configurationViewController - IDEDataModelConfigurationEditor - - - entityContentsViewController - IDEDataModelEntityContentsEditor - - - fetchRequestViewController - IDEDataModelFetchRequestEditor - - - hierarchySegmentControl - NSSegmentedControl - - - tabView - NSTabView - - - - IBProjectSource - ./Classes/IDEDataModelEditor.h - - - - IDEDataModelEntityContentsEditor - IDEDMEditorController - - IDEDataModelBrowserEditor - IDEDataModelDiagramEditor - IDEDataModelEditor - NSTabView - - - - browserViewController - IDEDataModelBrowserEditor - - - diagramViewController - IDEDataModelDiagramEditor - - - parentEditor - IDEDataModelEditor - - - tabView - NSTabView - - - - IBProjectSource - ./Classes/IDEDataModelEntityContentsEditor.h - - - - IDEDataModelFetchRequestEditor - IDEDMEditorController - - NSArrayController - IDEDataModelEditor - IDECapsuleListView - - - - entityController - NSArrayController - - - parentEditor - IDEDataModelEditor - - - tableView - IDECapsuleListView - - - - IBProjectSource - ./Classes/IDEDataModelFetchRequestEditor.h - - - - IDEDataModelPropertiesTableController - IDEDMEditorController - - IDEDMArrayController - NSTableColumn - NSArrayController - IDEDataModelBrowserEditor - IDEDMHighlightImageAndTextCell - XDTableView - - - - arrayController - IDEDMArrayController - - - entitiesColumn - NSTableColumn - - - entityArrayController - NSArrayController - - - parentEditor - IDEDataModelBrowserEditor - - - propertyNameAndImageCell - IDEDMHighlightImageAndTextCell - - - tableView - XDTableView - - - - IBProjectSource - ./Classes/IDEDataModelPropertiesTableController.h - - - - IDEDocDownloadsTableViewController - NSObject - - NSButtonCell - DVTTableView - IDEDocViewingPrefPaneController - - - - _downloadButtonCell - NSButtonCell - - - _tableView - DVTTableView - - - prefPaneController - IDEDocViewingPrefPaneController - - - - IBProjectSource - ./Classes/IDEDocDownloadsTableViewController.h - - - - IDEDocSetOutlineView - NSOutlineView - - IBProjectSource - ./Classes/IDEDocSetOutlineView.h - - - - IDEDocSetOutlineViewController - NSObject - - id - id - id - id - id - - - - getDocSetAction: - id - - - showProblemInfoForUpdate: - id - - - subscribeToPublisherAction: - id - - - unsubscribeFromPublisher: - id - - - updateDocSetAction: - id - - - - docSetOutlineView - IDEDocSetOutlineView - - - docSetOutlineView - - docSetOutlineView - IDEDocSetOutlineView - - - - IBProjectSource - ./Classes/IDEDocSetOutlineViewController.h - - - - IDEDocViewingPrefPaneController - IDEViewController - - id - id - id - id - id - id - id - id - id - id - id - - - - addSubscription: - id - - - checkForAndInstallUpdatesNow: - id - - - deleteDocSet: - id - - - downloadAction: - id - - - minimumFontSizeComboBoxAction: - id - - - minimumFontSizeEnabledAction: - id - - - showHelp: - id - - - showSubscriptionSheet: - id - - - subscriptionCancelAction: - id - - - toggleAutoCheckForAndInstallUpdates: - id - - - toggleDocSetInfo: - id - - - - DVTGradientImageButton - DVTGradientImageButton - DVTGradientImageButton - NSSplitView - NSView - NSView - DVTBorderedView - DVTBorderedView - NSButton - NSTextView - IDEDocSetOutlineViewController - IDEDocDownloadsTableViewController - NSComboBox - NSTextField - NSButton - NSTextField - NSWindow - NSButton - - - - _addButton - DVTGradientImageButton - - - _deleteButton - DVTGradientImageButton - - - _showInfoAreaButton - DVTGradientImageButton - - - _splitView - NSSplitView - - - _splitViewDocSetInfoSubview - NSView - - - _splitViewDocSetsListSubview - NSView - - - borderedViewAroundSplitView - DVTBorderedView - - - borderedViewBelowTable - DVTBorderedView - - - checkAndInstallNowButton - NSButton - - - docSetInfoTextView - NSTextView - - - docSetOutlineViewController - IDEDocSetOutlineViewController - - - downloadsTableViewController - IDEDocDownloadsTableViewController - - - minimumFontSizeControl - NSComboBox - - - noUpdatesAvailableMessage - NSTextField - - - showInfoButton - NSButton - - - subscriptionTextField - NSTextField - - - subscriptionWindow - NSWindow - - - validateAddSubscriptionButton - NSButton - - - - IBProjectSource - ./Classes/IDEDocViewingPrefPaneController.h - - - - IDEEditor - IDEViewController - - IBProjectSource - ./Classes/IDEEditor.h - - - - IDEViewController - DVTViewController - - IBProjectSource - ./Classes/IDEViewController.h - - - - IKImageView - - id - id - id - id - - - - copy: - id - - - crop: - id - - - cut: - id - - - paste: - id - - - - IBProjectSource - ./Classes/IKImageView.h - - - - NSDocument - - id - id - id - id - id - id - - - - printDocument: - id - - - revertDocumentToSaved: - id - - - runPageLayout: - id - - - saveDocument: - id - - - saveDocumentAs: - id - - - saveDocumentTo: - id - - - - IBProjectSource - ./Classes/NSDocument.h - - - - NSResponder - - _insertFindPattern: - id - - - _insertFindPattern: - - _insertFindPattern: - id - - - - IBProjectSource - ./Classes/NSResponder.h - - - - QLPreviewBubble - NSObject - - id - id - - - - hide: - id - - - show: - id - - - - parentWindow - NSWindow - - - parentWindow - - parentWindow - NSWindow - - - - IBProjectSource - ./Classes/QLPreviewBubble.h - - - - QTMovieView - - id - id - id - id - id - - - - showAll: - id - - - showCustomButton: - id - - - toggleLoops: - id - - - zoomIn: - id - - - zoomOut: - id - - - - IBProjectSource - ./Classes/QTMovieView.h - - - - WebView - - id - id - id - id - - - - reloadFromOrigin: - id - - - resetPageZoom: - id - - - zoomPageIn: - id - - - zoomPageOut: - id - - - - IBProjectSource - ./Classes/WebView.h - - - - XDDiagramView - NSView - - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - - - - _graphLayouterMenuItemAction: - id - - - _zoomPopUpButtonAction: - id - - - alignBottomEdges: - id - - - alignCentersHorizontallyInContainer: - id - - - alignCentersVerticallyInContainer: - id - - - alignHorizontalCenters: - id - - - alignLeftEdges: - id - - - alignRightEdges: - id - - - alignTopEdges: - id - - - alignVerticalCenters: - id - - - bringToFront: - id - - - collapseAllCompartments: - id - - - copy: - id - - - cut: - id - - - delete: - id - - - deleteBackward: - id - - - deleteForward: - id - - - deselectAll: - id - - - diagramZoomIn: - id - - - diagramZoomOut: - id - - - expandAllCompartments: - id - - - flipHorizontally: - id - - - flipVertically: - id - - - layoutGraphicsConcentrically: - id - - - layoutGraphicsHierarchically: - id - - - lock: - id - - - makeSameHeight: - id - - - makeSameWidth: - id - - - moveDown: - id - - - moveDownAndModifySelection: - id - - - moveLeft: - id - - - moveLeftAndModifySelection: - id - - - moveRight: - id - - - moveRightAndModifySelection: - id - - - moveUp: - id - - - moveUpAndModifySelection: - id - - - paste: - id - - - rollDownAllCompartments: - id - - - rollUpAllCompartments: - id - - - selectAll: - id - - - sendToBack: - id - - - sizeToFit: - id - - - toggleGridShown: - id - - - toggleHiddenGraphicsShown: - id - - - togglePageBreaksShown: - id - - - toggleRuler: - id - - - toggleSnapsToGrid: - id - - - unlock: - id - - - - _diagramController - IDEDataModelDiagramEditor - - - _diagramController - - _diagramController - IDEDataModelDiagramEditor - - - - IBProjectSource - ./Classes/XDDiagramView.h - - - - XDTableView - NSTableView - - showAllTableColumns: - id - - - showAllTableColumns: - - showAllTableColumns: - id - - - - IBProjectSource - ./Classes/XDTableView.h - - - - NYTAppDelegate - NSObject - - id - id - - - - applicationShouldTerminate: - id - - - applicationWillFinishLaunching: - id - - - - IBProjectSource - ./Classes/NYTAppDelegate.h - - - - - 0 - IBCocoaFramework - YES - 3 - - {11, 11} - {10, 3} - - YES - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NumberValueTransformer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NumberValueTransformer + + + + + + + + + + + + + + + + + + (not logged in) + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Notifications for YouTube/en.lproj/NYTRulesWindowController.xib b/Notifications for YouTube/en.lproj/NYTRulesWindowController.xib new file mode 100755 index 0000000..9a1c0fd --- /dev/null +++ b/Notifications for YouTube/en.lproj/NYTRulesWindowController.xib @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + predicate + user.displayName contains[cd] $value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + title + + + + + + + + + + title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Notifications for YouTube/en.lproj/RulesPredicates.strings b/Notifications for YouTube/en.lproj/RulesPredicates.strings new file mode 100755 index 0000000..5f99e48 --- /dev/null +++ b/Notifications for YouTube/en.lproj/RulesPredicates.strings @@ -0,0 +1,7 @@ +/* + RulesPredicates.strings + Notifications for YouTube + + Created by Kim Wittenburg on 22.07.13. + Copyright (c) 2013 Kim Wittenburg. All rights reserved. +*/ diff --git a/Notifications for YouTube/main.m b/Notifications for YouTube/main.m old mode 100644 new mode 100755 index 29bb528..9ec2a04 --- a/Notifications for YouTube/main.m +++ b/Notifications for YouTube/main.m @@ -8,6 +8,8 @@ #import +#import "ISO8601DateFormatter.h" + int main(int argc, char *argv[]) { return NSApplicationMain(argc, (const char **)argv); diff --git a/Notifications for YouTube/play-icon-bw.png b/Notifications for YouTube/play-icon-bw.png new file mode 100755 index 0000000..0ac99a9 Binary files /dev/null and b/Notifications for YouTube/play-icon-bw.png differ diff --git a/Notifications for YouTube/play-icon-bw@2x.png b/Notifications for YouTube/play-icon-bw@2x.png new file mode 100755 index 0000000..ec0703e Binary files /dev/null and b/Notifications for YouTube/play-icon-bw@2x.png differ diff --git a/Notifications for YouTube/play-icon-s.png b/Notifications for YouTube/play-icon-s.png new file mode 100755 index 0000000..fddba23 Binary files /dev/null and b/Notifications for YouTube/play-icon-s.png differ diff --git a/Notifications for YouTube/play-icon-s@2x.png b/Notifications for YouTube/play-icon-s@2x.png new file mode 100755 index 0000000..d36476d Binary files /dev/null and b/Notifications for YouTube/play-icon-s@2x.png differ diff --git a/Notifications for YouTube/play-icon.png b/Notifications for YouTube/play-icon.png new file mode 100755 index 0000000..0db4f03 Binary files /dev/null and b/Notifications for YouTube/play-icon.png differ diff --git a/Notifications for YouTube/play-icon@2x.png b/Notifications for YouTube/play-icon@2x.png new file mode 100755 index 0000000..b11b552 Binary files /dev/null and b/Notifications for YouTube/play-icon@2x.png differ diff --git a/Notifications for YouTube/poweredByYT.png b/Notifications for YouTube/poweredByYT.png new file mode 100755 index 0000000..ce983ca Binary files /dev/null and b/Notifications for YouTube/poweredByYT.png differ diff --git a/OAuth 2/GTMHTTPFetchHistory.h b/OAuth 2/GTMHTTPFetchHistory.h new file mode 100755 index 0000000..96018f5 --- /dev/null +++ b/OAuth 2/GTMHTTPFetchHistory.h @@ -0,0 +1,187 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTMHTTPFetchHistory.h +// + +// +// Users of the GTMHTTPFetcher class may optionally create and set a fetch +// history object. The fetch history provides "memory" between subsequent +// fetches, including: +// +// - For fetch responses with Etag headers, the fetch history +// remembers the response headers. Future fetcher requests to the same URL +// will be given an "If-None-Match" header, telling the server to return +// a 304 Not Modified status if the response is unchanged, reducing the +// server load and network traffic. +// +// - Optionally, the fetch history can cache the ETagged data that was returned +// in the responses that contained Etag headers. If a later fetch +// results in a 304 status, the fetcher will return the cached ETagged data +// to the client along with a 200 status, hiding the 304. +// +// - The fetch history can track cookies. +// + +#pragma once + +#import + +#import "GTMHTTPFetcher.h" + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GTMHTTPFETCHHISTORY_DEFINE_GLOBALS + #define _EXTERN + #define _INITIALIZE_AS(x) =x +#else + #if defined(__cplusplus) + #define _EXTERN extern "C" + #else + #define _EXTERN extern + #endif + #define _INITIALIZE_AS(x) +#endif + + +// default data cache size for when we're caching responses to handle "not +// modified" errors for the client +#if GTM_IPHONE +// iPhone: up to 1MB memory +_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(1*1024*1024); +#else +// Mac OS X: up to 15MB memory +_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(15*1024*1024); +#endif + +// forward declarations +@class GTMURLCache; +@class GTMCookieStorage; + +@interface GTMHTTPFetchHistory : NSObject { + @private + GTMURLCache *etaggedDataCache_; + BOOL shouldRememberETags_; + BOOL shouldCacheETaggedData_; // if NO, then only headers are cached + GTMCookieStorage *cookieStorage_; +} + +// With caching enabled, previously-cached data will be returned instead of +// 304 Not Modified responses when repeating a fetch of an URL that previously +// included an ETag header in its response +@property (assign) BOOL shouldRememberETags; // default: NO +@property (assign) BOOL shouldCacheETaggedData; // default: NO + +// the default ETag data cache capacity is kGTMDefaultETaggedDataCacheMemoryCapacity +@property (assign) NSUInteger memoryCapacity; + +@property (retain) GTMCookieStorage *cookieStorage; + +- (id)initWithMemoryCapacity:(NSUInteger)totalBytes + shouldCacheETaggedData:(BOOL)shouldCacheETaggedData; + +- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet; + +- (void)clearETaggedDataCache; +- (void)clearHistory; + +- (void)removeAllCookies; + +@end + + +// GTMURLCache and GTMCachedURLResponse have interfaces similar to their +// NSURLCache counterparts, in hopes that someday the NSURLCache versions +// can be used. But in 10.5.8, those are not reliable enough except when +// used with +setSharedURLCache. Our goal here is just to cache +// responses for handling If-None-Match requests that return +// "Not Modified" responses, not for replacing the general URL +// caches. + +@interface GTMCachedURLResponse : NSObject { + @private + NSURLResponse *response_; + NSData *data_; + NSDate *useDate_; // date this response was last saved or used + NSDate *reservationDate_; // date this response's ETag was used +} + +@property (readonly) NSURLResponse* response; +@property (readonly) NSData* data; + +// date the response was saved or last accessed +@property (retain) NSDate *useDate; + +// date the response's ETag header was last used for a fetch request +@property (retain) NSDate *reservationDate; + +- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data; +@end + +@interface GTMURLCache : NSObject { + NSMutableDictionary *responses_; // maps request URL to GTMCachedURLResponse + NSUInteger memoryCapacity_; // capacity of NSDatas in the responses + NSUInteger totalDataSize_; // sum of sizes of NSDatas of all responses + NSTimeInterval reservationInterval_; // reservation expiration interval +} + +@property (assign) NSUInteger memoryCapacity; + +- (id)initWithMemoryCapacity:(NSUInteger)totalBytes; + +- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request; +- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request; +- (void)removeCachedResponseForRequest:(NSURLRequest *)request; +- (void)removeAllCachedResponses; + +// for unit testing +- (void)setReservationInterval:(NSTimeInterval)secs; +- (NSDictionary *)responses; +- (NSUInteger)totalDataSize; +@end + +@interface GTMCookieStorage : NSObject { + @private + // The cookie storage object manages an array holding cookies, but the array + // is allocated externally (it may be in a fetcher object or the static + // fetcher cookie array.) See the fetcher's setCookieStorageMethod: + // for allocation of this object and assignment of its cookies array. + NSMutableArray *cookies_; +} + +// add all NSHTTPCookies in the supplied array to the storage array, +// replacing cookies in the storage array as appropriate +// Side effect: removes expired cookies from the storage array +- (void)setCookies:(NSArray *)newCookies; + +// retrieve all cookies appropriate for the given URL, considering +// domain, path, cookie name, expiration, security setting. +// Side effect: removes expired cookies from the storage array +- (NSArray *)cookiesForURL:(NSURL *)theURL; + +// return a cookie with the same name, domain, and path as the +// given cookie, or else return nil if none found +// +// Both the cookie being tested and all stored cookies should +// be valid (non-nil name, domains, paths) +- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie; + +// remove any expired cookies, excluding cookies with nil expirations +- (void)removeExpiredCookies; + +- (void)removeAllCookies; + +@end diff --git a/OAuth 2/GTMHTTPFetchHistory.m b/OAuth 2/GTMHTTPFetchHistory.m new file mode 100755 index 0000000..7bf0684 --- /dev/null +++ b/OAuth 2/GTMHTTPFetchHistory.m @@ -0,0 +1,590 @@ +/* Copyright (c) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTMHTTPFetchHistory.m +// + +#define GTMHTTPFETCHHISTORY_DEFINE_GLOBALS 1 + +#import "GTMHTTPFetchHistory.h" + +const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute +static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match"; +static NSString* const kGTMETagHeader = @"Etag"; + +@implementation GTMCookieStorage + +- (id)init { + self = [super init]; + if (self != nil) { + cookies_ = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc { + [cookies_ release]; + [super dealloc]; +} + +// add all cookies in the new cookie array to the storage, +// replacing stored cookies as appropriate +// +// Side effect: removes expired cookies from the storage array +- (void)setCookies:(NSArray *)newCookies { + + @synchronized(cookies_) { + [self removeExpiredCookies]; + + for (NSHTTPCookie *newCookie in newCookies) { + if ([[newCookie name] length] > 0 + && [[newCookie domain] length] > 0 + && [[newCookie path] length] > 0) { + + // remove the cookie if it's currently in the array + NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie]; + if (oldCookie) { + [cookies_ removeObjectIdenticalTo:oldCookie]; + } + + // make sure the cookie hasn't already expired + NSDate *expiresDate = [newCookie expiresDate]; + if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) { + [cookies_ addObject:newCookie]; + } + + } else { + NSAssert1(NO, @"Cookie incomplete: %@", newCookie); + } + } + } +} + +- (void)deleteCookie:(NSHTTPCookie *)cookie { + @synchronized(cookies_) { + NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie]; + if (foundCookie) { + [cookies_ removeObjectIdenticalTo:foundCookie]; + } + } +} + +// retrieve all cookies appropriate for the given URL, considering +// domain, path, cookie name, expiration, security setting. +// Side effect: removed expired cookies from the storage array +- (NSArray *)cookiesForURL:(NSURL *)theURL { + + NSMutableArray *foundCookies = nil; + + @synchronized(cookies_) { + [self removeExpiredCookies]; + + // we'll prepend "." to the desired domain, since we want the + // actual domain "nytimes.com" to still match the cookie domain + // ".nytimes.com" when we check it below with hasSuffix + NSString *host = [[theURL host] lowercaseString]; + NSString *path = [theURL path]; + NSString *scheme = [theURL scheme]; + + NSString *domain = nil; + BOOL isLocalhostRetrieval = NO; + + if ([host isEqual:@"localhost"]) { + isLocalhostRetrieval = YES; + } else { + if (host) { + domain = [@"." stringByAppendingString:host]; + } + } + + NSUInteger numberOfCookies = [cookies_ count]; + for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { + + NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx]; + + NSString *cookieDomain = [[storedCookie domain] lowercaseString]; + NSString *cookiePath = [storedCookie path]; + BOOL cookieIsSecure = [storedCookie isSecure]; + + BOOL isDomainOK; + + if (isLocalhostRetrieval) { + // prior to 10.5.6, the domain stored into NSHTTPCookies for localhost + // is "localhost.local" + isDomainOK = [cookieDomain isEqual:@"localhost"] + || [cookieDomain isEqual:@"localhost.local"]; + } else { + isDomainOK = [domain hasSuffix:cookieDomain]; + } + + BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath]; + BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"]; + + if (isDomainOK && isPathOK && isSecureOK) { + if (foundCookies == nil) { + foundCookies = [NSMutableArray arrayWithCapacity:1]; + } + [foundCookies addObject:storedCookie]; + } + } + } + return foundCookies; +} + +// return a cookie from the array with the same name, domain, and path as the +// given cookie, or else return nil if none found +// +// Both the cookie being tested and all cookies in the storage array should +// be valid (non-nil name, domains, paths) +// +// note: this should only be called from inside a @synchronized(cookies_) block +- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie { + + NSUInteger numberOfCookies = [cookies_ count]; + NSString *name = [cookie name]; + NSString *domain = [cookie domain]; + NSString *path = [cookie path]; + + NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)", + name, domain, path); + + for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { + + NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx]; + + if ([[storedCookie name] isEqual:name] + && [[storedCookie domain] isEqual:domain] + && [[storedCookie path] isEqual:path]) { + + return storedCookie; + } + } + return nil; +} + + +// internal routine to remove any expired cookies from the array, excluding +// cookies with nil expirations +// +// note: this should only be called from inside a @synchronized(cookies_) block +- (void)removeExpiredCookies { + + // count backwards since we're deleting items from the array + for (NSInteger idx = [cookies_ count] - 1; idx >= 0; idx--) { + + NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx]; + + NSDate *expiresDate = [storedCookie expiresDate]; + if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) { + [cookies_ removeObjectAtIndex:idx]; + } + } +} + +- (void)removeAllCookies { + @synchronized(cookies_) { + [cookies_ removeAllObjects]; + } +} +@end + +// +// GTMCachedURLResponse +// + +@implementation GTMCachedURLResponse + +@synthesize response = response_; +@synthesize data = data_; +@synthesize reservationDate = reservationDate_; +@synthesize useDate = useDate_; + +- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data { + self = [super init]; + if (self != nil) { + response_ = [response retain]; + data_ = [data retain]; + useDate_ = [[NSDate alloc] init]; + } + return self; +} + +- (void)dealloc { + [response_ release]; + [data_ release]; + [useDate_ release]; + [reservationDate_ release]; + [super dealloc]; +} + +- (NSString *)description { + NSString *reservationStr = reservationDate_ ? + [NSString stringWithFormat:@" resDate:%@", reservationDate_] : @""; + + return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}", + [self class], self, + data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil, + useDate_, + reservationStr]; +} + +- (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other { + return [useDate_ compare:[other useDate]]; +} + +@end + +// +// GTMURLCache +// + +@implementation GTMURLCache + +@dynamic memoryCapacity; + +- (id)init { + return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity]; +} + +- (id)initWithMemoryCapacity:(NSUInteger)totalBytes { + self = [super init]; + if (self != nil) { + memoryCapacity_ = totalBytes; + + responses_ = [[NSMutableDictionary alloc] initWithCapacity:5]; + + reservationInterval_ = kCachedURLReservationInterval; + } + return self; +} + +- (void)dealloc { + [responses_ release]; + [super dealloc]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p: {responses:%@}", + [self class], self, [responses_ allValues]]; +} + +// setters/getters + +- (void)pruneCacheResponses { + // internal routine to remove the least-recently-used responses when the + // cache has grown too large + if (memoryCapacity_ >= totalDataSize_) return; + + // sort keys by date + SEL sel = @selector(compareUseDate:); + NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel]; + + // the least-recently-used keys are at the beginning of the sorted array; + // remove those (except ones still reserved) until the total data size is + // reduced sufficiently + for (NSURL *key in sortedKeys) { + GTMCachedURLResponse *response = [responses_ objectForKey:key]; + + NSDate *resDate = [response reservationDate]; + BOOL isResponseReserved = (resDate != nil) + && ([resDate timeIntervalSinceNow] > -reservationInterval_); + + if (!isResponseReserved) { + // we can remove this response from the cache + NSUInteger storedSize = [[response data] length]; + totalDataSize_ -= storedSize; + [responses_ removeObjectForKey:key]; + } + + // if we've removed enough response data, then we're done + if (memoryCapacity_ >= totalDataSize_) break; + } +} + +- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse + forRequest:(NSURLRequest *)request { + @synchronized(self) { + // remove any previous entry for this request + [self removeCachedResponseForRequest:request]; + + // cache this one only if it's not bigger than our cache + NSUInteger storedSize = [[cachedResponse data] length]; + if (storedSize < memoryCapacity_) { + + NSURL *key = [request URL]; + [responses_ setObject:cachedResponse forKey:key]; + totalDataSize_ += storedSize; + + [self pruneCacheResponses]; + } + } +} + +- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request { + GTMCachedURLResponse *response; + + @synchronized(self) { + NSURL *key = [request URL]; + response = [[[responses_ objectForKey:key] retain] autorelease]; + + // touch the date to indicate this was recently retrieved + [response setUseDate:[NSDate date]]; + } + return response; +} + +- (void)removeCachedResponseForRequest:(NSURLRequest *)request { + @synchronized(self) { + NSURL *key = [request URL]; + totalDataSize_ -= [[[responses_ objectForKey:key] data] length]; + [responses_ removeObjectForKey:key]; + } +} + +- (void)removeAllCachedResponses { + @synchronized(self) { + [responses_ removeAllObjects]; + totalDataSize_ = 0; + } +} + +- (NSUInteger)memoryCapacity { + return memoryCapacity_; +} + +- (void)setMemoryCapacity:(NSUInteger)totalBytes { + @synchronized(self) { + BOOL didShrink = (totalBytes < memoryCapacity_); + memoryCapacity_ = totalBytes; + + if (didShrink) { + [self pruneCacheResponses]; + } + } +} + +// methods for unit testing +- (void)setReservationInterval:(NSTimeInterval)secs { + reservationInterval_ = secs; +} + +- (NSDictionary *)responses { + return responses_; +} + +- (NSUInteger)totalDataSize { + return totalDataSize_; +} + +@end + +// +// GTMHTTPFetchHistory +// + +@interface GTMHTTPFetchHistory () +- (NSString *)cachedETagForRequest:(NSURLRequest *)request; +- (void)removeCachedDataForRequest:(NSURLRequest *)request; +@end + +@implementation GTMHTTPFetchHistory + +@synthesize cookieStorage = cookieStorage_; + +@dynamic shouldRememberETags; +@dynamic shouldCacheETaggedData; +@dynamic memoryCapacity; + +- (id)init { + return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity + shouldCacheETaggedData:NO]; +} + +- (id)initWithMemoryCapacity:(NSUInteger)totalBytes + shouldCacheETaggedData:(BOOL)shouldCacheETaggedData { + self = [super init]; + if (self != nil) { + etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes]; + shouldRememberETags_ = shouldCacheETaggedData; + shouldCacheETaggedData_ = shouldCacheETaggedData; + cookieStorage_ = [[GTMCookieStorage alloc] init]; + } + return self; +} + +- (void)dealloc { + [etaggedDataCache_ release]; + [cookieStorage_ release]; + [super dealloc]; +} + +- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet { + if ([self shouldRememberETags]) { + // If this URL is in the history, and no ETag has been set, then + // set the ETag header field + + // if we have a history, we're tracking across fetches, so we don't + // want to pull results from any other cache + [request setCachePolicy:NSURLRequestReloadIgnoringCacheData]; + + if (isHTTPGet) { + // we'll only add an ETag if there's no ETag specified in the user's + // request + NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader]; + if (specifiedETag == nil) { + // no ETag: extract the previous ETag for this request from the + // fetch history, and add it to the request + NSString *cachedETag = [self cachedETagForRequest:request]; + + if (cachedETag != nil) { + [request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader]; + } + } else { + // has an ETag: remove any stored response in the fetch history + // for this request, as the If-None-Match header could lead to + // a 304 Not Modified, and we want that error delivered to the + // user since they explicitly specified the ETag + [self removeCachedDataForRequest:request]; + } + } + } +} + +- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request + response:(NSURLResponse *)response + downloadedData:(NSData *)downloadedData { + if (![self shouldRememberETags]) return; + + if (![response respondsToSelector:@selector(allHeaderFields)]) return; + + NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; + + if (statusCode != kGTMHTTPFetcherStatusNotModified) { + // save this ETag string for successful results (<300) + // If there's no last modified string, clear the dictionary + // entry for this URL. Also cache or delete the data, if appropriate + // (when etaggedDataCache is non-nil.) + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + NSString* etag = [headers objectForKey:kGTMETagHeader]; + + if (etag != nil && statusCode < 300) { + + // we want to cache responses for the headers, even if the client + // doesn't want the response body data caches + NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil; + + GTMCachedURLResponse *cachedResponse; + cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response + data:dataToStore] autorelease]; + [etaggedDataCache_ storeCachedResponse:cachedResponse + forRequest:request]; + } else { + [etaggedDataCache_ removeCachedResponseForRequest:request]; + } + } +} + +- (NSString *)cachedETagForRequest:(NSURLRequest *)request { + GTMCachedURLResponse *cachedResponse; + cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request]; + + NSURLResponse *response = [cachedResponse response]; + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + NSString *cachedETag = [headers objectForKey:kGTMETagHeader]; + if (cachedETag) { + // since the request having an ETag implies this request is about + // to be fetched again, reserve the cached response to ensure that + // that it will be around at least until the fetch completes + // + // when the fetch completes, either the cached response will be replaced + // with a new response, or the cachedDataForRequest: method below will + // clear the reservation + [cachedResponse setReservationDate:[NSDate date]]; + } + return cachedETag; +} + +- (NSData *)cachedDataForRequest:(NSURLRequest *)request { + GTMCachedURLResponse *cachedResponse; + cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request]; + + NSData *cachedData = [cachedResponse data]; + + // since the data for this cached request is being obtained from the cache, + // we can clear the reservation as the fetch has completed + [cachedResponse setReservationDate:nil]; + + return cachedData; +} + +- (void)removeCachedDataForRequest:(NSURLRequest *)request { + [etaggedDataCache_ removeCachedResponseForRequest:request]; +} + +- (void)clearETaggedDataCache { + [etaggedDataCache_ removeAllCachedResponses]; +} + +- (void)clearHistory { + [self clearETaggedDataCache]; + [cookieStorage_ removeAllCookies]; +} + +- (void)removeAllCookies { + [cookieStorage_ removeAllCookies]; +} + +- (BOOL)shouldRememberETags { + return shouldRememberETags_; +} + +- (void)setShouldRememberETags:(BOOL)flag { + BOOL wasRemembering = shouldRememberETags_; + shouldRememberETags_ = flag; + + if (wasRemembering && !flag) { + // free up the cache memory + [self clearETaggedDataCache]; + } +} + +- (BOOL)shouldCacheETaggedData { + return shouldCacheETaggedData_; +} + +- (void)setShouldCacheETaggedData:(BOOL)flag { + BOOL wasCaching = shouldCacheETaggedData_; + shouldCacheETaggedData_ = flag; + + if (flag) { + self.shouldRememberETags = YES; + } + + if (wasCaching && !flag) { + // users expect turning off caching to free up the cache memory + [self clearETaggedDataCache]; + } +} + +- (NSUInteger)memoryCapacity { + return [etaggedDataCache_ memoryCapacity]; +} + +- (void)setMemoryCapacity:(NSUInteger)totalBytes { + [etaggedDataCache_ setMemoryCapacity:totalBytes]; +} + +@end diff --git a/OAuth 2/GTMHTTPFetcher.h b/OAuth 2/GTMHTTPFetcher.h new file mode 100755 index 0000000..dedc4c0 --- /dev/null +++ b/OAuth 2/GTMHTTPFetcher.h @@ -0,0 +1,704 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTMHTTPFetcher.h +// + +// This is essentially a wrapper around NSURLConnection for POSTs and GETs. +// If setPostData: is called, then POST is assumed. +// +// When would you use this instead of NSURLConnection? +// +// - When you just want the result from a GET, POST, or PUT +// - When you want the "standard" behavior for connections (redirection handling +// an so on) +// - When you want automatic retry on failures +// - When you want to avoid cookie collisions with Safari and other applications +// - When you are fetching resources with ETags and want to avoid the overhead +// of repeated fetches of unchanged data +// - When you need to set a credential for the http operation +// +// This is assumed to be a one-shot fetch request; don't reuse the object +// for a second fetch. +// +// The fetcher may be created auto-released, in which case it will release +// itself after the fetch completion callback. The fetcher is implicitly +// retained as long as a connection is pending. +// +// But if you may need to cancel the fetcher, retain it and have the delegate +// release the fetcher in the callbacks. +// +// Sample usage: +// +// NSURLRequest *request = [NSURLRequest requestWithURL:myURL]; +// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher fetcherWithRequest:request]; +// +// // optional upload body data +// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]]; +// +// [myFetcher beginFetchWithDelegate:self +// didFinishSelector:@selector(myFetcher:finishedWithData:error:)]; +// +// Upon fetch completion, the callback selector is invoked; it should have +// this signature (you can use any callback method name you want so long as +// the signature matches this): +// +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData error:(NSError *)error; +// +// The block callback version looks like: +// +// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) { +// if (error != nil) { +// // status code or network error +// } else { +// // succeeded +// } +// }]; + +// +// NOTE: Fetches may retrieve data from the server even though the server +// returned an error. The failure selector is called when the server +// status is >= 300, with an NSError having domain +// kGTMHTTPFetcherStatusDomain and code set to the server status. +// +// Status codes are at +// +// +// Downloading to disk: +// +// To have downloaded data saved directly to disk, specify either a path for the +// downloadPath property, or a file handle for the downloadFileHandle property. +// When downloading to disk, callbacks will be passed a nil for the NSData* +// arguments. +// +// +// HTTP methods and headers: +// +// Alternative HTTP methods, like PUT, and custom headers can be specified by +// creating the fetcher with an appropriate NSMutableURLRequest +// +// +// Proxies: +// +// Proxy handling is invisible so long as the system has a valid credential in +// the keychain, which is normally true (else most NSURL-based apps would have +// difficulty.) But when there is a proxy authetication error, the the fetcher +// will call the failedWithError: method with the NSURLChallenge in the error's +// userInfo. The error method can get the challenge info like this: +// +// NSURLAuthenticationChallenge *challenge +// = [[error userInfo] objectForKey:kGTMHTTPFetcherErrorChallengeKey]; +// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy]; +// +// If a proxy error occurs, you can ask the user for the proxy username/password +// and call fetcher's setProxyCredential: to provide those for the +// next attempt to fetch. +// +// +// Cookies: +// +// There are three supported mechanisms for remembering cookies between fetches. +// +// By default, GTMHTTPFetcher uses a mutable array held statically to track +// cookies for all instantiated fetchers. This avoids server cookies being set +// by servers for the application from interfering with Safari cookie settings, +// and vice versa. The fetcher cookies are lost when the application quits. +// +// To rely instead on WebKit's global NSHTTPCookieStorage, call +// setCookieStorageMethod: with kGTMHTTPFetcherCookieStorageMethodSystemDefault. +// +// If the fetcher is created from a GTMHTTPFetcherService object +// then the cookie storage mechanism is set to use the cookie storage in the +// service object rather than the static storage. +// +// +// Fetching for periodic checks: +// +// The fetcher object tracks ETag headers from responses and +// provide an "If-None-Match" header. This allows the server to save +// bandwidth by providing a status message instead of repeated response +// data. +// +// To get this behavior, create the fetcher from an GTMHTTPFetcherService object +// and look for a fetch callback error with code 304 +// (kGTMHTTPFetcherStatusNotModified) like this: +// +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error { +// if ([error code] == kGTMHTTPFetcherStatusNotModified) { +// // |data| is empty; use the data from the previous finishedWithData: for this URL +// } else { +// // handle other server status code +// } +// } +// +// +// Monitoring received data +// +// The optional received data selector can be set with setReceivedDataSelector: +// and should have the signature +// +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; +// +// The number bytes received so far is available as [fetcher downloadedLength]. +// This number may go down if a redirect causes the download to begin again from +// a new server. +// +// If supplied by the server, the anticipated total download size is available +// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown +// download sizes.) +// +// +// Automatic retrying of fetches +// +// The fetcher can optionally create a timer and reattempt certain kinds of +// fetch failures (status codes 408, request timeout; 503, service unavailable; +// 504, gateway timeout; networking errors NSURLErrorTimedOut and +// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to +// customize the type of errors which will be retried. +// +// Retries are done in an exponential-backoff fashion (that is, after 1 second, +// 2, 4, 8, and so on.) +// +// Enabling automatic retries looks like this: +// [myFetcher setRetryEnabled:YES]; +// +// With retries enabled, the success or failure callbacks are called only +// when no more retries will be attempted. Calling the fetcher's stopFetching +// method will terminate the retry timer, without the finished or failure +// selectors being invoked. +// +// Optionally, the client may set the maximum retry interval: +// [myFetcher setMaxRetryInterval:60.0]; // in seconds; default is 60 seconds +// // for downloads, 600 for uploads +// +// Also optionally, the client may provide a callback selector to determine +// if a status code or other error should be retried. +// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)]; +// +// If set, the retry selector should have the signature: +// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error +// and return YES to set the retry timer or NO to fail without additional +// fetch attempts. +// +// The retry method may return the |suggestedWillRetry| argument to get the +// default retry behavior. Server status codes are present in the +// error argument, and have the domain kGTMHTTPFetcherStatusDomain. The +// user's method may look something like this: +// +// -(BOOL)myFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error { +// +// // perhaps examine [error domain] and [error code], or [fetcher retryCount] +// // +// // return YES to start the retry timer, NO to proceed to the failure +// // callback, or |suggestedWillRetry| to get default behavior for the +// // current error domain and code values. +// return suggestedWillRetry; +// } + + + +#pragma once + +#import + +#if defined(GTL_TARGET_NAMESPACE) + // we're using target namespace macros + #import "GTLDefines.h" +#elif defined(GDATA_TARGET_NAMESPACE) + #import "GDataDefines.h" +#else + #if TARGET_OS_IPHONE + #ifndef GTM_FOUNDATION_ONLY + #define GTM_FOUNDATION_ONLY 1 + #endif + #ifndef GTM_IPHONE + #define GTM_IPHONE 1 + #endif + #endif +#endif + +#if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000) + #define GTM_BACKGROUND_FETCHING 1 +#endif + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS + #define _EXTERN + #define _INITIALIZE_AS(x) =x +#else + #if defined(__cplusplus) + #define _EXTERN extern "C" + #else + #define _EXTERN extern + #endif + #define _INITIALIZE_AS(x) +#endif + +// notifications +// +// fetch started and stopped, and fetch retry delay started and stopped +_EXTERN NSString* const kGTMHTTPFetcherStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStartedNotification"); +_EXTERN NSString* const kGTMHTTPFetcherStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStoppedNotification"); +_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStartedNotification"); +_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStoppedNotification"); + +// callback constants +_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GTMHTTPFetcher"); +_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus"); +_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge"); +_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // data returned with a kGTMHTTPFetcherStatusDomain error + +enum { + kGTMHTTPFetcherErrorDownloadFailed = -1, + kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2, + kGTMHTTPFetcherErrorChunkUploadFailed = -3, + kGTMHTTPFetcherErrorFileHandleException = -4, + kGTMHTTPFetcherErrorBackgroundExpiration = -6, + + // The code kGTMHTTPFetcherErrorAuthorizationFailed (-5) has been removed; + // look for status 401 instead. + + kGTMHTTPFetcherStatusNotModified = 304, + kGTMHTTPFetcherStatusBadRequest = 400, + kGTMHTTPFetcherStatusUnauthorized = 401, + kGTMHTTPFetcherStatusForbidden = 403, + kGTMHTTPFetcherStatusPreconditionFailed = 412 +}; + +// cookie storage methods +enum { + kGTMHTTPFetcherCookieStorageMethodStatic = 0, + kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1, + kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2, + kGTMHTTPFetcherCookieStorageMethodNone = 3 +}; + +#ifdef __cplusplus +extern "C" { +#endif + +void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...); + +// Utility functions for applications self-identifying to servers via a +// user-agent header + +// Make a proper app name without whitespace from the given string, removing +// whitespace and other characters that may be special parsed marks of +// the full user-agent string. +NSString *GTMCleanedUserAgentString(NSString *str); + +// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1" +NSString *GTMSystemVersionString(void); + +// Make a generic name and version for the current application, like +// com.example.MyApp/1.2.3 relying on the bundle identifier and the +// CFBundleShortVersionString or CFBundleVersion. If no bundle ID +// is available, the process name preceded by "proc_" is used. +NSString *GTMApplicationIdentifier(NSBundle *bundle); + +#ifdef __cplusplus +} // extern "C" +#endif + +@class GTMHTTPFetcher; + +@protocol GTMCookieStorageProtocol +// This protocol allows us to call into the service without requiring +// GTMCookieStorage sources in this project +// +// The public interface for cookie handling is the GTMCookieStorage class, +// accessible from a fetcher service object's fetchHistory or from the fetcher's +// +staticCookieStorage method. +- (NSArray *)cookiesForURL:(NSURL *)theURL; +- (void)setCookies:(NSArray *)newCookies; +@end + +@protocol GTMHTTPFetchHistoryProtocol +// This protocol allows us to call the fetch history object without requiring +// GTMHTTPFetchHistory sources in this project +- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet; +- (BOOL)shouldCacheETaggedData; +- (NSData *)cachedDataForRequest:(NSURLRequest *)request; +- (id )cookieStorage; +- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request + response:(NSURLResponse *)response + downloadedData:(NSData *)downloadedData; +- (void)removeCachedDataForRequest:(NSURLRequest *)request; +@end + +@protocol GTMHTTPFetcherServiceProtocol +// This protocol allows us to call into the service without requiring +// GTMHTTPFetcherService sources in this project +- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher; +- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher; + +- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request; +- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher; +@end + +@protocol GTMFetcherAuthorizationProtocol +@required +// This protocol allows us to call the authorizer without requiring its sources +// in this project +- (void)authorizeRequest:(NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel; + +- (void)stopAuthorization; + +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request; + +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request; + +- (NSString *)userEmail; + +@optional +@property (assign) id fetcherService; // WEAK + +- (BOOL)primeForRefresh; +@end + +// GTMHTTPFetcher objects are used for async retrieval of an http get or post +// +// See additional comments at the beginning of this file +@interface GTMHTTPFetcher : NSObject { + @protected + NSMutableURLRequest *request_; + NSURLConnection *connection_; + NSMutableData *downloadedData_; + NSString *downloadPath_; + NSString *temporaryDownloadPath_; + NSFileHandle *downloadFileHandle_; + unsigned long long downloadedLength_; + NSURLCredential *credential_; // username & password + NSURLCredential *proxyCredential_; // credential supplied to proxy servers + NSData *postData_; + NSInputStream *postStream_; + NSMutableData *loggedStreamData_; + NSURLResponse *response_; // set in connection:didReceiveResponse: + id delegate_; + SEL finishedSel_; // should by implemented by delegate + SEL sentDataSel_; // optional, set with setSentDataSelector + SEL receivedDataSel_; // optional, set with setReceivedDataSelector +#if NS_BLOCKS_AVAILABLE + void (^completionBlock_)(NSData *, NSError *); + void (^receivedDataBlock_)(NSData *); + void (^sentDataBlock_)(NSInteger, NSInteger, NSInteger); + BOOL (^retryBlock_)(BOOL, NSError *); +#elif !__LP64__ + // placeholders: for 32-bit builds, keep the size of the object's ivar section + // the same with and without blocks + id completionPlaceholder_; + id receivedDataPlaceholder_; + id sentDataPlaceholder_; + id retryPlaceholder_; +#endif + BOOL hasConnectionEnded_; // set if the connection need not be cancelled + BOOL isCancellingChallenge_; // set only when cancelling an auth challenge + BOOL isStopNotificationNeeded_; // set when start notification has been sent + BOOL shouldFetchInBackground_; +#if GTM_BACKGROUND_FETCHING + NSUInteger backgroundTaskIdentifer_; // UIBackgroundTaskIdentifier +#endif + id userData_; // retained, if set by caller + NSMutableDictionary *properties_; // more data retained for caller + NSArray *runLoopModes_; // optional, for 10.5 and later + id fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies + NSInteger cookieStorageMethod_; // constant from above + id cookieStorage_; + + id authorizer_; + + // the service object that created and monitors this fetcher, if any + id service_; + NSString *serviceHost_; + NSInteger servicePriority_; + NSThread *thread_; + + BOOL isRetryEnabled_; // user wants auto-retry + SEL retrySel_; // optional; set with setRetrySelector + NSTimer *retryTimer_; + NSUInteger retryCount_; + NSTimeInterval maxRetryInterval_; // default 600 seconds + NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds + NSTimeInterval retryFactor_; // default interval multiplier is 2 + NSTimeInterval lastRetryInterval_; + BOOL hasAttemptedAuthRefresh_; + + NSString *comment_; // comment for log + NSString *log_; +} + +// Create a fetcher +// +// fetcherWithRequest will return an autoreleased fetcher, but if +// the connection is successfully created, the connection should retain the +// fetcher for the life of the connection as well. So the caller doesn't have +// to retain the fetcher explicitly unless they want to be able to cancel it. ++ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request; + +// Convenience methods that make a request, like +fetcherWithRequest ++ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL; ++ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString; + +// Designated initializer +- (id)initWithRequest:(NSURLRequest *)request; + +// Fetcher request +// +// The underlying request is mutable and may be modified by the caller +@property (retain) NSMutableURLRequest *mutableRequest; + +// Setting the credential is optional; it is used if the connection receives +// an authentication challenge +@property (retain) NSURLCredential *credential; + +// Setting the proxy credential is optional; it is used if the connection +// receives an authentication challenge from a proxy +@property (retain) NSURLCredential *proxyCredential; + +// If post data or stream is not set, then a GET retrieval method is assumed +@property (retain) NSData *postData; +@property (retain) NSInputStream *postStream; + +// The default cookie storage method is kGTMHTTPFetcherCookieStorageMethodStatic +// without a fetch history set, and kGTMHTTPFetcherCookieStorageMethodFetchHistory +// with a fetch history set +// +// Applications needing control of cookies across a sequence of fetches should +// create fetchers from a GTMHTTPFetcherService object (which encapsulates +// fetch history) for a well-defined cookie store +@property (assign) NSInteger cookieStorageMethod; + ++ (id )staticCookieStorage; + +// Object to add authorization to the request, if needed +@property (retain) id authorizer; + +// The service object that created and monitors this fetcher, if any +@property (retain) id service; + +// The host, if any, used to classify this fetcher in the fetcher service +@property (copy) NSString *serviceHost; + +// The priority, if any, used for starting fetchers in the fetcher service +// +// Lower values are higher priority; the default is 0, and values may +// be negative or positive. This priority affects only the start order of +// fetchers that are being delayed by a fetcher service. +@property (assign) NSInteger servicePriority; + +// The thread used to run this fetcher in the fetcher service +@property (retain) NSThread *thread; + +// The delegate is retained during the connection +@property (retain) id delegate; + +// On iOS 4 and later, the fetch may optionally continue while the app is in the +// background until finished or stopped by OS expiration +// +// The default value is NO +// +// For Mac OS X, background fetches are always supported, and this property +// is ignored +@property (assign) BOOL shouldFetchInBackground; + +// The delegate's optional sentData selector may be used to monitor upload +// progress. It should have a signature like: +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher +// didSendBytes:(NSInteger)bytesSent +// totalBytesSent:(NSInteger)totalBytesSent +// totalBytesExpectedToSend:(NSInteger)totalBytesExpectedToSend; +// +// +doesSupportSentDataCallback indicates if this delegate method is supported ++ (BOOL)doesSupportSentDataCallback; + +@property (assign) SEL sentDataSelector; + +// The delegate's optional receivedData selector may be used to monitor download +// progress. It should have a signature like: +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher +// receivedData:(NSData *)dataReceivedSoFar; +// +// The dataReceived argument will be nil when downloading to a path or to a +// file handle. +// +// Applications should not use this method to accumulate the received data; +// the callback method or block supplied to the beginFetch call will have +// the complete NSData received. +@property (assign) SEL receivedDataSelector; + +#if NS_BLOCKS_AVAILABLE +// The full interface to the block is provided rather than just a typedef for +// its parameter list in order to get more useful code completion in the Xcode +// editor +@property (copy) void (^sentDataBlock)(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger bytesExpectedToSend); + +// The dataReceived argument will be nil when downloading to a path or to +// a file handle +@property (copy) void (^receivedDataBlock)(NSData *dataReceivedSoFar); +#endif + +// retrying; see comments at the top of the file. Calling +// setRetryEnabled(YES) resets the min and max retry intervals. +@property (assign, getter=isRetryEnabled) BOOL retryEnabled; + +// Retry selector or block is optional for retries. +// +// If present, it should have the signature: +// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error +// and return YES to cause a retry. See comments at the top of this file. +@property (assign) SEL retrySelector; + +#if NS_BLOCKS_AVAILABLE +@property (copy) BOOL (^retryBlock)(BOOL suggestedWillRetry, NSError *error); +#endif + +// Retry intervals must be strictly less than maxRetryInterval, else +// they will be limited to maxRetryInterval and no further retries will +// be attempted. Setting maxRetryInterval to 0.0 will reset it to the +// default value, 600 seconds. + +@property (assign) NSTimeInterval maxRetryInterval; + +// Starting retry interval. Setting minRetryInterval to 0.0 will reset it +// to a random value between 1.0 and 2.0 seconds. Clients should normally not +// call this except for unit testing. +@property (assign) NSTimeInterval minRetryInterval; + +// Multiplier used to increase the interval between retries, typically 2.0. +// Clients should not need to call this. +@property (assign) double retryFactor; + +// Number of retries attempted +@property (readonly) NSUInteger retryCount; + +// interval delay to precede next retry +@property (readonly) NSTimeInterval nextRetryInterval; + +// Begin fetching the request +// +// The delegate can optionally implement the finished selectors or pass NULL +// for it. +// +// Returns YES if the fetch is initiated. The delegate is retained between +// the beginFetch call until after the finish callback. +// +// An error is passed to the callback for server statuses 300 or +// higher, with the status stored as the error object's code. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error; +// +// If the application has specified a downloadPath or downloadFileHandle +// for the fetcher, the data parameter passed to the callback will be nil. + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL; + +#if NS_BLOCKS_AVAILABLE +- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler; +#endif + + +// Returns YES if this is in the process of fetching a URL +- (BOOL)isFetching; + +// Cancel the fetch of the request that's currently in progress +- (void)stopFetching; + +// Return the status code from the server response +@property (readonly) NSInteger statusCode; + +// Return the http headers from the response +@property (retain, readonly) NSDictionary *responseHeaders; + +// The response, once it's been received +@property (retain) NSURLResponse *response; + +// Bytes downloaded so far +@property (readonly) unsigned long long downloadedLength; + +// Buffer of currently-downloaded data +@property (readonly, retain) NSData *downloadedData; + +// Path in which to non-atomically create a file for storing the downloaded data +// +// The path must be set before fetching begins. The download file handle +// will be created for the path, and can be used to monitor progress. If a file +// already exists at the path, it will be overwritten. +@property (copy) NSString *downloadPath; + +// If downloadFileHandle is set, data received is immediately appended to +// the file handle rather than being accumulated in the downloadedData property +// +// The file handle supplied must allow writing and support seekToFileOffset:, +// and must be set before fetching begins. Setting a download path will +// override the file handle property. +@property (retain) NSFileHandle *downloadFileHandle; + +// The optional fetchHistory object is used for a sequence of fetchers to +// remember ETags, cache ETagged data, and store cookies. Typically, this +// is set by a GTMFetcherService object when it creates a fetcher. +// +// Side effect: setting fetch history implicitly calls setCookieStorageMethod: +@property (retain) id fetchHistory; + +// userData is retained for the convenience of the caller +@property (retain) id userData; + +// Stored property values are retained for the convenience of the caller +@property (copy) NSMutableDictionary *properties; + +- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property +- (id)propertyForKey:(NSString *)key; + +- (void)addPropertiesFromDictionary:(NSDictionary *)dict; + +// Comments are useful for logging +@property (copy) NSString *comment; + +- (void)setCommentWithFormat:(id)format, ...; + +// Log of request and response, if logging is enabled +@property (copy) NSString *log; + +// Using the fetcher while a modal dialog is displayed requires setting the +// run-loop modes to include NSModalPanelRunLoopMode +@property (retain) NSArray *runLoopModes; + +// Users who wish to replace GTMHTTPFetcher's use of NSURLConnection +// can do so globally here. The replacement should be a subclass of +// NSURLConnection. ++ (Class)connectionClass; ++ (void)setConnectionClass:(Class)theClass; + +// Spin the run loop, discarding events, until the fetch has completed +// +// This is only for use in testing or in tools without a user interface. +// +// Synchronous fetches should never be done by shipping apps; they are +// sufficient reason for rejection from the app store. +- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds; + +#if STRIP_GTM_FETCH_LOGGING +// if logging is stripped, provide a stub for the main method +// for controlling logging ++ (void)setLoggingEnabled:(BOOL)flag; +#endif // STRIP_GTM_FETCH_LOGGING + +@end diff --git a/OAuth 2/GTMHTTPFetcher.m b/OAuth 2/GTMHTTPFetcher.m new file mode 100755 index 0000000..c7a7198 --- /dev/null +++ b/OAuth 2/GTMHTTPFetcher.m @@ -0,0 +1,1746 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTMHTTPFetcher.m +// + +#define GTMHTTPFETCHER_DEFINE_GLOBALS 1 + +#import "GTMHTTPFetcher.h" + +#if GTM_BACKGROUND_FETCHING +#import +#endif + +static id gGTMFetcherStaticCookieStorage = nil; +static Class gGTMFetcherConnectionClass = nil; + +// the default max retry interview is 10 minutes for uploads (POST/PUT/PATCH), +// 1 minute for downloads +const NSTimeInterval kUnsetMaxRetryInterval = -1; +const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0; +const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.; + +// +// GTMHTTPFetcher +// + +@interface GTMHTTPFetcher () + +@property (copy) NSString *temporaryDownloadPath; +@property (retain) id cookieStorage; +@property (readwrite, retain) NSData *downloadedData; +#if NS_BLOCKS_AVAILABLE +@property (copy) void (^completionBlock)(NSData *, NSError *); +#endif + +- (BOOL)beginFetchMayDelay:(BOOL)mayDelay + mayAuthorize:(BOOL)mayAuthorize; +- (void)failToBeginFetchWithError:(NSError *)error; + +#if GTM_BACKGROUND_FETCHING +- (void)endBackgroundTask; +- (void)backgroundFetchExpired; +#endif + +- (BOOL)authorizeRequest; +- (void)authorizer:(id )auth + request:(NSMutableURLRequest *)request + finishedWithError:(NSError *)error; + +- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath; +- (NSFileManager *)fileManager; +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks; +- (BOOL)shouldReleaseCallbacksUponCompletion; + +- (void)addCookiesToRequest:(NSMutableURLRequest *)request; +- (void)handleCookiesForResponse:(NSURLResponse *)response; + +- (void)logNowWithError:(NSError *)error; + +- (void)invokeFetchCallbacksWithData:(NSData *)data + error:(NSError *)error; +- (void)invokeFetchCallback:(SEL)sel + target:(id)target + data:(NSData *)data + error:(NSError *)error; +- (void)releaseCallbacks; + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; + +- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error; +- (void)destroyRetryTimer; +- (void)beginRetryTimer; +- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs; +- (void)sendStopNotificationIfNeeded; +- (void)retryFetch; +- (void)retryTimerFired:(NSTimer *)timer; +@end + +@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal) +- (void)setupStreamLogging; +- (void)logFetchWithError:(NSError *)error; +@end + +@implementation GTMHTTPFetcher + ++ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request { + return [[[[self class] alloc] initWithRequest:request] autorelease]; +} + ++ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL { + return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]]; +} + ++ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString { + return [self fetcherWithURL:[NSURL URLWithString:requestURLString]]; +} + ++ (void)initialize { + // note that initialize is guaranteed by the runtime to be called in a + // thread-safe manner + if (!gGTMFetcherStaticCookieStorage) { + Class cookieStorageClass = NSClassFromString(@"GTMCookieStorage"); + if (cookieStorageClass) { + gGTMFetcherStaticCookieStorage = [[cookieStorageClass alloc] init]; + } + } +} + +- (id)init { + return [self initWithRequest:nil]; +} + +- (id)initWithRequest:(NSURLRequest *)request { + self = [super init]; + if (self) { + request_ = [request mutableCopy]; + + if (gGTMFetcherStaticCookieStorage != nil) { + // the user has compiled with the cookie storage class available; + // default to static cookie storage, so our cookies are independent + // of the cookies of other apps + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; + } else { + // default to system default cookie storage + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodSystemDefault]; + } + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + // disallow use of fetchers in a copy property + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p (%@)", + [self class], self, [self.mutableRequest URL]]; +} + +#if !GTM_IPHONE +- (void)finalize { + [self stopFetchReleasingCallbacks:YES]; // releases connection_, destroys timers + [super finalize]; +} +#endif + +- (void)dealloc { +#if DEBUG + NSAssert(!isStopNotificationNeeded_, + @"unbalanced fetcher notification for %@", [request_ URL]); +#endif + + // Note: if a connection or a retry timer was pending, then this instance + // would be retained by those so it wouldn't be getting dealloc'd, + // hence we don't need to stopFetch here + [request_ release]; + [connection_ release]; + [downloadedData_ release]; + [downloadPath_ release]; + [temporaryDownloadPath_ release]; + [downloadFileHandle_ release]; + [credential_ release]; + [proxyCredential_ release]; + [postData_ release]; + [postStream_ release]; + [loggedStreamData_ release]; + [response_ release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock_ release]; + [receivedDataBlock_ release]; + [sentDataBlock_ release]; + [retryBlock_ release]; +#endif + [userData_ release]; + [properties_ release]; + [runLoopModes_ release]; + [fetchHistory_ release]; + [cookieStorage_ release]; + [authorizer_ release]; + [service_ release]; + [serviceHost_ release]; + [thread_ release]; + [retryTimer_ release]; + [comment_ release]; + [log_ release]; + + [super dealloc]; +} + +#pragma mark - + +// Begin fetching the URL (or begin a retry fetch). The delegate is retained +// for the duration of the fetch connection. + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSelector { + GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0); + GTMAssertSelectorNilOrImplementedWithArgs(delegate, receivedDataSel_, @encode(GTMHTTPFetcher *), @encode(NSData *), 0); + GTMAssertSelectorNilOrImplementedWithArgs(delegate, retrySel_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), 0); + + // We'll retain the delegate only during the outstanding connection (similar + // to what Cocoa does with performSelectorOnMainThread:) and during + // authorization or delays, since the app would crash + // if the delegate was released before the fetch calls back + [self setDelegate:delegate]; + finishedSel_ = finishedSelector; + + return [self beginFetchMayDelay:YES + mayAuthorize:YES]; +} + +- (BOOL)beginFetchMayDelay:(BOOL)mayDelay + mayAuthorize:(BOOL)mayAuthorize { + // This is the internal entry point for re-starting fetches + NSError *error = nil; + + if (connection_ != nil) { + NSAssert1(connection_ != nil, @"fetch object %@ being reused; this should never happen", self); + goto CannotBeginFetch; + } + + if (request_ == nil) { + NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request"); + goto CannotBeginFetch; + } + + self.downloadedData = nil; + downloadedLength_ = 0; + + if (mayDelay && service_) { + BOOL shouldFetchNow = [service_ fetcherShouldBeginFetching:self]; + if (!shouldFetchNow) { + // the fetch is deferred, but will happen later + return YES; + } + } + + NSString *effectiveHTTPMethod = [request_ valueForHTTPHeaderField:@"X-HTTP-Method-Override"]; + if (effectiveHTTPMethod == nil) { + effectiveHTTPMethod = [request_ HTTPMethod]; + } + BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil + || [effectiveHTTPMethod isEqual:@"GET"]); + + if (postData_ || postStream_) { + if (isEffectiveHTTPGet) { + [request_ setHTTPMethod:@"POST"]; + isEffectiveHTTPGet = NO; + } + + if (postData_) { + [request_ setHTTPBody:postData_]; + } else { + if ([self respondsToSelector:@selector(setupStreamLogging)]) { + [self performSelector:@selector(setupStreamLogging)]; + } + + [request_ setHTTPBodyStream:postStream_]; + } + } + + // We authorize after setting up the http method and body in the request + // because OAuth 1 may need to sign the request body + if (mayAuthorize && authorizer_) { + BOOL isAuthorized = [authorizer_ isAuthorizedRequest:request_]; + if (!isAuthorized) { + // authorization needed + return [self authorizeRequest]; + } + } + + [fetchHistory_ updateRequest:request_ isHTTPGet:isEffectiveHTTPGet]; + + // set the default upload or download retry interval, if necessary + if (isRetryEnabled_ + && maxRetryInterval_ <= kUnsetMaxRetryInterval) { + if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) { + [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval]; + } else { + [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval]; + } + } + + [self addCookiesToRequest:request_]; + + if (downloadPath_ != nil) { + // downloading to a path, so create a temporary file and a file handle for + // downloading + NSString *tempPath = [self createTempDownloadFilePathForPath:downloadPath_]; + + BOOL didCreate = [[NSData data] writeToFile:tempPath + options:0 + error:&error]; + if (!didCreate) goto CannotBeginFetch; + + [self setTemporaryDownloadPath:tempPath]; + + NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:tempPath]; + if (fh == nil) goto CannotBeginFetch; + + [self setDownloadFileHandle:fh]; + } + + // finally, start the connection + + Class connectionClass = [[self class] connectionClass]; + + NSArray *runLoopModes = nil; + + // use the connection-specific run loop modes, if they were provided, + // or else use the GTMHTTPFetcher default run loop modes, if any + if (runLoopModes_) { + runLoopModes = runLoopModes_; + } + + if ([runLoopModes count] == 0) { + + // if no run loop modes were specified, then we'll start the connection + // on the current run loop in the current mode + connection_ = [[connectionClass connectionWithRequest:request_ + delegate:self] retain]; + } else { + + // schedule on current run loop in the specified modes + connection_ = [[connectionClass alloc] initWithRequest:request_ + delegate:self + startImmediately:NO]; + for (NSString *mode in runLoopModes) { + [connection_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:mode]; + } + [connection_ start]; + } + hasConnectionEnded_ = NO; + + if (!connection_) { + NSAssert(connection_ != nil, @"beginFetchWithDelegate could not create a connection"); + goto CannotBeginFetch; + } + + if (downloadFileHandle_ != nil) { + // downloading to a file, so downloadedData_ remains nil + } else { + self.downloadedData = [NSMutableData data]; + } + +#if GTM_BACKGROUND_FETCHING + backgroundTaskIdentifer_ = 0; // UIBackgroundTaskInvalid is 0 on iOS 4 + if (shouldFetchInBackground_) { + // For iOS 3 compatibility, ensure that UIApp supports backgrounding + UIApplication *app = [UIApplication sharedApplication]; + if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { + // Tell UIApplication that we want to continue even when the app is in the + // background + NSThread *thread = [NSThread currentThread]; + backgroundTaskIdentifer_ = [app beginBackgroundTaskWithExpirationHandler:^{ + // Callback - this block is always invoked by UIApplication on the main + // thread, but we want to run the user's callbacks on the thread used + // to start the fetch + [self performSelector:@selector(backgroundFetchExpired) + onThread:thread + withObject:nil + waitUntilDone:YES]; + }]; + } + } +#endif + + // once connection_ is non-nil we can send the start notification + isStopNotificationNeeded_ = YES; + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherStartedNotification + object:self]; + return YES; + +CannotBeginFetch: + [self failToBeginFetchWithError:error]; + return NO; +} + +- (void)failToBeginFetchWithError:(NSError *)error { + if (error == nil) { + error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorDownloadFailed + userInfo:nil]; + } + + [[self retain] autorelease]; // In case the callback releases us + + [self invokeFetchCallbacksWithData:nil + error:error]; + + [self releaseCallbacks]; + + [service_ fetcherDidStop:self]; + + self.authorizer = nil; + + if (temporaryDownloadPath_) { + [[self fileManager] removeItemAtPath:temporaryDownloadPath_ + error:NULL]; + self.temporaryDownloadPath = nil; + } +} + +#if GTM_BACKGROUND_FETCHING +- (void)backgroundFetchExpired { + // On background expiration, we stop the fetch and invoke the callbacks + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorBackgroundExpiration + userInfo:nil]; + [self invokeFetchCallbacksWithData:nil + error:error]; + + // Stopping the fetch here will indirectly call endBackgroundTask + [self stopFetchReleasingCallbacks:NO]; + + [self releaseCallbacks]; + self.authorizer = nil; +} + +- (void)endBackgroundTask { + // Whenever the connection stops or background execution expires, + // we need to tell UIApplication we're done + if (backgroundTaskIdentifer_) { + // If backgroundTaskIdentifer_ is non-zero, we know we're on iOS 4 + UIApplication *app = [UIApplication sharedApplication]; + [app endBackgroundTask:backgroundTaskIdentifer_]; + + backgroundTaskIdentifer_ = 0; + } +} +#endif + +- (BOOL)authorizeRequest { + id authorizer = self.authorizer; + SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:); + if ([authorizer respondsToSelector:asyncAuthSel]) { + SEL callbackSel = @selector(authorizer:request:finishedWithError:); + [authorizer authorizeRequest:request_ + delegate:self + didFinishSelector:callbackSel]; + return YES; + } else { + NSAssert(authorizer == nil, @"invalid authorizer for fetch"); + + // no authorizing possible, and authorizing happens only after any delay; + // just begin fetching + return [self beginFetchMayDelay:NO + mayAuthorize:NO]; + } +} + +- (void)authorizer:(id )auth + request:(NSMutableURLRequest *)request + finishedWithError:(NSError *)error { + if (error != nil) { + // we can't fetch without authorization + [self failToBeginFetchWithError:error]; + } else { + [self beginFetchMayDelay:NO + mayAuthorize:NO]; + } +} + +#if NS_BLOCKS_AVAILABLE +- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler { + self.completionBlock = handler; + + // the user may have called setDelegate: earlier if they want to use other + // delegate-style callbacks during the fetch; otherwise, the delegate is nil, + // which is fine + return [self beginFetchWithDelegate:[self delegate] + didFinishSelector:nil]; +} +#endif + +- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath { + NSString *tempDir = nil; + +#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED >= 1060)) + // find an appropriate directory for the download, ideally on the same disk + // as the final target location so the temporary file won't have to be moved + // to a different disk + // + // available in SDKs for 10.6 and iOS 4 + // + // Oct 2011: We previously also used URLForDirectory for + // (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)) + // but that is returning a non-temporary directory for iOS, unfortunately + + SEL sel = @selector(URLForDirectory:inDomain:appropriateForURL:create:error:); + if ([NSFileManager instancesRespondToSelector:sel]) { + NSError *error = nil; + NSURL *targetURL = [NSURL fileURLWithPath:targetPath]; + NSFileManager *fileMgr = [self fileManager]; + + NSURL *tempDirURL = [fileMgr URLForDirectory:NSItemReplacementDirectory + inDomain:NSUserDomainMask + appropriateForURL:targetURL + create:YES + error:&error]; + tempDir = [tempDirURL path]; + } +#endif + + if (tempDir == nil) { + tempDir = NSTemporaryDirectory(); + } + + static unsigned int counter = 0; + NSString *name = [NSString stringWithFormat:@"gtmhttpfetcher_%u_%u", + ++counter, (unsigned int) arc4random()]; + NSString *result = [tempDir stringByAppendingPathComponent:name]; + return result; +} + +- (void)addCookiesToRequest:(NSMutableURLRequest *)request { + // get cookies for this URL from our storage array, if + // we have a storage array + if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault + && cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodNone) { + + NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]]; + if ([cookies count] > 0) { + + NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary + if (cookieHeader) { + [request addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name + } + } + } +} + +// Returns YES if this is in the process of fetching a URL, or waiting to +// retry, or waiting for authorization, or waiting to be issued by the +// service object +- (BOOL)isFetching { + if (connection_ != nil || retryTimer_ != nil) return YES; + + BOOL isAuthorizing = [authorizer_ isAuthorizingRequest:request_]; + if (isAuthorizing) return YES; + + BOOL isDelayed = [service_ isDelayingFetcher:self]; + return isDelayed; +} + +// Returns the status code set in connection:didReceiveResponse: +- (NSInteger)statusCode { + + NSInteger statusCode; + + if (response_ != nil + && [response_ respondsToSelector:@selector(statusCode)]) { + + statusCode = [(NSHTTPURLResponse *)response_ statusCode]; + } else { + // Default to zero, in hopes of hinting "Unknown" (we can't be + // sure that things are OK enough to use 200). + statusCode = 0; + } + return statusCode; +} + +- (NSDictionary *)responseHeaders { + if (response_ != nil + && [response_ respondsToSelector:@selector(allHeaderFields)]) { + + NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; + return headers; + } + return nil; +} + +- (void)releaseCallbacks { + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + self.completionBlock = nil; + self.sentDataBlock = nil; + self.receivedDataBlock = nil; + self.retryBlock = nil; +#endif +} + +// Cancel the fetch of the URL that's currently in progress. +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { + // if the connection or the retry timer is all that's retaining the fetcher, + // we want to be sure this instance survives stopping at least long enough for + // the stack to unwind + [[self retain] autorelease]; + + [self destroyRetryTimer]; + + if (connection_) { + // in case cancelling the connection calls this recursively, we want + // to ensure that we'll only release the connection and delegate once, + // so first set connection_ to nil + NSURLConnection* oldConnection = connection_; + connection_ = nil; + + if (!hasConnectionEnded_) { + [oldConnection cancel]; + } + + // this may be called in a callback from the connection, so use autorelease + [oldConnection autorelease]; + } + + // send the stopped notification + [self sendStopNotificationIfNeeded]; + + [authorizer_ stopAuthorization]; + + if (shouldReleaseCallbacks) { + [self releaseCallbacks]; + + self.authorizer = nil; + } + + [service_ fetcherDidStop:self]; + + if (temporaryDownloadPath_) { + [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_ + error:NULL]; + self.temporaryDownloadPath = nil; + } + +#if GTM_BACKGROUND_FETCHING + [self endBackgroundTask]; +#endif +} + +// external stop method +- (void)stopFetching { + [self stopFetchReleasingCallbacks:YES]; +} + +- (void)sendStopNotificationIfNeeded { + if (isStopNotificationNeeded_) { + isStopNotificationNeeded_ = NO; + + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherStoppedNotification + object:self]; + } +} + +- (void)retryFetch { + + [self stopFetchReleasingCallbacks:NO]; + + [self beginFetchWithDelegate:delegate_ + didFinishSelector:finishedSel_]; +} + +- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { + NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; + + // loop until the callbacks have been called and released, and until + // the connection is no longer pending, or until the timeout has expired + while ((!hasConnectionEnded_ +#if NS_BLOCKS_AVAILABLE + || completionBlock_ != nil +#endif + || delegate_ != nil) + && [giveUpDate timeIntervalSinceNow] > 0) { + + // run the current run loop 1/1000 of a second to give the networking + // code a chance to work + NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001]; + [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; + } +} + +- (NSFileManager *)fileManager { + // use a temporary instance of NSFileManager for thread-safety + return [[[NSFileManager alloc] init] autorelease]; +} + +#pragma mark NSURLConnection Delegate Methods + +// +// NSURLConnection Delegate Methods +// + +// This method just says "follow all redirects", which _should_ be the default behavior, +// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem +// but the redirects were not being followed until I added this method. May be +// a bug in the NSURLConnection code, or the documentation. +// +// In OS X 10.4.8 and earlier, the redirect request doesn't +// get the original's headers and body. This causes POSTs to fail. +// So we construct a new request, a copy of the original, with overrides from the +// redirect. +// +// Docs say that if redirectResponse is nil, just return the redirectRequest. + +- (NSURLRequest *)connection:(NSURLConnection *)connection + willSendRequest:(NSURLRequest *)redirectRequest + redirectResponse:(NSURLResponse *)redirectResponse { + + if (redirectRequest && redirectResponse) { + // save cookies from the response + [self handleCookiesForResponse:redirectResponse]; + + NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease]; + // copy the URL + NSURL *redirectURL = [redirectRequest URL]; + NSURL *url = [newRequest URL]; + + // disallow scheme changes (say, from https to http) + NSString *redirectScheme = [url scheme]; + NSString *newScheme = [redirectURL scheme]; + NSString *newResourceSpecifier = [redirectURL resourceSpecifier]; + + if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame + && newScheme != nil + && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { + + // allow the change from http to https + redirectScheme = newScheme; + } + + NSString *newUrlString = [NSString stringWithFormat:@"%@:%@", + redirectScheme, newResourceSpecifier]; + + NSURL *newURL = [NSURL URLWithString:newUrlString]; + [newRequest setURL:newURL]; + + // any headers in the redirect override headers in the original. + NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields]; + for (NSString *key in redirectHeaders) { + NSString *value = [redirectHeaders objectForKey:key]; + [newRequest setValue:value forHTTPHeaderField:key]; + } + + [self addCookiesToRequest:newRequest]; + + redirectRequest = newRequest; + + // log the response we just received + [self setResponse:redirectResponse]; + [self logNowWithError:nil]; + + // update the request for future logging + NSMutableURLRequest *mutable = [[redirectRequest mutableCopy] autorelease]; + [self setMutableRequest:mutable]; +} + return redirectRequest; +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + // this method is called when the server has determined that it + // has enough information to create the NSURLResponse + // it can be called multiple times, for example in the case of a + // redirect, so each time we reset the data. + [downloadedData_ setLength:0]; + [downloadFileHandle_ truncateFileAtOffset:0]; + downloadedLength_ = 0; + + [self setResponse:response]; + + // save cookies from the response + [self handleCookiesForResponse:response]; +} + + +// handleCookiesForResponse: handles storage of cookies for responses passed to +// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse: +- (void)handleCookiesForResponse:(NSURLResponse *)response { + + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault + || cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodNone) { + + // do nothing special for NSURLConnection's default storage mechanism + // or when we're ignoring cookies + + } else if ([response respondsToSelector:@selector(allHeaderFields)]) { + + // grab the cookies from the header as NSHTTPCookies and store them either + // into our static array or into the fetchHistory + + NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields]; + if (responseHeaderFields) { + + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields + forURL:[response URL]]; + if ([cookies count] > 0) { + [cookieStorage_ setCookies:cookies]; + } + } + } +} + +-(void)connection:(NSURLConnection *)connection + didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + + if ([challenge previousFailureCount] <= 2) { + + NSURLCredential *credential = credential_; + + if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) { + credential = proxyCredential_; + } + + // Here, if credential is still nil, then we *could* try to get it from + // NSURLCredentialStorage's defaultCredentialForProtectionSpace:. + // We don't, because we're assuming: + // + // - for server credentials, we only want ones supplied by the program + // calling http fetcher + // - for proxy credentials, if one were necessary and available in the + // keychain, it would've been found automatically by NSURLConnection + // and this challenge delegate method never would've been called + // anyway + + if (credential) { + // try the credential + [[challenge sender] useCredential:credential + forAuthenticationChallenge:challenge]; + return; + } + } + + // If we don't have credentials, or we've already failed auth 3x, + // report the error, putting the challenge as a value in the userInfo + // dictionary +#if DEBUG + NSAssert(!isCancellingChallenge_, @"isCancellingChallenge_ unexpected"); +#endif + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge + forKey:kGTMHTTPFetcherErrorChallengeKey]; + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed + userInfo:userInfo]; + + // cancelAuthenticationChallenge seems to indirectly call + // connection:didFailWithError: now, though that isn't documented + // + // we'll use an ivar to make the indirect invocation of the + // delegate method do nothing + isCancellingChallenge_ = YES; + [[challenge sender] cancelAuthenticationChallenge:challenge]; + isCancellingChallenge_ = NO; + + [self connection:connection didFailWithError:error]; +} + +- (void)invokeFetchCallbacksWithData:(NSData *)data + error:(NSError *)error { + [[self retain] autorelease]; // In case the callback releases us + + [self invokeFetchCallback:finishedSel_ + target:delegate_ + data:data + error:error]; + +#if NS_BLOCKS_AVAILABLE + if (completionBlock_) { + completionBlock_(data, error); + } +#endif +} + +- (void)invokeFetchCallback:(SEL)sel + target:(id)target + data:(NSData *)data + error:(NSError *)error { + // This method is available to subclasses which may provide a customized + // target pointer + if (target && sel) { + NSMethodSignature *sig = [target methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:target]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&data atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } +} + +- (void)invokeSentDataCallback:(SEL)sel + target:(id)target + didSendBodyData:(NSInteger)bytesWritten + totalBytesWritten:(NSInteger)totalBytesWritten + totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { + if (target && sel) { + NSMethodSignature *sig = [target methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:target]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&bytesWritten atIndex:3]; + [invocation setArgument:&totalBytesWritten atIndex:4]; + [invocation setArgument:&totalBytesExpectedToWrite atIndex:5]; + [invocation invoke]; + } +} + +- (BOOL)invokeRetryCallback:(SEL)sel + target:(id)target + willRetry:(BOOL)willRetry + error:(NSError *)error { + if (target && sel) { + NSMethodSignature *sig = [target methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:target]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&willRetry atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + + [invocation getReturnValue:&willRetry]; + } + return willRetry; +} + +- (void)connection:(NSURLConnection *)connection + didSendBodyData:(NSInteger)bytesWritten + totalBytesWritten:(NSInteger)totalBytesWritten +totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { + + SEL sel = [self sentDataSelector]; + [self invokeSentDataCallback:sel + target:delegate_ + didSendBodyData:bytesWritten + totalBytesWritten:totalBytesWritten + totalBytesExpectedToWrite:totalBytesExpectedToWrite]; + +#if NS_BLOCKS_AVAILABLE + if (sentDataBlock_) { + sentDataBlock_(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); + } +#endif +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { +#if DEBUG + // the download file handle should be set before the fetch is started, not + // after + NSAssert((downloadFileHandle_ == nil) != (downloadedData_ == nil), + @"received data accumulates as NSData or NSFileHandle, not both"); +#endif + + if (downloadFileHandle_ != nil) { + // append to file + @try { + [downloadFileHandle_ writeData:data]; + + downloadedLength_ = [downloadFileHandle_ offsetInFile]; + } + @catch (NSException *exc) { + // couldn't write to file, probably due to a full disk + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[exc reason] + forKey:NSLocalizedDescriptionKey]; + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:kGTMHTTPFetcherErrorFileHandleException + userInfo:userInfo]; + [self connection:connection didFailWithError:error]; + return; + } + } else { + // append to mutable data + [downloadedData_ appendData:data]; + + downloadedLength_ = [downloadedData_ length]; + } + + if (receivedDataSel_) { + [delegate_ performSelector:receivedDataSel_ + withObject:self + withObject:downloadedData_]; + } + +#if NS_BLOCKS_AVAILABLE + if (receivedDataBlock_) { + receivedDataBlock_(downloadedData_); + } +#endif +} + + +// For error 304's ("Not Modified") where we've cached the data, return +// status 200 ("OK") to the caller (but leave the fetcher status as 304) +// and copy the cached data. +// +// For other errors or if there's no cached data, just return the actual status. +- (NSInteger)statusAfterHandlingNotModifiedError { + + NSInteger status = [self statusCode]; + if (status == kGTMHTTPFetcherStatusNotModified + && [fetchHistory_ shouldCacheETaggedData]) { + + NSData *cachedData = [fetchHistory_ cachedDataForRequest:request_]; + if (cachedData) { + // forge the status to pass on to the delegate + status = 200; + + // copy our stored data + if (downloadFileHandle_ != nil) { + @try { + // Downloading to a file handle won't save to the cache (the data is + // likely inappropriately large for caching), but will still read from + // the cache, on the unlikely chance that the response was Not Modified + // and the URL response was indeed present in the cache. + [downloadFileHandle_ truncateFileAtOffset:0]; + [downloadFileHandle_ writeData:cachedData]; + downloadedLength_ = [downloadFileHandle_ offsetInFile]; + } + @catch (NSException *) { + // Failed to write data, likely due to lack of disk space + status = kGTMHTTPFetcherErrorFileHandleException; + } + } else { + [downloadedData_ setData:cachedData]; + downloadedLength_ = [cachedData length]; + } + } + } + return status; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + // we no longer need to cancel the connection + hasConnectionEnded_ = YES; + + // skip caching ETagged results when the data is being saved to a file + if (downloadFileHandle_ == nil) { + [fetchHistory_ updateFetchHistoryWithRequest:request_ + response:response_ + downloadedData:downloadedData_]; + } else { + [fetchHistory_ removeCachedDataForRequest:request_]; + } + + [[self retain] autorelease]; // in case the callback releases us + + [self logNowWithError:nil]; + + NSInteger status = [self statusAfterHandlingNotModifiedError]; + + // we want to send the stop notification before calling the delegate's + // callback selector, since the callback selector may release all of + // the fetcher properties that the client is using to track the fetches + // + // We'll also stop now so that, to any observers watching the notifications, + // it doesn't look like our wait for a retry (which may be long, + // 30 seconds or more) is part of the network activity + [self sendStopNotificationIfNeeded]; + + BOOL shouldStopFetching = YES; + NSError *error = nil; + + if (status >= 0 && status < 300) { + // success + if (downloadPath_) { + // avoid deleting the downloaded file when the fetch stops + [downloadFileHandle_ closeFile]; + self.downloadFileHandle = nil; + + NSFileManager *fileMgr = [self fileManager]; + [fileMgr removeItemAtPath:downloadPath_ + error:NULL]; + + if ([fileMgr moveItemAtPath:temporaryDownloadPath_ + toPath:downloadPath_ + error:&error]) { + self.temporaryDownloadPath = nil; + } + } + } else { + // status over 300; retry or notify the delegate of failure + if ([self shouldRetryNowForStatus:status error:nil]) { + // retrying + [self beginRetryTimer]; + shouldStopFetching = NO; + } else { + NSDictionary *userInfo = nil; + if ([downloadedData_ length] > 0) { + userInfo = [NSDictionary dictionaryWithObject:downloadedData_ + forKey:kGTMHTTPFetcherStatusDataKey]; + } + error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:status + userInfo:userInfo]; + } + } + + if (shouldStopFetching) { + // call the callbacks + [self invokeFetchCallbacksWithData:downloadedData_ + error:error]; + + BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion]; + [self stopFetchReleasingCallbacks:shouldRelease]; + } +} + +- (BOOL)shouldReleaseCallbacksUponCompletion { + // a subclass can override this to keep callbacks around after the + // connection has finished successfully + return YES; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + // prevent the failure callback from being called twice, since the stopFetch + // call below (either the explicit one at the end of this method, or the + // implicit one when the retry occurs) will release the delegate + if (connection_ == nil) return; + + // if this method was invoked indirectly by cancellation of an authentication + // challenge, defer this until it is called again with the proper error object + if (isCancellingChallenge_) return; + + // we no longer need to cancel the connection + hasConnectionEnded_ = YES; + + [self logNowWithError:error]; + + // see comment about sendStopNotificationIfNeeded + // in connectionDidFinishLoading: + [self sendStopNotificationIfNeeded]; + + if ([self shouldRetryNowForStatus:0 error:error]) { + + [self beginRetryTimer]; + + } else { + + [[self retain] autorelease]; // in case the callback releases us + + [self invokeFetchCallbacksWithData:nil + error:error]; + + [self stopFetchReleasingCallbacks:YES]; + } +} + +- (void)logNowWithError:(NSError *)error { + // if the logging category is available, then log the current request, + // response, data, and error + if ([self respondsToSelector:@selector(logFetchWithError:)]) { + [self performSelector:@selector(logFetchWithError:) withObject:error]; + } +} + +#pragma mark Retries + +- (BOOL)isRetryError:(NSError *)error { + + struct retryRecord { + NSString *const domain; + int code; + }; + + struct retryRecord retries[] = { + { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout + { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable + { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout + { NSURLErrorDomain, NSURLErrorTimedOut }, + { NSURLErrorDomain, NSURLErrorNetworkConnectionLost }, + { nil, 0 } + }; + + // NSError's isEqual always returns false for equal but distinct instances + // of NSError, so we have to compare the domain and code values explicitly + + for (int idx = 0; retries[idx].domain != nil; idx++) { + + if ([[error domain] isEqual:retries[idx].domain] + && [error code] == retries[idx].code) { + + return YES; + } + } + return NO; +} + + +// shouldRetryNowForStatus:error: returns YES if the user has enabled retries +// and the status or error is one that is suitable for retrying. "Suitable" +// means either the isRetryError:'s list contains the status or error, or the +// user's retrySelector: is present and returns YES when called, or the +// authorizer may be able to fix. +- (BOOL)shouldRetryNowForStatus:(NSInteger)status + error:(NSError *)error { + // Determine if a refreshed authorizer may avoid an authorization error + BOOL shouldRetryForAuthRefresh = NO; + BOOL isFirstAuthError = (authorizer_ != nil) + && !hasAttemptedAuthRefresh_ + && (status == kGTMHTTPFetcherStatusUnauthorized); // 401 + + if (isFirstAuthError) { + if ([authorizer_ respondsToSelector:@selector(primeForRefresh)]) { + BOOL hasPrimed = [authorizer_ primeForRefresh]; + if (hasPrimed) { + shouldRetryForAuthRefresh = YES; + hasAttemptedAuthRefresh_ = YES; + [request_ setValue:nil forHTTPHeaderField:@"Authorization"]; + } + } + } + + // Determine if we're doing exponential backoff retries + BOOL shouldDoIntervalRetry = [self isRetryEnabled] + && ([self nextRetryInterval] < [self maxRetryInterval]); + + BOOL willRetry = NO; + BOOL canRetry = shouldRetryForAuthRefresh || shouldDoIntervalRetry; + if (canRetry) { + // Check if this is a retryable error + if (error == nil) { + // Make an error for the status + NSDictionary *userInfo = nil; + if ([downloadedData_ length] > 0) { + userInfo = [NSDictionary dictionaryWithObject:downloadedData_ + forKey:kGTMHTTPFetcherStatusDataKey]; + } + error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:status + userInfo:userInfo]; + } + + willRetry = shouldRetryForAuthRefresh || [self isRetryError:error]; + + // If the user has installed a retry callback, consult that + willRetry = [self invokeRetryCallback:retrySel_ + target:delegate_ + willRetry:willRetry + error:error]; +#if NS_BLOCKS_AVAILABLE + if (retryBlock_) { + willRetry = retryBlock_(willRetry, error); + } +#endif + } + return willRetry; +} + +- (void)beginRetryTimer { + + NSTimeInterval nextInterval = [self nextRetryInterval]; + NSTimeInterval maxInterval = [self maxRetryInterval]; + + NSTimeInterval newInterval = MIN(nextInterval, maxInterval); + + [self primeRetryTimerWithNewTimeInterval:newInterval]; +} + +- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs { + + [self destroyRetryTimer]; + + lastRetryInterval_ = secs; + + retryTimer_ = [NSTimer scheduledTimerWithTimeInterval:secs + target:self + selector:@selector(retryTimerFired:) + userInfo:nil + repeats:NO]; + [retryTimer_ retain]; + + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherRetryDelayStartedNotification + object:self]; +} + +- (void)retryTimerFired:(NSTimer *)timer { + + [self destroyRetryTimer]; + + retryCount_++; + + [self retryFetch]; +} + +- (void)destroyRetryTimer { + if (retryTimer_) { + [retryTimer_ invalidate]; + [retryTimer_ autorelease]; + retryTimer_ = nil; + + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherRetryDelayStoppedNotification + object:self]; + } +} + +- (NSUInteger)retryCount { + return retryCount_; +} + +- (NSTimeInterval)nextRetryInterval { + // the next wait interval is the factor (2.0) times the last interval, + // but never less than the minimum interval + NSTimeInterval secs = lastRetryInterval_ * retryFactor_; + secs = MIN(secs, maxRetryInterval_); + secs = MAX(secs, minRetryInterval_); + + return secs; +} + +- (BOOL)isRetryEnabled { + return isRetryEnabled_; +} + +- (void)setRetryEnabled:(BOOL)flag { + + if (flag && !isRetryEnabled_) { + // We defer initializing these until the user calls setRetryEnabled + // to avoid using the random number generator if it's not needed. + // However, this means min and max intervals for this fetcher are reset + // as a side effect of calling setRetryEnabled. + // + // make an initial retry interval random between 1.0 and 2.0 seconds + [self setMinRetryInterval:0.0]; + [self setMaxRetryInterval:kUnsetMaxRetryInterval]; + [self setRetryFactor:2.0]; + lastRetryInterval_ = 0.0; + } + isRetryEnabled_ = flag; +}; + +- (NSTimeInterval)maxRetryInterval { + return maxRetryInterval_; +} + +- (void)setMaxRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + maxRetryInterval_ = secs; + } else { + maxRetryInterval_ = kUnsetMaxRetryInterval; + } +} + +- (double)minRetryInterval { + return minRetryInterval_; +} + +- (void)setMinRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + minRetryInterval_ = secs; + } else { + // set min interval to a random value between 1.0 and 2.0 seconds + // so that if multiple clients start retrying at the same time, they'll + // repeat at different times and avoid overloading the server + minRetryInterval_ = 1.0 + ((double)(arc4random() & 0x0FFFF) / (double) 0x0FFFF); + } +} + +#pragma mark Getters and Setters + +@dynamic cookieStorageMethod, + retryEnabled, + maxRetryInterval, + minRetryInterval, + retryCount, + nextRetryInterval, + statusCode, + responseHeaders, + fetchHistory, + userData, + properties; + +@synthesize mutableRequest = request_, + credential = credential_, + proxyCredential = proxyCredential_, + postData = postData_, + postStream = postStream_, + delegate = delegate_, + authorizer = authorizer_, + service = service_, + serviceHost = serviceHost_, + servicePriority = servicePriority_, + thread = thread_, + sentDataSelector = sentDataSel_, + receivedDataSelector = receivedDataSel_, + retrySelector = retrySel_, + retryFactor = retryFactor_, + response = response_, + downloadedLength = downloadedLength_, + downloadedData = downloadedData_, + downloadPath = downloadPath_, + temporaryDownloadPath = temporaryDownloadPath_, + downloadFileHandle = downloadFileHandle_, + runLoopModes = runLoopModes_, + comment = comment_, + log = log_, + cookieStorage = cookieStorage_; + +#if NS_BLOCKS_AVAILABLE +@synthesize completionBlock = completionBlock_, + sentDataBlock = sentDataBlock_, + receivedDataBlock = receivedDataBlock_, + retryBlock = retryBlock_; +#endif + +@synthesize shouldFetchInBackground = shouldFetchInBackground_; + +- (NSInteger)cookieStorageMethod { + return cookieStorageMethod_; +} + +- (void)setCookieStorageMethod:(NSInteger)method { + + cookieStorageMethod_ = method; + + if (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) { + // system default + [request_ setHTTPShouldHandleCookies:YES]; + + // no need for a cookie storage object + self.cookieStorage = nil; + + } else { + // not system default + [request_ setHTTPShouldHandleCookies:NO]; + + if (method == kGTMHTTPFetcherCookieStorageMethodStatic) { + // store cookies in the static array + NSAssert(gGTMFetcherStaticCookieStorage != nil, + @"cookie storage requires GTMHTTPFetchHistory"); + + self.cookieStorage = gGTMFetcherStaticCookieStorage; + } else if (method == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { + // store cookies in the fetch history + self.cookieStorage = [fetchHistory_ cookieStorage]; + } else { + // kGTMHTTPFetcherCookieStorageMethodNone - ignore cookies + self.cookieStorage = nil; + } + } +} + ++ (id )staticCookieStorage { + return gGTMFetcherStaticCookieStorage; +} + ++ (BOOL)doesSupportSentDataCallback { +#if GTM_IPHONE + // NSURLConnection's didSendBodyData: delegate support appears to be + // available starting in iPhone OS 3.0 + return (NSFoundationVersionNumber >= 678.47); +#else + // per WebKit's MaxFoundationVersionWithoutdidSendBodyDataDelegate + // + // indicates if NSURLConnection will invoke the didSendBodyData: delegate + // method + return (NSFoundationVersionNumber > 677.21); +#endif +} + +- (id )fetchHistory { + return fetchHistory_; +} + +- (void)setFetchHistory:(id )fetchHistory { + [fetchHistory_ autorelease]; + fetchHistory_ = [fetchHistory retain]; + + if (fetchHistory_ != nil) { + // set the fetch history's cookie array to be the cookie store + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodFetchHistory]; + + } else { + // the fetch history was removed + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { + // fall back to static storage + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; + } + } +} + +- (id)userData { + return userData_; +} + +- (void)setUserData:(id)theObj { + [userData_ autorelease]; + userData_ = [theObj retain]; +} + +- (void)setProperties:(NSMutableDictionary *)dict { + [properties_ autorelease]; + + // This copies rather than retains the parameter for compatiblity with + // an earlier version that took an immutable parameter and copied it. + properties_ = [dict mutableCopy]; +} + +- (NSMutableDictionary *)properties { + return properties_; +} + +- (void)setProperty:(id)obj forKey:(NSString *)key { + if (properties_ == nil && obj != nil) { + [self setProperties:[NSMutableDictionary dictionary]]; + } + [properties_ setValue:obj forKey:key]; +} + +- (id)propertyForKey:(NSString *)key { + return [properties_ objectForKey:key]; +} + +- (void)addPropertiesFromDictionary:(NSDictionary *)dict { + if (properties_ == nil && dict != nil) { + [self setProperties:[[dict mutableCopy] autorelease]]; + } else { + [properties_ addEntriesFromDictionary:dict]; + } +} + +- (void)setCommentWithFormat:(id)format, ... { +#if !STRIP_GTM_FETCH_LOGGING + NSString *result = format; + if (format) { + va_list argList; + va_start(argList, format); + result = [[[NSString alloc] initWithFormat:format + arguments:argList] autorelease]; + va_end(argList); + } + [self setComment:result]; +#endif +} + ++ (Class)connectionClass { + if (gGTMFetcherConnectionClass == nil) { + gGTMFetcherConnectionClass = [NSURLConnection class]; + } + return gGTMFetcherConnectionClass; +} + ++ (void)setConnectionClass:(Class)theClass { + gGTMFetcherConnectionClass = theClass; +} + +#if STRIP_GTM_FETCH_LOGGING ++ (void)setLoggingEnabled:(BOOL)flag { +} +#endif // STRIP_GTM_FETCH_LOGGING + +@end + +void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...) { + + // verify that the object's selector is implemented with the proper + // number and type of arguments +#if DEBUG + va_list argList; + va_start(argList, sel); + + if (obj && sel) { + // check that the selector is implemented + if (![obj respondsToSelector:sel]) { + NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed", + NSStringFromClass([obj class]), + NSStringFromSelector(sel)); + NSCAssert(0, @"callback selector unimplemented or misnamed"); + } else { + const char *expectedArgType; + unsigned int argCount = 2; // skip self and _cmd + NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; + + // check that each expected argument is present and of the correct type + while ((expectedArgType = va_arg(argList, const char*)) != 0) { + + if ([sig numberOfArguments] > argCount) { + const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; + + if(0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) { + NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), (argCount - 2), expectedArgType); + NSCAssert(0, @"callback selector argument type mistake"); + } + } + argCount++; + } + + // check that the proper number of arguments are present in the selector + if (argCount != [sig numberOfArguments]) { + NSLog( @"\"%@\" selector \"%@\" should have %d arguments", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), (argCount - 2)); + NSCAssert(0, @"callback selector arguments incorrect"); + } + } + } + + va_end(argList); +#endif +} + +NSString *GTMCleanedUserAgentString(NSString *str) { + // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html + // and http://www-archive.mozilla.org/build/user-agent-strings.html + + if (str == nil) return nil; + + NSMutableString *result = [NSMutableString stringWithString:str]; + + // Replace spaces with underscores + [result replaceOccurrencesOfString:@" " + withString:@"_" + options:0 + range:NSMakeRange(0, [result length])]; + + // Delete http token separators and remaining whitespace + static NSCharacterSet *charsToDelete = nil; + if (charsToDelete == nil) { + // Make a set of unwanted characters + NSString *const kSeparators = @"()<>@,;:\\\"/[]?={}"; + + NSMutableCharacterSet *mutableChars; + mutableChars = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy] autorelease]; + [mutableChars addCharactersInString:kSeparators]; + charsToDelete = [mutableChars copy]; // hang on to an immutable copy + } + + while (1) { + NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete]; + if (separatorRange.location == NSNotFound) break; + + [result deleteCharactersInRange:separatorRange]; + }; + + return result; +} + +NSString *GTMSystemVersionString(void) { + NSString *systemString = @""; + +#if TARGET_OS_MAC && !TARGET_OS_IPHONE + // Mac build + static NSString *savedSystemString = nil; + if (savedSystemString == nil) { + // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading + // the system plist file. + NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist"; + NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath]; + NSString *versString = [plist objectForKey:@"ProductVersion"]; + if ([versString length] == 0) { + versString = @"10.?.?"; + } + savedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString]; + } + systemString = savedSystemString; +#elif TARGET_OS_IPHONE + // Compiling against the iPhone SDK + + static NSString *savedSystemString = nil; + if (savedSystemString == nil) { + // Avoid the slowness of calling currentDevice repeatedly on the iPhone + UIDevice* currentDevice = [UIDevice currentDevice]; + + NSString *rawModel = [currentDevice model]; + NSString *model = GTMCleanedUserAgentString(rawModel); + + NSString *systemVersion = [currentDevice systemVersion]; + + savedSystemString = [[NSString alloc] initWithFormat:@"%@/%@", + model, systemVersion]; // "iPod_Touch/2.2" + } + systemString = savedSystemString; + +#elif (GTL_IPHONE || GDATA_IPHONE) + // Compiling iOS libraries against the Mac SDK + systemString = @"iPhone/x.x"; + +#elif defined(_SYS_UTSNAME_H) + // Foundation-only build + struct utsname unameRecord; + uname(&unameRecord); + + systemString = [NSString stringWithFormat:@"%s/%s", + unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1" +#endif + + return systemString; +} + +// Return a generic name and version for the current application; this avoids +// anonymous server transactions. +NSString *GTMApplicationIdentifier(NSBundle *bundle) { + static NSString *sAppID = nil; + if (sAppID != nil) return sAppID; + + // If there's a bundle ID, use that; otherwise, use the process name + if (bundle == nil) { + bundle = [NSBundle mainBundle]; + } + + NSString *identifier; + NSString *bundleID = [bundle bundleIdentifier]; + if ([bundleID length] > 0) { + identifier = bundleID; + } else { + // Fall back on the procname, prefixed by "proc" to flag that it's + // autogenerated and perhaps unreliable + NSString *procName = [[NSProcessInfo processInfo] processName]; + identifier = [NSString stringWithFormat:@"proc_%@", procName]; + } + + // Clean up whitespace and special characters + identifier = GTMCleanedUserAgentString(identifier); + + // If there's a version number, append that + NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if ([version length] == 0) { + version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + } + + // Clean up whitespace and special characters + version = GTMCleanedUserAgentString(version); + + // Glue the two together (cleanup done above or else cleanup would strip the + // slash) + if ([version length] > 0) { + identifier = [identifier stringByAppendingFormat:@"/%@", version]; + } + + sAppID = [identifier copy]; + return sAppID; +} diff --git a/OAuth 2/GTMOAuth2Authentication.h b/OAuth 2/GTMOAuth2Authentication.h new file mode 100755 index 0000000..50fd188 --- /dev/null +++ b/OAuth 2/GTMOAuth2Authentication.h @@ -0,0 +1,325 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +// This class implements the OAuth 2 protocol for authorizing requests. +// http://tools.ietf.org/html/draft-ietf-oauth-v2 + +#import + +// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines +#import "GTMHTTPFetcher.h" + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS + #define _EXTERN + #define _INITIALIZE_AS(x) =x +#else + #if defined(__cplusplus) + #define _EXTERN extern "C" + #else + #define _EXTERN extern + #endif + #define _INITIALIZE_AS(x) +#endif + +// Until all OAuth 2 providers are up to the same spec, we'll provide a crude +// way here to override the "Bearer" string in the Authorization header +#ifndef GTM_OAUTH2_BEARER +#define GTM_OAUTH2_BEARER "Bearer" +#endif + +// Service provider name allows stored authorization to be associated with +// the authorizing service +_EXTERN NSString* const kGTMOAuth2ServiceProviderGoogle _INITIALIZE_AS(@"Google"); + +// +// GTMOAuth2SignIn constants, included here for use by clients +// +_EXTERN NSString* const kGTMOAuth2ErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuth2"); + +// Error userInfo keys +_EXTERN NSString* const kGTMOAuth2ErrorMessageKey _INITIALIZE_AS(@"error"); +_EXTERN NSString* const kGTMOAuth2ErrorRequestKey _INITIALIZE_AS(@"request"); +_EXTERN NSString* const kGTMOAuth2ErrorJSONKey _INITIALIZE_AS(@"json"); + +enum { + // Error code indicating that the window was prematurely closed + kGTMOAuth2ErrorWindowClosed = -1000, + kGTMOAuth2ErrorAuthorizationFailed = -1001, + kGTMOAuth2ErrorTokenExpired = -1002, + kGTMOAuth2ErrorTokenUnavailable = -1003, + kGTMOAuth2ErrorUnauthorizableRequest = -1004 +}; + + +// Notifications for token fetches +_EXTERN NSString* const kGTMOAuth2FetchStarted _INITIALIZE_AS(@"kGTMOAuth2FetchStarted"); +_EXTERN NSString* const kGTMOAuth2FetchStopped _INITIALIZE_AS(@"kGTMOAuth2FetchStopped"); + +_EXTERN NSString* const kGTMOAuth2FetcherKey _INITIALIZE_AS(@"fetcher"); +_EXTERN NSString* const kGTMOAuth2FetchTypeKey _INITIALIZE_AS(@"FetchType"); +_EXTERN NSString* const kGTMOAuth2FetchTypeToken _INITIALIZE_AS(@"token"); +_EXTERN NSString* const kGTMOAuth2FetchTypeRefresh _INITIALIZE_AS(@"refresh"); +_EXTERN NSString* const kGTMOAuth2FetchTypeAssertion _INITIALIZE_AS(@"assertion"); +_EXTERN NSString* const kGTMOAuth2FetchTypeUserInfo _INITIALIZE_AS(@"userInfo"); + +// Token-issuance errors +_EXTERN NSString* const kGTMOAuth2ErrorKey _INITIALIZE_AS(@"error"); + +_EXTERN NSString* const kGTMOAuth2ErrorInvalidRequest _INITIALIZE_AS(@"invalid_request"); +_EXTERN NSString* const kGTMOAuth2ErrorInvalidClient _INITIALIZE_AS(@"invalid_client"); +_EXTERN NSString* const kGTMOAuth2ErrorInvalidGrant _INITIALIZE_AS(@"invalid_grant"); +_EXTERN NSString* const kGTMOAuth2ErrorUnauthorizedClient _INITIALIZE_AS(@"unauthorized_client"); +_EXTERN NSString* const kGTMOAuth2ErrorUnsupportedGrantType _INITIALIZE_AS(@"unsupported_grant_type"); +_EXTERN NSString* const kGTMOAuth2ErrorInvalidScope _INITIALIZE_AS(@"invalid_scope"); + +// Notification that sign-in has completed, and token fetches will begin (useful +// for displaying interstitial messages after the window has closed) +_EXTERN NSString* const kGTMOAuth2UserSignedIn _INITIALIZE_AS(@"kGTMOAuth2UserSignedIn"); + +// Notification for token changes +_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshed"); +_EXTERN NSString* const kGTMOAuth2RefreshTokenChanged _INITIALIZE_AS(@"kGTMOAuth2RefreshTokenChanged"); + +// Notification for WebView loading +_EXTERN NSString* const kGTMOAuth2WebViewStartedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStartedLoading"); +_EXTERN NSString* const kGTMOAuth2WebViewStoppedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStoppedLoading"); +_EXTERN NSString* const kGTMOAuth2WebViewKey _INITIALIZE_AS(@"kGTMOAuth2WebViewKey"); +_EXTERN NSString* const kGTMOAuth2WebViewStopKindKey _INITIALIZE_AS(@"kGTMOAuth2WebViewStopKindKey"); +_EXTERN NSString* const kGTMOAuth2WebViewFinished _INITIALIZE_AS(@"finished"); +_EXTERN NSString* const kGTMOAuth2WebViewFailed _INITIALIZE_AS(@"failed"); +_EXTERN NSString* const kGTMOAuth2WebViewCancelled _INITIALIZE_AS(@"cancelled"); + +// Notification for network loss during html sign-in display +_EXTERN NSString* const kGTMOAuth2NetworkLost _INITIALIZE_AS(@"kGTMOAuthNetworkLost"); +_EXTERN NSString* const kGTMOAuth2NetworkFound _INITIALIZE_AS(@"kGTMOAuthNetworkFound"); + +@interface GTMOAuth2Authentication : NSObject { + @private + NSString *clientID_; + NSString *clientSecret_; + NSString *redirectURI_; + NSMutableDictionary *parameters_; + + // authorization parameters + NSURL *tokenURL_; + NSDate *expirationDate_; + + NSDictionary *additionalTokenRequestParameters_; + + // queue of requests for authorization waiting for a valid access token + GTMHTTPFetcher *refreshFetcher_; + NSMutableArray *authorizationQueue_; + + id fetcherService_; // WEAK + + Class parserClass_; + + BOOL shouldAuthorizeAllRequests_; + + // arbitrary data retained for the user + id userData_; + NSMutableDictionary *properties_; +} + +// OAuth2 standard protocol parameters +// +// These should be the plain strings; any needed escaping will be provided by +// the library. + +// Request properties +@property (copy) NSString *clientID; +@property (copy) NSString *clientSecret; +@property (copy) NSString *redirectURI; +@property (retain) NSString *scope; +@property (retain) NSString *tokenType; +@property (retain) NSString *assertion; + +// Apps may optionally add parameters here to be provided to the token +// endpoint on token requests and refreshes +@property (retain) NSDictionary *additionalTokenRequestParameters; + +// Response properties +@property (retain) NSMutableDictionary *parameters; + +@property (retain) NSString *accessToken; +@property (retain) NSString *refreshToken; +@property (retain) NSNumber *expiresIn; +@property (retain) NSString *code; +@property (retain) NSString *errorString; + +// URL for obtaining access tokens +@property (copy) NSURL *tokenURL; + +// Calculated expiration date (expiresIn seconds added to the +// time the access token was received.) +@property (copy) NSDate *expirationDate; + +// Service identifier, like "Google"; not used for authentication +// +// The provider name is just for allowing stored authorization to be associated +// with the authorizing service. +@property (copy) NSString *serviceProvider; + +// User email and verified status; not used for authentication +// +// The verified string can be checked with -boolValue. If the result is false, +// then the email address is listed with the account on the server, but the +// address has not been confirmed as belonging to the owner of the account. +@property (retain) NSString *userEmail; +@property (retain) NSString *userEmailIsVerified; + +// Property indicating if this auth has a refresh token so is suitable for +// authorizing a request. This does not guarantee that the token is valid. +@property (readonly) BOOL canAuthorize; + +// Property indicating if this object will authorize plain http request +// (as well as any non-https requests.) Default is NO, only requests with the +// scheme https are authorized, since security may be compromised if tokens +// are sent over the wire using an unencrypted protocol like http. +@property (assign) BOOL shouldAuthorizeAllRequests; + +// userData is retained for the convenience of the caller +@property (retain) id userData; + +// Stored property values are retained for the convenience of the caller +@property (retain) NSDictionary *properties; + +// Property for the optional fetcher service instance to be used to create +// fetchers +// +// Fetcher service objects retain authorizations, so this is weak to avoid +// circular retains. +@property (assign) id fetcherService; // WEAK + +// Alternative JSON parsing class; this should implement the +// GTMOAuth2ParserClass informal protocol. If this property is +// not set, the class SBJSON must be available in the runtime. +@property (assign) Class parserClass; + +// Convenience method for creating an authentication object ++ (id)authenticationWithServiceProvider:(NSString *)serviceProvider + tokenURL:(NSURL *)tokenURL + redirectURI:(NSString *)redirectURI + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret; + +// Clear out any authentication values, prepare for a new request fetch +- (void)reset; + +// Main authorization entry points +// +// These will refresh the access token, if necessary, add the access token to +// the request, then invoke the callback. +// +// The request argument may be nil to just force a refresh of the access token, +// if needed. +// +// NOTE: To avoid accidental leaks of bearer tokens, the request must +// be for a URL with the scheme https unless the shouldAuthorizeAllRequests +// property is set. + +// The finish selector should have a signature matching +// - (void)authentication:(GTMOAuth2Authentication *)auth +// request:(NSMutableURLRequest *)request +// finishedWithError:(NSError *)error; + +- (void)authorizeRequest:(NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel; + +#if NS_BLOCKS_AVAILABLE +- (void)authorizeRequest:(NSMutableURLRequest *)request + completionHandler:(void (^)(NSError *error))handler; +#endif + +// Synchronous entry point; authorizing this way cannot refresh an expired +// access token +- (BOOL)authorizeRequest:(NSMutableURLRequest *)request; + +// If the authentication is waiting for a refresh to complete, spin the run +// loop, discarding events, until the fetch has completed +// +// This is only for use in testing or in tools without a user interface. +- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds; + + +////////////////////////////////////////////////////////////////////////////// +// +// Internal properties and methods for use by GTMOAuth2SignIn +// + +// Pending fetcher to get a new access token, if any +@property (retain) GTMHTTPFetcher *refreshFetcher; + +// Check if a request is queued up to be authorized +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request; + +// Check if a request appears to be authorized +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request; + +// Stop any pending refresh fetch +- (void)stopAuthorization; + +// OAuth fetch user-agent header value +- (NSString *)userAgent; + +// Parse and set token and token secret from response data +- (void)setKeysForResponseString:(NSString *)str; +- (void)setKeysForResponseDictionary:(NSDictionary *)dict; + +// Persistent token string for keychain storage +// +// We'll use the format "refresh_token=foo&serviceProvider=bar" so we can +// easily alter what portions of the auth data are stored +// +// Use these methods for serialization +- (NSString *)persistenceResponseString; +- (void)setKeysForPersistenceResponseString:(NSString *)str; + +// method to begin fetching an access token, used by the sign-in object +- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSel; + +// Entry point to post a notification about a fetcher currently used for +// obtaining or refreshing a token; the sign-in object will also use this +// to indicate when the user's email address is being fetched. +// +// Fetch type constants are above under "notifications for token fetches" +- (void)notifyFetchIsRunning:(BOOL)isStarting + fetcher:(GTMHTTPFetcher *)fetcher + type:(NSString *)fetchType; + +// Arbitrary key-value properties retained for the user +- (void)setProperty:(id)obj forKey:(NSString *)key; +- (id)propertyForKey:(NSString *)key; + +// +// Utilities +// + ++ (NSString *)encodedOAuthValueForString:(NSString *)str; + ++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict; + ++ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr; + ++ (NSString *)scopeWithStrings:(NSString *)firsStr, ... NS_REQUIRES_NIL_TERMINATION; +@end + +#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/OAuth 2/GTMOAuth2Authentication.m b/OAuth 2/GTMOAuth2Authentication.m new file mode 100755 index 0000000..ed13181 --- /dev/null +++ b/OAuth 2/GTMOAuth2Authentication.m @@ -0,0 +1,1166 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#define GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS 1 +#import "GTMOAuth2Authentication.h" + +// standard OAuth keys +static NSString *const kOAuth2AccessTokenKey = @"access_token"; +static NSString *const kOAuth2RefreshTokenKey = @"refresh_token"; +static NSString *const kOAuth2ClientIDKey = @"client_id"; +static NSString *const kOAuth2ClientSecretKey = @"client_secret"; +static NSString *const kOAuth2RedirectURIKey = @"redirect_uri"; +static NSString *const kOAuth2ResponseTypeKey = @"response_type"; +static NSString *const kOAuth2ScopeKey = @"scope"; +static NSString *const kOAuth2ErrorKey = @"error"; +static NSString *const kOAuth2TokenTypeKey = @"token_type"; +static NSString *const kOAuth2ExpiresInKey = @"expires_in"; +static NSString *const kOAuth2CodeKey = @"code"; +static NSString *const kOAuth2AssertionKey = @"assertion"; + +// additional persistent keys +static NSString *const kServiceProviderKey = @"serviceProvider"; +static NSString *const kUserEmailKey = @"email"; +static NSString *const kUserEmailIsVerifiedKey = @"isVerified"; + +// fetcher keys +static NSString *const kTokenFetchDelegateKey = @"delegate"; +static NSString *const kTokenFetchSelectorKey = @"sel"; + +static NSString *const kRefreshFetchArgsKey = @"requestArgs"; + +// If GTMNSJSONSerialization is available, it is used for formatting JSON +#if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \ + (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000)) +@interface GTMNSJSONSerialization : NSObject ++ (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error; +@end +#endif + +@interface GTMOAuth2ParserClass : NSObject +// just enough of SBJSON to be able to parse +- (id)objectWithString:(NSString*)repr error:(NSError**)error; +@end + +// wrapper class for requests needing authorization and their callbacks +@interface GTMOAuth2AuthorizationArgs : NSObject { + @private + NSMutableURLRequest *request_; + id delegate_; + SEL sel_; + id completionHandler_; + NSThread *thread_; + NSError *error_; +} + +@property (retain) NSMutableURLRequest *request; +@property (retain) id delegate; +@property (assign) SEL selector; +@property (copy) id completionHandler; +@property (retain) NSThread *thread; +@property (retain) NSError *error; + ++ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req + delegate:(id)delegate + selector:(SEL)sel + completionHandler:(id)completionHandler + thread:(NSThread *)thread; +@end + +@implementation GTMOAuth2AuthorizationArgs + +@synthesize request = request_, + delegate = delegate_, + selector = sel_, + completionHandler = completionHandler_, + thread = thread_, + error = error_; + ++ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req + delegate:(id)delegate + selector:(SEL)sel + completionHandler:(id)completionHandler + thread:(NSThread *)thread { + GTMOAuth2AuthorizationArgs *obj; + obj = [[[GTMOAuth2AuthorizationArgs alloc] init] autorelease]; + obj.request = req; + obj.delegate = delegate; + obj.selector = sel; + obj.completionHandler = completionHandler; + obj.thread = thread; + return obj; +} + +- (void)dealloc { + [request_ release]; + [delegate_ release]; + [completionHandler_ release]; + [thread_ release]; + [error_ release]; + + [super dealloc]; +} +@end + + +@interface GTMOAuth2Authentication () + +@property (retain) NSMutableArray *authorizationQueue; + +- (void)setKeysForResponseJSONData:(NSData *)data; + +- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args; + +- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args; + +- (BOOL)shouldRefreshAccessToken; + +- (void)updateExpirationDate; + +- (NSDictionary *)dictionaryWithJSONData:(NSData *)data; + +- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher + finishedWithData:(NSData *)data + error:(NSError *)error; + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error; + +- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args; + ++ (void)invokeDelegate:(id)delegate + selector:(SEL)sel + object:(id)obj1 + object:(id)obj2 + object:(id)obj3; + ++ (NSString *)unencodedOAuthParameterForString:(NSString *)str; ++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict; + ++ (NSDictionary *)dictionaryWithResponseData:(NSData *)data; + +@end + +@implementation GTMOAuth2Authentication + +@synthesize clientID = clientID_, + clientSecret = clientSecret_, + redirectURI = redirectURI_, + parameters = parameters_, + tokenURL = tokenURL_, + expirationDate = expirationDate_, + additionalTokenRequestParameters = additionalTokenRequestParameters_, + refreshFetcher = refreshFetcher_, + fetcherService = fetcherService_, + parserClass = parserClass_, + shouldAuthorizeAllRequests = shouldAuthorizeAllRequests_, + userData = userData_, + properties = properties_, + authorizationQueue = authorizationQueue_; + +// Response parameters +@dynamic accessToken, + refreshToken, + code, + assertion, + errorString, + tokenType, + scope, + expiresIn, + serviceProvider, + userEmail, + userEmailIsVerified; + +@dynamic canAuthorize; + ++ (id)authenticationWithServiceProvider:(NSString *)serviceProvider + tokenURL:(NSURL *)tokenURL + redirectURI:(NSString *)redirectURI + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret { + GTMOAuth2Authentication *obj = [[[self alloc] init] autorelease]; + obj.serviceProvider = serviceProvider; + obj.tokenURL = tokenURL; + obj.redirectURI = redirectURI; + obj.clientID = clientID; + obj.clientSecret = clientSecret; + return obj; +} + +- (id)init { + self = [super init]; + if (self) { + authorizationQueue_ = [[NSMutableArray alloc] init]; + parameters_ = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSString *)description { + NSArray *props = [NSArray arrayWithObjects:@"accessToken", @"refreshToken", + @"code", @"assertion", @"expirationDate", @"errorString", + nil]; + NSMutableString *valuesStr = [NSMutableString string]; + NSString *separator = @""; + for (NSString *prop in props) { + id result = [self valueForKey:prop]; + if (result) { + [valuesStr appendFormat:@"%@%@=\"%@\"", separator, prop, result]; + separator = @", "; + } + } + + return [NSString stringWithFormat:@"%@ %p: {%@}", + [self class], self, valuesStr]; +} + +- (void)dealloc { + [clientID_ release]; + [clientSecret_ release]; + [redirectURI_ release]; + [parameters_ release]; + [tokenURL_ release]; + [expirationDate_ release]; + [additionalTokenRequestParameters_ release]; + [refreshFetcher_ release]; + [authorizationQueue_ release]; + [userData_ release]; + [properties_ release]; + + [super dealloc]; +} + +#pragma mark - + +- (void)setKeysForResponseDictionary:(NSDictionary *)dict { + if (dict == nil) return; + + // If a new code or access token is being set, remove the old expiration + NSString *newCode = [dict objectForKey:kOAuth2CodeKey]; + NSString *newAccessToken = [dict objectForKey:kOAuth2AccessTokenKey]; + if (newCode || newAccessToken) { + self.expiresIn = nil; + } + + BOOL didRefreshTokenChange = NO; + NSString *refreshToken = [dict objectForKey:kOAuth2RefreshTokenKey]; + if (refreshToken) { + NSString *priorRefreshToken = self.refreshToken; + + if (priorRefreshToken != refreshToken + && (priorRefreshToken == nil + || ![priorRefreshToken isEqual:refreshToken])) { + didRefreshTokenChange = YES; + } + } + + [self.parameters addEntriesFromDictionary:dict]; + [self updateExpirationDate]; + + if (didRefreshTokenChange) { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMOAuth2RefreshTokenChanged + object:self + userInfo:nil]; + } + // NSLog(@"keys set ----------------------------\n%@", dict); +} + +- (void)setKeysForResponseString:(NSString *)str { + NSDictionary *dict = [[self class] dictionaryWithResponseString:str]; + [self setKeysForResponseDictionary:dict]; +} + +- (void)setKeysForResponseJSONData:(NSData *)data { + NSDictionary *dict = [self dictionaryWithJSONData:data]; + [self setKeysForResponseDictionary:dict]; +} + +- (NSDictionary *)dictionaryWithJSONData:(NSData *)data { + NSMutableDictionary *obj = nil; + NSError *error = nil; + + Class serializer = NSClassFromString(@"NSJSONSerialization"); + if (serializer) { + const NSUInteger kOpts = (1UL << 0); // NSJSONReadingMutableContainers + obj = [serializer JSONObjectWithData:data + options:kOpts + error:&error]; +#if DEBUG + if (error) { + NSString *str = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + NSLog(@"NSJSONSerialization error %@ parsing %@", + error, str); + } +#endif + return obj; + } else { + // try SBJsonParser or SBJSON + Class jsonParseClass = NSClassFromString(@"SBJsonParser"); + if (!jsonParseClass) { + jsonParseClass = NSClassFromString(@"SBJSON"); + } + if (jsonParseClass) { + GTMOAuth2ParserClass *parser = [[[jsonParseClass alloc] init] autorelease]; + NSString *jsonStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + if (jsonStr) { + obj = [parser objectWithString:jsonStr error:&error]; +#if DEBUG + if (error) { + NSLog(@"%@ error %@ parsing %@", NSStringFromClass(jsonParseClass), + error, jsonStr); + } +#endif + return obj; + } + } else { +#if DEBUG + NSAssert(0, @"GTMOAuth2Authentication: No parser available"); +#endif + } + } + return nil; +} + +#pragma mark Authorizing Requests + +// General entry point for authorizing requests + +#if NS_BLOCKS_AVAILABLE +// Authorizing with a completion block +- (void)authorizeRequest:(NSMutableURLRequest *)request + completionHandler:(void (^)(NSError *error))handler { + + GTMOAuth2AuthorizationArgs *args; + args = [GTMOAuth2AuthorizationArgs argsWithRequest:request + delegate:nil + selector:NULL + completionHandler:handler + thread:[NSThread currentThread]]; + [self authorizeRequestArgs:args]; +} +#endif + +// Authorizing with a callback selector +// +// Selector has the signature +// - (void)authentication:(GTMOAuth2Authentication *)auth +// request:(NSMutableURLRequest *)request +// finishedWithError:(NSError *)error; +- (void)authorizeRequest:(NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel { + GTMAssertSelectorNilOrImplementedWithArgs(delegate, sel, + @encode(GTMOAuth2Authentication *), + @encode(NSMutableURLRequest *), + @encode(NSError *), 0); + + GTMOAuth2AuthorizationArgs *args; + args = [GTMOAuth2AuthorizationArgs argsWithRequest:request + delegate:delegate + selector:sel + completionHandler:nil + thread:[NSThread currentThread]]; + [self authorizeRequestArgs:args]; +} + +// Internal routine common to delegate and block invocations +- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args { + BOOL didAttempt = NO; + + @synchronized(authorizationQueue_) { + + BOOL shouldRefresh = [self shouldRefreshAccessToken]; + if (shouldRefresh) { + // attempt to refresh now; once we have a fresh access token, we will + // authorize the request and call back to the user + didAttempt = YES; + if (self.refreshFetcher == nil) { + // there's not already a refresh pending + SEL finishedSel = @selector(auth:finishedRefreshWithFetcher:error:); + self.refreshFetcher = [self beginTokenFetchWithDelegate:self + didFinishSelector:finishedSel]; + if (self.refreshFetcher) { + [authorizationQueue_ addObject:args]; + } + } else { + // there's already a refresh pending + [authorizationQueue_ addObject:args]; + } + } + + if (!shouldRefresh || self.refreshFetcher == nil) { + // we're not fetching a new access token, so we can authorize the request + // now + didAttempt = [self authorizeRequestImmediateArgs:args]; + } + } + return didAttempt; +} + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error { + @synchronized(authorizationQueue_) { + // If there's an error, we want to try using the old access token anyway, + // in case it's a backend problem preventing refresh, in which case + // access tokens past their expiration date may still work + + self.refreshFetcher = nil; + + // Swap in a new auth queue in case the callbacks try to immediately auth + // another request + NSArray *pendingAuthQueue = [NSArray arrayWithArray:authorizationQueue_]; + [authorizationQueue_ removeAllObjects]; + + BOOL hasAccessToken = ([self.accessToken length] > 0); + + if (hasAccessToken && error == nil) { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMOAuth2AccessTokenRefreshed + object:self + userInfo:nil]; + } + + for (GTMOAuth2AuthorizationArgs *args in pendingAuthQueue) { + if (!hasAccessToken && args.error == nil) { + args.error = error; + } + + [self authorizeRequestImmediateArgs:args]; + } + } +} + +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request { + BOOL wasFound = NO; + @synchronized(authorizationQueue_) { + for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) { + if ([args request] == request) { + wasFound = YES; + break; + } + } + } + return wasFound; +} + +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request { + NSString *authStr = [request valueForHTTPHeaderField:@"Authorization"]; + return ([authStr length] > 0); +} + +- (void)stopAuthorization { + @synchronized(authorizationQueue_) { + [authorizationQueue_ removeAllObjects]; + + [self.refreshFetcher stopFetching]; + self.refreshFetcher = nil; + } +} + +- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args { + // This authorization entry point never attempts to refresh the access token, + // but does call the completion routine + NSMutableURLRequest *request = args.request; + + NSString *scheme = [[request URL] scheme]; + BOOL isAuthorizableRequest = self.shouldAuthorizeAllRequests + || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame; + if (!isAuthorizableRequest) { + // Request is not https, so may be insecure + // + // The NSError will be created below +#if DEBUG + NSLog(@"Cannot authorize request with scheme %@ (%@)", scheme, request); +#endif + } + + NSString *accessToken = self.accessToken; + if (isAuthorizableRequest && [accessToken length] > 0) { + if (request) { + // we have a likely valid access token + NSString *value = [NSString stringWithFormat:@"%s %@", + GTM_OAUTH2_BEARER, accessToken]; + [request setValue:value forHTTPHeaderField:@"Authorization"]; + } + + // We've authorized the request, even if the previous refresh + // failed with an error + args.error = nil; + } else if (args.error == nil) { + NSDictionary *userInfo = nil; + if (request) { + userInfo = [NSDictionary dictionaryWithObject:request + forKey:kGTMOAuth2ErrorRequestKey]; + } + NSInteger code = (isAuthorizableRequest ? + kGTMOAuth2ErrorAuthorizationFailed : + kGTMOAuth2ErrorUnauthorizableRequest); + args.error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain + code:code + userInfo:userInfo]; + } + + // Invoke any callbacks on the proper thread + if (args.delegate || args.completionHandler) { + NSThread *targetThread = args.thread; + BOOL isSameThread = [targetThread isEqual:[NSThread currentThread]]; + + [self performSelector:@selector(invokeCallbackArgs:) + onThread:targetThread + withObject:args + waitUntilDone:isSameThread]; + } + + BOOL didAuth = (args.error == nil); + return didAuth; +} + +- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args { + // Invoke the callbacks + NSError *error = args.error; + + id delegate = args.delegate; + SEL sel = args.selector; + if (delegate && sel) { + NSMutableURLRequest *request = args.request; + + NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&request atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } +#if NS_BLOCKS_AVAILABLE + id handler = args.completionHandler; + if (handler) { + void (^authCompletionBlock)(NSError *) = handler; + authCompletionBlock(error); + } +#endif +} + +- (BOOL)authorizeRequest:(NSMutableURLRequest *)request { + // Entry point for synchronous authorization mechanisms + GTMOAuth2AuthorizationArgs *args; + args = [GTMOAuth2AuthorizationArgs argsWithRequest:request + delegate:nil + selector:NULL + completionHandler:nil + thread:[NSThread currentThread]]; + return [self authorizeRequestImmediateArgs:args]; +} + +- (BOOL)canAuthorize { + NSString *token = self.refreshToken; + if (token == nil) { + // For services which do not support refresh tokens, we'll just check + // the access token + token = self.accessToken; + } + BOOL canAuth = [token length] > 0; + return canAuth; +} + +- (BOOL)shouldRefreshAccessToken { + // We should refresh the access token when it's missing or nearly expired + // and we have a refresh token + BOOL shouldRefresh = NO; + NSString *accessToken = self.accessToken; + NSString *refreshToken = self.refreshToken; + NSString *assertion = self.assertion; + + BOOL hasRefreshToken = ([refreshToken length] > 0); + BOOL hasAccessToken = ([accessToken length] > 0); + BOOL hasAssertion = ([assertion length] > 0); + + // Determine if we need to refresh the access token + if (hasRefreshToken || hasAssertion) { + if (!hasAccessToken) { + shouldRefresh = YES; + } else { + // We'll consider the token expired if it expires 60 seconds from now + // or earlier + NSDate *expirationDate = self.expirationDate; + NSTimeInterval timeToExpire = [expirationDate timeIntervalSinceNow]; + if (expirationDate == nil || timeToExpire < 60.0) { + // access token has expired, or will in a few seconds + shouldRefresh = YES; + } + } + } + return shouldRefresh; +} + +- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { + // If there is a refresh fetcher pending, wait for it. + // + // This is only intended for unit test or for use in command-line tools. + GTMHTTPFetcher *fetcher = self.refreshFetcher; + [fetcher waitForCompletionWithTimeout:timeoutInSeconds]; +} + +#pragma mark Token Fetch + +- (NSString *)userAgent { + NSBundle *bundle = [NSBundle mainBundle]; + NSString *appID = [bundle bundleIdentifier]; + + NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (version == nil) { + version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + } + + if (appID && version) { + appID = [appID stringByAppendingFormat:@"/%@", version]; + } + + NSString *userAgent = @"gtm-oauth2"; + if (appID) { + userAgent = [userAgent stringByAppendingFormat:@" %@", appID]; + } + return userAgent; +} + +- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSel { + + NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary]; + + NSString *commentTemplate; + NSString *fetchType; + + NSString *refreshToken = self.refreshToken; + NSString *code = self.code; + NSString *assertion = self.assertion; + + if (refreshToken) { + // We have a refresh token + [paramsDict setObject:@"refresh_token" forKey:@"grant_type"]; + [paramsDict setObject:refreshToken forKey:@"refresh_token"]; + + fetchType = kGTMOAuth2FetchTypeRefresh; + commentTemplate = @"refresh token for %@"; + } else if (code) { + // We have a code string + [paramsDict setObject:@"authorization_code" forKey:@"grant_type"]; + [paramsDict setObject:code forKey:@"code"]; + + NSString *redirectURI = self.redirectURI; + if ([redirectURI length] > 0) { + [paramsDict setObject:redirectURI forKey:@"redirect_uri"]; + } + + NSString *scope = self.scope; + if ([scope length] > 0) { + [paramsDict setObject:scope forKey:@"scope"]; + } + + fetchType = kGTMOAuth2FetchTypeToken; + commentTemplate = @"fetch tokens for %@"; + } else if (assertion) { + // We have an assertion string + [paramsDict setObject:assertion forKey:@"assertion"]; + [paramsDict setObject:@"http://oauth.net/grant_type/jwt/1.0/bearer" + forKey:@"grant_type"]; + commentTemplate = @"fetch tokens for %@"; + fetchType = kGTMOAuth2FetchTypeAssertion; + } else { +#if DEBUG + NSAssert(0, @"unexpected lack of code or refresh token for fetching"); +#endif + return nil; + } + + NSString *clientID = self.clientID; + if ([clientID length] > 0) { + [paramsDict setObject:clientID forKey:@"client_id"]; + } + + NSString *clientSecret = self.clientSecret; + if ([clientSecret length] > 0) { + [paramsDict setObject:clientSecret forKey:@"client_secret"]; + } + + NSDictionary *additionalParams = self.additionalTokenRequestParameters; + if (additionalParams) { + [paramsDict addEntriesFromDictionary:additionalParams]; + } + + NSString *paramStr = [[self class] encodedQueryParametersForDictionary:paramsDict]; + NSData *paramData = [paramStr dataUsingEncoding:NSUTF8StringEncoding]; + + NSURL *tokenURL = self.tokenURL; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:tokenURL]; + [request setValue:@"application/x-www-form-urlencoded" + forHTTPHeaderField:@"Content-Type"]; + + NSString *userAgent = [self userAgent]; + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + + GTMHTTPFetcher *fetcher; + id fetcherService = self.fetcherService; + if (fetcherService) { + fetcher = [fetcherService fetcherWithRequest:request]; + + // Don't use an authorizer for an auth token fetch + fetcher.authorizer = nil; + } else { + fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; + } + + [fetcher setCommentWithFormat:commentTemplate, [tokenURL host]]; + fetcher.postData = paramData; + fetcher.retryEnabled = YES; + fetcher.maxRetryInterval = 15.0; + + // Fetcher properties will retain the delegate + [fetcher setProperty:delegate forKey:kTokenFetchDelegateKey]; + if (finishedSel) { + NSString *selStr = NSStringFromSelector(finishedSel); + [fetcher setProperty:selStr forKey:kTokenFetchSelectorKey]; + } + + if ([fetcher beginFetchWithDelegate:self + didFinishSelector:@selector(tokenFetcher:finishedWithData:error:)]) { + // Fetch began + [self notifyFetchIsRunning:YES fetcher:fetcher type:fetchType]; + return fetcher; + } else { + // Failed to start fetching; typically a URL issue + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:-1 + userInfo:nil]; + [[self class] invokeDelegate:delegate + selector:finishedSel + object:self + object:nil + object:error]; + return nil; + } +} + +- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher + finishedWithData:(NSData *)data + error:(NSError *)error { + [self notifyFetchIsRunning:NO fetcher:fetcher type:nil]; + + NSDictionary *responseHeaders = [fetcher responseHeaders]; + NSString *responseType = [responseHeaders valueForKey:@"Content-Type"]; + BOOL isResponseJSON = [responseType hasPrefix:@"application/json"]; + BOOL hasData = ([data length] > 0); + + if (error) { + // Failed; if the error body is JSON, parse it and add it to the error's + // userInfo dictionary + if (hasData) { + if (isResponseJSON) { + NSDictionary *errorJson = [self dictionaryWithJSONData:data]; + if ([errorJson count] > 0) { +#if DEBUG + NSLog(@"Error %@\nError data:\n%@", error, errorJson); +#endif + // Add the JSON error body to the userInfo of the error + NSMutableDictionary *userInfo; + userInfo = [NSMutableDictionary dictionaryWithObject:errorJson + forKey:kGTMOAuth2ErrorJSONKey]; + NSDictionary *prevUserInfo = [error userInfo]; + if (prevUserInfo) { + [userInfo addEntriesFromDictionary:prevUserInfo]; + } + error = [NSError errorWithDomain:[error domain] + code:[error code] + userInfo:userInfo]; + } + } + } + } else { + // Succeeded; we have an access token +#if DEBUG + NSAssert(hasData, @"data missing in token response"); +#endif + + if (hasData) { + if (isResponseJSON) { + [self setKeysForResponseJSONData:data]; + } else { + // Support for legacy token servers that return form-urlencoded data + NSString *dataStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + [self setKeysForResponseString:dataStr]; + } + +#if DEBUG + // Watch for token exchanges that return a non-bearer or unlabeled token + NSString *tokenType = [self tokenType]; + if (tokenType == nil + || [tokenType caseInsensitiveCompare:@"bearer"] != NSOrderedSame) { + NSLog(@"GTMOAuth2: Unexpected token type: %@", tokenType); + } +#endif + } + } + + id delegate = [fetcher propertyForKey:kTokenFetchDelegateKey]; + SEL sel = NULL; + NSString *selStr = [fetcher propertyForKey:kTokenFetchSelectorKey]; + if (selStr) sel = NSSelectorFromString(selStr); + + [[self class] invokeDelegate:delegate + selector:sel + object:self + object:fetcher + object:error]; + + // Prevent a circular reference from retaining the delegate + [fetcher setProperty:nil forKey:kTokenFetchDelegateKey]; +} + +#pragma mark Fetch Notifications + +- (void)notifyFetchIsRunning:(BOOL)isStarting + fetcher:(GTMHTTPFetcher *)fetcher + type:(NSString *)fetchType { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + NSString *name = (isStarting ? kGTMOAuth2FetchStarted : kGTMOAuth2FetchStopped); + NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: + fetcher, kGTMOAuth2FetcherKey, + fetchType, kGTMOAuth2FetchTypeKey, // fetchType may be nil + nil]; + [nc postNotificationName:name + object:self + userInfo:dict]; +} + +#pragma mark Persistent Response Strings + +- (void)setKeysForPersistenceResponseString:(NSString *)str { + // All persistence keys can be set directly as if returned by a server + [self setKeysForResponseString:str]; +} + +// This returns a "response string" that can be passed later to +// setKeysForResponseString: to reuse an old access token in a new auth object +- (NSString *)persistenceResponseString { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:4]; + + NSString *refreshToken = self.refreshToken; + NSString *accessToken = nil; + if (refreshToken == nil) { + // We store the access token only for services that do not support refresh + // tokens; otherwise, we assume the access token is too perishable to + // be worth storing + accessToken = self.accessToken; + } + + // Any nil values will not set a dictionary entry + [dict setValue:refreshToken forKey:kOAuth2RefreshTokenKey]; + [dict setValue:accessToken forKey:kOAuth2AccessTokenKey]; + [dict setValue:self.serviceProvider forKey:kServiceProviderKey]; + [dict setValue:self.userEmail forKey:kUserEmailKey]; + [dict setValue:self.userEmailIsVerified forKey:kUserEmailIsVerifiedKey]; + [dict setValue:self.scope forKey:kOAuth2ScopeKey]; + + NSString *result = [[self class] encodedQueryParametersForDictionary:dict]; + return result; +} + +- (BOOL)primeForRefresh { + if (self.refreshToken == nil) { + // Cannot refresh without a refresh token + return NO; + } + self.accessToken = nil; + self.expiresIn = nil; + self.expirationDate = nil; + self.errorString = nil; + return YES; +} + +- (void)reset { + // Reset all per-authorization values + self.code = nil; + self.accessToken = nil; + self.refreshToken = nil; + self.assertion = nil; + self.expiresIn = nil; + self.errorString = nil; + self.expirationDate = nil; + self.userEmail = nil; + self.userEmailIsVerified = nil; +} + +#pragma mark Accessors for Response Parameters + +- (NSString *)accessToken { + return [self.parameters objectForKey:kOAuth2AccessTokenKey]; +} + +- (void)setAccessToken:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2AccessTokenKey]; +} + +- (NSString *)refreshToken { + return [self.parameters objectForKey:kOAuth2RefreshTokenKey]; +} + +- (void)setRefreshToken:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2RefreshTokenKey]; +} + +- (NSString *)code { + return [self.parameters objectForKey:kOAuth2CodeKey]; +} + +- (void)setCode:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2CodeKey]; +} + +- (NSString *)assertion { + return [self.parameters objectForKey:kOAuth2AssertionKey]; +} + +- (void)setAssertion:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2AssertionKey]; +} + +- (NSString *)errorString { + return [self.parameters objectForKey:kOAuth2ErrorKey]; +} + +- (void)setErrorString:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2ErrorKey]; +} + +- (NSString *)tokenType { + return [self.parameters objectForKey:kOAuth2TokenTypeKey]; +} + +- (void)setTokenType:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2TokenTypeKey]; +} + +- (NSString *)scope { + return [self.parameters objectForKey:kOAuth2ScopeKey]; +} + +- (void)setScope:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2ScopeKey]; +} + +- (NSNumber *)expiresIn { + return [self.parameters objectForKey:kOAuth2ExpiresInKey]; +} + +- (void)setExpiresIn:(NSNumber *)num { + [self.parameters setValue:num forKey:kOAuth2ExpiresInKey]; + [self updateExpirationDate]; +} + +- (void)updateExpirationDate { + // Update our absolute expiration time to something close to when + // the server expects the expiration + NSDate *date = nil; + NSNumber *expiresIn = self.expiresIn; + if (expiresIn) { + unsigned long deltaSeconds = [expiresIn unsignedLongValue]; + if (deltaSeconds > 0) { + date = [NSDate dateWithTimeIntervalSinceNow:deltaSeconds]; + } + } + self.expirationDate = date; +} + +// +// Keys custom to this class, not part of OAuth 2 +// + +- (NSString *)serviceProvider { + return [self.parameters objectForKey:kServiceProviderKey]; +} + +- (void)setServiceProvider:(NSString *)str { + [self.parameters setValue:str forKey:kServiceProviderKey]; +} + +- (NSString *)userEmail { + return [self.parameters objectForKey:kUserEmailKey]; +} + +- (void)setUserEmail:(NSString *)str { + [self.parameters setValue:str forKey:kUserEmailKey]; +} + +- (NSString *)userEmailIsVerified { + return [self.parameters objectForKey:kUserEmailIsVerifiedKey]; +} + +- (void)setUserEmailIsVerified:(NSString *)str { + [self.parameters setValue:str forKey:kUserEmailIsVerifiedKey]; +} + +#pragma mark User Properties + +- (void)setProperty:(id)obj forKey:(NSString *)key { + if (obj == nil) { + // User passed in nil, so delete the property + [properties_ removeObjectForKey:key]; + } else { + // Be sure the property dictionary exists + if (properties_ == nil) { + [self setProperties:[NSMutableDictionary dictionary]]; + } + [properties_ setObject:obj forKey:key]; + } +} + +- (id)propertyForKey:(NSString *)key { + id obj = [properties_ objectForKey:key]; + + // Be sure the returned pointer has the life of the autorelease pool, + // in case self is released immediately + return [[obj retain] autorelease]; +} + +#pragma mark Utility Routines + ++ (NSString *)encodedOAuthValueForString:(NSString *)str { + CFStringRef originalString = (CFStringRef) str; + CFStringRef leaveUnescaped = NULL; + CFStringRef forceEscaped = CFSTR("!*'();:@&=+$,/?%#[]"); + + CFStringRef escapedStr = NULL; + if (str) { + escapedStr = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + originalString, + leaveUnescaped, + forceEscaped, + kCFStringEncodingUTF8); + [(id)CFMakeCollectable(escapedStr) autorelease]; + } + + return (NSString *)escapedStr; +} + ++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict { + // Make a string like "cat=fluffy@dog=spot" + NSMutableString *result = [NSMutableString string]; + NSArray *sortedKeys = [[dict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + NSString *joiner = @""; + for (NSString *key in sortedKeys) { + NSString *value = [dict objectForKey:key]; + NSString *encodedValue = [self encodedOAuthValueForString:value]; + NSString *encodedKey = [self encodedOAuthValueForString:key]; + [result appendFormat:@"%@%@=%@", joiner, encodedKey, encodedValue]; + joiner = @"&"; + } + return result; +} + ++ (void)invokeDelegate:(id)delegate + selector:(SEL)sel + object:(id)obj1 + object:(id)obj2 + object:(id)obj3 { + if (delegate && sel) { + NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate]; + [invocation setArgument:&obj1 atIndex:2]; + [invocation setArgument:&obj2 atIndex:3]; + [invocation setArgument:&obj3 atIndex:4]; + [invocation invoke]; + } +} + ++ (NSString *)unencodedOAuthParameterForString:(NSString *)str { + NSString *plainStr = [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + return plainStr; +} + ++ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr { + // Build a dictionary from a response string of the form + // "cat=fluffy&dog=spot". Missing or empty values are considered + // empty strings; keys and values are percent-decoded. + if (responseStr == nil) return nil; + + NSArray *items = [responseStr componentsSeparatedByString:@"&"]; + + NSMutableDictionary *responseDict = [NSMutableDictionary dictionaryWithCapacity:[items count]]; + + for (NSString *item in items) { + NSString *key = nil; + NSString *value = @""; + + NSRange equalsRange = [item rangeOfString:@"="]; + if (equalsRange.location != NSNotFound) { + // The parameter has at least one '=' + key = [item substringToIndex:equalsRange.location]; + + // There are characters after the '=' + value = [item substringFromIndex:(equalsRange.location + 1)]; + } else { + // The parameter has no '=' + key = item; + } + + NSString *plainKey = [[self class] unencodedOAuthParameterForString:key]; + NSString *plainValue = [[self class] unencodedOAuthParameterForString:value]; + + [responseDict setObject:plainValue forKey:plainKey]; + } + + return responseDict; +} + ++ (NSDictionary *)dictionaryWithResponseData:(NSData *)data { + NSString *responseStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + NSDictionary *dict = [self dictionaryWithResponseString:responseStr]; + return dict; +} + ++ (NSString *)scopeWithStrings:(NSString *)str, ... { + // concatenate the strings, joined by a single space + NSString *result = @""; + NSString *joiner = @""; + if (str) { + va_list argList; + va_start(argList, str); + while (str) { + result = [result stringByAppendingFormat:@"%@%@", joiner, str]; + joiner = @" "; + str = va_arg(argList, id); + } + va_end(argList); + } + return result; +} + +@end + +#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/OAuth 2/GTMOAuth2SignIn.h b/OAuth 2/GTMOAuth2SignIn.h new file mode 100755 index 0000000..16822db --- /dev/null +++ b/OAuth 2/GTMOAuth2SignIn.h @@ -0,0 +1,184 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// This sign-in object opens and closes the web view window as needed for +// users to sign in. For signing in to Google, it also obtains +// the authenticated user's email address. +// +// Typically, this will be managed for the application by +// GTMOAuth2ViewControllerTouch or GTMOAuth2WindowController, so this +// class's interface is interesting only if +// you are creating your own window controller for sign-in. +// +// +// Delegate methods implemented by the window controller +// +// The window controller implements two methods for use by the sign-in object, +// the webRequestSelector and the finishedSelector: +// +// webRequestSelector has a signature matching +// - (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request +// +// The web request selector will be invoked with a request to be displayed, or +// nil to close the window when the final callback request has been encountered. +// +// +// finishedSelector has a signature matching +// - (void)signin:(GTMOAuth2SignIn *)signin finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error +// +// The finished selector will be invoked when sign-in has completed, except +// when explicitly canceled by calling cancelSigningIn +// + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#import +#import + +// GTMHTTPFetcher brings in GTLDefines/GDataDefines +#import "GTMHTTPFetcher.h" + +#import "GTMOAuth2Authentication.h" + +@class GTMOAuth2SignIn; + +@interface GTMOAuth2SignIn : NSObject { + @private + GTMOAuth2Authentication *auth_; + + // the endpoint for displaying the sign-in page + NSURL *authorizationURL_; + NSDictionary *additionalAuthorizationParameters_; + + id delegate_; + SEL webRequestSelector_; + SEL finishedSelector_; + + BOOL hasHandledCallback_; + + GTMHTTPFetcher *pendingFetcher_; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + BOOL shouldFetchGoogleUserEmail_; + BOOL shouldFetchGoogleUserProfile_; + NSDictionary *userProfile_; +#endif + + SCNetworkReachabilityRef reachabilityRef_; + NSTimer *networkLossTimer_; + NSTimeInterval networkLossTimeoutInterval_; + BOOL hasNotifiedNetworkLoss_; + + id userData_; +} + +@property (nonatomic, retain) GTMOAuth2Authentication *authentication; + +@property (nonatomic, retain) NSURL *authorizationURL; +@property (nonatomic, retain) NSDictionary *additionalAuthorizationParameters; + +// The delegate is released when signing in finishes or is cancelled +@property (nonatomic, retain) id delegate; +@property (nonatomic, assign) SEL webRequestSelector; +@property (nonatomic, assign) SEL finishedSelector; + +@property (nonatomic, retain) id userData; + +// By default, signing in to Google will fetch the user's email, but will not +// fetch the user's profile. +// +// The email is saved in the auth object. +// The profile is available immediately after sign-in. +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +@property (nonatomic, assign) BOOL shouldFetchGoogleUserEmail; +@property (nonatomic, assign) BOOL shouldFetchGoogleUserProfile; +@property (nonatomic, retain, readonly) NSDictionary *userProfile; +#endif + +// The default timeout for an unreachable network during display of the +// sign-in page is 30 seconds; set this to 0 to have no timeout +@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval; + +// The delegate is retained until sign-in has completed or been canceled +// +// designated initializer +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + delegate:(id)delegate + webRequestSelector:(SEL)webRequestSelector + finishedSelector:(SEL)finishedSelector; + +// A default authentication object for signing in to Google services +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret; +#endif + +#pragma mark Methods used by the Window Controller + +// Start the sequence of fetches and sign-in window display for sign-in +- (BOOL)startSigningIn; + +// Stop any pending fetches, and close the window (but don't call the +// delegate's finishedSelector) +- (void)cancelSigningIn; + +// Window controllers must tell the sign-in object about any redirect +// requested by the web view, and any changes in the webview window title +// +// If these return YES then the event was handled by the +// sign-in object (typically by closing the window) and should be ignored by +// the window controller's web view + +- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest; +- (BOOL)titleChanged:(NSString *)title; +- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage; +- (BOOL)loadFailedWithError:(NSError *)error; + +// Window controllers must tell the sign-in object if the window was closed +// prematurely by the user (but not by the sign-in object); this calls the +// delegate's finishedSelector +- (void)windowWasClosed; + +#pragma mark - + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +// Revocation of an authorized token from Google ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth; + +// Create a fetcher for obtaining the user's Google email address or profile, +// according to the current auth scopes. +// +// The auth object must have been created with appropriate scopes. +// +// The fetcher's response data can be parsed with NSJSONSerialization. ++ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth; +#endif + +#pragma mark - + +// Standard authentication values ++ (NSString *)nativeClientRedirectURI; +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (NSURL *)googleAuthorizationURL; ++ (NSURL *)googleTokenURL; ++ (NSURL *)googleUserInfoURL; +#endif + +@end + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/OAuth 2/GTMOAuth2SignIn.m b/OAuth 2/GTMOAuth2SignIn.m new file mode 100755 index 0000000..9755feb --- /dev/null +++ b/OAuth 2/GTMOAuth2SignIn.m @@ -0,0 +1,814 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#define GTMOAUTH2SIGNIN_DEFINE_GLOBALS 1 +#import "GTMOAuth2SignIn.h" + +// we'll default to timing out if the network becomes unreachable for more +// than 30 seconds when the sign-in page is displayed +static const NSTimeInterval kDefaultNetworkLossTimeoutInterval = 30.0; + +// URI indicating an installed app is signing in. This is described at +// +// http://code.google.com/apis/accounts/docs/OAuth2.html#IA +// +NSString *const kOOBString = @"urn:ietf:wg:oauth:2.0:oob"; + + +@interface GTMOAuth2Authentication (InternalMethods) +- (NSDictionary *)dictionaryWithJSONData:(NSData *)data; +@end + +@interface GTMOAuth2SignIn () +@property (assign) BOOL hasHandledCallback; +@property (retain) GTMHTTPFetcher *pendingFetcher; +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +@property (nonatomic, retain, readwrite) NSDictionary *userProfile; +#endif + +- (void)invokeFinalCallbackWithError:(NSError *)error; + +- (BOOL)startWebRequest; ++ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL + paramString:(NSString *)paramStr; +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +- (void)fetchGoogleUserInfo; +#endif +- (void)finishSignInWithError:(NSError *)error; + +- (void)handleCallbackReached; + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +- (void)infoFetcher:(GTMHTTPFetcher *)fetcher + finishedWithData:(NSData *)data + error:(NSError *)error; +#endif + +- (void)closeTheWindow; + +- (void)startReachabilityCheck; +- (void)stopReachabilityCheck; +- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef + changedFlags:(SCNetworkConnectionFlags)flags; +- (void)reachabilityTimerFired:(NSTimer *)timer; +@end + +@implementation GTMOAuth2SignIn + +@synthesize authentication = auth_; + +@synthesize authorizationURL = authorizationURL_; +@synthesize additionalAuthorizationParameters = additionalAuthorizationParameters_; + +@synthesize delegate = delegate_; +@synthesize webRequestSelector = webRequestSelector_; +@synthesize finishedSelector = finishedSelector_; +@synthesize hasHandledCallback = hasHandledCallback_; +@synthesize pendingFetcher = pendingFetcher_; +@synthesize userData = userData_; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +@synthesize shouldFetchGoogleUserEmail = shouldFetchGoogleUserEmail_; +@synthesize shouldFetchGoogleUserProfile = shouldFetchGoogleUserProfile_; +@synthesize userProfile = userProfile_; +#endif + +@synthesize networkLossTimeoutInterval = networkLossTimeoutInterval_; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (NSURL *)googleAuthorizationURL { + NSString *str = @"https://accounts.google.com/o/oauth2/auth"; + return [NSURL URLWithString:str]; +} + ++ (NSURL *)googleTokenURL { + NSString *str = @"https://accounts.google.com/o/oauth2/token"; + return [NSURL URLWithString:str]; +} + ++ (NSURL *)googleRevocationURL { + NSString *urlStr = @"https://accounts.google.com/o/oauth2/revoke"; + return [NSURL URLWithString:urlStr]; +} + ++ (NSURL *)googleUserInfoURL { + NSString *urlStr = @"https://www.googleapis.com/oauth2/v1/userinfo"; + return [NSURL URLWithString:urlStr]; +} +#endif + ++ (NSString *)nativeClientRedirectURI { + return kOOBString; +} + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret { + NSString *redirectURI = [self nativeClientRedirectURI]; + NSURL *tokenURL = [self googleTokenURL]; + + GTMOAuth2Authentication *auth; + auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle + tokenURL:tokenURL + redirectURI:redirectURI + clientID:clientID + clientSecret:clientSecret]; + auth.scope = scope; + + return auth; +} +#endif + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + delegate:(id)delegate + webRequestSelector:(SEL)webRequestSelector + finishedSelector:(SEL)finishedSelector { + // check the selectors on debug builds + GTMAssertSelectorNilOrImplementedWithArgs(delegate, webRequestSelector, + @encode(GTMOAuth2SignIn *), @encode(NSURLRequest *), 0); + GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, + @encode(GTMOAuth2SignIn *), @encode(GTMOAuth2Authentication *), + @encode(NSError *), 0); + + // designated initializer + self = [super init]; + if (self) { + auth_ = [auth retain]; + authorizationURL_ = [authorizationURL retain]; + delegate_ = [delegate retain]; + webRequestSelector_ = webRequestSelector; + finishedSelector_ = finishedSelector; + + // for Google authentication, we want to automatically fetch user info +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + NSString *host = [authorizationURL host]; + if ([host hasSuffix:@".google.com"]) { + shouldFetchGoogleUserEmail_ = YES; + } +#endif + + // default timeout for a lost internet connection while the server + // UI is displayed is 30 seconds + networkLossTimeoutInterval_ = kDefaultNetworkLossTimeoutInterval; + } + return self; +} + +- (void)dealloc { + [self stopReachabilityCheck]; + + [auth_ release]; + [authorizationURL_ release]; + [additionalAuthorizationParameters_ release]; + [delegate_ release]; + [pendingFetcher_ release]; +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + [userProfile_ release]; +#endif + [userData_ release]; + + [super dealloc]; +} + +#pragma mark Sign-in Sequence Methods + +// stop any pending fetches, and close the window (but don't call the +// delegate's finishedSelector) +- (void)cancelSigningIn { + [self.pendingFetcher stopFetching]; + self.pendingFetcher = nil; + + [self.authentication stopAuthorization]; + + [self closeTheWindow]; + + [delegate_ autorelease]; + delegate_ = nil; +} + +// +// This is the entry point to begin the sequence +// - display the authentication web page, and monitor redirects +// - exchange the code for an access token and a refresh token +// - for Google sign-in, fetch the user's email address +// - tell the delegate we're finished +// +- (BOOL)startSigningIn { + // For signing in to Google, append the scope for obtaining the authenticated + // user email and profile, as appropriate +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + GTMOAuth2Authentication *auth = self.authentication; + if (self.shouldFetchGoogleUserEmail) { + NSString *const emailScope = @"https://www.googleapis.com/auth/userinfo.email"; + NSString *scope = auth.scope; + if ([scope rangeOfString:emailScope].location == NSNotFound) { + scope = [GTMOAuth2Authentication scopeWithStrings:scope, emailScope, nil]; + auth.scope = scope; + } + } + + if (self.shouldFetchGoogleUserProfile) { + NSString *const profileScope = @"https://www.googleapis.com/auth/userinfo.profile"; + NSString *scope = auth.scope; + if ([scope rangeOfString:profileScope].location == NSNotFound) { + scope = [GTMOAuth2Authentication scopeWithStrings:scope, profileScope, nil]; + auth.scope = scope; + } + } +#endif + + // start the authorization + return [self startWebRequest]; +} + +- (NSMutableDictionary *)parametersForWebRequest { + GTMOAuth2Authentication *auth = self.authentication; + NSString *clientID = auth.clientID; + NSString *redirectURI = auth.redirectURI; + + BOOL hasClientID = ([clientID length] > 0); + BOOL hasRedirect = ([redirectURI length] > 0 + || redirectURI == [[self class] nativeClientRedirectURI]); + if (!hasClientID || !hasRedirect) { +#if DEBUG + NSAssert(hasClientID, @"GTMOAuth2SignIn: clientID needed"); + NSAssert(hasRedirect, @"GTMOAuth2SignIn: redirectURI needed"); +#endif + return NO; + } + + // invoke the UI controller's web request selector to display + // the authorization page + + // add params to the authorization URL + NSString *scope = auth.scope; + if ([scope length] == 0) scope = nil; + + NSMutableDictionary *paramsDict = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"code", @"response_type", + clientID, @"client_id", + scope, @"scope", // scope may be nil + nil]; + if (redirectURI) { + [paramsDict setObject:redirectURI forKey:@"redirect_uri"]; + } + return paramsDict; +} + +- (BOOL)startWebRequest { + NSMutableDictionary *paramsDict = [self parametersForWebRequest]; + + NSDictionary *additionalParams = self.additionalAuthorizationParameters; + if (additionalParams) { + [paramsDict addEntriesFromDictionary:additionalParams]; + } + + NSString *paramStr = [GTMOAuth2Authentication encodedQueryParametersForDictionary:paramsDict]; + + NSURL *authorizationURL = self.authorizationURL; + NSMutableURLRequest *request; + request = [[self class] mutableURLRequestWithURL:authorizationURL + paramString:paramStr]; + + [delegate_ performSelector:self.webRequestSelector + withObject:self + withObject:request]; + + // at this point, we're waiting on the server-driven html UI, so + // we want notification if we lose connectivity to the web server + [self startReachabilityCheck]; + return YES; +} + +// utility for making a request from an old URL with some additional parameters ++ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL + paramString:(NSString *)paramStr { + NSString *query = [oldURL query]; + if ([query length] > 0) { + query = [query stringByAppendingFormat:@"&%@", paramStr]; + } else { + query = paramStr; + } + + NSString *portStr = @""; + NSString *oldPort = [[oldURL port] stringValue]; + if ([oldPort length] > 0) { + portStr = [@":" stringByAppendingString:oldPort]; + } + + NSString *qMark = [query length] > 0 ? @"?" : @""; + NSString *newURLStr = [NSString stringWithFormat:@"%@://%@%@%@%@%@", + [oldURL scheme], [oldURL host], portStr, + [oldURL path], qMark, query]; + NSURL *newURL = [NSURL URLWithString:newURLStr]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:newURL]; + return request; +} + +// entry point for the window controller to tell us that the window +// prematurely closed +- (void)windowWasClosed { + [self stopReachabilityCheck]; + + NSError *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain + code:kGTMOAuth2ErrorWindowClosed + userInfo:nil]; + [self invokeFinalCallbackWithError:error]; +} + +// internal method to tell the window controller to close the window +- (void)closeTheWindow { + [self stopReachabilityCheck]; + + // a nil request means the window should be closed + [delegate_ performSelector:self.webRequestSelector + withObject:self + withObject:nil]; +} + +// entry point for the window controller to tell us what web page has been +// requested +// +// When the request is for the callback URL, this method invokes +// handleCallbackReached and returns YES +- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest { + // for Google's installed app sign-in protocol, we'll look for the + // end-of-sign-in indicator in the titleChanged: method below + NSString *redirectURI = self.authentication.redirectURI; + if (redirectURI == nil) return NO; + + // when we're searching for the window title string, then we can ignore + // redirects + NSString *standardURI = [[self class] nativeClientRedirectURI]; + if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO; + + // compare the redirectURI, which tells us when the web sign-in is done, + // to the actual redirection + NSURL *redirectURL = [NSURL URLWithString:redirectURI]; + NSURL *requestURL = [redirectedRequest URL]; + + // avoid comparing to nil host and path values (such as when redirected to + // "about:blank") + NSString *requestHost = [requestURL host]; + NSString *requestPath = [requestURL path]; + BOOL isCallback; + if (requestHost && requestPath) { + isCallback = [[redirectURL host] isEqual:[requestURL host]] + && [[redirectURL path] isEqual:[requestURL path]]; + } else if (requestURL) { + // handle "about:blank" + isCallback = [redirectURL isEqual:requestURL]; + } else { + isCallback = NO; + } + + if (!isCallback) { + // tell the caller that this request is nothing interesting + return NO; + } + + // we've reached the callback URL + + // try to get the access code + if (!self.hasHandledCallback) { + NSString *responseStr = [[redirectedRequest URL] query]; + [self.authentication setKeysForResponseString:responseStr]; + +#if DEBUG + NSAssert([self.authentication.code length] > 0 + || [self.authentication.errorString length] > 0, + @"response lacks auth code or error"); +#endif + + [self handleCallbackReached]; + } + // tell the delegate that we did handle this request + return YES; +} + +// entry point for the window controller to tell us when a new page title has +// been loadded +// +// When the title indicates sign-in has completed, this method invokes +// handleCallbackReached and returns YES +- (BOOL)titleChanged:(NSString *)title { + // return YES if the OAuth flow ending title was detected + + // right now we're just looking for a parameter list following the last space + // in the title string, but hopefully we'll eventually get something better + // from the server to search for + NSRange paramsRange = [title rangeOfString:@" " + options:NSBackwardsSearch]; + NSUInteger spaceIndex = paramsRange.location; + if (spaceIndex != NSNotFound) { + NSString *responseStr = [title substringFromIndex:(spaceIndex + 1)]; + + NSDictionary *dict = [GTMOAuth2Authentication dictionaryWithResponseString:responseStr]; + + NSString *code = [dict objectForKey:@"code"]; + NSString *error = [dict objectForKey:@"error"]; + if ([code length] > 0 || [error length] > 0) { + + if (!self.hasHandledCallback) { + [self.authentication setKeysForResponseDictionary:dict]; + + [self handleCallbackReached]; + } + return YES; + } + } + return NO; +} + +- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage { + // We're ignoring these. + return NO; +}; + +// entry point for the window controller to tell us when a load has failed +// in the webview +// +// if the initial authorization URL fails, bail out so the user doesn't +// see an empty webview +- (BOOL)loadFailedWithError:(NSError *)error { + NSURL *authorizationURL = self.authorizationURL; + NSURL *failedURL = [[error userInfo] valueForKey:@"NSErrorFailingURLKey"]; // NSURLErrorFailingURLErrorKey defined in 10.6 + + BOOL isAuthURL = [[failedURL host] isEqual:[authorizationURL host]] + && [[failedURL path] isEqual:[authorizationURL path]]; + + if (isAuthURL) { + // We can assume that we have no pending fetchers, since we only + // handle failure to load the initial authorization URL + [self closeTheWindow]; + [self invokeFinalCallbackWithError:error]; + return YES; + } + return NO; +} + +- (void)handleCallbackReached { + // the callback page was requested, or the authenticate code was loaded + // into a page's title, so exchange the auth code for access & refresh tokens + // and tell the window to close + + // avoid duplicate signals that the callback point has been reached + self.hasHandledCallback = YES; + + [self closeTheWindow]; + + NSError *error = nil; + + GTMOAuth2Authentication *auth = self.authentication; + NSString *code = auth.code; + if ([code length] > 0) { + // exchange the code for a token + SEL sel = @selector(auth:finishedWithFetcher:error:); + GTMHTTPFetcher *fetcher = [auth beginTokenFetchWithDelegate:self + didFinishSelector:sel]; + if (fetcher == nil) { + error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:-1 + userInfo:nil]; + } else { + self.pendingFetcher = fetcher; + } + + // notify the app so it can put up a post-sign in, pre-token exchange UI + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMOAuth2UserSignedIn + object:self + userInfo:nil]; + } else { + // the callback lacked an auth code + NSString *errStr = auth.errorString; + NSDictionary *userInfo = nil; + if ([errStr length] > 0) { + userInfo = [NSDictionary dictionaryWithObject:errStr + forKey:kGTMOAuth2ErrorMessageKey]; + } + + error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain + code:kGTMOAuth2ErrorAuthorizationFailed + userInfo:userInfo]; + } + + if (error) { + [self finishSignInWithError:error]; + } +} + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error { + self.pendingFetcher = nil; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + if (error == nil + && (self.shouldFetchGoogleUserEmail || self.shouldFetchGoogleUserProfile) + && [self.authentication.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) { + // fetch the user's information from the Google server + [self fetchGoogleUserInfo]; + } else { + // we're not authorizing with Google, so we're done + [self finishSignInWithError:error]; + } +#else + [self finishSignInWithError:error]; +#endif +} + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth { + // create a fetcher for obtaining the user's email or profile + NSURL *infoURL = [[self class] googleUserInfoURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:infoURL]; + + NSString *userAgent = [auth userAgent]; + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"]; + + GTMHTTPFetcher *fetcher; + id fetcherService = auth.fetcherService; + if (fetcherService) { + fetcher = [fetcherService fetcherWithRequest:request]; + } else { + fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; + } + fetcher.authorizer = auth; + fetcher.retryEnabled = YES; + fetcher.maxRetryInterval = 15.0; + fetcher.comment = @"user info"; + return fetcher; +} + +- (void)fetchGoogleUserInfo { + // fetch the user's email address or profile + GTMOAuth2Authentication *auth = self.authentication; + GTMHTTPFetcher *fetcher = [[self class] userInfoFetcherWithAuth:auth]; + [fetcher beginFetchWithDelegate:self + didFinishSelector:@selector(infoFetcher:finishedWithData:error:)]; + + self.pendingFetcher = fetcher; + + [auth notifyFetchIsRunning:YES + fetcher:fetcher + type:kGTMOAuth2FetchTypeUserInfo]; +} + +- (void)infoFetcher:(GTMHTTPFetcher *)fetcher + finishedWithData:(NSData *)data + error:(NSError *)error { + GTMOAuth2Authentication *auth = self.authentication; + [auth notifyFetchIsRunning:NO + fetcher:fetcher + type:nil]; + + self.pendingFetcher = nil; + + if (error) { +#if DEBUG + if (data) { + NSString *dataStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + NSLog(@"infoFetcher error: %@\n%@", error, dataStr); + } +#endif + } else { + // We have the authenticated user's info + if (data) { + NSDictionary *profileDict = [auth dictionaryWithJSONData:data]; + if (profileDict) { + self.userProfile = profileDict; + + // Save the email into the auth object + NSString *email = [profileDict objectForKey:@"email"]; + [auth setUserEmail:email]; + + NSNumber *verified = [profileDict objectForKey:@"verified_email"]; + [auth setUserEmailIsVerified:[verified stringValue]]; + } + } + } + [self finishSignInWithError:error]; +} + +#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + +- (void)finishSignInWithError:(NSError *)error { + [self invokeFinalCallbackWithError:error]; +} + +// convenience method for making the final call to our delegate +- (void)invokeFinalCallbackWithError:(NSError *)error { + if (delegate_ && finishedSelector_) { + GTMOAuth2Authentication *auth = self.authentication; + + NSMethodSignature *sig = [delegate_ methodSignatureForSelector:finishedSelector_]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:finishedSelector_]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&auth atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + + // we'll no longer send messages to the delegate + // + // we want to autorelease it rather than assign to the property in case + // the delegate is below us in the call stack + [delegate_ autorelease]; + delegate_ = nil; +} + +#pragma mark Reachability monitoring + +static void ReachabilityCallBack(SCNetworkReachabilityRef target, + SCNetworkConnectionFlags flags, + void *info) { + // pass the flags to the signIn object + GTMOAuth2SignIn *signIn = (GTMOAuth2SignIn *)info; + + [signIn reachabilityTarget:target + changedFlags:flags]; +} + +- (void)startReachabilityCheck { + // the user may set the timeout to 0 to skip the reachability checking + // during display of the sign-in page + if (networkLossTimeoutInterval_ <= 0.0 || reachabilityRef_ != NULL) { + return; + } + + // create a reachability target from the authorization URL, add our callback, + // and schedule it on the run loop so we'll be notified if the network drops + NSURL *url = self.authorizationURL; + const char* host = [[url host] UTF8String]; + reachabilityRef_ = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault, + host); + if (reachabilityRef_) { + BOOL isScheduled = NO; + SCNetworkReachabilityContext ctx = { 0, self, NULL, NULL, NULL }; + + if (SCNetworkReachabilitySetCallback(reachabilityRef_, + ReachabilityCallBack, &ctx)) { + if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef_, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode)) { + isScheduled = YES; + } + } + + if (!isScheduled) { + CFRelease(reachabilityRef_); + reachabilityRef_ = NULL; + } + } +} + +- (void)destroyUnreachabilityTimer { + [networkLossTimer_ invalidate]; + [networkLossTimer_ autorelease]; + networkLossTimer_ = nil; +} + +- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef + changedFlags:(SCNetworkConnectionFlags)flags { + BOOL isConnected = (flags & kSCNetworkFlagsReachable) != 0 + && (flags & kSCNetworkFlagsConnectionRequired) == 0; + + if (isConnected) { + // server is again reachable + [self destroyUnreachabilityTimer]; + + if (hasNotifiedNetworkLoss_) { + // tell the user that the network has been found + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMOAuth2NetworkFound + object:self + userInfo:nil]; + hasNotifiedNetworkLoss_ = NO; + } + } else { + // the server has become unreachable; start the timer, if necessary + if (networkLossTimer_ == nil + && networkLossTimeoutInterval_ > 0 + && !hasNotifiedNetworkLoss_) { + SEL sel = @selector(reachabilityTimerFired:); + networkLossTimer_ = [[NSTimer scheduledTimerWithTimeInterval:networkLossTimeoutInterval_ + target:self + selector:sel + userInfo:nil + repeats:NO] retain]; + } + } +} + +- (void)reachabilityTimerFired:(NSTimer *)timer { + // the user may call [[notification object] cancelSigningIn] to + // dismiss the sign-in + if (!hasNotifiedNetworkLoss_) { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMOAuth2NetworkLost + object:self + userInfo:nil]; + hasNotifiedNetworkLoss_ = YES; + } + + [self destroyUnreachabilityTimer]; +} + +- (void)stopReachabilityCheck { + [self destroyUnreachabilityTimer]; + + if (reachabilityRef_) { + SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef_, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode); + SCNetworkReachabilitySetCallback(reachabilityRef_, NULL, NULL); + + CFRelease(reachabilityRef_); + reachabilityRef_ = NULL; + } +} + +#pragma mark Token Revocation + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth { + if (auth.canAuthorize + && [auth.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) { + + // create a signed revocation request for this authentication object + NSURL *url = [self googleRevocationURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; + + NSString *token = auth.refreshToken; + NSString *encoded = [GTMOAuth2Authentication encodedOAuthValueForString:token]; + NSString *body = [@"token=" stringByAppendingString:encoded]; + + [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]]; + [request setHTTPMethod:@"POST"]; + + NSString *userAgent = [auth userAgent]; + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + + // there's nothing to be done if revocation succeeds or fails + GTMHTTPFetcher *fetcher; + id fetcherService = auth.fetcherService; + if (fetcherService) { + fetcher = [fetcherService fetcherWithRequest:request]; + } else { + fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; + } + fetcher.comment = @"revoke token"; + + // Use a completion handler fetch for better debugging, but only if we're + // guaranteed that blocks are available in the runtime +#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1060)) || \ + (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)) + // Blocks are available + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + #if DEBUG + if (error) { + NSString *errStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + NSLog(@"revoke error: %@", errStr); + } + #endif // DEBUG + }]; +#else + // Blocks may not be available + [fetcher beginFetchWithDelegate:nil didFinishSelector:NULL]; +#endif + } + + [auth reset]; +} +#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + +@end + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/OAuth 2/GTMOAuth2WindowController.h b/OAuth 2/GTMOAuth2WindowController.h new file mode 100755 index 0000000..55cec11 --- /dev/null +++ b/OAuth 2/GTMOAuth2WindowController.h @@ -0,0 +1,333 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// GTMOAuth2WindowController +// +// This window controller for Mac handles sign-in via OAuth2 to Google or +// other services. +// +// This controller is not reusable; create a new instance of this controller +// every time the user will sign in. +// +// Sample usage for signing in to a Google service: +// +// static NSString *const kKeychainItemName = @”My App: Google Plus”; +// NSString *scope = @"https://www.googleapis.com/auth/plus.me"; +// +// +// GTMOAuth2WindowController *windowController; +// windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope +// clientID:clientID +// clientSecret:clientSecret +// keychainItemName:kKeychainItemName +// resourceBundle:nil] autorelease]; +// +// [windowController signInSheetModalForWindow:mMainWindow +// delegate:self +// finishedSelector:@selector(windowController:finishedWithAuth:error:)]; +// +// The finished selector should have a signature matching this: +// +// - (void)windowController:(GTMOAuth2WindowController *)windowController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error { +// if (error != nil) { +// // sign in failed +// } else { +// // sign in succeeded +// // +// // with the GTL library, pass the authentication to the service object, +// // like +// // [[self contactService] setAuthorizer:auth]; +// // +// // or use it to sign a request directly, like +// // BOOL isAuthorizing = [self authorizeRequest:request +// // delegate:self +// // didFinishSelector:@selector(auth:finishedWithError:)]; +// } +// } +// +// To sign in to services other than Google, use the longer init method, +// as shown in the sample application +// +// If the network connection is lost for more than 30 seconds while the sign-in +// html is displayed, the notification kGTLOAuthNetworkLost will be sent. + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#include + +#if !TARGET_OS_IPHONE + +#import +#import + +// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines +#import "GTMHTTPFetcher.h" + +#import "GTMOAuth2SignIn.h" +#import "GTMOAuth2Authentication.h" +#import "GTMHTTPFetchHistory.h" // for GTMCookieStorage + +@class GTMOAuth2SignIn; + +@interface GTMOAuth2WindowController : NSWindowController { + @private + // IBOutlets + NSButton *keychainCheckbox_; + WebView *webView_; + NSButton *webCloseButton_; + NSButton *webBackButton_; + + // the object responsible for the sign-in networking sequence; it holds + // onto the authentication object as well + GTMOAuth2SignIn *signIn_; + + // the page request to load when awakeFromNib occurs + NSURLRequest *initialRequest_; + + // local storage for WebKit cookies so they're not shared with Safari + GTMCookieStorage *cookieStorage_; + + // the user we're calling back + // + // the delegate is retained only until the callback is invoked + // or the sign-in is canceled + id delegate_; + SEL finishedSelector_; + +#if NS_BLOCKS_AVAILABLE + void (^completionBlock_)(GTMOAuth2Authentication *, NSError *); +#elif !__LP64__ + // placeholders: for 32-bit builds, keep the size of the object's ivar section + // the same with and without blocks +#ifndef __clang_analyzer__ + id completionPlaceholder_; +#endif +#endif + + // flag allowing application to quit during display of sign-in sheet on 10.6 + // and later + BOOL shouldAllowApplicationTermination_; + + // delegate method for handling URLs to be opened in external windows + SEL externalRequestSelector_; + + BOOL isWindowShown_; + + // paranoid flag to ensure we only close once during the sign-in sequence + BOOL hasDoneFinalRedirect_; + + // paranoid flag to ensure we only call the user back once + BOOL hasCalledFinished_; + + // if non-nil, we display as a sheet on the specified window + NSWindow *sheetModalForWindow_; + + // if non-empty, the name of the application and service used for the + // keychain item + NSString *keychainItemName_; + + // if non-nil, the html string to be displayed immediately upon opening + // of the web view + NSString *initialHTMLString_; + + // if true, we allow default WebView handling of cookies, so the + // same user remains signed in each time the dialog is displayed + BOOL shouldPersistUser_; + + // user-defined data + id userData_; + NSMutableDictionary *properties_; +} + +// User interface elements +@property (nonatomic, assign) IBOutlet NSButton *keychainCheckbox; +@property (nonatomic, assign) IBOutlet WebView *webView; +@property (nonatomic, assign) IBOutlet NSButton *webCloseButton; +@property (nonatomic, assign) IBOutlet NSButton *webBackButton; + +// The application and service name to use for saving the auth tokens +// to the keychain +@property (nonatomic, copy) NSString *keychainItemName; + +// If true, the sign-in will remember which user was last signed in +// +// Defaults to false, so showing the sign-in window will always ask for +// the username and password, rather than skip to the grant authorization +// page. During development, it may be convenient to set this to true +// to speed up signing in. +@property (nonatomic, assign) BOOL shouldPersistUser; + +// Optional html string displayed immediately upon opening the web view +// +// This string is visible just until the sign-in web page loads, and +// may be used for a "Loading..." type of message +@property (nonatomic, copy) NSString *initialHTMLString; + +// The default timeout for an unreachable network during display of the +// sign-in page is 30 seconds, after which the notification +// kGTLOAuthNetworkLost is sent; set this to 0 to have no timeout +@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval; + +// On 10.6 and later, the sheet can allow application termination by calling +// NSWindow's setPreventsApplicationTerminationWhenModal: +@property (nonatomic, assign) BOOL shouldAllowApplicationTermination; + +// Selector for a delegate method to handle requests sent to an external +// browser. +// +// Selector should have a signature matching +// - (void)windowController:(GTMOAuth2WindowController *)controller +// opensRequest:(NSURLRequest *)request; +// +// The controller's default behavior is to use NSWorkspace's openURL: +@property (nonatomic, assign) SEL externalRequestSelector; + +// The underlying object to hold authentication tokens and authorize http +// requests +@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication; + +// The underlying object which performs the sign-in networking sequence +@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn; + +// Any arbitrary data object the user would like the controller to retain +@property (nonatomic, retain) id userData; + +// Stored property values are retained for the convenience of the caller +- (void)setProperty:(id)obj forKey:(NSString *)key; +- (id)propertyForKey:(NSString *)key; + +@property (nonatomic, retain) NSDictionary *properties; + +- (IBAction)closeWindow:(id)sender; +- (IBAction)toggleStorePasswordInKeychain:(id)sender; + +// Create a controller for authenticating to Google services +// +// scope is the requested scope of authorization +// (like "http://www.google.com/m8/feeds") +// +// keychainItemName is used for storing the token on the keychain, +// and is required for the "remember for later" checkbox to be shown; +// keychainItemName should be like "My Application: Google Contacts" +// (or set to nil if no persistent keychain storage is desired) +// +// resourceBundle may be nil if the window is in the main bundle's nib +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName // may be nil + resourceBundle:(NSBundle *)bundle; // may be nil + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle; +#endif + +// Create a controller for authenticating to non-Google services, taking +// explicit endpoint URLs and an authentication object ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName // may be nil + resourceBundle:(NSBundle *)bundle; // may be nil + +// This is the designated initializer +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle; + +// Entry point to begin displaying the sign-in window +// +// the finished selector should have a signature matching +// - (void)windowController:(GTMOAuth2WindowController *)windowController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error { +// +// Once the finished method has been invoked with no error, the auth object +// may be used to authorize requests (refreshing the access token, if necessary, +// and adding the auth header) like: +// +// [authorizer authorizeRequest:myNSMutableURLRequest] +// delegate:self +// didFinishSelector:@selector(auth:finishedWithError:)]; +// +// or can be stored in a GTL service object like +// GTLServiceGoogleContact *service = [self contactService]; +// [service setAuthorizer:auth]; +// +// The delegate is retained only until the finished selector is invoked or +// the sign-in is canceled +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +#if NS_BLOCKS_AVAILABLE +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + completionHandler:(void (^)(GTMOAuth2Authentication *auth, NSError *error))handler; +#endif + +- (void)cancelSigningIn; + +// Subclasses may override authNibName to specify a custom name ++ (NSString *)authNibName; + +// apps may replace the sign-in class with their own subclass of it ++ (Class)signInClass; ++ (void)setSignInClass:(Class)theClass; + +// Revocation of an authorized token from Google +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth; +#endif + +// Keychain +// +// The keychain checkbox is shown if the keychain application service +// name (typically set in the initWithScope: method) is non-empty +// + +// Create an authentication object for Google services from the access +// token and secret stored in the keychain; if no token is available, return +// an unauthorized auth object +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret; +#endif + +// Add tokens from the keychain, if available, to the authentication object +// +// returns YES if the authentication object was authorized from the keychain ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; + +// Method for deleting the stored access token and secret, useful for "signing +// out" ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName; + +// Method for saving the stored access token and secret; typically, this method +// is used only by the window controller ++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; +@end + +#endif // #if !TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/OAuth 2/GTMOAuth2WindowController.m b/OAuth 2/GTMOAuth2WindowController.m new file mode 100755 index 0000000..37c1e10 --- /dev/null +++ b/OAuth 2/GTMOAuth2WindowController.m @@ -0,0 +1,738 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#if !TARGET_OS_IPHONE + +#import "GTMOAuth2WindowController.h" + +@interface GTMOAuth2WindowController () +@property (nonatomic, retain) GTMOAuth2SignIn *signIn; +@property (nonatomic, copy) NSURLRequest *initialRequest; +@property (nonatomic, retain) GTMCookieStorage *cookieStorage; +@property (nonatomic, retain) NSWindow *sheetModalForWindow; + +- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil; +- (void)setupSheetTerminationHandling; +- (void)destroyWindow; +- (void)handlePrematureWindowClose; +- (BOOL)shouldUseKeychain; +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request; +- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error; +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; + +- (void)handleCookiesForResponse:(NSURLResponse *)response; +- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request; +@end + +const char *kKeychainAccountName = "OAuth"; + +@implementation GTMOAuth2WindowController + +// IBOutlets +@synthesize keychainCheckbox = keychainCheckbox_, + webView = webView_, + webCloseButton = webCloseButton_, + webBackButton = webBackButton_; + +// regular ivars +@synthesize signIn = signIn_, + initialRequest = initialRequest_, + cookieStorage = cookieStorage_, + sheetModalForWindow = sheetModalForWindow_, + keychainItemName = keychainItemName_, + initialHTMLString = initialHTMLString_, + shouldAllowApplicationTermination = shouldAllowApplicationTermination_, + externalRequestSelector = externalRequestSelector_, + shouldPersistUser = shouldPersistUser_, + userData = userData_, + properties = properties_; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +// Create a controller for authenticating to Google services ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + return [[[self alloc] initWithScope:scope + clientID:clientID + clientSecret:clientSecret + keychainItemName:keychainItemName + resourceBundle:bundle] autorelease]; +} + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + Class signInClass = [[self class] signInClass]; + GTMOAuth2Authentication *auth; + auth = [signInClass standardGoogleAuthenticationForScope:scope + clientID:clientID + clientSecret:clientSecret]; + NSURL *authorizationURL = [signInClass googleAuthorizationURL]; + return [self initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + resourceBundle:bundle]; +} +#endif + +// Create a controller for authenticating to any service ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + return [[[self alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + resourceBundle:bundle] autorelease]; +} + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + if (bundle == nil) { + bundle = [NSBundle mainBundle]; + } + + NSString *nibName = [[self class] authNibName]; + NSString *nibPath = [bundle pathForResource:nibName + ofType:@"nib"]; + self = [super initWithWindowNibPath:nibPath + owner:self]; + if (self != nil) { + // use the supplied auth and OAuth endpoint URLs + Class signInClass = [[self class] signInClass]; + signIn_ = [[signInClass alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + delegate:self + webRequestSelector:@selector(signIn:displayRequest:) + finishedSelector:@selector(signIn:finishedWithAuth:error:)]; + keychainItemName_ = [keychainItemName copy]; + + // create local, temporary storage for WebKit cookies + cookieStorage_ = [[GTMCookieStorage alloc] init]; + } + return self; +} + +- (void)dealloc { + [signIn_ release]; + [initialRequest_ release]; + [cookieStorage_ release]; + [delegate_ release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock_ release]; +#endif + [sheetModalForWindow_ release]; + [keychainItemName_ release]; + [initialHTMLString_ release]; + [userData_ release]; + [properties_ release]; + + [super dealloc]; +} + +- (void)awakeFromNib { + // load the requested initial sign-in page + [self.webView setResourceLoadDelegate:self]; + [self.webView setPolicyDelegate:self]; + + // the app may prefer some html other than blank white to be displayed + // before the sign-in web page loads + NSString *html = self.initialHTMLString; + if ([html length] > 0) { + [[self.webView mainFrame] loadHTMLString:html baseURL:nil]; + } + + // hide the keychain checkbox if we're not supporting keychain + BOOL hideKeychainCheckbox = ![self shouldUseKeychain]; + + const NSTimeInterval kJanuary2011 = 1293840000; + BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011); + if (isDateValid) { + // start the asynchronous load of the sign-in web page + [[self.webView mainFrame] performSelector:@selector(loadRequest:) + withObject:self.initialRequest + afterDelay:0.01 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + } else { + // clock date is invalid, so signing in would fail with an unhelpful error + // from the server. Warn the user in an html string showing a watch icon, + // question mark, and the system date and time. Hopefully this will clue + // in brighter users, or at least let them make a useful screenshot to show + // to developers. + // + // Even better is for apps to check the system clock and show some more + // helpful, localized instructions for users; this is really a fallback. + NSString *htmlTemplate = @"
" + @"⌚ ?
System Clock Incorrect
%@" + @"
"; + NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]]; + + [[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil]; + hideKeychainCheckbox = YES; + } + +#if DEBUG + // Verify that Javascript is enabled + BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled]; + NSAssert(hasJS, @"GTMOAuth2: Javascript is required"); +#endif + + [keychainCheckbox_ setHidden:hideKeychainCheckbox]; +} + ++ (NSString *)authNibName { + // subclasses may override this to specify a custom nib name + return @"GTMOAuth2Window"; +} + +#pragma mark - + +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + // check the selector on debug builds + GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, + @encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *), + @encode(NSError *), 0); + + delegate_ = [delegate retain]; + finishedSelector_ = finishedSelector; + + [self signInCommonForWindow:parentWindowOrNil]; +} + +#if NS_BLOCKS_AVAILABLE +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler { + completionBlock_ = [handler copy]; + + [self signInCommonForWindow:parentWindowOrNil]; +} +#endif + +- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil { + self.sheetModalForWindow = parentWindowOrNil; + hasDoneFinalRedirect_ = NO; + hasCalledFinished_ = NO; + + [self.signIn startSigningIn]; +} + +- (void)cancelSigningIn { + // The user has explicitly asked us to cancel signing in + // (so no further callback is required) + hasCalledFinished_ = YES; + + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + [completionBlock_ autorelease]; + completionBlock_ = nil; +#endif + + // The signIn object's cancel method will close the window + [self.signIn cancelSigningIn]; + hasDoneFinalRedirect_ = YES; +} + +- (IBAction)closeWindow:(id)sender { + // dismiss the window/sheet before we call back the client + [self destroyWindow]; + [self handlePrematureWindowClose]; +} + +- (IBAction)toggleStorePasswordInKeychain:(id)sender { + if ([sender state] == NSOffState) { + NSBeginAlertSheet(NSLocalizedString(@"Do you want to disable saving your login data in your keychain?", nil), NSLocalizedString(@"Keep enabled", nil), NSLocalizedString(@"Disable", nil), nil, self.window, self, @selector(disableKeychainSheetDidEnd:returnCode:contextInfo:), nil, NULL, NSLocalizedString(@"If you do not save the password you will have to sign in every time you use this feature.", nil)); + } +} + +- (void)disableKeychainSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + if (returnCode == NSAlertDefaultReturn) { + self.keychainCheckbox.state = NSOnState; + } +} + +#pragma mark SignIn callbacks + +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request { + // this is the signIn object's webRequest method, telling the controller + // to either display the request in the webview, or close the window + // + // All web requests and all window closing goes through this routine + +#if DEBUG + if ((isWindowShown_ && request != nil) + || (!isWindowShown_ && request == nil)) { + NSLog(@"Window state unexpected for request %@", [request URL]); + return; + } +#endif + + if (request != nil) { + // display the request + self.initialRequest = request; + + NSWindow *parentWindow = self.sheetModalForWindow; + if (parentWindow) { + [self setupSheetTerminationHandling]; + + NSWindow *sheet = [self window]; + [NSApp beginSheet:sheet + modalForWindow:parentWindow + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; + } else { + // modeless + [self showWindow:self]; + } + isWindowShown_ = YES; + } else { + // request was nil + [self destroyWindow]; + } +} + +- (void)setupSheetTerminationHandling { + NSWindow *sheet = [self window]; + + SEL sel = @selector(setPreventsApplicationTerminationWhenModal:); + if ([sheet respondsToSelector:sel]) { + // setPreventsApplicationTerminationWhenModal is available in NSWindow + // on 10.6 and later + BOOL boolVal = !self.shouldAllowApplicationTermination; + NSMethodSignature *sig = [sheet methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:sheet]; + [invocation setArgument:&boolVal atIndex:2]; + [invocation invoke]; + } +} + +- (void)destroyWindow { + // no request; close the window (but not immediately, in case + // we're called in response to some window event) + NSWindow *parentWindow = self.sheetModalForWindow; + if (parentWindow) { + [NSApp endSheet:[self window]]; + } else { + [[self window] performSelector:@selector(close) + withObject:nil + afterDelay:0.1 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + + // Avoid more callbacks after the delayed close happens, as the window + // controller may be gone. + [[self webView] setResourceLoadDelegate:nil]; + } + isWindowShown_ = NO; +} + +- (void)handlePrematureWindowClose { + if (!hasDoneFinalRedirect_) { + // tell the sign-in object to tell the user's finished method + // that we're done + [self.signIn windowWasClosed]; + hasDoneFinalRedirect_ = YES; + } +} + +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { + [sheet orderOut:self]; + + self.sheetModalForWindow = nil; +} + +- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error { + if (!hasCalledFinished_) { + hasCalledFinished_ = YES; + + if (error == nil) { + BOOL shouldUseKeychain = [self shouldUseKeychain]; + if (shouldUseKeychain) { + BOOL canAuthorize = auth.canAuthorize; + BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState); + + NSString *keychainItemName = self.keychainItemName; + + if (isKeychainChecked && canAuthorize) { + // save the auth params in the keychain + [[self class] saveAuthToKeychainForName:keychainItemName + authentication:auth]; + } else { + // remove the auth params from the keychain + [[self class] removeAuthFromKeychainForName:keychainItemName]; + } + } + } + + if (delegate_ && finishedSelector_) { + SEL sel = finishedSelector_; + NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&auth atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + if (completionBlock_) { + completionBlock_(auth, error); + + // release the block here to avoid a retain loop on the controller + [completionBlock_ autorelease]; + completionBlock_ = nil; + } +#endif + } +} + +static Class gSignInClass = Nil; + ++ (Class)signInClass { + if (gSignInClass == Nil) { + gSignInClass = [GTMOAuth2SignIn class]; + } + return gSignInClass; +} + ++ (void)setSignInClass:(Class)theClass { + gSignInClass = theClass; +} + +#pragma mark Token Revocation + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth { + [[self signInClass] revokeTokenForGoogleAuthentication:auth]; +} +#endif + +#pragma mark WebView methods + +- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource { + // override WebKit's cookie storage with our own to avoid cookie persistence + // across sign-ins and interaction with the Safari browser's sign-in state + [self handleCookiesForResponse:redirectResponse]; + request = [self addCookiesToRequest:request]; + + if (!hasDoneFinalRedirect_) { + hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request]; + if (hasDoneFinalRedirect_) { + // signIn has told the window to close + return nil; + } + } + return request; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource { + // override WebKit's cookie storage with our own + [self handleCookiesForResponse:response]; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource { + NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"]; + if ([title length] > 0) { + [self.signIn titleChanged:title]; + } + + [signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_]; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource { + [self.signIn loadFailedWithError:error]; +} + +- (void)windowWillClose:(NSNotification *)note { + if (isWindowShown_) { + [self handlePrematureWindowClose]; + } + isWindowShown_ = NO; +} + +- (void)webView:(WebView *)webView +decidePolicyForNewWindowAction:(NSDictionary *)actionInformation + request:(NSURLRequest *)request + newFrameName:(NSString *)frameName +decisionListener:(id)listener { + SEL sel = self.externalRequestSelector; + if (sel) { + [delegate_ performSelector:sel + withObject:self + withObject:request]; + } else { + // default behavior is to open the URL in NSWorkspace's default browser + NSURL *url = [request URL]; + [[NSWorkspace sharedWorkspace] openURL:url]; + } + [listener ignore]; +} + +#pragma mark Cookie management + +// Rather than let the WebView use Safari's default cookie storage, we intercept +// requests and response to segregate and later discard cookies from signing in. +// +// This allows the application to actually sign out by discarding the auth token +// rather than the user being kept signed in by the cookies. + +- (void)handleCookiesForResponse:(NSURLResponse *)response { + if (self.shouldPersistUser) { + // we'll let WebKit handle the cookies; they'll persist across apps + // and across runs of this app + return; + } + + if ([response respondsToSelector:@selector(allHeaderFields)]) { + // grab the cookies from the header as NSHTTPCookies and store them locally + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + if (headers) { + NSURL *url = [response URL]; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers + forURL:url]; + if ([cookies count] > 0) { + [cookieStorage_ setCookies:cookies]; + } + } + } +} + +- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request { + if (self.shouldPersistUser) { + // we'll let WebKit handle the cookies; they'll persist across apps + // and across runs of this app + return request; + } + + // override WebKit's usual automatic storage of cookies + NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease]; + [mutableRequest setHTTPShouldHandleCookies:NO]; + + // add our locally-stored cookies for this URL, if any + NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]]; + if ([cookies count] > 0) { + NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headers objectForKey:@"Cookie"]; + if (cookieHeader) { + [mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"]; + } + } + return mutableRequest; +} + +#pragma mark Keychain support + ++ (NSString *)prefsKeyForName:(NSString *)keychainItemName { + NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName]; + return result; +} + ++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth { + + [self removeAuthFromKeychainForName:keychainItemName]; + + // don't save unless we have a token that can really authorize requests + if (!auth.canAuthorize) return NO; + + // make a response string containing the values we want to save + NSString *password = [auth persistenceResponseString]; + + SecKeychainRef defaultKeychain = NULL; + SecKeychainItemRef *dontWantItemRef= NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + const char *utf8Password = [password UTF8String]; + + OSStatus err = SecKeychainAddGenericPassword(defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + (UInt32) strlen(utf8Password), utf8Password, + dontWantItemRef); + BOOL didSucceed = (err == noErr); + if (didSucceed) { + // write to preferences that we have a keychain item (so we know later + // that we can read from the keychain without raising a permissions dialog) + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:prefKey]; + } + + return didSucceed; +} + ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName { + + SecKeychainRef defaultKeychain = NULL; + SecKeychainItemRef itemRef = NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + + // we don't really care about the password here, we just want to + // get the SecKeychainItemRef so we can delete it. + OSStatus err = SecKeychainFindGenericPassword (defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + 0, NULL, // ignore password + &itemRef); + if (err != noErr) { + // failure to find is success + return YES; + } else { + // found something, so delete it + err = SecKeychainItemDelete(itemRef); + CFRelease(itemRef); + + // remove our preference key + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults removeObjectForKey:prefKey]; + + return (err == noErr); + } +} + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret { + Class signInClass = [self signInClass]; + NSURL *tokenURL = [signInClass googleTokenURL]; + NSString *redirectURI = [signInClass nativeClientRedirectURI]; + + GTMOAuth2Authentication *auth; + auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle + tokenURL:tokenURL + redirectURI:redirectURI + clientID:clientID + clientSecret:clientSecret]; + + [GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName + authentication:auth]; + return auth; +} +#endif + ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)newAuth { + [newAuth setAccessToken:nil]; + + // before accessing the keychain, check preferences to verify that we've + // previously saved a token to the keychain (so we don't needlessly raise + // a keychain access permission dialog) + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + BOOL flag = [defaults boolForKey:prefKey]; + if (!flag) { + return NO; + } + + BOOL didGetTokens = NO; + + SecKeychainRef defaultKeychain = NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + SecKeychainItemRef *dontWantItemRef = NULL; + + void *passwordBuff = NULL; + UInt32 passwordBuffLength = 0; + + OSStatus err = SecKeychainFindGenericPassword(defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + &passwordBuffLength, &passwordBuff, + dontWantItemRef); + if (err == noErr && passwordBuff != NULL) { + + NSString *password = [[[NSString alloc] initWithBytes:passwordBuff + length:passwordBuffLength + encoding:NSUTF8StringEncoding] autorelease]; + + // free the password buffer that was allocated above + SecKeychainItemFreeContent(NULL, passwordBuff); + + if (password != nil) { + [newAuth setKeysForResponseString:password]; + didGetTokens = YES; + } + } + return didGetTokens; +} + +#pragma mark User Properties + +- (void)setProperty:(id)obj forKey:(NSString *)key { + if (obj == nil) { + // User passed in nil, so delete the property + [properties_ removeObjectForKey:key]; + } else { + // Be sure the property dictionary exists + if (properties_ == nil) { + [self setProperties:[NSMutableDictionary dictionary]]; + } + [properties_ setObject:obj forKey:key]; + } +} + +- (id)propertyForKey:(NSString *)key { + id obj = [properties_ objectForKey:key]; + + // Be sure the returned pointer has the life of the autorelease pool, + // in case self is released immediately + return [[obj retain] autorelease]; +} + +#pragma mark Accessors + +- (GTMOAuth2Authentication *)authentication { + return self.signIn.authentication; +} + +- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val { + self.signIn.networkLossTimeoutInterval = val; +} + +- (NSTimeInterval)networkLossTimeoutInterval { + return self.signIn.networkLossTimeoutInterval; +} + +- (BOOL)shouldUseKeychain { + NSString *name = self.keychainItemName; + return ([name length] > 0); +} + +@end + +#endif // #if !TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/OAuth 2/OAuth2.h b/OAuth 2/OAuth2.h new file mode 100755 index 0000000..ade8d0c --- /dev/null +++ b/OAuth 2/OAuth2.h @@ -0,0 +1,13 @@ +// +// NSObject_OAuth2.h +// Notifications for YouTube +// +// Created by Kim Wittenburg on 22.09.12. +// Copyright (c) 2012 Kim Wittenburg. All rights reserved. +// + +#import "GTMHTTPFetcher.h" +#import "GTMHTTPFetchHistory.h" +#import "GTMOAuth2Authentication.h" +#import "GTMOAuth2SignIn.h" +#import "GTMOAuth2WindowController.h" \ No newline at end of file diff --git a/OAuth 2/de.lproj/GTMOAuth2Window.xib b/OAuth 2/de.lproj/GTMOAuth2Window.xib new file mode 100755 index 0000000..369a314 --- /dev/null +++ b/OAuth 2/de.lproj/GTMOAuth2Window.xib @@ -0,0 +1,568 @@ + + + + 1050 + 12E55 + 3084 + 1187.39 + 626.00 + + YES + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.WebKitIBPlugin + + + YES + 3084 + 2053 + + + + YES + NSButton + NSButtonCell + NSCustomObject + NSUserDefaultsController + NSView + NSWindowTemplate + WebView + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.WebKitIBPlugin + + + PluginDependencyRecalculationVersion + + + + YES + + GTMOAuth2WindowController + + + FirstResponder + + + NSApplication + + + 11 + 2 + {{522, 328}, {515, 419}} + 536870912 + Anmelden + NSWindow + + + {475, 290} + + + 256 + + YES + + + 274 + + YES + + YES + Apple HTML pasteboard type + Apple PDF pasteboard type + Apple PICT pasteboard type + Apple URL pasteboard type + Apple Web Archive pasteboard type + NSColor pasteboard type + NSFilenamesPboardType + NSStringPboardType + NeXT RTFD pasteboard type + NeXT Rich Text Format v1.0 pasteboard type + NeXT TIFF v4.0 pasteboard type + WebURLsWithTitlesPboardType + public.png + public.url + public.url-name + + + {{0, 20}, {515, 399}} + + + + + + + + YES + + YES + WebKitDefaultFixedFontSize + WebKitDefaultFontSize + WebKitMinimumFontSize + + + YES + + + + + + + YES + YES + + + + 289 + {{479, 0}, {16, 19}} + + YES + + 67108864 + 134217728 + + + LucidaGrande + 13 + 1044 + + + -2041823232 + 134 + + NSImage + NSStopProgressTemplate + + + Gw + 400 + 75 + + NO + + + + 289 + {{437, 0}, {16, 19}} + + + YES + + 67108864 + 134217728 + + + + -2041823232 + 134 + + NSImage + NSGoLeftTemplate + + + + 400 + 75 + + NO + + + + 289 + {{456, 0}, {16, 19}} + + + YES + + 67108864 + 134217728 + + + + -2042347520 + 134 + + NSImage + NSGoRightTemplate + + + + 400 + 75 + + NO + + + + 292 + {{2, 1}, {429, 18}} + + + YES + + -2080374784 + 131072 + Kennwort in meinem Schlüsselbund sichern + + LucidaGrande + 11 + 3100 + + + 1211912448 + 2 + + NSImage + NSSwitch + + + NSSwitch + + + + 200 + 25 + + NO + + + {515, 419} + + + + {{0, 0}, {1680, 1028}} + {475, 312} + {10000000000000, 10000000000000} + YES + + + YES + + + + + YES + + + window + + + + 8 + + + + closeWindow: + + + + 42 + + + + keychainCheckbox + + + + 46 + + + + webBackButton + + + + 47 + + + + webCloseButton + + + + 48 + + + + webView + + + + 49 + + + + toggleStorePasswordInKeychain: + + + + 50 + + + + delegate + + + + 7 + + + + goBack: + + + + 28 + + + + goForward: + + + + 29 + + + + enabled: webView.canGoBack + + + + + + enabled: webView.canGoBack + enabled + webView.canGoBack + 2 + + + 31 + + + + enabled: webView.canGoForward + + + + + + enabled: webView.canGoForward + enabled + webView.canGoForward + 2 + + + 35 + + + + + YES + + 0 + + YES + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 3 + + + YES + + + + + + 4 + + + YES + + + + + + + + + + 5 + + + + + 17 + + + YES + + + + + + 18 + + + + + 19 + + + YES + + + + + + 20 + + + + + 26 + + + YES + + + + + + 27 + + + + + 32 + + + + + 43 + + + YES + + + + + + 44 + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 17.IBPluginDependency + 18.IBPluginDependency + 19.IBPluginDependency + 20.IBPluginDependency + 26.IBPluginDependency + 27.IBPluginDependency + 3.IBPluginDependency + 3.IBWindowTemplateEditedContentRect + 3.NSWindowTemplate.visibleAtLaunch + 32.IBPluginDependency + 4.IBPluginDependency + 43.IBPluginDependency + 44.IBPluginDependency + 5.IBPluginDependency + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{112, 709}, {515, 419}} + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.WebKitIBPlugin + + + + YES + + + + + + YES + + + + + 50 + + + 0 + IBCocoaFramework + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + 3 + + YES + + YES + NSGoLeftTemplate + NSGoRightTemplate + NSStopProgressTemplate + NSSwitch + + + YES + {9, 9} + {9, 9} + {11, 11} + {15, 15} + + + + diff --git a/OAuth 2/en.lproj/GTMOAuth2Window.xib b/OAuth 2/en.lproj/GTMOAuth2Window.xib new file mode 100755 index 0000000..b38c33f --- /dev/null +++ b/OAuth 2/en.lproj/GTMOAuth2Window.xib @@ -0,0 +1,664 @@ + + + + 1050 + 12E55 + 3084 + 1187.39 + 626.00 + + YES + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.WebKitIBPlugin + + + YES + 3084 + 2053 + + + + YES + NSButton + NSButtonCell + NSCustomObject + NSUserDefaultsController + NSView + NSWindowTemplate + WebView + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.WebKitIBPlugin + + + PluginDependencyRecalculationVersion + + + + YES + + GTMOAuth2WindowController + + + FirstResponder + + + NSApplication + + + 11 + 2 + {{558, 328}, {515, 419}} + 536870912 + Sign In + NSWindow + + + {475, 290} + + + 256 + + YES + + + 274 + + YES + + YES + Apple HTML pasteboard type + Apple PDF pasteboard type + Apple PICT pasteboard type + Apple URL pasteboard type + Apple Web Archive pasteboard type + NSColor pasteboard type + NSFilenamesPboardType + NSStringPboardType + NeXT RTFD pasteboard type + NeXT Rich Text Format v1.0 pasteboard type + NeXT TIFF v4.0 pasteboard type + WebURLsWithTitlesPboardType + public.png + public.url + public.url-name + + + {{0, 20}, {515, 399}} + + + + + + + + + YES + + YES + WebKitDefaultFixedFontSize + WebKitDefaultFontSize + WebKitMinimumFontSize + + + YES + + + + + + + YES + YES + + + + 289 + {{479, 0}, {16, 19}} + + + YES + + 67108864 + 134217728 + + + LucidaGrande + 13 + 1044 + + + -2041823232 + 134 + + NSImage + NSStopProgressTemplate + + + Gw + 400 + 75 + + NO + + + + 289 + {{437, 0}, {16, 19}} + + + + YES + + 67108864 + 134217728 + + + + -2041823232 + 134 + + NSImage + NSGoLeftTemplate + + + + 400 + 75 + + NO + + + + 289 + {{456, 0}, {16, 19}} + + + + YES + + 67108864 + 134217728 + + + + -2042347520 + 134 + + NSImage + NSGoRightTemplate + + + + 400 + 75 + + NO + + + + 292 + {{2, 1}, {429, 18}} + + + + YES + + -2080374784 + 131072 + Save password in my keychain + + LucidaGrande + 11 + 3100 + + + 1211912448 + 2 + + NSImage + NSSwitch + + + NSSwitch + + + + 200 + 25 + + NO + + + {515, 419} + + + + + {{0, 0}, {1680, 1028}} + {475, 312} + {10000000000000, 10000000000000} + YES + + + YES + + + + + YES + + + window + + + + 8 + + + + closeWindow: + + + + 42 + + + + keychainCheckbox + + + + 46 + + + + webBackButton + + + + 47 + + + + webCloseButton + + + + 48 + + + + webView + + + + 49 + + + + toggleStorePasswordInKeychain: + + + + 50 + + + + delegate + + + + 7 + + + + goBack: + + + + 28 + + + + goForward: + + + + 29 + + + + enabled: webView.canGoBack + + + + + + enabled: webView.canGoBack + enabled + webView.canGoBack + 2 + + + 31 + + + + enabled: webView.canGoForward + + + + + + enabled: webView.canGoForward + enabled + webView.canGoForward + 2 + + + 35 + + + + + YES + + 0 + + YES + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 3 + + + YES + + + + + + 4 + + + YES + + + + + + + + + + 5 + + + + + 17 + + + YES + + + + + + 18 + + + + + 19 + + + YES + + + + + + 20 + + + + + 26 + + + YES + + + + + + 27 + + + + + 32 + + + + + 43 + + + YES + + + + + + 44 + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 17.IBPluginDependency + 18.IBPluginDependency + 19.IBPluginDependency + 20.IBPluginDependency + 26.IBPluginDependency + 27.IBPluginDependency + 3.IBPluginDependency + 3.IBWindowTemplateEditedContentRect + 3.NSWindowTemplate.visibleAtLaunch + 32.IBPluginDependency + 4.IBPluginDependency + 43.IBPluginDependency + 44.IBPluginDependency + 5.IBPluginDependency + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{112, 709}, {515, 419}} + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.WebKitIBPlugin + + + + YES + + + + + + YES + + + + + 50 + + + + YES + + GTMOAuth2WindowController + NSWindowController + + YES + + YES + closeWindow: + toggleStorePasswordInKeychain: + + + YES + id + id + + + + YES + + YES + closeWindow: + toggleStorePasswordInKeychain: + + + YES + + closeWindow: + id + + + toggleStorePasswordInKeychain: + id + + + + + YES + + YES + keychainCheckbox + webBackButton + webCloseButton + webView + + + YES + NSButton + NSButton + NSButton + WebView + + + + YES + + YES + keychainCheckbox + webBackButton + webCloseButton + webView + + + YES + + keychainCheckbox + NSButton + + + webBackButton + NSButton + + + webCloseButton + NSButton + + + webView + WebView + + + + + IBProjectSource + ./Classes/GTMOAuth2WindowController.h + + + + + 0 + IBCocoaFramework + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + 3 + + YES + + YES + NSGoLeftTemplate + NSGoRightTemplate + NSStopProgressTemplate + NSSwitch + + + YES + {9, 9} + {9, 9} + {11, 11} + {15, 15} + + + +