379 lines
11 KiB
Objective-C
379 lines
11 KiB
Objective-C
/*
|
|
* Copyright 2022 Anthony Cohn-Richardby <anthonyc@gmx.co.uk>
|
|
*
|
|
* This file is part of NetSurf, http://www.netsurf-browser.org/
|
|
*
|
|
* NetSurf is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* NetSurf is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
#import <stdio.h>
|
|
#import <string.h>
|
|
#import <errno.h>
|
|
|
|
#import "Website.h"
|
|
#import "AppDelegate.h"
|
|
|
|
@interface Website(Private)
|
|
+(void)zeroHistoryFileAtPath: (NSString*)path olderThanDate: (NSDate*)date;
|
|
@end
|
|
|
|
@implementation Website
|
|
|
|
-(id)initWithName: (NSString*)aName url: (NSString*)aUrl {
|
|
if (self = [super init]) {
|
|
int nlen = [aName length];
|
|
int urlen = [aUrl length];
|
|
data = malloc(sizeof (struct website_data) + nlen + urlen);
|
|
data->len_name = nlen;
|
|
data->len_url = urlen;
|
|
data->timeIntervalSinceReferenceDate = [[NSDate date]
|
|
timeIntervalSinceReferenceDate];
|
|
memcpy(data->data, [aName cString], nlen);
|
|
memcpy(data->data + nlen, [aUrl cString], urlen);
|
|
fileOffset = -1;
|
|
filename = nil;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
-(id)initWithData: (struct website_data*)someData atFileOffset: (long)aFileOffset {
|
|
if (self = [super init]) {
|
|
data = someData;
|
|
fileOffset = aFileOffset;
|
|
filename = nil;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
-(id)initWithDictionary: (NSDictionary*)aDictionary fromFileNamed: (NSString*)aFilename {
|
|
NSString *aName = [aDictionary objectForKey: @"name"];
|
|
NSString *aUrl = [aDictionary objectForKey: @"url"];
|
|
if ([self initWithName: aName url: aUrl] != nil) {
|
|
filename = [aFilename retain];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
-(void)dealloc {
|
|
free(data);
|
|
[parentFolder release];
|
|
[filename release];
|
|
[super dealloc];
|
|
}
|
|
|
|
-(NSString*)name {
|
|
return [NSString stringWithCString: data->data length: data->len_name];
|
|
}
|
|
|
|
-(NSString*)url {
|
|
return [NSString stringWithCString: data->data + data->len_name length:
|
|
data->len_url];
|
|
}
|
|
|
|
-(NSDate*)dateViewed {
|
|
return [NSDate dateWithTimeIntervalSinceReferenceDate:
|
|
data->timeIntervalSinceReferenceDate];
|
|
}
|
|
|
|
-(void)setName: (NSString*)aName {
|
|
NSString *url = [self url];
|
|
int nlen = [aName length];
|
|
int urlen = data->len_url;
|
|
data = realloc(data, sizeof (struct website_data) + nlen + urlen);
|
|
data->len_name = nlen;
|
|
data->len_url = urlen;
|
|
memcpy(data->data, [aName cString], nlen);
|
|
memcpy(data->data + nlen, [url cString], urlen);
|
|
fileOffset = -1;
|
|
}
|
|
|
|
-(NSString*)filename {
|
|
return filename;
|
|
}
|
|
-(void)setFilename: (NSString*)aFilename {
|
|
[filename autorelease];
|
|
filename = [aFilename retain];
|
|
}
|
|
|
|
// Set when init from bookmarks or added to folder
|
|
-(BookmarkFolder*)parentFolder {
|
|
return parentFolder;
|
|
}
|
|
|
|
-(void)setParentFolder: (BookmarkFolder*)aBookmarkFolder {
|
|
[parentFolder release];
|
|
parentFolder = [aBookmarkFolder retain];
|
|
}
|
|
|
|
-(NSDictionary*)asDictionary {
|
|
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
|
[dict setObject: [self name] forKey: @"name"];
|
|
[dict setObject: [self url] forKey: @"url"];
|
|
return dict;
|
|
}
|
|
|
|
-(long)fileOffset {
|
|
return fileOffset;
|
|
}
|
|
|
|
-(void)open {
|
|
[[NSApp delegate] openWebsite: self];
|
|
}
|
|
|
|
-(Website*)copy {
|
|
Website *website = [[Website alloc] initWithName: [self name] url: [self url]];
|
|
[website autorelease];
|
|
return website;
|
|
}
|
|
// MARK: - History implementation
|
|
|
|
-(void)addToHistory {
|
|
static NSString *path = nil;
|
|
if (path == nil) {
|
|
NSCalendarDate *date = [NSCalendarDate calendarDate];
|
|
int month = [date monthOfYear];
|
|
int year = [date yearOfCommonEra];
|
|
NSString *dir = [NSHomeDirectory() stringByAppendingPathComponent:
|
|
HISTORY_PATH];
|
|
NSError *err;
|
|
[[NSFileManager defaultManager] createDirectoryAtPath: dir
|
|
withIntermediateDirectories: YES attributes: nil error: &err];
|
|
path = [[NSString alloc] initWithFormat: @"%@/history_%d_%02d", dir, year,
|
|
month];
|
|
}
|
|
FILE *f = fopen([path cString], "a");
|
|
if (f != NULL) {
|
|
int len = sizeof (struct website_data) + data->len_url + data->len_name;
|
|
fwrite(data, len, 1, f);
|
|
fclose(f);
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:
|
|
WebsiteHistoryUpdatedNotificationName object: self];
|
|
}
|
|
|
|
+(NSMutableArray*)getHistoryFromFile: (NSString*)file matching: (NSString*)queryString {
|
|
size_t nread, wsize;
|
|
long fileoff;
|
|
int lens[2];
|
|
NSString *historyRoot = [NSString stringWithFormat: @"%@/%@", NSHomeDirectory(),
|
|
HISTORY_PATH];
|
|
NSString *path = [historyRoot stringByAppendingPathComponent: file];
|
|
FILE *f = fopen([path cString], "r");
|
|
struct website_data *wdata;
|
|
Website *website;
|
|
NSMutableArray *ret = [NSMutableArray array];
|
|
|
|
if (f == NULL) {
|
|
NSLog(@"Error opening file: %@", path);
|
|
return ret;
|
|
}
|
|
NSLog(@"Parsing file: %@", path);
|
|
fileoff = 0;
|
|
while (1) {
|
|
if ((nread = fread(lens, sizeof (int), 2, f)) < 2) {
|
|
break;
|
|
}
|
|
wsize = lens[0] + lens[1] + sizeof (struct website_data);
|
|
// 0 Value of url_len implies this has been cleared. Skip.
|
|
if (lens[1] == 0) {
|
|
fseek(f, wsize - (nread * sizeof (int)), SEEK_CUR);
|
|
continue;
|
|
}
|
|
// Else it's valid, rewind and read the whole structure in.
|
|
fseek(f, -nread * sizeof (int), SEEK_CUR);
|
|
wdata = malloc(wsize);
|
|
if (wdata == NULL) {
|
|
perror("NULL MALLOC?!");
|
|
return ret;
|
|
}
|
|
fread(wdata, wsize, 1, f);
|
|
website = [[[Website alloc] initWithData: wdata atFileOffset: fileoff]
|
|
autorelease];
|
|
// If there's a search value set, skip non-matching websites.
|
|
if (queryString == nil || [[website name] rangeOfString: queryString options:
|
|
NSCaseInsensitiveSearch].location != NSNotFound) {
|
|
[ret addObject: website];
|
|
}
|
|
fileoff = ftell(f);
|
|
}
|
|
fclose(f);
|
|
return ret;
|
|
}
|
|
|
|
+(NSArray*)getAllHistoryFiles {
|
|
NSString *path = [NSString stringWithFormat: @"%@/%@", NSHomeDirectory(),
|
|
HISTORY_PATH];
|
|
NSError *error = nil;
|
|
NSPredicate *historyPredicate = [NSPredicate predicateWithFormat:
|
|
@"count >= 8 AND self beginswith 'history_'"];
|
|
NSMutableArray *files = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:
|
|
path error: &error] mutableCopy];
|
|
if (error != nil) {
|
|
NSLog(@"Error fetching files in history dir: %@", path);
|
|
return [NSArray array];
|
|
}
|
|
[files filterUsingPredicate: historyPredicate];
|
|
[files sortUsingSelector: @selector(caseInsensitiveCompare:)];
|
|
return files;
|
|
}
|
|
|
|
+(void)deleteHistoryOlderThanDays: (NSUInteger)days {
|
|
NSCalendarDate *now = [NSCalendarDate date];
|
|
NSCalendarDate *deletionThreshold = [now dateByAddingYears: 0 months: 0
|
|
days: -days hours: 0 minutes: 0 seconds: 0];
|
|
[Website deleteHistoryOlderThanDate: deletionThreshold];
|
|
}
|
|
|
|
+(void)deleteHistoryOlderThanDate: (NSDate*)date {
|
|
// Get month&year for month
|
|
NSCalendarDate *calendarDate = [NSCalendarDate
|
|
dateWithTimeIntervalSinceReferenceDate:
|
|
[date timeIntervalSinceReferenceDate]];
|
|
NSInteger targetMonth = [calendarDate monthOfYear];
|
|
NSInteger targetYear = [calendarDate yearOfCommonEra];
|
|
// Iterate through all history files & delete older
|
|
NSArray *historyFiles = [Website getAllHistoryFiles];
|
|
NSArray *yearAndDate;
|
|
NSEnumerator *fileEnumerator = [historyFiles objectEnumerator];
|
|
NSString *filename, *fullPath;
|
|
NSInteger fileYear, fileMonth;
|
|
BOOL isFileOld;
|
|
NSError *err = nil;
|
|
NSString *historyPath = [NSString stringWithFormat: @"%@/%@",
|
|
NSHomeDirectory(), HISTORY_PATH];
|
|
while ((filename = [fileEnumerator nextObject]) != nil) {
|
|
yearAndDate = [[filename substringFromIndex: 8]
|
|
componentsSeparatedByString: @"_"];
|
|
fileYear = [[yearAndDate firstObject] integerValue];
|
|
fileMonth = [[yearAndDate objectAtIndex: 1] integerValue];
|
|
isFileOld = fileYear < targetYear || (fileYear == targetYear &&
|
|
fileMonth < targetMonth);
|
|
if (isFileOld) {
|
|
fullPath = [NSString stringWithFormat: @"%@/%@", historyPath,
|
|
filename];
|
|
err = nil;
|
|
[[NSFileManager defaultManager] removeItemAtPath: fullPath
|
|
error: &err];
|
|
if (err != nil)
|
|
NSLog(@"Error removing file at: %@", fullPath);
|
|
}
|
|
}
|
|
// Open history file for current month or done if not exist
|
|
NSString *currentMonth = [historyFiles lastObject];
|
|
yearAndDate = [[currentMonth substringFromIndex: 8]
|
|
componentsSeparatedByString: @"_"];
|
|
fileYear = [[yearAndDate firstObject] integerValue];
|
|
fileMonth = [[yearAndDate objectAtIndex: 1] integerValue];
|
|
if (fileYear == targetYear || fileMonth == targetMonth) {
|
|
fullPath = [NSString stringWithFormat: @"%@/%@", historyPath,
|
|
currentMonth];
|
|
char *cTmpPath = tempnam(NULL, NULL);
|
|
NSString *tmpPath = [NSString stringWithCString: cTmpPath];
|
|
NSLog(@"Copying file from '%@', to '%@'", fullPath, tmpPath);
|
|
err = nil;
|
|
[[NSFileManager defaultManager] copyItemAtPath: fullPath toPath: tmpPath
|
|
error: &err];
|
|
if (err != nil) {
|
|
NSLog(@"Error copying history to temp file at path: %@ (%@)", tmpPath, err);
|
|
return;
|
|
}
|
|
[Website zeroHistoryFileAtPath: tmpPath olderThanDate: date];
|
|
err = nil;
|
|
[[NSFileManager defaultManager] removeItemAtPath: fullPath error: &err];
|
|
if (err != nil) {
|
|
NSLog(@"Failed to remove existing history file at path: %@", fullPath);
|
|
return;
|
|
}
|
|
[[NSFileManager defaultManager] moveItemAtPath: tmpPath toPath: fullPath
|
|
error: &err];
|
|
if (err != nil) {
|
|
NSLog(@"Failed to copy back the tmp file at %@, to path %@", tmpPath,
|
|
fullPath);
|
|
} else {
|
|
NSLog(@"Copied history file back in place");
|
|
}
|
|
}
|
|
}
|
|
|
|
+(void)zeroHistoryFileAtPath: (NSString*)path olderThanDate: (NSDate*)date {
|
|
NSFileHandle *handle = [NSFileHandle fileHandleForUpdatingAtPath: path];
|
|
if (handle == nil) {
|
|
NSLog(@"Failed to open history file for updating at path: %@", path);
|
|
return;
|
|
}
|
|
NSTimeInterval ival = [date timeIntervalSinceReferenceDate];
|
|
NSLog(@"clear history older than: %f", ival);
|
|
NSData *data;
|
|
NSError *err = nil;
|
|
BOOL foundEnd = NO;
|
|
int totalSize, largestSize;
|
|
largestSize = 100;
|
|
struct website_data *wdata = malloc(largestSize);
|
|
do {
|
|
data = [handle readDataOfLength: offsetof(struct website_data, data)];
|
|
if (data == nil || [data length] == 0) {
|
|
NSLog(@"readDataUpToLength 1 failed (EOF?)");
|
|
break;
|
|
}
|
|
[data getBytes: wdata length: offsetof(struct website_data, data)];
|
|
|
|
data = [handle readDataOfLength: wdata->len_name + wdata->len_url];
|
|
if (data == nil || [data length] == 0) {
|
|
NSLog(@"readDataUpToLength 2 failed (EOF?)");
|
|
break;
|
|
}
|
|
totalSize = sizeof (struct website_data) + wdata->len_name + wdata->len_url;
|
|
if (totalSize > largestSize) {
|
|
wdata = realloc(wdata, totalSize);
|
|
largestSize = totalSize;
|
|
}
|
|
[data getBytes: &(wdata->data) length: wdata->len_name + wdata->len_url];
|
|
if (wdata->len_url == 0) {
|
|
// Deleted entry, skip.
|
|
continue;
|
|
}
|
|
if (wdata->timeIntervalSinceReferenceDate > ival) {
|
|
NSLog(@"End found");
|
|
foundEnd = YES;
|
|
break;
|
|
}
|
|
} while (!foundEnd);
|
|
free(wdata);
|
|
|
|
if (!foundEnd) {
|
|
NSLog(@"Reached the end of history file without finding newer entries. Clear entire file.");
|
|
[handle truncateFileAtOffset: 0];
|
|
return;
|
|
}
|
|
unsigned long long offset = [handle offsetInFile];
|
|
if (err != nil) {
|
|
NSLog(@"Failed to get file offset");
|
|
return;
|
|
}
|
|
offset -= (unsigned long long)totalSize;
|
|
if (offset == 0) {
|
|
NSLog(@"No entries need clearing.");
|
|
return;
|
|
}
|
|
wdata = calloc(1, offset);
|
|
wdata->len_name = offset - sizeof (struct website_data);
|
|
wdata->len_url = 0;
|
|
[handle seekToFileOffset: 0];
|
|
[handle writeData: [NSData dataWithBytesNoCopy: wdata length: offset]];
|
|
[handle closeFile];
|
|
NSLog(@"Zero'd %@ to %llu", path, offset);
|
|
}
|
|
@end
|