mirror of
https://github.com/ViennaRSS/vienna-rss.git
synced 2026-04-07 19:27:39 +00:00
1267 lines
55 KiB
Objective-C
1267 lines
55 KiB
Objective-C
//
|
|
// RefreshManager.m
|
|
// Vienna
|
|
//
|
|
// Created by Steve on 7/19/05.
|
|
// Copyright (c) 2004-2018 Steve Palmer and Vienna contributors (see menu item 'About Vienna' for list of contributors). All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
#import "RefreshManager.h"
|
|
|
|
@import os.log;
|
|
|
|
#import "FeedCredentials.h"
|
|
#import "ActivityItem.h"
|
|
#import "ActivityLog.h"
|
|
#import "StringExtensions.h"
|
|
#import "Preferences.h"
|
|
#import "Constants.h"
|
|
#import "OpenReader.h"
|
|
#import "NSNotificationAdditions.h"
|
|
#import "Article.h"
|
|
#import "Folder.h"
|
|
#import "Database.h"
|
|
#import "TRVSURLSessionOperation.h"
|
|
#import "URLRequestExtensions.h"
|
|
#import "Vienna-Swift.h"
|
|
#import "XMLFeed.h"
|
|
#import "XMLFeedParser.h"
|
|
|
|
typedef NS_ENUM (NSInteger, Redirect301Status) {
|
|
HTTP301Unknown = 0,
|
|
HTTP301Pending,
|
|
HTTP301Unsafe,
|
|
HTTP301Safe
|
|
};
|
|
|
|
#define VNA_LOG os_log_create("--", "RefreshManager")
|
|
|
|
@interface RefreshManager ()
|
|
|
|
@property (readwrite, copy) NSString * statusMessage;
|
|
@property (nonatomic) NSTimer * unsafe301RedirectionTimer;
|
|
@property (copy) NSString * riskyIPAddress;
|
|
@property (nonatomic) Redirect301Status redirect301Status;
|
|
@property (nonatomic) NSMutableArray * redirect301WaitQueue;
|
|
@property (nonatomic, readonly) NSURLSession * urlSession;
|
|
|
|
-(BOOL)isRefreshingFolder:(Folder *)folder ofType:(RefreshTypes)type;
|
|
-(void)getCredentialsForFolder;
|
|
-(void)setFolderErrorFlag:(Folder *)folder flag:(BOOL)theFlag;
|
|
-(void)setFolderUpdatingFlag:(Folder *)folder flag:(BOOL)theFlag;
|
|
-(void)pumpSubscriptionRefresh:(Folder *)folder shouldForceRefresh:(BOOL)force;
|
|
-(void)pumpFolderIconRefresh:(Folder *)folder;
|
|
-(void)refreshFeed:(Folder *)folder fromURL:(NSURL *)url withLog:(ActivityItem *)aItem shouldForceRefresh:(BOOL)force;
|
|
-(NSString *)getRedirectURL:(NSData *)data;
|
|
-(void)syncFinishedForFolder:(Folder *)folder;
|
|
|
|
@end
|
|
|
|
@implementation RefreshManager
|
|
|
|
+(void)initialize
|
|
{
|
|
}
|
|
|
|
|
|
/* init
|
|
* Initialise the class.
|
|
*/
|
|
-(instancetype)init
|
|
{
|
|
if ((self = [super init]) != nil) {
|
|
countOfNewArticles = 0;
|
|
authQueue = [[NSMutableArray alloc] init];
|
|
statusMessageDuringRefresh = nil;
|
|
networkQueue = [[NSOperationQueue alloc] init];
|
|
networkQueue.name = @"VNAHTTPSession queue";
|
|
networkQueue.maxConcurrentOperationCount = [[Preferences standardPreferences] integerForKey:MAPref_ConcurrentDownloads];
|
|
NSString * osVersion;
|
|
NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion;
|
|
osVersion = [NSString stringWithFormat:@"%ld_%ld_%ld", version.majorVersion, version.minorVersion, version.patchVersion];
|
|
Preferences * prefs = [Preferences standardPreferences];
|
|
NSString *name = prefs.userAgentName;
|
|
|
|
NSString * userAgent = [NSString stringWithFormat:MA_DefaultUserAgentString, name, [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], osVersion];
|
|
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
config.timeoutIntervalForResource = 300;
|
|
config.URLCache = nil;
|
|
config.HTTPAdditionalHeaders = @{@"User-Agent": userAgent};
|
|
config.HTTPMaximumConnectionsPerHost = 6;
|
|
config.HTTPShouldUsePipelining = YES;
|
|
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
|
|
|
|
NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
|
|
[nc addObserver:self selector:@selector(handleGotAuthenticationForFolder:) name:MA_Notify_GotAuthenticationForFolder object:nil];
|
|
[nc addObserver:self selector:@selector(handleCancelAuthenticationForFolder:) name:MA_Notify_CancelAuthenticationForFolder
|
|
object:nil];
|
|
[nc addObserver:self selector:@selector(handleWillDeleteFolder:) name:VNADatabaseWillDeleteFolderNotification object:nil];
|
|
[nc addObserver:self selector:@selector(handleChangeConcurrentDownloads:) name:MA_Notify_ConcurrentDownloadsChange object:nil];
|
|
// be notified on system wake up after sleep
|
|
[[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(handleDidWake:)
|
|
name:@"NSWorkspaceDidWakeNotification" object:nil];
|
|
_queue = dispatch_queue_create("uk.co.opencommunity.vienna2.refresh", NULL);
|
|
_redirect301WaitQueue = [[NSMutableArray alloc] init];
|
|
hasStarted = NO;
|
|
}
|
|
return self;
|
|
} // init
|
|
|
|
/* sharedManager
|
|
* Returns the single instance of the refresh manager.
|
|
*/
|
|
+(RefreshManager *)sharedManager
|
|
{
|
|
// Singleton
|
|
static RefreshManager * _refreshManager = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
_refreshManager = [[RefreshManager alloc] init];
|
|
});
|
|
return _refreshManager;
|
|
}
|
|
|
|
-(void)handleChangeConcurrentDownloads:(NSNotification *)nc
|
|
{
|
|
NSLog(@"Handling new downloads count");
|
|
networkQueue.maxConcurrentOperationCount = [[Preferences standardPreferences] integerForKey:MAPref_ConcurrentDownloads];
|
|
}
|
|
|
|
/* handleWillDeleteFolder
|
|
* Trap the notification that is broadcast just before a folder is being deleted.
|
|
* We use this to remove that folder from the refresh queue, if it is present, and
|
|
* interrupt a connection on that folder. Otherwise our retain on the folder will
|
|
* prevent it from being fully released until the end of the refresh by which time
|
|
* the folder list pane will probably have completed its post delete update.
|
|
*/
|
|
-(void)handleWillDeleteFolder:(NSNotification *)nc
|
|
{
|
|
Folder * folder = [[Database sharedManager] folderFromID:[nc.object integerValue]];
|
|
if (folder != nil) {
|
|
for (TRVSURLSessionOperation *theRequest in networkQueue.operations) {
|
|
NSMutableURLRequest *urlRequest = (NSMutableURLRequest *)(theRequest.task.originalRequest);
|
|
if (((NSDictionary *)[urlRequest vna_userInfo])[@"folder"] == folder) {
|
|
[theRequest.task cancel];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
-(void)handleDidWake:(NSNotification *)nc
|
|
{
|
|
NSString * currentAddress = [NSHost currentHost].address;
|
|
if (![currentAddress isEqualToString:self.riskyIPAddress]) {
|
|
// we might have moved to a new network
|
|
// so, at the next occurence we should test if we can safely handle
|
|
// 301 redirects
|
|
self.redirect301Status = HTTP301Unknown;
|
|
[self.unsafe301RedirectionTimer invalidate];
|
|
self.unsafe301RedirectionTimer = nil;
|
|
}
|
|
}
|
|
|
|
/* handleGotAuthenticationForFolder [delegate]
|
|
* Called when somewhere just provided us the needed authentication for the specified
|
|
* folder. Note that we don't know if the authentication is valid yet - just that a
|
|
* user name and password has been provided.
|
|
*/
|
|
-(void)handleGotAuthenticationForFolder:(NSNotification *)nc
|
|
{
|
|
Folder * folder = (Folder *)nc.object;
|
|
[[Database sharedManager] clearFlag:VNAFolderFlagNeedCredentials forFolder:folder.itemId];
|
|
[authQueue removeObject:folder];
|
|
[self refreshSubscriptions:@[folder] ignoringSubscriptionStatus:YES];
|
|
|
|
// Get the next one in the queue, if any
|
|
[self getCredentialsForFolder];
|
|
}
|
|
|
|
/* handleCancelAuthenticationForFolder
|
|
* Called when somewhere cancelled our request to authenticate the specified
|
|
* folder.
|
|
*/
|
|
-(void)handleCancelAuthenticationForFolder:(NSNotification *)nc
|
|
{
|
|
Folder * folder = (Folder *)nc.object;
|
|
[authQueue removeObject:folder];
|
|
|
|
// Get the next one in the queue, if any
|
|
[self getCredentialsForFolder];
|
|
}
|
|
|
|
-(void)forceRefreshSubscriptionForFolders:(NSArray *)foldersArray
|
|
{
|
|
statusMessageDuringRefresh = NSLocalizedString(@"Forcing Refresh subscriptions…", nil);
|
|
|
|
for (Folder * folder in foldersArray) {
|
|
if (folder.type == VNAFolderTypeGroup) {
|
|
[self forceRefreshSubscriptionForFolders:[[Database sharedManager] arrayOfFolders:folder.itemId]];
|
|
} else if (folder.type == VNAFolderTypeOpenReader) {
|
|
if (![self isRefreshingFolder:folder ofType:MA_Refresh_GoogleFeed] &&
|
|
![self isRefreshingFolder:folder ofType:MA_ForceRefresh_Google_Feed])
|
|
{
|
|
[self pumpSubscriptionRefresh:folder shouldForceRefresh:YES];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* refreshSubscriptions
|
|
* Add the folders specified in the foldersArray to the refresh queue.
|
|
*/
|
|
-(void)refreshSubscriptions:(NSArray *)foldersArray ignoringSubscriptionStatus:(BOOL)ignoreSubStatus
|
|
{
|
|
statusMessageDuringRefresh = NSLocalizedString(@"Refreshing subscriptions…", nil);
|
|
|
|
for (Folder * folder in foldersArray) {
|
|
if (folder.isGroupFolder) {
|
|
[self refreshSubscriptions:[[Database sharedManager] arrayOfFolders:folder.itemId] ignoringSubscriptionStatus:NO];
|
|
} else if (folder.isRSSFolder) {
|
|
if ((!folder.isUnsubscribed || ignoreSubStatus) && ![self isRefreshingFolder:folder ofType:MA_Refresh_Feed]) {
|
|
[self pumpSubscriptionRefresh:folder shouldForceRefresh:NO];
|
|
}
|
|
} else if (folder.isOpenReaderFolder) {
|
|
if ((!folder.isUnsubscribed || ignoreSubStatus) && ![self isRefreshingFolder:folder ofType:MA_Refresh_GoogleFeed] &&
|
|
![self isRefreshingFolder:folder ofType:MA_ForceRefresh_Google_Feed])
|
|
{
|
|
// we depend of pieces of info gathered by loadSubscriptions
|
|
NSOperation * op = [NSBlockOperation blockOperationWithBlock:^(void) {
|
|
if (folder.isSyncedOK) { // provide feedback that there is no need to fetch this feed
|
|
NSString * name = [folder.name hasPrefix:[Database untitledFeedFolderName]] ? folder.feedURL : folder.name;
|
|
ActivityItem * aItem = [[ActivityLog defaultLog] itemByName:name];
|
|
[aItem setStatus:NSLocalizedString(@"No new articles available", nil)];
|
|
[aItem clearDetails];
|
|
[self setFolderErrorFlag:folder flag:NO];
|
|
[folder clearNonPersistedFlag:VNAFolderFlagSyncedOK]; // get ready for next request
|
|
} else {
|
|
[self pumpSubscriptionRefresh:folder shouldForceRefresh:NO];
|
|
}
|
|
}];
|
|
NSOperation * unreadCountOperation = [OpenReader sharedManager].unreadCountOperation;
|
|
if (unreadCountOperation != nil && !unreadCountOperation.isFinished) {
|
|
[op addDependency:unreadCountOperation];
|
|
}
|
|
[[NSOperationQueue mainQueue] addOperation:op];
|
|
}
|
|
}
|
|
}
|
|
} // refreshSubscriptions
|
|
|
|
/* refreshFolderIconCacheForSubscriptions
|
|
* Add the folders specified in the foldersArray to the refresh queue.
|
|
*/
|
|
-(void)refreshFolderIconCacheForSubscriptions:(NSArray *)foldersArray
|
|
{
|
|
statusMessageDuringRefresh = NSLocalizedString(@"Refreshing folder images…", nil);
|
|
|
|
for (Folder * folder in foldersArray) {
|
|
if (folder.type == VNAFolderTypeGroup) {
|
|
[self refreshFolderIconCacheForSubscriptions:[[Database sharedManager] arrayOfFolders:folder.itemId]];
|
|
} else if (folder.type == VNAFolderTypeRSS || folder.type == VNAFolderTypeOpenReader) {
|
|
[self refreshFavIconForFolder:folder];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* refreshFavIconForFolder
|
|
* Adds the specified folder to the refresh queue.
|
|
*/
|
|
/**
|
|
* Refreshes the favicon for the specified folder
|
|
*
|
|
* @param folder The folder object to refresh the favicon for
|
|
*/
|
|
-(void)refreshFavIconForFolder:(Folder *)folder
|
|
{
|
|
// Do nothing if there's no homepage associated with the feed
|
|
// or if the feed already has a favicon.
|
|
if ((folder.type == VNAFolderTypeRSS || folder.type == VNAFolderTypeOpenReader) &&
|
|
(folder.homePage == nil || folder.homePage.vna_isBlank || folder.hasCachedImage))
|
|
{
|
|
[[Database sharedManager] clearFlag:VNAFolderFlagCheckForImage forFolder:folder.itemId];
|
|
return;
|
|
}
|
|
|
|
if (![self isRefreshingFolder:folder ofType:MA_Refresh_FavIcon]) {
|
|
[self pumpFolderIconRefresh:folder];
|
|
}
|
|
}
|
|
|
|
/* isRefreshingFolder
|
|
* Returns whether refresh queue has a queue item for the specified folder
|
|
* and refresh type.
|
|
*/
|
|
-(BOOL)isRefreshingFolder:(Folder *)folder ofType:(RefreshTypes)type
|
|
{
|
|
for (TRVSURLSessionOperation *theRequest in networkQueue.operations) {
|
|
NSMutableURLRequest *urlRequest = (NSMutableURLRequest *)(theRequest.task.originalRequest);
|
|
if ((((NSDictionary *)[urlRequest vna_userInfo])[@"folder"] == folder) &&
|
|
([[((NSDictionary *)[urlRequest vna_userInfo]) valueForKey:@"type"] integerValue] == @(type).integerValue))
|
|
{
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
/* cancelAll
|
|
* Cancel all active refreshes.
|
|
*/
|
|
-(void)cancelAll
|
|
{
|
|
[networkQueue cancelAllOperations];
|
|
}
|
|
|
|
|
|
/* countOfNewArticles
|
|
*/
|
|
-(NSUInteger)countOfNewArticles
|
|
{
|
|
return countOfNewArticles;
|
|
}
|
|
|
|
/* getCredentialsForFolder
|
|
* Initiate the UI to request the credentials for the specified folder.
|
|
*/
|
|
-(void)getCredentialsForFolder
|
|
{
|
|
if (credentialsController == nil) {
|
|
credentialsController = [[FeedCredentials alloc] init];
|
|
}
|
|
|
|
// Pull next folder out of the queue. The UI will post a
|
|
// notification when it is done and we can move on to the
|
|
// next one.
|
|
if (authQueue.count > 0 && !credentialsController.window.visible) {
|
|
Folder * folder = authQueue[0];
|
|
[credentialsController requestCredentialsInWindow:NSApp.mainWindow forFolder:folder];
|
|
}
|
|
}
|
|
|
|
/* setFolderErrorFlag
|
|
* Sets or clears the folder error flag then broadcasts an update indicating that the folder
|
|
* has changed.
|
|
*/
|
|
-(void)setFolderErrorFlag:(Folder *)folder flag:(BOOL)theFlag
|
|
{
|
|
if (theFlag) {
|
|
[folder setNonPersistedFlag:VNAFolderFlagError];
|
|
} else {
|
|
[folder clearNonPersistedFlag:VNAFolderFlagError];
|
|
}
|
|
[[NSNotificationCenter defaultCenter] vna_postNotificationOnMainThreadWithName:MA_Notify_FoldersUpdated object:@(folder.itemId)];
|
|
}
|
|
|
|
/* setFolderUpdatingFlag
|
|
* Sets or clears the folder updating flag then broadcasts an update indicating that the folder
|
|
* has changed.
|
|
*/
|
|
-(void)setFolderUpdatingFlag:(Folder *)folder flag:(BOOL)theFlag
|
|
{
|
|
if (theFlag) {
|
|
[folder setNonPersistedFlag:VNAFolderFlagUpdating];
|
|
} else {
|
|
[folder clearNonPersistedFlag:VNAFolderFlagUpdating];
|
|
}
|
|
[[NSNotificationCenter defaultCenter] vna_postNotificationOnMainThreadWithName:MA_Notify_FoldersUpdated object:@(folder.itemId)];
|
|
}
|
|
|
|
/* pumpFolderIconRefresh
|
|
* Initiate a connect to refresh the icon for a folder.
|
|
*/
|
|
-(void)pumpFolderIconRefresh:(Folder *)folder
|
|
{
|
|
// The activity log name we use depends on whether or not this folder has a real name.
|
|
NSString * name = [folder.name hasPrefix:[Database untitledFeedFolderName]] ? folder.feedURL : folder.name;
|
|
ActivityItem *aItem = [[ActivityLog defaultLog] itemByName:name];
|
|
|
|
NSString * favIconPath;
|
|
|
|
if (folder.type == VNAFolderTypeRSS) {
|
|
[aItem appendDetail:NSLocalizedString(@"Retrieving folder image", nil)];
|
|
favIconPath = [NSString stringWithFormat:@"%@/favicon.ico", folder.homePage.vna_trimmed.vna_baseURL];
|
|
} else { // Open Reader feed
|
|
[aItem appendDetail:NSLocalizedString(@"Retrieving folder image for Open Reader Feed", nil)];
|
|
favIconPath = [NSString stringWithFormat:@"%@/favicon.ico", folder.homePage.vna_trimmed.vna_baseURL];
|
|
}
|
|
|
|
NSMutableURLRequest *myRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:favIconPath]];
|
|
__weak typeof(self)weakSelf = self;
|
|
[self addConnection:myRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
if (error) {
|
|
[aItem appendDetail:[NSString stringWithFormat:@"%@ %@",
|
|
NSLocalizedString(@"Error retrieving RSS Icon:", nil), error.localizedDescription ]];
|
|
[[Database sharedManager] clearFlag:VNAFolderFlagCheckForImage forFolder:folder.itemId];
|
|
} else {
|
|
[weakSelf setFolderUpdatingFlag:folder flag:NO];
|
|
if (((NSHTTPURLResponse *)response).statusCode == 404) {
|
|
[aItem appendDetail:NSLocalizedString(@"RSS Icon not found!", nil)];
|
|
} else if (((NSHTTPURLResponse *)response).statusCode == 200) {
|
|
NSImage *iconImage = [[NSImage alloc] initWithData:data];
|
|
if (iconImage != nil && iconImage.valid) {
|
|
iconImage.size = NSMakeSize(16, 16);
|
|
folder.image = iconImage;
|
|
|
|
// Broadcast a notification since the folder image has now changed
|
|
[[NSNotificationCenter defaultCenter] vna_postNotificationOnMainThreadWithName:MA_Notify_FoldersUpdated
|
|
object:@(folder.itemId)];
|
|
|
|
// Log additional details about this.
|
|
[aItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"Folder image retrieved from %@",
|
|
nil), myRequest.URL]];
|
|
|
|
NSString * byteCount = [NSByteCountFormatter stringFromByteCount:(long long)data.length
|
|
countStyle:NSByteCountFormatterCountStyleFile];
|
|
[aItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"%@ received",
|
|
@"Number of bytes received, e.g. 1 MB received"),
|
|
byteCount]];
|
|
} else {
|
|
[aItem appendDetail:NSLocalizedString(@"RSS Icon not found!", nil)];
|
|
}
|
|
} else {
|
|
[aItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"HTTP code %d reported from server",
|
|
nil), (int)((NSHTTPURLResponse *)response).statusCode]];
|
|
}
|
|
|
|
[[Database sharedManager] clearFlag:VNAFolderFlagCheckForImage forFolder:folder.itemId];
|
|
}
|
|
}];
|
|
} // pumpFolderIconRefresh
|
|
|
|
#pragma mark Core of feed refresh
|
|
|
|
/* pumpSubscriptionRefresh
|
|
* Pick the folder at the head of the refresh queue and spawn a connection to
|
|
* refresh that folder.
|
|
*/
|
|
-(void)pumpSubscriptionRefresh:(Folder *)folder shouldForceRefresh:(BOOL)force
|
|
{
|
|
// If this folder needs credentials, add the folder to the list requiring authentication
|
|
// and since we can't progress without it, skip this folder on the connection
|
|
if (folder.flags & VNAFolderFlagNeedCredentials) {
|
|
[authQueue addObject:folder];
|
|
[self getCredentialsForFolder];
|
|
return;
|
|
}
|
|
|
|
|
|
// The activity log name we use depends on whether or not this folder has a real name.
|
|
NSString * name = [folder.name hasPrefix:[Database untitledFeedFolderName]] ? folder.feedURL : folder.name;
|
|
ActivityItem * aItem = [[ActivityLog defaultLog] itemByName:name];
|
|
|
|
// Compute the URL for this connection
|
|
NSString * urlString = folder.feedURL;
|
|
NSURL * url = nil;
|
|
|
|
if ([urlString hasPrefix:@"file://"]) {
|
|
url = [NSURL fileURLWithPath:[urlString substringFromIndex:7].stringByExpandingTildeInPath];
|
|
} else if ([urlString hasPrefix:@"feed://"]) {
|
|
url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", [urlString substringFromIndex:7]]];
|
|
} else {
|
|
url = [NSURL URLWithString:urlString];
|
|
}
|
|
|
|
// Seed the activity log for this feed.
|
|
[aItem clearDetails];
|
|
[aItem setStatus:NSLocalizedString(@"Retrieving articles", nil)];
|
|
|
|
// Mark the folder as being refreshed. The updating status is not
|
|
// persistent so we set this directly on the folder rather than
|
|
// through the database.
|
|
[self setFolderUpdatingFlag:folder flag:YES];
|
|
|
|
// Additional detail for the log
|
|
if (folder.type == VNAFolderTypeOpenReader) {
|
|
[aItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"Connecting to Open Reader server to retrieve %@", nil),
|
|
urlString]];
|
|
} else {
|
|
[aItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"Connecting to %@", nil), urlString]];
|
|
}
|
|
|
|
// Kick off the connection
|
|
[self refreshFeed:folder fromURL:url withLog:aItem shouldForceRefresh:force];
|
|
} // pumpSubscriptionRefresh
|
|
|
|
/* refreshFeed
|
|
* Refresh a folder's newsfeed using the specified URL.
|
|
*/
|
|
-(void)refreshFeed:(Folder *)folder fromURL:(NSURL *)url withLog:(ActivityItem *)aItem shouldForceRefresh:(BOOL)force
|
|
{
|
|
NSMutableURLRequest *myRequest;
|
|
|
|
if (folder.type == VNAFolderTypeRSS) {
|
|
myRequest = [NSMutableURLRequest requestWithURL:url];
|
|
NSString * theLastUpdateString = folder.lastUpdateString;
|
|
if (![theLastUpdateString isEqualToString:@""]) {
|
|
[myRequest setValue:theLastUpdateString forHTTPHeaderField:@"If-Modified-Since"];
|
|
[myRequest setValue:@"feed" forHTTPHeaderField:@"A-IM"];
|
|
}
|
|
[myRequest vna_setUserInfo:@{ @"folder": folder, @"log": aItem, @"type": @(MA_Refresh_Feed) }];
|
|
[myRequest addValue:
|
|
@"application/rss+xml,application/rdf+xml,application/atom+xml,text/xml,application/xml,application/xhtml+xml,application/feed+json,application/json;q=0.9,text/html;q=0.8,*/*;q=0.5"
|
|
forHTTPHeaderField:@"Accept"];
|
|
// if authentication infos are present, try basic authentication first
|
|
if (![folder.username isEqualToString:@""]) {
|
|
NSString* usernameAndPassword = [NSString vna_toBase64String:[NSString stringWithFormat:@"%@:%@", folder.username, folder.password]];
|
|
[myRequest setValue:[NSString stringWithFormat:@"Basic %@", usernameAndPassword] forHTTPHeaderField:@"Authorization"];
|
|
}
|
|
|
|
|
|
__weak typeof(self)weakSelf = self;
|
|
[self addConnection:myRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
if (error) {
|
|
[weakSelf folderRefreshFailed:myRequest error:error];
|
|
} else {
|
|
[weakSelf folderRefreshCompleted:myRequest response:response data:data];
|
|
}
|
|
}];
|
|
} else { // Open Reader feed
|
|
[[OpenReader sharedManager] refreshFeed:folder withLog:(ActivityItem *)aItem shouldIgnoreArticleLimit:force];
|
|
}
|
|
if (!hasStarted) {
|
|
hasStarted = YES;
|
|
countOfNewArticles = 0;
|
|
[[OpenReader sharedManager] resetCountOfNewArticles];
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MA_Notify_RefreshStatus object:nil];
|
|
}
|
|
} // refreshFeed
|
|
|
|
|
|
// failure callback
|
|
-(void)folderRefreshFailed:(NSMutableURLRequest *)request error:(NSError *)error
|
|
{
|
|
os_log_debug(VNA_LOG, "Refresh of %@ failed. Reason: %{public}@", error.userInfo[NSURLErrorFailingURLStringErrorKey], error.localizedDescription);
|
|
Folder * folder = ((NSDictionary *)[request vna_userInfo])[@"folder"];
|
|
if (error.code == NSURLErrorCancelled) {
|
|
// Stopping the connection isn't an error, so clear any existing error flag.
|
|
[self setFolderErrorFlag:folder flag:NO];
|
|
|
|
// If this folder also requires an image refresh, add that
|
|
if ((folder.flags & VNAFolderFlagCheckForImage)) {
|
|
[self refreshFavIconForFolder:folder];
|
|
}
|
|
} else if (error.code == NSURLErrorUserAuthenticationRequired) { //Error caused by lack of authentication
|
|
if (![authQueue containsObject:folder]) {
|
|
[authQueue addObject:folder];
|
|
}
|
|
[self getCredentialsForFolder];
|
|
}
|
|
ActivityItem *aItem = (ActivityItem *)((NSDictionary *)[request vna_userInfo])[@"log"];
|
|
[self setFolderErrorFlag:folder flag:YES];
|
|
[aItem appendDetail:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Error retrieving RSS feed:", nil),
|
|
error.localizedDescription ]];
|
|
[aItem setStatus:NSLocalizedString(@"Error", nil)];
|
|
[self syncFinishedForFolder:folder];
|
|
} // folderRefreshFailed
|
|
|
|
/* folderRefreshCompleted
|
|
* Called when a folder refresh completed.
|
|
*/
|
|
-(void)folderRefreshCompleted:(NSMutableURLRequest *)connector response:(NSURLResponse *)response data:(NSData *)receivedData
|
|
{
|
|
dispatch_async(_queue, ^() {
|
|
// TODO : refactor code to separate feed refresh code and UI
|
|
|
|
Folder * folder = (Folder *)((NSDictionary *)[connector vna_userInfo])[@"folder"];
|
|
ActivityItem *connectorItem = ((NSDictionary *)[connector vna_userInfo])[@"log"];
|
|
NSURL * url = connector.URL;
|
|
NSInteger folderId = folder.itemId;
|
|
Database *dbManager = [Database sharedManager];
|
|
NSInteger responseStatusCode;
|
|
NSString * lastModifiedString;
|
|
|
|
// hack for handling file:// URLs
|
|
if (url.fileURL) {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSString * filePath = [url.path stringByRemovingPercentEncoding];
|
|
BOOL isDirectory = NO;
|
|
if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory] && !isDirectory) {
|
|
responseStatusCode = 200;
|
|
lastModifiedString = [[fileManager attributesOfItemAtPath:filePath error:nil] fileModificationDate].description;
|
|
} else {
|
|
responseStatusCode = 404;
|
|
}
|
|
} else {
|
|
responseStatusCode = ((NSHTTPURLResponse *)response).statusCode;
|
|
lastModifiedString = SafeString([((NSHTTPURLResponse *)response).allHeaderFields valueForKey:@"Last-Modified"]);
|
|
}
|
|
|
|
if (responseStatusCode == 304) {
|
|
// No modification from last check
|
|
|
|
[dbManager setLastUpdate:[NSDate date] forFolder:folderId];
|
|
|
|
[self setFolderErrorFlag:folder flag:NO];
|
|
[connectorItem appendDetail:NSLocalizedString(@"Got HTTP status 304 - No news from last check", nil)];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[connectorItem setStatus:NSLocalizedString(@"No new articles available", nil)];
|
|
[self syncFinishedForFolder:folder];
|
|
});
|
|
return;
|
|
} else if (responseStatusCode == 410) {
|
|
// We got HTTP 410 which means the feed has been intentionally removed so unsubscribe the feed.
|
|
[dbManager setFlag:VNAFolderFlagUnsubscribed forFolder:folderId];
|
|
|
|
} else if (responseStatusCode == 200 || responseStatusCode == 226) {
|
|
if (receivedData != nil) {
|
|
[self finalizeFolderRefresh:@{
|
|
@"folder": folder,
|
|
@"log": connectorItem,
|
|
@"url": url,
|
|
@"data": receivedData,
|
|
@"mimeType": response.MIMEType,
|
|
@"lastModifiedString": lastModifiedString,
|
|
}];
|
|
}
|
|
} else { //other HTTP response codes like 404, 403...
|
|
[connectorItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"HTTP code %d reported from server", nil),
|
|
(int)responseStatusCode]];
|
|
[connectorItem appendDetail:[NSHTTPURLResponse localizedStringForStatusCode:responseStatusCode]];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[connectorItem setStatus:NSLocalizedString(@"Error", nil)];
|
|
});
|
|
[self setFolderErrorFlag:folder flag:YES];
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self syncFinishedForFolder:folder];
|
|
});
|
|
}); //block for dispatch_async on _queue
|
|
} // folderRefreshCompleted
|
|
|
|
-(void)finalizeFolderRefresh:(NSDictionary *)parameters
|
|
{
|
|
if (!parameters) {
|
|
os_log_error(VNA_LOG, "%{public}s has no parameters", __PRETTY_FUNCTION__);
|
|
}
|
|
Folder * folder = (Folder *)parameters[@"folder"];
|
|
NSInteger folderId = folder.itemId;
|
|
Database * dbManager = [Database sharedManager];
|
|
ActivityItem *connectorItem = parameters[@"log"];
|
|
NSURL * url = parameters[@"url"];
|
|
NSData * receivedData = parameters[@"data"];
|
|
NSString * lastModifiedString = parameters[@"lastModifiedString"];
|
|
|
|
// Check whether this is an HTML redirect. If so, create a new connection using
|
|
// the redirect.
|
|
NSString * redirectURL = [self getRedirectURL:receivedData];
|
|
if (redirectURL != nil) {
|
|
if ([redirectURL isEqualToString:url.absoluteString]) {
|
|
// To prevent an infinite loop, don't redirect to the same URL.
|
|
[connectorItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"Improper infinitely looping URL redirect to %@",
|
|
nil), url.absoluteString]];
|
|
} else {
|
|
[self refreshFeed:folder fromURL:[NSURL URLWithString:redirectURL] withLog:connectorItem shouldForceRefresh:NO];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Empty data feed is OK if we got HTTP 200
|
|
__block NSUInteger newArticlesFromFeed = 0;
|
|
if (receivedData.length > 0) {
|
|
Preferences *standardPreferences = [Preferences standardPreferences];
|
|
if (standardPreferences.shouldSaveFeedSource) {
|
|
NSString * feedSourcePath = folder.feedSourceFilePath;
|
|
|
|
if ([standardPreferences boolForKey:MAPref_ShouldSaveFeedSourceBackup]) {
|
|
BOOL isDirectory = YES;
|
|
NSFileManager *defaultManager = [NSFileManager defaultManager];
|
|
if ([defaultManager fileExistsAtPath:feedSourcePath isDirectory:&isDirectory] && !isDirectory) {
|
|
NSString * backupPath = [feedSourcePath stringByAppendingPathExtension:@"bak"];
|
|
if (![defaultManager fileExistsAtPath:backupPath] || [defaultManager removeItemAtPath:backupPath error:NULL]) { // Remove any old backup first
|
|
[defaultManager moveItemAtPath:feedSourcePath toPath:backupPath error:NULL];
|
|
}
|
|
}
|
|
}
|
|
|
|
[receivedData writeToFile:feedSourcePath options:NSAtomicWrite error:NULL];
|
|
}
|
|
|
|
id<VNAFeed> newFeed;
|
|
NSError *error;
|
|
NSString *mimeType = parameters[@"mimeType"];
|
|
if ([mimeType containsString:@"application/feed+json"] ||
|
|
[mimeType containsString:@"application/json"]) {
|
|
VNAJSONFeedParser *parser = [[VNAJSONFeedParser alloc] init];
|
|
newFeed = [parser feedWithJSONData:receivedData error:&error];
|
|
} else {
|
|
VNAXMLFeedParser *parser = [[VNAXMLFeedParser alloc] init];
|
|
newFeed = [parser feedWithXMLData:receivedData error:&error];
|
|
}
|
|
if (!newFeed) {
|
|
NSString *errorDebugDescription = error.userInfo[NSDebugDescriptionErrorKey];
|
|
if (errorDebugDescription) {
|
|
os_log_error(VNA_LOG, "%@", errorDebugDescription);
|
|
}
|
|
// Mark the feed as failed
|
|
[self setFolderErrorFlag:folder flag:YES];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[connectorItem setStatus:NSLocalizedString(@"Error parsing data in feed", nil)];
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Log number of bytes we received
|
|
NSString * byteCount = [NSByteCountFormatter stringFromByteCount:receivedData.length
|
|
countStyle:NSByteCountFormatterCountStyleFile];
|
|
[connectorItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"%@ received",
|
|
@"Number of bytes received, e.g. 1 MB received"),
|
|
byteCount]];
|
|
|
|
if (newFeed.items.count == 0) {
|
|
// Mark the feed as empty
|
|
[self setFolderErrorFlag:folder flag:YES];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[connectorItem setStatus:NSLocalizedString(@"No articles in feed", nil)];
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Extract the latest title and description
|
|
NSString * feedTitle = newFeed.title;
|
|
NSString * feedDescription = newFeed.feedDescription;
|
|
NSString * feedLink = newFeed.homePageURL;
|
|
|
|
// Synthesize feed link if it is missing
|
|
if (feedLink == nil || feedLink.vna_isBlank) {
|
|
feedLink = folder.feedURL.vna_baseURL;
|
|
}
|
|
if (feedLink != nil && ![feedLink hasPrefix:@"http:"] && ![feedLink hasPrefix:@"https:"]) {
|
|
feedLink = [NSURL URLWithString:feedLink relativeToURL:url].absoluteString;
|
|
}
|
|
|
|
|
|
// We'll be collecting articles into this array
|
|
NSMutableArray *articleArray = [NSMutableArray array];
|
|
NSMutableArray *articleGuidArray = [NSMutableArray array];
|
|
|
|
NSDate *itemAlternativeDate = newFeed.modifiedDate;
|
|
if (itemAlternativeDate == nil) {
|
|
itemAlternativeDate = [NSDate date];
|
|
}
|
|
|
|
// Parse off items.
|
|
|
|
for (id<VNAFeedItem> newsItem in newFeed.items) {
|
|
NSDate * articleDate = newsItem.modifiedDate;
|
|
|
|
NSString * articleGuid = newsItem.guid;
|
|
|
|
// This routine attempts to synthesize a GUID from an incomplete item that lacks an
|
|
// ID field. Generally we'll have three things to work from: a link, a title and a
|
|
// description. The link alone is not sufficiently unique and I've seen feeds where
|
|
// the description is also not unique. The title field generally does vary but we need
|
|
// to be careful since separate articles with different descriptions may have the same
|
|
// title. The solution is to use the link and title and build a GUID from those.
|
|
// We add the folderId at the beginning to ensure that items in different feeds do not share a guid.
|
|
if ([articleGuid isEqualToString:@""]) {
|
|
articleGuid = [NSString stringWithFormat:@"%ld-%@-%@", (long)folderId, newsItem.url, newsItem.title];
|
|
}
|
|
// This is a horrible hack for horrible feeds that contain more than one item with the same guid.
|
|
// Bad feeds! I'm talking to you, kerbalstuff.com
|
|
NSUInteger articleIndex = [articleGuidArray indexOfObject:articleGuid];
|
|
if (articleIndex != NSNotFound) {
|
|
// We rebuild complex guids which should eliminate most duplicates
|
|
Article * firstFoundArticle = articleArray[articleIndex];
|
|
if (articleDate == nil) {
|
|
// first, hack the initial article (which is probably the first loaded / most recent one)
|
|
NSString * firstFoundArticleNewGuid =
|
|
[NSString stringWithFormat:@"%ld-%@-%@", (long)folderId, firstFoundArticle.link, firstFoundArticle.title];
|
|
firstFoundArticle.guid = firstFoundArticleNewGuid;
|
|
articleGuidArray[articleIndex] = firstFoundArticleNewGuid;
|
|
// then hack the guid for the item being processed
|
|
articleGuid = [NSString stringWithFormat:@"%ld-%@-%@", (long)folderId, newsItem.url, newsItem.title];
|
|
} else {
|
|
// first, hack the initial article (which is probably the first loaded / most recent one)
|
|
NSString * firstFoundArticleNewGuid =
|
|
[NSString stringWithFormat:@"%ld-%@-%@-%@", (long)folderId,
|
|
[NSString stringWithFormat:@"%1.3f", firstFoundArticle.date.timeIntervalSince1970], firstFoundArticle.link,
|
|
firstFoundArticle.title];
|
|
firstFoundArticle.guid = firstFoundArticleNewGuid;
|
|
articleGuidArray[articleIndex] = firstFoundArticleNewGuid;
|
|
// then hack the guid for the item being processed
|
|
articleGuid =
|
|
[NSString stringWithFormat:@"%ld-%@-%@-%@", (long)folderId,
|
|
[NSString stringWithFormat:@"%1.3f", articleDate.timeIntervalSince1970], newsItem.url, newsItem.title];
|
|
}
|
|
}
|
|
[articleGuidArray addObject:articleGuid];
|
|
|
|
// set the article date if it is missing. We'll use the
|
|
// last modified date of the feed and set each article to be 1 second older than the
|
|
// previous one. So the array is effectively newest first.
|
|
if (articleDate == nil) {
|
|
articleDate = itemAlternativeDate;
|
|
itemAlternativeDate = [itemAlternativeDate dateByAddingTimeInterval:-1.0];
|
|
}
|
|
|
|
Article * article = [[Article alloc] initWithGuid:articleGuid];
|
|
article.folderId = folderId;
|
|
article.author = newsItem.authors;
|
|
article.body = newsItem.content;
|
|
if (!newsItem.title || newsItem.title.vna_isBlank) {
|
|
NSString *newTitle = newsItem.content.vna_titleTextFromHTML.vna_stringByUnescapingExtendedCharacters;
|
|
if (newTitle.vna_isBlank) {
|
|
article.title = NSLocalizedString(@"(No title)", @"Fallback for feed items without a title");
|
|
} else {
|
|
article.title = newTitle;
|
|
}
|
|
} else {
|
|
article.title = newsItem.title;
|
|
}
|
|
NSString * articleLink = newsItem.url;
|
|
if (![articleLink hasPrefix:@"http:"] && ![articleLink hasPrefix:@"https:"]) {
|
|
articleLink = [NSURL URLWithString:articleLink relativeToURL:url].absoluteString;
|
|
}
|
|
if (articleLink == nil) {
|
|
articleLink = feedLink;
|
|
}
|
|
article.link = articleLink;
|
|
article.date = articleDate;
|
|
NSString * enclosureLink = newsItem.enclosure;
|
|
if ([enclosureLink isNotEqualTo:@""] && ![enclosureLink hasPrefix:@"http:"] && ![enclosureLink hasPrefix:@"https:"]) {
|
|
enclosureLink = [NSURL URLWithString:enclosureLink relativeToURL:url].absoluteString;
|
|
}
|
|
article.enclosure = enclosureLink;
|
|
if ([enclosureLink isNotEqualTo:@""]) {
|
|
[article setHasEnclosure:YES];
|
|
}
|
|
[articleArray addObject:article];
|
|
}
|
|
|
|
|
|
// Here's where we add the articles to the database
|
|
if (articleArray.count > 0u) {
|
|
NSArray *guidHistory = [dbManager guidHistoryForFolderId:folderId];
|
|
for (Article * article in articleArray) {
|
|
if ([folder createArticle:article
|
|
guidHistory:guidHistory] && (article.status == ArticleStatusNew))
|
|
{
|
|
++newArticlesFromFeed;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// A notify is only needed if we added any new articles.
|
|
if (feedTitle != nil && !feedTitle.vna_isBlank && [folder.name hasPrefix:[Database untitledFeedFolderName]]) {
|
|
// If there's an existing feed with this title, make ours unique
|
|
// BUGBUG: This duplicates logic in database.m so consider moving it there.
|
|
NSString * oldFeedTitle = feedTitle;
|
|
NSString * newFeedTitle = feedTitle;
|
|
NSUInteger index = 1;
|
|
|
|
while (([dbManager folderFromName:newFeedTitle]) != nil) {
|
|
newFeedTitle = [NSString stringWithFormat:@"%@ (%lu)", oldFeedTitle, (unsigned long)index++];
|
|
}
|
|
|
|
connectorItem.name = newFeedTitle;
|
|
[dbManager setName:newFeedTitle forFolder:folderId];
|
|
}
|
|
if (feedDescription != nil) {
|
|
[dbManager setDescription:feedDescription forFolder:folderId];
|
|
}
|
|
if (feedLink != nil) {
|
|
[dbManager setHomePage:feedLink forFolder:folderId];
|
|
}
|
|
|
|
// Remember the last modified date
|
|
if (lastModifiedString != nil && lastModifiedString.length > 0) {
|
|
[dbManager setLastUpdateString:lastModifiedString forFolder:folderId];
|
|
}
|
|
// Set the last update date for this folder.
|
|
[dbManager setLastUpdate:[NSDate date] forFolder:folderId];
|
|
|
|
|
|
// Mark the feed as succeeded
|
|
[self setFolderErrorFlag:folder flag:NO];
|
|
[folder clearNonPersistedFlag:VNAFolderFlagBuggySync];
|
|
}
|
|
|
|
// Send status to the activity log
|
|
if (newArticlesFromFeed == 0) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[connectorItem setStatus:NSLocalizedString(@"No new articles available", nil)];
|
|
});
|
|
} else {
|
|
NSString * logText = [NSString stringWithFormat:NSLocalizedString(@"%d new articles retrieved", nil), (int)newArticlesFromFeed];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
connectorItem.status = logText;
|
|
});
|
|
[[NSNotificationCenter defaultCenter] vna_postNotificationOnMainThreadWithName:MA_Notify_ArticleListContentChange object:@(folder.
|
|
itemId)];
|
|
}
|
|
|
|
// Done with this connection
|
|
|
|
// Add to count of new articles so far
|
|
countOfNewArticles += newArticlesFromFeed;
|
|
|
|
// If this folder also requires an image refresh, do that
|
|
if ((folder.flags & VNAFolderFlagCheckForImage)) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self refreshFavIconForFolder:folder];
|
|
});
|
|
}
|
|
}
|
|
|
|
/* getRedirectURL
|
|
* Scans the XML data and checks whether it is actually an HTML redirect. If so, returns the
|
|
* redirection URL. (Yes, I'm aware that some of this could be better implemented with calls to
|
|
* strnstr and its ilk but I have a deep rooted distrust of the standard C runtime stemming from
|
|
* a childhood trauma with buffer overflows so bear with me.)
|
|
*/
|
|
-(NSString *)getRedirectURL:(NSData *)data
|
|
{
|
|
const char *scanPtr = data.bytes;
|
|
const char *scanPtrEnd = scanPtr + data.length;
|
|
|
|
// Make sure this is HTML otherwise this is likely just valid
|
|
// XML and we can ignore everything else.
|
|
const char *htmlTagPtr = "<html>";
|
|
while (scanPtr < scanPtrEnd && *htmlTagPtr != '\0') {
|
|
if (*scanPtr != ' ') {
|
|
if (tolower(*scanPtr) != *htmlTagPtr) {
|
|
return nil;
|
|
}
|
|
++htmlTagPtr;
|
|
}
|
|
++scanPtr;
|
|
}
|
|
|
|
// Look for the meta attribute
|
|
const char *metaTag = "<meta ";
|
|
const char *headEndTag = "</head>";
|
|
const char *metaTagPtr = metaTag;
|
|
const char *headEndTagPtr = headEndTag;
|
|
while (scanPtr < scanPtrEnd) {
|
|
if (tolower(*scanPtr) == *metaTagPtr) {
|
|
++metaTagPtr;
|
|
} else {
|
|
metaTagPtr = metaTag;
|
|
if (tolower(*scanPtr) == *headEndTagPtr) {
|
|
++headEndTagPtr;
|
|
} else {
|
|
headEndTagPtr = headEndTag;
|
|
}
|
|
}
|
|
if (*headEndTagPtr == '\0') {
|
|
return nil;
|
|
}
|
|
if (*metaTagPtr == '\0') {
|
|
// Now see if this meta tag has http-equiv attribute
|
|
const char *httpEquivAttr = "http-equiv=\"refresh\"";
|
|
const char *httpEquivAttrPtr = httpEquivAttr;
|
|
while (scanPtr < scanPtrEnd && *scanPtr != '>') {
|
|
if (tolower(*scanPtr) == *httpEquivAttrPtr) {
|
|
++httpEquivAttrPtr;
|
|
} else if (*scanPtr != ' ') {
|
|
httpEquivAttrPtr = httpEquivAttr;
|
|
}
|
|
if (*httpEquivAttrPtr == '\0') {
|
|
// OK. This is our meta tag. Now look for the URL field
|
|
while (scanPtr < scanPtrEnd - 3 && *scanPtr != '>') {
|
|
if (tolower(*scanPtr) == 'u' && tolower(*(scanPtr + 1)) == 'r' && tolower(*(scanPtr + 2)) == 'l' &&
|
|
*(scanPtr + 3) == '=')
|
|
{
|
|
const char *urlStart = scanPtr + 4;
|
|
const char *urlEnd = urlStart;
|
|
|
|
// Finally, gather the URL for the redirect and return it as an
|
|
// auto-released string.
|
|
while (urlEnd < scanPtrEnd && *urlEnd != '"' && *urlEnd != ' ' && *urlEnd != '>') {
|
|
++urlEnd;
|
|
}
|
|
if (urlEnd == scanPtrEnd) {
|
|
return nil;
|
|
}
|
|
return [[NSString alloc] initWithBytes:urlStart length:(urlEnd - urlStart) encoding:NSASCIIStringEncoding];
|
|
}
|
|
++scanPtr;
|
|
}
|
|
}
|
|
++scanPtr;
|
|
}
|
|
|
|
// Not our meta tag so look for another
|
|
metaTagPtr = metaTag;
|
|
}
|
|
++scanPtr;
|
|
}
|
|
return nil;
|
|
} // getRedirectURL
|
|
|
|
-(void)syncFinishedForFolder:(Folder *)folder
|
|
{
|
|
[self setFolderUpdatingFlag:folder flag:NO];
|
|
}
|
|
|
|
#pragma mark NSURLSession redirection delegate
|
|
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(
|
|
NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
|
|
{
|
|
completionHandler(newRequest);
|
|
NSMutableURLRequest *originalRequest = (NSMutableURLRequest *)task.originalRequest;
|
|
if ([originalRequest vna_userInfo] != nil) {
|
|
Folder * folder = (Folder *)[originalRequest vna_userInfo][@"folder"];
|
|
NSInteger type = [[(NSDictionary *)[originalRequest vna_userInfo] valueForKey:@"type"] integerValue];
|
|
if (((NSHTTPURLResponse *)response).statusCode == 301 && folder != nil && type == MA_Refresh_Feed) {
|
|
// We got a permanent redirect from the feed so we probably need to change the feed URL to the new location.
|
|
[self verify301Status:task];
|
|
}
|
|
}
|
|
}
|
|
|
|
// We got a permanent redirect from the feed
|
|
// We check if we really need to change the feed URL to a new location
|
|
// to avoid issue #380 : https://github.com/ViennaRSS/vienna-rss/issues/380
|
|
-(void)verify301Status:(NSURLSessionTask *)task
|
|
{
|
|
NSMutableURLRequest *originalRequest = ((NSMutableURLRequest *)(task.originalRequest));
|
|
if (task != nil) {
|
|
// we might have successive redirections for one task
|
|
if ([self.redirect301WaitQueue containsObject:task]) {
|
|
[self.redirect301WaitQueue removeObject:task];
|
|
}
|
|
[self.redirect301WaitQueue addObject:task];
|
|
}
|
|
|
|
NSURL * newURL = task.currentRequest.URL;
|
|
if ([newURL.host isEqualToString:originalRequest.URL.host]) {
|
|
[self validate301WaitQueue];
|
|
}
|
|
|
|
|
|
switch (self.redirect301Status) {
|
|
case HTTP301Unknown: {
|
|
self.redirect301Status = HTTP301Pending;
|
|
// build a test request, assuming that
|
|
// there is no valid reason for
|
|
// www.example.com to be permanently redirected
|
|
// (cf RFC 6761 http://www.iana.org/go/rfc6761)
|
|
NSURL * testURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@://www.example.com", originalRequest.URL.scheme]];
|
|
NSMutableURLRequest *testRequest = [NSMutableURLRequest requestWithURL:testURL];
|
|
__weak typeof(self)weakSelf = self;
|
|
[self addConnection:testRequest
|
|
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
if (error) {
|
|
NSDictionary *headers = ((NSHTTPURLResponse *)response).allHeaderFields;
|
|
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
os_log_fault(VNA_LOG, "Test requested failed.\n\nHeaders: %@\n\nData: %@", headers, dataStr);
|
|
[weakSelf void301WaitQueue];
|
|
} else {
|
|
if (![((NSHTTPURLResponse *)response).URL.host isEqualToString:testRequest.URL.host]) {
|
|
// we probably have a misconfigured router / proxy
|
|
// which redirects permanently every site, even www.example.com
|
|
[weakSelf void301WaitQueue];
|
|
} else {
|
|
// we can now assume that 301 redirects we encounter are safe
|
|
[weakSelf validate301WaitQueue];
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
break;
|
|
|
|
case HTTP301Pending:
|
|
break;
|
|
|
|
case HTTP301Unsafe:
|
|
[self purge301WaitQueue];
|
|
break;
|
|
|
|
case HTTP301Safe:
|
|
[self process301WaitQueue];
|
|
break;
|
|
} /* switch */
|
|
} /* verify301Status */
|
|
|
|
-(void)void301WaitQueue
|
|
{
|
|
self.redirect301Status = HTTP301Unsafe;
|
|
// we will not consider 301 redirections as permanent for 24 hours
|
|
self.unsafe301RedirectionTimer = [NSTimer scheduledTimerWithTimeInterval:24 * 3600
|
|
target:self
|
|
selector:@selector(reset301Status:)
|
|
userInfo:nil
|
|
repeats:NO];
|
|
self.riskyIPAddress = [NSHost currentHost].address;
|
|
[self purge301WaitQueue];
|
|
}
|
|
|
|
-(void)purge301WaitQueue
|
|
{
|
|
for (id obj in [self.redirect301WaitQueue reverseObjectEnumerator]) {
|
|
NSURLSessionTask *theConnector = (NSURLSessionTask *)obj;
|
|
[self.redirect301WaitQueue removeObject:obj];
|
|
NSMutableURLRequest * originalRequest = (NSMutableURLRequest *)theConnector.originalRequest;
|
|
ActivityItem *connectorItem = ((NSDictionary *)[originalRequest vna_userInfo])[@"log"];
|
|
[connectorItem appendDetail:NSLocalizedString(@"Redirection attempt treated as temporary for safety concern", nil)];
|
|
}
|
|
}
|
|
|
|
-(void)validate301WaitQueue
|
|
{
|
|
self.redirect301Status = HTTP301Safe;
|
|
[self process301WaitQueue];
|
|
}
|
|
|
|
-(void)process301WaitQueue
|
|
{
|
|
for (id obj in [self.redirect301WaitQueue reverseObjectEnumerator]) {
|
|
NSURLSessionTask *theConnector = (NSURLSessionTask *)obj;
|
|
[self.redirect301WaitQueue removeObject:obj];
|
|
NSString * theNewURLString = theConnector.currentRequest.URL.absoluteString;
|
|
NSMutableURLRequest * originalRequest = (NSMutableURLRequest *)theConnector.originalRequest;
|
|
Folder * theFolder = (Folder *)((NSDictionary *)[originalRequest vna_userInfo])[@"folder"];
|
|
[[Database sharedManager] setFeedURL:theNewURLString forFolder:theFolder.itemId];
|
|
ActivityItem *connectorItem = ((NSDictionary *)[originalRequest vna_userInfo])[@"log"];
|
|
[connectorItem appendDetail:[NSString stringWithFormat:NSLocalizedString(@"Feed URL updated to %@",
|
|
nil), theNewURLString]];
|
|
}
|
|
}
|
|
|
|
-(void)reset301Status:(NSTimer *)timer
|
|
{
|
|
self.redirect301Status = HTTP301Unknown;
|
|
[timer invalidate];
|
|
timer = nil;
|
|
}
|
|
|
|
#pragma mark Network queue management
|
|
|
|
/* addConnection
|
|
* Add the specified connection to the connections queue
|
|
* that we manage.
|
|
*/
|
|
-(NSOperation *)addConnection:(NSMutableURLRequest *)urlRequest completionHandler:(void (^)(NSData *data, NSURLResponse *response,
|
|
NSError *error))completionHandler
|
|
{
|
|
TRVSURLSessionOperation *op =
|
|
[[TRVSURLSessionOperation alloc] initWithSession:self.urlSession request:urlRequest completionHandler:completionHandler];
|
|
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
|
|
if (self->networkQueue.operationCount == 0) {
|
|
[self performSelector:@selector(finishConnectionQueue) withObject:nil afterDelay:0.1];
|
|
}
|
|
[self updateStatus];
|
|
}];
|
|
[completionOperation addDependency:op];
|
|
[[NSOperationQueue mainQueue] addOperation:completionOperation];
|
|
|
|
[networkQueue addOperation:op];
|
|
return op;
|
|
} // addConnection
|
|
|
|
/* suspendConnectionsQueue
|
|
* suspend the connections queue that we manage.
|
|
* Useful for managing dependencies inside the queue
|
|
*/
|
|
-(void)suspendConnectionsQueue
|
|
{
|
|
[networkQueue setSuspended:YES];
|
|
}
|
|
|
|
/* resumeConnectionsQueue
|
|
* release the connections queue that we manage,
|
|
* after we suspended it.
|
|
*/
|
|
-(void)resumeConnectionsQueue
|
|
{
|
|
[networkQueue setSuspended:NO];
|
|
}
|
|
|
|
-(BOOL)isConnecting
|
|
{
|
|
return networkQueue.operationCount > 0;
|
|
}
|
|
|
|
-(void)updateStatus
|
|
{
|
|
if (hasStarted) {
|
|
statusMessageDuringRefresh =
|
|
[NSString stringWithFormat:@"%@: (%lu) - %@", NSLocalizedString(@"Queue", nil), (unsigned long)networkQueue.operationCount,
|
|
NSLocalizedString(@"Refreshing subscriptions…", nil)];
|
|
}
|
|
self.statusMessage = self->statusMessageDuringRefresh;
|
|
}
|
|
|
|
/* finishConnectionQueue
|
|
* this is run on the main thread
|
|
* at the exhaustion of the network queue
|
|
*/
|
|
-(void)finishConnectionQueue
|
|
{
|
|
if (hasStarted && networkQueue.operationCount == 0) {
|
|
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
[nc postNotificationName:MA_Notify_RefreshStatus object:nil];
|
|
[nc postNotificationName:MA_Notify_ArticleListContentChange object:nil];
|
|
statusMessageDuringRefresh = NSLocalizedString(@"Refresh completed", nil);
|
|
hasStarted = NO;
|
|
os_log_info(VNA_LOG, "Finished refreshing");
|
|
} else {
|
|
statusMessageDuringRefresh = @"";
|
|
}
|
|
[self updateStatus];
|
|
}
|
|
|
|
#pragma mark NSURLSession Authentication delegates
|
|
|
|
- (void)URLSession:(NSURLSession *)session
|
|
task:(NSURLSessionTask *)task
|
|
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
|
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler
|
|
{
|
|
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic] ||
|
|
[challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest])
|
|
{
|
|
if ([challenge previousFailureCount] == 3) {
|
|
completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
|
|
} else {
|
|
NSMutableURLRequest *urlRequest = (NSMutableURLRequest *)(task.originalRequest);
|
|
Folder * folder = ((NSDictionary *)[urlRequest vna_userInfo])[@"folder"];
|
|
if (![folder.username isEqualToString:@""]) {
|
|
NSURLCredential *credential = [NSURLCredential credentialWithUser:folder.username
|
|
password:folder.password
|
|
persistence:NSURLCredentialPersistenceNone];
|
|
if (credential) {
|
|
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
|
} else {
|
|
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
|
}
|
|
} else {
|
|
if (![authQueue containsObject:folder]) {
|
|
[authQueue addObject:folder];
|
|
}
|
|
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
|
[self getCredentialsForFolder];
|
|
}
|
|
}
|
|
} else {
|
|
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
|
}
|
|
} // URLSession
|
|
|
|
/* dealloc
|
|
* Clean up after ourselves.
|
|
*/
|
|
-(void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
[_urlSession invalidateAndCancel];
|
|
}
|
|
@end
|