Fjalapeno has submitted this change and it was merged.

Change subject: Fetch site info to get main page
......................................................................


Fetch site info to get main page

Prevents a crash in the app when we don't know the main page for a given
language code.  This is done by fetching the site's "siteinfo" metadata,
and extracting the "mainpage" field from it.

The diff is large because:

- I added new fetcher
- I replaced "isMain" logic, which was quite pervasive
- I refactored some stuff along the way.

Also removes a lot of cruft:
- Using SessionSingleton to check main pages (now checked directly on
  MWKArticle, using prexisting data from mobileview)
- Removed hard-coded JSON file mapping language codes to main pages (as
  language codes did not match to ISO language codes used for iOS
  preferences)
- Removed "default" NSUserDefault for current article, which was
  previously "Main_Page" (now we just load the default site's main page
  using aforementioned approach)
- Moved setting of "default" NSUserDefaults to AppDelegate, to ensure
  they're registered deterministically at launch time—preventing race
  conditions between SessionSingleton init and default access
- Cleaned up setting of search language/site (now a proper, settable
  property & computed property pair)
- Refactored "wikipedia.org" into WMFDefaultSiteDOmain constant
- Refactored API networking utilities for easier reuse
  (WMFApiJsonResponseSerializer)

Bug: T100687
Change-Id: Ie5b7bb2194212edce46349abc550404e0b71725d
---
M MediaWikiKit/MediaWikiKit/MWKArticle.h
M MediaWikiKit/MediaWikiKit/MWKArticle.m
M MediaWikiKit/MediaWikiKit/MWKSite.h
M MediaWikiKit/MediaWikiKit/MWKSite.m
A MediaWikiKit/MediaWikiKit/MWKSiteInfo.h
A MediaWikiKit/MediaWikiKit/MWKSiteInfo.m
M Wikipedia.xcodeproj/project.pbxproj
M Wikipedia/AppDelegate.m
M Wikipedia/AssetsFile/WMFAssetsFile.h
M Wikipedia/AssetsFile/WMFAssetsFile.m
D Wikipedia/Categories/Alerts/AlertWebView.h
D Wikipedia/Categories/Alerts/AlertWebView.m
M Wikipedia/Categories/MWKArticle+WMFSharing.m
D Wikipedia/Categories/MWKArticle+isMain.h
D Wikipedia/Categories/MWKArticle+isMain.m
M Wikipedia/Categories/MWKSection+DisplayHtml.m
M Wikipedia/Networking/Fetchers/ArticleFetcher.m
A Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.h
A Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.m
M Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.h
M Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.m
A Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.h
A Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.m
M Wikipedia/Networking/WMFNetworkUtilities.h
M Wikipedia/Networking/WMFNetworkUtilities.m
M Wikipedia/Protocols/WMFArticleProtocol.m
M Wikipedia/Session/SessionSingleton.h
M Wikipedia/Session/SessionSingleton.m
M Wikipedia/View Controllers/Languages/LanguagesViewController.h
M Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
M Wikipedia/View Controllers/Navigation/Secondary/SecondaryMenuViewController.m
M Wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
M Wikipedia/View 
Controllers/WebView/Footer/SubFooters/Options/WMFOptionsFooterViewController.m
M Wikipedia/View Controllers/WebView/WebViewController.m
A WikipediaUnitTests/Fixtures/ENWikiSiteInfo.json
A WikipediaUnitTests/Fixtures/NOWikiSiteInfo.json
M WikipediaUnitTests/MWKArticle+WMFSharingTests.m
A WikipediaUnitTests/MWKSiteInfoFetcherTests.m
M WikipediaUnitTests/Utilities/NSBundle+TestAssets.m
A WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.h
A WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.m
M WikipediaUnitTests/WMFAsyncTestCase.h
M WikipediaUnitTests/WMFAsyncTestCase.m
M WikipediaUnitTests/WMFDateFormatterTests.m
44 files changed, 777 insertions(+), 444 deletions(-)

Approvals:
  Fjalapeno: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/MediaWikiKit/MediaWikiKit/MWKArticle.h 
b/MediaWikiKit/MediaWikiKit/MWKArticle.h
index 01733fc..3b10a39 100644
--- a/MediaWikiKit/MediaWikiKit/MWKArticle.h
+++ b/MediaWikiKit/MediaWikiKit/MWKArticle.h
@@ -40,6 +40,9 @@
 @property (readonly, strong, nonatomic) MWKProtectionStatus* protection;     
// required
 @property (readonly, assign, nonatomic) BOOL editable;                       
// required
 
+/// Whether or not the receiver is the main page for its @c site.
+@property (readonly, assign, nonatomic, getter = isMain) BOOL main;
+
 @property (readwrite, copy, nonatomic) NSString* thumbnailURL;   // optional; 
pulled separately via search
 @property (readwrite, copy, nonatomic) NSString* imageURL;       // optional; 
pulled in article request
 
diff --git a/MediaWikiKit/MediaWikiKit/MWKArticle.m 
b/MediaWikiKit/MediaWikiKit/MWKArticle.m
index efca10f..bf266db 100644
--- a/MediaWikiKit/MediaWikiKit/MWKArticle.m
+++ b/MediaWikiKit/MediaWikiKit/MWKArticle.m
@@ -10,6 +10,15 @@
 #import <BlocksKit/BlocksKit.h>
 #import "WikipediaAppUtils.h"
 
+typedef NS_ENUM (NSUInteger, MWKArticleSchemaVersion) {
+    /**
+     * Initial schema verison, added @c main boolean field.
+     */
+    MWKArticleSchemaVersion_1 = 1
+};
+
+static MWKArticleSchemaVersion const MWKArticleCurrentSchemaVersion = 
MWKArticleSchemaVersion_1;
+
 @interface MWKArticle ()
 
 // Identifiers
@@ -25,6 +34,7 @@
 @property (readwrite, copy, nonatomic) NSString* displaytitle;              // 
optional
 @property (readwrite, strong, nonatomic) MWKProtectionStatus* protection;     
// required
 @property (readwrite, assign, nonatomic) BOOL editable;                       
// required
+@property (readwrite, assign, nonatomic, getter = isMain) BOOL main;
 
 @property (readwrite, copy, nonatomic) NSString* entityDescription;            
// optional; currently pulled separately via wikidata
 
@@ -79,7 +89,8 @@
            && WMF_EQUAL(self.thumbnailURL, isEqualToString:, 
other.thumbnailURL)
            && WMF_EQUAL(self.imageURL, isEqualToString:, other.imageURL)
            && self.articleId == other.articleId
-           && self.languagecount == other.languagecount;
+           && self.languagecount == other.languagecount
+           && self.isMain == other.isMain;
 }
 
 - (BOOL)isDeeplyEqualToArticle:(MWKArticle*)article {
@@ -96,6 +107,8 @@
 
 - (id)dataExport {
     NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
+
+    dict[@"schemaVersion"] = @(MWKArticleCurrentSchemaVersion);
 
     if (self.redirected) {
         dict[@"redirected"] = self.redirected.prefixedText;
@@ -124,10 +137,15 @@
         dict[@"imageURL"] = self.imageURL;
     }
 
-    return [NSDictionary dictionaryWithDictionary:dict];
+    dict[@"mainpage"] = @(self.isMain);
+
+    return [dict copy];
 }
 
 - (void)importMobileViewJSON:(NSDictionary*)dict {
+    // uncomment when schema is bumped to perform migrations if necessary
+//    MWKArticleSchemaVersion schemaVersion = [dict[@"schemaVersion"] 
unsignedIntegerValue];
+
     self.lastmodified   = [self requiredDate:@"lastmodified" dict:dict];
     self.lastmodifiedby = [self requiredUser:@"lastmodifiedby" dict:dict];
     self.articleId      = [[self requiredNumber:@"id" dict:dict] intValue];
@@ -155,6 +173,23 @@
         return [[MWKSection alloc] initWithArticle:self dict:sectionData];
     }];
 
+    /*
+       mainpage might be returned w/ old JSON boolean handling, check for both 
until 1.26wmf8 is deployed everywhere
+     */
+    id mainPageValue = dict[@"mainpage"];
+    if (mainPageValue == nil) {
+        // field not present due to "empty string" behavior (see below), or 
we're loading legacy cache data
+        self.main = NO;
+    } else if ([mainPageValue isKindOfClass:[NSString class]]) {
+        // old mediawiki convention was to use a field w/ an empty string as 
"true" and omit the field for "false"
+        NSAssert([mainPageValue length] == 0, @"Assuming empty string for 
boolean field.");
+        self.main = YES;
+    } else {
+        // proper JSON boolean types!
+        NSAssert([mainPageValue isKindOfClass:[NSNumber class]], @"Expected 
main page to be a boolean. Got %@", dict);
+        self.main = [mainPageValue boolValue];
+    }
+
     if ([sectionsData count] > 0) {
         self.sections = [[MWKSectionList alloc] initWithArticle:self 
sections:sectionsData];
     }
diff --git a/MediaWikiKit/MediaWikiKit/MWKSite.h 
b/MediaWikiKit/MediaWikiKit/MWKSite.h
index 4b8ea70..c8f6339 100644
--- a/MediaWikiKit/MediaWikiKit/MWKSite.h
+++ b/MediaWikiKit/MediaWikiKit/MWKSite.h
@@ -10,16 +10,33 @@
 @class MWKTitle;
 @class MWKUser;
 
-@interface MWKSite : NSObject
+/// Represents a mediawiki instance dedicated to a specific language.
+@interface MWKSite : NSObject <NSCopying>
 
+/// The hostname for the site, defaults to @c WMFDefaultSiteDomain.
 @property (nonatomic, copy, readonly) NSString* domain;
+
+/// The language code for the site. Should be ISO 639-x/IETF BCP 47
+/// @see kCFLocaleLanguageCode
 @property (nonatomic, copy, readonly) NSString* language;
+
+///
+/// @name Computed Properties
+///
+
+- (NSURL*)apiEndpoint;
+
+- (NSURL*)mobileApiEndpoint;
 
 - (instancetype)initWithDomain:(NSString*)domain language:(NSString*)language 
NS_DESIGNATED_INITIALIZER;
 
-/// Convenience factory method wrapping the designated initializer.
+/// Create a site using @c language and the default domain.
+- (instancetype)initWithLanguage:(NSString*)language;
+
 + (instancetype)siteWithDomain:(NSString*)domain language:(NSString*)language;
 
++ (instancetype)siteWithLanguage:(NSString*)language;
+
 /// @return A site with the default domain and the language code returned by 
@c locale.
 + (instancetype)siteWithLocale:(NSLocale*)locale;
 
diff --git a/MediaWikiKit/MediaWikiKit/MWKSite.m 
b/MediaWikiKit/MediaWikiKit/MWKSite.m
index ecfd382..66def57 100644
--- a/MediaWikiKit/MediaWikiKit/MWKSite.m
+++ b/MediaWikiKit/MediaWikiKit/MWKSite.m
@@ -6,6 +6,12 @@
 
 NSString* const WMFDefaultSiteDomain = @"wikipedia.org";
 
+static NSString* const MWKSiteSchemaVersionKey = @"siteSchemaVersion";
+
+typedef NS_ENUM (NSUInteger, MWKSiteNSCodingSchemaVersion) {
+    MWKSiteNSCodingSchemaVersion_1 = 1
+};
+
 @interface MWKSite ()
 
 @property (readwrite, copy, nonatomic) NSString* domain;
@@ -24,6 +30,14 @@
         self.language = language;
     }
     return self;
+}
+
+- (instancetype)initWithLanguage:(NSString*)language {
+    return [self initWithDomain:WMFDefaultSiteDomain language:language];
+}
+
++ (instancetype)siteWithLanguage:(NSString*)language {
+    return [[self alloc] initWithLanguage:language];
 }
 
 + (MWKSite*)siteWithDomain:(NSString*)domain language:(NSString*)language {
@@ -48,6 +62,29 @@
     return [[MWKTitle alloc] initWithInternalLink:path site:self];
 }
 
+#pragma mark - Computed Properties
+
+- (NSURL*)mobileApiEndpoint {
+    return [self apiEndpoint:YES];
+}
+
+- (NSURL*)apiEndpoint {
+    return [self apiEndpoint:NO];
+}
+
+- (NSURL*)apiEndpoint:(BOOL)isMobile {
+    NSURLComponents* apiEndpointComponents = [[NSURLComponents alloc] init];
+    apiEndpointComponents.scheme = @"https";
+    NSMutableArray* hostComponents = [NSMutableArray 
arrayWithObject:self.language];
+    if (isMobile) {
+        [hostComponents addObject:@"m"];
+    }
+    [hostComponents addObject:self.domain];
+    apiEndpointComponents.host = [hostComponents 
componentsJoinedByString:@"."];
+    apiEndpointComponents.path = @"/w/api.php";
+    return [apiEndpointComponents URL];
+}
+
 #pragma mark - NSObject
 
 - (BOOL)isEqual:(id)object {
@@ -69,4 +106,9 @@
     return self.domain.hash ^ 
flipBitsWithAdditionalRotation(self.language.hash, 1);
 }
 
+- (id)copyWithZone:(NSZone*)zone {
+    // immutable
+    return self;
+}
+
 @end
diff --git a/MediaWikiKit/MediaWikiKit/MWKSiteInfo.h 
b/MediaWikiKit/MediaWikiKit/MWKSiteInfo.h
new file mode 100644
index 0000000..fa70d4c
--- /dev/null
+++ b/MediaWikiKit/MediaWikiKit/MWKSiteInfo.h
@@ -0,0 +1,39 @@
+//
+//  MWKSiteInfo.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "MWKDataObject.h"
+
+@class MWKTitle;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Type for siteinfo API responses.
+/// @see https://www.mediawiki.org/wiki/API:Siteinfo
+@interface MWKSiteInfo : NSObject <NSCopying>
+
+/// Site described by the receiver.
+@property (readonly, copy, nonatomic) MWKSite* site;
+
+/// Raw title for the receiver's main page.
+@property (readonly, copy, nonatomic) NSString* mainPageTitleText;
+
+- (instancetype)initWithSite:(MWKSite*)site
+           mainPageTitleText:(NSString*)mainPage NS_DESIGNATED_INITIALIZER;
+
+- (BOOL)isEqualToSiteInfo:(MWKSiteInfo*)siteInfo;
+
+///
+/// @name Computed Properties
+///
+
+/// @return Parsed @c MWKTitle from @c mainPage.
+- (MWKTitle*)mainPageTitle;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/MediaWikiKit/MediaWikiKit/MWKSiteInfo.m 
b/MediaWikiKit/MediaWikiKit/MWKSiteInfo.m
new file mode 100644
index 0000000..3718593
--- /dev/null
+++ b/MediaWikiKit/MediaWikiKit/MWKSiteInfo.m
@@ -0,0 +1,74 @@
+//
+//  MWKSiteInfo.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "MWKSiteInfo.h"
+#import "NSObjectUtilities.h"
+
+typedef NS_ENUM (NSUInteger, MWKSiteInfoNSCodingSchemaVersion) {
+    MWKSiteInfoNSCodingSchemaVersion_1 = 1
+};
+
+static NSString* const MWKSiteInfoNSCodingSchemaVersionKey = 
@"siteInfoSchemaVersion";
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MWKSiteInfo ()
+@property (readwrite, copy, nonatomic) MWKSite* site;
+@property (readwrite, copy, nonatomic) NSString* mainPageTitleText;
+@end
+
+@implementation MWKSiteInfo
+
+- (instancetype)initWithSite:(MWKSite*)site 
mainPageTitleText:(NSString*)mainPage {
+    self = [super init];
+    if (self) {
+        self.site              = site;
+        self.mainPageTitleText = mainPage;
+    }
+    return self;
+}
+
+- (instancetype)initWithSite:(MWKSite*)site exportedData:(NSDictionary*)data {
+    return [self initWithSite:site mainPageTitleText:data[@"mainPage"]];
+}
+
+- (NSString*)description {
+    return [NSString stringWithFormat:@"%@ {"
+            "\t site: %@,\n"
+            "\t mainPage: %@ \n"
+            "}\n", [super description], self.site, self.mainPageTitleText];
+}
+
+- (BOOL)isEqual:(id)object {
+    if (self == object) {
+        return YES;
+    } else if ([object isKindOfClass:[MWKSiteInfo class]]) {
+        return [self isEqualToSiteInfo:object];
+    } else {
+        return NO;
+    }
+}
+
+- (BOOL)isEqualToSiteInfo:(MWKSiteInfo*)siteInfo {
+    return WMF_EQUAL_PROPERTIES(self, site, isEqualToSite:, siteInfo)
+           && WMF_EQUAL_PROPERTIES(self, mainPageTitleText, isEqualToString:, 
siteInfo);
+}
+
+- (NSUInteger)hash {
+    return self.site.hash ^ 
flipBitsWithAdditionalRotation(self.mainPageTitleText.hash, 1);
+}
+
+#pragma mark - Computed Properties
+
+- (MWKTitle*)mainPageTitle {
+    return [self.site titleWithString:self.mainPageTitleText];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Wikipedia.xcodeproj/project.pbxproj 
b/Wikipedia.xcodeproj/project.pbxproj
index e4847e2..cd2778d 100644
--- a/Wikipedia.xcodeproj/project.pbxproj
+++ b/Wikipedia.xcodeproj/project.pbxproj
@@ -24,7 +24,6 @@
                04224500197F5E09005DD0BF /* AbuseFilterAlert.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 042244FC197F5E09005DD0BF /* AbuseFilterAlert.m 
*/; };
                04224501197F5E09005DD0BF /* BulletedLabel.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 042244FE197F5E09005DD0BF /* BulletedLabel.m */; 
};
                04224502197F5E09005DD0BF /* BulletedLabel.xib in Resources */ = 
{isa = PBXBuildFile; fileRef = 042244FF197F5E09005DD0BF /* BulletedLabel.xib 
*/; };
-               0424874D1A54801900A5C905 /* MWKArticle+isMain.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 0424874C1A54801900A5C905 /* MWKArticle+isMain.m 
*/; };
                042487521A54BECD00A5C905 /* MWKArticle+Convenience.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = 042487511A54BECD00A5C905 /* 
MWKArticle+Convenience.m */; };
                04272E7B1940EEBC00CC682F /* WMFAssetsFile.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 04272E791940EEBC00CC682F /* WMFAssetsFile.m */; 
};
                04292FF2185FBA70002A13FC /* SearchResultCell.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 04292FF0185FBA70002A13FC /* SearchResultCell.m 
*/; };
@@ -156,7 +155,6 @@
                04CCCFF61935094000E3F60C /* PrimaryMenuViewController.m in 
Sources */ = {isa = PBXBuildFile; fileRef = 04CCCFF31935094000E3F60C /* 
PrimaryMenuViewController.m */; };
                04CCCFF71935094000E3F60C /* PrimaryMenuTableViewCell.m in 
Sources */ = {isa = PBXBuildFile; fileRef = 04CCCFF51935094000E3F60C /* 
PrimaryMenuTableViewCell.m */; };
                04CFA120194900D50088269A /* TopMenuTextFieldContainer.m in 
Sources */ = {isa = PBXBuildFile; fileRef = 04CFA11F194900D50088269A /* 
TopMenuTextFieldContainer.m */; };
-               04D122321899B8AC006B9A30 /* AlertWebView.m in Sources */ = {isa 
= PBXBuildFile; fileRef = 04D122311899B8AC006B9A30 /* AlertWebView.m */; };
                04D149DD18877343006B4104 /* AlertLabel.m in Sources */ = {isa = 
PBXBuildFile; fileRef = 04D149DA18877343006B4104 /* AlertLabel.m */; };
                04D149DF18877343006B4104 /* UIViewController+Alert.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = 04D149DC18877343006B4104 /* 
UIViewController+Alert.m */; };
                04D308281998A8AA0034F106 /* NearbyThumbnailView.m in Sources */ 
= {isa = PBXBuildFile; fileRef = 04D308271998A8AA0034F106 /* 
NearbyThumbnailView.m */; };
@@ -193,6 +191,10 @@
                701FF5EE601DEA3FCAB7EFD3 /* libPods.a in Frameworks */ = {isa = 
PBXBuildFile; fileRef = D82982ED992F47428037BDF2 /* libPods.a */; };
                954BA118838BF8BA6B01C34A /* libPods-WikipediaUnitTests.a in 
Frameworks */ = {isa = PBXBuildFile; fileRef = 8CE61C6963F825760822A28A /* 
libPods-WikipediaUnitTests.a */; };
                BC092B961B18E89200093C59 /* NSString+WMFPageUtilities.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC092B951B18E89200093C59 /* 
NSString+WMFPageUtilities.m */; };
+               BC092B9C1B18F8D700093C59 /* MWKSiteInfo.m in Sources */ = {isa 
= PBXBuildFile; fileRef = BC092B9B1B18F8D700093C59 /* MWKSiteInfo.m */; };
+               BC092B9F1B1907FC00093C59 /* MWKSiteInfoFetcher.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BC092B9E1B1907FC00093C59 /* 
MWKSiteInfoFetcher.m */; };
+               BC092BA21B19135700093C59 /* WMFApiJsonResponseSerializer.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC092BA11B19135700093C59 /* 
WMFApiJsonResponseSerializer.m */; };
+               BC092BA71B19189100093C59 /* MWKSiteInfoFetcherTests.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC092BA61B19189100093C59 /* 
MWKSiteInfoFetcherTests.m */; };
                BC0FED621AAA0263002488D7 /* WMFCodingStyle.m in Sources */ = 
{isa = PBXBuildFile; fileRef = BC6FEAE01A9B7EFD00A1D890 /* WMFCodingStyle.m */; 
};
                BC0FED631AAA0263002488D7 /* MWKTestCase.m in Sources */ = {isa 
= PBXBuildFile; fileRef = BCB669BB1A83F6D300C7B1FE /* MWKTestCase.m */; };
                BC0FED641AAA0263002488D7 /* MWKArticleStoreTestCase.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BCB669BD1A83F6D300C7B1FE /* 
MWKArticleStoreTestCase.m */; };
@@ -222,7 +224,10 @@
                BC50C37F1A83C784006DC7AF /* WMFNetworkUtilities.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BC50C37E1A83C784006DC7AF /* 
WMFNetworkUtilities.m */; };
                BC50C3871A83CBDA006DC7AF /* MWKImageInfoResponseSerializer.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC50C3861A83CBDA006DC7AF /* 
MWKImageInfoResponseSerializer.m */; };
                BC5FE5751B1DFF5400273BC0 /* ArticleFetcherTests.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BC5FE5741B1DFF5400273BC0 /* 
ArticleFetcherTests.m */; };
+               BC5FE5701B1DF02900273BC0 /* ENWikiSiteInfo.json in Resources */ 
= {isa = PBXBuildFile; fileRef = BC5FE56F1B1DF02900273BC0 /* 
ENWikiSiteInfo.json */; };
+               BC5FE5721B1DF38A00273BC0 /* NOWikiSiteInfo.json in Resources */ 
= {isa = PBXBuildFile; fileRef = BC5FE5711B1DF38A00273BC0 /* 
NOWikiSiteInfo.json */; };
                BC69C3141AB0C1FF0090B039 /* WMFImageInfoController.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = BC69C3131AB0C1FF0090B039 /* 
WMFImageInfoController.m */; };
+               BC6BF4001B19213600362968 /* XCTestCase+WMFLocaleTesting.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC6BF3FE1B19209900362968 /* 
XCTestCase+WMFLocaleTesting.m */; };
                BC7DFCD61AA4E5FE000035C3 /* WMFImageURLParsing.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BC7DFCD51AA4E5FE000035C3 /* 
WMFImageURLParsing.m */; };
                BC86B9361A92966B00B4C039 /* 
AFHTTPRequestOperationManager+UniqueRequests.m in Sources */ = {isa = 
PBXBuildFile; fileRef = BC86B9351A92966B00B4C039 /* 
AFHTTPRequestOperationManager+UniqueRequests.m */; };
                BC86B93D1A929CC500B4C039 /* 
UICollectionViewFlowLayout+NSCopying.m in Sources */ = {isa = PBXBuildFile; 
fileRef = BC86B93C1A929CC500B4C039 /* UICollectionViewFlowLayout+NSCopying.m 
*/; };
@@ -421,8 +426,6 @@
                042244FD197F5E09005DD0BF /* BulletedLabel.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
BulletedLabel.h; sourceTree = "<group>"; };
                042244FE197F5E09005DD0BF /* BulletedLabel.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= BulletedLabel.m; sourceTree = "<group>"; };
                042244FF197F5E09005DD0BF /* BulletedLabel.xib */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = 
BulletedLabel.xib; sourceTree = "<group>"; };
-               0424874B1A54801900A5C905 /* MWKArticle+isMain.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"MWKArticle+isMain.h"; sourceTree = "<group>"; };
-               0424874C1A54801900A5C905 /* MWKArticle+isMain.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= "MWKArticle+isMain.m"; sourceTree = "<group>"; };
                042487501A54BECD00A5C905 /* MWKArticle+Convenience.h */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path 
= "MWKArticle+Convenience.h"; sourceTree = "<group>"; };
                042487511A54BECD00A5C905 /* MWKArticle+Convenience.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
path = "MWKArticle+Convenience.m"; sourceTree = "<group>"; };
                04272E781940EEBC00CC682F /* WMFAssetsFile.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; 
lineEnding = 0; path = WMFAssetsFile.h; sourceTree = "<group>"; 
xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
@@ -670,8 +673,6 @@
                04CCCFF51935094000E3F60C /* PrimaryMenuTableViewCell.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; path = PrimaryMenuTableViewCell.m; sourceTree = "<group>"; };
                04CFA11E194900D50088269A /* TopMenuTextFieldContainer.h */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; 
path = TopMenuTextFieldContainer.h; sourceTree = "<group>"; };
                04CFA11F194900D50088269A /* TopMenuTextFieldContainer.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; path = TopMenuTextFieldContainer.m; sourceTree = "<group>"; 
};
-               04D122301899B8AC006B9A30 /* AlertWebView.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
AlertWebView.h; sourceTree = "<group>"; };
-               04D122311899B8AC006B9A30 /* AlertWebView.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= AlertWebView.m; sourceTree = "<group>"; };
                04D149D918877343006B4104 /* AlertLabel.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
AlertLabel.h; sourceTree = "<group>"; };
                04D149DA18877343006B4104 /* AlertLabel.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= AlertLabel.m; sourceTree = "<group>"; };
                04D149DB18877343006B4104 /* UIViewController+Alert.h */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path 
= "UIViewController+Alert.h"; sourceTree = "<group>"; };
@@ -738,6 +739,13 @@
                8CE61C6963F825760822A28A /* libPods-WikipediaUnitTests.a */ = 
{isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; 
path = "libPods-WikipediaUnitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
                BC092B951B18E89200093C59 /* NSString+WMFPageUtilities.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; path = "NSString+WMFPageUtilities.m"; sourceTree = 
"<group>"; };
                BC092B971B18E8AF00093C59 /* NSString+WMFPageUtilities.h */ = 
{isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = 
"NSString+WMFPageUtilities.h"; sourceTree = "<group>"; };
+               BC092B9A1B18F8D700093C59 /* MWKSiteInfo.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
MWKSiteInfo.h; sourceTree = "<group>"; };
+               BC092B9B1B18F8D700093C59 /* MWKSiteInfo.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= MWKSiteInfo.m; sourceTree = "<group>"; };
+               BC092B9D1B1907FC00093C59 /* MWKSiteInfoFetcher.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
MWKSiteInfoFetcher.h; sourceTree = "<group>"; };
+               BC092B9E1B1907FC00093C59 /* MWKSiteInfoFetcher.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= MWKSiteInfoFetcher.m; sourceTree = "<group>"; };
+               BC092BA01B19135700093C59 /* WMFApiJsonResponseSerializer.h */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; 
name = WMFApiJsonResponseSerializer.h; path = 
Serializers/WMFApiJsonResponseSerializer.h; sourceTree = "<group>"; };
+               BC092BA11B19135700093C59 /* WMFApiJsonResponseSerializer.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; name = WMFApiJsonResponseSerializer.m; path = 
Serializers/WMFApiJsonResponseSerializer.m; sourceTree = "<group>"; };
+               BC092BA61B19189100093C59 /* MWKSiteInfoFetcherTests.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
path = MWKSiteInfoFetcherTests.m; sourceTree = "<group>"; };
                BC2375981AB78D8A00B0BAA8 /* 
NSParagraphStyle+WMFNaturalAlignmentStyle.h */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"NSParagraphStyle+WMFNaturalAlignmentStyle.h"; sourceTree = "<group>"; };
                BC2375991AB78D8A00B0BAA8 /* 
NSParagraphStyle+WMFNaturalAlignmentStyle.m */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = 
"NSParagraphStyle+WMFNaturalAlignmentStyle.m"; sourceTree = "<group>"; };
                BC23759D1AB8928600B0BAA8 /* WMFDateFormatterTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= WMFDateFormatterTests.m; sourceTree = "<group>"; };
@@ -754,8 +762,12 @@
                BC50C3851A83CBDA006DC7AF /* MWKImageInfoResponseSerializer.h */ 
= {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.h; name = MWKImageInfoResponseSerializer.h; path = 
Serializers/MWKImageInfoResponseSerializer.h; sourceTree = "<group>"; };
                BC50C3861A83CBDA006DC7AF /* MWKImageInfoResponseSerializer.m */ 
= {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; name = MWKImageInfoResponseSerializer.m; path = 
Serializers/MWKImageInfoResponseSerializer.m; sourceTree = "<group>"; };
                BC5FE5741B1DFF5400273BC0 /* ArticleFetcherTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= ArticleFetcherTests.m; sourceTree = "<group>"; };
+               BC5FE56F1B1DF02900273BC0 /* ENWikiSiteInfo.json */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 
ENWikiSiteInfo.json; sourceTree = "<group>"; };
+               BC5FE5711B1DF38A00273BC0 /* NOWikiSiteInfo.json */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 
NOWikiSiteInfo.json; sourceTree = "<group>"; };
                BC69C3121AB0C1FF0090B039 /* WMFImageInfoController.h */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name 
= WMFImageInfoController.h; path = "Image Gallery/WMFImageInfoController.h"; 
sourceTree = "<group>"; };
                BC69C3131AB0C1FF0090B039 /* WMFImageInfoController.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
name = WMFImageInfoController.m; path = "Image 
Gallery/WMFImageInfoController.m"; sourceTree = "<group>"; };
+               BC6BF3FD1B19209900362968 /* XCTestCase+WMFLocaleTesting.h */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; 
path = "XCTestCase+WMFLocaleTesting.h"; sourceTree = "<group>"; };
+               BC6BF3FE1B19209900362968 /* XCTestCase+WMFLocaleTesting.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; path = "XCTestCase+WMFLocaleTesting.m"; sourceTree = 
"<group>"; };
                BC6FEAE01A9B7EFD00A1D890 /* WMFCodingStyle.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= WMFCodingStyle.m; sourceTree = "<group>"; };
                BC7A4A231B17B3510003E73E /* NSObjectUtilities.h */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.c.h; path = 
NSObjectUtilities.h; sourceTree = "<group>"; };
                BC7ACB621AB34C9C00791497 /* WMFAsyncTestCase.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = 
WMFAsyncTestCase.h; path = ../WMFAsyncTestCase.h; sourceTree = "<group>"; };
@@ -1653,6 +1665,8 @@
                                0487047D19F8262600B7D307 /* 
WikiTextSectionUploader.m */,
                                BC955BC51A82BEFD000EF9E4 /* 
MWKImageInfoFetcher.h */,
                                BC955BC61A82BEFD000EF9E4 /* 
MWKImageInfoFetcher.m */,
+                               BC092B9D1B1907FC00093C59 /* 
MWKSiteInfoFetcher.h */,
+                               BC092B9E1B1907FC00093C59 /* 
MWKSiteInfoFetcher.m */,
                        );
                        path = Fetchers;
                        sourceTree = "<group>";
@@ -1797,8 +1811,6 @@
                                042487501A54BECD00A5C905 /* 
MWKArticle+Convenience.h */,
                                042487511A54BECD00A5C905 /* 
MWKArticle+Convenience.m */,
                                049B2BDA1AB9016A00CE27FF /* 
MWKArticle+ConvenienceSpecs */,
-                               0424874B1A54801900A5C905 /* MWKArticle+isMain.h 
*/,
-                               0424874C1A54801900A5C905 /* MWKArticle+isMain.m 
*/,
                                0472BC16193AD88C00C40BDA /* 
MWKSection+DisplayHtml.h */,
                                0472BC17193AD88C00C40BDA /* 
MWKSection+DisplayHtml.m */,
                                041C6204199ED2A20061516F /* MWKSection+TOC.h */,
@@ -1976,8 +1988,6 @@
                        children = (
                                04D149D918877343006B4104 /* AlertLabel.h */,
                                04D149DA18877343006B4104 /* AlertLabel.m */,
-                               04D122301899B8AC006B9A30 /* AlertWebView.h */,
-                               04D122311899B8AC006B9A30 /* AlertWebView.m */,
                                04D149DB18877343006B4104 /* 
UIViewController+Alert.h */,
                                04D149DC18877343006B4104 /* 
UIViewController+Alert.m */,
                        );
@@ -2085,6 +2095,7 @@
                                BC92A7721AFA88D3003C4212 /* 
MWKSection+WMFSharingTests.m */,
                                BCAFC5CF1AFD5E7D004615BA /* 
MWKArticle+WMFSharingTests.m */,
                                BC5FE5741B1DFF5400273BC0 /* 
ArticleFetcherTests.m */,
+                               BC092BA61B19189100093C59 /* 
MWKSiteInfoFetcherTests.m */,
                        );
                        name = Tests;
                        path = WikipediaUnitTests;
@@ -2122,6 +2133,8 @@
                                BCEC778E1AC9AEC800D9DDA5 /* 
MWKImage+AssociationTestUtils.m */,
                                BCEC77931AC9C74700D9DDA5 /* 
NSArray+WMFShuffle.h */,
                                BCEC77941AC9C74700D9DDA5 /* 
NSArray+WMFShuffle.m */,
+                               BC6BF3FD1B19209900362968 /* 
XCTestCase+WMFLocaleTesting.h */,
+                               BC6BF3FE1B19209900362968 /* 
XCTestCase+WMFLocaleTesting.m */,
                        );
                        path = Utilities;
                        sourceTree = "<group>";
@@ -2156,6 +2169,8 @@
                        children = (
                                BC50C3851A83CBDA006DC7AF /* 
MWKImageInfoResponseSerializer.h */,
                                BC50C3861A83CBDA006DC7AF /* 
MWKImageInfoResponseSerializer.m */,
+                               BC092BA01B19135700093C59 /* 
WMFApiJsonResponseSerializer.h */,
+                               BC092BA11B19135700093C59 /* 
WMFApiJsonResponseSerializer.m */,
                        );
                        name = Serializers;
                        sourceTree = "<group>";
@@ -2200,6 +2215,8 @@
                                BCB669941A83F6C300C7B1FE /* 
MWKRecentSearchList.m */,
                                BCB58F611A8A9F1000465627 /* MWKLicense.h */,
                                BCB58F621A8A9F1000465627 /* MWKLicense.m */,
+                               BC092B9A1B18F8D700093C59 /* MWKSiteInfo.h */,
+                               BC092B9B1B18F8D700093C59 /* MWKSiteInfo.m */,
                        );
                        name = "Data classes";
                        sourceTree = "<group>";
@@ -2274,6 +2291,8 @@
                                BCD41DE71B11CC5800231BB1 /* test-notes.txt */,
                                BCD41DE81B11CC5800231BB1 /* user-anon.json */,
                                BCD41DE91B11CC5800231BB1 /* user-loggedin.json 
*/,
+                               BC5FE56F1B1DF02900273BC0 /* ENWikiSiteInfo.json 
*/,
+                               BC5FE5711B1DF38A00273BC0 /* NOWikiSiteInfo.json 
*/,
                        );
                        path = Fixtures;
                        sourceTree = "<group>";
@@ -2759,6 +2778,8 @@
                                BCD41DF61B11CC5E00231BB1 /* user-loggedin.json 
in Resources */,
                                BCCEC12A1B1F68CF00A8B522 /* test-notes.txt in 
Resources */,
                                BCCEC1271B1F68CF00A8B522 /* section0.json in 
Resources */,
+                               BC5FE5721B1DF38A00273BC0 /* NOWikiSiteInfo.json 
in Resources */,
+                               BC5FE5701B1DF02900273BC0 /* ENWikiSiteInfo.json 
in Resources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
@@ -2941,6 +2962,7 @@
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
+                               BC6BF4001B19213600362968 /* 
XCTestCase+WMFLocaleTesting.m in Sources */,
                                0EBC56971AD5B69300E82CDD /* 
BITHockeyManager+WMFExtensions.m in Sources */,
                                BC2375C11ABB14CC00B0BAA8 /* 
WMFArticleImageInjectionTests.m in Sources */,
                                043B6E8F1ACDE0CF0005C60B /* 
NSAttributedString+WMFSavedPagesAttributedStrings.m in Sources */,
@@ -2977,6 +2999,7 @@
                                BCAFC5D01AFD5E7D004615BA /* 
MWKArticle+WMFSharingTests.m in Sources */,
                                BC0FED691AAA0268002488D7 /* 
MWKProtectionStatusTests.m in Sources */,
                                BC0FED6C1AAA0268002488D7 /* 
MWKImageStorageTests.m in Sources */,
+                               BC092BA71B19189100093C59 /* 
MWKSiteInfoFetcherTests.m in Sources */,
                                BC0FED721AAA026C002488D7 /* 
WMFErrorForApiErrorObjectTests.m in Sources */,
                                BCA6765A1AC0600500A16160 /* 
MWKDataStore+TemporaryDataStore.m in Sources */,
                                04F122671ACB818F002FC3B5 /* 
NSString+FormattedAttributedString.m in Sources */,
@@ -3028,7 +3051,7 @@
                                0449E63918AAA26A00D51524 /* 
NSHTTPCookieStorage+CloneCookie.m in Sources */,
                                04C695CE18ED08D900D9F2DA /* 
UIView+WMFSearchSubviews.m in Sources */,
                                0487048E19F8262600B7D307 /* 
WikipediaZeroMessageFetcher.m in Sources */,
-                               0424874D1A54801900A5C905 /* MWKArticle+isMain.m 
in Sources */,
+                               BC092BA21B19135700093C59 /* 
WMFApiJsonResponseSerializer.m in Sources */,
                                D4F277FB194235A00032BA38 /* 
ProtectedEditAttemptFunnel.m in Sources */,
                                0487047E19F8262600B7D307 /* 
AccountCreationTokenFetcher.m in Sources */,
                                049566C218F5F4CB0058EA12 /* ZeroConfigState.m 
in Sources */,
@@ -3056,6 +3079,7 @@
                                04F0E2EA186EDC1A00468738 /* 
UIWebView+ElementLocation.m in Sources */,
                                C42D94871A937DE000A4871A /* 
WMFProgressLineView.m in Sources */,
                                0447866F1852B5010050563B /* SessionSingleton.m 
in Sources */,
+                               BC092B9F1B1907FC00093C59 /* 
MWKSiteInfoFetcher.m in Sources */,
                                BCB669AA1A83F6C400C7B1FE /* MWKImage.m in 
Sources */,
                                D4E8A8A4190835C100DA4765 /* DataMigrator.m in 
Sources */,
                                BCB3AEA71AC1DDE4004AD205 /* 
OldDataSchemaMigrator.m in Sources */,
@@ -3101,6 +3125,7 @@
                                C42D94861A937DE000A4871A /* WMFBorderButton.m 
in Sources */,
                                BC92A7711AFA841C003C4212 /* 
MWKSection+WMFSharing.m in Sources */,
                                047801BE18AE987900DBB747 /* 
UIButton+ColorMask.m in Sources */,
+                               BC092B9C1B18F8D700093C59 /* MWKSiteInfo.m in 
Sources */,
                                BC86B93D1A929CC500B4C039 /* 
UICollectionViewFlowLayout+NSCopying.m in Sources */,
                                04D686C91AB28FE40009B44A /* 
UIImage+WMFFocalImageDrawing.m in Sources */,
                                BC092B961B18E89200093C59 /* 
NSString+WMFPageUtilities.m in Sources */,
@@ -3162,7 +3187,6 @@
                                04D686F81AB2949C0009B44A /* PaddedLabel.m in 
Sources */,
                                04C695D218ED213000D9F2DA /* 
UIScrollView+NoHorizontalScrolling.m in Sources */,
                                04F0E2EE186FB2D100468738 /* 
TOCSectionCellView.m in Sources */,
-                               04D122321899B8AC006B9A30 /* AlertWebView.m in 
Sources */,
                                04B0E3EA1AE8252800379AE0 /* NSURL+WMFRest.m in 
Sources */,
                                040D835E1AB0EE45000896D5 /* WMFGeometry.c in 
Sources */,
                                04C0A0781936786000D55325 /* 
UIViewController+ModalPresent.m in Sources */,
diff --git a/Wikipedia/AppDelegate.m b/Wikipedia/AppDelegate.m
index 952e7b0..dbab9b6 100644
--- a/Wikipedia/AppDelegate.m
+++ b/Wikipedia/AppDelegate.m
@@ -11,6 +11,25 @@
 #import "UIWindow+WMFMainScreenWindow.h"
 #import "WikipediaAppUtils.h"
 
+/**
+ * Register default application preferences.
+ * @note This function must be a static constructor so that it's invoked when 
the translation unit is loaded, since
+ *       waiting until application launch doesn't register them before unit 
tests are run.
+ */
+__attribute__((constructor)) static void WMFRegisterDefaultPrefs() {
+    NSString* defaultLanguage = [[NSLocale currentLocale] 
objectForKey:NSLocaleLanguageCode];
+    [[NSUserDefaults standardUserDefaults] registerDefaults:@{
+         @"CurrentArticleDomain": defaultLanguage,
+         @"Domain": defaultLanguage,
+         @"ZeroWarnWhenLeaving": @YES,
+         @"ZeroOnDialogShownOnce": @NO,
+         @"FakeZeroOn": @NO,
+         @"ShowOnboarding": @YES,
+         @"LastHousekeepingDate": [NSDate date],
+         @"SendUsageReports": @YES,
+         @"AccessSavedPagesMessageShown": @NO
+     }];
+}
 
 @interface AppDelegate ()
 <DataMigrationProgressDelegate>
diff --git a/Wikipedia/AssetsFile/WMFAssetsFile.h 
b/Wikipedia/AssetsFile/WMFAssetsFile.h
index cd970d6..5a27eef 100644
--- a/Wikipedia/AssetsFile/WMFAssetsFile.h
+++ b/Wikipedia/AssetsFile/WMFAssetsFile.h
@@ -3,8 +3,7 @@
 typedef NS_ENUM (NSUInteger, WMFAssetsFileType) {
     WMFAssetsFileTypeUndefined = 0,
     WMFAssetsFileTypeConfig,
-    WMFAssetsFileTypeLanguages,
-    WMFAssetsFileTypeMainPages,
+    WMFAssetsFileTypeLanguages
 };
 
 @interface WMFAssetsFile : NSObject
diff --git a/Wikipedia/AssetsFile/WMFAssetsFile.m 
b/Wikipedia/AssetsFile/WMFAssetsFile.m
index cbe8a0d..a72c232 100644
--- a/Wikipedia/AssetsFile/WMFAssetsFile.m
+++ b/Wikipedia/AssetsFile/WMFAssetsFile.m
@@ -67,19 +67,10 @@
     switch (self.fileType) {
         case WMFAssetsFileTypeConfig:
             return @"ios.json";
-            break;
-
         case WMFAssetsFileTypeLanguages:
             return @"languages.json";
-            break;
-
-        case WMFAssetsFileTypeMainPages:
-            return @"mainpages.json";
-            break;
-
         default:
             return nil;
-            break;
     }
 }
 
diff --git a/Wikipedia/Categories/Alerts/AlertWebView.h 
b/Wikipedia/Categories/Alerts/AlertWebView.h
deleted file mode 100644
index 9da4f5d..0000000
--- a/Wikipedia/Categories/Alerts/AlertWebView.h
+++ /dev/null
@@ -1,12 +0,0 @@
-//  Created by Monte Hurd on 1/29/14.
-//  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
-
-#import <UIKit/UIKit.h>
-
-@interface AlertWebView : UIView <UIWebViewDelegate>
-
-- (instancetype)initWithHtml:(NSString*)html
-                 bannerImage:(UIImage*)bannerImage
-                 bannerColor:(UIColor*)bannerColor;
-
-@end
diff --git a/Wikipedia/Categories/Alerts/AlertWebView.m 
b/Wikipedia/Categories/Alerts/AlertWebView.m
deleted file mode 100644
index c52550d..0000000
--- a/Wikipedia/Categories/Alerts/AlertWebView.m
+++ /dev/null
@@ -1,172 +0,0 @@
-//  Created by Monte Hurd on 1/29/14.
-//  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
-
-#import "AlertWebView.h"
-#import "SessionSingleton.h"
-
-#define ALERT_WEB_VIEW_BAR_HEIGHT 50
-#define ALERT_WEB_VIEW_BANNER_BUTTON_HEIGHT 120
-
-@interface AlertWebView ()
-
-@property (strong, nonatomic) UIWebView* webView;
-@property (strong, nonatomic) UIButton* bannerButton;
-
-@end
-
-@implementation AlertWebView
-
-- (instancetype)initWithHtml:(NSString*)html
-                 bannerImage:(UIImage*)bannerImage
-                 bannerColor:(UIColor*)bannerColor {
-    self = [super init];
-    if (self) {
-        self.backgroundColor = [UIColor whiteColor];
-
-        self.webView                 = [[UIWebView alloc] init];
-        self.webView.backgroundColor = [UIColor whiteColor];
-
-        self.webView.delegate = self;
-        self.bannerButton     = [[UIButton alloc] init];
-
-        self.webView.translatesAutoresizingMaskIntoConstraints      = NO;
-        self.bannerButton.translatesAutoresizingMaskIntoConstraints = NO;
-
-        self.bannerButton.backgroundColor = bannerColor;
-
-        [self.bannerButton setImage:bannerImage forState:UIControlStateNormal];
-
-        [self.bannerButton setAdjustsImageWhenHighlighted:NO];
-
-        [self.bannerButton addTarget:self action:@selector(tap) 
forControlEvents:UIControlEventTouchUpInside];
-
-        self.userInteractionEnabled              = YES;
-        self.bannerButton.userInteractionEnabled = YES;
-
-        [self addSubview:self.webView];
-        [self addSubview:self.bannerButton];
-
-        // If a banner image was specified, but no HTML, make the web view 
background transparent.
-        if (!html || html.length == 0) {
-            self.backgroundColor = [UIColor clearColor];
-            [self.webView setBackgroundColor:[UIColor clearColor]];
-            [self.webView setOpaque:NO];
-        }
-
-        NSURL* baseUrl = [[SessionSingleton sharedInstance] 
urlForLanguage:[SessionSingleton sharedInstance].currentArticleSite.language];
-
-        [self.webView loadHTMLString:html baseURL:baseUrl];
-    }
-    return self;
-}
-
-// Force web view links to open in Safari.
-// From: http://stackoverflow.com/a/2532884
-- (BOOL)webView:(UIWebView*)webView 
shouldStartLoadWithRequest:(NSURLRequest*)request 
navigationType:(UIWebViewNavigationType)navigationType;
-{
-    NSURL* requestURL = [request URL];
-    if (
-        (
-            [[requestURL scheme] isEqualToString:@"http"]
-            ||
-            [[requestURL scheme] isEqualToString:@"https"]
-            ||
-            [[requestURL scheme] isEqualToString:@"mailto"])
-        && (navigationType == UIWebViewNavigationTypeLinkClicked)
-        ) {
-        return ![[UIApplication sharedApplication] openURL:requestURL];
-    }
-    return YES;
-}
-
-- (void)tap {
-    [self removeFromSuperview];
-}
-
-- (void)removeFromSuperview {
-    [[NSNotificationCenter defaultCenter] 
postNotificationName:@"HtmlAlertWasHidden" object:self userInfo:nil];
-
-    [super removeFromSuperview];
-}
-
-- (void)updateConstraints {
-    [super updateConstraints];
-    [self addConstraints];
-}
-
-- (void)addConstraints {
-    [self removeConstraints:self.constraints];
-
-    NSDictionary* viewsDictionary = @{
-        @"webView": self.webView,
-        @"bannerButton": self.bannerButton
-    };
-
-    CGFloat bannerButtonHeight = (self.bannerButton.imageView.image) ? 
ALERT_WEB_VIEW_BANNER_BUTTON_HEIGHT : 0;
-    CGFloat barHeight          = ALERT_WEB_VIEW_BAR_HEIGHT;
-
-    UIInterfaceOrientation orientation = [[UIApplication sharedApplication] 
statusBarOrientation];
-    if (UIInterfaceOrientationIsLandscape(orientation)) {
-        bannerButtonHeight /= 2;
-        barHeight          /= 1.5;
-    }
-
-    NSDictionary* metrics = @{
-        @"barHeight": @(barHeight),
-        @"bannerButtonHeight": @(bannerButtonHeight)
-    };
-
-    NSArray* viewConstraintArrays = @
-    [
-        [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[webView]|"
-                                                options:0
-                                                metrics:nil
-                                                  views:viewsDictionary],
-
-        [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[bannerButton]|"
-                                                options:0
-                                                metrics:nil
-                                                  views:viewsDictionary],
-
-        [NSLayoutConstraint 
constraintsWithVisualFormat:@"V:|[bannerButton(bannerButtonHeight)][webView]|"
-                                                options:0
-                                                metrics:metrics
-                                                  views:viewsDictionary],
-    ];
-
-    [self addConstraints:[viewConstraintArrays 
valueForKeyPath:@"@unionOfArrays.self"]];
-}
-
-- (void)layoutSubviews {
-    [self setNeedsDisplay];
-    [self setNeedsUpdateConstraints];
-    [super layoutSubviews];
-}
-
-- (void)drawRect:(CGRect)rect {
-    [super drawRect:rect];
-    CGContextRef context = UIGraphicsGetCurrentContext();
-
-    if (!self.bannerButton.imageView.image) {
-        CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMinY(rect) 
+ ALERT_WEB_VIEW_BAR_HEIGHT);
-        CGContextAddLineToPoint(context, CGRectGetMaxX(rect), 
CGRectGetMinY(rect) + ALERT_WEB_VIEW_BAR_HEIGHT);
-    }
-
-    CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMinY(rect));
-    CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMinY(rect));
-
-    CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] 
CGColor]);
-    CGContextSetLineWidth(context, 1.0f / [UIScreen mainScreen].scale);
-    CGContextStrokePath(context);
-}
-
-/*
-   // Only override drawRect: if you perform custom drawing.
-   // An empty implementation adversely affects performance during animation.
-   - (void)drawRect:(CGRect)rect
-   {
-    // Drawing code
-   }
- */
-
-@end
diff --git a/Wikipedia/Categories/MWKArticle+WMFSharing.m 
b/Wikipedia/Categories/MWKArticle+WMFSharing.m
index d949873..46cd4e7 100644
--- a/Wikipedia/Categories/MWKArticle+WMFSharing.m
+++ b/Wikipedia/Categories/MWKArticle+WMFSharing.m
@@ -7,7 +7,6 @@
 //
 
 #import "MWKArticle+WMFSharing.h"
-#import "MWKArticle+isMain.h"
 #import "NSString+WMFHTMLParsing.h"
 #import "MWKSection+WMFSharing.h"
 #import <BlocksKit/BlocksKit.h>
diff --git a/Wikipedia/Categories/MWKArticle+isMain.h 
b/Wikipedia/Categories/MWKArticle+isMain.h
deleted file mode 100644
index e64dc8a..0000000
--- a/Wikipedia/Categories/MWKArticle+isMain.h
+++ /dev/null
@@ -1,10 +0,0 @@
-//  Created by Monte Hurd on 12/31/14.
-//  Copyright (c) 2014 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
-
-#import "MWKArticle.h"
-
-@interface MWKArticle (isMain)
-
-- (BOOL)isMain;
-
-@end
diff --git a/Wikipedia/Categories/MWKArticle+isMain.m 
b/Wikipedia/Categories/MWKArticle+isMain.m
deleted file mode 100644
index 0b9f549..0000000
--- a/Wikipedia/Categories/MWKArticle+isMain.m
+++ /dev/null
@@ -1,13 +0,0 @@
-//  Created by Monte Hurd on 12/31/14.
-//  Copyright (c) 2014 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
-
-#import "MWKArticle+isMain.h"
-#import "SessionSingleton.h"
-
-@implementation MWKArticle (isMain)
-
-- (BOOL)isMain {
-    return [[SessionSingleton sharedInstance] articleIsAMainArticle:self];
-}
-
-@end
diff --git a/Wikipedia/Categories/MWKSection+DisplayHtml.m 
b/Wikipedia/Categories/MWKSection+DisplayHtml.m
index 6b00c8f..8dd330b 100644
--- a/Wikipedia/Categories/MWKSection+DisplayHtml.m
+++ b/Wikipedia/Categories/MWKSection+DisplayHtml.m
@@ -10,7 +10,7 @@
 @implementation MWKSection (DisplayHtml)
 
 - (NSString*)displayHTML:(NSString*)html {
-    BOOL isMainPage = [[SessionSingleton sharedInstance] 
articleIsAMainArticle:[SessionSingleton sharedInstance].currentArticle];
+    BOOL isMainPage = [SessionSingleton sharedInstance].currentArticle.isMain;
 
     return
         [NSString stringWithFormat:@"<div 
id='section_heading_and_content_block_%ld'>%@<div id='content_block_%ld' 
class='content_block'>%@%@</div></div>",
diff --git a/Wikipedia/Networking/Fetchers/ArticleFetcher.m 
b/Wikipedia/Networking/Fetchers/ArticleFetcher.m
index 5a45896..aadb1fd 100644
--- a/Wikipedia/Networking/Fetchers/ArticleFetcher.m
+++ b/Wikipedia/Networking/Fetchers/ArticleFetcher.m
@@ -143,7 +143,6 @@
 - (NSDictionary*)getParamsForTitle:(NSString*)title {
     NSMutableDictionary* params = @{
         @"format": @"json",
-        @"formatversion": @2,
         @"action": @"mobileview",
         @"sectionprop": WMFJoinedPropertyParameters(@[@"toclevel", @"line", 
@"anchor", @"level", @"number",
                                                       @"fromtitle", @"index"]),
diff --git a/Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.h 
b/Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.h
new file mode 100644
index 0000000..cdc848f
--- /dev/null
+++ b/Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.h
@@ -0,0 +1,32 @@
+//
+//  MWKSiteInfoFetcher.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "FetcherBase.h"
+
+@class MWKSiteInfo;
+@class MWKSite;
+@class AFHTTPRequestOperationManager;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MWKSiteInfoFetcher : FetcherBase
+@property (strong, nonatomic) AFHTTPRequestOperationManager* requestManager;
+
+/// Attempt to fetch siteinfo for the given site.
+- (void)fetchInfoForSite:(MWKSite*)site
+                 success:(void (^)(MWKSiteInfo*))success
+                 failure:(void (^)(NSError*))failure;
+
+- (void)fetchInfoForSite:(MWKSite*)site
+                 success:(void (^)(MWKSiteInfo*))success
+                 failure:(void (^)(NSError*))failure
+           callbackQueue:(dispatch_queue_t)queue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.m 
b/Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.m
new file mode 100644
index 0000000..0a308a1
--- /dev/null
+++ b/Wikipedia/Networking/Fetchers/MWKSiteInfoFetcher.m
@@ -0,0 +1,70 @@
+//
+//  MWKSiteInfoFetcher.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "MWKSiteInfoFetcher.h"
+#import "AFHTTPRequestOperationManager+WMFConfig.h"
+#import "AFHTTPRequestOperationManager+UniqueRequests.h"
+#import "WMFNetworkUtilities.h"
+#import "WMFApiJsonResponseSerializer.h"
+#import "MWKSite.h"
+#import "MWKSiteInfo.h"
+
+@interface MWKSiteInfoFetcher ()
+@end
+
+@implementation MWKSiteInfoFetcher
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.requestManager                    = 
[AFHTTPRequestOperationManager wmf_createDefaultManager];
+        self.requestManager.responseSerializer = [WMFApiJsonResponseSerializer 
serializer];
+    }
+    return self;
+}
+
+- (void)fetchInfoForSite:(MWKSite*)site success:(void 
(^)(MWKSiteInfo*))success failure:(void (^)(NSError*))error {
+    return [self fetchInfoForSite:site success:success failure:error 
callbackQueue:dispatch_get_main_queue()];
+}
+
+- (void)fetchInfoForSite:(MWKSite*)site
+                 success:(void (^)(MWKSiteInfo*))success
+                 failure:(void (^)(NSError*))failure
+           callbackQueue:(dispatch_queue_t)queue {
+    NSParameterAssert(site);
+    NSDictionary* params = @{
+        @"action": @"query",
+        @"meta": @"siteinfo",
+        @"format": @"json",
+        @"siprop": @"general"
+    };
+    [self.requestManager wmf_idempotentGET:site.apiEndpoint.absoluteString
+                                parameters:params
+                                   success:^(AFHTTPRequestOperation* op, 
NSDictionary* json) {
+        NSDictionary* generalProps = [json valueForKeyPath:@"query.general"];
+        MWKSiteInfo* info = [[MWKSiteInfo alloc] initWithSite:site 
mainPageTitleText:generalProps[@"mainpage"]];
+        dispatch_async(queue, ^{
+            [self.fetchFinishedDelegate fetchFinished:self
+                                          fetchedData:info
+                                               
status:FETCH_FINAL_STATUS_SUCCEEDED
+                                                error:nil];
+            success(info);
+        });
+    }
+                                   failure:^(AFHTTPRequestOperation* op, 
NSError* error) {
+        dispatch_async(queue, ^{
+            [self.fetchFinishedDelegate fetchFinished:self
+                                          fetchedData:nil
+                                               status:error.wmf_fetchStatus
+                                                error:nil];
+            failure(error);
+        });
+    }];
+}
+
+@end
diff --git a/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.h 
b/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.h
index 76601e7..16e8bd5 100644
--- a/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.h
+++ b/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.h
@@ -6,10 +6,9 @@
 //  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
 //
 
-#import <AFNetworking/AFURLResponseSerialization.h>
+#import "WMFApiJsonResponseSerializer.h"
 
-
-@interface MWKImageInfoResponseSerializer : AFJSONResponseSerializer
+@interface MWKImageInfoResponseSerializer : WMFApiJsonResponseSerializer
 
 + (NSArray*)requiredExtMetadataKeys;
 
diff --git a/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.m 
b/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.m
index 1c506fe..17c4181 100644
--- a/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.m
+++ b/Wikipedia/Networking/Serializers/MWKImageInfoResponseSerializer.m
@@ -7,7 +7,6 @@
 //
 
 #import "MWKImageInfoResponseSerializer.h"
-#import "WMFNetworkUtilities.h"
 #import "MWKImageInfo.h"
 #import "NSString+WMFHTMLParsing.h"
 
@@ -39,18 +38,9 @@
              ExtMetadataArtistKey];
 }
 
-- (id)responseObjectForResponse:(NSURLResponse*)response
-                           data:(NSData*)data
-                          error:(NSError* __autoreleasing*)error {
+- (id)responseObjectForResponse:(NSURLResponse*)response data:(NSData*)data 
error:(NSError* __autoreleasing*)error {
     NSDictionary* json = [super responseObjectForResponse:response data:data 
error:error];
-    if (!json || *error) {
-        return nil;
-    }
-    NSDictionary* apiError = json[@"error"];
-    if (apiError) {
-        if (error) {
-            *error = WMFErrorForApiErrorObject(apiError);
-        }
+    if (!json) {
         return nil;
     }
     NSDictionary* indexedImages     = json[@"query"][@"pages"];
diff --git a/Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.h 
b/Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.h
new file mode 100644
index 0000000..0889acf
--- /dev/null
+++ b/Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.h
@@ -0,0 +1,12 @@
+//
+//  WMFApiJsonResponseSerializer.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import <AFNetworking/AFURLResponseSerialization.h>
+
+@interface WMFApiJsonResponseSerializer : AFJSONResponseSerializer
+@end
diff --git a/Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.m 
b/Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.m
new file mode 100644
index 0000000..b28d9b4
--- /dev/null
+++ b/Wikipedia/Networking/Serializers/WMFApiJsonResponseSerializer.m
@@ -0,0 +1,35 @@
+//
+//  WMFApiJsonResponseSerializer.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "WMFApiJsonResponseSerializer.h"
+#import "WMFNetworkUtilities.h"
+
+@implementation WMFApiJsonResponseSerializer
+
+- (id)parseJSON:(id)json error:(NSError**)error {
+    return json;
+}
+
+- (id)responseObjectForResponse:(NSURLResponse*)response
+                           data:(NSData*)data
+                          error:(NSError* __autoreleasing*)error {
+    NSDictionary* json = [super responseObjectForResponse:response data:data 
error:error];
+    if (!json || *error) {
+        return nil;
+    }
+    NSDictionary* apiError = json[@"error"];
+    if (apiError) {
+        if (error) {
+            *error = WMFErrorForApiErrorObject(apiError);
+        }
+        return nil;
+    }
+    return json;
+}
+
+@end
diff --git a/Wikipedia/Networking/WMFNetworkUtilities.h 
b/Wikipedia/Networking/WMFNetworkUtilities.h
index 07f9af1..821a16b 100644
--- a/Wikipedia/Networking/WMFNetworkUtilities.h
+++ b/Wikipedia/Networking/WMFNetworkUtilities.h
@@ -10,7 +10,7 @@
 
 /// @name Constants
 
-FOUNDATION_EXPORT NSString* const WMFNetworkingErrorDomain;
+extern NSString* const WMFNetworkingErrorDomain;
 
 typedef NS_ENUM (NSInteger, WMFNetworkingError) {
     WMFNetworkingError_APIError
@@ -23,6 +23,14 @@
  * Take an array of strings and concatenate them with "|" as a delimiter.
  * @return A string of the concatenated elements, or an empty string if @c 
props is empty or @c nil.
  */
-FOUNDATION_EXPORT NSString* WMFJoinedPropertyParameters(NSArray* props);
+extern NSString* WMFJoinedPropertyParameters(NSArray* props);
 
-FOUNDATION_EXPORT NSError* WMFErrorForApiErrorObject(NSDictionary* apiError);
+extern NSError* WMFErrorForApiErrorObject(NSDictionary* apiError);
+
+#import "FetcherBase.h"
+
+@interface NSError (WMFFetchFinalStatus)
+
+- (FetchFinalStatus)wmf_fetchStatus;
+
+@end
diff --git a/Wikipedia/Networking/WMFNetworkUtilities.m 
b/Wikipedia/Networking/WMFNetworkUtilities.m
index 5c8cfa9..366a317 100644
--- a/Wikipedia/Networking/WMFNetworkUtilities.m
+++ b/Wikipedia/Networking/WMFNetworkUtilities.m
@@ -28,4 +28,13 @@
     maybeMapApiToUserInfo(NSLocalizedFailureReasonErrorKey, @"info");
     maybeMapApiToUserInfo(NSLocalizedRecoverySuggestionErrorKey, @"*");
     return [NSError errorWithDomain:WMFNetworkingErrorDomain 
code:WMFNetworkingError_APIError userInfo:userInfoBuilder];
-}
\ No newline at end of file
+}
+
+@implementation NSError (WMFFetchFinalStatus)
+
+- (FetchFinalStatus)wmf_fetchStatus {
+    return ([self.domain isEqual:NSURLErrorDomain] && self.code == 
NSURLErrorCancelled) ?
+           FETCH_FINAL_STATUS_CANCELLED : FETCH_FINAL_STATUS_FAILED;
+}
+
+@end
\ No newline at end of file
diff --git a/Wikipedia/Protocols/WMFArticleProtocol.m 
b/Wikipedia/Protocols/WMFArticleProtocol.m
index 7dee18e..5759009 100644
--- a/Wikipedia/Protocols/WMFArticleProtocol.m
+++ b/Wikipedia/Protocols/WMFArticleProtocol.m
@@ -28,7 +28,7 @@
 - (void)startLoading {
     NSString* value = [self.request.URL wmf_getValue];
     if ([value isEqualToString:@"is-main-page"]) {
-        BOOL isMainPage = [[SessionSingleton sharedInstance] 
articleIsAMainArticle:[SessionSingleton sharedInstance].currentArticle];
+        BOOL isMainPage = [SessionSingleton 
sharedInstance].currentArticle.isMain;
         NSData* data    = [[@(isMainPage)stringValue] 
dataUsingEncoding:NSUTF8StringEncoding];
 
         [self sendResponseWithData:data];
diff --git a/Wikipedia/Session/SessionSingleton.h 
b/Wikipedia/Session/SessionSingleton.h
index dc65dbc..ddc0a16 100644
--- a/Wikipedia/Session/SessionSingleton.h
+++ b/Wikipedia/Session/SessionSingleton.h
@@ -13,7 +13,11 @@
 @class MWKTitle;
 @class MWKArticle;
 
+#warning FIXME: setting ivar values on SessionSingleton is not thread safe!
+
 @interface SessionSingleton : NSObject
+
+- (instancetype)initWithDataStore:(MWKDataStore*)dataStore;
 
 + (SessionSingleton*)sharedInstance;
 
@@ -27,48 +31,15 @@
 @property (strong, nonatomic, readonly) MWKUserDataStore* userDataStore;
 
 /**
- *  Set the language for searches.
- *  Setting this will update the searchSite with the new language.
- *  Initial value will be the device language.
- *
- *  @param language The new language for searches
+ * Language code used as a component in the Wikipedia host name: 
<code>searchLanguage + "wikipedia.org"</code>.
+ * @note This is usually RFC 639-x or BCP-47, but not always. Some language 
wikis either don't have a standard language
+ *       code or are another weird edge case.
  */
-- (void)setSearchLanguage:(NSString*)language;
+@property (copy, nonatomic) NSString* searchLanguage;
 
-/**
- *  The search site. This set automatically when setting the search language.
- *
- *  Initial value will be the site for the device language.
- *  This will never be nil.
- */
-@property (strong, nonatomic, readonly) MWKSite* searchSite;
 
-/**
- *  Main Article Title for current search site and language
- */
-@property (nonatomic, strong, readonly) MWKTitle* mainArticleTitle;
-
-/**
- *  Main Article Title for the specified language code
- *
- *  @param site The site to use for the main article
- *  @param code The language for the main article
- *
- *  @return The main article
- */
-- (MWKTitle*)mainArticleTitleForSite:(MWKSite*)site 
languageCode:(NSString*)code;
-
-/**
- *  Determine if an article is "a" main article.
- *  This will return yes if the article is the main article for the given 
article's domain.
- *  This is agnostic to the searchSite. For example:
- *  This will return YES for the french main article even if the searchSite is 
set to english.
- *
- *  @param article The article to test
- *
- *  @return Yes if the article is a main article, otherwise no
- */
-- (BOOL)articleIsAMainArticle:(MWKArticle*)article;
+/// @return Site initialized with @c searchLanguage and the default domain.
+- (MWKSite*)searchSite;
 
 /**
  *  The current article's site. This set automatically when setting the 
current article.
@@ -100,16 +71,14 @@
  */
 @property (nonatomic, assign) MWKHistoryDiscoveryMethod 
currentArticleDiscoveryMethod;
 
-
 @property (strong, nonatomic, readonly) NSString* searchApiUrl;
 
-- (NSString*)searchApiUrlForLanguage:(NSString*)language;
+@property (nonatomic) BOOL fallback __deprecated; //< Is this really necessary?
+
+- (NSString*)searchApiUrlForLanguage:(NSString*)language __deprecated_msg("Use 
-[MWKSite apiEndpoint] instead.");
 - (NSString*)searchLanguage;
 
-@property (nonatomic) BOOL fallback;
-
-- (NSURL*)urlForLanguage:(NSString*)language;
-
+- (NSURL*)urlForLanguage:(NSString*)language __deprecated_msg("Use -[MWKSite 
apiEndpoint] instead.");
 
 // Search and Nearby fetch thumbnails which are tossed in the tmp dir so we
 // don't have to worry about pruning. However, when we then load an article
@@ -117,7 +86,5 @@
 // data store. This dictionary gives us an easy place to see what temp thumb
 // file is known to be associated with an article title.
 @property (strong, nonatomic) NSMutableDictionary* titleToTempDirThumbURLMap;
-
-- (instancetype)initWithDataStore:(MWKDataStore*)dataStore;
 
 @end
diff --git a/Wikipedia/Session/SessionSingleton.m 
b/Wikipedia/Session/SessionSingleton.m
index 84cd049..39940d4 100644
--- a/Wikipedia/Session/SessionSingleton.m
+++ b/Wikipedia/Session/SessionSingleton.m
@@ -23,17 +23,16 @@
 @end
 
 @implementation SessionSingleton
-
 @synthesize currentArticleSite = _currentArticleSite;
 @synthesize currentArticle     = _currentArticle;
 
 #pragma mark - Setup
 
 + (SessionSingleton*)sharedInstance {
-    static dispatch_once_t once;
-    static id sharedInstance;
-    dispatch_once(&once, ^{
-        sharedInstance = [[self alloc] init];
+    static dispatch_once_t onceToken;
+    static SessionSingleton* sharedInstance;
+    dispatch_once(&onceToken, ^{
+        sharedInstance = [self new];
     });
     return sharedInstance;
 }
@@ -52,9 +51,8 @@
 - (instancetype)initWithDataStore:(MWKDataStore*)dataStore {
     self = [super init];
     if (self) {
+        #warning FIXME: move to AppDelegate, if we should be doing this at all 
(slows down app launch)
         [WikipediaAppUtils copyAssetsFolderToAppDataDocuments];
-
-        [self registerStandardUserDefaults];
 
         URLCache* urlCache = [[URLCache alloc] 
initWithMemoryCapacity:MegabytesToBytes(64)
                                                          
diskCapacity:MegabytesToBytes(64)
@@ -75,41 +73,6 @@
     return self;
 }
 
-- (void)registerStandardUserDefaults {
-    NSString* systemLang = [[NSLocale preferredLanguages] objectAtIndex:0];
-    NSString* lang       = [WikipediaAppUtils 
wikiLangForSystemLang:systemLang];
-    if (lang == nil) {
-        lang = @"en";
-    }
-
-    NSString* langName = [WikipediaAppUtils domainNameForCode:lang];
-    if (langName == nil) {
-        langName = lang;
-    }
-
-    NSString* mainPage = [self mainArticleTitleTextForLanguageCode:lang];
-    if (mainPage == nil) {
-        mainPage = @"Main Page";
-    }
-
-    NSDictionary* userDefaultsDefaults = @{
-        @"CurrentArticleTitle": mainPage,
-        @"CurrentArticleDomain": lang,
-        @"Domain": lang,
-        @"DomainName": langName,
-        @"DomainMainArticleTitle": mainPage,
-        @"Site": @"wikipedia.org",
-        @"ZeroWarnWhenLeaving": @YES,
-        @"ZeroOnDialogShownOnce": @NO,
-        @"FakeZeroOn": @NO,
-        @"ShowOnboarding": @YES,
-        @"LastHousekeepingDate": [NSDate date],
-        @"SendUsageReports": @YES,
-        @"AccessSavedPagesMessageShown": @NO
-    };
-    [[NSUserDefaults standardUserDefaults] 
registerDefaults:userDefaultsDefaults];
-}
-
 #pragma mark - Site
 
 - (void)setCurrentArticleSite:(MWKSite*)site {
@@ -118,37 +81,6 @@
         [[NSUserDefaults standardUserDefaults] setObject:site.language 
forKey:@"CurrentArticleDomain"];
         [[NSUserDefaults standardUserDefaults] synchronize];
     }
-}
-
-#pragma mark - Main Article
-
-- (WMFAssetsFile*)mainPages {
-    if (!_mainPages) {
-        _mainPages = [[WMFAssetsFile alloc] 
initWithFileType:WMFAssetsFileTypeMainPages];
-    }
-
-    return _mainPages;
-}
-
-- (NSString*)mainArticleTitleTextForLanguageCode:(NSString*)code {
-    NSDictionary* mainPageNames = self.mainPages.dictionary;
-    NSString* titleText         = mainPageNames[code];
-    return titleText;
-}
-
-- (MWKTitle*)mainArticleTitleForSite:(MWKSite*)site 
languageCode:(NSString*)code {
-    MWKTitle* title = [site titleWithString:[self 
mainArticleTitleTextForLanguageCode:code]];
-    return title;
-}
-
-- (MWKTitle*)mainArticleTitle {
-    MWKTitle* title = [self mainArticleTitleForSite:self.searchSite 
languageCode:self.searchSite.language];
-    return title;
-}
-
-- (BOOL)articleIsAMainArticle:(MWKArticle*)article {
-    MWKTitle* mainArticleTitleForArticleLanguage = [self 
mainArticleTitleForSite:article.site languageCode:article.site.language];
-    return ([article.title.prefixedText 
isEqualToString:mainArticleTitleForArticleLanguage.prefixedText]);
 }
 
 #pragma mark - Article
@@ -173,28 +105,31 @@
     if (!_currentArticle) {
         self.currentArticle = [self lastLoadedArticle];
     }
-
     return _currentArticle;
 }
 
 #pragma mark - Last known/loaded
 
 - (MWKSite*)lastKnownSite {
-    NSString* lang = [[NSUserDefaults standardUserDefaults] 
objectForKey:@"CurrentArticleDomain"];
-    MWKSite* site  = [[MWKSite alloc] initWithDomain:@"wikipedia.org" 
language:lang];
-    return site;
+    return [MWKSite siteWithLanguage:[[NSUserDefaults standardUserDefaults] 
objectForKey:@"CurrentArticleDomain"]];
 }
 
 - (MWKTitle*)lastLoadedTitle {
     MWKSite* lastKnownSite = [self lastKnownSite];
     NSString* titleText    = [[NSUserDefaults standardUserDefaults] 
objectForKey:@"CurrentArticleTitle"];
-    MWKTitle* title        = [lastKnownSite titleWithString:titleText];
+    if (!titleText) {
+        return nil;
+    }
+    MWKTitle* title = [lastKnownSite titleWithString:titleText];
     return title;
 }
 
 - (MWKArticle*)lastLoadedArticle {
     MWKTitle* lastLoadedTitle = [self lastLoadedTitle];
-    MWKArticle* article       = [self.dataStore 
articleWithTitle:lastLoadedTitle];
+    if (!lastLoadedTitle) {
+        return nil;
+    }
+    MWKArticle* article = [self.dataStore articleWithTitle:lastLoadedTitle];
     return article;
 }
 
@@ -221,7 +156,7 @@
 
 - (MWKSite*)searchSite {
     if (_searchSite == nil) {
-        _searchSite = [[MWKSite alloc] initWithDomain:@"wikipedia.org" 
language:[self searchLanguage]];
+        _searchSite = [[MWKSite alloc] initWithDomain:WMFDefaultSiteDomain 
language:[self searchLanguage]];
     }
     return _searchSite;
 }
diff --git a/Wikipedia/View Controllers/Languages/LanguagesViewController.h 
b/Wikipedia/View Controllers/Languages/LanguagesViewController.h
index aa44e48..440fd82 100644
--- a/Wikipedia/View Controllers/Languages/LanguagesViewController.h
+++ b/Wikipedia/View Controllers/Languages/LanguagesViewController.h
@@ -18,8 +18,6 @@
 
 @property (nonatomic) NavBarMode navBarMode;
 
-@property (nonatomic, weak) id invokingVC;
-
 @property (weak, nonatomic) id truePresentingVC;
 
 @property (strong, nonatomic) IBOutlet UITableView* tableView;
diff --git a/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m 
b/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
index 44aab88..697830b 100644
--- a/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
+++ b/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
@@ -13,14 +13,40 @@
 #import "TopMenuContainerView.h"
 #import "QueuesSingleton.h"
 #import "RandomArticleFetcher.h"
+#import "MWKSiteInfo.h"
+#import "MWKSiteInfoFetcher.h"
+#import "UIViewController+ModalPresent.h"
+#import "LanguagesViewController.h"
+#import "UIViewController+ModalPop.h"
+#import "UIViewController+Alert.h"
+#import "QueuesSingleton.h"
 
 @interface CenterNavController ()
-
+<LanguageSelectionDelegate>
 @property (strong, nonatomic) NSString* wikipediaZeroLearnMoreExternalUrl;
+
+@property (readonly, strong, nonatomic) MWKSiteInfoFetcher* siteInfoFetcher;
 
 @end
 
 @implementation CenterNavController
+@synthesize siteInfoFetcher = _siteInfoFetcher;
+
+- (MWKSiteInfoFetcher*)siteInfoFetcher {
+    if (!_siteInfoFetcher) {
+        _siteInfoFetcher = [MWKSiteInfoFetcher new];
+        /*
+           HAX: Force this particular site info fetcher to share the article 
operation queue. This allows for the
+           cancellation of site info requests when going to the main page, 
e.g. when clicking a link after clicking
+           "Today" in the main menu.
+
+           This is done here and not for all site info fetchers to prevent 
unintended side effects.
+         */
+        _siteInfoFetcher.requestManager.operationQueue =
+            [[[QueuesSingleton sharedInstance] articleFetchManager] 
operationQueue];
+    }
+    return _siteInfoFetcher;
+}
 
 #pragma mark View lifecycle
 
@@ -66,22 +92,25 @@
     self.view.userInteractionEnabled = !isTransitioningBetweenViewControllers;
 }
 
+- (WebViewController*)webViewController {
+    return [self searchNavStackForViewControllerOfClass:[WebViewController 
class]];
+}
+
 #pragma mark Article
 
 - (void)loadArticleWithTitle:(MWKTitle*)title
                     animated:(BOOL)animated
              discoveryMethod:(MWKHistoryDiscoveryMethod)discoveryMethod
                   popToWebVC:(BOOL)popToWebVC {
-    WebViewController* webVC = [self 
searchNavStackForViewControllerOfClass:[WebViewController class]];
-    if (webVC) {
-        MWKArticle* article = [[SessionSingleton sharedInstance].dataStore 
articleWithTitle:title];
-        [SessionSingleton sharedInstance].currentArticle = article;
+    WebViewController* webVC = [self webViewController];
+    NSParameterAssert(webVC);
+    MWKArticle* article = [[SessionSingleton sharedInstance].dataStore 
articleWithTitle:title];
+    [SessionSingleton sharedInstance].currentArticle = article;
 
-        [webVC navigateToPage:title
-              discoveryMethod:discoveryMethod];
-        if (popToWebVC) {
-            [ROOT popToViewController:webVC animated:animated];
-        }
+    [webVC navigateToPage:title
+          discoveryMethod:discoveryMethod];
+    if (popToWebVC) {
+        [ROOT popToViewController:webVC animated:animated];
     }
 }
 
@@ -137,11 +166,21 @@
 }
 
 - (void)loadTodaysArticle {
-    MWKTitle* pageTitle = [[SessionSingleton sharedInstance] mainArticleTitle];
-    [self loadArticleWithTitle:pageTitle
-                      animated:YES
-               discoveryMethod:MWKHistoryDiscoveryMethodSearch
-                    popToWebVC:NO];
+    [self.siteInfoFetcher fetchInfoForSite:[[SessionSingleton sharedInstance] 
searchSite]
+                                   success:^(MWKSiteInfo* siteInfo) {
+        [self loadArticleWithTitle:siteInfo.mainPageTitle
+                          animated:YES
+                   discoveryMethod:MWKHistoryDiscoveryMethodSearch
+                        popToWebVC:NO];
+    }
+                                   failure:^(NSError* error) {
+        if ([error.domain isEqual:NSURLErrorDomain]
+            && error.code == NSURLErrorCannotFindHost) {
+            [self showLanguages];
+        } else {
+            [[self webViewController] showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:2.0];
+        }
+    }];
 }
 
 - (void)loadRandomArticle {
@@ -150,6 +189,19 @@
     (void)[[RandomArticleFetcher alloc] 
initAndFetchRandomArticleForDomain:[SessionSingleton 
sharedInstance].currentArticleSite.language
                                                                
withManager:[QueuesSingleton sharedInstance].articleFetchManager
                                                         
thenNotifyDelegate:self];
+}
+
+- (void)showLanguages {
+    [self performModalSequeWithID:@"modal_segue_show_languages"
+                  transitionStyle:UIModalTransitionStyleCoverVertical
+                            block:^(LanguagesViewController* languagesVC) {
+        languagesVC.languageSelectionDelegate = self;
+    }];
+}
+
+- (void)languageSelected:(NSDictionary*)langData 
sender:(LanguagesViewController*)sender {
+    [NAV switchPreferredLanguageToId:langData[@"code"]];
+    [self popModalToRoot];
 }
 
 - (void)fetchFinished:(id)sender
@@ -180,13 +232,7 @@
 
 - (void)switchPreferredLanguageToId:(NSString*)languageId {
     [[SessionSingleton sharedInstance] setSearchLanguage:languageId];
-
-    MWKTitle* pageTitle = [[SessionSingleton sharedInstance] 
mainArticleTitleForSite:[SessionSingleton sharedInstance].searchSite 
languageCode:languageId];
-
-    [self loadArticleWithTitle:pageTitle
-                      animated:YES
-               discoveryMethod:MWKHistoryDiscoveryMethodSearch
-                    popToWebVC:NO];
+    [self loadTodaysArticle];
 }
 
 @end
diff --git a/Wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m b/Wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m
index 472e819..52c68e4 100644
--- a/Wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m
+++ b/Wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m
@@ -729,7 +729,6 @@
                   transitionStyle:UIModalTransitionStyleCoverVertical
                             block:^(LanguagesViewController* languagesVC) {
         languagesVC.languageSelectionDelegate = self;
-        languagesVC.invokingVC = self;
     }];
 }
 
diff --git a/Wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m 
b/Wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
index a77391f..6070e1a 100644
--- a/Wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
+++ b/Wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
@@ -830,7 +830,7 @@
     if (
         ![NAV.topViewController isMemberOfClass:[WebViewController class]]
         ||
-        [[SessionSingleton sharedInstance] 
articleIsAMainArticle:[SessionSingleton sharedInstance].currentArticle]
+        [SessionSingleton sharedInstance].currentArticle.isMain
         ) {
         // Hide TOC button if web view isn't on top or if current article is 
the main page.
         self.navBarMode = NAVBAR_MODE_DEFAULT;
diff --git a/Wikipedia/View 
Controllers/WebView/Footer/SubFooters/Options/WMFOptionsFooterViewController.m 
b/Wikipedia/View 
Controllers/WebView/Footer/SubFooters/Options/WMFOptionsFooterViewController.m
index ab6ae2d..ab296cb 100644
--- a/Wikipedia/View 
Controllers/WebView/Footer/SubFooters/Options/WMFOptionsFooterViewController.m
+++ b/Wikipedia/View 
Controllers/WebView/Footer/SubFooters/Options/WMFOptionsFooterViewController.m
@@ -153,7 +153,6 @@
                   transitionStyle:UIModalTransitionStyleCoverVertical
                             block:^(LanguagesViewController* languagesVC){
         languagesVC.downloadLanguagesForCurrentArticle = YES;
-        languagesVC.invokingVC = self;
         languagesVC.languageSelectionDelegate = self;
     }];
 }
diff --git a/Wikipedia/View Controllers/WebView/WebViewController.m 
b/Wikipedia/View Controllers/WebView/WebViewController.m
index 70e4aa6..130cd16 100644
--- a/Wikipedia/View Controllers/WebView/WebViewController.m
+++ b/Wikipedia/View Controllers/WebView/WebViewController.m
@@ -360,7 +360,7 @@
         -100        100 pixels below the top of the screen.
 
      */
-    if ([[SessionSingleton sharedInstance] 
articleIsAMainArticle:[[SessionSingleton sharedInstance] currentArticle]]) {
+    if ([[[SessionSingleton sharedInstance] currentArticle] isMain]) {
         // Prevent top of footer from being scrolled up past bottom of screen.
         return -self.view.frame.size.height;
     } else {
@@ -577,7 +577,7 @@
         return;
     }
 
-    if ([self.session articleIsAMainArticle:self.session.currentArticle]) {
+    if (self.session.currentArticle.isMain) {
         return;
     }
 
@@ -1068,7 +1068,7 @@
 
 - (void)saveWebViewScrollOffset {
     // Don't record scroll position of "main" pages.
-    if ([self.session articleIsAMainArticle:self.session.currentArticle]) {
+    if (self.session.currentArticle.isMain) {
         return;
     }
 
@@ -1331,14 +1331,7 @@
 - (void)navigateToPage:(MWKTitle*)title
        discoveryMethod:(MWKHistoryDiscoveryMethod)discoveryMethod {
     NSString* cleanTitle = title.prefixedText;
-
-    // Don't try to load nothing. Core data takes exception with such nonsense.
-    if (cleanTitle == nil) {
-        return;
-    }
-    if (cleanTitle.length == 0) {
-        return;
-    }
+    NSParameterAssert(cleanTitle.length);
 
     [self hideKeyboard];
 
@@ -1364,13 +1357,20 @@
 }
 
 - (void)reloadCurrentArticleFromNetwork {
-    [self navigateToPage:self.session.currentArticle.title
-         discoveryMethod:MWKHistoryDiscoveryMethodReloadFromNetwork];
+    [self 
reloadCurrentArticleOrMainPageWithMethod:MWKHistoryDiscoveryMethodReloadFromNetwork];
 }
 
 - (void)reloadCurrentArticleFromCache {
-    [self navigateToPage:self.session.currentArticle.title
-         discoveryMethod:MWKHistoryDiscoveryMethodReloadFromCache];
+    [self 
reloadCurrentArticleOrMainPageWithMethod:MWKHistoryDiscoveryMethodReloadFromCache];
+}
+
+- 
(void)reloadCurrentArticleOrMainPageWithMethod:(MWKHistoryDiscoveryMethod)method
 {
+    MWKTitle* page = self.session.currentArticle.title;
+    if (page) {
+        [self navigateToPage:page discoveryMethod:method];
+    } else {
+        [NAV loadTodaysArticle];
+    }
 }
 
 - (void)cancelArticleLoading {
@@ -1533,7 +1533,7 @@
 
     MWKArticle* article = self.session.currentArticle;
 
-    if ([self.session articleIsAMainArticle:article]) {
+    if (article.isMain) {
         return @"";
     }
 
@@ -1729,7 +1729,7 @@
         self.lastScrollOffset = scrollOffset;
     }
 
-    if (![self.session articleIsAMainArticle:self.session.currentArticle]) {
+    if (!self.session.currentArticle.isMain) {
         NSString* lastModifiedByUserName =
             (lastModifiedBy && !lastModifiedBy.anonymous) ? 
lastModifiedBy.name : nil;
         [self.footerViewController updateLanguageCount:langCount];
diff --git a/WikipediaUnitTests/Fixtures/ENWikiSiteInfo.json 
b/WikipediaUnitTests/Fixtures/ENWikiSiteInfo.json
new file mode 100644
index 0000000..a8ce9bf
--- /dev/null
+++ b/WikipediaUnitTests/Fixtures/ENWikiSiteInfo.json
@@ -0,0 +1 @@
+{"query":{"general":{"mainpage":"Main 
Page","base":"http://en.wikipedia.org/wiki/Main_Page","sitename":"Wikipedia","logo":"//en.wikipedia.org/static/images/project-logos/enwiki.png","generator":"MediaWiki
 
1.26wmf7","phpversion":"5.6.99-hhvm","phpsapi":"srv","hhvmversion":"3.6.1","dbtype":"mysql","dbversion":"10.0.16-MariaDB-log","imagewhitelistenabled":"","langconversion":"","titleconversion":"","linkprefixcharset":"","linkprefix":"","linktrail":"/^([a-z]+)(.*)$/sD","legaltitlechars":"
 
%!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+","invalidusernamechars":"@:","git-hash":"de40a205d34f6fc0abeebfb4f833f1ddb630f106","git-branch":"wmf/1.26wmf7","case":"first-letter","lang":"en","fallback":[],"fallback8bitEncoding":"windows-1252","writeapi":"","timezone":"UTC","timeoffset":0,"articlepath":"/wiki/$1","scriptpath":"/w","script":"/w/index.php","variantarticlepath":false,"server":"//en.wikipedia.org","servername":"en.wikipedia.org","wikiid":"enwiki","time":"2015-06-02T14:44:08Z","misermode":"","maxuploadsize":1048576000,"thumblimits":[120,150,180,200,220,250,300],"imagelimits":[{"width":320,"height":240},{"width":640,"height":480},{"width":800,"height":600},{"width":1024,"height":768},{"width":1280,"height":1024}],"favicon":"//en.wikipedia.org/static/favicon/wikipedia.ico"}}}
diff --git a/WikipediaUnitTests/Fixtures/NOWikiSiteInfo.json 
b/WikipediaUnitTests/Fixtures/NOWikiSiteInfo.json
new file mode 100644
index 0000000..1ccd1a0
--- /dev/null
+++ b/WikipediaUnitTests/Fixtures/NOWikiSiteInfo.json
@@ -0,0 +1 @@
+{"query":{"general":{"mainpage":"Portal:Forside","base":"http://no.wikipedia.org/wiki/Portal:Forside","sitename":"Wikipedia","logo":"//no.wikipedia.org/static/images/project-logos/nowiki.png","generator":"MediaWiki
 
1.26wmf7","phpversion":"5.6.99-hhvm","phpsapi":"srv","hhvmversion":"3.6.1","dbtype":"mysql","dbversion":"10.0.16-MariaDB-log","imagewhitelistenabled":"","langconversion":"","titleconversion":"","linkprefixcharset":"","linkprefix":"","linktrail":"/^([\u00e6\u00f8\u00e5a-z]+)(.*)$/sDu","legaltitlechars":"
 
%!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+","invalidusernamechars":"@:","git-hash":"de40a205d34f6fc0abeebfb4f833f1ddb630f106","git-branch":"wmf/1.26wmf7","case":"first-letter","lang":"nb","fallback":[{"code":"en"}],"fallback8bitEncoding":"windows-1252","writeapi":"","timezone":"Europe/Berlin","timeoffset":120,"articlepath":"/wiki/$1","scriptpath":"/w","script":"/w/index.php","variantarticlepath":false,"server":"//no.wikipedia.org","servername":"no.wikipedia.org","wikiid":"nowiki","time":"2015-06-02T14:14:20Z","misermode":"","maxuploadsize":1048576000,"thumblimits":[120,150,180,200,220,250,300],"imagelimits":[{"width":320,"height":240},{"width":640,"height":480},{"width":800,"height":600},{"width":1024,"height":768},{"width":1280,"height":1024}],"favicon":"//no.wikipedia.org/static/favicon/wikipedia.ico"}}}
diff --git a/WikipediaUnitTests/MWKArticle+WMFSharingTests.m 
b/WikipediaUnitTests/MWKArticle+WMFSharingTests.m
index 52274c4..6a8f564 100644
--- a/WikipediaUnitTests/MWKArticle+WMFSharingTests.m
+++ b/WikipediaUnitTests/MWKArticle+WMFSharingTests.m
@@ -14,7 +14,6 @@
 #import "MWKTitle.h"
 #import "MWKSite.h"
 #import "MWKArticle+WMFSharing.h"
-#import "MWKArticle+isMain.h"
 #import "MWKDataStore+TemporaryDataStore.h"
 
 #define HC_SHORTHAND 1
@@ -35,9 +34,10 @@
                                          
wmf_jsonFromContentsOfFile:@"MainPageMobileView"]
                                         objectForKey:@"mobileview"];
 
+    [self.article importMobileViewJSON:mainPageMobileView];
+
     NSAssert([self.article isMain], @"supposed to be testing main pages!");
 
-    [self.article importMobileViewJSON:mainPageMobileView];
     assertThat(self.article.shareSnippet, is(@"Gary Cooper was an American 
film actor known for his natural, authentic, and understated acting style. He 
was a movie star from the end of the silent film era through the end of the 
golden age of Classical Hollywood. Cooper began his career as a film extra and 
stunt rider and soon established himself as a Western hero in films such as The 
Virginian. He played the lead in adventure films and dramas such as A Farewell 
to Arms and The Lives of a Bengal Lancer, and extended his range of 
performances to include roles in most major film genres. He portrayed champions 
of the common man in films such as Mr. Deeds Goes to Town, Meet John Doe, 
Sergeant York, The Pride of the Yankees, and For Whom the Bell Tolls. In his 
later years, he delivered award-winning performances in High Noon and Friendly 
Persuasion. Cooper received three Academy Awards and appeared on the Motion 
Picture Herald exhibitors poll of top ten film personalities every year from 
1936 to 1958. His screen persona embodied the American folk hero. Ongoing: 
Nepal earthquake – Yemeni Civil WarRecent deaths: Ruth Rendell – Maya 
Plisetskaya"));
 }
 
diff --git a/WikipediaUnitTests/MWKSiteInfoFetcherTests.m 
b/WikipediaUnitTests/MWKSiteInfoFetcherTests.m
new file mode 100644
index 0000000..a992059
--- /dev/null
+++ b/WikipediaUnitTests/MWKSiteInfoFetcherTests.m
@@ -0,0 +1,139 @@
+//
+//  MWKSiteInfoFetcherTests.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+#import "WMFAsyncTestCase.h"
+#import "MWKSite.h"
+#import "MWKSiteInfo.h"
+#import "MWKSiteInfoFetcher.h"
+#import "XCTestCase+WMFLocaleTesting.h"
+#import "NSArray+WMFShuffle.h"
+#import <AFNetworking/AFHTTPRequestOperationManager.h>
+#import "WMFTestFixtureUtilities.h"
+#import "AFHTTPRequestOperationManager+UniqueRequests.h"
+
+#define MOCKITO_SHORTHAND 1
+#import <OCMockito/OCMockito.h>
+
+#define HC_SHORTHAND 1
+#import <OCHamcrest/OCHamcrest.h>
+
+@interface MWKSiteInfoFetcherTests : WMFAsyncTestCase
+@property (strong, nonatomic) MWKSiteInfoFetcher* fetcher;
+@end
+
+@implementation MWKSiteInfoFetcherTests
+
+- (void)setUp {
+    [super setUp];
+    self.fetcher = [MWKSiteInfoFetcher new];
+}
+
+- (void)testENWikiFixture {
+    [self runSuccessfulCallbackTestWithFixture:@"ENWikiSiteInfo" site:[MWKSite 
siteWithLanguage:@"en"]];
+}
+
+- (void)testNOWikiFixture {
+    [self runSuccessfulCallbackTestWithFixture:@"NOWikiSiteInfo" site:[MWKSite 
siteWithLanguage:@"no"]];
+}
+
+- (void)testErrorHandling {
+    AFHTTPRequestOperationManager* mockReqManager =
+        mock([[NSBundle mainBundle] 
classNamed:NSStringFromClass([AFHTTPRequestOperationManager class])]);
+    self.fetcher.requestManager = mockReqManager;
+
+    MWKSite* testSite = [MWKSite siteWithCurrentLocale];
+
+    XCTestExpectation* failedFetchExpectation = [self 
expectationWithDescription:@"failureCallback"];
+
+    NSError* expectedError = [NSError errorWithDomain:@"foo" code:1 
userInfo:nil];
+
+    [self.fetcher fetchInfoForSite:testSite
+                           success:^(MWKSiteInfo* siteInfo) {}
+                           failure:^(NSError* e){
+        assertThat(e, is(expectedError));
+        [failedFetchExpectation fulfill];
+    }
+                     
callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
+
+    MKTArgumentCaptor* failureBlockCaptor = [[MKTArgumentCaptor alloc] init];
+    [MKTVerify(mockReqManager) 
wmf_idempotentGET:testSite.apiEndpoint.absoluteString
+                                      parameters:anything()
+                                         success:anything()
+                                         failure:[failureBlockCaptor capture]];
+    void (^ failureCallback)(AFHTTPRequestOperation* op, NSError* err) = 
[failureBlockCaptor value];
+    failureCallback(nil, expectedError);
+
+    WaitForExpectations();
+}
+
+- (void)runSuccessfulCallbackTestWithFixture:(NSString*)fixture 
site:(MWKSite*)testSite {
+    AFHTTPRequestOperationManager* mockReqManager =
+        mock([[NSBundle mainBundle] 
classNamed:NSStringFromClass([AFHTTPRequestOperationManager class])]);
+    self.fetcher.requestManager = mockReqManager;
+
+    NSDictionary* fixtureJSON = [[self wmf_bundle] 
wmf_jsonFromContentsOfFile:fixture];
+
+    XCTestExpectation* successfulFetchExpectation = [self 
expectationWithDescription:@"successCallback"];
+
+    [self.fetcher fetchInfoForSite:testSite success:^(MWKSiteInfo* siteInfo) {
+        assertThat(siteInfo.mainPageTitleText, is([fixtureJSON 
valueForKeyPath:@"query.general.mainpage"]));
+        [successfulFetchExpectation fulfill];
+    } failure:^(NSError* e){} 
callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
+
+    MKTArgumentCaptor* successBlockCaptor = [[MKTArgumentCaptor alloc] init];
+    [MKTVerify(mockReqManager) 
wmf_idempotentGET:testSite.apiEndpoint.absoluteString
+                                      parameters:anything()
+                                         success:[successBlockCaptor capture]
+                                         failure:anything()];
+    void (^ responseCallback)(AFHTTPRequestOperation* op, NSDictionary* json) 
= [successBlockCaptor value];
+    responseCallback(nil, fixtureJSON);
+
+    WaitForExpectations();
+}
+
+#if 0
+// Disabled since doing network I/O is slow. Run manually if necessary
+- (void)testRealFetchOfPopularLocales {
+    [self runTestWithLocales:@[@"en_US", @"fr_FR", @"en_GB"]];
+}
+
+#endif
+
+#if 0
+// Warning, this test is flaky by nature. Only run manually and don't commit 
w/ it enabled.
+- (void)testRealFetchOfRandomLocales {
+    [self runTestWithLocales:
+     [[[NSLocale availableLocaleIdentifiers]
+       wmf_shuffledCopy]
+      subarrayWithRange:NSMakeRange(0, 100)]];
+}
+
+#endif
+
+- (void)runTestWithLocales:(NSArray*)localeIdentifiers {
+    NSMutableArray* errors = [NSMutableArray new];
+    [self wmf_runParallelTestsWithLocales:localeIdentifiers
+                                    block:^(NSLocale* locale, 
XCTestExpectation* expectation) {
+        MWKSite* site = [MWKSite siteWithLocale:locale];
+        [self.fetcher fetchInfoForSite:site
+                               success:^(MWKSiteInfo* info) {
+            NSLog(@"Site info for %@: %@", locale.localeIdentifier, info);
+            [expectation fulfill];
+        } failure:^(NSError* error) {
+            @synchronized(errors) {
+                [errors addObject:@[locale.localeIdentifier, error]];
+            }
+            [expectation fulfill];
+        } 
callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
+    }];
+    XCTAssert(errors.count == 0, @"Failed to fetch site info for locales: %@", 
errors);
+}
+
+@end
diff --git a/WikipediaUnitTests/Utilities/NSBundle+TestAssets.m 
b/WikipediaUnitTests/Utilities/NSBundle+TestAssets.m
index 8dbdf76..7b06932 100644
--- a/WikipediaUnitTests/Utilities/NSBundle+TestAssets.m
+++ b/WikipediaUnitTests/Utilities/NSBundle+TestAssets.m
@@ -15,17 +15,16 @@
     NSData* data = [NSData dataWithContentsOfFile:[self 
pathForResource:filename ofType:type]
                                           options:0
                                             error:&error];
-    NSParameterAssert(!error);
+    NSAssert(!error, @"Unexpected error reading test fixture: %@.%@, %@", 
filename, type, error);
     return data;
 }
 
 - (id)wmf_jsonFromContentsOfFile:(NSString*)filename {
     NSError* error;
-
     id json = [NSJSONSerialization JSONObjectWithData:[self 
wmf_dataFromContentsOfFile:filename ofType:@"json"]
                                               options:0
                                                 error:&error];
-    NSParameterAssert(!error);
+    NSAssert(!error, @"Error reading JSON data from filename %@: %@", 
filename, error);
     return json;
 }
 
diff --git a/WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.h 
b/WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.h
new file mode 100644
index 0000000..40f08de
--- /dev/null
+++ b/WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.h
@@ -0,0 +1,21 @@
+//
+//  XCTestCase+WMFLocaleTesting.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import <XCTest/XCTest.h>
+
+typedef void (^ WMFLocaleTest)(NSLocale* locale, XCTestExpectation* e);
+
+@interface XCTestCase (WMFLocaleTesting)
+
+- (void)wmf_runParallelTestsWithLocales:(NSArray*)localeIdentifiers 
block:(WMFLocaleTest)block;
+
+- (void)wmf_runParallelTestsWithLocales:(NSArray*)localeIdentifiers
+                                timeout:(NSTimeInterval)timeout
+                                  block:(WMFLocaleTest)block;
+
+@end
diff --git a/WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.m 
b/WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.m
new file mode 100644
index 0000000..2d7fd91
--- /dev/null
+++ b/WikipediaUnitTests/Utilities/XCTestCase+WMFLocaleTesting.m
@@ -0,0 +1,41 @@
+//
+//  XCTestCase+WMFLocaleTesting.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "XCTestCase+WMFLocaleTesting.h"
+#import <BlocksKit/BlocksKit.h>
+#import "WMFAsyncTestCase.h"
+
+@implementation XCTestCase (WMFLocaleTesting)
+
+- (void)wmf_runParallelTestsWithLocales:(NSArray*)localeIdentifiers 
block:(WMFLocaleTest)block {
+    [self wmf_runParallelTestsWithLocales:localeIdentifiers
+                                  timeout:localeIdentifiers.count * 
WMFDefaultExpectationTimeout
+                                    block:block];
+}
+
+- (void)wmf_runParallelTestsWithLocales:(NSArray*)localeIdentifiers
+                                timeout:(NSTimeInterval)timeout
+                                  block:(WMFLocaleTest)block {
+    NSDictionary* expectationsForLocale =
+        [localeIdentifiers bk_reduce:[NSMutableDictionary 
dictionaryWithCapacity:localeIdentifiers.count]
+                           withBlock:^id (NSMutableDictionary* sum, NSString* 
localeID) {
+        sum[localeID] = [self expectationWithDescription:localeID];
+        return sum;
+    }];
+    dispatch_queue_t concurrentBackgroundQueue = 
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
+    dispatch_apply(localeIdentifiers.count, concurrentBackgroundQueue, 
^(size_t i) {
+        NSString* localeID = localeIdentifiers[i];
+        NSLocale* locale = [NSLocale localeWithLocaleIdentifier:localeID];
+        block(locale, expectationsForLocale[localeID]);
+    });
+    // allow 1 sec per locale
+    NSLog(@"Waiting %f seconds to run a test with %lu locales...", timeout, 
localeIdentifiers.count);
+    [self waitForExpectationsWithTimeout:localeIdentifiers.count handler:nil];
+}
+
+@end
diff --git a/WikipediaUnitTests/WMFAsyncTestCase.h 
b/WikipediaUnitTests/WMFAsyncTestCase.h
index 6e74988..f1f8320 100644
--- a/WikipediaUnitTests/WMFAsyncTestCase.h
+++ b/WikipediaUnitTests/WMFAsyncTestCase.h
@@ -10,7 +10,7 @@
 
 #define PushExpectation() ([self pushExpectation:__FILE__ line:__LINE__])
 
-extern float const WMFDefaultExpectationTimeout;
+extern NSTimeInterval const WMFDefaultExpectationTimeout;
 
 #define WaitForExpectations() ([self 
waitForExpectationsWithTimeout:WMFDefaultExpectationTimeout handler:nil])
 
diff --git a/WikipediaUnitTests/WMFAsyncTestCase.m 
b/WikipediaUnitTests/WMFAsyncTestCase.m
index c8c3db4..b5270f9 100644
--- a/WikipediaUnitTests/WMFAsyncTestCase.m
+++ b/WikipediaUnitTests/WMFAsyncTestCase.m
@@ -8,7 +8,7 @@
 
 #import "WMFAsyncTestCase.h"
 
-float const WMFDefaultExpectationTimeout = 0.1;
+NSTimeInterval const WMFDefaultExpectationTimeout = 0.1;
 
 @interface WMFAsyncTestCase ()
 @property NSMutableArray* expectations;
diff --git a/WikipediaUnitTests/WMFDateFormatterTests.m 
b/WikipediaUnitTests/WMFDateFormatterTests.m
index 4798a4d..ec99770 100644
--- a/WikipediaUnitTests/WMFDateFormatterTests.m
+++ b/WikipediaUnitTests/WMFDateFormatterTests.m
@@ -9,6 +9,7 @@
 #import <UIKit/UIKit.h>
 #import <XCTest/XCTest.h>
 #import "NSDateFormatter+WMFExtensions.h"
+#import "XCTestCase+WMFLocaleTesting.h"
 
 #define HC_SHORTHAND 1
 #import <OCHamcrest/OCHamcrest.h>
@@ -27,13 +28,9 @@
 }
 
 - (void)testShortTimeFormatterIsValidForAllLocales {
-    NSString* testTimestamp     = @"2015-02-10T10:31:27Z";
-    NSArray* availableLocaleIDs = [NSLocale availableLocaleIdentifiers];
-
-    dispatch_apply(availableLocaleIDs.count, 
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {
-        NSString* localeID = availableLocaleIDs[i];
-        NSLocale* locale = [NSLocale localeWithLocaleIdentifier:localeID];
-
+    NSString* testTimestamp = @"2015-02-10T10:31:27Z";
+    [self wmf_runParallelTestsWithLocales:[NSLocale availableLocaleIdentifiers]
+                                    block:^(NSLocale* locale, 
XCTestExpectation* e) {
         // need to parse date using the "regular" formatter
         NSDate* decodedDate = [[NSDateFormatter wmf_iso8601Formatter] 
dateFromString:testTimestamp];
         NSParameterAssert(decodedDate);
@@ -42,10 +39,11 @@
         assertThat([[NSDateFormatter wmf_shortTimeFormatterWithLocale:locale] 
stringFromDate:decodedDate],
                    describedAs(@"expected non-nil for locale: %0 from 
timestamp %1",
                                notNilValue(),
-                               localeID,
+                               locale.localeIdentifier,
                                testTimestamp,
                                nil));
-    });
+        [e fulfill];
+    }];
 }
 
 - (void)testShortTimeFormatterExamples {

-- 
To view, visit https://gerrit.wikimedia.org/r/215326
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie5b7bb2194212edce46349abc550404e0b71725d
Gerrit-PatchSet: 9
Gerrit-Project: apps/ios/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Bgerstle <bgers...@wikimedia.org>
Gerrit-Reviewer: Bgerstle <bgers...@wikimedia.org>
Gerrit-Reviewer: Fjalapeno <cfl...@wikimedia.org>
Gerrit-Reviewer: Mhurd <mh...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to