11final class DataMigrator {
22
3+ /// `DefaultsWrapper` is used to single out a dictionary for the migration process.
4+ /// This way we can delete just the value for its key and leave the rest of shared defaults untouched.
5+ private struct DefaultsWrapper {
6+ static let dictKey = " defaults_staging_dictionary "
7+ let defaultsDict : [ String : Any ]
8+ }
9+
310 private let coreDataStack : CoreDataStack
411 private let backupLocation : URL ?
512 private let keychainUtils : KeychainUtils
613 private let localDefaults : UserDefaults
714 private let sharedDefaults : UserDefaults ?
15+ private let localFileStore : LocalFileStore
816
917 init ( coreDataStack: CoreDataStack = ContextManager . sharedInstance ( ) ,
1018 backupLocation: URL ? = FileManager . default. containerURL ( forSecurityApplicationGroupIdentifier: " group.org.wordpress " ) ? . appendingPathComponent ( " WordPress.sqlite " ) ,
1119 keychainUtils: KeychainUtils = KeychainUtils ( ) ,
1220 localDefaults: UserDefaults = UserDefaults . standard,
13- sharedDefaults: UserDefaults ? = UserDefaults ( suiteName: WPAppGroupName) ) {
21+ sharedDefaults: UserDefaults ? = UserDefaults ( suiteName: WPAppGroupName) ,
22+ localFileStore: LocalFileStore = FileManager . default) {
1423 self . coreDataStack = coreDataStack
1524 self . backupLocation = backupLocation
1625 self . keychainUtils = keychainUtils
1726 self . localDefaults = localDefaults
1827 self . sharedDefaults = sharedDefaults
28+ self . localFileStore = localFileStore
1929 }
2030
2131 enum DataMigratorError : Error {
2232 case localDraftsNotSynced
2333 case databaseCopyError
24- case keychainError
2534 case sharedUserDefaultsNil
2635 }
2736
@@ -34,7 +43,7 @@ final class DataMigrator {
3443 completion ? ( . failure( . databaseCopyError) )
3544 return
3645 }
37- guard copyUserDefaults ( from : localDefaults , to : sharedDefaults ) else {
46+ guard populateSharedDefaults ( ) else {
3847 completion ? ( . failure( . sharedUserDefaultsNil) )
3948 return
4049 }
@@ -47,14 +56,29 @@ final class DataMigrator {
4756 completion ? ( . failure( . databaseCopyError) )
4857 return
4958 }
50- guard copyUserDefaults ( from : sharedDefaults , to : localDefaults ) else {
59+ guard populateFromSharedDefaults ( ) else {
5160 completion ? ( . failure( . sharedUserDefaultsNil) )
5261 return
5362 }
63+
64+ copyTodayWidgetDataToJetpack ( )
5465 BloggingRemindersScheduler . handleRemindersMigration ( )
5566 completion ? ( . success( ( ) ) )
5667 }
5768
69+ /// Copies WP's Today Widget data (in Keychain and User Defaults) into JP.
70+ ///
71+ /// Both WP and JP's extensions are already reading and storing data in the same location, but in case of Today Widget,
72+ /// the keys used for Keychain and User Defaults are differentiated to prevent one app overwriting the other.
73+ ///
74+ /// Note: This method is not private for unit testing purposes.
75+ /// It requires time to properly mock the dependencies in `importData`.
76+ ///
77+ func copyTodayWidgetDataToJetpack( ) {
78+ copyTodayWidgetKeychain ( )
79+ copyTodayWidgetUserDefaults ( )
80+ copyTodayWidgetCacheFiles ( )
81+ }
5882}
5983
6084// MARK: - Private Functions
@@ -96,26 +120,152 @@ private extension DataMigrator {
96120 return true
97121 }
98122
99- func copyKeychain( from sourceAccessGroup: String ? , to destinationAccessGroup: String ? ) -> Bool {
100- do {
101- try keychainUtils. copyKeychain ( from: sourceAccessGroup, to: destinationAccessGroup)
102- } catch {
103- DDLogError ( " Error copying keychain: \( error) " )
123+ func populateSharedDefaults( ) -> Bool {
124+ guard let sharedDefaults = sharedDefaults else {
104125 return false
105126 }
106127
128+ let data = localDefaults. dictionaryRepresentation ( )
129+ var temporaryDictionary : [ String : Any ] = [ : ]
130+ for (key, value) in data {
131+ temporaryDictionary [ key] = value
132+ }
133+ sharedDefaults. set ( temporaryDictionary, forKey: DefaultsWrapper . dictKey)
107134 return true
108135 }
109136
110- func copyUserDefaults( from source: UserDefaults ? , to destination: UserDefaults ? ) -> Bool {
111- guard let source, let destination else {
137+ func populateFromSharedDefaults( ) -> Bool {
138+ guard let sharedDefaults = sharedDefaults,
139+ let temporaryDictionary = sharedDefaults. dictionary ( forKey: DefaultsWrapper . dictKey) else {
112140 return false
113141 }
114- let data = source. dictionaryRepresentation ( )
115- for (key, value) in data {
116- destination. set ( value, forKey: key)
117- }
118142
143+ for (key, value) in temporaryDictionary {
144+ localDefaults. set ( value, forKey: key)
145+ }
146+ sharedDefaults. removeObject ( forKey: DefaultsWrapper . dictKey)
119147 return true
120148 }
121149}
150+
151+ // MARK: - Today Widget Extension Constants
152+
153+ private extension DataMigrator {
154+
155+ func copyTodayWidgetKeychain( ) {
156+ guard let authToken = try ? keychainUtils. password ( for: WPWidgetConstants . keychainTokenKey. rawValue,
157+ serviceName: WPWidgetConstants . keychainServiceName. rawValue,
158+ accessGroup: WPAppKeychainAccessGroup) else {
159+ return
160+ }
161+
162+ try ? keychainUtils. store ( username: WPWidgetConstants . keychainTokenKey. valueForJetpack ( ) ,
163+ password: authToken,
164+ serviceName: WPWidgetConstants . keychainServiceName. valueForJetpack ( ) ,
165+ updateExisting: true )
166+ }
167+
168+ func copyTodayWidgetUserDefaults( ) {
169+ guard let sharedDefaults else {
170+ return
171+ }
172+
173+ let userDefaultKeys : [ WPWidgetConstants ] = [
174+ . userDefaultsSiteIdKey,
175+ . userDefaultsLoggedInKey,
176+ . statsUserDefaultsSiteIdKey,
177+ . statsUserDefaultsSiteUrlKey,
178+ . statsUserDefaultsSiteNameKey,
179+ . statsUserDefaultsSiteTimeZoneKey
180+ ]
181+
182+ userDefaultKeys. forEach { key in
183+ // go to the next key if there's nothing stored under the current key.
184+ guard let objectToMigrate = sharedDefaults. object ( forKey: key. rawValue) else {
185+ return
186+ }
187+
188+ sharedDefaults. set ( objectToMigrate, forKey: key. valueForJetpack ( ) )
189+ }
190+ }
191+
192+ func copyTodayWidgetCacheFiles( ) {
193+ let fileNames : [ WPWidgetConstants ] = [
194+ . todayFilename,
195+ . allTimeFilename,
196+ . thisWeekFilename,
197+ . statsTodayFilename,
198+ . statsThisWeekFilename,
199+ . statsAllTimeFilename
200+ ]
201+
202+ fileNames. forEach { fileName in
203+ guard let sourceURL = localFileStore. containerURL ( forAppGroup: WPAppGroupName) ? . appendingPathComponent ( fileName. rawValue) ,
204+ let targetURL = localFileStore. containerURL ( forAppGroup: WPAppGroupName) ? . appendingPathComponent ( fileName. valueForJetpack ( ) ) ,
205+ localFileStore. fileExists ( at: sourceURL) else {
206+ return
207+ }
208+
209+ if localFileStore. fileExists ( at: targetURL) {
210+ try ? localFileStore. removeItem ( at: targetURL)
211+ }
212+
213+ try ? localFileStore. copyItem ( at: sourceURL, to: targetURL)
214+ }
215+ }
216+
217+ /// Keys relevant for migration, copied from WidgetConfiguration.
218+ ///
219+ enum WPWidgetConstants : String {
220+ // Constants for Home Widget
221+ case keychainTokenKey = " OAuth2Token "
222+ case keychainServiceName = " TodayWidget "
223+ case userDefaultsSiteIdKey = " WordPressHomeWidgetsSiteId "
224+ case userDefaultsLoggedInKey = " WordPressHomeWidgetsLoggedIn "
225+ case todayFilename = " HomeWidgetTodayData.plist " // HomeWidgetTodayData
226+ case allTimeFilename = " HomeWidgetAllTimeData.plist " // HomeWidgetAllTimeData
227+ case thisWeekFilename = " HomeWidgetThisWeekData.plist " // HomeWidgetThisWeekData
228+
229+ // Constants for Stats Widget
230+ case statsUserDefaultsSiteIdKey = " WordPressTodayWidgetSiteId "
231+ case statsUserDefaultsSiteNameKey = " WordPressTodayWidgetSiteName "
232+ case statsUserDefaultsSiteUrlKey = " WordPressTodayWidgetSiteUrl "
233+ case statsUserDefaultsSiteTimeZoneKey = " WordPressTodayWidgetTimeZone "
234+ case statsTodayFilename = " TodayData.plist " // TodayWidgetStats
235+ case statsThisWeekFilename = " ThisWeekData.plist " // ThisWeekWidgetStats
236+ case statsAllTimeFilename = " AllTimeData.plist " // AllTimeWidgetStats
237+
238+ func valueForJetpack( ) -> String {
239+ switch self {
240+ case . keychainTokenKey:
241+ return " OAuth2Token "
242+ case . keychainServiceName:
243+ return " JetpackTodayWidget "
244+ case . userDefaultsSiteIdKey:
245+ return " JetpackHomeWidgetsSiteId "
246+ case . userDefaultsLoggedInKey:
247+ return " JetpackHomeWidgetsLoggedIn "
248+ case . todayFilename:
249+ return " JetpackHomeWidgetTodayData.plist "
250+ case . allTimeFilename:
251+ return " JetpackHomeWidgetAllTimeData.plist "
252+ case . thisWeekFilename:
253+ return " JetpackHomeWidgetThisWeekData.plist "
254+ case . statsUserDefaultsSiteIdKey:
255+ return " JetpackTodayWidgetSiteId "
256+ case . statsUserDefaultsSiteNameKey:
257+ return " JetpackTodayWidgetSiteName "
258+ case . statsUserDefaultsSiteUrlKey:
259+ return " JetpackTodayWidgetSiteUrl "
260+ case . statsUserDefaultsSiteTimeZoneKey:
261+ return " JetpackTodayWidgetTimeZone "
262+ case . statsTodayFilename:
263+ return " JetpackTodayData.plist "
264+ case . statsThisWeekFilename:
265+ return " JetpackThisWeekData.plist "
266+ case . statsAllTimeFilename:
267+ return " JetpackAllTimeData.plist "
268+ }
269+ }
270+ }
271+ }
0 commit comments