From fda6bfc61ecb024888957a0393d7e88e95d34999 Mon Sep 17 00:00:00 2001 From: Patrick Wardle Date: Wed, 19 Aug 2015 20:52:36 -1000 Subject: [PATCH] fixes/improvements to flagged items & search views variable length 'loaded in' field mouse over for buttons (+refactor) files now searched --- AppDelegate.m | 3 + FlaggedItems.h | 15 ++- FlaggedItems.m | 201 ++++++++++++++++++++++++++++++++------- ItemView.m | 149 +---------------------------- SearchWindowController.h | 1 + SearchWindowController.m | 99 +++++++++++++++---- TaskEnumerator.m | 26 ++++- TaskTableController.m | 107 +-------------------- UI/FlaggedItems.xib | 16 +++- UI/SearchWindow.xib | 58 ++++------- Utilities.h | 3 + Utilities.m | 99 +++++++++++++++++++ 12 files changed, 426 insertions(+), 351 deletions(-) diff --git a/AppDelegate.m b/AppDelegate.m index 55f802b..2f63c02 100755 --- a/AppDelegate.m +++ b/AppDelegate.m @@ -1758,6 +1758,9 @@ bail: //alloc/init flagItemsWindowController = [[FlaggedItems alloc] initWithWindowNibName:@"FlaggedItems"]; } + + //init/prep it + [self.flagItemsWindowController prepare]; //show it [self.flagItemsWindowController showWindow:self]; diff --git a/FlaggedItems.h b/FlaggedItems.h index 12d93dc..843276a 100644 --- a/FlaggedItems.h +++ b/FlaggedItems.h @@ -7,10 +7,11 @@ // #import +#import "InfoWindowController.h" @interface FlaggedItems : NSWindowController -//PROPERTIES +/* PROPERTIES */ //flag for first time init's @property BOOL didInit; @@ -18,7 +19,19 @@ //table @property (weak) IBOutlet NSTableView *flaggedItemTable; +//table items +@property(nonatomic, retain)NSMutableArray* flaggedItems; + //vt window controller @property (nonatomic, retain)VTInfoWindowController* vtWindowController; +//info window controller +@property(retain, nonatomic)InfoWindowController* infoWindowController; + +/* METHODS */ + +//init/prepare +// ->make sure everything is cleanly init'd before displaying +-(void)prepare; + @end diff --git a/FlaggedItems.m b/FlaggedItems.m index 427b605..1c02455 100644 --- a/FlaggedItems.m +++ b/FlaggedItems.m @@ -10,8 +10,7 @@ #import "FlaggedItems.h" #import "ItemView.h" #import "KKRow.h" - -//TODO: mouse over for info/show in finder buttons! +#import "Utilities.h" @interface FlaggedItems () @@ -20,8 +19,10 @@ @implementation FlaggedItems @synthesize didInit; +@synthesize flaggedItems; @synthesize flaggedItemTable; @synthesize vtWindowController; +@synthesize infoWindowController; //automatically called when nib is loaded // ->center window @@ -40,51 +41,67 @@ return; } -//automatically invoked when window is loaded -// ->set to white --(void)windowDidLoad +//init/prepare +// ->make sure everything is cleanly init'd before displaying +-(void)prepare { - //super - [super windowDidLoad]; + //array of flagged tasks + NSMutableArray* flaggedTasks = nil; - //table reload + //init array for flagged items + flaggedItems = [NSMutableArray array]; + + //table reload w/ blank array + // ->make sure all is reset + [self.flaggedItemTable reloadData]; + + //populate flagged items array + for(Binary* flaggedItem in ((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems) + { + //when binary is task binary + // ->find/save all tasks instances + if(YES == flaggedItem.isTaskBinary) + { + //get all tasks instances + flaggedTasks = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator tasksForBinary:flaggedItem]; + + //save all tasks + [self.flaggedItems addObjectsFromArray:flaggedTasks]; + } + //when binary is dylib + // ->just add, since will be processed as single item + else + { + //add + [self.flaggedItems addObject:flaggedItem]; + + } + } + + //always reload [self.flaggedItemTable reloadData]; -} - -//TODO: don't need -//automatically invoked when window is closing -// ->make ourselves unmodal --(void)windowWillClose:(NSNotification *)notification -{ - //make un-modal - //[[NSApplication sharedApplication] stopModal]; return; } + //table delegate // ->return number of rows, which is just number of items in the currently selected plugin -(NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { - return ((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems.count; + return self.flaggedItems.count; } //table delegate method // ->return cell for row -(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { - //flagged items - NSMutableArray* flaggedItems = nil; - //row view NSView* rowView = nil; - //grab flagged items - flaggedItems = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems; - //sanity check // ->make sure there is table item for row - if(row >= flaggedItems.count) + if(row >= self.flaggedItems.count) { //bail goto bail; @@ -92,7 +109,7 @@ //create the view // ->inits row w/ all required info - rowView = createItemView(tableView, self, [flaggedItems objectAtIndex:row]); + rowView = createItemView(tableView, self, [self.flaggedItems objectAtIndex:row]); //bail bail: @@ -127,6 +144,28 @@ bail: return rowView; } +//automatically invoked when mouse entered +// ->highlight button +-(void)mouseEntered:(NSEvent*)theEvent +{ + //mouse entered + // ->highlight (visual) state + buttonAppearance(self.flaggedItemTable, theEvent, NO); + + return; +} + +//automatically invoked when mouse exits +// ->unhighlight/reset button +-(void)mouseExited:(NSEvent*)theEvent +{ + //mouse exited + // ->so reset button to original (visual) state + buttonAppearance(self.flaggedItemTable, theEvent, YES); + + return; +} + //invoked when the user clicks 'virus total' icon // ->launch browser and browse to virus total's page -(void)showVTInfo:(id)sender @@ -137,26 +176,20 @@ bail: //row NSInteger itemRow = 0; - //flagged items - NSMutableArray* flaggedItems = nil; - - //grab flagged items - flaggedItems = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems; - //grab sender's row itemRow = [self.flaggedItemTable rowForView:sender]; //sanity check(s) // ->make sure row is decent if( (-1 == itemRow) || - (itemRow >= flaggedItems.count) ) + (itemRow >= self.flaggedItems.count) ) { //bail goto bail; } //extract item for row - item = flaggedItems[itemRow]; + item = self.flaggedItems[itemRow]; //alloc/init info window vtWindowController = [[VTInfoWindowController alloc] initWithItem:item]; @@ -171,4 +204,104 @@ bail: return; } +//automatically invoked when user clicks the 'info' icon +// ->create/configure/display info window for task/dylib/file/etc +-(IBAction)showInfo:(id)sender +{ + //item + // ->task, dylib, file, etc + id item = nil; + + //row + NSInteger itemRow = 0; + + //grab sender's row + itemRow = [self.flaggedItemTable rowForView:sender]; + + //sanity check(s) + // ->make sure row is decent + if( (-1 == itemRow) || + (itemRow >= self.flaggedItems.count) ) + { + //bail + goto bail; + } + + //grab item + item = self.flaggedItems[itemRow]; + + //alloc/init info window + infoWindowController = [[InfoWindowController alloc] initWithItem:item]; + + //show it + [self.infoWindowController.windowController showWindow:self]; + +//bail +bail: + + return; +} + + +//automatically invoked when user clicks the 'show in finder' icon +// ->open Finder to show item +-(IBAction)showInFinder:(id)sender +{ + //item + // ->task, dylib, or file + id item = nil; + + //path to item + NSString* path = nil; + + //row + NSInteger itemRow = 0; + + //grab sender's row + itemRow = [self.flaggedItemTable rowForView:sender]; + + //sanity check(s) + // ->make sure row is decent + if( (-1 == itemRow) || + (itemRow >= self.flaggedItems.count) ) + { + //bail + goto bail; + } + + //extract item for row + item = self.flaggedItems[itemRow]; + + //tasks + // ->grab path from binary + if(YES == [item isKindOfClass:[Task class]]) + { + //get path + path = ((Task*)item).binary.path; + } + //dylib + // ->use as is, to extract path + else if(YES == [item isKindOfClass:[Binary class]]) + { + //get path + path = [item path]; + } + + //sanity check + if(nil == path) + { + //bail + goto bail; + } + + //open Finder + // ->will reveal binary + [[NSWorkspace sharedWorkspace] selectFile:path inFileViewerRootedAtPath:nil]; + + //bail +bail: + + return; +} + @end diff --git a/ItemView.m b/ItemView.m index 7cf0c2b..83aead8 100644 --- a/ItemView.m +++ b/ItemView.m @@ -14,7 +14,6 @@ #import "SearchWindowController.h" #import "3rdParty/OrderedDictionary.h" - //create customize item view NSTableCellView* createItemView(NSTableView* tableView, id owner, id item) { @@ -148,6 +147,7 @@ NSImage* getCodeSigningIcon(Binary* binary) } //create & customize global dylib/file view +// ->has 'loaded in...' string NSTableCellView* createLoadedItemView(NSTableView* tableView, id owner, id item) { //item cell @@ -210,8 +210,7 @@ NSTableCellView* createLoadedItemView(NSTableView* tableView, id owner, id item) //brand new cells need tracking areas // ->determine if new, by checking default (.xib/IB) value - if( (YES == [loadedItemCell.textField.stringValue isEqualToString:@"Task Name"]) || - (YES == [loadedItemCell.textField.stringValue isEqualToString:@"File Name"]) ) + if(YES == [loadedItemCell.textField.stringValue isEqualToString:@"Name"]) { //only dylibs have VT button if(YES == [item isKindOfClass:[Binary class]]) @@ -282,147 +281,6 @@ bail: return loadedItemCell; } - -//TODO: for search, each task is unique, so same for flagged tasks? YES, get args etc :) -//create & customize flagged item view -NSTableCellView* createFlaggedItemView(NSTableView* tableView, id owner, Binary* binary) -{ - //item cell - NSTableCellView* flaggedItemCell = nil; - - //matching or host tasks - NSMutableArray* tasks = nil; - - //pid or 'loaded in' string - NSMutableString* pidString = nil; - - //task's name frame - CGRect nameFrame = {0}; - - //for main (task) binaries - // ->just need task's pid - if(YES == binary.isTaskBinary) - { - //get all matching tasks - tasks = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator tasksForBinary:binary]; - - //start 'tasks: ...' str - pidString = [NSMutableString stringWithFormat:@"(tasks:"]; - - //add all tasks - for(Task* task in tasks) - { - //append name - [pidString appendFormat:@" %@,", task.pid]; - } - - //remove last ',' - if(YES == [pidString hasSuffix:@","]) - { - //remove - [pidString deleteCharactersInRange:NSMakeRange([pidString length]-1, 1)]; - } - - //terminate list/output - [pidString appendString:@")"]; - } - //for dylibs - // ->list all tasks the flagged dylib is loaded in - else - { - //get host tasks - tasks = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator loadedIn:binary]; - - //start 'loaded in: ...' str - pidString = [NSMutableString stringWithFormat:@"(loaded in:"]; - - //add all tasks - for(Task* task in tasks) - { - //append name - [pidString appendFormat:@" %@,", task.binary.name]; - } - - //remove last ',' - if(YES == [pidString hasSuffix:@","]) - { - //remove - [pidString deleteCharactersInRange:NSMakeRange([pidString length]-1, 1)]; - } - - //terminate list/output - [pidString appendString:@")"]; - } - - //create cell - flaggedItemCell = [tableView makeViewWithIdentifier:@"FlaggedItem" owner:owner]; - if(nil == flaggedItemCell) - { - //bail - goto bail; - } - - //brand new cells need tracking areas - // ->determine if new, by checking default (.xib/IB) value - if(YES == [flaggedItemCell.textField.stringValue isEqualToString:@"Flagged Item Name"]) - { - //add tracking area - // ->'vt' button - addTrackingArea(flaggedItemCell, TABLE_ROW_VT_BUTTON, owner); - - //add tracking area - // ->'info' button - addTrackingArea(flaggedItemCell, TABLE_ROW_INFO_BUTTON, owner); - - //add tracking area - // ->'show' button - addTrackingArea(flaggedItemCell, TABLE_ROW_SHOW_BUTTON, owner); - } - - //set icon - flaggedItemCell.imageView.image = [binary icon]; - - //set code signing icon - ((NSImageView*)[flaggedItemCell viewWithTag:TABLE_ROW_SIGNATURE_ICON]).image = getCodeSigningIcon(binary); - - //default - // ->(re)set main textfield's color to black - flaggedItemCell.textField.textColor = [NSColor blackColor]; - - //set main text - // ->name - [flaggedItemCell.textField setStringValue:binary.name]; - - //get name frame - nameFrame = flaggedItemCell.textField.frame; - - //adjust width to fit text - nameFrame.size.width = [flaggedItemCell.textField.stringValue sizeWithAttributes: @{NSFontAttributeName: flaggedItemCell.textField.font}].width + 5; - - //disable autolayout for name - flaggedItemCell.textField.translatesAutoresizingMaskIntoConstraints = YES; - - //update name frame - // ->should now be exact size of text - flaggedItemCell.textField.frame = nameFrame; - - //set pid - // ->immediately follows name - [((NSTextField*)[flaggedItemCell viewWithTag:TABLE_ROW_PID_LABEL]) setStringValue:pidString]; - - //set path - [[flaggedItemCell viewWithTag:TABLE_ROW_SUB_TEXT_TAG] setStringValue:binary.path]; - - //config VT button - configVTButton(flaggedItemCell, owner, binary); - -//bail -bail: - - return flaggedItemCell; -} - - //create & customize Task view NSTableCellView* createTaskView(NSTableView* tableView, id owner, Task* task) { @@ -449,7 +307,8 @@ NSTableCellView* createTaskView(NSTableView* tableView, id owner, Task* task) //brand new cells need tracking areas // ->determine if new, by checking default (.xib/IB) value - if(YES == [taskCell.textField.stringValue isEqualToString:@"Task Name"]) + if( (YES == [taskCell.textField.stringValue isEqualToString:@"Task Name"]) || + (YES == [taskCell.textField.stringValue isEqualToString:@"Name"]) ) { //add tracking area // ->'vt' button diff --git a/SearchWindowController.h b/SearchWindowController.h index 746e25f..e61d44a 100644 --- a/SearchWindowController.h +++ b/SearchWindowController.h @@ -42,6 +42,7 @@ @property(retain, nonatomic)InfoWindowController* infoWindowController; +//TODO: remove! //tasks @property(nonatomic, retain)NSMutableDictionary* tasks; diff --git a/SearchWindowController.m b/SearchWindowController.m index 7fab5c8..cd0b0b5 100644 --- a/SearchWindowController.m +++ b/SearchWindowController.m @@ -15,7 +15,8 @@ #import "ItemView.h" #import "KKRow.h" #import "Filter.h" -#import "SearchResult.h" +#import "SearchResult.h"] +#import "Utilities.h" @implementation SearchWindowController @@ -49,6 +50,8 @@ return; } +//TODO: center window each time? + /* //automatically invoked when window is loaded // ->set to white @@ -71,6 +74,7 @@ } */ +//TODO: delete/remove //automatically invoked when window is closing // ->make ourselves unmodal -(void)windowWillClose:(NSNotification *)notification @@ -82,7 +86,7 @@ } //init/prepare -// ->make sure everything is cleanly init'd +// ->make sure everything is cleanly init'd before displaying -(void)prepare { //init filter obj @@ -233,9 +237,32 @@ bail: return rowView; } + +//automatically invoked when mouse entered +// ->highlight button +-(void)mouseEntered:(NSEvent*)theEvent +{ + //mouse entered + // ->highlight (visual) state + buttonAppearance(self.searchTable, theEvent, NO); + + return; +} + +//automatically invoked when mouse exits +// ->unhighlight/reset button +-(void)mouseExited:(NSEvent*)theEvent +{ + //mouse exited + // ->so reset button to original (visual) state + buttonAppearance(self.searchTable, theEvent, YES); + + return; +} + //automatically invoked when user presses 'Enter' in search box // ->search! -- (IBAction)search:(id)sender +-(IBAction)search:(id)sender { //search string NSString* searchString = nil; @@ -252,12 +279,12 @@ bail: //matching dylibs NSMutableDictionary* matchingDylibs = nil; + //matching files + NSMutableDictionary* matchingFiles = nil; + //task Task* task = nil; - //search result - //SearchResult* searchResult = nil; - //alloc array for matching items matchingItems = [NSMutableArray array]; @@ -267,6 +294,9 @@ bail: //alloc dictionary for matching dylibs matchingDylibs = [NSMutableDictionary dictionary]; + //alloc dictionary for matching files + matchingFiles = [NSMutableDictionary dictionary]; + //reset search results [self.searchResults removeAllObjects]; @@ -288,24 +318,16 @@ bail: } } - //first; search for all matching tasks + //1st: search for all matching tasks [self.filterObj filterTasks:searchString items:allTasks results:matchingTasks]; - //TODO: don't need for loop for this? - //convert/save into search result objects - for(Task* task in matchingTasks) - { - //init - //searchResult = [[SearchResult alloc] initWithItem:task]; - - //add to table items - [self.searchResults addObject:task]; - } + //add all tasks + [self.searchResults addObjectsFromArray:matchingTasks]; //refresh table to display task matches [self.searchTable reloadData]; - //second; search for all matching dylibs + //2nd: search for all matching dylibs //sync @synchronized(allTasks) { @@ -345,6 +367,45 @@ bail: //refresh table to display dylib [self.searchTable reloadData]; + //3rd: search for all matching files + //sync + @synchronized(allTasks) + { + //walk all tasks + // ->scan each for file matches, only processing first match + for(NSNumber* taskPid in allTasks) + { + //extract task + task = allTasks[taskPid]; + + //filter + [self.filterObj filterFiles:searchString items:task.files results:matchingItems]; + + //process all matching dylibs + // ->but first check if processed due to matching in another task already + for(File* file in matchingItems) + { + //ignore if already seen/processed + if(nil != matchingFiles[file.path]) + { + //skip + continue; + } + + //process + [self.searchResults addObject:file]; + + //save + matchingFiles[file.path] = file; + } + + }//all tasks + + }//sync + + //refresh table to display dylib + [self.searchTable reloadData]; + //bail bail: @@ -510,7 +571,7 @@ bail: //show it [self.infoWindowController.windowController showWindow:self]; - //bail +//bail bail: return; diff --git a/TaskEnumerator.m b/TaskEnumerator.m index c62b111..c1412b8 100644 --- a/TaskEnumerator.m +++ b/TaskEnumerator.m @@ -70,9 +70,15 @@ //new tasks OrderedDictionary* newTasks = nil; + //xpc connection + NSXPCConnection* xpcConnection = nil; + //determine if network is connected // ->sets 'isConnected' flag ((AppDelegate*)[[NSApplication sharedApplication] delegate]).isConnected = isNetworkConnected(); + + //get xpc connection + xpcConnection = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).xpcConnection; //get all tasks // ->pids and binary obj with just path/name @@ -170,13 +176,25 @@ }//signing info for all new tasks //begin dylib enumeration + // ->this is really just for search/filter views since they are re-gen'd per task on each bottom-pane click for(NSNumber* key in newTasks) { //get task newTask = newTasks[key]; //enumerate - [newTask enumerateDylibs:((AppDelegate*)[[NSApplication sharedApplication] delegate]).xpcConnection allDylibs:self.dylibs]; + [newTask enumerateDylibs:xpcConnection allDylibs:self.dylibs]; + } + + //begin file enumeration + // ->for search view + for(NSNumber* key in newTasks) + { + //get task + newTask = newTasks[key]; + + //enumerate + [newTask enumerateFiles:xpcConnection]; } return; @@ -464,12 +482,12 @@ bail: } //get all tasks that host the flagged dylib - taskHosts = [self tasksForBinary:dylib]; + taskHosts = [self loadedIn:dylib]; //skip dylibs that are hosted in more than one task // or aren't hosted in dead task if( (1 != taskHosts.count) || - (taskHosts.firstObject != deadTask.binary) ) + (taskHosts.firstObject != deadTask.binary) ) { //skip continue; @@ -592,7 +610,7 @@ bail: //sync @synchronized(self.tasks) { - //reload each row w/ new VT info + //iterate over all tasks for(NSNumber* taskPid in self.tasks) { //extract task diff --git a/TaskTableController.m b/TaskTableController.m index cb54279..5aff982 100644 --- a/TaskTableController.m +++ b/TaskTableController.m @@ -274,120 +274,23 @@ bail: //automatically invoked when mouse entered +// ->highlight button -(void)mouseEntered:(NSEvent*)theEvent { //mouse entered // ->highlight (visual) state - [self buttonAppearance:theEvent shouldReset:NO]; + buttonAppearance(self.itemView, theEvent, NO); return; } -//automaticall invoked when mouse exits +//automatically invoked when mouse exits +// ->unhighlight/reset button -(void)mouseExited:(NSEvent*)theEvent { //mouse exited // ->so reset button to original (visual) state - [self buttonAppearance:theEvent shouldReset:YES]; - - return; -} - -//set or unset button's highlight --(void)buttonAppearance:(NSEvent*)theEvent shouldReset:(BOOL)shouldReset -{ - //mouse point - NSPoint mousePoint = {0}; - - //row index - NSUInteger rowIndex = -1; - - //current row - NSTableCellView* currentRow = nil; - - //tag - NSUInteger tag = 0; - - //button - NSButton* button = nil; - - //button's label - NSTextField* label = nil; - - //image name - NSString* imageName = nil; - - //extract tag - tag = [((NSDictionary*)theEvent.userData)[@"tag"] unsignedIntegerValue]; - - //restore button back to default (visual) state - if(YES == shouldReset) - { - //set image name - // ->'info' - if(TABLE_ROW_INFO_BUTTON == tag) - { - //set - imageName = @"info"; - } - //set image name - // ->'info' - else if(TABLE_ROW_SHOW_BUTTON == tag) - { - //set - imageName = @"show"; - } - } - //highlight button - else - { - //set image name - // ->'info' - if(TABLE_ROW_INFO_BUTTON == tag) - { - //set - imageName = @"infoOver"; - } - //set image name - // ->'info' - else if(TABLE_ROW_SHOW_BUTTON == tag) - { - //set - imageName = @"showOver"; - } - } - - //grab mouse point - mousePoint = [self.itemView convertPoint:[theEvent locationInWindow] fromView:nil]; - - //compute row indow - rowIndex = [self.itemView rowAtPoint:mousePoint]; - - //sanity check - if(-1 == rowIndex) - { - //bail - goto bail; - } - - //get row that's about to be selected - currentRow = [self.itemView viewAtColumn:0 row:rowIndex makeIfNecessary:YES]; - - //get button - // ->tag id of button, passed in userData var - button = [currentRow viewWithTag:[((NSDictionary*)theEvent.userData)[@"tag"] unsignedIntegerValue]]; - label = [currentRow viewWithTag: 1 + [((NSDictionary*)theEvent.userData)[@"tag"] unsignedIntegerValue]]; - - //restore default button image - // ->for 'info' and 'show' buttons - if(nil != imageName) - { - //set image - [button setImage:[NSImage imageNamed:imageName]]; - } - -//bail -bail: + buttonAppearance(self.itemView, theEvent, YES); return; } diff --git a/UI/FlaggedItems.xib b/UI/FlaggedItems.xib index 3ebd0d8..479a8f9 100644 --- a/UI/FlaggedItems.xib +++ b/UI/FlaggedItems.xib @@ -1,5 +1,5 @@ - + @@ -17,7 +17,7 @@ - + @@ -71,7 +71,7 @@ - + @@ -87,6 +87,9 @@ + + + @@ -141,6 +144,9 @@ + + + @@ -155,9 +161,8 @@ - + - @@ -170,6 +175,7 @@ + diff --git a/UI/SearchWindow.xib b/UI/SearchWindow.xib index b1238a9..1497740 100644 --- a/UI/SearchWindow.xib +++ b/UI/SearchWindow.xib @@ -1,5 +1,5 @@ - + @@ -72,7 +72,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -162,11 +162,11 @@ - + - + @@ -185,6 +185,7 @@ + @@ -203,9 +204,9 @@ - + - + @@ -239,35 +240,12 @@ - + - - - - - - - - - - - - -