From 00ad3a24b77603ec68dec185deba9d3877939959 Mon Sep 17 00:00:00 2001 From: Ravi Date: Wed, 21 Oct 2020 12:21:07 -0700 Subject: [PATCH 01/58] Fix 4 files, remove changes from the rest --- Base.lproj/FirstTime.xib | 2 +- FirstTime.rtf | 2 +- README.md | 2 +- es.lproj/TigerTimerWindow.strings | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Base.lproj/FirstTime.xib b/Base.lproj/FirstTime.xib index d7fd3fb6..b9cc7fb8 100644 --- a/Base.lproj/FirstTime.xib +++ b/Base.lproj/FirstTime.xib @@ -375,7 +375,7 @@ Cg - + diff --git a/FirstTime.rtf b/FirstTime.rtf index 7a23506e..5e2b5874 100644 --- a/FirstTime.rtf +++ b/FirstTime.rtf @@ -21,7 +21,7 @@ \pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\sl312\slmult1\pardirnatural \ls1\ilvl0 \fs24 \cf0 {\listtext 1. } -\b Click "Edit Blacklist" +\b Click "Edit Blocklist" \b0 and add the domain names of your most distracting websites -- for example, "facebook.com". Or use the Import function to add a pre-made list of websites.\ {\listtext 2. } \b Move the slider diff --git a/README.md b/README.md index a97aa3ea..58d7a751 100755 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ABOUT ----- -SelfControl is a free and open-source application for macOS that lets you block your own access to distracting websites, your mail servers, or anything else on the Internet. Just set a period of time to block for, add sites to your blacklist, and click "Start." Until that timer expires, you will be unable to access those sites—even if you restart your computer or delete the application. +SelfControl is a free and open-source application for macOS that lets you block your own access to distracting websites, your mail servers, or anything else on the Internet. Just set a period of time to block for, add sites to your blocklist, and click "Start." Until that timer expires, you will be unable to access those sites—even if you restart your computer or delete the application. CREDITS ------- diff --git a/es.lproj/TigerTimerWindow.strings b/es.lproj/TigerTimerWindow.strings index 5953f233..fe8a9ec5 100644 --- a/es.lproj/TigerTimerWindow.strings +++ b/es.lproj/TigerTimerWindow.strings @@ -5,8 +5,8 @@ /* Class = "NSTextFieldCell"; title = "00:00:00"; ObjectID = "34"; */ "34.title" = "00:00:00"; -/* Class = "NSButtonCell"; title = "Add to Blacklist"; ObjectID = "50"; */ -"50.title" = "Add to Blacklist"; +/* Class = "NSButtonCell"; title = "Add to Blocklist"; ObjectID = "50"; */ +"50.title" = "Add to Blocklist"; /* Class = "NSPanel"; title = "Window"; ObjectID = "60"; */ "60.title" = "Ventana"; From 5758ed0a33df5f4a0daf6daacfbd8dcda88bac06 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 22 Feb 2021 12:45:51 -0800 Subject: [PATCH 02/58] Improve Netflix block reliability by adding common subdomains (fixes #630) --- Block Management/BlockManager.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Block Management/BlockManager.m b/Block Management/BlockManager.m index 045ab853..2640b47f 100644 --- a/Block Management/BlockManager.m +++ b/Block Management/BlockManager.m @@ -315,6 +315,12 @@ - (NSArray*)commonSubdomainsForHostName:(NSString*)hostName { [newHosts addObject: @"api.twitter.com"]; } + if ([hostName hasSuffix: @"netflix.com"]) { + [newHosts addObject: @"assets.nflxext.com"]; + [newHosts addObject: @"codex.nflxext.com"]; + [newHosts addObject: @"nflxext.com"]; + } + // Block the domain with no subdomains, if www.domain is blocked if([hostName rangeOfString: @"www."].location == 0) { [newHosts addObject: [hostName substringFromIndex: 4]]; From 3825845414bf6f3e90123f65841fc65bdd57b686 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 7 Mar 2021 23:57:08 -0800 Subject: [PATCH 03/58] Add block to backup host files created by VPNs, using a new HostFileBlockerSet class --- AppController.m | 1 - Block Management/BlockManager.h | 4 +- Block Management/BlockManager.m | 58 ++++++------ Block Management/HostFileBlocker.h | 26 ++++-- Block Management/HostFileBlocker.m | 34 +++++--- Block Management/HostFileBlockerSet.h | 20 +++++ Block Management/HostFileBlockerSet.m | 121 ++++++++++++++++++++++++++ Daemon/SCDaemonBlockMethods.m | 17 ++-- SelfControl.xcodeproj/project.pbxproj | 14 +++ 9 files changed, 236 insertions(+), 59 deletions(-) create mode 100644 Block Management/HostFileBlockerSet.h create mode 100644 Block Management/HostFileBlockerSet.m diff --git a/AppController.m b/AppController.m index edde00df..afa4cbd1 100755 --- a/AppController.m +++ b/AppController.m @@ -29,7 +29,6 @@ #import "SCSettings.h" #import #import "SCXPCClient.h" -#import "HostFileBlocker.h" #import "SCBlockFileReaderWriter.h" #import "SCUIUtilities.h" #import diff --git a/Block Management/BlockManager.h b/Block Management/BlockManager.h index 10e2c878..3aa31d2d 100644 --- a/Block Management/BlockManager.h +++ b/Block Management/BlockManager.h @@ -22,15 +22,15 @@ #import #import "PacketFilter.h" -#import "HostFileBlocker.h" #import "NSString+IPAddress.h" @class SCBlockEntry; +@class HostFileBlockerSet; @interface BlockManager : NSObject { NSOperationQueue* opQueue; PacketFilter* pf; - HostFileBlocker* hostsBlocker; + HostFileBlockerSet* hostBlockerSet; BOOL hostsBlockingEnabled; BOOL isAllowlist; BOOL allowLocal; diff --git a/Block Management/BlockManager.m b/Block Management/BlockManager.m index 2640b47f..33b8f1e6 100644 --- a/Block Management/BlockManager.m +++ b/Block Management/BlockManager.m @@ -25,6 +25,7 @@ #import "SCBlockEntry.h" #include #include +#import "HostFileBlockerSet.h" @implementation BlockManager @@ -51,7 +52,7 @@ - (BlockManager*)initAsAllowlist:(BOOL)allowlist allowLocal:(BOOL)local includeC [opQueue setMaxConcurrentOperationCount: 35]; pf = [[PacketFilter alloc] initAsAllowlist: allowlist]; - hostsBlocker = [[HostFileBlocker alloc] init]; + hostBlockerSet = [[HostFileBlockerSet alloc] init]; hostsBlockingEnabled = NO; isAllowlist = allowlist; @@ -65,13 +66,16 @@ - (BlockManager*)initAsAllowlist:(BOOL)allowlist allowLocal:(BOOL)local includeC } - (void)prepareToAddBlock { - if([hostsBlocker containsSelfControlBlock]) { - [hostsBlocker removeSelfControlBlock]; - [hostsBlocker writeNewFileContents]; - } + for (HostFileBlocker* blocker in hostBlockerSet.blockers) { + if([blocker containsSelfControlBlock]) { + [blocker removeSelfControlBlock]; + [blocker writeNewFileContents]; + } + } - if(!isAllowlist && ![hostsBlocker containsSelfControlBlock] && [hostsBlocker createBackupHostsFile]) { - [hostsBlocker addSelfControlBlockHeader]; + if(!isAllowlist && ![hostBlockerSet.defaultBlocker containsSelfControlBlock]) { + [hostBlockerSet createBackupHostsFile]; + [hostBlockerSet addSelfControlBlockHeader]; hostsBlockingEnabled = YES; } else { hostsBlockingEnabled = NO; @@ -83,7 +87,7 @@ - (void)enterAppendMode { NSLog(@"ERROR: can't append to allowlist block"); return; } - if(![hostsBlocker containsSelfControlBlock]) { + if(![hostBlockerSet.defaultBlocker containsSelfControlBlock]) { NSLog(@"ERROR: can't append to hosts block that doesn't yet exist"); return; } @@ -100,7 +104,7 @@ - (void)finishAppending { NSTimeInterval runTime = [finishedRunning timeIntervalSinceDate: startedRunning]; NSLog(@"BlockManager: Operation queue ran in %f seconds!", runTime); - [hostsBlocker writeNewFileContents]; + [hostBlockerSet writeNewFileContents]; [pf finishAppending]; [pf refreshPFRules]; appendMode = NO; @@ -115,8 +119,8 @@ - (void)finalizeBlock { NSLog(@"BlockManager: Operation queue ran in %f seconds!", runTime); if(hostsBlockingEnabled) { - [hostsBlocker addSelfControlBlockFooter]; - [hostsBlocker writeNewFileContents]; + [hostBlockerSet addSelfControlBlockFooter]; + [hostBlockerSet writeNewFileContents]; } [pf startBlock]; @@ -163,9 +167,9 @@ - (void)addBlockEntry:(SCBlockEntry*)entry { if(hostsBlockingEnabled && ![entry.hostname isEqualToString: @"*"] && !entry.port && !isIP) { if (appendMode) { - [hostsBlocker appendExistingBlockWithRuleForDomain: entry.hostname]; + [hostBlockerSet appendExistingBlockWithRuleForDomain: entry.hostname]; } else { - [hostsBlocker addRuleBlockingDomain: entry.hostname]; + [hostBlockerSet addRuleBlockingDomain: entry.hostname]; } } } @@ -199,12 +203,12 @@ - (BOOL)clearBlock { [pf stopBlock: false]; BOOL pfSuccess = ![pf containsSelfControlBlock]; - [hostsBlocker removeSelfControlBlock]; - BOOL hostSuccess = [hostsBlocker writeNewFileContents]; + [hostBlockerSet removeSelfControlBlock]; + BOOL hostSuccess = [hostBlockerSet writeNewFileContents]; // Revert the host file blocker's file contents to disk so we can check // whether or not it still contains the block (aka we messed up). - [hostsBlocker revertFileContentsToDisk]; - hostSuccess = hostSuccess && ![hostsBlocker containsSelfControlBlock]; + [hostBlockerSet revertFileContentsToDisk]; + hostSuccess = hostSuccess && ![hostBlockerSet containsSelfControlBlock]; BOOL clearedSuccessfully = hostSuccess && pfSuccess; @@ -217,12 +221,12 @@ - (BOOL)clearBlock { } if (!hostSuccess) { NSLog(@"WARNING: Error removing hostfile block. Attempting to restore host file backup."); - [hostsBlocker restoreBackupHostsFile]; + [hostBlockerSet restoreBackupHostsFile]; } clearedSuccessfully = ![self blockIsActive]; - if ([hostsBlocker containsSelfControlBlock]) { + if ([hostBlockerSet.defaultBlocker containsSelfControlBlock]) { NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); } if ([pf containsSelfControlBlock]) { @@ -233,7 +237,7 @@ - (BOOL)clearBlock { } } - [hostsBlocker deleteBackupHostsFile]; + [hostBlockerSet deleteBackupHostsFile]; return clearedSuccessfully; } @@ -242,12 +246,12 @@ - (BOOL)forceClearBlock { [pf stopBlock: YES]; BOOL pfSuccess = ![pf containsSelfControlBlock]; - [hostsBlocker removeSelfControlBlock]; - BOOL hostSuccess = [hostsBlocker writeNewFileContents]; + [hostBlockerSet removeSelfControlBlock]; + BOOL hostSuccess = [hostBlockerSet writeNewFileContents]; // Revert the host file blocker's file contents to disk so we can check // whether or not it still contains the block (aka we messed up). - [hostsBlocker revertFileContentsToDisk]; - hostSuccess = hostSuccess && ![hostsBlocker containsSelfControlBlock]; + [hostBlockerSet revertFileContentsToDisk]; + hostSuccess = hostSuccess && ![hostBlockerSet containsSelfControlBlock]; BOOL clearedSuccessfully = hostSuccess && pfSuccess; @@ -259,12 +263,12 @@ - (BOOL)forceClearBlock { } if (!hostSuccess) { NSLog(@"WARNING: Error removing hostfile block. Attempting to restore host file backup."); - [hostsBlocker restoreBackupHostsFile]; + [hostBlockerSet restoreBackupHostsFile]; } clearedSuccessfully = ![self blockIsActive]; - if ([hostsBlocker containsSelfControlBlock]) { + if ([hostBlockerSet.defaultBlocker containsSelfControlBlock]) { NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); } if (clearedSuccessfully) { @@ -276,7 +280,7 @@ - (BOOL)forceClearBlock { } - (BOOL)blockIsActive { - return [hostsBlocker containsSelfControlBlock] || [pf containsSelfControlBlock]; + return [hostBlockerSet.defaultBlocker containsSelfControlBlock] || [pf containsSelfControlBlock]; } - (NSArray*)commonSubdomainsForHostName:(NSString*)hostName { diff --git a/Block Management/HostFileBlocker.h b/Block Management/HostFileBlocker.h index 7f2788ec..eb47b3eb 100755 --- a/Block Management/HostFileBlocker.h +++ b/Block Management/HostFileBlocker.h @@ -22,15 +22,7 @@ #import - -@interface HostFileBlocker : NSObject { - NSLock* strLock; - NSMutableString* newFileContents; - NSStringEncoding stringEnc; - NSFileManager* fileMan; -} - -+ (BOOL)blockFoundInHostsFile; +@protocol HostFileBlocker - (BOOL)deleteBackupHostsFile; @@ -54,3 +46,19 @@ - (void)removeSelfControlBlock; @end + + +@interface HostFileBlocker : NSObject { + NSString* hostFilePath; + + NSLock* strLock; + NSMutableString* newFileContents; + NSStringEncoding stringEnc; + NSFileManager* fileMan; +} + +- (instancetype)initWithPath:(NSString*)path; + ++ (BOOL)blockFoundInHostsFile; + +@end diff --git a/Block Management/HostFileBlocker.m b/Block Management/HostFileBlocker.m index 0c8b2aec..5cba14fe 100755 --- a/Block Management/HostFileBlocker.m +++ b/Block Management/HostFileBlocker.m @@ -38,11 +38,15 @@ @implementation HostFileBlocker -- (HostFileBlocker*)init { +- (instancetype)init { + return [self initWithPath: kHostFileBlockerPath]; +} +- (instancetype)initWithPath:(NSString*)path { if(self = [super init]) { + hostFilePath = path; fileMan = [[NSFileManager alloc] init]; strLock = [[NSLock alloc] init]; - newFileContents = [NSMutableString stringWithContentsOfFile: kHostFileBlockerPath usedEncoding: &stringEnc error: NULL]; + newFileContents = [NSMutableString stringWithContentsOfFile: hostFilePath usedEncoding: &stringEnc error: NULL]; if(!newFileContents) { // if we lost our hosts file, replace it with the OS X default newFileContents = [NSMutableString stringWithString: kDefaultHostsFileContents]; @@ -65,7 +69,7 @@ + (BOOL)blockFoundInHostsFile { - (void)revertFileContentsToDisk { [strLock lock]; - newFileContents = [NSMutableString stringWithContentsOfFile: kHostFileBlockerPath usedEncoding: &stringEnc error: NULL]; + newFileContents = [NSMutableString stringWithContentsOfFile: hostFilePath usedEncoding: &stringEnc error: NULL]; if(!newFileContents) { newFileContents = [NSMutableString stringWithString: kDefaultHostsFileContents]; } @@ -76,37 +80,43 @@ - (void)revertFileContentsToDisk { - (BOOL)writeNewFileContents { [strLock lock]; - BOOL ret = [newFileContents writeToFile: kHostFileBlockerPath atomically: YES encoding: stringEnc error: NULL]; + BOOL ret = [newFileContents writeToFile: hostFilePath atomically: YES encoding: stringEnc error: NULL]; [strLock unlock]; return ret; } +- (NSString*)backupHostFilePath { + return [hostFilePath stringByAppendingPathExtension: @"bak"]; +} + - (BOOL)createBackupHostsFile { [self deleteBackupHostsFile]; - if (![fileMan fileExistsAtPath: @"/etc/hosts"]) { - [kDefaultHostsFileContents writeToFile: @"/etc/hosts" atomically:true encoding: NSUTF8StringEncoding error: NULL]; + if (![fileMan fileExistsAtPath: hostFilePath]) { + [kDefaultHostsFileContents writeToFile: hostFilePath atomically:true encoding: NSUTF8StringEncoding error: NULL]; } - if(![fileMan isReadableFileAtPath: @"/etc/hosts"] || [fileMan fileExistsAtPath: @"/etc/hosts.bak"]) { + if(![fileMan isReadableFileAtPath: hostFilePath] || [fileMan fileExistsAtPath: [self backupHostFilePath]]) { return NO; } - return [fileMan copyItemAtPath: @"/etc/hosts" toPath: @"/etc/hosts.bak" error: nil]; + return [fileMan copyItemAtPath: hostFilePath toPath: [self backupHostFilePath] error: nil]; } - (BOOL)deleteBackupHostsFile { - if(![fileMan isDeletableFileAtPath: @"/etc/hosts.bak"]) + if(![fileMan isDeletableFileAtPath: [self backupHostFilePath]]) return NO; - return [fileMan removeItemAtPath: @"/etc/hosts.bak" error: nil]; + return [fileMan removeItemAtPath: [self backupHostFilePath] error: nil]; } - (BOOL)restoreBackupHostsFile { - if(![fileMan removeItemAtPath: @"/etc/hosts" error: nil]) + NSString* backupPath = [self backupHostFilePath]; + + if(![fileMan removeItemAtPath: hostFilePath error: nil]) return NO; - if(![fileMan isReadableFileAtPath: @"/etc/hosts.bak"] || ![fileMan moveItemAtPath: @"/etc/hosts.bak" toPath: @"/etc/hosts" error: nil]) + if(![fileMan isReadableFileAtPath: backupPath] || ![fileMan moveItemAtPath: backupPath toPath: hostFilePath error: nil]) return NO; return YES; diff --git a/Block Management/HostFileBlockerSet.h b/Block Management/HostFileBlockerSet.h new file mode 100644 index 00000000..249022d6 --- /dev/null +++ b/Block Management/HostFileBlockerSet.h @@ -0,0 +1,20 @@ +// +// HostFileBlockerSet.h +// SelfControl +// +// Created by Charlie Stigler on 3/7/21. +// + +#import +#import "HostFileBlocker.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface HostFileBlockerSet : NSObject + +@property (readonly) NSArray* blockers; +@property (readonly) HostFileBlocker* defaultBlocker; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Block Management/HostFileBlockerSet.m b/Block Management/HostFileBlockerSet.m new file mode 100644 index 00000000..0cc61b2e --- /dev/null +++ b/Block Management/HostFileBlockerSet.m @@ -0,0 +1,121 @@ +// +// HostFileBlockerSet.m +// SelfControl +// +// Created by Charlie Stigler on 3/7/21. +// + +#import "HostFileBlockerSet.h" + +@implementation HostFileBlockerSet + +- (instancetype)init { + return [self initWithCommonFiles]; +} +- (instancetype)initWithCommonFiles { + NSFileManager* fileMan = [NSFileManager defaultManager]; + NSArray* commonBackupHostFilePaths = @[ + // Juniper Pulse + @"/etc/pulse-hosts.bak", + @"/etc/jnpr-pulse-hosts.bak", + @"/etc/pulse.hosts.bak", + @"/etc/jnpr-nc-hosts.bak", + + // Cisco AnyConnect + @"/etc/hosts.ac" + ]; + + NSMutableArray* hostFileBlockers = [NSMutableArray arrayWithCapacity: commonBackupHostFilePaths.count + 1]; + + _defaultBlocker = [HostFileBlocker new]; + [hostFileBlockers addObject: _defaultBlocker]; + + for (NSString* path in commonBackupHostFilePaths) { + if ([fileMan isReadableFileAtPath: path]) { + NSLog(@"INFO: found backup VPN host file at %@", path); + HostFileBlocker* blocker = [[HostFileBlocker alloc] initWithPath: path]; + [hostFileBlockers addObject: blocker]; + } + } + + _blockers = hostFileBlockers; + + return self; +} + +- (BOOL)deleteBackupHostsFile { + BOOL ret = YES; + for (HostFileBlocker* blocker in self.blockers) { + ret = ret && [blocker deleteBackupHostsFile]; + } + return ret; +} + +- (void)revertFileContentsToDisk { + for (HostFileBlocker* blocker in self.blockers) { + [blocker revertFileContentsToDisk]; + } +} + +- (BOOL)writeNewFileContents { + BOOL ret = YES; + for (HostFileBlocker* blocker in self.blockers) { + ret = ret && [blocker writeNewFileContents]; + } + return ret; +} + +- (void)addSelfControlBlockHeader { + for (HostFileBlocker* blocker in self.blockers) { + [blocker addSelfControlBlockHeader]; + } +} + +- (void)addSelfControlBlockFooter { + for (HostFileBlocker* blocker in self.blockers) { + [blocker addSelfControlBlockFooter]; + } +} + +- (BOOL)createBackupHostsFile { + BOOL ret = YES; + for (HostFileBlocker* blocker in self.blockers) { + ret = ret && [blocker createBackupHostsFile]; + } + return ret; +} + +- (BOOL)restoreBackupHostsFile { + BOOL ret = YES; + for (HostFileBlocker* blocker in self.blockers) { + ret = ret && [blocker restoreBackupHostsFile]; + } + return ret; +} + +- (void)addRuleBlockingDomain:(NSString*)domainName { + for (HostFileBlocker* blocker in self.blockers) { + [blocker addRuleBlockingDomain: domainName]; + } +} +- (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName { + for (HostFileBlocker* blocker in self.blockers) { + [blocker appendExistingBlockWithRuleForDomain: domainName]; + } +} + +- (BOOL)containsSelfControlBlock { + BOOL ret = NO; + for (HostFileBlocker* blocker in self.blockers) { + ret = ret || [blocker containsSelfControlBlock]; + } + return ret; +} + +- (void)removeSelfControlBlock { + for (HostFileBlocker* blocker in self.blockers) { + [blocker removeSelfControlBlock]; + } +} + +@end diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 85943456..06a86de2 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -12,6 +12,7 @@ #import "BlockManager.h" #import "SCDaemon.h" #import "LaunchctlHelper.h" +#import "HostFileBlockerSet.h" NSTimeInterval METHOD_LOCK_TIMEOUT = 5.0; NSTimeInterval CHECKUP_LOCK_TIMEOUT = 0.5; // use a shorter lock timeout for checkups, because we'd prefer not to have tons pile up @@ -326,30 +327,30 @@ + (void)checkupBlock { // check if anybody removed our rules, and if so // re-add them. PacketFilter* pf = [[PacketFilter alloc] init]; - HostFileBlocker* hostFileBlocker = [[HostFileBlocker alloc] init]; - if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlocker containsSelfControlBlock])) { + HostFileBlockerSet* hostFileBlockerSet = [[HostFileBlockerSet alloc] init]; + if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlockerSet.defaultBlocker containsSelfControlBlock])) { NSLog(@"INFO: Block is missing in PF or hosts, re-adding..."); // The firewall is missing at least the block header. Let's clear everything // before we re-add to make sure everything goes smoothly. [pf stopBlock: false]; - [hostFileBlocker removeSelfControlBlock]; - BOOL success = [hostFileBlocker writeNewFileContents]; + [hostFileBlockerSet removeSelfControlBlock]; + BOOL success = [hostFileBlockerSet writeNewFileContents]; // Revert the host file blocker's file contents to disk so we can check // whether or not it still contains the block after our write (aka we messed up). - [hostFileBlocker revertFileContentsToDisk]; - if(!success || [hostFileBlocker containsSelfControlBlock]) { + [hostFileBlockerSet revertFileContentsToDisk]; + if(!success || [hostFileBlockerSet.defaultBlocker containsSelfControlBlock]) { NSLog(@"WARNING: Error removing host file block. Attempting to restore backup."); - if([hostFileBlocker restoreBackupHostsFile]) + if([hostFileBlockerSet restoreBackupHostsFile]) NSLog(@"INFO: Host file backup restored."); else NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); } // Get rid of the backup file since we're about to make a new one. - [hostFileBlocker deleteBackupHostsFile]; + [hostFileBlockerSet deleteBackupHostsFile]; // Perform the re-add of the rules [SCHelperToolUtilities installBlockRulesFromSettings]; diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index bc06afef..c6d21b1d 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -50,6 +50,11 @@ CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB529BBE0F32B7ED00564FB8 /* AppController.m */; }; CB54D44C0F93E33300AA22E9 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E901D0F397FFA006DE6E4 /* Security.framework */; }; CB587E500F50FE8800C66A09 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB587E4F0F50FE8800C66A09 /* SystemConfiguration.framework */; }; + CB5888DC25F60DC300B5C64D /* HostFileBlockerSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */; }; + CB5888E225F60DC300B5C64D /* HostFileBlockerSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */; }; + CB5888E325F60DC400B5C64D /* HostFileBlockerSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */; }; + CB5888E425F60DC500B5C64D /* HostFileBlockerSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */; }; + CB5888EA25F60DC500B5C64D /* HostFileBlockerSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */; }; CB58948025B3FC6D00E9A5C0 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; CB58948725B3FC6F00E9A5C0 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; CB5DFCB72251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; @@ -549,6 +554,8 @@ CB529BBD0F32B7ED00564FB8 /* AppController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppController.h; sourceTree = ""; }; CB529BBE0F32B7ED00564FB8 /* AppController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppController.m; sourceTree = ""; }; CB587E4F0F50FE8800C66A09 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = ""; }; + CB5888B125F6056400B5C64D /* HostFileBlockerSet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostFileBlockerSet.h; sourceTree = ""; }; + CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostFileBlockerSet.m; sourceTree = ""; }; CB5DFCB52251DD1F0084CEC2 /* SCConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCConstants.h; sourceTree = ""; }; CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCConstants.m; sourceTree = ""; }; CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "selfcontrold-Info.plist"; sourceTree = ""; }; @@ -1732,6 +1739,8 @@ CB81AB8925B8E6BE006956F7 /* SCBlockEntry.m */, CBB0AE280FA74566006229B3 /* HostFileBlocker.h */, CBB0AE290FA74566006229B3 /* HostFileBlocker.m */, + CB5888B125F6056400B5C64D /* HostFileBlockerSet.h */, + CB5888B225F6056400B5C64D /* HostFileBlockerSet.m */, CBCA91101960D87300AFD20C /* PacketFilter.h */, CBCA91111960D87300AFD20C /* PacketFilter.m */, CB25806016C1FDBE0059C99A /* BlockManager.h */, @@ -3611,6 +3620,7 @@ CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */, CB81A94925B7B5B5006956F7 /* SCMigrationUtilities.m in Sources */, CB81A9F325B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, + CB5888DC25F60DC300B5C64D /* HostFileBlockerSet.m in Sources */, CB1465B825B027E700130D2E /* SCErr.m in Sources */, CBB1731920F05C07007FCAE9 /* SCMiscUtilities.m in Sources */, CB58948025B3FC6D00E9A5C0 /* HostFileBlocker.m in Sources */, @@ -3660,6 +3670,7 @@ CB74D1202480E566002B2079 /* SCDaemon.m in Sources */, CBADC28225B22BC7000EE5BB /* SCSentry.m in Sources */, CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */, + CB5888EA25F60DC500B5C64D /* HostFileBlockerSet.m in Sources */, CB62FC3F24B1327A00ADBC40 /* SCMiscUtilities.m in Sources */, CB1465BC25B027E700130D2E /* SCErr.m in Sources */, CB81AA4025B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, @@ -3686,6 +3697,7 @@ CB9C80FF19CFB79700CDCAE1 /* AppDelegate.m in Sources */, CB9C80FC19CFB79700CDCAE1 /* main.m in Sources */, CB5DFCBA2251DD1F0084CEC2 /* SCConstants.m in Sources */, + CB5888E325F60DC400B5C64D /* HostFileBlockerSet.m in Sources */, CB81AC2B25B909E1006956F7 /* SCUIUtilities.m in Sources */, CB32D2AC21902CF800B8CD68 /* SCSettings.m in Sources */, CB81A9F525B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, @@ -3722,6 +3734,7 @@ CB1465BB25B027E700130D2E /* SCErr.m in Sources */, CBB1731B20F05C09007FCAE9 /* SCMiscUtilities.m in Sources */, CB81A9F625B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, + CB5888E425F60DC500B5C64D /* HostFileBlockerSet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3738,6 +3751,7 @@ CB1CA64E25ABA5BB0084A551 /* SCXPCAuthorization.m in Sources */, CB81A9D125B7C269006956F7 /* SCBlockUtilities.m in Sources */, CBB67D5C25D6165B006E4BC9 /* NSArray+XPMArgumentsNormalizer.m in Sources */, + CB5888E225F60DC300B5C64D /* HostFileBlockerSet.m in Sources */, CBB67D5D25D6165B006E4BC9 /* NSDictionary+RubyDescription.m in Sources */, CBB67D5425D6165B006E4BC9 /* XPMArgsKonstants.m in Sources */, CBB67D5325D6165B006E4BC9 /* XPMMutableAttributedArray.m in Sources */, From 152407e7c03ac81693a3021a85c8a793ae953c5c Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Thu, 18 Mar 2021 23:58:56 -0700 Subject: [PATCH 04/58] Add SCFileWatcher and watch hosts file for changes to revert back --- Common/SCFileWatcher.h | 27 +++++++ Common/SCFileWatcher.m | 104 ++++++++++++++++++++++++++ Daemon/SCDaemon.m | 14 ++++ Daemon/SCDaemonBlockMethods.h | 2 + Daemon/SCDaemonBlockMethods.m | 90 +++++++++++++--------- SelfControl.xcodeproj/project.pbxproj | 18 +++++ 6 files changed, 220 insertions(+), 35 deletions(-) create mode 100644 Common/SCFileWatcher.h create mode 100644 Common/SCFileWatcher.m diff --git a/Common/SCFileWatcher.h b/Common/SCFileWatcher.h new file mode 100644 index 00000000..4ef12241 --- /dev/null +++ b/Common/SCFileWatcher.h @@ -0,0 +1,27 @@ +// +// SCFileWatcher.h +// SelfControl +// +// Created by Charlie Stigler on 3/20/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^SCFileWatcherCallback)(NSError* _Nullable error); + +@interface SCFileWatcher : NSObject + +@property (readonly) FSEventStreamRef eventStream; +@property (strong, readonly) SCFileWatcherCallback callbackBlock; +@property (strong, readonly) NSString* filePath; + ++ (instancetype)watcherWithFile:(NSString*)watchPath block:(void(^)(NSError* error))callbackBlock; +- (instancetype)initWithFile:(NSString*)watchPath block:(void(^)(NSError* error))callbackBlock; + +- (void)stopWatching; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Common/SCFileWatcher.m b/Common/SCFileWatcher.m new file mode 100644 index 00000000..4a26448e --- /dev/null +++ b/Common/SCFileWatcher.m @@ -0,0 +1,104 @@ +// +// SCFileWatcher.m +// SelfControl +// +// Created by Charlie Stigler on 3/20/21. +// + +#import "SCFileWatcher.h" +#include + +@implementation SCFileWatcher + +static void SCFileWatcherGlobalCallback( + ConstFSEventStreamRef streamRef, + void *callbackCtxInfo, + size_t numEvents, + void *eventPaths, // CFArrayRef + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]) +{ + NSArray* paths = (__bridge NSArray*)eventPaths; + SCFileWatcher* watcher = (__bridge SCFileWatcher*)callbackCtxInfo; + [watcher directoryWatcherTriggered: paths flags: eventFlags]; +} + +- (void)directoryWatcherTriggered:(NSArray*)eventPaths flags:(const FSEventStreamEventFlags[])eventFlags { + BOOL triggerFileWatcher = NO; + + for (unsigned int i = 0; i < eventPaths.count; i++) { + NSString* eventPath = [eventPaths[i] stringByStandardizingPath]; + + if ([eventPath isEqualToString: self.filePath]) { + triggerFileWatcher = YES; + } + } + + if (triggerFileWatcher) { + self.callbackBlock(nil); + } +} + ++ (instancetype)watcherWithFile:(NSString*)watchPath block:(void(^)(NSError* error))callbackBlock { + return [[SCFileWatcher new] initWithFile: watchPath block: callbackBlock]; +} + +- (instancetype)initWithFile:(NSString*)watchPath block:(void(^)(NSError* error))callbackBlock { + self = [super init]; + + NSFileManager* fileMan = [NSFileManager defaultManager]; + _filePath = [watchPath stringByStandardizingPath]; + BOOL isDirectory; + [fileMan fileExistsAtPath: self.filePath isDirectory: &isDirectory]; + + NSString* directoryPath; + if (isDirectory) { + directoryPath = self.filePath; + } else { + directoryPath = [self.filePath stringByDeletingLastPathComponent]; + } + + FSEventStreamContext callbackCtx; + callbackCtx.version = 0; + callbackCtx.info = (__bridge void *)self; + callbackCtx.retain = NULL; + callbackCtx.release = NULL; + callbackCtx.copyDescription = NULL; + + FSEventStreamRef eventStream = FSEventStreamCreate( + kCFAllocatorDefault, + &SCFileWatcherGlobalCallback, + &callbackCtx, // context + (__bridge CFArrayRef)@[directoryPath], + kFSEventStreamEventIdSinceNow, + 1.5, // seconds to throttle callbacks + kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagMarkSelf | kFSEventStreamCreateFlagIgnoreSelf | kFSEventStreamCreateFlagFileEvents + ); + + FSEventStreamScheduleWithRunLoop(eventStream, + [[NSRunLoop currentRunLoop] getCFRunLoop], + kCFRunLoopDefaultMode); + if (!FSEventStreamStart(eventStream)) { + NSLog(@"WARNING: failed to start watching file %@", watchPath); + FSEventStreamUnscheduleFromRunLoop(eventStream, [[NSRunLoop currentRunLoop] getCFRunLoop], kCFRunLoopDefaultMode); + FSEventStreamInvalidate(eventStream); + FSEventStreamRelease(eventStream); + return nil; + } + + _eventStream = eventStream; + _callbackBlock = callbackBlock; + + return self; +} + +- (void)stopWatching { + FSEventStreamStop(self.eventStream); + FSEventStreamUnscheduleFromRunLoop(self.eventStream, [[NSRunLoop currentRunLoop] getCFRunLoop], kCFRunLoopDefaultMode); + FSEventStreamInvalidate(self.eventStream); + FSEventStreamRelease(self.eventStream); + + _eventStream = NULL; +} + +@end diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index d6f118cb..ab0aee1b 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -9,6 +9,7 @@ #import "SCDaemonProtocol.h" #import "SCDaemonXPC.h" #import"SCDaemonBlockMethods.h" +#import "SCFileWatcher.h" static NSString* serviceName = @"org.eyebeam.selfcontrold"; float const INACTIVITY_LIMIT_SECS = 60 * 2; // 2 minutes @@ -27,6 +28,8 @@ @interface SCDaemon () @property (strong, readwrite) NSTimer* inactivityTimer; @property (nonatomic, strong, readwrite) NSDate* lastActivityDate; +@property (nonatomic, strong) SCFileWatcher* hostsFileWatcher; + @end @implementation SCDaemon @@ -63,6 +66,13 @@ - (void)start { [self startInactivityTimer]; [self resetInactivityTimer]; + + self.hostsFileWatcher = [SCFileWatcher watcherWithFile: @"/etc/hosts" block:^(NSError * _Nonnull error) { + if ([SCBlockUtilities anyBlockIsRunning]) { + NSLog(@"INFO: hosts file changed, checking block integrity"); + [SCDaemonBlockMethods checkBlockIntegrity]; + } + }]; } - (void)startCheckupTimer { @@ -127,6 +137,10 @@ - (void)dealloc { [self.inactivityTimer invalidate]; self.inactivityTimer = nil; } + if (self.hostsFileWatcher) { + [self.hostsFileWatcher stopWatching]; + self.hostsFileWatcher = nil; + } } #pragma mark - NSXPCListenerDelegate diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index 72bfede8..ca1ccf6d 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -29,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN // (i.e. extends the block) + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; ++ (void)checkBlockIntegrity; + @end NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 06a86de2..e66d8804 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -280,12 +280,13 @@ + (void)checkupBlock { [SCSentry addBreadcrumb: @"Daemon method checkupBlock called" category: @"daemon"]; SCSettings* settings = [SCSettings sharedSettings]; - NSTimeInterval integrityCheckIntervalSecs = 10.0; + NSTimeInterval integrityCheckIntervalSecs = 15.0; static NSDate* lastBlockIntegrityCheck; if (lastBlockIntegrityCheck == nil) { lastBlockIntegrityCheck = [NSDate distantPast]; } + BOOL shouldRunIntegrityCheck = NO; if(![SCBlockUtilities anyBlockIsRunning]) { // No block appears to be running at all in our settings. // Most likely, the user removed it trying to get around the block. Boo! @@ -326,44 +327,63 @@ + (void)checkupBlock { // The block is still on. Every once in a while, we should // check if anybody removed our rules, and if so // re-add them. - PacketFilter* pf = [[PacketFilter alloc] init]; - HostFileBlockerSet* hostFileBlockerSet = [[HostFileBlockerSet alloc] init]; - if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlockerSet.defaultBlocker containsSelfControlBlock])) { - NSLog(@"INFO: Block is missing in PF or hosts, re-adding..."); - // The firewall is missing at least the block header. Let's clear everything - // before we re-add to make sure everything goes smoothly. - - [pf stopBlock: false]; - - [hostFileBlockerSet removeSelfControlBlock]; - BOOL success = [hostFileBlockerSet writeNewFileContents]; - // Revert the host file blocker's file contents to disk so we can check - // whether or not it still contains the block after our write (aka we messed up). - [hostFileBlockerSet revertFileContentsToDisk]; - if(!success || [hostFileBlockerSet.defaultBlocker containsSelfControlBlock]) { - NSLog(@"WARNING: Error removing host file block. Attempting to restore backup."); - - if([hostFileBlockerSet restoreBackupHostsFile]) - NSLog(@"INFO: Host file backup restored."); - else - NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); - } - - // Get rid of the backup file since we're about to make a new one. - [hostFileBlockerSet deleteBackupHostsFile]; - - // Perform the re-add of the rules - [SCHelperToolUtilities installBlockRulesFromSettings]; - - [SCHelperToolUtilities clearCachesIfRequested]; - - [SCSentry addBreadcrumb: @"Daemon found compromised block integrity and re-added rules" category: @"daemon"]; - NSLog(@"INFO: Checkup ran, readded block rules."); - } else NSLog(@"INFO: Checkup ran with integrity check, no action needed."); + shouldRunIntegrityCheck = YES; } [[SCDaemon sharedDaemon] resetInactivityTimer]; [self.daemonMethodLock unlock]; + + // if we need to run an integrity check, we need to do it at the very end after we give up our lock + // because checkBlockIntegrity requests its own lock, and we don't want it to deadlock + if (shouldRunIntegrityCheck) { + [SCDaemonBlockMethods checkBlockIntegrity]; + } +} + ++ (void)checkBlockIntegrity { + if (![SCDaemonBlockMethods lockOrTimeout: nil timeout: CHECKUP_LOCK_TIMEOUT]) { + return; + } + + [SCSentry addBreadcrumb: @"Daemon method checkBlockIntegrity called" category: @"daemon"]; + + SCSettings* settings = [SCSettings sharedSettings]; + PacketFilter* pf = [[PacketFilter alloc] init]; + HostFileBlockerSet* hostFileBlockerSet = [[HostFileBlockerSet alloc] init]; + if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlockerSet.defaultBlocker containsSelfControlBlock])) { + NSLog(@"INFO: Block is missing in PF or hosts, re-adding..."); + // The firewall is missing at least the block header. Let's clear everything + // before we re-add to make sure everything goes smoothly. + + [pf stopBlock: false]; + + [hostFileBlockerSet removeSelfControlBlock]; + BOOL success = [hostFileBlockerSet writeNewFileContents]; + // Revert the host file blocker's file contents to disk so we can check + // whether or not it still contains the block after our write (aka we messed up). + [hostFileBlockerSet revertFileContentsToDisk]; + if(!success || [hostFileBlockerSet.defaultBlocker containsSelfControlBlock]) { + NSLog(@"WARNING: Error removing host file block. Attempting to restore backup."); + + if([hostFileBlockerSet restoreBackupHostsFile]) + NSLog(@"INFO: Host file backup restored."); + else + NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); + } + + // Get rid of the backup file since we're about to make a new one. + [hostFileBlockerSet deleteBackupHostsFile]; + + // Perform the re-add of the rules + [SCHelperToolUtilities installBlockRulesFromSettings]; + + [SCHelperToolUtilities clearCachesIfRequested]; + + [SCSentry addBreadcrumb: @"Daemon found compromised block integrity and re-added rules" category: @"daemon"]; + NSLog(@"INFO: Integrity check ran; readded block rules."); + } else NSLog(@"INFO: Integrity check ran; no action needed."); + + [self.daemonMethodLock unlock]; } @end diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index c6d21b1d..7bf8a0d1 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -172,6 +172,13 @@ CBBF4E8B1582F8BD00E364D9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CBBF4E891582F8BD00E364D9 /* InfoPlist.strings */; }; CBBF4E8E1582F8E000E364D9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CBBF4E8C1582F8E000E364D9 /* Localizable.strings */; }; CBBF4EE515830D7300E364D9 /* TimerWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBBF4EE715830D7300E364D9 /* TimerWindow.xib */; }; + CBC1F4B426070358008E3FA8 /* SCFileWatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = CBC1F4B226070358008E3FA8 /* SCFileWatcher.h */; }; + CBC1F4B526070358008E3FA8 /* SCFileWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */; }; + CBC1F4B626070358008E3FA8 /* SCFileWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */; }; + CBC1F4B726070358008E3FA8 /* SCFileWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */; }; + CBC1F4B826070358008E3FA8 /* SCFileWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */; }; + CBC1F4B926070358008E3FA8 /* SCFileWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */; }; + CBC1F4BA26070358008E3FA8 /* SCFileWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */; }; CBC2F8580F4672FE00CF2A42 /* LaunchctlHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */; }; CBCA91121960D87300AFD20C /* PacketFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = CBCA91111960D87300AFD20C /* PacketFilter.m */; }; CBD2677011ED92DE00042CD8 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E90190F397FF6006DE6E4 /* CoreFoundation.framework */; }; @@ -1351,6 +1358,8 @@ CBBF4E941582F8FC00E364D9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; CBBF4E951582F8FC00E364D9 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; CBBF4E961582F8FC00E364D9 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; + CBC1F4B226070358008E3FA8 /* SCFileWatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCFileWatcher.h; sourceTree = ""; }; + CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCFileWatcher.m; sourceTree = ""; }; CBC25B1319F6CBDE0013E190 /* pt-BR */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; CBC25B1919F6CC030013E190 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = ""; }; CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LaunchctlHelper.m; sourceTree = ""; }; @@ -1642,6 +1651,8 @@ CB81A9E325B7C2B9006956F7 /* Utility */, CBADC27C25B22BC7000EE5BB /* SCSentry.h */, CBADC27D25B22BC7000EE5BB /* SCSentry.m */, + CBC1F4B226070358008E3FA8 /* SCFileWatcher.h */, + CBC1F4B326070358008E3FA8 /* SCFileWatcher.m */, CB81A9F025B7C5F7006956F7 /* SCBlockFileReaderWriter.h */, CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */, CB1465B625B027E700130D2E /* SCErr.h */, @@ -2859,6 +2870,7 @@ CB81A94825B7B5B5006956F7 /* SCMigrationUtilities.h in Headers */, CB81AA3A25B7D152006956F7 /* SCHelperToolUtilities.h in Headers */, CB81AAB725B7E6C7006956F7 /* DeprecationSilencers.h in Headers */, + CBC1F4B426070358008E3FA8 /* SCFileWatcher.h in Headers */, CB81A9CF25B7C269006956F7 /* SCBlockUtilities.h in Headers */, CB81A9F225B7C5F7006956F7 /* SCBlockFileReaderWriter.h in Headers */, ); @@ -3613,6 +3625,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CBC1F4B526070358008E3FA8 /* SCFileWatcher.m in Sources */, CBADC27E25B22BC7000EE5BB /* SCSentry.m in Sources */, 8D11072D0486CEB800E47090 /* main.m in Sources */, CB81AC2A25B909E1006956F7 /* SCUIUtilities.m in Sources */, @@ -3646,6 +3659,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CBC1F4B926070358008E3FA8 /* SCFileWatcher.m in Sources */, CBDF919A225C5A9700358B95 /* SCMiscUtilities.m in Sources */, CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */, CB81AA3F25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, @@ -3671,6 +3685,7 @@ CBADC28225B22BC7000EE5BB /* SCSentry.m in Sources */, CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */, CB5888EA25F60DC500B5C64D /* HostFileBlockerSet.m in Sources */, + CBC1F4BA26070358008E3FA8 /* SCFileWatcher.m in Sources */, CB62FC3F24B1327A00ADBC40 /* SCMiscUtilities.m in Sources */, CB1465BC25B027E700130D2E /* SCErr.m in Sources */, CB81AA4025B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, @@ -3707,6 +3722,7 @@ CB81AB8C25B8E6BE006956F7 /* SCBlockEntry.m in Sources */, CBB1731C20F05C0A007FCAE9 /* SCMiscUtilities.m in Sources */, CBADC28025B22BC7000EE5BB /* SCSentry.m in Sources */, + CBC1F4B726070358008E3FA8 /* SCFileWatcher.m in Sources */, CB81A94B25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3730,6 +3746,7 @@ CB81A9D325B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB9C812319CFBB4400CDCAE1 /* LaunchctlHelper.m in Sources */, CB1CA64D25ABA5BB0084A551 /* SCXPCAuthorization.m in Sources */, + CBC1F4B826070358008E3FA8 /* SCFileWatcher.m in Sources */, CB9C812219CFBB3800CDCAE1 /* PacketFilter.m in Sources */, CB1465BB25B027E700130D2E /* SCErr.m in Sources */, CBB1731B20F05C09007FCAE9 /* SCMiscUtilities.m in Sources */, @@ -3771,6 +3788,7 @@ CBB0AE2A0FA74566006229B3 /* HostFileBlocker.m in Sources */, CB81A94A25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CB32D2A921902CB300B8CD68 /* SCSettings.m in Sources */, + CBC1F4B626070358008E3FA8 /* SCFileWatcher.m in Sources */, CB1CA65025ABA5BB0084A551 /* SCXPCClient.m in Sources */, CB25806216C1FDBE0059C99A /* BlockManager.m in Sources */, CB25806716C237F10059C99A /* NSString+IPAddress.m in Sources */, From 99b3bb89b7a10c5b3b0caecf885684421435f404 Mon Sep 17 00:00:00 2001 From: Matt Bettinson Date: Sun, 4 Apr 2021 02:05:40 -0400 Subject: [PATCH 05/58] Add instagram.com to commonDistractingWebsites (#674) * Add instagram.com to commonDistractingWebsites in HostImporter --- Block Management/HostImporter.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Block Management/HostImporter.m b/Block Management/HostImporter.m index 478f7f64..43b2475d 100755 --- a/Block Management/HostImporter.m +++ b/Block Management/HostImporter.m @@ -42,6 +42,7 @@ + (NSArray*)commonDistractingWebsites { @"vine.co", @"pinterest.com", @"stumbleupon.com", + @"instagram.com", ]; } + (NSArray*)newsAndPublications { From 6d91df8a2709f98af3b687e77f6041a88276850a Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 18 Apr 2021 00:35:23 -0700 Subject: [PATCH 06/58] Move block duration slider to its own SCDurationSlider class --- AppController.h | 3 +- AppController.m | 39 ++------ Base.lproj/MainMenu.xib | 2 +- SCDurationSlider.h | 25 +++++ SCDurationSlider.m | 138 ++++++++++++++++++++++++++ SCUIUtilities.h | 3 - SCUIUtilities.m | 31 ------ SelfControl.xcodeproj/project.pbxproj | 6 ++ 8 files changed, 179 insertions(+), 68 deletions(-) create mode 100644 SCDurationSlider.h create mode 100644 SCDurationSlider.m diff --git a/AppController.h b/AppController.h index 4db32f47..7ef083a4 100755 --- a/AppController.h +++ b/AppController.h @@ -30,11 +30,12 @@ #import #import #import "SCSettings.h" +#import "SCDurationSlider.h" // The main controller for the SelfControl app, which includes several methods // to handle command flow and acts as delegate for the initial window. @interface AppController : NSObject { - IBOutlet NSSlider* blockDurationSlider_; + IBOutlet SCDurationSlider* blockDurationSlider_; IBOutlet NSTextField* blockSliderTimeDisplayLabel_; IBOutlet NSTextField* blocklistTeaserLabel_; IBOutlet NSButton* submitButton_; diff --git a/AppController.m b/AppController.m index afa4cbd1..50d45365 100755 --- a/AppController.m +++ b/AppController.m @@ -68,16 +68,13 @@ - (IBAction)updateTimeSliderDisplay:(id)sender { // if the duration is larger than we can display on our slider // chop it down to our max display value so the user doesn't // accidentally start a much longer block than intended - if (numMinutes > blockDurationSlider_.maxValue) { - [self setDefaultsBlockDurationOnMainThread: @(floor(blockDurationSlider_.maxValue))]; + if (numMinutes > blockDurationSlider_.maxDuration) { + [self setDefaultsBlockDurationOnMainThread: @(floor(blockDurationSlider_.maxDuration))]; numMinutes = [defaults_ integerForKey: @"BlockDuration"]; } - // Time-display code cleaned up thanks to the contributions of many users + blockSliderTimeDisplayLabel_.stringValue = blockDurationSlider_.durationDescription; - NSString* timeString = [SCUIUtilities timeSliderDisplayStringFromNumberOfMinutes:numMinutes]; - - [blockSliderTimeDisplayLabel_ setStringValue:timeString]; [submitButton_ setEnabled: (numMinutes > 0) && ([[defaults_ arrayForKey: @"Blocklist"] count] > 0)]; } @@ -353,32 +350,10 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { blockIsOn = ![SCUIUtilities blockIsRunning]; // Change block duration slider for hidden user defaults settings - long numTickMarks = ([defaults_ integerForKey: @"MaxBlockLength"] / [defaults_ integerForKey: @"BlockLengthInterval"]) + 1; - [blockDurationSlider_ setMaxValue: [defaults_ integerForKey: @"MaxBlockLength"]]; - [blockDurationSlider_ setNumberOfTickMarks: numTickMarks]; - - [NSValueTransformer registerValueTransformerWithName: @"BlockDurationSliderTransformer" - transformedValueClass: [NSNumber class] - returningTransformedValueWithBlock:^id _Nonnull(id _Nonnull value) { - // if it's not a number or convertable to one, IDK man - if (![value respondsToSelector: @selector(intValue)]) return @0; - - // instead of having 0 as the first option (who would ever want to start a 0-minute block?) - // we make it 1 minute, which is super handy for testing blocklists - // (of course, if the next tick mark is 1 minute anyway, we can skip that) - if ([value intValue] == 0 && [self->defaults_ integerForKey: @"BlockLengthInterval"] != 1) { - return @1; - } - - return value; - }]; - [blockDurationSlider_ bind: @"value" - toObject: [NSUserDefaultsController sharedUserDefaultsController] - withKeyPath: @"values.BlockDuration" - options: @{ - NSContinuouslyUpdatesValueBindingOption: @YES, - NSValueTransformerNameBindingOption: @"BlockDurationSliderTransformer" - }]; + blockDurationSlider_.maxDuration = [defaults_ integerForKey: @"MaxBlockLength"]; + blockDurationSlider_.durationTickInterval = [defaults_ integerForKey: @"BlockLengthInterval"]; + [blockDurationSlider_ bindDurationToObject: [NSUserDefaultsController sharedUserDefaultsController] + keyPath: @"values.BlockDuration"]; blocklistTeaserLabel_.stringValue = [SCUIUtilities blockTeaserStringWithMaxLength: 60]; diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 5cac760f..ca070870 100755 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -239,7 +239,7 @@ - + diff --git a/SCDurationSlider.h b/SCDurationSlider.h new file mode 100644 index 00000000..cefef204 --- /dev/null +++ b/SCDurationSlider.h @@ -0,0 +1,25 @@ +// +// SCTimeSlider.h +// SelfControl +// +// Created by Charlie Stigler on 4/17/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SCDurationSlider : NSSlider + +@property (nonatomic, assign) NSInteger maxDuration; +@property (nonatomic, assign) NSInteger durationTickInterval; +@property (readonly) NSInteger durationValueMinutes; +@property (readonly) NSString* durationDescription; + +- (NSInteger)durationValueMinutes; +- (void)bindDurationToObject:(id)obj keyPath:(NSString*)keyPath; +- (NSString*)durationDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SCDurationSlider.m b/SCDurationSlider.m new file mode 100644 index 00000000..690facb1 --- /dev/null +++ b/SCDurationSlider.m @@ -0,0 +1,138 @@ +// +// SCTimeSlider.m +// SelfControl +// +// Created by Charlie Stigler on 4/17/21. +// + +#import "SCDurationSlider.h" +#import "SCTimeIntervalFormatter.h" +#import + +#define kValueTransformerName @"BlockDurationSliderTransformer" + +@implementation SCDurationSlider + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + if (self = [super initWithCoder: coder]) { + [self initializeDurationProperties]; + } + return self; +} +- (instancetype)init { + if (self = [super init]) { + [self initializeDurationProperties]; + } + return self; +} + +- (void)initializeDurationProperties { + // default: 1 day max with ticks every 15 minutes + _maxDuration = 1440; + _durationTickInterval = 15; + + // register an NSValueTransformer + [self registerMinutesValueTransformer]; +} + +- (void)setMaxDuration:(NSInteger)maxDuration { + _maxDuration = maxDuration; + [self recalculateSliderIntervals]; +} +- (void)setDurationTickInterval:(NSInteger)durationTickInterval { + _durationTickInterval = durationTickInterval; + [self recalculateSliderIntervals]; +} + +- (void)recalculateSliderIntervals { + // no dividing by 0 for us! + if (self.durationTickInterval == 0) { + _durationTickInterval = 1; + } + + long numTickMarks = (self.maxDuration / self.durationTickInterval) + 1; + [self setMaxValue: self.maxDuration]; + [self setNumberOfTickMarks: numTickMarks]; +} + +- (void)registerMinutesValueTransformer { + [NSValueTransformer registerValueTransformerWithName: kValueTransformerName + transformedValueClass: [NSNumber class] + returningTransformedValueWithBlock:^id _Nonnull(id _Nonnull value) { + // if it's not a number or convertable to one, IDK man + if (![value respondsToSelector: @selector(intValue)]) return @0; + + NSInteger minutesValue= [self sliderValueToMinutes: [value intValue]]; + + return @(minutesValue); + }]; +} + +- (NSInteger)sliderValueToMinutes:(NSInteger)value { + // instead of having 0 as the first option (who would ever want to start a 0-minute block?) + // we make it 1 minute, which is super handy for testing blocklists + // (of course, if the next tick mark is 1 minute anyway, we can skip that) + if (value == 0 && self.durationTickInterval != 1) { + return 1; + } + + return value; +} +- (NSInteger)durationValueMinutes { + return [self sliderValueToMinutes: self.intValue]; +} + +- (void)bindDurationToObject:(id)obj keyPath:(NSString*)keyPath { + [self bind: @"value" + toObject: obj + withKeyPath: keyPath + options: @{ + NSContinuouslyUpdatesValueBindingOption: @YES, + NSValueTransformerNameBindingOption: kValueTransformerName + }]; +} + +- (NSString*)durationDescription { + return [SCDurationSlider timeSliderDisplayStringFromNumberOfMinutes: self.durationValueMinutes]; +} + +// String conversion utility methods + ++ (NSString *)timeSliderDisplayStringFromTimeInterval:(NSTimeInterval)numberOfSeconds { + static SCTimeIntervalFormatter* formatter = nil; + if (formatter == nil) { + formatter = [[SCTimeIntervalFormatter alloc] init]; + } + + NSString* formatted = [formatter stringForObjectValue:@(numberOfSeconds)]; + return formatted; +} + ++ (NSString *)timeSliderDisplayStringFromNumberOfMinutes:(NSInteger)numberOfMinutes { + if (numberOfMinutes < 0) return @"Invalid duration"; + + static NSCalendar* gregorian = nil; + if (gregorian == nil) { + gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + } + + NSRange secondsRangePerMinute = [gregorian + rangeOfUnit:NSCalendarUnitSecond + inUnit:NSCalendarUnitMinute + forDate:[NSDate date]]; + NSInteger numberOfSecondsPerMinute = (NSInteger)NSMaxRange(secondsRangePerMinute); + + NSTimeInterval numberOfSecondsSelected = (NSTimeInterval)(numberOfSecondsPerMinute * numberOfMinutes); + + NSString* displayString = [SCDurationSlider timeSliderDisplayStringFromTimeInterval:numberOfSecondsSelected]; + return displayString; +} + + +@end diff --git a/SCUIUtilities.h b/SCUIUtilities.h index ab0ad2ae..428dc3b8 100644 --- a/SCUIUtilities.h +++ b/SCUIUtilities.h @@ -22,9 +22,6 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)promptBrowserRestartIfNecessary; -+ (NSString *)timeSliderDisplayStringFromTimeInterval:(NSTimeInterval)numberOfSeconds; -+ (NSString *)timeSliderDisplayStringFromNumberOfMinutes:(NSInteger)numberOfMinutes; - + (NSString*)blockTeaserStringWithMaxLength:(NSInteger)maxStringLen; // presents an error via a popup in the app diff --git a/SCUIUtilities.m b/SCUIUtilities.m index 156b634f..26141ebc 100644 --- a/SCUIUtilities.m +++ b/SCUIUtilities.m @@ -7,7 +7,6 @@ #import "SCUIUtilities.h" #import -#import "SCTimeIntervalFormatter.h" @implementation SCUIUtilities @@ -87,36 +86,6 @@ + (NSString*)blockTeaserStringWithMaxLength:(NSInteger)maxStringLen { return [startStr stringByAppendingString: siteStr]; } -+ (NSString *)timeSliderDisplayStringFromTimeInterval:(NSTimeInterval)numberOfSeconds { - static SCTimeIntervalFormatter* formatter = nil; - if (formatter == nil) { - formatter = [[SCTimeIntervalFormatter alloc] init]; - } - - NSString* formatted = [formatter stringForObjectValue:@(numberOfSeconds)]; - return formatted; -} - -+ (NSString *)timeSliderDisplayStringFromNumberOfMinutes:(NSInteger)numberOfMinutes { - if (numberOfMinutes < 0) return @"Invalid duration"; - - static NSCalendar* gregorian = nil; - if (gregorian == nil) { - gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - } - - NSRange secondsRangePerMinute = [gregorian - rangeOfUnit:NSCalendarUnitSecond - inUnit:NSCalendarUnitMinute - forDate:[NSDate date]]; - NSInteger numberOfSecondsPerMinute = (NSInteger)NSMaxRange(secondsRangePerMinute); - - NSTimeInterval numberOfSecondsSelected = (NSTimeInterval)(numberOfSecondsPerMinute * numberOfMinutes); - - NSString* displayString = [SCUIUtilities timeSliderDisplayStringFromTimeInterval:numberOfSecondsSelected]; - return displayString; -} - + (BOOL)networkConnectionIsAvailable { SCNetworkReachabilityFlags flags; diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 7bf8a0d1..ff696fe8 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -125,6 +125,7 @@ CB9365620F8581B000EF284E /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CB9365610F8581B000EF284E /* dsa_pub.pem */; }; CB9366E80F85BEF100EF284E /* NSRemoveTemplate.jpg in Resources */ = {isa = PBXBuildFile; fileRef = CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */; }; CB9366E90F85BEF100EF284E /* NSAddTemplate.jpg in Resources */ = {isa = PBXBuildFile; fileRef = CB9366E70F85BEF100EF284E /* NSAddTemplate.jpg */; }; + CB953114262BC64F000C8309 /* SCDurationSlider.m in Sources */ = {isa = PBXBuildFile; fileRef = CB953113262BC64F000C8309 /* SCDurationSlider.m */; }; CB9C80FC19CFB79700CDCAE1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9C80FB19CFB79700CDCAE1 /* main.m */; }; CB9C80FF19CFB79700CDCAE1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9C80FE19CFB79700CDCAE1 /* AppDelegate.m */; }; CB9C810419CFB79700CDCAE1 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CB9C810219CFB79700CDCAE1 /* MainMenu.xib */; }; @@ -606,6 +607,8 @@ CB9365610F8581B000EF284E /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NSRemoveTemplate.jpg; sourceTree = ""; }; CB9366E70F85BEF100EF284E /* NSAddTemplate.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = NSAddTemplate.jpg; sourceTree = ""; }; + CB953112262BC64F000C8309 /* SCDurationSlider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDurationSlider.h; sourceTree = ""; }; + CB953113262BC64F000C8309 /* SCDurationSlider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDurationSlider.m; sourceTree = ""; }; CB9C80F119CFAAC600CDCAE1 /* ko */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; CB9C80F219CFAACC00CDCAE1 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; CB9C80F719CFB79700CDCAE1 /* SelfControl Killer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SelfControl Killer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1679,6 +1682,8 @@ CBE5C40A0F4D4531003DB900 /* ButtonWithPopupMenu.m */, CB529BBD0F32B7ED00564FB8 /* AppController.h */, CB529BBE0F32B7ED00564FB8 /* AppController.m */, + CB953112262BC64F000C8309 /* SCDurationSlider.h */, + CB953113262BC64F000C8309 /* SCDurationSlider.m */, CBD4848719D764440020F949 /* PreferencesGeneralViewController.h */, CBD4848819D764440020F949 /* PreferencesGeneralViewController.m */, CBD4848C19D768C90020F949 /* PreferencesAdvancedViewController.h */, @@ -3650,6 +3655,7 @@ CBD4848F19D768C90020F949 /* PreferencesAdvancedViewController.m in Sources */, CB81A9D025B7C269006956F7 /* SCBlockUtilities.m in Sources */, CBB7DEEA0F53313F00ABF3EA /* DomainListWindowController.m in Sources */, + CB953114262BC64F000C8309 /* SCDurationSlider.m in Sources */, CBF3B574217BADD7006D5F52 /* SCSettings.m in Sources */, CB25806616C237F10059C99A /* NSString+IPAddress.m in Sources */, ); From 9b6b2b008c989dbcb14381f48eac93c5caa4ceab Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 18 Apr 2021 19:27:27 -0700 Subject: [PATCH 07/58] Switched extend block dialog to slider (instead of text inputs/steppers) --- Base.lproj/TimerWindow.xib | 119 ++++++++++------------------------ Daemon/SCDaemonBlockMethods.m | 1 - SCDurationSlider.m | 3 +- TimerWindowController.h | 9 ++- TimerWindowController.m | 16 ++++- 5 files changed, 58 insertions(+), 90 deletions(-) diff --git a/Base.lproj/TimerWindow.xib b/Base.lproj/TimerWindow.xib index 7050bab2..62049ce8 100755 --- a/Base.lproj/TimerWindow.xib +++ b/Base.lproj/TimerWindow.xib @@ -108,9 +108,9 @@ - - + + @@ -121,8 +121,8 @@ - - + + @@ -185,81 +185,20 @@ DQ - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + - + - - + + - + - + - + @@ -232,9 +232,9 @@ DQ - - - + + + @@ -242,6 +242,7 @@ DQ + @@ -257,7 +258,7 @@ DQ - + + + + + diff --git a/Base.lproj/TimerWindow.xib b/Base.lproj/TimerWindow.xib index 52a3e12b..21109c2b 100755 --- a/Base.lproj/TimerWindow.xib +++ b/Base.lproj/TimerWindow.xib @@ -13,28 +13,26 @@ - - + + - + - + - - - - - - - + + + - + - + + + - - - - + + + + + + + + - - - + - - - + + - + + @@ -116,25 +126,25 @@ - + - - + + - - + + - + @@ -142,8 +152,8 @@ - + @@ -232,7 +245,7 @@ DQ - + @@ -242,11 +255,13 @@ DQ - + + + @@ -260,486 +275,8 @@ DQ - - - -YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T -S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T -U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN -EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf -EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxESbE1N -ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG -AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW -AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT -AAMAAAACAAEAAYdzAAcAABGcAAAA0AAAAAAAABGcYXBwbAIAAABtbnRyR1JBWVhZWiAH3AAIABcADwAu -AA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAHlkc2NtAAABPAAA -CBpjcHJ0AAAJWAAAACN3dHB0AAAJfAAAABRrVFJDAAAJkAAACAxkZXNjAAAAAAAAAB9HZW5lcmljIEdy -YXkgR2FtbWEgMi4yIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNr -U0sAAAAuAAABhGRhREsAAAA6AAABsmNhRVMAAAA4AAAB7HZpVk4AAABAAAACJHB0QlIAAABKAAACZHVr -VUEAAAAsAAACrmZyRlUAAAA+AAAC2mh1SFUAAAA0AAADGHpoVFcAAAAaAAADTGtvS1IAAAAiAAADZm5i -Tk8AAAA6AAADiGNzQ1oAAAAoAAADwmhlSUwAAAAkAAAD6nJvUk8AAAAqAAAEDmRlREUAAABOAAAEOGl0 -SVQAAABOAAAEhnN2U0UAAAA4AAAE1HpoQ04AAAAaAAAFDGphSlAAAAAmAAAFJmVsR1IAAAAqAAAFTHB0 -UE8AAABSAAAFdm5sTkwAAABAAAAFyGVzRVMAAABMAAAGCHRoVEgAAAAyAAAGVHRyVFIAAAAkAAAGhmZp -RkkAAABGAAAGqmhySFIAAAA+AAAG8HBsUEwAAABKAAAHLmFyRUcAAAAsAAAHeHJ1UlUAAAA6AAAHpGVu -VVMAAAA8AAAH3gBWAWEAZQBvAGIAZQBjAG4A4QAgAHMAaQB2AOEAIABnAGEAbQBhACAAMgAsADIARwBl -AG4AZQByAGkAcwBrACAAZwByAOUAIAAyACwAMgAgAGcAYQBtAG0AYQAtAHAAcgBvAGYAaQBsAEcAYQBt -AG0AYQAgAGQAZQAgAGcAcgBpAHMAbwBzACAAZwBlAG4A6AByAGkAYwBhACAAMgAuADIAQx6lAHUAIABo -AOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4AZwAgAEcAYQBtAG0AYQAgADIALgAyAFAAZQBy -AGYAaQBsACAARwBlAG4A6QByAGkAYwBvACAAZABhACAARwBhAG0AYQAgAGQAZQAgAEMAaQBuAHoAYQBz -ACAAMgAsADIEFwQwBDMEMAQ7BEwEPQQwACAARwByAGEAeQAtBDMEMAQ8BDAAIAAyAC4AMgBQAHIAbwBm -AGkAbAAgAGcA6QBuAOkAcgBpAHEAdQBlACAAZwByAGkAcwAgAGcAYQBtAG0AYQAgADIALAAyAMEAbAB0 -AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABnAGEAbQBtAGEAIAAyAC4AMpAadShwcJaOUUlepgAy -AC4AMoJyX2ljz4/wx3y8GAAg1ozAyQAgrBC5yAAgADIALgAyACDVBLhc0wzHfABHAGUAbgBlAHIAaQBz -AGsAIABnAHIA5QAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBpAGwATwBiAGUAYwBuAOEAIAFh -AGUAZADhACAAZwBhAG0AYQAgADIALgAyBdIF0AXeBdQAIAXQBeQF1QXoACAF2wXcBdwF2QAgADIALgAy -AEcAYQBtAGEAIABnAHIAaQAgAGcAZQBuAGUAcgBpAGMBAwAgADIALAAyAEEAbABsAGcAZQBtAGUAaQBu -AGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBmAGkAbAAgAEcAYQBtAG0AYQAgADIALAAy -AFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwAgAGQAZQBsAGwAYQAg -AGcAYQBtAG0AYQAgADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBt -AGEAcAByAG8AZgBpAGxmbpAacHBepnz7ZXAAMgAuADJjz4/wZYdO9k4AgiwwsDDsMKQwrDDzMN4AIAAy -AC4AMgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPMACADkwO6A8EDuQAgA5MDrAO8A7wDsQAgADIALgAy -AFAAZQByAGYAaQBsACAAZwBlAG4A6QByAGkAYwBvACAAZABlACAAYwBpAG4AegBlAG4AdABvAHMAIABk -AGEAIABHAGEAbQBtAGEAIAAyACwAMgBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAIABnAGEAbQBt -AGEAIAAyACwAMgAtAHAAcgBvAGYAaQBlAGwAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABk -AGUAIABnAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAGUAcwAgADIALAAyDiMOMQ4HDioONQ5BDgEOIQ4h -DjIOQA4BDiMOIg5MDhcOMQ5IDicORA4bACAAMgAuADIARwBlAG4AZQBsACAARwByAGkAIABHAGEAbQBh -ACAAMgAsADIAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAG4AIABnAGEAbQBtAGEAIAAyACwAMgAg -AC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0AawBpACAARwByAGEAeQAgAEcAYQBtAG0AYQAg -ADIALgAyACAAcAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABz -AHoAYQByAG8BWwBjAGkAIABnAGEAbQBtAGEAIAAyACwAMgY6BicGRQYnACAAMgAuADIAIAZEBkgGRgAg -BjEGRQYnBi8GSgAgBjkGJwZFBB4EMQRJBDAETwAgBEEENQRABDAETwAgBDMEMAQ8BDwEMAAgADIALAAy -AC0EPwRABD4ERAQ4BDsETABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAEcAYQBtAG0AYQAgADIALgAy -ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUgSW5jLiwgMjAxMgAAWFlaIAAA -AAAAAPNRAAEAAAABFsxjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBP -AFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADl -AOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGp -AbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2 -AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQT -BCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXF -BdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfS -B+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9 -ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0N -DSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBD -EGEQfhCbELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPl -FAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3 -GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7 -HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1 -IaEhziH7IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtybo -JxgnSSd6J6sn3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizX -LQwtQS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNG -M38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2 -OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGs -Qe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mp -SfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIx -UnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtF -W5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTp -ZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8e -b3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnn -ekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VH -hauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/ -kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3S -nkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sC -q3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjR -uUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dB -x7/IPci8yTrJuco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV -1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN -5pbnH+ep6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt -9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9IrLC0uWiRjbGFzc25hbWVYJGNsYXNzZXNf -EBBOU0JpdG1hcEltYWdlUmVwoy0vMFpOU0ltYWdlUmVwWE5TT2JqZWN00issMjNXTlNBcnJheaIyMNIr -LDU2Xk5TTXV0YWJsZUFycmF5ozUyMNM4OQ86OzxXTlNXaGl0ZVxOU0NvbG9yU3BhY2VEMCAwABADgAzS -Kyw+P1dOU0NvbG9yoj4w0issQUJXTlNJbWFnZaJBMAAIABEAGgAkACkAMgA3AEkATABRAFMAYgBoAHUA -fACLAJIAnwCmAK4AsACyALQAuQC7AL0AxADJANQA1gDYANoA3wDiAOQA5gDoAO8BBgEiASQBJhOWE5sT -phOvE8ITxhPRE9oT3xPnE+oT7xP+FAIUCRQRFB4UIxQlFCcULBQ0FDcUPBREAAAAAAAAAgEAAAAAAAAA -QwAAAAAAAAAAAAAAAAAAFEc - - - - -YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T -S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T -U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN -EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf -EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxESbE1N -ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG -AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW -AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT -AAMAAAACAAEAAYdzAAcAABGcAAAA0AAAAAAAABGcYXBwbAIAAABtbnRyR1JBWVhZWiAH3AAIABcADwAu -AA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAHlkc2NtAAABPAAA -CBpjcHJ0AAAJWAAAACN3dHB0AAAJfAAAABRrVFJDAAAJkAAACAxkZXNjAAAAAAAAAB9HZW5lcmljIEdy -YXkgR2FtbWEgMi4yIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNr -U0sAAAAuAAABhGRhREsAAAA6AAABsmNhRVMAAAA4AAAB7HZpVk4AAABAAAACJHB0QlIAAABKAAACZHVr -VUEAAAAsAAACrmZyRlUAAAA+AAAC2mh1SFUAAAA0AAADGHpoVFcAAAAaAAADTGtvS1IAAAAiAAADZm5i -Tk8AAAA6AAADiGNzQ1oAAAAoAAADwmhlSUwAAAAkAAAD6nJvUk8AAAAqAAAEDmRlREUAAABOAAAEOGl0 -SVQAAABOAAAEhnN2U0UAAAA4AAAE1HpoQ04AAAAaAAAFDGphSlAAAAAmAAAFJmVsR1IAAAAqAAAFTHB0 -UE8AAABSAAAFdm5sTkwAAABAAAAFyGVzRVMAAABMAAAGCHRoVEgAAAAyAAAGVHRyVFIAAAAkAAAGhmZp -RkkAAABGAAAGqmhySFIAAAA+AAAG8HBsUEwAAABKAAAHLmFyRUcAAAAsAAAHeHJ1UlUAAAA6AAAHpGVu -VVMAAAA8AAAH3gBWAWEAZQBvAGIAZQBjAG4A4QAgAHMAaQB2AOEAIABnAGEAbQBhACAAMgAsADIARwBl -AG4AZQByAGkAcwBrACAAZwByAOUAIAAyACwAMgAgAGcAYQBtAG0AYQAtAHAAcgBvAGYAaQBsAEcAYQBt -AG0AYQAgAGQAZQAgAGcAcgBpAHMAbwBzACAAZwBlAG4A6AByAGkAYwBhACAAMgAuADIAQx6lAHUAIABo -AOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4AZwAgAEcAYQBtAG0AYQAgADIALgAyAFAAZQBy -AGYAaQBsACAARwBlAG4A6QByAGkAYwBvACAAZABhACAARwBhAG0AYQAgAGQAZQAgAEMAaQBuAHoAYQBz -ACAAMgAsADIEFwQwBDMEMAQ7BEwEPQQwACAARwByAGEAeQAtBDMEMAQ8BDAAIAAyAC4AMgBQAHIAbwBm -AGkAbAAgAGcA6QBuAOkAcgBpAHEAdQBlACAAZwByAGkAcwAgAGcAYQBtAG0AYQAgADIALAAyAMEAbAB0 -AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABnAGEAbQBtAGEAIAAyAC4AMpAadShwcJaOUUlepgAy -AC4AMoJyX2ljz4/wx3y8GAAg1ozAyQAgrBC5yAAgADIALgAyACDVBLhc0wzHfABHAGUAbgBlAHIAaQBz -AGsAIABnAHIA5QAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBpAGwATwBiAGUAYwBuAOEAIAFh -AGUAZADhACAAZwBhAG0AYQAgADIALgAyBdIF0AXeBdQAIAXQBeQF1QXoACAF2wXcBdwF2QAgADIALgAy -AEcAYQBtAGEAIABnAHIAaQAgAGcAZQBuAGUAcgBpAGMBAwAgADIALAAyAEEAbABsAGcAZQBtAGUAaQBu -AGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBmAGkAbAAgAEcAYQBtAG0AYQAgADIALAAy -AFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwAgAGQAZQBsAGwAYQAg -AGcAYQBtAG0AYQAgADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBt -AGEAcAByAG8AZgBpAGxmbpAacHBepnz7ZXAAMgAuADJjz4/wZYdO9k4AgiwwsDDsMKQwrDDzMN4AIAAy -AC4AMgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPMACADkwO6A8EDuQAgA5MDrAO8A7wDsQAgADIALgAy -AFAAZQByAGYAaQBsACAAZwBlAG4A6QByAGkAYwBvACAAZABlACAAYwBpAG4AegBlAG4AdABvAHMAIABk -AGEAIABHAGEAbQBtAGEAIAAyACwAMgBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAIABnAGEAbQBt -AGEAIAAyACwAMgAtAHAAcgBvAGYAaQBlAGwAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABk -AGUAIABnAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAGUAcwAgADIALAAyDiMOMQ4HDioONQ5BDgEOIQ4h -DjIOQA4BDiMOIg5MDhcOMQ5IDicORA4bACAAMgAuADIARwBlAG4AZQBsACAARwByAGkAIABHAGEAbQBh -ACAAMgAsADIAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAG4AIABnAGEAbQBtAGEAIAAyACwAMgAg -AC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0AawBpACAARwByAGEAeQAgAEcAYQBtAG0AYQAg -ADIALgAyACAAcAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABz -AHoAYQByAG8BWwBjAGkAIABnAGEAbQBtAGEAIAAyACwAMgY6BicGRQYnACAAMgAuADIAIAZEBkgGRgAg -BjEGRQYnBi8GSgAgBjkGJwZFBB4EMQRJBDAETwAgBEEENQRABDAETwAgBDMEMAQ8BDwEMAAgADIALAAy -AC0EPwRABD4ERAQ4BDsETABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAEcAYQBtAG0AYQAgADIALgAy -ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUgSW5jLiwgMjAxMgAAWFlaIAAA -AAAAAPNRAAEAAAABFsxjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBP -AFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADl -AOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGp -AbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2 -AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQT -BCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXF -BdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfS -B+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9 -ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0N -DSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBD -EGEQfhCbELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPl -FAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3 -GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7 -HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1 -IaEhziH7IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtybo -JxgnSSd6J6sn3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizX -LQwtQS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNG -M38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2 -OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGs -Qe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mp -SfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIx -UnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtF -W5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTp -ZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8e -b3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnn -ekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VH -hauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/ -kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3S -nkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sC -q3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjR -uUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dB -x7/IPci8yTrJuco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV -1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN -5pbnH+ep6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt -9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9IrLC0uWiRjbGFzc25hbWVYJGNsYXNzZXNf -EBBOU0JpdG1hcEltYWdlUmVwoy0vMFpOU0ltYWdlUmVwWE5TT2JqZWN00issMjNXTlNBcnJheaIyMNIr -LDU2Xk5TTXV0YWJsZUFycmF5ozUyMNM4OQ86OzxXTlNXaGl0ZVxOU0NvbG9yU3BhY2VEMCAwABADgAzS -Kyw+P1dOU0NvbG9yoj4w0issQUJXTlNJbWFnZaJBMAAIABEAGgAkACkAMgA3AEkATABRAFMAYgBoAHUA -fACLAJIAnwCmAK4AsACyALQAuQC7AL0AxADJANQA1gDYANoA3wDiAOQA5gDoAO8BBgEiASQBJhOWE5sT -phOvE8ITxhPRE9oT3xPnE+oT7xP+FAIUCRQRFB4UIxQlFCcULBQ0FDcUPBREAAAAAAAAAgEAAAAAAAAA -QwAAAAAAAAAAAAAAAAAAFEc - - - - -YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T -S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T -U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN -EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf -EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxESbE1N -ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG -AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW -AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT -AAMAAAACAAEAAYdzAAcAABGcAAAA0AAAAAAAABGcYXBwbAIAAABtbnRyR1JBWVhZWiAH3AAIABcADwAu -AA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAHlkc2NtAAABPAAA -CBpjcHJ0AAAJWAAAACN3dHB0AAAJfAAAABRrVFJDAAAJkAAACAxkZXNjAAAAAAAAAB9HZW5lcmljIEdy -YXkgR2FtbWEgMi4yIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNr -U0sAAAAuAAABhGRhREsAAAA6AAABsmNhRVMAAAA4AAAB7HZpVk4AAABAAAACJHB0QlIAAABKAAACZHVr -VUEAAAAsAAACrmZyRlUAAAA+AAAC2mh1SFUAAAA0AAADGHpoVFcAAAAaAAADTGtvS1IAAAAiAAADZm5i -Tk8AAAA6AAADiGNzQ1oAAAAoAAADwmhlSUwAAAAkAAAD6nJvUk8AAAAqAAAEDmRlREUAAABOAAAEOGl0 -SVQAAABOAAAEhnN2U0UAAAA4AAAE1HpoQ04AAAAaAAAFDGphSlAAAAAmAAAFJmVsR1IAAAAqAAAFTHB0 -UE8AAABSAAAFdm5sTkwAAABAAAAFyGVzRVMAAABMAAAGCHRoVEgAAAAyAAAGVHRyVFIAAAAkAAAGhmZp -RkkAAABGAAAGqmhySFIAAAA+AAAG8HBsUEwAAABKAAAHLmFyRUcAAAAsAAAHeHJ1UlUAAAA6AAAHpGVu -VVMAAAA8AAAH3gBWAWEAZQBvAGIAZQBjAG4A4QAgAHMAaQB2AOEAIABnAGEAbQBhACAAMgAsADIARwBl -AG4AZQByAGkAcwBrACAAZwByAOUAIAAyACwAMgAgAGcAYQBtAG0AYQAtAHAAcgBvAGYAaQBsAEcAYQBt -AG0AYQAgAGQAZQAgAGcAcgBpAHMAbwBzACAAZwBlAG4A6AByAGkAYwBhACAAMgAuADIAQx6lAHUAIABo -AOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4AZwAgAEcAYQBtAG0AYQAgADIALgAyAFAAZQBy -AGYAaQBsACAARwBlAG4A6QByAGkAYwBvACAAZABhACAARwBhAG0AYQAgAGQAZQAgAEMAaQBuAHoAYQBz -ACAAMgAsADIEFwQwBDMEMAQ7BEwEPQQwACAARwByAGEAeQAtBDMEMAQ8BDAAIAAyAC4AMgBQAHIAbwBm -AGkAbAAgAGcA6QBuAOkAcgBpAHEAdQBlACAAZwByAGkAcwAgAGcAYQBtAG0AYQAgADIALAAyAMEAbAB0 -AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABnAGEAbQBtAGEAIAAyAC4AMpAadShwcJaOUUlepgAy -AC4AMoJyX2ljz4/wx3y8GAAg1ozAyQAgrBC5yAAgADIALgAyACDVBLhc0wzHfABHAGUAbgBlAHIAaQBz -AGsAIABnAHIA5QAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBpAGwATwBiAGUAYwBuAOEAIAFh -AGUAZADhACAAZwBhAG0AYQAgADIALgAyBdIF0AXeBdQAIAXQBeQF1QXoACAF2wXcBdwF2QAgADIALgAy -AEcAYQBtAGEAIABnAHIAaQAgAGcAZQBuAGUAcgBpAGMBAwAgADIALAAyAEEAbABsAGcAZQBtAGUAaQBu -AGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBmAGkAbAAgAEcAYQBtAG0AYQAgADIALAAy -AFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwAgAGQAZQBsAGwAYQAg -AGcAYQBtAG0AYQAgADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBt -AGEAcAByAG8AZgBpAGxmbpAacHBepnz7ZXAAMgAuADJjz4/wZYdO9k4AgiwwsDDsMKQwrDDzMN4AIAAy -AC4AMgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPMACADkwO6A8EDuQAgA5MDrAO8A7wDsQAgADIALgAy -AFAAZQByAGYAaQBsACAAZwBlAG4A6QByAGkAYwBvACAAZABlACAAYwBpAG4AegBlAG4AdABvAHMAIABk -AGEAIABHAGEAbQBtAGEAIAAyACwAMgBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAIABnAGEAbQBt -AGEAIAAyACwAMgAtAHAAcgBvAGYAaQBlAGwAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABk -AGUAIABnAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAGUAcwAgADIALAAyDiMOMQ4HDioONQ5BDgEOIQ4h -DjIOQA4BDiMOIg5MDhcOMQ5IDicORA4bACAAMgAuADIARwBlAG4AZQBsACAARwByAGkAIABHAGEAbQBh -ACAAMgAsADIAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAG4AIABnAGEAbQBtAGEAIAAyACwAMgAg -AC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0AawBpACAARwByAGEAeQAgAEcAYQBtAG0AYQAg -ADIALgAyACAAcAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABz -AHoAYQByAG8BWwBjAGkAIABnAGEAbQBtAGEAIAAyACwAMgY6BicGRQYnACAAMgAuADIAIAZEBkgGRgAg -BjEGRQYnBi8GSgAgBjkGJwZFBB4EMQRJBDAETwAgBEEENQRABDAETwAgBDMEMAQ8BDwEMAAgADIALAAy -AC0EPwRABD4ERAQ4BDsETABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAEcAYQBtAG0AYQAgADIALgAy -ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUgSW5jLiwgMjAxMgAAWFlaIAAA -AAAAAPNRAAEAAAABFsxjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBP -AFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADl -AOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGp -AbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2 -AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQT -BCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXF -BdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfS -B+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9 -ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0N -DSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBD -EGEQfhCbELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPl -FAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3 -GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7 -HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1 -IaEhziH7IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtybo -JxgnSSd6J6sn3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizX -LQwtQS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNG -M38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2 -OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGs -Qe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mp -SfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIx -UnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtF -W5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTp -ZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8e -b3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnn -ekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VH -hauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/ -kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3S -nkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sC -q3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjR -uUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dB -x7/IPci8yTrJuco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV -1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN -5pbnH+ep6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt -9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9IrLC0uWiRjbGFzc25hbWVYJGNsYXNzZXNf -EBBOU0JpdG1hcEltYWdlUmVwoy0vMFpOU0ltYWdlUmVwWE5TT2JqZWN00issMjNXTlNBcnJheaIyMNIr -LDU2Xk5TTXV0YWJsZUFycmF5ozUyMNM4OQ86OzxXTlNXaGl0ZVxOU0NvbG9yU3BhY2VEMCAwABADgAzS -Kyw+P1dOU0NvbG9yoj4w0issQUJXTlNJbWFnZaJBMAAIABEAGgAkACkAMgA3AEkATABRAFMAYgBoAHUA -fACLAJIAnwCmAK4AsACyALQAuQC7AL0AxADJANQA1gDYANoA3wDiAOQA5gDoAO8BBgEiASQBJhOWE5sT -phOvE8ITxhPRE9oT3xPnE+oT7xP+FAIUCRQRFB4UIxQlFCcULBQ0FDcUPBREAAAAAAAAAgEAAAAAAAAA -QwAAAAAAAAAAAAAAAAAAFEc - - - - -YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T -S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T -U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN -EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf -EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxESbE1N -ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG -AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW -AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT -AAMAAAACAAEAAYdzAAcAABGcAAAA0AAAAAAAABGcYXBwbAIAAABtbnRyR1JBWVhZWiAH3AAIABcADwAu -AA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAHlkc2NtAAABPAAA -CBpjcHJ0AAAJWAAAACN3dHB0AAAJfAAAABRrVFJDAAAJkAAACAxkZXNjAAAAAAAAAB9HZW5lcmljIEdy -YXkgR2FtbWEgMi4yIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNr -U0sAAAAuAAABhGRhREsAAAA6AAABsmNhRVMAAAA4AAAB7HZpVk4AAABAAAACJHB0QlIAAABKAAACZHVr -VUEAAAAsAAACrmZyRlUAAAA+AAAC2mh1SFUAAAA0AAADGHpoVFcAAAAaAAADTGtvS1IAAAAiAAADZm5i -Tk8AAAA6AAADiGNzQ1oAAAAoAAADwmhlSUwAAAAkAAAD6nJvUk8AAAAqAAAEDmRlREUAAABOAAAEOGl0 -SVQAAABOAAAEhnN2U0UAAAA4AAAE1HpoQ04AAAAaAAAFDGphSlAAAAAmAAAFJmVsR1IAAAAqAAAFTHB0 -UE8AAABSAAAFdm5sTkwAAABAAAAFyGVzRVMAAABMAAAGCHRoVEgAAAAyAAAGVHRyVFIAAAAkAAAGhmZp -RkkAAABGAAAGqmhySFIAAAA+AAAG8HBsUEwAAABKAAAHLmFyRUcAAAAsAAAHeHJ1UlUAAAA6AAAHpGVu -VVMAAAA8AAAH3gBWAWEAZQBvAGIAZQBjAG4A4QAgAHMAaQB2AOEAIABnAGEAbQBhACAAMgAsADIARwBl -AG4AZQByAGkAcwBrACAAZwByAOUAIAAyACwAMgAgAGcAYQBtAG0AYQAtAHAAcgBvAGYAaQBsAEcAYQBt -AG0AYQAgAGQAZQAgAGcAcgBpAHMAbwBzACAAZwBlAG4A6AByAGkAYwBhACAAMgAuADIAQx6lAHUAIABo -AOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4AZwAgAEcAYQBtAG0AYQAgADIALgAyAFAAZQBy -AGYAaQBsACAARwBlAG4A6QByAGkAYwBvACAAZABhACAARwBhAG0AYQAgAGQAZQAgAEMAaQBuAHoAYQBz -ACAAMgAsADIEFwQwBDMEMAQ7BEwEPQQwACAARwByAGEAeQAtBDMEMAQ8BDAAIAAyAC4AMgBQAHIAbwBm -AGkAbAAgAGcA6QBuAOkAcgBpAHEAdQBlACAAZwByAGkAcwAgAGcAYQBtAG0AYQAgADIALAAyAMEAbAB0 -AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABnAGEAbQBtAGEAIAAyAC4AMpAadShwcJaOUUlepgAy -AC4AMoJyX2ljz4/wx3y8GAAg1ozAyQAgrBC5yAAgADIALgAyACDVBLhc0wzHfABHAGUAbgBlAHIAaQBz -AGsAIABnAHIA5QAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBpAGwATwBiAGUAYwBuAOEAIAFh -AGUAZADhACAAZwBhAG0AYQAgADIALgAyBdIF0AXeBdQAIAXQBeQF1QXoACAF2wXcBdwF2QAgADIALgAy -AEcAYQBtAGEAIABnAHIAaQAgAGcAZQBuAGUAcgBpAGMBAwAgADIALAAyAEEAbABsAGcAZQBtAGUAaQBu -AGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBmAGkAbAAgAEcAYQBtAG0AYQAgADIALAAy -AFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwAgAGQAZQBsAGwAYQAg -AGcAYQBtAG0AYQAgADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBt -AGEAcAByAG8AZgBpAGxmbpAacHBepnz7ZXAAMgAuADJjz4/wZYdO9k4AgiwwsDDsMKQwrDDzMN4AIAAy -AC4AMgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPMACADkwO6A8EDuQAgA5MDrAO8A7wDsQAgADIALgAy -AFAAZQByAGYAaQBsACAAZwBlAG4A6QByAGkAYwBvACAAZABlACAAYwBpAG4AegBlAG4AdABvAHMAIABk -AGEAIABHAGEAbQBtAGEAIAAyACwAMgBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAIABnAGEAbQBt -AGEAIAAyACwAMgAtAHAAcgBvAGYAaQBlAGwAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABk -AGUAIABnAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAGUAcwAgADIALAAyDiMOMQ4HDioONQ5BDgEOIQ4h -DjIOQA4BDiMOIg5MDhcOMQ5IDicORA4bACAAMgAuADIARwBlAG4AZQBsACAARwByAGkAIABHAGEAbQBh -ACAAMgAsADIAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAG4AIABnAGEAbQBtAGEAIAAyACwAMgAg -AC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0AawBpACAARwByAGEAeQAgAEcAYQBtAG0AYQAg -ADIALgAyACAAcAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABz -AHoAYQByAG8BWwBjAGkAIABnAGEAbQBtAGEAIAAyACwAMgY6BicGRQYnACAAMgAuADIAIAZEBkgGRgAg -BjEGRQYnBi8GSgAgBjkGJwZFBB4EMQRJBDAETwAgBEEENQRABDAETwAgBDMEMAQ8BDwEMAAgADIALAAy -AC0EPwRABD4ERAQ4BDsETABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAEcAYQBtAG0AYQAgADIALgAy -ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUgSW5jLiwgMjAxMgAAWFlaIAAA -AAAAAPNRAAEAAAABFsxjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBP -AFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADl -AOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGp -AbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2 -AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQT -BCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXF -BdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfS -B+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9 -ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0N -DSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBD -EGEQfhCbELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPl -FAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3 -GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7 -HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1 -IaEhziH7IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtybo -JxgnSSd6J6sn3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizX -LQwtQS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNG -M38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2 -OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGs -Qe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mp -SfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIx -UnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtF -W5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTp -ZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8e -b3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnn -ekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VH -hauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/ -kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3S -nkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sC -q3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjR -uUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dB -x7/IPci8yTrJuco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV -1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN -5pbnH+ep6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt -9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9IrLC0uWiRjbGFzc25hbWVYJGNsYXNzZXNf -EBBOU0JpdG1hcEltYWdlUmVwoy0vMFpOU0ltYWdlUmVwWE5TT2JqZWN00issMjNXTlNBcnJheaIyMNIr -LDU2Xk5TTXV0YWJsZUFycmF5ozUyMNM4OQ86OzxXTlNXaGl0ZVxOU0NvbG9yU3BhY2VEMCAwABADgAzS -Kyw+P1dOU0NvbG9yoj4w0issQUJXTlNJbWFnZaJBMAAIABEAGgAkACkAMgA3AEkATABRAFMAYgBoAHUA -fACLAJIAnwCmAK4AsACyALQAuQC7AL0AxADJANQA1gDYANoA3wDiAOQA5gDoAO8BBgEiASQBJhOWE5sT -phOvE8ITxhPRE9oT3xPnE+oT7xP+FAIUCRQRFB4UIxQlFCcULBQ0FDcUPBREAAAAAAAAAgEAAAAAAAAA -QwAAAAAAAAAAAAAAAAAAFEc - - - - -YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T -S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T -U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN -EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf -EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxESbE1N -ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG -AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW -AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT -AAMAAAACAAEAAYdzAAcAABGcAAAA0AAAAAAAABGcYXBwbAIAAABtbnRyR1JBWVhZWiAH3AAIABcADwAu -AA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAHlkc2NtAAABPAAA -CBpjcHJ0AAAJWAAAACN3dHB0AAAJfAAAABRrVFJDAAAJkAAACAxkZXNjAAAAAAAAAB9HZW5lcmljIEdy -YXkgR2FtbWEgMi4yIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNr -U0sAAAAuAAABhGRhREsAAAA6AAABsmNhRVMAAAA4AAAB7HZpVk4AAABAAAACJHB0QlIAAABKAAACZHVr -VUEAAAAsAAACrmZyRlUAAAA+AAAC2mh1SFUAAAA0AAADGHpoVFcAAAAaAAADTGtvS1IAAAAiAAADZm5i -Tk8AAAA6AAADiGNzQ1oAAAAoAAADwmhlSUwAAAAkAAAD6nJvUk8AAAAqAAAEDmRlREUAAABOAAAEOGl0 -SVQAAABOAAAEhnN2U0UAAAA4AAAE1HpoQ04AAAAaAAAFDGphSlAAAAAmAAAFJmVsR1IAAAAqAAAFTHB0 -UE8AAABSAAAFdm5sTkwAAABAAAAFyGVzRVMAAABMAAAGCHRoVEgAAAAyAAAGVHRyVFIAAAAkAAAGhmZp -RkkAAABGAAAGqmhySFIAAAA+AAAG8HBsUEwAAABKAAAHLmFyRUcAAAAsAAAHeHJ1UlUAAAA6AAAHpGVu -VVMAAAA8AAAH3gBWAWEAZQBvAGIAZQBjAG4A4QAgAHMAaQB2AOEAIABnAGEAbQBhACAAMgAsADIARwBl -AG4AZQByAGkAcwBrACAAZwByAOUAIAAyACwAMgAgAGcAYQBtAG0AYQAtAHAAcgBvAGYAaQBsAEcAYQBt -AG0AYQAgAGQAZQAgAGcAcgBpAHMAbwBzACAAZwBlAG4A6AByAGkAYwBhACAAMgAuADIAQx6lAHUAIABo -AOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4AZwAgAEcAYQBtAG0AYQAgADIALgAyAFAAZQBy -AGYAaQBsACAARwBlAG4A6QByAGkAYwBvACAAZABhACAARwBhAG0AYQAgAGQAZQAgAEMAaQBuAHoAYQBz -ACAAMgAsADIEFwQwBDMEMAQ7BEwEPQQwACAARwByAGEAeQAtBDMEMAQ8BDAAIAAyAC4AMgBQAHIAbwBm -AGkAbAAgAGcA6QBuAOkAcgBpAHEAdQBlACAAZwByAGkAcwAgAGcAYQBtAG0AYQAgADIALAAyAMEAbAB0 -AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABnAGEAbQBtAGEAIAAyAC4AMpAadShwcJaOUUlepgAy -AC4AMoJyX2ljz4/wx3y8GAAg1ozAyQAgrBC5yAAgADIALgAyACDVBLhc0wzHfABHAGUAbgBlAHIAaQBz -AGsAIABnAHIA5QAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBpAGwATwBiAGUAYwBuAOEAIAFh -AGUAZADhACAAZwBhAG0AYQAgADIALgAyBdIF0AXeBdQAIAXQBeQF1QXoACAF2wXcBdwF2QAgADIALgAy -AEcAYQBtAGEAIABnAHIAaQAgAGcAZQBuAGUAcgBpAGMBAwAgADIALAAyAEEAbABsAGcAZQBtAGUAaQBu -AGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBmAGkAbAAgAEcAYQBtAG0AYQAgADIALAAy -AFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwAgAGQAZQBsAGwAYQAg -AGcAYQBtAG0AYQAgADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBt -AGEAcAByAG8AZgBpAGxmbpAacHBepnz7ZXAAMgAuADJjz4/wZYdO9k4AgiwwsDDsMKQwrDDzMN4AIAAy -AC4AMgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPMACADkwO6A8EDuQAgA5MDrAO8A7wDsQAgADIALgAy -AFAAZQByAGYAaQBsACAAZwBlAG4A6QByAGkAYwBvACAAZABlACAAYwBpAG4AegBlAG4AdABvAHMAIABk -AGEAIABHAGEAbQBtAGEAIAAyACwAMgBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAIABnAGEAbQBt -AGEAIAAyACwAMgAtAHAAcgBvAGYAaQBlAGwAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABk -AGUAIABnAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAGUAcwAgADIALAAyDiMOMQ4HDioONQ5BDgEOIQ4h -DjIOQA4BDiMOIg5MDhcOMQ5IDicORA4bACAAMgAuADIARwBlAG4AZQBsACAARwByAGkAIABHAGEAbQBh -ACAAMgAsADIAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAG4AIABnAGEAbQBtAGEAIAAyACwAMgAg -AC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0AawBpACAARwByAGEAeQAgAEcAYQBtAG0AYQAg -ADIALgAyACAAcAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABz -AHoAYQByAG8BWwBjAGkAIABnAGEAbQBtAGEAIAAyACwAMgY6BicGRQYnACAAMgAuADIAIAZEBkgGRgAg -BjEGRQYnBi8GSgAgBjkGJwZFBB4EMQRJBDAETwAgBEEENQRABDAETwAgBDMEMAQ8BDwEMAAgADIALAAy -AC0EPwRABD4ERAQ4BDsETABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAEcAYQBtAG0AYQAgADIALgAy -ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUgSW5jLiwgMjAxMgAAWFlaIAAA -AAAAAPNRAAEAAAABFsxjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBP -AFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADl -AOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGp -AbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2 -AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQT -BCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXF -BdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfS -B+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9 -ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0N -DSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBD -EGEQfhCbELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPl -FAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3 -GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7 -HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1 -IaEhziH7IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtybo -JxgnSSd6J6sn3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizX -LQwtQS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNG -M38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2 -OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGs -Qe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mp -SfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIx -UnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtF -W5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTp -ZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8e -b3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnn -ekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VH -hauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/ -kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3S -nkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sC -q3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjR -uUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dB -x7/IPci8yTrJuco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV -1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN -5pbnH+ep6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt -9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9IrLC0uWiRjbGFzc25hbWVYJGNsYXNzZXNf -EBBOU0JpdG1hcEltYWdlUmVwoy0vMFpOU0ltYWdlUmVwWE5TT2JqZWN00issMjNXTlNBcnJheaIyMNIr -LDU2Xk5TTXV0YWJsZUFycmF5ozUyMNM4OQ86OzxXTlNXaGl0ZVxOU0NvbG9yU3BhY2VEMCAwABADgAzS -Kyw+P1dOU0NvbG9yoj4w0issQUJXTlNJbWFnZaJBMAAIABEAGgAkACkAMgA3AEkATABRAFMAYgBoAHUA -fACLAJIAnwCmAK4AsACyALQAuQC7AL0AxADJANQA1gDYANoA3wDiAOQA5gDoAO8BBgEiASQBJhOWE5sT -phOvE8ITxhPRE9oT3xPnE+oT7xP+FAIUCRQRFB4UIxQlFCcULBQ0FDcUPBREAAAAAAAAAgEAAAAAAAAA -QwAAAAAAAAAAAAAAAAAAFEc - - + diff --git a/da.lproj/FirstTime.xib b/da.lproj/FirstTime.xib index 2c8808fd..761e821f 100644 --- a/da.lproj/FirstTime.xib +++ b/da.lproj/FirstTime.xib @@ -894,9 +894,12 @@ Cg - - + @@ -245,7 +245,7 @@ DQ - + diff --git a/SCDurationSlider.m b/SCDurationSlider.m index 915df70f..25e75376 100644 --- a/SCDurationSlider.m +++ b/SCDurationSlider.m @@ -42,45 +42,8 @@ - (void)initializeDurationProperties { - (void)setMaxDuration:(NSInteger)maxDuration { _maxDuration = maxDuration; - [self recalculateSliderIntervals]; -} - -- (void)recalculateSliderIntervals { [self setMinValue: 1]; // never start a block shorter than 1 minute [self setMaxValue: self.maxDuration]; - - // how many tick marks should we have? it's based on the max block duration - // TODO: can we make this work better with the start at 1-minute (vs 0)? if not, should we just eliminate ticks? - long tickInterval = 1; - if (self.maxDuration >= 5256000) { - // if max block duration is at least 10 years, tick marks are per-year - tickInterval = 525600; - } else if (self.maxDuration >= 525600) { - // if max block duration is at least 1 year, tick marks are per-month - tickInterval = 43800; - } else if (self.maxDuration >= 131400) { - // if max block duration is at least 3 months, tick marks are per-week - tickInterval = 10080; - } else if (self.maxDuration >= 10080) { - // if max block duration is at least 1 week, tick marks are per-day - tickInterval = 1440; - } else if (self.maxDuration >= 5760) { - // if max block duration is at least 4 days, tick marks are per 6 hours - tickInterval = 360; - } else if (self.maxDuration >= 1440) { - // if max block duration is at least 1 day, tick marks are per hour - tickInterval = 60; - } else if (self.maxDuration >= 720) { - // if max block duration is at least 12 hours, tick marks are per 30 minutes - tickInterval = 30; - } else if (self.maxDuration >= 60) { - // if max block duration is at least 1 hour, tick marks are per minute - tickInterval = 1; - } - - // no more than 72 ticks max - long numTicks = MIN(self.maxDuration / tickInterval, 72) + 1; - [self setNumberOfTickMarks: numTicks]; } - (void)registerMinutesValueTransformer { From 13d5753c8a0837ba76d28fe177110111bc94192f Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 26 Apr 2021 22:26:13 -0700 Subject: [PATCH 14/58] Send user notification when block ends --- AppController.m | 11 ++++++++++- Daemon/SCDaemonBlockMethods.m | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AppController.m b/AppController.m index 567b5f17..1a875c70 100755 --- a/AppController.m +++ b/AppController.m @@ -160,6 +160,15 @@ - (void)refreshUserInterface { // make sure the dock badge is cleared [[NSApp dockTile] setBadgeLabel: nil]; + // send a notification letting the user know the block ended + // TODO: make this sent from a background process so it shows if app is closed + // (but we can't send it from the selfcontrold process, because it's running as root) + NSUserNotificationCenter* userNoteCenter = [NSUserNotificationCenter defaultUserNotificationCenter]; + NSUserNotification* endedNote = [NSUserNotification new]; + endedNote.title = @"Your SelfControl block has ended!"; + endedNote.informativeText = @"All sites are now accessible."; + [userNoteCenter deliverNotification: endedNote]; + [self closeTimerWindow]; } @@ -359,7 +368,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self refreshUserInterface]; - NSOperatingSystemVersion minRequiredVersion = (NSOperatingSystemVersion){10,10,0}; // Mountain Lion + NSOperatingSystemVersion minRequiredVersion = (NSOperatingSystemVersion){10,10,0}; // Yosemite NSString* minRequiredVersionString = @"10.10 (Yosemite)"; if (![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: minRequiredVersion]) { NSLog(@"ERROR: Unsupported version for SelfControl"); diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 8a662a8f..83122455 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -316,7 +316,7 @@ + (void)checkupBlock { [SCHelperToolUtilities removeBlock]; [SCHelperToolUtilities sendConfigurationChangedNotification]; - + [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; // once the checkups stop, the daemon will clear itself in a while due to inactivity From 6f034018af4f7bd0b4bfa2ccce4d63fa2778cff9 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 26 Apr 2021 23:42:40 -0700 Subject: [PATCH 15/58] Temporarily disable import from mail apps, since they mostly didn't work --- Base.lproj/DomainList.xib | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Base.lproj/DomainList.xib b/Base.lproj/DomainList.xib index f0f3a64d..1e644be1 100755 --- a/Base.lproj/DomainList.xib +++ b/Base.lproj/DomainList.xib @@ -30,7 +30,7 @@ - + - + - +