1/******************************************************************************
2 * Copyright (c) 2005-2019 Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23#import <Foundation/Foundation.h>
24
25#import <Sparkle/Sparkle.h>
26
27#include <libtransmission/transmission.h>
28#include <libtransmission/utils.h>
29
30#import "VDKQueue.h"
31
32#import "PrefsController.h"
33#import "BlocklistDownloaderViewController.h"
34#import "BlocklistScheduler.h"
35#import "Controller.h"
36#import "PortChecker.h"
37#import "BonjourController.h"
38#import "NSApplicationAdditions.h"
39#import "NSStringAdditions.h"
40
41#define DOWNLOAD_FOLDER     0
42#define DOWNLOAD_TORRENT    2
43
44#define RPC_IP_ADD_TAG      0
45#define RPC_IP_REMOVE_TAG   1
46
47#define TOOLBAR_GENERAL     @"TOOLBAR_GENERAL"
48#define TOOLBAR_TRANSFERS   @"TOOLBAR_TRANSFERS"
49#define TOOLBAR_GROUPS      @"TOOLBAR_GROUPS"
50#define TOOLBAR_BANDWIDTH   @"TOOLBAR_BANDWIDTH"
51#define TOOLBAR_PEERS       @"TOOLBAR_PEERS"
52#define TOOLBAR_NETWORK     @"TOOLBAR_NETWORK"
53#define TOOLBAR_REMOTE      @"TOOLBAR_REMOTE"
54
55#define RPC_KEYCHAIN_SERVICE    "Transmission:Remote"
56#define RPC_KEYCHAIN_NAME       "Remote"
57
58#define WEBUI_URL   @"http://localhost:%ld/"
59
60@interface PrefsController (Private)
61
62- (void) setPrefView: (id) sender;
63
64- (void) setKeychainPassword: (const char *) password forService: (const char *) service username: (const char *) username;
65
66@end
67
68@implementation PrefsController
69
70- (id) initWithHandle: (tr_session *) handle
71{
72    if ((self = [super initWithWindowNibName: @"PrefsWindow"]))
73    {
74        fHandle = handle;
75
76        fDefaults = [NSUserDefaults standardUserDefaults];
77
78        //check for old version download location (before 1.1)
79        NSString * choice;
80        if ((choice = [fDefaults stringForKey: @"DownloadChoice"]))
81        {
82            [fDefaults setBool: [choice isEqualToString: @"Constant"] forKey: @"DownloadLocationConstant"];
83            [fDefaults setBool: YES forKey: @"DownloadAsk"];
84
85            [fDefaults removeObjectForKey: @"DownloadChoice"];
86        }
87
88        //check for old version blocklist (before 2.12)
89        NSDate * blocklistDate;
90        if ((blocklistDate = [fDefaults objectForKey: @"BlocklistLastUpdate"]))
91        {
92            [fDefaults setObject: blocklistDate forKey: @"BlocklistNewLastUpdateSuccess"];
93            [fDefaults setObject: blocklistDate forKey: @"BlocklistNewLastUpdate"];
94            [fDefaults removeObjectForKey: @"BlocklistLastUpdate"];
95
96            NSURL * blocklistDir = [[[NSFileManager defaultManager] URLsForDirectory: NSApplicationDirectory inDomains: NSUserDomainMask][0] URLByAppendingPathComponent: @"Transmission/blocklists/"];
97            [[NSFileManager defaultManager] moveItemAtURL: [blocklistDir URLByAppendingPathComponent: @"level1.bin"]
98                toURL: [blocklistDir URLByAppendingPathComponent: [NSString stringWithUTF8String: DEFAULT_BLOCKLIST_FILENAME]]
99                error: nil];
100        }
101
102        //save a new random port
103        if ([fDefaults boolForKey: @"RandomPort"])
104            [fDefaults setInteger: tr_sessionGetPeerPort(fHandle) forKey: @"BindPort"];
105
106        //set auto import
107        NSString * autoPath;
108        if ([fDefaults boolForKey: @"AutoImport"] && (autoPath = [fDefaults stringForKey: @"AutoImportDirectory"]))
109            [[(Controller *)[NSApp delegate] fileWatcherQueue] addPath: [autoPath stringByExpandingTildeInPath] notifyingAbout: VDKQueueNotifyAboutWrite];
110
111        //set special-handling of magnet link add window checkbox
112        [self updateShowAddMagnetWindowField];
113
114        //set blocklist scheduler
115        [[BlocklistScheduler scheduler] updateSchedule];
116
117        //set encryption
118        [self setEncryptionMode: nil];
119
120        //update rpc whitelist
121        [self updateRPCPassword];
122
123        fRPCWhitelistArray = [[fDefaults arrayForKey: @"RPCWhitelist"] mutableCopy];
124        if (!fRPCWhitelistArray)
125            fRPCWhitelistArray = [NSMutableArray arrayWithObject: @"127.0.0.1"];
126        [self updateRPCWhitelist];
127
128        //reset old Sparkle settings from previous versions
129        [fDefaults removeObjectForKey: @"SUScheduledCheckInterval"];
130        if ([fDefaults objectForKey: @"CheckForUpdates"])
131        {
132            [[SUUpdater sharedUpdater] setAutomaticallyChecksForUpdates: [fDefaults boolForKey: @"CheckForUpdates"]];
133            [fDefaults removeObjectForKey: @"CheckForUpdates"];
134        }
135
136        [self setAutoUpdateToBeta: nil];
137    }
138
139    return self;
140}
141
142- (void) dealloc
143{
144    [[NSNotificationCenter defaultCenter] removeObserver: self];
145
146    [fPortStatusTimer invalidate];
147    if (fPortChecker)
148    {
149        [fPortChecker cancelProbe];
150    }
151}
152
153- (void) awakeFromNib
154{
155    fHasLoaded = YES;
156
157    [[self window] setRestorationClass: [self class]];
158
159    NSToolbar * toolbar = [[NSToolbar alloc] initWithIdentifier: @"Preferences Toolbar"];
160    [toolbar setDelegate: self];
161    [toolbar setAllowsUserCustomization: NO];
162    [toolbar setDisplayMode: NSToolbarDisplayModeIconAndLabel];
163    [toolbar setSizeMode: NSToolbarSizeModeRegular];
164    [toolbar setSelectedItemIdentifier: TOOLBAR_GENERAL];
165    [[self window] setToolbar: toolbar];
166
167    [self setPrefView: nil];
168
169    //set download folder
170    [fFolderPopUp selectItemAtIndex: [fDefaults boolForKey: @"DownloadLocationConstant"] ? DOWNLOAD_FOLDER : DOWNLOAD_TORRENT];
171
172    //set stop ratio
173    [fRatioStopField setFloatValue: [fDefaults floatForKey: @"RatioLimit"]];
174
175    //set idle seeding minutes
176    [fIdleStopField setIntegerValue: [fDefaults integerForKey: @"IdleLimitMinutes"]];
177
178    //set limits
179    [self updateLimitFields];
180
181    //set speed limit
182    [fSpeedLimitUploadField setIntValue: [fDefaults integerForKey: @"SpeedLimitUploadLimit"]];
183    [fSpeedLimitDownloadField setIntValue: [fDefaults integerForKey: @"SpeedLimitDownloadLimit"]];
184
185    //set port
186    [fPortField setIntValue: [fDefaults integerForKey: @"BindPort"]];
187    fNatStatus = -1;
188
189    [self updatePortStatus];
190    fPortStatusTimer = [NSTimer scheduledTimerWithTimeInterval: 5.0 target: self selector: @selector(updatePortStatus) userInfo: nil repeats: YES];
191
192    //set peer connections
193    [fPeersGlobalField setIntValue: [fDefaults integerForKey: @"PeersTotal"]];
194    [fPeersTorrentField setIntValue: [fDefaults integerForKey: @"PeersTorrent"]];
195
196    //set queue values
197    [fQueueDownloadField setIntValue: [fDefaults integerForKey: @"QueueDownloadNumber"]];
198    [fQueueSeedField setIntValue: [fDefaults integerForKey: @"QueueSeedNumber"]];
199    [fStalledField setIntValue: [fDefaults integerForKey: @"StalledMinutes"]];
200
201    //set blocklist
202    NSString * blocklistURL = [fDefaults stringForKey: @"BlocklistURL"];
203    if (blocklistURL)
204        [fBlocklistURLField setStringValue: blocklistURL];
205
206    [self updateBlocklistButton];
207    [self updateBlocklistFields];
208
209    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateLimitFields)
210                                                 name: @"UpdateSpeedLimitValuesOutsidePrefs" object: nil];
211
212    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateRatioStopField)
213                                                 name: @"UpdateRatioStopValueOutsidePrefs" object: nil];
214
215    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateLimitStopField)
216                                                 name: @"UpdateIdleStopValueOutsidePrefs" object: nil];
217
218    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateBlocklistFields)
219        name: @"BlocklistUpdated" object: nil];
220
221    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateBlocklistURLField)
222        name: NSControlTextDidChangeNotification object: fBlocklistURLField];
223
224    //set rpc port
225    [fRPCPortField setIntValue: [fDefaults integerForKey: @"RPCPort"]];
226
227    //set rpc password
228    if (fRPCPassword)
229        [fRPCPasswordField setStringValue: fRPCPassword];
230}
231
232- (NSToolbarItem *) toolbar: (NSToolbar *) toolbar itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
233{
234    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
235
236    if ([ident isEqualToString: TOOLBAR_GENERAL])
237    {
238        [item setLabel: NSLocalizedString(@"General", "Preferences -> toolbar item title")];
239        [item setImage: [NSImage imageNamed: NSImageNamePreferencesGeneral]];
240        [item setTarget: self];
241        [item setAction: @selector(setPrefView:)];
242        [item setAutovalidates: NO];
243    }
244    else if ([ident isEqualToString: TOOLBAR_TRANSFERS])
245    {
246        [item setLabel: NSLocalizedString(@"Transfers", "Preferences -> toolbar item title")];
247        [item setImage: [NSImage imageNamed: @"Transfers"]];
248        [item setTarget: self];
249        [item setAction: @selector(setPrefView:)];
250        [item setAutovalidates: NO];
251    }
252    else if ([ident isEqualToString: TOOLBAR_GROUPS])
253    {
254        [item setLabel: NSLocalizedString(@"Groups", "Preferences -> toolbar item title")];
255        [item setImage: [NSImage imageNamed: @"Groups"]];
256        [item setTarget: self];
257        [item setAction: @selector(setPrefView:)];
258        [item setAutovalidates: NO];
259    }
260    else if ([ident isEqualToString: TOOLBAR_BANDWIDTH])
261    {
262        [item setLabel: NSLocalizedString(@"Bandwidth", "Preferences -> toolbar item title")];
263        [item setImage: [NSImage imageNamed: @"Bandwidth"]];
264        [item setTarget: self];
265        [item setAction: @selector(setPrefView:)];
266        [item setAutovalidates: NO];
267    }
268    else if ([ident isEqualToString: TOOLBAR_PEERS])
269    {
270        [item setLabel: NSLocalizedString(@"Peers", "Preferences -> toolbar item title")];
271        [item setImage: [NSImage imageNamed: NSImageNameUserGroup]];
272        [item setTarget: self];
273        [item setAction: @selector(setPrefView:)];
274        [item setAutovalidates: NO];
275    }
276    else if ([ident isEqualToString: TOOLBAR_NETWORK])
277    {
278        [item setLabel: NSLocalizedString(@"Network", "Preferences -> toolbar item title")];
279        [item setImage: [NSImage imageNamed: NSImageNameNetwork]];
280        [item setTarget: self];
281        [item setAction: @selector(setPrefView:)];
282        [item setAutovalidates: NO];
283    }
284    else if ([ident isEqualToString: TOOLBAR_REMOTE])
285    {
286        [item setLabel: NSLocalizedString(@"Remote", "Preferences -> toolbar item title")];
287        [item setImage: [NSImage imageNamed: @"Remote"]];
288        [item setTarget: self];
289        [item setAction: @selector(setPrefView:)];
290        [item setAutovalidates: NO];
291    }
292    else
293    {
294        return nil;
295    }
296
297    return item;
298}
299
300- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
301{
302    return @[TOOLBAR_GENERAL, TOOLBAR_TRANSFERS, TOOLBAR_GROUPS, TOOLBAR_BANDWIDTH,
303                                        TOOLBAR_PEERS, TOOLBAR_NETWORK, TOOLBAR_REMOTE];
304}
305
306- (NSArray *) toolbarSelectableItemIdentifiers: (NSToolbar *) toolbar
307{
308    return [self toolbarAllowedItemIdentifiers: toolbar];
309}
310
311- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
312{
313    return [self toolbarAllowedItemIdentifiers: toolbar];
314}
315
316+ (void) restoreWindowWithIdentifier: (NSString *) identifier state: (NSCoder *) state completionHandler: (void (^)(NSWindow *, NSError *)) completionHandler
317{
318    NSWindow * window = [[(Controller *)[NSApp delegate] prefsController] window];
319    completionHandler(window, nil);
320}
321
322//for a beta release, always use the beta appcast
323#if defined(TR_BETA_RELEASE)
324#define SPARKLE_TAG YES
325#else
326#define SPARKLE_TAG [fDefaults boolForKey: @"AutoUpdateBeta"]
327#endif
328- (void) setAutoUpdateToBeta: (id) sender
329{
330    // TODO: Support beta releases (if/when necessary)
331}
332
333- (void) setPort: (id) sender
334{
335    const tr_port port = [sender intValue];
336    [fDefaults setInteger: port forKey: @"BindPort"];
337    tr_sessionSetPeerPort(fHandle, port);
338
339    fPeerPort = -1;
340    [self updatePortStatus];
341}
342
343- (void) randomPort: (id) sender
344{
345    const tr_port port = tr_sessionSetPeerPortRandom(fHandle);
346    [fDefaults setInteger: port forKey: @"BindPort"];
347    [fPortField setIntValue: port];
348
349    fPeerPort = -1;
350    [self updatePortStatus];
351}
352
353- (void) setRandomPortOnStart: (id) sender
354{
355    tr_sessionSetPeerPortRandomOnStart(fHandle, [(NSButton *)sender state] == NSOnState);
356}
357
358- (void) setNat: (id) sender
359{
360    tr_sessionSetPortForwardingEnabled(fHandle, [fDefaults boolForKey: @"NatTraversal"]);
361
362    fNatStatus = -1;
363    [self updatePortStatus];
364}
365
366- (void) updatePortStatus
367{
368    const tr_port_forwarding fwd = tr_sessionGetPortForwarding(fHandle);
369    const int port = tr_sessionGetPeerPort(fHandle);
370    BOOL natStatusChanged = (fNatStatus != fwd);
371    BOOL peerPortChanged = (fPeerPort != port);
372
373    if (natStatusChanged || peerPortChanged)
374    {
375        fNatStatus = fwd;
376        fPeerPort = port;
377
378        [fPortStatusField setStringValue: @""];
379        [fPortStatusImage setImage: nil];
380        [fPortStatusProgress startAnimation: self];
381
382        if (fPortChecker)
383        {
384            [fPortChecker cancelProbe];
385        }
386        BOOL delay = natStatusChanged || tr_sessionIsPortForwardingEnabled(fHandle);
387        fPortChecker = [[PortChecker alloc] initForPort: fPeerPort delay: delay withDelegate: self];
388    }
389}
390
391- (void) portCheckerDidFinishProbing: (PortChecker *) portChecker
392{
393    [fPortStatusProgress stopAnimation: self];
394    switch ([fPortChecker status])
395    {
396        case PORT_STATUS_OPEN:
397            [fPortStatusField setStringValue: NSLocalizedString(@"Port is open", "Preferences -> Network -> port status")];
398            [fPortStatusImage setImage: [NSImage imageNamed: NSImageNameStatusAvailable]];
399            break;
400        case PORT_STATUS_CLOSED:
401            [fPortStatusField setStringValue: NSLocalizedString(@"Port is closed", "Preferences -> Network -> port status")];
402            [fPortStatusImage setImage: [NSImage imageNamed: NSImageNameStatusUnavailable]];
403            break;
404        case PORT_STATUS_ERROR:
405            [fPortStatusField setStringValue: NSLocalizedString(@"Port check site is down", "Preferences -> Network -> port status")];
406            [fPortStatusImage setImage: [NSImage imageNamed: NSImageNameStatusPartiallyAvailable]];
407            break;
408        default:
409            NSAssert1(NO, @"Port checker returned invalid status: %d", [fPortChecker status]);
410            break;
411    }
412    fPortChecker = nil;
413}
414
415- (NSArray *) sounds
416{
417    NSMutableArray * sounds = [NSMutableArray array];
418
419    NSArray * directories = NSSearchPathForDirectoriesInDomains(NSAllLibrariesDirectory, NSUserDomainMask | NSLocalDomainMask | NSSystemDomainMask, YES);
420
421    for (__strong NSString * directory in directories)
422    {
423        directory = [directory stringByAppendingPathComponent: @"Sounds"];
424
425        BOOL isDirectory;
426        if ([[NSFileManager defaultManager] fileExistsAtPath: directory isDirectory: &isDirectory] && isDirectory)
427        {
428            NSArray * directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath: directory error: NULL];
429            for (__strong NSString * sound in directoryContents)
430            {
431                sound = [sound stringByDeletingPathExtension];
432                if ([NSSound soundNamed: sound])
433                    [sounds addObject: sound];
434            }
435        }
436    }
437
438    return sounds;
439}
440
441- (void) setSound: (id) sender
442{
443    //play sound when selecting
444    NSSound * sound;
445    if ((sound = [NSSound soundNamed: [sender titleOfSelectedItem]]))
446        [sound play];
447}
448
449- (void) setUTP: (id) sender
450{
451    tr_sessionSetUTPEnabled(fHandle, [fDefaults boolForKey: @"UTPGlobal"]);
452}
453
454- (void) setPeersGlobal: (id) sender
455{
456    const int count = [sender intValue];
457    [fDefaults setInteger: count forKey: @"PeersTotal"];
458    tr_sessionSetPeerLimit(fHandle, count);
459}
460
461- (void) setPeersTorrent: (id) sender
462{
463    const int count = [sender intValue];
464    [fDefaults setInteger: count forKey: @"PeersTorrent"];
465    tr_sessionSetPeerLimitPerTorrent(fHandle, count);
466}
467
468- (void) setPEX: (id) sender
469{
470    tr_sessionSetPexEnabled(fHandle, [fDefaults boolForKey: @"PEXGlobal"]);
471}
472
473- (void) setDHT: (id) sender
474{
475    tr_sessionSetDHTEnabled(fHandle, [fDefaults boolForKey: @"DHTGlobal"]);
476}
477
478- (void) setLPD: (id) sender
479{
480    tr_sessionSetLPDEnabled(fHandle, [fDefaults boolForKey: @"LocalPeerDiscoveryGlobal"]);
481}
482
483- (void) setEncryptionMode: (id) sender
484{
485    const tr_encryption_mode mode = [fDefaults boolForKey: @"EncryptionPrefer"] ?
486        ([fDefaults boolForKey: @"EncryptionRequire"] ? TR_ENCRYPTION_REQUIRED : TR_ENCRYPTION_PREFERRED) : TR_CLEAR_PREFERRED;
487    tr_sessionSetEncryption(fHandle, mode);
488}
489
490- (void) setBlocklistEnabled: (id) sender
491{
492    tr_blocklistSetEnabled(fHandle, [fDefaults boolForKey: @"BlocklistNew"]);
493
494    [[BlocklistScheduler scheduler] updateSchedule];
495
496    [self updateBlocklistButton];
497}
498
499- (void) updateBlocklist: (id) sender
500{
501    [BlocklistDownloaderViewController downloadWithPrefsController: self];
502}
503
504- (void) setBlocklistAutoUpdate: (id) sender
505{
506    [[BlocklistScheduler scheduler] updateSchedule];
507}
508
509- (void) updateBlocklistFields
510{
511    const BOOL exists = tr_blocklistExists(fHandle);
512
513    if (exists)
514    {
515        NSString * countString = [NSString formattedUInteger: tr_blocklistGetRuleCount(fHandle)];
516        [fBlocklistMessageField setStringValue: [NSString stringWithFormat: NSLocalizedString(@"%@ IP address rules in list",
517            "Prefs -> blocklist -> message"), countString]];
518    }
519    else
520        [fBlocklistMessageField setStringValue: NSLocalizedString(@"A blocklist must first be downloaded",
521            "Prefs -> blocklist -> message")];
522
523    NSString * updatedDateString;
524    if (exists)
525    {
526        NSDate * updatedDate = [fDefaults objectForKey: @"BlocklistNewLastUpdateSuccess"];
527
528        if (updatedDate)
529            updatedDateString = [NSDateFormatter localizedStringFromDate: updatedDate dateStyle: NSDateFormatterFullStyle timeStyle: NSDateFormatterShortStyle];
530        else
531            updatedDateString = NSLocalizedString(@"N/A", "Prefs -> blocklist -> message");
532    }
533    else
534        updatedDateString = NSLocalizedString(@"Never", "Prefs -> blocklist -> message");
535
536    [fBlocklistDateField setStringValue: [NSString stringWithFormat: @"%@: %@",
537        NSLocalizedString(@"Last updated", "Prefs -> blocklist -> message"), updatedDateString]];
538}
539
540- (void) updateBlocklistURLField
541{
542    NSString * blocklistString = [fBlocklistURLField stringValue];
543
544    [fDefaults setObject: blocklistString forKey: @"BlocklistURL"];
545    tr_blocklistSetURL(fHandle, [blocklistString UTF8String]);
546
547    [self updateBlocklistButton];
548}
549
550- (void) updateBlocklistButton
551{
552    NSString * blocklistString = [fDefaults objectForKey: @"BlocklistURL"];
553    const BOOL enable = (blocklistString && ![blocklistString isEqualToString: @""])
554                            && [fDefaults boolForKey: @"BlocklistNew"];
555    [fBlocklistButton setEnabled: enable];
556}
557
558- (void) setAutoStartDownloads: (id) sender
559{
560    tr_sessionSetPaused(fHandle, ![fDefaults boolForKey: @"AutoStartDownload"]);
561}
562
563- (void) applySpeedSettings: (id) sender
564{
565    tr_sessionLimitSpeed(fHandle, TR_UP, [fDefaults boolForKey: @"CheckUpload"]);
566    tr_sessionSetSpeedLimit_KBps(fHandle, TR_UP, [fDefaults integerForKey: @"UploadLimit"]);
567
568    tr_sessionLimitSpeed(fHandle, TR_DOWN, [fDefaults boolForKey: @"CheckDownload"]);
569    tr_sessionSetSpeedLimit_KBps(fHandle, TR_DOWN, [fDefaults integerForKey: @"DownloadLimit"]);
570
571    [[NSNotificationCenter defaultCenter] postNotificationName: @"SpeedLimitUpdate" object: nil];
572}
573
574- (void) applyAltSpeedSettings
575{
576    tr_sessionSetAltSpeed_KBps(fHandle, TR_UP, [fDefaults integerForKey: @"SpeedLimitUploadLimit"]);
577    tr_sessionSetAltSpeed_KBps(fHandle, TR_DOWN, [fDefaults integerForKey: @"SpeedLimitDownloadLimit"]);
578
579    [[NSNotificationCenter defaultCenter] postNotificationName: @"SpeedLimitUpdate" object: nil];
580}
581
582- (void) applyRatioSetting: (id) sender
583{
584    tr_sessionSetRatioLimited(fHandle, [fDefaults boolForKey: @"RatioCheck"]);
585    tr_sessionSetRatioLimit(fHandle, [fDefaults floatForKey: @"RatioLimit"]);
586
587    //reload main table for seeding progress
588    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil];
589
590    //reload global settings in inspector
591    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGlobalOptions" object: nil];
592}
593
594- (void) setRatioStop: (id) sender
595{
596    [fDefaults setFloat: [sender floatValue] forKey: @"RatioLimit"];
597
598    [self applyRatioSetting: nil];
599}
600
601- (void) updateRatioStopField
602{
603    if (fHasLoaded)
604        [fRatioStopField setFloatValue: [fDefaults floatForKey: @"RatioLimit"]];
605}
606
607- (void) updateRatioStopFieldOld
608{
609    [self updateRatioStopField];
610
611    [self applyRatioSetting: nil];
612}
613
614- (void) applyIdleStopSetting: (id) sender
615{
616    tr_sessionSetIdleLimited(fHandle, [fDefaults boolForKey: @"IdleLimitCheck"]);
617    tr_sessionSetIdleLimit(fHandle, [fDefaults integerForKey: @"IdleLimitMinutes"]);
618
619    //reload main table for remaining seeding time
620    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil];
621
622    //reload global settings in inspector
623    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGlobalOptions" object: nil];
624}
625
626- (void) setIdleStop: (id) sender
627{
628    [fDefaults setInteger: [sender integerValue] forKey: @"IdleLimitMinutes"];
629
630    [self applyIdleStopSetting: nil];
631}
632
633- (void) updateLimitStopField
634{
635    if (fHasLoaded)
636        [fIdleStopField setIntegerValue: [fDefaults integerForKey: @"IdleLimitMinutes"]];
637}
638
639- (void) updateLimitFields
640{
641    if (!fHasLoaded)
642        return;
643
644    [fUploadField setIntValue: [fDefaults integerForKey: @"UploadLimit"]];
645    [fDownloadField setIntValue: [fDefaults integerForKey: @"DownloadLimit"]];
646}
647
648- (void) setGlobalLimit: (id) sender
649{
650    [fDefaults setInteger: [sender intValue] forKey: sender == fUploadField ? @"UploadLimit" : @"DownloadLimit"];
651    [self applySpeedSettings: self];
652}
653
654- (void) setSpeedLimit: (id) sender
655{
656    [fDefaults setInteger: [sender intValue] forKey: sender == fSpeedLimitUploadField
657                                                        ? @"SpeedLimitUploadLimit" : @"SpeedLimitDownloadLimit"];
658    [self applyAltSpeedSettings];
659}
660
661- (void) setAutoSpeedLimit: (id) sender
662{
663    tr_sessionUseAltSpeedTime(fHandle, [fDefaults boolForKey: @"SpeedLimitAuto"]);
664}
665
666- (void) setAutoSpeedLimitTime: (id) sender
667{
668    tr_sessionSetAltSpeedBegin(fHandle, [PrefsController dateToTimeSum: [fDefaults objectForKey: @"SpeedLimitAutoOnDate"]]);
669    tr_sessionSetAltSpeedEnd(fHandle, [PrefsController dateToTimeSum: [fDefaults objectForKey: @"SpeedLimitAutoOffDate"]]);
670}
671
672- (void) setAutoSpeedLimitDay: (id) sender
673{
674    tr_sessionSetAltSpeedDay(fHandle, [[sender selectedItem] tag]);
675}
676
677+ (NSInteger) dateToTimeSum: (NSDate *) date
678{
679    NSCalendar * calendar = [NSCalendar currentCalendar];
680    NSDateComponents * components = [calendar components: NSHourCalendarUnit | NSMinuteCalendarUnit fromDate: date];
681    return [components hour] * 60 + [components minute];
682}
683
684+ (NSDate *) timeSumToDate: (NSInteger) sum
685{
686    NSDateComponents * comps = [[NSDateComponents alloc] init];
687    [comps setHour: sum / 60];
688    [comps setMinute: sum % 60];
689
690    return [[NSCalendar currentCalendar] dateFromComponents: comps];
691}
692
693- (BOOL) control: (NSControl *) control textShouldBeginEditing: (NSText *) fieldEditor
694{
695    fInitialString = [control stringValue];
696
697    return YES;
698}
699
700- (BOOL) control: (NSControl *) control didFailToFormatString: (NSString *) string errorDescription: (NSString *) error
701{
702    NSBeep();
703    if (fInitialString)
704    {
705        [control setStringValue: fInitialString];
706        fInitialString = nil;
707    }
708    return NO;
709}
710
711- (void) setBadge: (id) sender
712{
713    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: self];
714}
715
716- (IBAction) openNotificationSystemPrefs: (NSButton *) sender
717{
718    [[NSWorkspace sharedWorkspace] openURL: [NSURL fileURLWithPath:@"/System/Library/PreferencePanes/Notifications.prefPane"]];
719}
720
721- (void) resetWarnings: (id) sender
722{
723    [fDefaults removeObjectForKey: @"WarningDuplicate"];
724    [fDefaults removeObjectForKey: @"WarningRemainingSpace"];
725    [fDefaults removeObjectForKey: @"WarningFolderDataSameName"];
726    [fDefaults removeObjectForKey: @"WarningResetStats"];
727    [fDefaults removeObjectForKey: @"WarningCreatorBlankAddress"];
728    [fDefaults removeObjectForKey: @"WarningCreatorPrivateBlankAddress"];
729    [fDefaults removeObjectForKey: @"WarningRemoveTrackers"];
730    [fDefaults removeObjectForKey: @"WarningInvalidOpen"];
731    [fDefaults removeObjectForKey: @"WarningRemoveCompleted"];
732    [fDefaults removeObjectForKey: @"WarningDonate"];
733    //[fDefaults removeObjectForKey: @"WarningLegal"];
734}
735
736- (void) setDefaultForMagnets: (id) sender
737{
738    NSString * bundleID = [[NSBundle mainBundle] bundleIdentifier];
739    const OSStatus result = LSSetDefaultHandlerForURLScheme((CFStringRef)@"magnet", (__bridge CFStringRef)bundleID);
740    if (result != noErr)
741        NSLog(@"Failed setting default magnet link handler");
742}
743
744- (void) setQueue: (id) sender
745{
746    //let's just do both - easier that way
747    tr_sessionSetQueueEnabled(fHandle, TR_DOWN, [fDefaults boolForKey: @"Queue"]);
748    tr_sessionSetQueueEnabled(fHandle, TR_UP, [fDefaults boolForKey: @"QueueSeed"]);
749
750    //handle if any transfers switch from queued to paused
751    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
752}
753
754- (void) setQueueNumber: (id) sender
755{
756    const NSInteger number = [sender intValue];
757    const BOOL seed = sender == fQueueSeedField;
758
759    [fDefaults setInteger: number forKey: seed ? @"QueueSeedNumber" : @"QueueDownloadNumber"];
760
761    tr_sessionSetQueueSize(fHandle, seed ? TR_UP : TR_DOWN, number);
762}
763
764- (void) setStalled: (id) sender
765{
766    tr_sessionSetQueueStalledEnabled(fHandle, [fDefaults boolForKey: @"CheckStalled"]);
767
768    //reload main table for stalled status
769    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil];
770}
771
772- (void) setStalledMinutes: (id) sender
773{
774    const NSInteger min = [sender intValue];
775    [fDefaults setInteger: min forKey: @"StalledMinutes"];
776    tr_sessionSetQueueStalledMinutes(fHandle, min);
777
778    //reload main table for stalled status
779    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: self];
780}
781
782- (void) setDownloadLocation: (id) sender
783{
784    [fDefaults setBool: [fFolderPopUp indexOfSelectedItem] == DOWNLOAD_FOLDER forKey: @"DownloadLocationConstant"];
785    [self updateShowAddMagnetWindowField];
786}
787
788- (void) folderSheetShow: (id) sender
789{
790    NSOpenPanel * panel = [NSOpenPanel openPanel];
791
792    [panel setPrompt: NSLocalizedString(@"Select", "Preferences -> Open panel prompt")];
793    [panel setAllowsMultipleSelection: NO];
794    [panel setCanChooseFiles: NO];
795    [panel setCanChooseDirectories: YES];
796    [panel setCanCreateDirectories: YES];
797
798    [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) {
799        if (result == NSFileHandlingPanelOKButton)
800        {
801            [fFolderPopUp selectItemAtIndex: DOWNLOAD_FOLDER];
802
803            NSString * folder = [[panel URLs][0] path];
804            [fDefaults setObject: folder forKey: @"DownloadFolder"];
805            [fDefaults setBool: YES forKey: @"DownloadLocationConstant"];
806            [self updateShowAddMagnetWindowField];
807
808            assert(folder.length > 0);
809            tr_sessionSetDownloadDir(fHandle, [folder fileSystemRepresentation]);
810        }
811        else
812        {
813            //reset if cancelled
814            [fFolderPopUp selectItemAtIndex: [fDefaults boolForKey: @"DownloadLocationConstant"] ? DOWNLOAD_FOLDER : DOWNLOAD_TORRENT];
815        }
816    }];
817}
818
819- (void) incompleteFolderSheetShow: (id) sender
820{
821    NSOpenPanel * panel = [NSOpenPanel openPanel];
822
823    [panel setPrompt: NSLocalizedString(@"Select", "Preferences -> Open panel prompt")];
824    [panel setAllowsMultipleSelection: NO];
825    [panel setCanChooseFiles: NO];
826    [panel setCanChooseDirectories: YES];
827    [panel setCanCreateDirectories: YES];
828
829    [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) {
830        if (result == NSFileHandlingPanelOKButton)
831        {
832            NSString * folder = [[panel URLs][0] path];
833            [fDefaults setObject: folder forKey: @"IncompleteDownloadFolder"];
834
835            assert(folder.length > 0);
836            tr_sessionSetIncompleteDir(fHandle, [folder fileSystemRepresentation]);
837        }
838        [fIncompleteFolderPopUp selectItemAtIndex: 0];
839    }];
840}
841
842- (void) doneScriptSheetShow:(id)sender
843{
844    NSOpenPanel * panel = [NSOpenPanel openPanel];
845
846    [panel setPrompt: NSLocalizedString(@"Select", "Preferences -> Open panel prompt")];
847    [panel setAllowsMultipleSelection: NO];
848    [panel setCanChooseFiles: YES];
849    [panel setCanChooseDirectories: NO];
850    [panel setCanCreateDirectories: NO];
851
852    [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) {
853        if (result == NSFileHandlingPanelOKButton)
854        {
855            NSString * filePath = [[panel URLs][0] path];
856
857            assert(filePath.length > 0);
858
859            [fDefaults setObject: filePath forKey: @"DoneScriptPath"];
860            tr_sessionSetTorrentDoneScript(fHandle, [filePath fileSystemRepresentation]);
861
862            [fDefaults setBool: YES forKey: @"DoneScriptEnabled"];
863            tr_sessionSetTorrentDoneScriptEnabled(fHandle, YES);
864        }
865        [fDoneScriptPopUp selectItemAtIndex: 0];
866    }];
867}
868
869- (void) setUseIncompleteFolder: (id) sender
870{
871    tr_sessionSetIncompleteDirEnabled(fHandle, [fDefaults boolForKey: @"UseIncompleteDownloadFolder"]);
872}
873
874- (void) setRenamePartialFiles: (id) sender
875{
876    tr_sessionSetIncompleteFileNamingEnabled(fHandle, [fDefaults boolForKey: @"RenamePartialFiles"]);
877}
878
879- (void) setShowAddMagnetWindow: (id) sender
880{
881    [fDefaults setBool: ([fShowMagnetAddWindowCheck state] == NSOnState) forKey: @"MagnetOpenAsk"];
882}
883
884- (void) updateShowAddMagnetWindowField
885{
886    if (![fDefaults boolForKey: @"DownloadLocationConstant"])
887    {
888        //always show the add window for magnet links when the download location is the same as the torrent file
889        [fShowMagnetAddWindowCheck setState: NSOnState];
890        [fShowMagnetAddWindowCheck setEnabled: NO];
891    }
892    else
893    {
894        [fShowMagnetAddWindowCheck setState: [fDefaults boolForKey: @"MagnetOpenAsk"]];
895        [fShowMagnetAddWindowCheck setEnabled: YES];
896    }
897}
898
899- (void) setDoneScriptEnabled: (id) sender
900{
901    if ([fDefaults boolForKey: @"DoneScriptEnabled"] && ![[NSFileManager defaultManager] fileExistsAtPath: [fDefaults stringForKey:@"DoneScriptPath"]])
902    {
903        // enabled is set but script file doesn't exist, so prompt for one and disable until they pick one
904        [fDefaults setBool: NO forKey: @"DoneScriptEnabled"];
905        [self doneScriptSheetShow: sender];
906    }
907    tr_sessionSetTorrentDoneScriptEnabled(fHandle, [fDefaults boolForKey: @"DoneScriptEnabled"]);
908}
909
910- (void) setAutoImport: (id) sender
911{
912    NSString * path;
913    if ((path = [fDefaults stringForKey: @"AutoImportDirectory"]))
914    {
915        VDKQueue * watcherQueue = [(Controller *)[NSApp delegate] fileWatcherQueue];
916        if ([fDefaults boolForKey: @"AutoImport"])
917        {
918            path = [path stringByExpandingTildeInPath];
919            [watcherQueue addPath: path notifyingAbout: VDKQueueNotifyAboutWrite];
920        }
921        else
922            [watcherQueue removeAllPaths];
923
924        [[NSNotificationCenter defaultCenter] postNotificationName: @"AutoImportSettingChange" object: self];
925    }
926    else
927        [self importFolderSheetShow: nil];
928}
929
930- (void) importFolderSheetShow: (id) sender
931{
932    NSOpenPanel * panel = [NSOpenPanel openPanel];
933
934    [panel setPrompt: NSLocalizedString(@"Select", "Preferences -> Open panel prompt")];
935    [panel setAllowsMultipleSelection: NO];
936    [panel setCanChooseFiles: NO];
937    [panel setCanChooseDirectories: YES];
938    [panel setCanCreateDirectories: YES];
939
940    [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) {
941        if (result == NSFileHandlingPanelOKButton)
942        {
943            VDKQueue * watcherQueue = [(Controller *)[NSApp delegate] fileWatcherQueue];
944            [watcherQueue removeAllPaths];
945
946            NSString * path = [[panel URLs][0] path];
947            [fDefaults setObject: path forKey: @"AutoImportDirectory"];
948            [watcherQueue addPath: [path stringByExpandingTildeInPath] notifyingAbout: VDKQueueNotifyAboutWrite];
949
950            [[NSNotificationCenter defaultCenter] postNotificationName: @"AutoImportSettingChange" object: self];
951        }
952        else
953        {
954            NSString * path = [fDefaults stringForKey: @"AutoImportDirectory"];
955            if (!path)
956                [fDefaults setBool: NO forKey: @"AutoImport"];
957        }
958
959        [fImportFolderPopUp selectItemAtIndex: 0];
960    }];
961}
962
963- (void) setAutoSize: (id) sender
964{
965    [[NSNotificationCenter defaultCenter] postNotificationName: @"AutoSizeSettingChange" object: self];
966}
967
968- (void) setRPCEnabled: (id) sender
969{
970    BOOL enable = [fDefaults boolForKey: @"RPC"];
971    tr_sessionSetRPCEnabled(fHandle, enable);
972
973    [self setRPCWebUIDiscovery: nil];
974}
975
976- (void) linkWebUI: (id) sender
977{
978    NSString * urlString = [NSString stringWithFormat: WEBUI_URL, [fDefaults integerForKey: @"RPCPort"]];
979    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: urlString]];
980}
981
982- (void) setRPCAuthorize: (id) sender
983{
984    tr_sessionSetRPCPasswordEnabled(fHandle, [fDefaults boolForKey: @"RPCAuthorize"]);
985}
986
987- (void) setRPCUsername: (id) sender
988{
989    tr_sessionSetRPCUsername(fHandle, [[fDefaults stringForKey: @"RPCUsername"] UTF8String]);
990}
991
992- (void) setRPCPassword: (id) sender
993{
994    fRPCPassword = [[sender stringValue] copy];
995
996    const char * password = [[sender stringValue] UTF8String];
997    [self setKeychainPassword: password forService: RPC_KEYCHAIN_SERVICE username: RPC_KEYCHAIN_NAME];
998
999    tr_sessionSetRPCPassword(fHandle, password);
1000}
1001
1002- (void) updateRPCPassword
1003{
1004    UInt32 passwordLength;
1005    const char * password = nil;
1006    SecKeychainFindGenericPassword(NULL, strlen(RPC_KEYCHAIN_SERVICE), RPC_KEYCHAIN_SERVICE,
1007        strlen(RPC_KEYCHAIN_NAME), RPC_KEYCHAIN_NAME, &passwordLength, (void **)&password, NULL);
1008
1009    if (password != NULL)
1010    {
1011        char fullPassword[passwordLength+1];
1012        strncpy(fullPassword, password, passwordLength);
1013        fullPassword[passwordLength] = '\0';
1014        SecKeychainItemFreeContent(NULL, (void *)password);
1015
1016        tr_sessionSetRPCPassword(fHandle, fullPassword);
1017
1018        fRPCPassword = [[NSString alloc] initWithUTF8String: fullPassword];
1019        [fRPCPasswordField setStringValue: fRPCPassword];
1020    }
1021    else
1022        fRPCPassword = nil;
1023}
1024
1025- (void) setRPCPort: (id) sender
1026{
1027    int port = [sender intValue];
1028    [fDefaults setInteger: port forKey: @"RPCPort"];
1029    tr_sessionSetRPCPort(fHandle, port);
1030
1031    [self setRPCWebUIDiscovery: nil];
1032}
1033
1034- (void) setRPCUseWhitelist: (id) sender
1035{
1036    tr_sessionSetRPCWhitelistEnabled(fHandle, [fDefaults boolForKey: @"RPCUseWhitelist"]);
1037}
1038
1039- (void) setRPCWebUIDiscovery: (id) sender
1040{
1041    if ([fDefaults boolForKey:@"RPC"] && [fDefaults boolForKey: @"RPCWebDiscovery"])
1042        [[BonjourController defaultController] startWithPort: [fDefaults integerForKey: @"RPCPort"]];
1043    else
1044    {
1045        if ([BonjourController defaultControllerExists])
1046            [[BonjourController defaultController] stop];
1047    }
1048}
1049
1050- (void) updateRPCWhitelist
1051{
1052    NSString * string = [fRPCWhitelistArray componentsJoinedByString: @","];
1053    tr_sessionSetRPCWhitelist(fHandle, [string UTF8String]);
1054}
1055
1056- (void) addRemoveRPCIP: (id) sender
1057{
1058    //don't allow add/remove when currently adding - it leads to weird results
1059    if ([fRPCWhitelistTable editedRow] != -1)
1060        return;
1061
1062    if ([[sender cell] tagForSegment: [sender selectedSegment]] == RPC_IP_REMOVE_TAG)
1063    {
1064        [fRPCWhitelistArray removeObjectsAtIndexes: [fRPCWhitelistTable selectedRowIndexes]];
1065        [fRPCWhitelistTable deselectAll: self];
1066        [fRPCWhitelistTable reloadData];
1067
1068        [fDefaults setObject: fRPCWhitelistArray forKey: @"RPCWhitelist"];
1069        [self updateRPCWhitelist];
1070    }
1071    else
1072    {
1073        [fRPCWhitelistArray addObject: @""];
1074        [fRPCWhitelistTable reloadData];
1075
1076        const int row = [fRPCWhitelistArray count] - 1;
1077        [fRPCWhitelistTable selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO];
1078        [fRPCWhitelistTable editColumn: 0 row: row withEvent: nil select: YES];
1079    }
1080}
1081
1082- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
1083{
1084    return [fRPCWhitelistArray count];
1085}
1086
1087- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (NSInteger) row
1088{
1089    return fRPCWhitelistArray[row];
1090}
1091
1092- (void) tableView: (NSTableView *) tableView setObjectValue: (id) object forTableColumn: (NSTableColumn *) tableColumn
1093    row: (NSInteger) row
1094{
1095    NSArray * components = [object componentsSeparatedByString: @"."];
1096    NSMutableArray * newComponents = [NSMutableArray arrayWithCapacity: 4];
1097
1098    //create better-formatted ip string
1099    BOOL valid = false;
1100    if ([components count] == 4)
1101    {
1102        valid = true;
1103        for (NSString * component in components)
1104        {
1105            if ([component isEqualToString: @"*"])
1106                [newComponents addObject: component];
1107            else
1108            {
1109                int num = [component intValue];
1110                if (num >= 0 && num < 256)
1111                    [newComponents addObject: [@(num) stringValue]];
1112                else
1113                {
1114                    valid = false;
1115                    break;
1116                }
1117            }
1118        }
1119    }
1120
1121    NSString * newIP;
1122    if (valid)
1123    {
1124        newIP = [newComponents componentsJoinedByString: @"."];
1125
1126        //don't allow the same ip address
1127        if ([fRPCWhitelistArray containsObject: newIP] && ![fRPCWhitelistArray[row] isEqualToString: newIP])
1128            valid = false;
1129    }
1130
1131    if (valid)
1132    {
1133        fRPCWhitelistArray[row] = newIP;
1134        [fRPCWhitelistArray sortUsingSelector: @selector(compareNumeric:)];
1135    }
1136    else
1137    {
1138        NSBeep();
1139        if ([fRPCWhitelistArray[row] isEqualToString: @""])
1140            [fRPCWhitelistArray removeObjectAtIndex: row];
1141    }
1142
1143    [fRPCWhitelistTable deselectAll: self];
1144    [fRPCWhitelistTable reloadData];
1145
1146    [fDefaults setObject: fRPCWhitelistArray forKey: @"RPCWhitelist"];
1147    [self updateRPCWhitelist];
1148}
1149
1150- (void) tableViewSelectionDidChange: (NSNotification *) notification
1151{
1152    [fRPCAddRemoveControl setEnabled: [fRPCWhitelistTable numberOfSelectedRows] > 0 forSegment: RPC_IP_REMOVE_TAG];
1153}
1154
1155- (void) helpForScript: (id) sender
1156{
1157    [[NSHelpManager sharedHelpManager] openHelpAnchor: @"script"
1158        inBook: [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleHelpBookName"]];
1159}
1160
1161- (void) helpForPeers: (id) sender
1162{
1163    [[NSHelpManager sharedHelpManager] openHelpAnchor: @"peers"
1164        inBook: [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleHelpBookName"]];
1165}
1166
1167- (void) helpForNetwork: (id) sender
1168{
1169    [[NSHelpManager sharedHelpManager] openHelpAnchor: @"network"
1170        inBook: [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleHelpBookName"]];
1171}
1172
1173- (void) helpForRemote: (id) sender
1174{
1175    [[NSHelpManager sharedHelpManager] openHelpAnchor: @"remote"
1176        inBook: [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleHelpBookName"]];
1177}
1178
1179- (void) rpcUpdatePrefs
1180{
1181    //encryption
1182    const tr_encryption_mode encryptionMode = tr_sessionGetEncryption(fHandle);
1183    [fDefaults setBool: encryptionMode != TR_CLEAR_PREFERRED forKey: @"EncryptionPrefer"];
1184    [fDefaults setBool: encryptionMode == TR_ENCRYPTION_REQUIRED forKey: @"EncryptionRequire"];
1185
1186    //download directory
1187    NSString * downloadLocation = [@(tr_sessionGetDownloadDir(fHandle)) stringByStandardizingPath];
1188    [fDefaults setObject: downloadLocation forKey: @"DownloadFolder"];
1189
1190    NSString * incompleteLocation = [@(tr_sessionGetIncompleteDir(fHandle)) stringByStandardizingPath];
1191    [fDefaults setObject: incompleteLocation forKey: @"IncompleteDownloadFolder"];
1192
1193    const BOOL useIncomplete = tr_sessionIsIncompleteDirEnabled(fHandle);
1194    [fDefaults setBool: useIncomplete forKey: @"UseIncompleteDownloadFolder"];
1195
1196    const BOOL usePartialFileRanaming = tr_sessionIsIncompleteFileNamingEnabled(fHandle);
1197    [fDefaults setBool: usePartialFileRanaming forKey: @"RenamePartialFiles"];
1198
1199    //utp
1200    const BOOL utp = tr_sessionIsUTPEnabled(fHandle);
1201    [fDefaults setBool: utp forKey: @"UTPGlobal"];
1202
1203    //peers
1204    const uint16_t peersTotal = tr_sessionGetPeerLimit(fHandle);
1205    [fDefaults setInteger: peersTotal forKey: @"PeersTotal"];
1206
1207    const uint16_t peersTorrent = tr_sessionGetPeerLimitPerTorrent(fHandle);
1208    [fDefaults setInteger: peersTorrent forKey: @"PeersTorrent"];
1209
1210    //pex
1211    const BOOL pex = tr_sessionIsPexEnabled(fHandle);
1212    [fDefaults setBool: pex forKey: @"PEXGlobal"];
1213
1214    //dht
1215    const BOOL dht = tr_sessionIsDHTEnabled(fHandle);
1216    [fDefaults setBool: dht forKey: @"DHTGlobal"];
1217
1218    //lpd
1219    const BOOL lpd = tr_sessionIsLPDEnabled(fHandle);
1220    [fDefaults setBool: lpd forKey: @"LocalPeerDiscoveryGlobal"];
1221
1222    //auto start
1223    const BOOL autoStart = !tr_sessionGetPaused(fHandle);
1224    [fDefaults setBool: autoStart forKey: @"AutoStartDownload"];
1225
1226    //port
1227    const tr_port port = tr_sessionGetPeerPort(fHandle);
1228    [fDefaults setInteger: port forKey: @"BindPort"];
1229
1230    const BOOL nat = tr_sessionIsPortForwardingEnabled(fHandle);
1231    [fDefaults setBool: nat forKey: @"NatTraversal"];
1232
1233    fPeerPort = -1;
1234    fNatStatus = -1;
1235    [self updatePortStatus];
1236
1237    const BOOL randomPort = tr_sessionGetPeerPortRandomOnStart(fHandle);
1238    [fDefaults setBool: randomPort forKey: @"RandomPort"];
1239
1240    //speed limit - down
1241    const BOOL downLimitEnabled = tr_sessionIsSpeedLimited(fHandle, TR_DOWN);
1242    [fDefaults setBool: downLimitEnabled forKey: @"CheckDownload"];
1243
1244    const int downLimit = tr_sessionGetSpeedLimit_KBps(fHandle, TR_DOWN);
1245    [fDefaults setInteger: downLimit forKey: @"DownloadLimit"];
1246
1247    //speed limit - up
1248    const BOOL upLimitEnabled = tr_sessionIsSpeedLimited(fHandle, TR_UP);
1249    [fDefaults setBool: upLimitEnabled forKey: @"CheckUpload"];
1250
1251    const int upLimit = tr_sessionGetSpeedLimit_KBps(fHandle, TR_UP);
1252    [fDefaults setInteger: upLimit forKey: @"UploadLimit"];
1253
1254    //alt speed limit enabled
1255    const BOOL useAltSpeed = tr_sessionUsesAltSpeed(fHandle);
1256    [fDefaults setBool: useAltSpeed forKey: @"SpeedLimit"];
1257
1258    //alt speed limit - down
1259    const int downLimitAlt = tr_sessionGetAltSpeed_KBps(fHandle, TR_DOWN);
1260    [fDefaults setInteger: downLimitAlt forKey: @"SpeedLimitDownloadLimit"];
1261
1262    //alt speed limit - up
1263    const int upLimitAlt = tr_sessionGetAltSpeed_KBps(fHandle, TR_UP);
1264    [fDefaults setInteger: upLimitAlt forKey: @"SpeedLimitUploadLimit"];
1265
1266    //alt speed limit schedule
1267    const BOOL useAltSpeedSched = tr_sessionUsesAltSpeedTime(fHandle);
1268    [fDefaults setBool: useAltSpeedSched forKey: @"SpeedLimitAuto"];
1269
1270    NSDate * limitStartDate = [PrefsController timeSumToDate: tr_sessionGetAltSpeedBegin(fHandle)];
1271    [fDefaults setObject: limitStartDate forKey: @"SpeedLimitAutoOnDate"];
1272
1273    NSDate * limitEndDate = [PrefsController timeSumToDate: tr_sessionGetAltSpeedEnd(fHandle)];
1274    [fDefaults setObject: limitEndDate forKey: @"SpeedLimitAutoOffDate"];
1275
1276    const int limitDay = tr_sessionGetAltSpeedDay(fHandle);
1277    [fDefaults setInteger: limitDay forKey: @"SpeedLimitAutoDay"];
1278
1279    //blocklist
1280    const BOOL blocklist = tr_blocklistIsEnabled(fHandle);
1281    [fDefaults setBool: blocklist forKey: @"BlocklistNew"];
1282
1283    NSString * blocklistURL = @(tr_blocklistGetURL(fHandle));
1284    [fDefaults setObject: blocklistURL forKey: @"BlocklistURL"];
1285
1286    //seed ratio
1287    const BOOL ratioLimited = tr_sessionIsRatioLimited(fHandle);
1288    [fDefaults setBool: ratioLimited forKey: @"RatioCheck"];
1289
1290    const float ratioLimit = tr_sessionGetRatioLimit(fHandle);
1291    [fDefaults setFloat: ratioLimit forKey: @"RatioLimit"];
1292
1293    //idle seed limit
1294    const BOOL idleLimited = tr_sessionIsIdleLimited(fHandle);
1295    [fDefaults setBool: idleLimited forKey: @"IdleLimitCheck"];
1296
1297    const NSUInteger idleLimitMin = tr_sessionGetIdleLimit(fHandle);
1298    [fDefaults setInteger: idleLimitMin forKey: @"IdleLimitMinutes"];
1299
1300    //queue
1301    const BOOL downloadQueue = tr_sessionGetQueueEnabled(fHandle, TR_DOWN);
1302    [fDefaults setBool: downloadQueue forKey: @"Queue"];
1303
1304    const int downloadQueueNum = tr_sessionGetQueueSize(fHandle, TR_DOWN);
1305    [fDefaults setInteger: downloadQueueNum forKey: @"QueueDownloadNumber"];
1306
1307    const BOOL seedQueue = tr_sessionGetQueueEnabled(fHandle, TR_UP);
1308    [fDefaults setBool: seedQueue forKey: @"QueueSeed"];
1309
1310    const int seedQueueNum = tr_sessionGetQueueSize(fHandle, TR_UP);
1311    [fDefaults setInteger: seedQueueNum forKey: @"QueueSeedNumber"];
1312
1313    const BOOL checkStalled = tr_sessionGetQueueStalledEnabled(fHandle);
1314    [fDefaults setBool: checkStalled forKey: @"CheckStalled"];
1315
1316    const int stalledMinutes = tr_sessionGetQueueStalledMinutes(fHandle);
1317    [fDefaults setInteger: stalledMinutes forKey: @"StalledMinutes"];
1318
1319    //done script
1320    const BOOL doneScriptEnabled = tr_sessionIsTorrentDoneScriptEnabled(fHandle);
1321    [fDefaults setBool: doneScriptEnabled forKey: @"DoneScriptEnabled"];
1322
1323    NSString * doneScriptPath = @(tr_sessionGetTorrentDoneScript(fHandle));
1324    [fDefaults setObject: doneScriptPath forKey: @"DoneScriptPath"];
1325
1326    //update gui if loaded
1327    if (fHasLoaded)
1328    {
1329        //encryption handled by bindings
1330
1331        //download directory handled by bindings
1332
1333        //utp handled by bindings
1334
1335        [fPeersGlobalField setIntValue: peersTotal];
1336        [fPeersTorrentField setIntValue: peersTorrent];
1337
1338        //pex handled by bindings
1339
1340        //dht handled by bindings
1341
1342        //lpd handled by bindings
1343
1344        [fPortField setIntValue: port];
1345        //port forwarding (nat) handled by bindings
1346        //random port handled by bindings
1347
1348        //limit check handled by bindings
1349        [fDownloadField setIntValue: downLimit];
1350
1351        //limit check handled by bindings
1352        [fUploadField setIntValue: upLimit];
1353
1354        [fSpeedLimitDownloadField setIntValue: downLimitAlt];
1355
1356        [fSpeedLimitUploadField setIntValue: upLimitAlt];
1357
1358        //speed limit schedule handled by bindings
1359
1360        //speed limit schedule times and day handled by bindings
1361
1362        [fBlocklistURLField setStringValue: blocklistURL];
1363        [self updateBlocklistButton];
1364        [self updateBlocklistFields];
1365
1366        //ratio limit enabled handled by bindings
1367        [fRatioStopField setFloatValue: ratioLimit];
1368
1369        //idle limit enabled handled by bindings
1370        [fIdleStopField setIntegerValue: idleLimitMin];
1371
1372        //queues enabled handled by bindings
1373        [fQueueDownloadField setIntValue: downloadQueueNum];
1374        [fQueueSeedField setIntValue: seedQueueNum];
1375
1376        //check stalled handled by bindings
1377        [fStalledField setIntValue: stalledMinutes];
1378    }
1379
1380    [[NSNotificationCenter defaultCenter] postNotificationName: @"SpeedLimitUpdate" object: nil];
1381
1382    //reload global settings in inspector
1383    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGlobalOptions" object: nil];
1384}
1385
1386@end
1387
1388@implementation PrefsController (Private)
1389
1390- (void) setPrefView: (id) sender
1391{
1392    NSString * identifier;
1393    if (sender)
1394    {
1395        identifier = [sender itemIdentifier];
1396        [[NSUserDefaults standardUserDefaults] setObject: identifier forKey: @"SelectedPrefView"];
1397    }
1398    else
1399        identifier = [[NSUserDefaults standardUserDefaults] stringForKey: @"SelectedPrefView"];
1400
1401    NSView * view;
1402    if ([identifier isEqualToString: TOOLBAR_TRANSFERS])
1403        view = fTransfersView;
1404    else if ([identifier isEqualToString: TOOLBAR_GROUPS])
1405        view = fGroupsView;
1406    else if ([identifier isEqualToString: TOOLBAR_BANDWIDTH])
1407        view = fBandwidthView;
1408    else if ([identifier isEqualToString: TOOLBAR_PEERS])
1409        view = fPeersView;
1410    else if ([identifier isEqualToString: TOOLBAR_NETWORK])
1411        view = fNetworkView;
1412    else if ([identifier isEqualToString: TOOLBAR_REMOTE])
1413        view = fRemoteView;
1414    else
1415    {
1416        identifier = TOOLBAR_GENERAL; //general view is the default selected
1417        view = fGeneralView;
1418    }
1419
1420    [[[self window] toolbar] setSelectedItemIdentifier: identifier];
1421
1422    NSWindow * window = [self window];
1423    if ([window contentView] == view)
1424        return;
1425
1426    NSRect windowRect = [window frame];
1427    const CGFloat difference = NSHeight([view frame]) - NSHeight([[window contentView] frame]);
1428    windowRect.origin.y -= difference;
1429    windowRect.size.height += difference;
1430
1431    [view setHidden: YES];
1432    [window setContentView: view];
1433    [window setFrame: windowRect display: YES animate: YES];
1434    [view setHidden: NO];
1435
1436    //set title label
1437    if (sender)
1438        [window setTitle: [sender label]];
1439    else
1440    {
1441        NSToolbar * toolbar = [window toolbar];
1442        NSString * itemIdentifier = [toolbar selectedItemIdentifier];
1443        for (NSToolbarItem * item in [toolbar items])
1444            if ([[item itemIdentifier] isEqualToString: itemIdentifier])
1445            {
1446                [window setTitle: [item label]];
1447                break;
1448            }
1449    }
1450}
1451
1452static NSString * getOSStatusDescription(OSStatus errorCode)
1453{
1454    return [[NSError errorWithDomain: NSOSStatusErrorDomain code: errorCode userInfo: NULL] description];
1455}
1456
1457- (void) setKeychainPassword: (const char *) password forService: (const char *) service username: (const char *) username
1458{
1459    SecKeychainItemRef item = NULL;
1460    NSUInteger passwordLength = strlen(password);
1461
1462    OSStatus result = SecKeychainFindGenericPassword(NULL, strlen(service), service, strlen(username), username, NULL, NULL, &item);
1463    if (result == noErr && item)
1464    {
1465        if (passwordLength > 0) //found, so update
1466        {
1467            result = SecKeychainItemModifyAttributesAndData(item, NULL, passwordLength, (const void *)password);
1468            if (result != noErr)
1469                NSLog(@"Problem updating Keychain item: %@", getOSStatusDescription(result));
1470        }
1471        else //remove the item
1472        {
1473            result = SecKeychainItemDelete(item);
1474            if (result != noErr)
1475            {
1476                NSLog(@"Problem removing Keychain item: %@", getOSStatusDescription(result));
1477            }
1478        }
1479    }
1480    else if (result == errSecItemNotFound) //not found, so add
1481    {
1482        if (passwordLength > 0)
1483        {
1484            result = SecKeychainAddGenericPassword(NULL, strlen(service), service, strlen(username), username,
1485                        passwordLength, (const void *)password, NULL);
1486            if (result != noErr)
1487                NSLog(@"Problem adding Keychain item: %@", getOSStatusDescription(result));
1488        }
1489    }
1490    else
1491        NSLog(@"Problem accessing Keychain: %@", getOSStatusDescription(result));
1492}
1493
1494@end
1495