From ce71dcd851a714a9bbb5da2660e796c429b4546c Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 7 Mar 2025 14:00:44 +1100 Subject: [PATCH 01/25] Extract definition for `Bundle` where DB lives --- WordPress/Classes/Utility/CoreData/ContextManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/CoreData/ContextManager.swift b/WordPress/Classes/Utility/CoreData/ContextManager.swift index 348a96b48801..119d61f586fb 100644 --- a/WordPress/Classes/Utility/CoreData/ContextManager.swift +++ b/WordPress/Classes/Utility/CoreData/ContextManager.swift @@ -154,7 +154,8 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift { DDLogWarn("Migration required for persistent store.") - guard let modelFileURL = Bundle.main.url(forResource: "WordPress", withExtension: "momd") else { + let dbBundle = Bundle.main + guard let modelFileURL = dbBundle.url(forResource: "WordPress", withExtension: "momd") else { fatalError("Can't find WordPress.momd") } From ea82febe91572ccf761003c1e93df910fcfd90d2 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 7 Mar 2025 17:24:50 +1100 Subject: [PATCH 02/25] =?UTF-8?q?App=20crashes=20=E2=80=93=20Move=20`WordP?= =?UTF-8?q?ress.xcdatamodeld`=20to=20WordPressData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WordPress.xcdatamodeld/.xccurrentversion | 0 .../WordPress 10.xcdatamodel/elements | Bin .../WordPress 10.xcdatamodel/layout | Bin .../WordPress 100.xcdatamodel/contents | 0 .../WordPress 101.xcdatamodel/contents | 0 .../WordPress 102.xcdatamodel/contents | 0 .../WordPress 103.xcdatamodel/contents | 0 .../WordPress 104.xcdatamodel/contents | 0 .../WordPress 105.xcdatamodel/contents | 0 .../WordPress 106.xcdatamodel/contents | 0 .../WordPress 107.xcdatamodel/contents | 0 .../WordPress 108.xcdatamodel/contents | 0 .../WordPress 109.xcdatamodel/contents | 0 .../WordPress 11.xcdatamodel/elements | Bin .../WordPress 11.xcdatamodel/layout | Bin .../WordPress 110.xcdatamodel/contents | 0 .../WordPress 111.xcdatamodel/contents | 0 .../WordPress 112.xcdatamodel/contents | 0 .../WordPress 113.xcdatamodel/contents | 0 .../WordPress 114.xcdatamodel/contents | 0 .../WordPress 115.xcdatamodel/contents | 0 .../WordPress 116.xcdatamodel/contents | 0 .../WordPress 117.xcdatamodel/contents | 0 .../WordPress 118.xcdatamodel/contents | 0 .../WordPress 119.xcdatamodel/contents | 0 .../WordPress 12.xcdatamodel/elements | Bin .../WordPress 12.xcdatamodel/layout | Bin .../WordPress 120.xcdatamodel/contents | 0 .../WordPress 121.xcdatamodel/contents | 0 .../WordPress 122.xcdatamodel/contents | 0 .../WordPress 123.xcdatamodel/contents | 0 .../WordPress 124.xcdatamodel/contents | 0 .../WordPress 125.xcdatamodel/contents | 0 .../WordPress 126.xcdatamodel/contents | 0 .../WordPress 127.xcdatamodel/contents | 0 .../WordPress 128.xcdatamodel/contents | 0 .../WordPress 129.xcdatamodel/contents | 0 .../WordPress 13.xcdatamodel/contents | 0 .../WordPress 130.xcdatamodel/contents | 0 .../WordPress 131.xcdatamodel/contents | 0 .../WordPress 132.xcdatamodel/contents | 0 .../WordPress 133.xcdatamodel/contents | 0 .../WordPress 134.xcdatamodel/contents | 0 .../WordPress 135.xcdatamodel/contents | 0 .../WordPress 136.xcdatamodel/contents | 0 .../WordPress 137.xcdatamodel/contents | 0 .../WordPress 138.xcdatamodel/contents | 0 .../WordPress 139.xcdatamodel/contents | 0 .../WordPress 14.xcdatamodel/contents | 0 .../WordPress 140.xcdatamodel/contents | 0 .../WordPress 141.xcdatamodel/contents | 0 .../WordPress 142.xcdatamodel/contents | 0 .../WordPress 143.xcdatamodel/contents | 0 .../WordPress 144.xcdatamodel/contents | 0 .../WordPress 145.xcdatamodel/contents | 0 .../WordPress 146.xcdatamodel/contents | 0 .../WordPress 147.xcdatamodel/contents | 0 .../WordPress 148.xcdatamodel/contents | 0 .../WordPress 149.xcdatamodel/contents | 0 .../WordPress 15.xcdatamodel/contents | 0 .../WordPress 150.xcdatamodel/contents | 0 .../WordPress 151.xcdatamodel/contents | 0 .../WordPress 152.xcdatamodel/contents | 0 .../WordPress 153.xcdatamodel/contents | 0 .../WordPress 154.xcdatamodel/contents | 0 .../WordPress 16.xcdatamodel/contents | 0 .../WordPress 17.xcdatamodel/contents | 0 .../WordPress 18.xcdatamodel/contents | 0 .../WordPress 19.xcdatamodel/contents | 0 .../WordPress 2.xcdatamodel/elements | Bin .../WordPress 2.xcdatamodel/layout | Bin .../WordPress 20.xcdatamodel/contents | 0 .../WordPress 21.xcdatamodel/contents | 0 .../WordPress 22.xcdatamodel/contents | 0 .../WordPress 23.xcdatamodel/contents | 0 .../WordPress 24.xcdatamodel/contents | 0 .../WordPress 25.xcdatamodel/contents | 0 .../WordPress 26.xcdatamodel/contents | 0 .../WordPress 27.xcdatamodel/contents | 0 .../WordPress 28.xcdatamodel/contents | 0 .../WordPress 29.xcdatamodel/contents | 0 .../WordPress 3.xcdatamodel/elements | Bin .../WordPress 3.xcdatamodel/layout | Bin .../WordPress 30.xcdatamodel/contents | 0 .../WordPress 31.xcdatamodel/contents | 0 .../WordPress 32.xcdatamodel/contents | 0 .../WordPress 33.xcdatamodel/contents | 0 .../WordPress 34.xcdatamodel/contents | 0 .../WordPress 35.xcdatamodel/contents | 0 .../WordPress 36.xcdatamodel/contents | 0 .../WordPress 37.xcdatamodel/contents | 0 .../WordPress 38.xcdatamodel/contents | 0 .../WordPress 39.xcdatamodel/contents | 0 .../WordPress 4.xcdatamodel/elements | Bin .../WordPress 4.xcdatamodel/layout | Bin .../WordPress 40.xcdatamodel/contents | 0 .../WordPress 41.xcdatamodel/contents | 0 .../WordPress 42.xcdatamodel/contents | 0 .../WordPress 43.xcdatamodel/contents | 0 .../WordPress 44.xcdatamodel/contents | 0 .../WordPress 45.xcdatamodel/contents | 0 .../WordPress 46.xcdatamodel/contents | 0 .../WordPress 47.xcdatamodel/contents | 0 .../WordPress 48.xcdatamodel/contents | 0 .../WordPress 49.xcdatamodel/contents | 0 .../WordPress 5.xcdatamodel/elements | Bin .../WordPress 5.xcdatamodel/layout | Bin .../WordPress 50.xcdatamodel/contents | 0 .../WordPress 51.xcdatamodel/contents | 0 .../WordPress 52.xcdatamodel/contents | 0 .../WordPress 53.xcdatamodel/contents | 0 .../WordPress 54.xcdatamodel/contents | 0 .../WordPress 55.xcdatamodel/contents | 0 .../WordPress 56.xcdatamodel/contents | 0 .../WordPress 57.xcdatamodel/contents | 0 .../WordPress 58.xcdatamodel/contents | 0 .../WordPress 59.xcdatamodel/contents | 0 .../WordPress 6.xcdatamodel/elements | Bin .../WordPress 6.xcdatamodel/layout | Bin .../WordPress 60.xcdatamodel/contents | 0 .../WordPress 61.xcdatamodel/contents | 0 .../WordPress 62.xcdatamodel/contents | 0 .../WordPress 63.xcdatamodel/contents | 0 .../WordPress 64.xcdatamodel/contents | 0 .../WordPress 65.xcdatamodel/contents | 0 .../WordPress 66.xcdatamodel/contents | 0 .../WordPress 67.xcdatamodel/contents | 0 .../WordPress 68.xcdatamodel/contents | 0 .../WordPress 69.xcdatamodel/contents | 0 .../WordPress 7.xcdatamodel/elements | Bin .../WordPress 7.xcdatamodel/layout | Bin .../WordPress 70.xcdatamodel/contents | 0 .../WordPress 71.xcdatamodel/contents | 0 .../WordPress 72.xcdatamodel/contents | 0 .../WordPress 73.xcdatamodel/contents | 0 .../WordPress 74.xcdatamodel/contents | 0 .../WordPress 75.xcdatamodel/contents | 0 .../WordPress 76.xcdatamodel/contents | 0 .../WordPress 77.xcdatamodel/contents | 0 .../WordPress 78.xcdatamodel/contents | 0 .../WordPress 79.xcdatamodel/contents | 0 .../WordPress 8.xcdatamodel/elements | Bin .../WordPress 8.xcdatamodel/layout | Bin .../WordPress 80.xcdatamodel/contents | 0 .../WordPress 81.xcdatamodel/contents | 0 .../WordPress 82.xcdatamodel/contents | 0 .../WordPress 83.xcdatamodel/contents | 0 .../WordPress 84.xcdatamodel/contents | 0 .../WordPress 85.xcdatamodel/contents | 0 .../WordPress 86.xcdatamodel/contents | 0 .../WordPress 87.xcdatamodel/contents | 0 .../WordPress 88.xcdatamodel/contents | 0 .../WordPress 89.xcdatamodel/contents | 0 .../WordPress 9.xcdatamodel/elements | Bin .../WordPress 9.xcdatamodel/layout | Bin .../WordPress 90.xcdatamodel/contents | 0 .../WordPress 91.xcdatamodel/contents | 0 .../WordPress 92.xcdatamodel/contents | 0 .../WordPress 93.xcdatamodel/contents | 0 .../WordPress 94.xcdatamodel/contents | 0 .../WordPress 95.xcdatamodel/contents | 0 .../WordPress 96.xcdatamodel/contents | 0 .../WordPress 97.xcdatamodel/contents | 0 .../WordPress 98.xcdatamodel/contents | 0 .../WordPress 99.xcdatamodel/contents | 0 .../WordPress.xcdatamodel/elements | Bin .../WordPress.xcdatamodel/layout | Bin .../Sources/WordPressData/WordPressData.swift | 28 ++++++++++++++++++ .../Utility/CoreData/ContextManager.swift | 11 ++----- .../Utility/CoreData/CoreDataHelper.swift | 4 +-- .../Migrator/CoreDataIterativeMigrator.swift | 22 ++------------ 171 files changed, 34 insertions(+), 31 deletions(-) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/.xccurrentversion (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 100.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 101.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 102.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 103.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 104.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 105.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 106.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 107.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 108.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 109.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 110.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 111.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 112.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 113.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 114.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 115.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 116.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 117.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 118.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 119.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 120.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 121.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 122.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 123.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 124.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 125.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 126.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 127.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 128.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 129.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 13.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 130.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 131.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 132.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 133.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 134.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 135.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 136.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 137.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 138.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 139.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 14.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 141.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 142.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 143.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 144.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 145.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 146.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 147.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 148.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 149.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 15.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 150.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 152.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 16.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 17.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 18.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 19.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 20.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 21.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 22.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 23.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 24.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 25.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 26.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 27.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 28.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 29.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 30.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 31.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 32.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 33.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 34.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 35.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 36.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 37.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 38.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 39.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 40.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 41.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 42.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 43.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 44.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 45.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 46.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 47.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 48.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 49.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 50.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 51.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 52.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 53.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 54.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 55.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 56.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 57.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 58.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 59.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 60.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 61.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 62.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 63.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 64.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 65.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 66.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 67.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 68.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 69.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 70.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 71.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 72.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 73.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 74.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 75.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 76.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 77.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 78.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 79.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 80.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 81.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 82.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 83.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 84.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 85.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 86.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 87.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 88.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 89.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/layout (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 90.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 91.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 92.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 93.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 94.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 95.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 96.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 97.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 98.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress 99.xcdatamodel/contents (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress.xcdatamodel/elements (100%) rename {WordPress/Classes => Modules/Sources/WordPressData}/WordPress.xcdatamodeld/WordPress.xcdatamodel/layout (100%) diff --git a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/.xccurrentversion similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/.xccurrentversion diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 10.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 100.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 100.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 100.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 100.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 101.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 101.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 101.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 101.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 102.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 102.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 102.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 102.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 103.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 103.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 103.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 103.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 104.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 104.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 104.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 104.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 105.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 105.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 105.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 105.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 106.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 106.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 106.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 106.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 107.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 107.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 107.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 107.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 108.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 108.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 108.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 108.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 109.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 109.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 109.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 109.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 11.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 110.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 110.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 110.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 110.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 111.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 111.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 111.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 111.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 112.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 112.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 112.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 112.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 113.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 113.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 113.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 113.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 114.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 114.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 114.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 114.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 115.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 115.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 115.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 115.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 116.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 116.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 116.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 116.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 117.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 117.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 117.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 117.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 118.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 118.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 118.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 118.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 119.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 119.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 119.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 119.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 12.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 120.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 120.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 120.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 120.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 121.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 121.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 121.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 121.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 122.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 122.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 122.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 122.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 123.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 123.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 123.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 123.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 124.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 124.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 124.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 124.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 125.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 125.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 125.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 125.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 126.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 126.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 126.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 126.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 127.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 127.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 127.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 127.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 128.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 128.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 128.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 128.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 129.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 129.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 129.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 129.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 13.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 13.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 13.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 13.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 130.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 130.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 130.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 130.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 131.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 131.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 131.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 131.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 132.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 132.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 132.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 132.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 133.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 133.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 133.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 133.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 134.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 134.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 134.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 134.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 135.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 135.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 135.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 135.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 136.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 136.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 136.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 136.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 137.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 137.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 137.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 137.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 138.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 138.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 138.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 138.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 139.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 139.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 139.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 139.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 14.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 14.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 14.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 14.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 141.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 141.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 141.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 141.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 142.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 142.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 142.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 142.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 143.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 143.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 143.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 143.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 144.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 144.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 144.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 144.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 145.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 145.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 145.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 145.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 146.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 146.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 146.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 146.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 147.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 147.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 147.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 147.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 148.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 148.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 148.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 148.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 149.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 149.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 149.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 149.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 15.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 15.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 15.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 15.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 150.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 150.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 150.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 150.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 152.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 152.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 152.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 152.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 16.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 16.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 16.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 16.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 17.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 17.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 17.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 17.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 18.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 18.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 18.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 18.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 19.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 19.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 19.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 19.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 2.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 20.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 20.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 20.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 20.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 21.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 21.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 21.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 21.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 22.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 22.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 22.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 22.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 23.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 23.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 23.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 23.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 24.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 24.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 24.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 24.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 25.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 25.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 25.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 25.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 26.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 26.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 26.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 26.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 27.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 27.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 27.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 27.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 28.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 28.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 28.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 28.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 29.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 29.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 29.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 29.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 3.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 30.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 30.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 30.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 30.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 31.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 31.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 31.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 31.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 32.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 32.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 32.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 32.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 33.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 33.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 33.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 33.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 34.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 34.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 34.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 34.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 35.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 35.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 35.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 35.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 36.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 36.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 36.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 36.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 37.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 37.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 37.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 37.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 38.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 38.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 38.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 38.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 39.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 39.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 39.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 39.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 4.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 40.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 40.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 40.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 40.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 41.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 41.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 41.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 41.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 42.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 42.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 42.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 42.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 43.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 43.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 43.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 43.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 44.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 44.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 44.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 44.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 45.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 45.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 45.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 45.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 46.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 46.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 46.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 46.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 47.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 47.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 47.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 47.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 48.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 48.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 48.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 48.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 49.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 49.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 49.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 49.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 5.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 50.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 50.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 50.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 50.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 51.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 51.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 51.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 51.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 52.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 52.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 52.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 52.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 53.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 53.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 53.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 53.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 54.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 54.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 54.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 54.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 55.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 55.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 55.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 55.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 56.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 56.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 56.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 56.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 57.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 57.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 57.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 57.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 58.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 58.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 58.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 58.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 59.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 59.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 59.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 59.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 6.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 60.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 60.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 60.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 60.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 61.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 61.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 61.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 61.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 62.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 62.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 62.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 62.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 63.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 63.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 63.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 63.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 64.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 64.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 64.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 64.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 65.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 65.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 65.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 65.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 66.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 66.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 66.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 66.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 67.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 67.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 67.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 67.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 68.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 68.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 68.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 68.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 69.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 69.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 69.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 69.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 7.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 70.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 70.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 70.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 70.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 71.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 71.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 71.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 71.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 72.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 72.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 72.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 72.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 73.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 73.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 73.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 73.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 74.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 74.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 74.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 74.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 75.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 75.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 75.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 75.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 76.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 76.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 76.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 76.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 77.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 77.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 77.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 77.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 78.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 78.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 78.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 78.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 79.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 79.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 79.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 79.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 8.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 80.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 80.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 80.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 80.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 81.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 81.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 81.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 81.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 82.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 82.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 82.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 82.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 83.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 83.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 83.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 83.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 84.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 84.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 84.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 84.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 85.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 85.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 85.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 85.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 86.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 86.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 86.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 86.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 87.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 87.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 87.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 87.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 88.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 88.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 88.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 88.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 89.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 89.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 89.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 89.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 9.xcdatamodel/layout diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 90.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 90.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 90.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 90.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 91.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 91.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 91.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 91.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 92.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 92.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 92.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 92.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 93.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 93.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 93.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 93.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 94.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 94.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 94.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 94.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 95.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 95.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 95.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 95.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 96.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 96.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 96.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 96.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 97.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 97.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 97.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 97.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 98.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 98.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 98.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 98.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 99.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 99.xcdatamodel/contents similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress 99.xcdatamodel/contents rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 99.xcdatamodel/contents diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress.xcdatamodel/elements b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress.xcdatamodel/elements similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress.xcdatamodel/elements rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress.xcdatamodel/elements diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress.xcdatamodel/layout b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress.xcdatamodel/layout similarity index 100% rename from WordPress/Classes/WordPress.xcdatamodeld/WordPress.xcdatamodel/layout rename to Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress.xcdatamodel/layout diff --git a/Modules/Sources/WordPressData/WordPressData.swift b/Modules/Sources/WordPressData/WordPressData.swift index 2e8f63502b5a..0552f8fba127 100644 --- a/Modules/Sources/WordPressData/WordPressData.swift +++ b/Modules/Sources/WordPressData/WordPressData.swift @@ -1 +1,29 @@ @_exported import WordPressDataObjC + +public let modelURL: URL = { + guard let url = Bundle.module.url(forResource: "WordPress", withExtension: "momd") else { + fatalError("Cannot find model file.") + } + return url +}() + +public func urlForModel(name: String, in directory: String?) -> URL? { + let bundle = Bundle(for: TemporaryDummyClassToPickUpModule.self) + var url = bundle.url(forResource: name, withExtension: "mom", subdirectory: directory) + + if url != nil { + return url + } + + let momdPaths = bundle.paths(forResourcesOfType: "momd", inDirectory: directory) + momdPaths.forEach { (path) in + if url != nil { + return + } + url = bundle.url(forResource: name, withExtension: "mom", subdirectory: URL(fileURLWithPath: path).lastPathComponent) + } + + return url +} + +private class TemporaryDummyClassToPickUpModule {} diff --git a/WordPress/Classes/Utility/CoreData/ContextManager.swift b/WordPress/Classes/Utility/CoreData/ContextManager.swift index 119d61f586fb..ef874a9bb1ed 100644 --- a/WordPress/Classes/Utility/CoreData/ContextManager.swift +++ b/WordPress/Classes/Utility/CoreData/ContextManager.swift @@ -154,12 +154,7 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift { DDLogWarn("Migration required for persistent store.") - let dbBundle = Bundle.main - guard let modelFileURL = dbBundle.url(forResource: "WordPress", withExtension: "momd") else { - fatalError("Can't find WordPress.momd") - } - - guard let versionInfo = NSDictionary(contentsOf: modelFileURL.appendingPathComponent("VersionInfo.plist")) else { + guard let versionInfo = NSDictionary(contentsOf: WordPressData.modelURL.appendingPathComponent("VersionInfo.plist")) else { fatalError("Can't get the object model's version info") } @@ -222,9 +217,7 @@ private extension ContextManager { private extension ContextManager { static func createPersistentContainer(storeURL: URL, modelName: String) -> NSPersistentContainer { - guard var modelFileURL = Bundle.main.url(forResource: "WordPress", withExtension: "momd") else { - fatalError("Can't find WordPress.momd") - } + var modelFileURL = WordPressData.modelURL if modelName != ContextManagerModelNameCurrent { modelFileURL = modelFileURL.appendingPathComponent(modelName).appendingPathExtension("mom") diff --git a/WordPress/Classes/Utility/CoreData/CoreDataHelper.swift b/WordPress/Classes/Utility/CoreData/CoreDataHelper.swift index 489faeb65f6b..182449749978 100644 --- a/WordPress/Classes/Utility/CoreData/CoreDataHelper.swift +++ b/WordPress/Classes/Utility/CoreData/CoreDataHelper.swift @@ -1,5 +1,6 @@ import Foundation import CoreData +import WordPressData import WordPressShared // MARK: - NSManagedObject Default entityName Helper @@ -357,8 +358,7 @@ extension CoreDataStack { } private func migrateDatabaseIfNecessary(at databaseLocation: URL) throws { - guard let modelFileURL = Bundle.main.url(forResource: "WordPress", withExtension: "momd"), - let objectModel = NSManagedObjectModel(contentsOf: modelFileURL) else { + guard let objectModel = NSManagedObjectModel(contentsOf: WordPressData.modelURL) else { return } try ContextManager.migrateDataModelsIfNecessary(storeURL: databaseLocation, objectModel: objectModel) diff --git a/WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift b/WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift index d22c071db2e9..c171ebb79adc 100644 --- a/WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift +++ b/WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift @@ -1,5 +1,6 @@ import Foundation import CoreData +import WordPressData /// CoreDataIterativeMigrator: Migrates through a series of models to allow for users to skip app versions without risk. /// @@ -271,7 +272,7 @@ private extension CoreDataIterativeMigrator { static func models(for names: [String]) throws -> [NSManagedObjectModel] { let models = try names.map { (name) -> NSManagedObjectModel in - guard let url = urlForModel(name: name, in: nil), + guard let url = WordPressData.urlForModel(name: name, in: nil), let model = NSManagedObjectModel(contentsOf: url) else { let description = "No model found for \(name)" throw error(with: .noModelFound, description: description) @@ -282,25 +283,6 @@ private extension CoreDataIterativeMigrator { return models } - - static func urlForModel(name: String, in directory: String?) -> URL? { - let bundle = Bundle(for: ContextManager.self) - var url = bundle.url(forResource: name, withExtension: "mom", subdirectory: directory) - - if url != nil { - return url - } - - let momdPaths = bundle.paths(forResourcesOfType: "momd", inDirectory: directory) - momdPaths.forEach { (path) in - if url != nil { - return - } - url = bundle.url(forResource: name, withExtension: "mom", subdirectory: URL(fileURLWithPath: path).lastPathComponent) - } - - return url - } } enum IterativeMigratorErrorCodes: Int { From 54d315078b64e16ecec9b8908cf3ae2c27848890 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 7 Mar 2025 17:29:55 +1100 Subject: [PATCH 03/25] =?UTF-8?q?App=20crashes=20=E2=80=93=20Move=20`Blogg?= =?UTF-8?q?ingPrompt`=20to=20WordPressData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WordPressData/Models/BloggingPrompt.swift | 45 +++++++++++++++++++ .../Extensions/Post+BloggingPrompts.swift | 1 + .../Models/BloggingPrompt+CoreDataClass.swift | 24 +++------- .../BloggingPrompt+CoreDataProperties.swift | 29 +----------- .../PromptRemindersScheduler.swift | 1 + .../Prompts/DashboardPromptsCardCell.swift | 1 + .../BloggingPromptCoordinator.swift | 1 + .../BloggingPromptTableViewCell.swift | 2 + .../BloggingPromptsViewController.swift | 1 + .../RootViewCoordinator+BloggingPrompt.swift | 1 + ...BloggingPromptsIntroductionPresenter.swift | 1 + .../Post/EditPostViewController.swift | 1 + .../BloggingPromptsHeaderView.swift | 1 + .../CreateButtonCoordinator.swift | 1 + 14 files changed, 64 insertions(+), 46 deletions(-) create mode 100644 Modules/Sources/WordPressData/Models/BloggingPrompt.swift diff --git a/Modules/Sources/WordPressData/Models/BloggingPrompt.swift b/Modules/Sources/WordPressData/Models/BloggingPrompt.swift new file mode 100644 index 000000000000..2efba01e130e --- /dev/null +++ b/Modules/Sources/WordPressData/Models/BloggingPrompt.swift @@ -0,0 +1,45 @@ +import CoreData +import WordPressSharedObjC + +public class BloggingPrompt: NSManagedObject { + + /// The unique ID for the prompt, received from the server. + @NSManaged public var promptID: Int32 + + /// The site ID for the prompt. + @NSManaged public var siteID: Int32 + + /// The prompt content to be displayed at entry points. + @NSManaged public var text: String + + /// The attribution source for the prompt. + @NSManaged public var attribution: String + + /// The prompt date. Time information should be ignored. + @NSManaged public var date: Date + + /// Whether the current user has answered the prompt in `siteID`. + @NSManaged public var answered: Bool + + /// The number of users that has answered the prompt. + @NSManaged public var answerCount: Int32 + + /// Contains avatar URLs of some users that have answered the prompt. + @NSManaged public var displayAvatarURLs: [URL] + + /// Contains additional tags that should be appended to the post for this prompt's answer. + @NSManaged public var additionalPostTags: [String]? + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: Self.classNameWithoutNamespaces()) + } + + @nonobjc public class func newObject(in context: NSManagedObjectContext) -> BloggingPrompt? { + return NSEntityDescription.insertNewObject(forEntityName: Self.classNameWithoutNamespaces(), into: context) as? BloggingPrompt + } + + public override func awakeFromInsert() { + self.date = .init(timeIntervalSince1970: 0) + self.displayAvatarURLs = [] + } +} diff --git a/WordPress/Classes/Extensions/Post+BloggingPrompts.swift b/WordPress/Classes/Extensions/Post+BloggingPrompts.swift index 7aac23b727aa..b2e8ef7963c7 100644 --- a/WordPress/Classes/Extensions/Post+BloggingPrompts.swift +++ b/WordPress/Classes/Extensions/Post+BloggingPrompts.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressData extension Post { diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift index b107887267a9..94eb00eec2bc 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -1,25 +1,9 @@ import Foundation import CoreData +import WordPressData import WordPressKit -public class BloggingPrompt: NSManagedObject { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: Self.classNameWithoutNamespaces()) - } - - @nonobjc public class func newObject(in context: NSManagedObjectContext) -> BloggingPrompt? { - return NSEntityDescription.insertNewObject(forEntityName: Self.classNameWithoutNamespaces(), into: context) as? BloggingPrompt - } - - public override func awakeFromInsert() { - self.date = .init(timeIntervalSince1970: 0) - self.displayAvatarURLs = [] - } - - var promptAttribution: BloggingPromptsAttribution? { - BloggingPromptsAttribution(rawValue: attribution.lowercased()) - } +extension BloggingPrompt { /// Convenience method to map properties from `BloggingPromptRemoteObject`. /// @@ -42,6 +26,10 @@ public class BloggingPrompt: NSManagedObject { } } + var promptAttribution: BloggingPromptsAttribution? { + BloggingPromptsAttribution(rawValue: attribution.lowercased()) + } + func textForDisplay() -> String { return text.stringByDecodingXMLCharacters().trim() } diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift index 47fac71d4113..d512e2beaa0b 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift @@ -1,31 +1,4 @@ import Foundation import CoreData -extension BloggingPrompt { - /// The unique ID for the prompt, received from the server. - @NSManaged public var promptID: Int32 - - /// The site ID for the prompt. - @NSManaged public var siteID: Int32 - - /// The prompt content to be displayed at entry points. - @NSManaged public var text: String - - /// The attribution source for the prompt. - @NSManaged public var attribution: String - - /// The prompt date. Time information should be ignored. - @NSManaged public var date: Date - - /// Whether the current user has answered the prompt in `siteID`. - @NSManaged public var answered: Bool - - /// The number of users that has answered the prompt. - @NSManaged public var answerCount: Int32 - - /// Contains avatar URLs of some users that have answered the prompt. - @NSManaged public var displayAvatarURLs: [URL] - - /// Contains additional tags that should be appended to the post for this prompt's answer. - @NSManaged public var additionalPostTags: [String]? -} +// FIXME: Delete diff --git a/WordPress/Classes/Utility/Blogging Prompts/PromptRemindersScheduler.swift b/WordPress/Classes/Utility/Blogging Prompts/PromptRemindersScheduler.swift index 43cc737d739c..86642b44a304 100644 --- a/WordPress/Classes/Utility/Blogging Prompts/PromptRemindersScheduler.swift +++ b/WordPress/Classes/Utility/Blogging Prompts/PromptRemindersScheduler.swift @@ -1,5 +1,6 @@ import Foundation import UserNotifications +import WordPressData /// Encapsulates the local notification scheduling logic for Blogging Prompts. /// diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift index e7f06e08bf67..25043aa3cad8 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift @@ -1,5 +1,6 @@ import UIKit import SafariServices +import WordPressData import WordPressShared import WordPressFlux diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift index e9e6ee74f50d..217e93f218ea 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressData /// Helps manage the flow related to Blogging Prompts. /// diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptTableViewCell.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptTableViewCell.swift index 663831890eb6..a8d6b5159985 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptTableViewCell.swift @@ -1,3 +1,5 @@ +import WordPressData + class BloggingPromptTableViewCell: UITableViewCell, NibReusable { // MARK: - Properties diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift index ece125b36186..db518db2178c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressData class BloggingPromptsViewController: UIViewController, NoResultsViewHost { diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift index ba0292e40518..d550bbfc5fd3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/RootViewCoordinator+BloggingPrompt.swift @@ -1,3 +1,4 @@ +import WordPressData /// Encapsulates logic related to Blogging Prompts in RootViewCoordinator. /// diff --git a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift index cdc4a4e17ce8..033abd321c6d 100644 --- a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift +++ b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import WordPressData /// Presents the BloggingPromptsFeatureIntroduction with actionable buttons /// and directs the flow according to which action button is tapped. diff --git a/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift b/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift index 524fda7ec2de..18edffbd433b 100644 --- a/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressData class EditPostViewController: UIViewController { diff --git a/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift b/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift index 3b8858a80a65..f6c2246ffee8 100644 --- a/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift +++ b/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressData class BloggingPromptsHeaderView: UIView, NibLoadable { @IBOutlet private weak var containerStackView: UIStackView! diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift index aea4b03d2c22..db61a7dfcc12 100644 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift @@ -1,4 +1,5 @@ import Gridicons +import WordPressData import WordPressFlux import WordPressUI From e1eb0b3704b166153a11c61f49dd75d1b11e7cb2 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 7 Mar 2025 17:39:29 +1100 Subject: [PATCH 04/25] =?UTF-8?q?Hardcode=20module=20name=20in=20`Blogging?= =?UTF-8?q?Prompt`=20model=20=E2=80=93=20Fixes=20the=20crash=3F!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents index 2267cb2313e1..14acbaeae04b 100644 --- a/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents +++ b/Modules/Sources/WordPressData/WordPress.xcdatamodeld/WordPress 154.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -215,7 +215,7 @@ - + From 43bb76ae038aed2e2ef34e0c640e5060dbaeea0a Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 11:53:00 +1100 Subject: [PATCH 05/25] Move `NSManagedObject+Lookup` to WordPressData --- .../Sources/WordPressData}/NSManagedObject+Lookup.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename {WordPress/Classes/Utility/CoreData => Modules/Sources/WordPressData}/NSManagedObject+Lookup.swift (89%) diff --git a/WordPress/Classes/Utility/CoreData/NSManagedObject+Lookup.swift b/Modules/Sources/WordPressData/NSManagedObject+Lookup.swift similarity index 89% rename from WordPress/Classes/Utility/CoreData/NSManagedObject+Lookup.swift rename to Modules/Sources/WordPressData/NSManagedObject+Lookup.swift index d28c9610bca7..bb1662958628 100644 --- a/WordPress/Classes/Utility/CoreData/NSManagedObject+Lookup.swift +++ b/Modules/Sources/WordPressData/NSManagedObject+Lookup.swift @@ -1,6 +1,7 @@ import CoreData -extension NSManagedObject { +// TODO: How to we test this? +public extension NSManagedObject { /// Lookup an object by its NSManagedObjectID /// From 038d840d8d6fa30b94527d6ddf693fb0b55924a0 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 12:28:02 +1100 Subject: [PATCH 06/25] Add CocoaLumberjack to WordPressData as a dependency --- Modules/Package.swift | 4 +++- Modules/Sources/WordPressData/NSManagedObject+Lookup.swift | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 02e73fbad4e3..8c9d65f20ebf 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -69,7 +69,9 @@ let package = Package( name: "WordPressData", dependencies: [ .target(name: "WordPressDataObjC"), - .target(name: "WordPressSharedObjC") + .target(name: "WordPressSharedObjC"), + .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), + .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack") ] ), .target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]), diff --git a/Modules/Sources/WordPressData/NSManagedObject+Lookup.swift b/Modules/Sources/WordPressData/NSManagedObject+Lookup.swift index bb1662958628..a55c7ccb26bb 100644 --- a/Modules/Sources/WordPressData/NSManagedObject+Lookup.swift +++ b/Modules/Sources/WordPressData/NSManagedObject+Lookup.swift @@ -1,4 +1,5 @@ import CoreData +import CocoaLumberjackSwift // TODO: How to we test this? public extension NSManagedObject { @@ -10,6 +11,7 @@ public extension NSManagedObject { /// - context: An NSManagedObjectContext that contains the associated object. /// - Returns: The `NSManagedObject` subclass associated with the given `objectID`, if it exists. static func lookup(withObjectID objectID: NSManagedObjectID, in context: NSManagedObjectContext) -> Self? { + DDLogInfo("Dummy log to test CocoaLumberjack import") // FIXME: Remove once we have a legit usage return try? context.existingObject(with: objectID) as? Self } } From f592cc1edb325d5a922e1d8e5c20e1bb19d0943a Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 13:33:40 +1100 Subject: [PATCH 07/25] Make `AsyncOperation` and `AsyncBlockOperation` `public` This is so the diff is neater next when moving it to WordPressShared --- .../Stats/Operation/AsyncBlockOperation.swift | 4 ++-- .../ViewRelated/Stats/Operation/AsyncOperation.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift b/WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift index d7cac4c0c75e..4306fcd8ce15 100644 --- a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift +++ b/WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift @@ -1,4 +1,4 @@ -class AsyncBlockOperation: AsyncOperation, @unchecked Sendable { +public class AsyncBlockOperation: AsyncOperation, @unchecked Sendable { private let block: (@escaping () -> Void) -> Void @@ -6,7 +6,7 @@ class AsyncBlockOperation: AsyncOperation, @unchecked Sendable { self.block = block } - override func main() { + public override func main() { self.block { [weak self] in self?.state = .isFinished } diff --git a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift b/WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift index 53bbf646e8bd..b31d09794e68 100644 --- a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift +++ b/WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift @@ -1,9 +1,9 @@ -class AsyncOperation: Operation, @unchecked Sendable { +public class AsyncOperation: Operation, @unchecked Sendable { enum State: String { case isReady, isExecuting, isFinished } - override var isAsynchronous: Bool { + public override var isAsynchronous: Bool { return true } @@ -18,15 +18,15 @@ class AsyncOperation: Operation, @unchecked Sendable { } } - override var isExecuting: Bool { + public override var isExecuting: Bool { return state == .isExecuting } - override var isFinished: Bool { + public override var isFinished: Bool { return state == .isFinished } - override func start() { + public override func start() { if isCancelled { state = .isFinished return From 916a7c65f50e945a9224c074b2665c4b26ecb2a3 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 16:00:18 +1100 Subject: [PATCH 08/25] Move `AsyncOperation` and `AsyncBlockOperation` to WordPressShared Turned out there needed to be move access control changes than those done in the previous commit... --- .../AsyncOperation}/AsyncBlockOperation.swift | 2 +- .../WordPressShared/AsyncOperation}/AsyncOperation.swift | 6 +++--- .../Tests/WordPressSharedTests}/AsyncOperationTests.swift | 2 +- WordPress/Classes/Services/MediaCoordinator.swift | 1 + WordPress/Classes/Services/NotificationSyncMediator.swift | 1 + WordPress/Classes/Stores/RemoteFeatureFlagStore.swift | 1 + WordPress/Classes/Utility/CoreData/ContextManager.swift | 1 + .../Stats/Operation/StatsPeriodAsyncOperation.swift | 2 ++ WordPress/WordPress.xcodeproj/project.pbxproj | 4 ---- 9 files changed, 11 insertions(+), 9 deletions(-) rename {WordPress/Classes/ViewRelated/Stats/Operation => Modules/Sources/WordPressShared/AsyncOperation}/AsyncBlockOperation.swift (81%) rename {WordPress/Classes/ViewRelated/Stats/Operation => Modules/Sources/WordPressShared/AsyncOperation}/AsyncOperation.swift (85%) rename {WordPress/WordPressTest => Modules/Tests/WordPressSharedTests}/AsyncOperationTests.swift (95%) diff --git a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift b/Modules/Sources/WordPressShared/AsyncOperation/AsyncBlockOperation.swift similarity index 81% rename from WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift rename to Modules/Sources/WordPressShared/AsyncOperation/AsyncBlockOperation.swift index 4306fcd8ce15..92013f12ce5c 100644 --- a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncBlockOperation.swift +++ b/Modules/Sources/WordPressShared/AsyncOperation/AsyncBlockOperation.swift @@ -2,7 +2,7 @@ public class AsyncBlockOperation: AsyncOperation, @unchecked Sendable { private let block: (@escaping () -> Void) -> Void - init(block: @escaping (@escaping () -> Void) -> Void) { + public init(block: @escaping (@escaping () -> Void) -> Void) { self.block = block } diff --git a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift b/Modules/Sources/WordPressShared/AsyncOperation/AsyncOperation.swift similarity index 85% rename from WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift rename to Modules/Sources/WordPressShared/AsyncOperation/AsyncOperation.swift index b31d09794e68..b4338ee9f00f 100644 --- a/WordPress/Classes/ViewRelated/Stats/Operation/AsyncOperation.swift +++ b/Modules/Sources/WordPressShared/AsyncOperation/AsyncOperation.swift @@ -1,5 +1,5 @@ -public class AsyncOperation: Operation, @unchecked Sendable { - enum State: String { +open class AsyncOperation: Operation, @unchecked Sendable { + public enum State: String { case isReady, isExecuting, isFinished } @@ -7,7 +7,7 @@ public class AsyncOperation: Operation, @unchecked Sendable { return true } - var state = State.isReady { + public var state = State.isReady { willSet { willChangeValue(forKey: state.rawValue) willChangeValue(forKey: newValue.rawValue) diff --git a/WordPress/WordPressTest/AsyncOperationTests.swift b/Modules/Tests/WordPressSharedTests/AsyncOperationTests.swift similarity index 95% rename from WordPress/WordPressTest/AsyncOperationTests.swift rename to Modules/Tests/WordPressSharedTests/AsyncOperationTests.swift index a6652296946c..774e83c5a442 100644 --- a/WordPress/WordPressTest/AsyncOperationTests.swift +++ b/Modules/Tests/WordPressSharedTests/AsyncOperationTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import WordPress +import WordPressShared class AsyncOperationTests: XCTestCase { let operation = AsyncOperation() diff --git a/WordPress/Classes/Services/MediaCoordinator.swift b/WordPress/Classes/Services/MediaCoordinator.swift index efa4ecf9b359..b73a0f4362b9 100644 --- a/WordPress/Classes/Services/MediaCoordinator.swift +++ b/WordPress/Classes/Services/MediaCoordinator.swift @@ -1,6 +1,7 @@ import Foundation import WordPressData import WordPressFlux +import WordPressShared import class AutomatticTracks.CrashLogging import enum Alamofire.AFError diff --git a/WordPress/Classes/Services/NotificationSyncMediator.swift b/WordPress/Classes/Services/NotificationSyncMediator.swift index a7ae5979b2b3..09e7fa022d98 100644 --- a/WordPress/Classes/Services/NotificationSyncMediator.swift +++ b/WordPress/Classes/Services/NotificationSyncMediator.swift @@ -1,6 +1,7 @@ import Foundation import WordPressData import WordPressKit +import WordPressShared /// Notes: /// diff --git a/WordPress/Classes/Stores/RemoteFeatureFlagStore.swift b/WordPress/Classes/Stores/RemoteFeatureFlagStore.swift index a284cf2205c6..33a55b235860 100644 --- a/WordPress/Classes/Stores/RemoteFeatureFlagStore.swift +++ b/WordPress/Classes/Stores/RemoteFeatureFlagStore.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressShared import WordPressKit class RemoteFeatureFlagStore { diff --git a/WordPress/Classes/Utility/CoreData/ContextManager.swift b/WordPress/Classes/Utility/CoreData/ContextManager.swift index ef874a9bb1ed..b492a06136c4 100644 --- a/WordPress/Classes/Utility/CoreData/ContextManager.swift +++ b/WordPress/Classes/Utility/CoreData/ContextManager.swift @@ -1,6 +1,7 @@ import CoreData import Foundation import WordPressData +import WordPressShared /// A constant representing the current version of the data model. /// diff --git a/WordPress/Classes/ViewRelated/Stats/Operation/StatsPeriodAsyncOperation.swift b/WordPress/Classes/ViewRelated/Stats/Operation/StatsPeriodAsyncOperation.swift index 6d9e898ee82b..80a4fe4e165a 100644 --- a/WordPress/Classes/ViewRelated/Stats/Operation/StatsPeriodAsyncOperation.swift +++ b/WordPress/Classes/ViewRelated/Stats/Operation/StatsPeriodAsyncOperation.swift @@ -1,3 +1,5 @@ +import WordPressShared + final class StatsPeriodAsyncOperation: AsyncOperation, @unchecked Sendable { typealias StatsPeriodCompletion = (TimeStatsType?, Error?) -> Void diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index aa9c113b6079..16ed3070d1b2 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1026,7 +1026,6 @@ 93F2E5441E9E5A570050D489 /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93F2E5431E9E5A570050D489 /* CoreSpotlight.framework */; }; 98BFF57E23984345008A1DCB /* AllTimeWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFF57D23984344008A1DCB /* AllTimeWidgetStats.swift */; }; 98E58A2F2360D23400E5534B /* TodayWidgetStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E58A2E2360D23400E5534B /* TodayWidgetStats.swift */; }; - 9A9D34FD23607CCC00BC95A3 /* AsyncOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9D34FC23607CCC00BC95A3 /* AsyncOperationTests.swift */; }; 9A9D34FF2360A4E200BC95A3 /* StatsPeriodAsyncOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9D34FE2360A4E200BC95A3 /* StatsPeriodAsyncOperationTests.swift */; }; A01C542E0E24E88400D411F2 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A01C542D0E24E88400D411F2 /* SystemConfiguration.framework */; }; A01C55480E25E0D000D411F2 /* defaultPostTemplate.html in Resources */ = {isa = PBXBuildFile; fileRef = A01C55470E25E0D000D411F2 /* defaultPostTemplate.html */; }; @@ -2800,7 +2799,6 @@ 98BFF57D23984344008A1DCB /* AllTimeWidgetStats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllTimeWidgetStats.swift; sourceTree = ""; }; 98E58A2E2360D23400E5534B /* TodayWidgetStats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodayWidgetStats.swift; sourceTree = ""; }; 98FB6E9F23074CE5002DDC8D /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; - 9A9D34FC23607CCC00BC95A3 /* AsyncOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperationTests.swift; sourceTree = ""; }; 9A9D34FE2360A4E200BC95A3 /* StatsPeriodAsyncOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodAsyncOperationTests.swift; sourceTree = ""; }; A01C542D0E24E88400D411F2 /* SystemConfiguration.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; A01C55470E25E0D000D411F2 /* defaultPostTemplate.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = defaultPostTemplate.html; path = Resources/HTML/defaultPostTemplate.html; sourceTree = ""; }; @@ -5935,7 +5933,6 @@ 9A9D34FB23607C8400BC95A3 /* Operations */ = { isa = PBXGroup; children = ( - 9A9D34FC23607CCC00BC95A3 /* AsyncOperationTests.swift */, 9A9D34FE2360A4E200BC95A3 /* StatsPeriodAsyncOperationTests.swift */, ); name = Operations; @@ -9682,7 +9679,6 @@ DCF892D0282FA42A00BB71E1 /* SiteStatsImmuTableRowsTests.swift in Sources */, 93EF094C19ED533500C89770 /* ContextManagerTests.swift in Sources */, F17A2A2023BFBD84001E96AC /* UIView+ExistingConstraints.swift in Sources */, - 9A9D34FD23607CCC00BC95A3 /* AsyncOperationTests.swift in Sources */, B5552D821CD1061F00B26DF6 /* StringExtensionsTests.swift in Sources */, 3F759FBA2A2DA93B0039A845 /* WPAccount+Fixture.swift in Sources */, 77A141172B68546100BF75DD /* BooleanUserDefaultsDebugViewModelTests.swift in Sources */, From 6cc4f5b1c0ae4ecd887ac5e8b66176a0403f2e5e Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 16:49:48 +1100 Subject: [PATCH 09/25] Use Swift 5 in WordPressData to avoid having to support `Sendable` ATM We'll obviously have to deal with this at some point, but doing it while moving the files from the app targets to the new module is not the right time. --- Modules/Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 8c9d65f20ebf..74c64fcdb022 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -72,7 +72,9 @@ let package = Package( .target(name: "WordPressSharedObjC"), .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack") - ] + ], + // Avoid supporting Sendable and concurrency for the time being... + swiftSettings: [.swiftLanguageMode(.v5)] ), .target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressCore", dependencies: [.target(name: "WordPressShared"), .product(name: "WordPressAPI", package: "wordpress-rs")]), From 3b54959aed6ac503cfcc79b643363ad7b2544544 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 16:01:00 +1100 Subject: [PATCH 10/25] =?UTF-8?q?WIP=20=E2=80=93=20Move=20ContextManager?= =?UTF-8?q?=20and=20all=20it=20needs=20to=20compile=20to=20WordPressData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Modules/Package.swift | 4 +++ .../ContextManager+ErrorHandling.swift | 1 + .../WordPressData}/ContextManager.swift | 30 ++++++++++++------- .../CoreDataIterativeMigrator.swift | 4 +-- .../Sources/WordPressData/Extensions.swift | 12 ++++---- .../ContextManagerTests.swift | 0 .../CoreDataHelperTests.swift | 0 .../Models/Blog/Blog+HomepageSettings.swift | 1 + .../Classes/Models/Blog/Blog+Lookup.swift | 1 + .../Classes/Models/Blog/Blog+SelfHosted.swift | 1 + WordPress/Classes/Models/Domain.swift | 1 + .../Actions/NotificationAction.swift | 2 ++ WordPress/Classes/Models/Post.swift | 1 + WordPress/Classes/Models/Role.swift | 1 + .../Services/NullBlogPropertySanitizer.swift | 1 + .../ContentMigrationCoordinator.swift | 2 ++ WordPress/WordPress.xcodeproj/project.pbxproj | 8 ----- 17 files changed, 45 insertions(+), 25 deletions(-) rename {WordPress/Classes/Utility/CoreData => Modules/Sources/WordPressData}/ContextManager+ErrorHandling.swift (99%) rename {WordPress/Classes/Utility/CoreData => Modules/Sources/WordPressData}/ContextManager.swift (88%) rename {WordPress/Classes/Utility/CoreData/Migrator => Modules/Sources/WordPressData}/CoreDataIterativeMigrator.swift (99%) rename WordPress/Classes/Utility/CoreData/CoreDataHelper.swift => Modules/Sources/WordPressData/Extensions.swift (97%) rename {WordPress/WordPressTest => Modules/Tests/WordPressDataTests}/ContextManagerTests.swift (100%) rename {WordPress/WordPressTest => Modules/Tests/WordPressDataTests}/CoreDataHelperTests.swift (100%) diff --git a/Modules/Package.swift b/Modules/Package.swift index 74c64fcdb022..12b84680e9df 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -94,6 +94,10 @@ let package = Package( ), .testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")], swiftSettings: [.swiftLanguageMode(.v5)]), .testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]), + .testTarget( + name: "WordPressDataTests", + dependencies: ["WordPressData"] + ), .testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"], swiftSettings: [.swiftLanguageMode(.v5)]), .testTarget(name: "AsyncImageKitTests", dependencies: [ .target(name: "AsyncImageKit"), diff --git a/WordPress/Classes/Utility/CoreData/ContextManager+ErrorHandling.swift b/Modules/Sources/WordPressData/ContextManager+ErrorHandling.swift similarity index 99% rename from WordPress/Classes/Utility/CoreData/ContextManager+ErrorHandling.swift rename to Modules/Sources/WordPressData/ContextManager+ErrorHandling.swift index 9d73ee139845..d5c330a35825 100644 --- a/WordPress/Classes/Utility/CoreData/ContextManager+ErrorHandling.swift +++ b/Modules/Sources/WordPressData/ContextManager+ErrorHandling.swift @@ -1,4 +1,5 @@ import CoreData +import CocoaLumberjackSwift // Imported from CoreData.CoreDataErrors private let coreDataKnownErrorCodes = [ diff --git a/WordPress/Classes/Utility/CoreData/ContextManager.swift b/Modules/Sources/WordPressData/ContextManager.swift similarity index 88% rename from WordPress/Classes/Utility/CoreData/ContextManager.swift rename to Modules/Sources/WordPressData/ContextManager.swift index b492a06136c4..dbe226122460 100644 --- a/WordPress/Classes/Utility/CoreData/ContextManager.swift +++ b/Modules/Sources/WordPressData/ContextManager.swift @@ -1,6 +1,6 @@ +import CocoaLumberjackSwift import CoreData import Foundation -import WordPressData import WordPressShared /// A constant representing the current version of the data model. @@ -57,13 +57,18 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift { super.init() mainContext.automaticallyMergesChangesFromParent = true - mainContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - NullBlogPropertySanitizer(context: mainContext).sanitize() + // FIXME: Temporary disabled to move on with migration. Gives error: + // Reference to var 'NSMergeByPropertyObjectTrumpMergePolicy' is not concurrency-safe because it involves shared mutable state + // mainContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + // FIXME: We still need to migrate this. Too many dependencies on the models + // NullBlogPropertySanitizer(context: mainContext).sanitize() } public func newDerivedContext() -> NSManagedObjectContext { let context = persistentContainer.newBackgroundContext() - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + // FIXME: Temporary disabled to move on with migration. Gives error: + // Reference to var 'NSMergeByPropertyObjectTrumpMergePolicy' is not concurrency-safe because it involves shared mutable state + // context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return context } @@ -228,13 +233,15 @@ private extension ContextManager { fatalError("Can't create object model named \(modelName) at \(modelFileURL)") } - let startupEvent = SentryStartupEvent() + // FIXME: Discarding Sentry in the context of this migration 1/5 + // let startupEvent = SentryStartupEvent() do { try migrateDataModelsIfNecessary(storeURL: storeURL, objectModel: objectModel) } catch { DDLogError("Unable to migrate store: \(error)") - startupEvent.add(error: error as NSError) + // FIXME: Discarding Sentry in the context of this migration 2/5 + // startupEvent.add(error: error as NSError) } let storeDescription = NSPersistentStoreDescription(url: storeURL) @@ -248,16 +255,19 @@ private extension ContextManager { } DDLogError("Error opening the database. \(error)\nDeleting the file and trying again") - startupEvent.add(error: error) + // FIXME: Discarding Sentry in the context of this migration 3/5 + // startupEvent.add(error: error) // make a backup of the old database do { try CoreDataIterativeMigrator.backupDatabase(at: storeURL) } catch { - startupEvent.add(error: error) + // FIXME: Discarding Sentry in the context of this migration 4/5 + // startupEvent.add(error: error) } - startupEvent.send(title: "Can't initialize Core Data stack") + // FIXME: Discarding Sentry in the context of this migration 5/5 + // startupEvent.send(title: "Can't initialize Core Data stack") objc_exception_throw( NSException( name: NSExceptionName(rawValue: "Can't initialize Core Data stack"), @@ -284,7 +294,7 @@ extension ContextManager { return ContextManager.internalSharedInstance } - static var shared: ContextManager { + public static var shared: ContextManager { return sharedInstance() } } diff --git a/WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift b/Modules/Sources/WordPressData/CoreDataIterativeMigrator.swift similarity index 99% rename from WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift rename to Modules/Sources/WordPressData/CoreDataIterativeMigrator.swift index c171ebb79adc..e7990317a3ab 100644 --- a/WordPress/Classes/Utility/CoreData/Migrator/CoreDataIterativeMigrator.swift +++ b/Modules/Sources/WordPressData/CoreDataIterativeMigrator.swift @@ -1,6 +1,6 @@ -import Foundation import CoreData -import WordPressData +import CocoaLumberjackSwift +import Foundation /// CoreDataIterativeMigrator: Migrates through a series of models to allow for users to skip app versions without risk. /// diff --git a/WordPress/Classes/Utility/CoreData/CoreDataHelper.swift b/Modules/Sources/WordPressData/Extensions.swift similarity index 97% rename from WordPress/Classes/Utility/CoreData/CoreDataHelper.swift rename to Modules/Sources/WordPressData/Extensions.swift index 182449749978..b8675d655960 100644 --- a/WordPress/Classes/Utility/CoreData/CoreDataHelper.swift +++ b/Modules/Sources/WordPressData/Extensions.swift @@ -1,6 +1,6 @@ import Foundation import CoreData -import WordPressData +import CocoaLumberjackSwift import WordPressShared // MARK: - NSManagedObject Default entityName Helper @@ -12,7 +12,7 @@ extension NSManagedObject { /// /// Note: entity().name returns nil as per iOS 10, in Unit Testing Targets. Awesome. /// - @objc class func entityName() -> String { + @objc public class func entityName() -> String { return entity().name ?? classNameWithoutNamespaces() } @@ -89,7 +89,8 @@ extension NSManagedObjectContext { /// /// - Parameter predicate: Defines the conditions that any given object should meet. /// - func firstObject(ofType type: T.Type, matching predicate: NSPredicate) -> T? { + // FIXME: Might be able to return internal after Role is migrated + public func firstObject(ofType type: T.Type, matching predicate: NSPredicate) -> T? { let request = T.safeFetchRequest() request.predicate = predicate request.fetchLimit = 1 @@ -141,7 +142,8 @@ extension NSManagedObjectContext { objects = try fetch(request) as? [T] } catch { DDLogError("Error loading Objects [\(String(describing: T.entityName))") - wpAssertionFailure("CoreData.loadObjects failed", userInfo: ["error": "\(error)"]) + // FIXME: Enabling this will require adding dependencies such as Tracks through the chain... + // wpAssertionFailure("CoreData.loadObjects failed", userInfo: ["error": "\(error)"]) } return objects ?? [] @@ -192,7 +194,7 @@ extension ContextManager.ContextManagerError: LocalizedError, CustomDebugStringC } } -extension CoreDataStack { +public extension CoreDataStack { /// Perform a query using the `mainContext` and return the result. /// /// - Warning: Do not return `NSManagedObject` instances from the closure. diff --git a/WordPress/WordPressTest/ContextManagerTests.swift b/Modules/Tests/WordPressDataTests/ContextManagerTests.swift similarity index 100% rename from WordPress/WordPressTest/ContextManagerTests.swift rename to Modules/Tests/WordPressDataTests/ContextManagerTests.swift diff --git a/WordPress/WordPressTest/CoreDataHelperTests.swift b/Modules/Tests/WordPressDataTests/CoreDataHelperTests.swift similarity index 100% rename from WordPress/WordPressTest/CoreDataHelperTests.swift rename to Modules/Tests/WordPressDataTests/CoreDataHelperTests.swift diff --git a/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift b/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift index dc3f03aaf731..b281b5f49a44 100644 --- a/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift +++ b/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressData enum HomepageType: String { case page diff --git a/WordPress/Classes/Models/Blog/Blog+Lookup.swift b/WordPress/Classes/Models/Blog/Blog+Lookup.swift index dbc57cc3ad6f..8395938ebeef 100644 --- a/WordPress/Classes/Models/Blog/Blog+Lookup.swift +++ b/WordPress/Classes/Models/Blog/Blog+Lookup.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressData /// An extension dedicated to looking up and returning blog objects public extension Blog { diff --git a/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift b/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift index 6855b738eb8c..c2243b8f40e7 100644 --- a/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift +++ b/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift @@ -1,6 +1,7 @@ import Foundation import CryptoKit import WordPressAPI +import WordPressData extension Blog { diff --git a/WordPress/Classes/Models/Domain.swift b/WordPress/Classes/Models/Domain.swift index d7376470e9ad..1e28a7658df2 100644 --- a/WordPress/Classes/Models/Domain.swift +++ b/WordPress/Classes/Models/Domain.swift @@ -1,5 +1,6 @@ import Foundation import CoreData +import WordPressData import WordPressKit public typealias Domain = RemoteDomain diff --git a/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift b/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift index 8a06e3d89daf..28e105f1a29e 100644 --- a/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift +++ b/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift @@ -1,3 +1,5 @@ +import WordPressData + /// Base Notification Action Command. class DefaultNotificationActionCommand: FormattableContentActionCommand { var on: Bool diff --git a/WordPress/Classes/Models/Post.swift b/WordPress/Classes/Models/Post.swift index 549ee6ff321c..c6b87c321a61 100644 --- a/WordPress/Classes/Models/Post.swift +++ b/WordPress/Classes/Models/Post.swift @@ -1,5 +1,6 @@ import Foundation import CoreData +import WordPressData @objc(Post) class Post: AbstractPost { diff --git a/WordPress/Classes/Models/Role.swift b/WordPress/Classes/Models/Role.swift index c414a1b5c7f2..f1752f23a7a5 100644 --- a/WordPress/Classes/Models/Role.swift +++ b/WordPress/Classes/Models/Role.swift @@ -1,5 +1,6 @@ import Foundation import CoreData +import WordPressData public class Role: NSManagedObject { @NSManaged public var name: String! diff --git a/WordPress/Classes/Services/NullBlogPropertySanitizer.swift b/WordPress/Classes/Services/NullBlogPropertySanitizer.swift index 6472f379833c..930665cf1ee6 100644 --- a/WordPress/Classes/Services/NullBlogPropertySanitizer.swift +++ b/WordPress/Classes/Services/NullBlogPropertySanitizer.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressData /// Delete invalid rows in the database whose required blog properties are NULL /// diff --git a/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift b/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift index c615cbaf6773..9e7a9dafdde0 100644 --- a/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift +++ b/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift @@ -1,3 +1,5 @@ +import WordPressData + /// Encapsulates logic related to content migration from WordPress to Jetpack. /// @objc class ContentMigrationCoordinator: NSObject { diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 16ed3070d1b2..3010865ada24 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1019,7 +1019,6 @@ 93CD939319099BE70049096E /* authtoken.json in Resources */ = {isa = PBXBuildFile; fileRef = 93CD939219099BE70049096E /* authtoken.json */; }; 93D86B981C691E71003D8E3E /* LocalCoreDataServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 59A9AB391B4C3ECD00A433DC /* LocalCoreDataServiceTests.m */; }; 93E5285619A77BAC003A1A9C /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93E5283B19A7741A003A1A9C /* NotificationCenter.framework */; }; - 93EF094C19ED533500C89770 /* ContextManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E9050319E6F242005513C9 /* ContextManagerTests.swift */; }; 93F2E53E1E9E5A010050D489 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E131CB5116CACA6B004B0314 /* CoreText.framework */; }; 93F2E5401E9E5A180050D489 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 93F2E53F1E9E5A180050D489 /* libsqlite3.tbd */; }; 93F2E5421E9E5A350050D489 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93F2E5411E9E5A350050D489 /* QuickLook.framework */; }; @@ -1059,7 +1058,6 @@ B5AEEC7C1ACACFDA008BF2A4 /* notifications-new-follower.json in Resources */ = {isa = PBXBuildFile; fileRef = B5AEEC771ACACFDA008BF2A4 /* notifications-new-follower.json */; }; B5AEEC7D1ACACFDA008BF2A4 /* notifications-replied-comment.json in Resources */ = {isa = PBXBuildFile; fileRef = B5AEEC781ACACFDA008BF2A4 /* notifications-replied-comment.json */; }; B5DA8A5F20ADAA1D00D5BDE1 /* plugin-directory-jetpack.json in Resources */ = {isa = PBXBuildFile; fileRef = B5DA8A5E20ADAA1C00D5BDE1 /* plugin-directory-jetpack.json */; }; - B5ECA6CD1DBAAD510062D7E0 /* CoreDataHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5ECA6CC1DBAAD510062D7E0 /* CoreDataHelperTests.swift */; }; B5EFB1C91B333C5A007608A3 /* NotificationSettingsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EFB1C81B333C5A007608A3 /* NotificationSettingsServiceTests.swift */; }; B5EFB1D11B33630C007608A3 /* notifications-settings.json in Resources */ = {isa = PBXBuildFile; fileRef = B5EFB1D01B33630C007608A3 /* notifications-settings.json */; }; B5FA22831C99F6180016CA7C /* Tracks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FA22821C99F6180016CA7C /* Tracks.swift */; }; @@ -2787,7 +2785,6 @@ 93D86B941C63EC31003D8E3E /* en-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-CA"; path = "en-CA.lproj/Localizable.strings"; sourceTree = ""; }; 93E5283B19A7741A003A1A9C /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; 93E9050219E6F240005513C9 /* WordPressTest-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WordPressTest-Bridging-Header.h"; sourceTree = ""; }; - 93E9050319E6F242005513C9 /* ContextManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextManagerTests.swift; sourceTree = ""; }; 93F2E53F1E9E5A180050D489 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 93F2E5411E9E5A350050D489 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; 93F2E5431E9E5A570050D489 /* CoreSpotlight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSpotlight.framework; path = System/Library/Frameworks/CoreSpotlight.framework; sourceTree = SDKROOT; }; @@ -2841,7 +2838,6 @@ B5AEEC771ACACFDA008BF2A4 /* notifications-new-follower.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "notifications-new-follower.json"; sourceTree = ""; }; B5AEEC781ACACFDA008BF2A4 /* notifications-replied-comment.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "notifications-replied-comment.json"; sourceTree = ""; }; B5DA8A5E20ADAA1C00D5BDE1 /* plugin-directory-jetpack.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "plugin-directory-jetpack.json"; sourceTree = ""; }; - B5ECA6CC1DBAAD510062D7E0 /* CoreDataHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataHelperTests.swift; sourceTree = ""; }; B5EFB1C81B333C5A007608A3 /* NotificationSettingsServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingsServiceTests.swift; sourceTree = ""; }; B5EFB1D01B33630C007608A3 /* notifications-settings.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "notifications-settings.json"; sourceTree = ""; }; B5FA22821C99F6180016CA7C /* Tracks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Tracks.swift; path = WordPressShareExtension/Tracks.swift; sourceTree = SOURCE_ROOT; wrapsLines = 0; }; @@ -6062,8 +6058,6 @@ B5AEEC731ACACF3B008BF2A4 /* Core Data */ = { isa = PBXGroup; children = ( - 93E9050319E6F242005513C9 /* ContextManagerTests.swift */, - B5ECA6CC1DBAAD510062D7E0 /* CoreDataHelperTests.swift */, 4A2C73E32A943DEA00ACE79E /* TaggedManagedObjectIDTests.swift */, 931D26FF19EDAE8600114F17 /* CoreDataMigrationTests.m */, FE320CC4294705990046899B /* ReaderPostBackupTests.swift */, @@ -9677,7 +9671,6 @@ 2481B1E8260D4EAC00AE59DB /* WPAccount+LookupTests.swift in Sources */, E10F3DA11E5C2CE0008FAADA /* PostListFilterTests.swift in Sources */, DCF892D0282FA42A00BB71E1 /* SiteStatsImmuTableRowsTests.swift in Sources */, - 93EF094C19ED533500C89770 /* ContextManagerTests.swift in Sources */, F17A2A2023BFBD84001E96AC /* UIView+ExistingConstraints.swift in Sources */, B5552D821CD1061F00B26DF6 /* StringExtensionsTests.swift in Sources */, 3F759FBA2A2DA93B0039A845 /* WPAccount+Fixture.swift in Sources */, @@ -9730,7 +9723,6 @@ 73178C3321BEE94700E37C9A /* SiteAssemblyServiceTests.swift in Sources */, D88A6492208D7A0A008AE9BC /* MockStockPhotosService.swift in Sources */, F5D0A65223CCD3B600B20D27 /* PreviewWebKitViewControllerTests.swift in Sources */, - B5ECA6CD1DBAAD510062D7E0 /* CoreDataHelperTests.swift in Sources */, 931D270019EDAE8600114F17 /* CoreDataMigrationTests.m in Sources */, 80EF9284280CFEB60064A971 /* DashboardPostsSyncManagerTests.swift in Sources */, 7EC9FE0B22C627DB00C5A888 /* PostEditorAnalyticsSessionTests.swift in Sources */, From 029f0e083858afd5cab9728bd987e12192df657d Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 16:51:00 +1100 Subject: [PATCH 11/25] Swift 5 in WordPressData tests --- Modules/Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 12b84680e9df..bd80c8144350 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -96,7 +96,8 @@ let package = Package( .testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]), .testTarget( name: "WordPressDataTests", - dependencies: ["WordPressData"] + dependencies: ["WordPressData"], + swiftSettings: [.swiftLanguageMode(.v5)] ), .testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"], swiftSettings: [.swiftLanguageMode(.v5)]), .testTarget(name: "AsyncImageKitTests", dependencies: [ From 78608b46d2153d2d674032b2f7e33d4df6c9983c Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 16:51:28 +1100 Subject: [PATCH 12/25] Replace a couple of `sharedInstance()` calls to `shared` Swift-ier + Saves making `sharedInstance()` public when migrating --- WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift | 2 +- .../Models/Notifications/Actions/NotificationAction.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift b/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift index b281b5f49a44..5c9f6a03b0ae 100644 --- a/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift +++ b/WordPress/Classes/Models/Blog/Blog+HomepageSettings.swift @@ -107,7 +107,7 @@ extension Blog { guard let pageID = homepageType == .page ? homepagePageID : homepageType == .posts ? homepagePostsPageID : nil else { return nil } - let context = ContextManager.sharedInstance().mainContext + let context = ContextManager.shared.mainContext return lookupPost(withID: Int64(pageID), in: context) as? Page } } diff --git a/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift b/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift index 28e105f1a29e..d16840db644a 100644 --- a/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift +++ b/WordPress/Classes/Models/Notifications/Actions/NotificationAction.swift @@ -17,7 +17,7 @@ class DefaultNotificationActionCommand: FormattableContentActionCommand { } private(set) lazy var mainContext: NSManagedObjectContext? = { - return ContextManager.sharedInstance().mainContext + return ContextManager.shared.mainContext }() private(set) lazy var actionsService: NotificationActionsService? = { From f73d016693b28af81bafce1ddbe72359a68d6a90 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 16:14:26 +1100 Subject: [PATCH 13/25] Fix `overrideInstance` testing property concurrency issue Not the best approach, but fine because the property is for testing only. --- Modules/Sources/WordPressData/ContextManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/WordPressData/ContextManager.swift b/Modules/Sources/WordPressData/ContextManager.swift index dbe226122460..5c4bb764115b 100644 --- a/Modules/Sources/WordPressData/ContextManager.swift +++ b/Modules/Sources/WordPressData/ContextManager.swift @@ -283,8 +283,9 @@ private extension ContextManager { extension ContextManager { private static let internalSharedInstance = ContextManager() + /// Tests purpose only - static var overrideInstance: ContextManager? + nonisolated(unsafe) static var overrideInstance: ContextManager? @objc class func sharedInstance() -> ContextManager { if let overrideInstance { From 0753eea285b2ffcb0cc9c8173e99569404fcc8c6 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 18:12:12 +1100 Subject: [PATCH 14/25] Make WordPressDataObj depend on CocoaLumberJack and WordPressShared Currently unused, but we'll use them soon when moving more of the Objective-C models. --- Modules/Package.swift | 8 +- .../Post+CoreDataProperties.swift | 27 ++ .../Sources/WordPressDataObjC}/AbstractPost.h | 0 .../WordPressDataObjC}/Models/AbstractPost.m | 0 .../Sources/WordPressDataObjC/Models}/Blog.m | 0 .../Sources/WordPressDataObjC/Models/Media.m | 223 ++++++++++++ .../WordPressDataObjC/Models/PostCategory.m | 20 ++ .../WordPressDataObjC/Models/WPAccount.m | 195 +++++++++++ .../WordPressDataObjC/include}/BasePost.h | 0 .../Sources/WordPressDataObjC/include/Blog.h | 318 ++++++++++++++++++ .../Sources/WordPressDataObjC/include/Media.h | 119 +++++++ .../WordPressDataObjC/include/PostCategory.h | 29 ++ .../WordPressDataObjC/include}/WPAccount.h | 0 .../Tests/WordPressUIUnitTests}/BasePost.m | 0 .../Classes/Models/PostContentProvider.h | 17 - 15 files changed, 938 insertions(+), 18 deletions(-) create mode 100644 Modules/Sources/WordPressData/Post+CoreDataProperties.swift rename {WordPress/Classes/Models => Modules/Sources/WordPressDataObjC}/AbstractPost.h (100%) rename {WordPress/Classes => Modules/Sources/WordPressDataObjC}/Models/AbstractPost.m (100%) rename {WordPress/Classes/Models/Blog => Modules/Sources/WordPressDataObjC/Models}/Blog.m (100%) create mode 100644 Modules/Sources/WordPressDataObjC/Models/Media.m create mode 100644 Modules/Sources/WordPressDataObjC/Models/PostCategory.m create mode 100644 Modules/Sources/WordPressDataObjC/Models/WPAccount.m rename {WordPress/Classes/Models => Modules/Sources/WordPressDataObjC/include}/BasePost.h (100%) create mode 100644 Modules/Sources/WordPressDataObjC/include/Blog.h create mode 100644 Modules/Sources/WordPressDataObjC/include/Media.h create mode 100644 Modules/Sources/WordPressDataObjC/include/PostCategory.h rename {WordPress/Classes/Models => Modules/Sources/WordPressDataObjC/include}/WPAccount.h (100%) rename {WordPress/Classes/Models => Modules/Tests/WordPressUIUnitTests}/BasePost.m (100%) delete mode 100644 WordPress/Classes/Models/PostContentProvider.h diff --git a/Modules/Package.swift b/Modules/Package.swift index bd80c8144350..5685d279c75a 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -64,7 +64,13 @@ let package = Package( .product(name: "ScreenObject", package: "ScreenObject"), .product(name: "XCUITestHelpers", package: "ScreenObject"), ], swiftSettings: [.swiftLanguageMode(.v5)]), - .target(name: "WordPressDataObjC"), + .target( + name: "WordPressDataObjC", + dependencies: [ + .target(name: "WordPressShared"), + .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), + ] + ), .target( name: "WordPressData", dependencies: [ diff --git a/Modules/Sources/WordPressData/Post+CoreDataProperties.swift b/Modules/Sources/WordPressData/Post+CoreDataProperties.swift new file mode 100644 index 000000000000..99d6ffa0f8de --- /dev/null +++ b/Modules/Sources/WordPressData/Post+CoreDataProperties.swift @@ -0,0 +1,27 @@ +import Foundation +import CoreData + +extension Post { + + @NSManaged var commentCount: NSNumber? + @NSManaged var disabledPublicizeConnections: [NSNumber: [String: String]]? + @NSManaged var likeCount: NSNumber? + @NSManaged var postFormat: String? + @NSManaged var postType: String? + @NSManaged var publicID: String? + @NSManaged var publicizeMessage: String? + @NSManaged var publicizeMessageID: String? + @NSManaged var tags: String? + @NSManaged var categories: Set? + @NSManaged var isStickyPost: Bool + + // If the post is created as an answer to a Blogging Prompt, the promptID is stored here. + @NSManaged var bloggingPromptID: String? + + // These were added manually, since the code generator for Swift is not generating them. + // + @NSManaged func addCategoriesObject(_ value: PostCategory) + @NSManaged func removeCategoriesObject(_ value: PostCategory) + @NSManaged func addCategories(_ values: Set) + @NSManaged func removeCategories(_ values: Set) +} diff --git a/WordPress/Classes/Models/AbstractPost.h b/Modules/Sources/WordPressDataObjC/AbstractPost.h similarity index 100% rename from WordPress/Classes/Models/AbstractPost.h rename to Modules/Sources/WordPressDataObjC/AbstractPost.h diff --git a/WordPress/Classes/Models/AbstractPost.m b/Modules/Sources/WordPressDataObjC/Models/AbstractPost.m similarity index 100% rename from WordPress/Classes/Models/AbstractPost.m rename to Modules/Sources/WordPressDataObjC/Models/AbstractPost.m diff --git a/WordPress/Classes/Models/Blog/Blog.m b/Modules/Sources/WordPressDataObjC/Models/Blog.m similarity index 100% rename from WordPress/Classes/Models/Blog/Blog.m rename to Modules/Sources/WordPressDataObjC/Models/Blog.m diff --git a/Modules/Sources/WordPressDataObjC/Models/Media.m b/Modules/Sources/WordPressDataObjC/Models/Media.m new file mode 100644 index 000000000000..0a300b4fdcc6 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/Models/Media.m @@ -0,0 +1,223 @@ +#import "Media.h" +@import WordPressDataObjC; +#import "WordPress-Swift.h" + +@implementation Media + +@dynamic alt; +@dynamic mediaID; +@dynamic remoteURL; +@dynamic remoteLargeURL; +@dynamic remoteMediumURL; +@dynamic localURL; +@dynamic shortcode; +@dynamic width; +@dynamic length; +@dynamic title; +@dynamic height; +@dynamic filename; +@dynamic filesize; +@dynamic creationDate; +@dynamic blog; +@dynamic posts; +@dynamic remoteStatusNumber; +@dynamic caption; +@dynamic desc; +@dynamic mediaTypeString; +@dynamic videopressGUID; +@dynamic localThumbnailIdentifier; +@dynamic localThumbnailURL; +@dynamic remoteThumbnailURL; +@dynamic postID; +@dynamic error; +@dynamic featuredOnPosts; +@dynamic autoUploadFailureCount; + +#pragma mark - + ++ (NSString *)stringFromMediaType:(MediaType)mediaType +{ + switch (mediaType) { + case MediaTypeImage: + return @"image"; + break; + case MediaTypeVideo: + return @"video"; + break; + case MediaTypePowerpoint: + return @"powerpoint"; + break; + case MediaTypeDocument: + return @"document"; + break; + case MediaTypeAudio: + return @"audio"; + break; + } +} + +#pragma mark - + +- (NSString *)fileExtension +{ + NSString *extension = [self.filename pathExtension]; + if (extension.length) { + return extension; + } + extension = [self.localURL pathExtension]; + if (extension.length) { + return extension; + } + extension = [self.remoteURL pathExtension]; + return extension; +} + +#pragma mark - Media Types + +- (MediaType)mediaType +{ + if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeImage]]) { + return MediaTypeImage; + } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeVideo]]) { + return MediaTypeVideo; + } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypePowerpoint]]) { + return MediaTypePowerpoint; + } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeDocument]]) { + return MediaTypeDocument; + } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeAudio]]) { + return MediaTypeAudio; + } + + return MediaTypeDocument; +} + +- (void)setMediaType:(MediaType)mediaType +{ + self.mediaTypeString = [[self class] stringFromMediaType:mediaType]; +} + +#pragma mark - Remote Status + +- (MediaRemoteStatus)remoteStatus +{ + return (MediaRemoteStatus)[[self remoteStatusNumber] intValue]; +} + +- (void)setRemoteStatus:(MediaRemoteStatus)aStatus +{ + [self setRemoteStatusNumber:@(aStatus)]; +} + +- (NSString *)remoteStatusText +{ + switch (self.remoteStatus) { + case MediaRemoteStatusPushing: + return NSLocalizedString(@"Uploading", @"Status for Media object that is being uploaded."); + case MediaRemoteStatusFailed: + return NSLocalizedString(@"Failed", @"Status for Media object that is failed upload or export."); + case MediaRemoteStatusSync: + return NSLocalizedString(@"Uploaded", @"Status for Media object that is uploaded and sync with server."); + case MediaRemoteStatusProcessing: + return NSLocalizedString(@"Pending", @"Status for Media object that is being processed locally."); + case MediaRemoteStatusLocal: + return NSLocalizedString(@"Local", @"Status for Media object that is only exists locally."); + case MediaRemoteStatusStub: + return NSLocalizedString(@"Stub", @"Status for Media object that is only has the mediaID locally."); + } +} + +#pragma mark - Absolute URLs + +- (NSURL *)absoluteThumbnailLocalURL; +{ + if (!self.localThumbnailURL.length) { + return nil; + } + return [self absoluteURLForLocalPath:self.localThumbnailURL cacheDirectory:YES]; +} + +- (void)setAbsoluteThumbnailLocalURL:(NSURL *)absoluteLocalURL +{ + self.localThumbnailURL = absoluteLocalURL.lastPathComponent; +} + +- (NSURL *)absoluteLocalURL +{ + if (!self.localURL.length) { + return nil; + } + return [self absoluteURLForLocalPath:self.localURL cacheDirectory:NO]; +} + +- (void)setAbsoluteLocalURL:(NSURL *)absoluteLocalURL +{ + self.localURL = absoluteLocalURL.lastPathComponent; +} + +- (NSURL *)absoluteURLForLocalPath:(NSString *)localPath cacheDirectory:(BOOL)cacheDirectory +{ + NSError *error; + NSURL *mediaDirectory = nil; + if (cacheDirectory) { + mediaDirectory = [[MediaFileManager cacheManager] directoryURLAndReturnError:&error]; + } else { + mediaDirectory = [MediaFileManager uploadsDirectoryURLAndReturnError:&error]; + } + if (error) { + DDLogInfo(@"Error resolving Media directory: %@", error); + return nil; + } + return [mediaDirectory URLByAppendingPathComponent:localPath.lastPathComponent]; +} + +#pragma mark - CoreData Helpers + +- (void)prepareForDeletion +{ + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *absolutePath = self.absoluteLocalURL.path; + if ([fileManager fileExistsAtPath:absolutePath] && + ![fileManager removeItemAtPath:absolutePath error:&error]) { + DDLogInfo(@"Error removing media files:%@", error); + } + NSString *absoluteThumbnailPath = self.absoluteThumbnailLocalURL.path; + if ([fileManager fileExistsAtPath:absoluteThumbnailPath] && + ![fileManager removeItemAtPath:absoluteThumbnailPath error:&error]) { + DDLogInfo(@"Error removing media files:%@", error); + } + [super prepareForDeletion]; +} + +- (void)remove +{ + [self.managedObjectContext performBlockAndWait:^{ + [self.managedObjectContext deleteObject:self]; + [[ContextManager sharedInstance] saveContextAndWait:self.managedObjectContext]; + }]; +} + +- (void)save +{ + [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; +} + +- (BOOL)hasRemote { + return self.mediaID.intValue != 0; +} + +- (void)setError:(NSError *)error +{ + if (error != nil) { + // Cherry pick keys that support secure coding. NSErrors thrown from the OS can + // contain types that don't adopt NSSecureCoding, leading to a Core Data exception and crash. + NSDictionary *userInfo = @{NSLocalizedDescriptionKey: error.localizedDescription}; + error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; + } + + [self willChangeValueForKey:@"error"]; + [self setPrimitiveValue:error forKey:@"error"]; + [self didChangeValueForKey:@"error"]; +} + +@end diff --git a/Modules/Sources/WordPressDataObjC/Models/PostCategory.m b/Modules/Sources/WordPressDataObjC/Models/PostCategory.m new file mode 100644 index 000000000000..79511f7be128 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/Models/PostCategory.m @@ -0,0 +1,20 @@ +#import "PostCategory.h" + +NSString * const PostCategoryEntityName = @"Category"; +NSString * const PostCategoryNameKey = @"categoryName"; +const NSInteger PostCategoryUncategorized = 1; + +@implementation PostCategory + +@dynamic categoryID; +@dynamic categoryName; +@dynamic parentID; +@dynamic posts; +@dynamic blog; + ++ (NSString *)entityName +{ + return @"Category"; +} + +@end diff --git a/Modules/Sources/WordPressDataObjC/Models/WPAccount.m b/Modules/Sources/WordPressDataObjC/Models/WPAccount.m new file mode 100644 index 000000000000..a40ea9fbf49e --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/Models/WPAccount.m @@ -0,0 +1,195 @@ +#import "WPAccount.h" +#import "WordPress-Swift.h" + +@interface WPAccount () + +@property (nonatomic, strong, readwrite) WordPressComRestApi *wordPressComRestApi; +@property (nonatomic, strong, readwrite) NSString *cachedToken; + +@end + +@implementation WPAccount + +@dynamic username; +@dynamic blogs; +@dynamic defaultBlog; +@dynamic primaryBlogID; +@dynamic uuid; +@dynamic dateCreated; +@dynamic email; +@dynamic emailVerified; +@dynamic displayName; +@dynamic userID; +@dynamic avatarURL; +@dynamic settings; +@synthesize wordPressComRestApi = _wordPressComRestApi; +@synthesize cachedToken; + +#pragma mark - NSManagedObject subclass methods + +- (void)prepareForDeletion +{ + // Only do these deletions in the primary context (no parent) + if (self.managedObjectContext.concurrencyType != NSMainQueueConcurrencyType) { + return; + } + + [_wordPressComRestApi invalidateAndCancelTasks]; + _wordPressComRestApi = nil; + self.authToken = nil; +} + +- (void)didTurnIntoFault +{ + [super didTurnIntoFault]; + _wordPressComRestApi = nil; + self.cachedToken = nil; +} + ++ (NSString *)entityName +{ + return @"Account"; +} + +#pragma mark - Custom accessors + +- (void)setUsername:(NSString *)username +{ + NSString *previousUsername = self.username; + + BOOL usernameChanged = ![previousUsername isEqualToString:username]; + NSString *authToken = nil; + + if (usernameChanged) { + authToken = self.authToken; + self.authToken = nil; + } + + [self willChangeValueForKey:@"username"]; + [self setPrimitiveValue:username forKey:@"username"]; + [self didChangeValueForKey:@"username"]; + + if (usernameChanged) { + self.authToken = authToken; + } +} + +- (NSString *)authToken +{ + if (self.cachedToken != nil) { + return self.cachedToken; + } + + NSString *token = [WPAccount tokenForUsername:self.username]; + self.cachedToken = token; + return token; +} + +- (void)setAuthToken:(NSString *)authToken +{ + self.cachedToken = nil; + + if (authToken) { + NSError *error = nil; + [SFHFKeychainUtils storeUsername:self.username + andPassword:authToken + forServiceName:[WPAccount authKeychainServiceName] + accessGroup:nil + updateExisting:YES + error:&error]; + + if (error) { + DDLogError(@"Error while updating WordPressComOAuthKeychainServiceName token: %@", error); + } + + } else { + NSError *error = nil; + [SFHFKeychainUtils deleteItemForUsername:self.username + andServiceName:[WPAccount authKeychainServiceName] + accessGroup:nil + error:&error]; + if (error) { + DDLogError(@"Error while deleting WordPressComOAuthKeychainServiceName token: %@", error); + } + } + + // Make sure to release any RestAPI alloc'ed, since it might have an invalid token + _wordPressComRestApi = nil; +} + +- (BOOL)hasAtomicSite { + for (Blog *blog in self.blogs) { + if ([blog isAtomic]) { + return YES; + } + } + return NO; +} + +#pragma mark - Static methods + ++ (NSString *)tokenForUsername:(NSString *)username +{ + NSError *error = nil; + [WPAccount migrateAuthKeyForUsername:username]; + NSString *authToken = [SFHFKeychainUtils getPasswordForUsername:username + andServiceName:[WPAccount authKeychainServiceName] + accessGroup:nil + error:&error]; + if (error) { + DDLogError(@"Error while retrieving WordPressComOAuthKeychainServiceName token: %@", error); + } + + return authToken; +} + ++ (void)migrateAuthKeyForUsername:(NSString *)username +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if ([AppConfiguration isJetpack]) { + SharedDataIssueSolver *sharedDataIssueSolver = [SharedDataIssueSolver instance]; + [sharedDataIssueSolver migrateAuthKeyFor:username]; + } + }); +} + ++ (NSString *)authKeychainServiceName +{ + return [AppConstants authKeychainServiceName]; +} + +#pragma mark - API Helpers + +- (WordPressComRestApi *)wordPressComRestApi +{ + if (!_wordPressComRestApi) { + if (self.authToken.length > 0) { + __weak __typeof(self) weakSelf = self; + _wordPressComRestApi = [WordPressComRestApi defaultApiWithOAuthToken:self.authToken + userAgent:[WPUserAgent wordPressUserAgent] + localeKey:[WordPressComRestApi LocaleKeyDefault]]; + [_wordPressComRestApi setInvalidTokenHandler:^{ + [weakSelf setAuthToken:nil]; + [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; + if (weakSelf.isDefaultWordPressComAccount) { + // At the time of writing, there is an implicit assumption on what the object parameter value means. + // For example, the WordPressAppDelegate.handleDefaultAccountChangedNotification(_:) subscriber inspects the object parameter to decide whether the notification was sent as a result of a login. + // If the object is non-nil, then the method considers the source a login. + // + // The code path in which we are is that of an invalid token, and that's neither a login nor a logout, it's more appropriate to consider it a logout. + // That's because if the token is invalid the app will soon received errors from the API and it's therefore better to force the user to login again. + [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:nil]; + } + }]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; + }); + } + } + return _wordPressComRestApi; + +} + +@end diff --git a/WordPress/Classes/Models/BasePost.h b/Modules/Sources/WordPressDataObjC/include/BasePost.h similarity index 100% rename from WordPress/Classes/Models/BasePost.h rename to Modules/Sources/WordPressDataObjC/include/BasePost.h diff --git a/Modules/Sources/WordPressDataObjC/include/Blog.h b/Modules/Sources/WordPressDataObjC/include/Blog.h new file mode 100644 index 000000000000..a82a4225b10c --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/Blog.h @@ -0,0 +1,318 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AbstractPost; +@class BlogSettings; +@class WPAccount; +@class WordPressComRestApi; +@class WordPressOrgRestApi; +@class WordPressOrgXMLRPCApi; +@class Role; +@class UserSuggestion; +@class SiteSuggestion; +@class PageTemplateCategory; +@class JetpackFeaturesRemovalCoordinator; +@class PublicizeInfo; + +extern NSString * const BlogEntityName; +extern NSString * const PostFormatStandard; + +typedef NS_ENUM(NSUInteger, BlogFeature) { + /// Can the blog be removed? + BlogFeatureRemovable, + /// Can the blog be hidden? + BlogFeatureVisibility, + /// Can the blog use the WordPress.com REST API? + BlogFeatureWPComRESTAPI, + /// Can we use an OAuth2 token with wp-login.php? + BlogFeatureOAuth2Login, + /// Does the blog support reblogs? + BlogFeatureReblog, + /// Does the blog support comment likes? + BlogFeatureCommentLikes, + /// Can we show stats for the blog? + BlogFeatureStats, + /// Can we show activity for the blog? + BlogFeatureActivity, + /// Does the blog support mentions? + BlogFeatureMentions, + /// Does the blog support xposts? + BlogFeatureXposts, + /// Does the blog support push notifications? + BlogFeaturePushNotifications, + /// Does the blog support theme browsing? + BlogFeatureThemeBrowsing, + /// Does the blog support custom themes? + BlogFeatureCustomThemes, + /// Does the blog support premium themes? + BlogFeaturePremiumThemes, + /// Does the blog support Menus management? + BlogFeatureMenus, + /// Does the blog support private visibility? + BlogFeaturePrivate, + /// Does the blog support sharing? + BlogFeatureSharing, + /// Does the blog support people management? + BlogFeaturePeople, + /// Can the blog's site be changed or deleted? + BlogFeatureSiteManagement, + /// Does the blog support different paid plans? + BlogFeaturePlans, + /// Does the blog support plugins? + BlogFeaturePluginManagement, + /// Does the blog support Jetpack image settings? + BlogFeatureJetpackImageSettings, + /// Does the blog support Jetpack settings + BlogFeatureJetpackSettings, + /// Does the blog support custom domains? + BlogFeatureDomains, + /// Does the blog support frame-nonce to authenticate previews? + BlogFeatureNoncePreviews, + /// Does the blog support editing media metadata? + BlogFeatureMediaMetadataEditing, + /// Does the blog support editing media alternative text? + BlogFeatureMediaAltEditing, + /// Does the blog support deleting media? + BlogFeatureMediaDeletion, + /// Does the blog support Stock Photos feature (free photos library) + BlogFeatureStockPhotos, + /// Does the blog support Tenor feature (free GIF library) + BlogFeatureTenor, + /// Does the blog support setting the homepage type and pages? + BlogFeatureHomepageSettings, + /// Does the blog support Jetpack contact info block? + BlogFeatureContactInfo, + BlogFeatureBlockEditorSettings, + /// Does the blog support the Layout grid block? + BlogFeatureLayoutGrid, + /// Does the blog support the tiled gallery block? + BlogFeatureTiledGallery, + /// Does the blog support the VideoPress block? + BlogFeatureVideoPress, + /// Does the blog support v5 of the VideoPress block? + BlogFeatureVideoPressV5, + /// Does the blog support Facebook embed block? + BlogFeatureFacebookEmbed, + /// Does the blog support Instagram embed block? + BlogFeatureInstagramEmbed, + /// Does the blog support Loom embed block? + BlogFeatureLoomEmbed, + /// Does the blog support Smartframe embed block? + BlogFeatureSmartframeEmbed, + /// Does the blog support File Downloads section in stats? + BlogFeatureFileDownloadsStats, + /// Does the blog support Blaze? + BlogFeatureBlaze, + /// Does the blog support listing and editing Pages? + BlogFeaturePages, + /// Does the blog support Site Monitoring? + BlogFeatureSiteMonitoring, +}; + +typedef NS_ENUM(NSInteger, SiteVisibility) { + SiteVisibilityPrivate = -1, + SiteVisibilityHidden = 0, + SiteVisibilityPublic = 1, + SiteVisibilityUnknown = NSIntegerMax +}; + +@interface Blog : NSManagedObject + +@property (nonatomic, strong, readwrite, nullable) NSNumber *blogID __deprecated_msg("Use dotComID instead"); +/// WordPress.com site ID stored as signed 32-bit integer. +@property (nonatomic, strong, readwrite, nullable) NSNumber *dotComID; +@property (nonatomic, strong, readwrite, nullable) NSString *xmlrpc; +@property (nonatomic, strong, readwrite, nullable) NSString *apiKey; +@property (nonatomic, strong, readwrite, nonnull) NSNumber *organizationID; +@property (nonatomic, strong, readwrite, nullable) NSNumber *hasOlderPosts; +@property (nonatomic, strong, readwrite, nullable) NSNumber *hasOlderPages; +@property (nonatomic, strong, readwrite, nullable) NSSet *posts; +@property (nonatomic, strong, readwrite, nullable) NSSet *categories; +@property (nonatomic, strong, readwrite, nullable) NSSet *tags; +@property (nonatomic, strong, readwrite, nullable) NSSet *comments; +@property (nonatomic, strong, readwrite, nullable) NSSet *connections; +@property (nonatomic, strong, readwrite, nullable) NSSet *inviteLinks; +@property (nonatomic, strong, readwrite, nullable) NSSet *domains; +@property (nonatomic, strong, readwrite, nullable) NSSet *themes; +@property (nonatomic, strong, readwrite, nullable) NSSet *media; +@property (nonatomic, strong, readwrite, nullable) NSSet *userSuggestions; +@property (nonatomic, strong, readwrite, nullable) NSSet *siteSuggestions; +@property (nonatomic, strong, readwrite, nullable) NSOrderedSet *menus; +@property (nonatomic, strong, readwrite, nullable) NSOrderedSet *menuLocations; +@property (nonatomic, strong, readwrite, nullable) NSSet *roles; +@property (nonatomic, strong, readwrite, nullable) NSString *currentThemeId; +@property (nonatomic, strong, readwrite, nullable) NSDate *lastPostsSync; +@property (nonatomic, strong, readwrite, nullable) NSDate *lastPagesSync; +@property (nonatomic, strong, readwrite, nullable) NSDate *lastCommentsSync; +@property (nonatomic, strong, readwrite, nullable) NSString *lastUpdateWarning; +@property (nonatomic, weak, readwrite, nullable) NSNumber *isActivated; +@property (nonatomic, strong, readwrite, nullable) NSDictionary *options; +@property (nonatomic, strong, readwrite, nullable) NSSet *postTypes; +@property (nonatomic, strong, readwrite, nullable) NSDictionary *postFormats; +@property (nonatomic, strong, readwrite, nullable) WPAccount *account; +@property (nonatomic, strong, readwrite, nullable) WPAccount *accountForDefaultBlog; +@property (nonatomic, assign, readwrite) BOOL videoPressEnabled; +@property (nonatomic, assign, readwrite) BOOL isMultiAuthor; +@property (nonatomic, assign, readwrite) BOOL isHostedAtWPcom; +@property (nonatomic, assign, readwrite) BOOL hasDomainCredit; +@property (nonatomic, strong, readwrite, nullable) NSString *icon; +@property (nonatomic, assign, readwrite) SiteVisibility siteVisibility; +@property (nonatomic, strong, readwrite, nullable) NSNumber *planID; +@property (nonatomic, strong, readwrite, nullable) NSString *planTitle; +@property (nonatomic, strong, readwrite, nullable) NSArray *planActiveFeatures; +@property (nonatomic, assign, readwrite) BOOL hasPaidPlan; +@property (nonatomic, strong, readwrite, nullable) NSSet *sharingButtons; +@property (nonatomic, strong, readwrite, nullable) NSDictionary *capabilities; +/// The blog's user ID for the current user +@property (nonatomic, strong, readwrite, nullable) NSNumber *userID; +/// Disk quota for site, this is only available for WP.com sites +@property (nonatomic, strong, readwrite, nullable) NSNumber *quotaSpaceAllowed; +@property (nonatomic, strong, readwrite, nullable) NSNumber *quotaSpaceUsed; +@property (nullable, nonatomic, retain) NSSet *pageTemplateCategories; + +/** + * @details Maps to a BlogSettings instance, which contains a collection of the available preferences, + * and their values. + */ +@property (nonatomic, strong, readwrite, nullable) BlogSettings *settings; + +/** + * @details Maps to a PublicizeInfo instance, which contains Jetpack Social auto-sharing information. + */ +@property (nonatomic, strong, readwrite, nullable) PublicizeInfo *publicizeInfo; + +/** + * @details Flags whether the current user is an admin on the blog. + */ +@property (nonatomic, assign, readwrite) BOOL isAdmin; + +/** + * @details Stores the username for self hosted sites + * + * @warn For WordPress.com or Jetpack Managed sites this will be nil. Use usernameForSite instead + */ +@property (nonatomic, strong, readwrite, nullable) NSString *username; +@property (nonatomic, strong, readwrite, nullable) NSString *password; + + +// Readonly Properties +@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormatNames; +@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormats; +@property (nonatomic, weak, readonly, nullable) NSArray *sortedConnections; +@property (nonatomic, readonly, nullable) NSArray *sortedRoles; +@property (nonatomic, strong, readonly, nullable) WordPressOrgXMLRPCApi *xmlrpcApi; +@property (nonatomic, strong, readonly, nullable) WordPressOrgRestApi *selfHostedSiteRestApi; +@property (nonatomic, weak, readonly, nullable) NSString *version; +@property (nonatomic, strong, readonly, nullable) NSString *authToken; +@property (nonatomic, strong, readonly, nullable) NSSet *allowedFileTypes; +@property (nonatomic, copy, readonly, nullable) NSString *usernameForSite; +@property (nonatomic, assign, readonly) BOOL canBlaze; + +/** + * @details URL properties (example: http://wp.koke.me/sub/xmlrpc.php) + */ + +// User to display the blog url to the user (IDN decoded, no http:) +// wp.koke.me/sub +@property (weak, readonly, nullable) NSString *displayURL; +// alias of displayURL +// kept for compatibilty, used as a key to store passwords +@property (weak, readonly, nullable) NSString *hostURL; +@property (weak, readonly, nullable) NSString *homeURL; +// http://wp.koke.me/sub +@property (nonatomic, strong, nullable) NSString *url; +// Used for reachability checks (IDN encoded) +// wp.koke.me +@property (weak, readonly, nullable) NSString *hostname; + +@property (weak, readonly, nullable) NSString *defaultPostFormatText; +// Used to check if the blog has an icon set up +@property (readonly) BOOL hasIcon; + +/** Determine timezone for blog from blog options. If no timezone information is stored on the device, then assume GMT+0 is the default. */ +@property (readonly, nullable) NSTimeZone *timeZone; + +#pragma mark - Blog information + +- (BOOL)isAtomic; +- (BOOL)isWPForTeams; +- (BOOL)isAutomatedTransfer; +- (BOOL)isPrivate; +- (BOOL)isPrivateAtWPCom; +- (nullable NSArray *)sortedCategories; +- (nullable id)getOptionValue:(NSString *) name; +- (void)setValue:(id)value forOption:(NSString *)name; +- (NSString *)loginUrl; +- (nullable NSString *)urlWithPath:(NSString *)path; +- (NSString *)adminUrlWithPath:(NSString *)path; +- (NSDictionary *) getImageResizeDimensions; +- (BOOL)supportsFeaturedImages; +- (BOOL)supports:(BlogFeature)feature; +- (BOOL)supportsPublicize; +- (BOOL)supportsShareButtons; +- (BOOL)isStatsActive; +- (BOOL)hasMappedDomain; + +/** + * Returnst the text description for a post format code + * + * @param postFormatCode of the post format you want to display + * + * @return a string with the post format description and if no description was found the postFormatCode sent. + */ +- (nullable NSString *)postFormatTextFromSlug:(nullable NSString *)postFormatSlug; +/** + Returns a human readable description for logging + + Instead of inspecting the core data object, this returns select information, more + useful for support. + */ +- (NSString *)logDescription; + +/** + Returns formatted Blog information to send to Support when user creates a new ticket. + */ +- (NSString *)supportDescription; + +/** + Returns formatted Blog State information to send to Support when user creates a new ticket. + */ +- (NSString *)stateDescription; + +/** + Returns a REST API client if available + + If the blog is a WordPress.com one or it has Jetpack it will return a REST API + client. Otherwise, the XML-RPC API should be used. + + @warning this method doesn't know if a Jetpack blog has the JSON API disabled + + @return a WordPressComRestApi object if available + */ +- (nullable WordPressComRestApi *)wordPressComRestApi; + +/** + Call this method to know if the blog is hosted at WPcom or accessed through Jetpack. + + @return YES if the blog is hosted at WPcom or if it's connected through Jetpack. + NO otherwise. + */ +- (BOOL)isAccessibleThroughWPCom; + +/** + Check if there is already a basic auth credential stored for this blog/site. + + @return YES if there is a credential + */ +- (BOOL)isBasicAuthCredentialStored; + +/// Checks the blogs installed WordPress version is more than or equal to the requiredVersion +/// @param requiredVersion The minimum version to check for +- (BOOL)hasRequiredWordPressVersion:(NSString *)requiredVersion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Modules/Sources/WordPressDataObjC/include/Media.h b/Modules/Sources/WordPressDataObjC/include/Media.h new file mode 100644 index 000000000000..79f71ba680da --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/Media.h @@ -0,0 +1,119 @@ +#import +#import "Blog.h" +#import "AbstractPost.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, MediaRemoteStatus) { + MediaRemoteStatusSync, /* Post synced. */ + MediaRemoteStatusFailed, /* Upload failed. */ + MediaRemoteStatusLocal, /* Only local version. */ + MediaRemoteStatusPushing, /* Uploading post. */ + MediaRemoteStatusProcessing, /* Intermediate status before uploading. */ + MediaRemoteStatusStub, /* We only have the mediaID information from the server */ +}; + +typedef NS_ENUM(NSUInteger, MediaType) { + MediaTypeImage, + MediaTypeVideo, + MediaTypeDocument, + MediaTypePowerpoint, + MediaTypeAudio +}; + +@interface Media : NSManagedObject + +// Managed properties + +@property (nonatomic, strong, nullable) NSString *alt; +@property (nonatomic, strong, nullable) NSString *caption; +@property (nonatomic, strong, nullable) NSDate *creationDate; +@property (nonatomic, strong, nullable) NSString *desc; +@property (nonatomic, strong, nullable) NSString *filename; +@property (nonatomic, strong, nullable) NSNumber *filesize; +@property (nonatomic, strong, nullable) NSNumber *height; +@property (nonatomic, strong, nullable) NSNumber *length; +@property (nonatomic, strong, nullable) NSString *localThumbnailIdentifier; +@property (nonatomic, strong, nullable) NSString *localThumbnailURL; +@property (nonatomic, strong, nullable) NSString *localURL; +@property (nonatomic, strong, nullable) NSNumber *mediaID; +@property (nonatomic, strong, nullable) NSString *mediaTypeString; +@property (nonatomic, strong, nullable) NSNumber *postID; +@property (nonatomic, strong, nullable) NSNumber *remoteStatusNumber; +@property (nonatomic, strong, nullable) NSString *remoteThumbnailURL; +@property (nonatomic, strong, nullable) NSString *remoteURL; +@property (nonatomic, strong, nullable) NSString *remoteLargeURL; +@property (nonatomic, strong, nullable) NSString *remoteMediumURL; +@property (nonatomic, strong, nullable) NSString *shortcode; +@property (nonatomic, strong, nullable) NSString *title; +@property (nonatomic, strong, nullable) NSString *videopressGUID; +@property (nonatomic, strong, nullable) NSNumber *width; +@property (nonatomic, strong, nullable) NSError *error; +@property (nonatomic, strong, nonnull) NSNumber *autoUploadFailureCount; + +// Relationships + +@property (nonatomic, strong) Blog *blog; +@property (nonatomic, strong, nullable) NSSet *posts; +@property (nonatomic, strong, nullable) NSSet *featuredOnPosts; + +// Helper properties + +@property (nonatomic, assign) MediaType mediaType; +@property (nonatomic, assign) MediaRemoteStatus remoteStatus; + +/** + Local file URL for the Media's asset. e.g. an image, video, gif or other file. + */ +@property (nonatomic, strong, nullable) NSURL *absoluteLocalURL; + +/** + Local file URL for a preprocessed **large** thumbnail that can be used for + a full-screen presentation. + + - warning: Deprecated! Use ``MediaImageService`` to access the thumbnails. + */ +@property (nonatomic, strong, nullable) NSURL *absoluteThumbnailLocalURL; + +/// Returns true if the media object already exists on the server +@property (nonatomic, readonly) BOOL hasRemote; + +// Helper methods + ++ (NSString *)stringFromMediaType:(MediaType)mediaType; + +- (nullable NSString *)fileExtension; + +// CoreData helpers + +- (void)remove; +- (void)save; + +@end + +@class AbstractPost; + +@interface Media (CoreDataGeneratedAccessors) + +- (void)addPostsObject:(AbstractPost *)value; +- (void)removePostsObject:(AbstractPost *)value; +- (void)addPosts:(NSSet *)values; +- (void)removePosts:(NSSet *)values; + +- (void)addFeaturedOnPostsObject:(AbstractPost *)value; +- (void)removeFeaturedOnPostsObject:(AbstractPost *)value; +- (void)addFeaturedOnPosts:(NSSet *)values; +- (void)removeFeaturedOnPosts:(NSSet *)values; + +@end + +// TODO: Remove it; it was added for compatibility during the WPMediaPicker removal +typedef NS_OPTIONS(NSInteger, WPMediaType){ + WPMediaTypeImage = 1, + WPMediaTypeVideo = 1 << 1, + WPMediaTypeAudio = 1 << 2, + WPMediaTypeOther = 1 << 3, + WPMediaTypeAll= 0XFF +}; + +NS_ASSUME_NONNULL_END diff --git a/Modules/Sources/WordPressDataObjC/include/PostCategory.h b/Modules/Sources/WordPressDataObjC/include/PostCategory.h new file mode 100644 index 000000000000..00501aef5e9c --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/PostCategory.h @@ -0,0 +1,29 @@ +#import +#import "Blog.h" + +extern NSString * const PostCategoryEntityName; +extern NSString * const PostCategoryNameKey; +extern const NSInteger PostCategoryUncategorized; + +@interface PostCategory : NSManagedObject + +@property (nonatomic, strong) NSNumber *categoryID; +@property (nonatomic, strong) NSString *categoryName; +@property (nonatomic, strong) NSNumber *parentID; +@property (nonatomic, strong) NSSet *posts; +@property (nonatomic, strong) Blog *blog; + ++ (NSString *)entityName; + +@end + +@class Post; + +@interface PostCategory (CoreDataGeneratedAccessors) + +- (void)addPostsObject:(Post *)value; +- (void)removePostsObject:(Post *)value; +- (void)addPosts:(NSSet *)values; +- (void)removePosts:(NSSet *)values; + +@end diff --git a/WordPress/Classes/Models/WPAccount.h b/Modules/Sources/WordPressDataObjC/include/WPAccount.h similarity index 100% rename from WordPress/Classes/Models/WPAccount.h rename to Modules/Sources/WordPressDataObjC/include/WPAccount.h diff --git a/WordPress/Classes/Models/BasePost.m b/Modules/Tests/WordPressUIUnitTests/BasePost.m similarity index 100% rename from WordPress/Classes/Models/BasePost.m rename to Modules/Tests/WordPressUIUnitTests/BasePost.m diff --git a/WordPress/Classes/Models/PostContentProvider.h b/WordPress/Classes/Models/PostContentProvider.h deleted file mode 100644 index a3d68ca2e442..000000000000 --- a/WordPress/Classes/Models/PostContentProvider.h +++ /dev/null @@ -1,17 +0,0 @@ -#import - -@protocol PostContentProvider -- (NSString *)titleForDisplay; -- (NSString *)authorForDisplay; -- (NSString *)contentForDisplay; -- (NSString *)contentPreviewForDisplay; -- (NSURL *)avatarURLForDisplay; // Some providers use a hardcoded URL or blavatar URL -- (NSString *)gravatarEmailForDisplay; -- (NSDate *)dateForDisplay; -@optional -- (NSString *)blogNameForDisplay; -- (NSURL *)featuredImageURLForDisplay; -- (NSURL *)authorURL; -- (NSString *)slugForDisplay; -- (NSArray *)tagsForDisplay; -@end From 075560a053dff874143a438c0489ddf22fc47637 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 18:14:51 +1100 Subject: [PATCH 15/25] WIP - Move various ObjC model - BUT NOTICE THE COMMENTED OUT CODE!!! --- .../Sources/WordPressDataObjC/AbstractPost.h | 142 -------- .../WordPressDataObjC/Models/AbstractPost.m | 2 +- .../Sources/WordPressDataObjC/Models/Media.m | 11 +- .../WordPressDataObjC/Models/WPAccount.m | 109 +++--- Modules/Tests/WordPressUIUnitTests/BasePost.m | 102 ------ WordPress/Classes/Models/Blog/Blog.h | 318 ------------------ WordPress/Classes/Models/WPAccount.m | 195 ----------- 7 files changed, 62 insertions(+), 817 deletions(-) delete mode 100644 Modules/Sources/WordPressDataObjC/AbstractPost.h delete mode 100644 Modules/Tests/WordPressUIUnitTests/BasePost.m delete mode 100644 WordPress/Classes/Models/Blog/Blog.h delete mode 100644 WordPress/Classes/Models/WPAccount.m diff --git a/Modules/Sources/WordPressDataObjC/AbstractPost.h b/Modules/Sources/WordPressDataObjC/AbstractPost.h deleted file mode 100644 index 41ed30bc1b4d..000000000000 --- a/Modules/Sources/WordPressDataObjC/AbstractPost.h +++ /dev/null @@ -1,142 +0,0 @@ -#import -#import -#import "BasePost.h" - -NS_ASSUME_NONNULL_BEGIN - -@class Media; -@class Comment; - -typedef NS_ENUM(NSUInteger, AbstractPostRemoteStatus) { - AbstractPostRemoteStatusPushing, // Uploading post - AbstractPostRemoteStatusFailed, // Upload failed - AbstractPostRemoteStatusLocal, // Only local version - AbstractPostRemoteStatusSync, // Post uploaded - AbstractPostRemoteStatusPushingMedia, // Push Media - AbstractPostRemoteStatusAutoSaved, // Post remote auto-saved - - // All the previous states were deprecated in 24.9 and are no longer used - // by the app. To get the status of the uploads, use `PostCoordinator`. - - /// The default state of the newly created local revision. - AbstractPostRemoteStatusLocalRevision, - /// The user saved the revision, and it needs to be uploaded to a server. - AbstractPostRemoteStatusSyncNeeded -}; - -@interface AbstractPost : BasePost - -// Relationships -@property (nonatomic, strong) Blog *blog; -@property (nonatomic, strong, nullable) NSDate * dateModified; -@property (nonatomic, strong) NSSet *media; -@property (weak, readonly) AbstractPost *original; -@property (weak, readonly) AbstractPost *revision; -@property (nonatomic, strong) NSSet *comments; -@property (nonatomic, strong, nullable) Media *featuredImage; - -/// This array will contain a list of revision IDs. -@property (nonatomic, strong, nullable) NSArray *revisions; -/// The default value of autoUploadAttemptsCount is 0. -@property (nonatomic, strong, nonnull) NSNumber *autoUploadAttemptsCount; - -/// Autosave attributes hold a snapshot of the post's content. -@property (nonatomic, copy, nullable) NSString *autosaveContent; -@property (nonatomic, copy, nullable) NSString *autosaveExcerpt; -@property (nonatomic, copy, nullable) NSString *autosaveTitle; -@property (nonatomic, copy, nullable) NSDate *autosaveModifiedDate; -@property (nonatomic, copy, nullable) NSNumber *autosaveIdentifier; - -/// Used to deduplicate new posts -@property (nonatomic, strong, nullable) NSUUID *foreignID; - -@property (nonatomic, strong, nullable) NSDate *confirmedChangesTimestamp; - -@property (nonatomic, strong, nullable) NSString *voiceContent; - -// Revision management -- (AbstractPost *)createRevision; -- (void)deleteRevision; -- (void)applyRevision; -- (AbstractPost *)updatePostFrom:(AbstractPost *)revision; -- (BOOL)isRevision; -- (BOOL)isOriginal; - -/// Returns the latest revision of a post. -/// -- (AbstractPost *)latest; -- (AbstractPost *)cloneFrom:(AbstractPost *)source; -- (BOOL)hasPhoto; -- (BOOL)hasVideo; -- (BOOL)hasCategories; -- (BOOL)hasTags; - -@property (nonatomic, assign, readonly) BOOL hasFailedMedia; - -/** - * @brief Call this method to know whether this post has a revision or not. - * - * @returns YES if this post has a revision, NO otherwise. - */ -- (BOOL)hasRevision; - -#pragma mark - Conveniece Methods -- (BOOL)shouldPublishImmediately; -- (NSString *)authorNameForDisplay; -- (NSString *)blavatarForDisplay; -- (NSString *)dateStringForDisplay; -- (BOOL)isMultiAuthorBlog; -- (BOOL)isPrivateAtWPCom; - - -#pragma mark - Unsaved Changes - -/** - Returns YES if the post is has a `future` post status - */ -- (BOOL)isScheduled; - -/** - Returns YES if the post is a draft - */ -- (BOOL)isDraft; - -/** - Returns YES if the post is a published. - */ -- (BOOL)isPublished; - -/** - Returns YES if the original post is a draft - */ -- (BOOL)originalIsDraft; - -// Does the post exist on the blog? -- (BOOL)hasRemote; - -// Save changes to disk -- (void)save; - -/** - * Updates the path for the display image by looking at the post content and trying to find an good image to use. - * If no appropiated image is found the path is set to nil. - */ -- (void)updatePathForDisplayImageBasedOnContent; - -@end - -@interface AbstractPost (CoreDataGeneratedAccessors) - -- (void)addMediaObject:(Media *)value; -- (void)removeMediaObject:(Media *)value; -- (void)addMedia:(NSSet *)values; -- (void)removeMedia:(NSSet *)values; - -- (void)addCommentsObject:(Comment *)value; -- (void)removeCommentsObject:(Comment *)value; -- (void)addComments:(NSSet *)values; -- (void)removeComments:(NSSet *)values; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Modules/Sources/WordPressDataObjC/Models/AbstractPost.m b/Modules/Sources/WordPressDataObjC/Models/AbstractPost.m index 55d68a55515b..fd80a52d270b 100644 --- a/Modules/Sources/WordPressDataObjC/Models/AbstractPost.m +++ b/Modules/Sources/WordPressDataObjC/Models/AbstractPost.m @@ -1,7 +1,7 @@ #import "AbstractPost.h" #import "Media.h" @import WordPressDataObjC; -#import "WordPress-Swift.h" +//#import "WordPress-Swift.h" #import "BasePost.h" @import WordPressKit; diff --git a/Modules/Sources/WordPressDataObjC/Models/Media.m b/Modules/Sources/WordPressDataObjC/Models/Media.m index 0a300b4fdcc6..f8a86b85c27b 100644 --- a/Modules/Sources/WordPressDataObjC/Models/Media.m +++ b/Modules/Sources/WordPressDataObjC/Models/Media.m @@ -1,6 +1,7 @@ +@import CocoaLumberjack; #import "Media.h" @import WordPressDataObjC; -#import "WordPress-Swift.h" +//#import "WordPress-Swift.h" @implementation Media @@ -159,9 +160,9 @@ - (NSURL *)absoluteURLForLocalPath:(NSString *)localPath cacheDirectory:(BOOL)ca NSError *error; NSURL *mediaDirectory = nil; if (cacheDirectory) { - mediaDirectory = [[MediaFileManager cacheManager] directoryURLAndReturnError:&error]; +// mediaDirectory = [[MediaFileManager cacheManager] directoryURLAndReturnError:&error]; } else { - mediaDirectory = [MediaFileManager uploadsDirectoryURLAndReturnError:&error]; +// mediaDirectory = [MediaFileManager uploadsDirectoryURLAndReturnError:&error]; } if (error) { DDLogInfo(@"Error resolving Media directory: %@", error); @@ -193,13 +194,13 @@ - (void)remove { [self.managedObjectContext performBlockAndWait:^{ [self.managedObjectContext deleteObject:self]; - [[ContextManager sharedInstance] saveContextAndWait:self.managedObjectContext]; +// [[ContextManager sharedInstance] saveContextAndWait:self.managedObjectContext]; }]; } - (void)save { - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; +// [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; } - (BOOL)hasRemote { diff --git a/Modules/Sources/WordPressDataObjC/Models/WPAccount.m b/Modules/Sources/WordPressDataObjC/Models/WPAccount.m index a40ea9fbf49e..b7411228fbd2 100644 --- a/Modules/Sources/WordPressDataObjC/Models/WPAccount.m +++ b/Modules/Sources/WordPressDataObjC/Models/WPAccount.m @@ -1,9 +1,10 @@ #import "WPAccount.h" -#import "WordPress-Swift.h" +//#import "WordPress-Swift.h" +@import CocoaLumberjack; @interface WPAccount () -@property (nonatomic, strong, readwrite) WordPressComRestApi *wordPressComRestApi; +//@property (nonatomic, strong, readwrite) WordPressComRestApi *wordPressComRestApi; @property (nonatomic, strong, readwrite) NSString *cachedToken; @end @@ -34,15 +35,15 @@ - (void)prepareForDeletion return; } - [_wordPressComRestApi invalidateAndCancelTasks]; - _wordPressComRestApi = nil; +// [_wordPressComRestApi invalidateAndCancelTasks]; +// _wordPressComRestApi = nil; self.authToken = nil; } - (void)didTurnIntoFault { [super didTurnIntoFault]; - _wordPressComRestApi = nil; +// _wordPressComRestApi = nil; self.cachedToken = nil; } @@ -91,12 +92,12 @@ - (void)setAuthToken:(NSString *)authToken if (authToken) { NSError *error = nil; - [SFHFKeychainUtils storeUsername:self.username - andPassword:authToken - forServiceName:[WPAccount authKeychainServiceName] - accessGroup:nil - updateExisting:YES - error:&error]; +// [SFHFKeychainUtils storeUsername:self.username +// andPassword:authToken +// forServiceName:[WPAccount authKeychainServiceName] +// accessGroup:nil +// updateExisting:YES +// error:&error]; if (error) { DDLogError(@"Error while updating WordPressComOAuthKeychainServiceName token: %@", error); @@ -104,10 +105,10 @@ - (void)setAuthToken:(NSString *)authToken } else { NSError *error = nil; - [SFHFKeychainUtils deleteItemForUsername:self.username - andServiceName:[WPAccount authKeychainServiceName] - accessGroup:nil - error:&error]; +// [SFHFKeychainUtils deleteItemForUsername:self.username +// andServiceName:[WPAccount authKeychainServiceName] +// accessGroup:nil +// error:&error]; if (error) { DDLogError(@"Error while deleting WordPressComOAuthKeychainServiceName token: %@", error); } @@ -146,50 +147,50 @@ + (NSString *)tokenForUsername:(NSString *)username + (void)migrateAuthKeyForUsername:(NSString *)username { static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if ([AppConfiguration isJetpack]) { - SharedDataIssueSolver *sharedDataIssueSolver = [SharedDataIssueSolver instance]; - [sharedDataIssueSolver migrateAuthKeyFor:username]; - } +// dispatch_once(&onceToken, ^{ +// if ([AppConfiguration isJetpack]) { +// SharedDataIssueSolver *sharedDataIssueSolver = [SharedDataIssueSolver instance]; +// [sharedDataIssueSolver migrateAuthKeyFor:username]; +// } }); } -+ (NSString *)authKeychainServiceName -{ - return [AppConstants authKeychainServiceName]; -} +//+ (NSString *)authKeychainServiceName +//{ +// return [AppConstants authKeychainServiceName]; +//} #pragma mark - API Helpers -- (WordPressComRestApi *)wordPressComRestApi -{ - if (!_wordPressComRestApi) { - if (self.authToken.length > 0) { - __weak __typeof(self) weakSelf = self; - _wordPressComRestApi = [WordPressComRestApi defaultApiWithOAuthToken:self.authToken - userAgent:[WPUserAgent wordPressUserAgent] - localeKey:[WordPressComRestApi LocaleKeyDefault]]; - [_wordPressComRestApi setInvalidTokenHandler:^{ - [weakSelf setAuthToken:nil]; - [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; - if (weakSelf.isDefaultWordPressComAccount) { - // At the time of writing, there is an implicit assumption on what the object parameter value means. - // For example, the WordPressAppDelegate.handleDefaultAccountChangedNotification(_:) subscriber inspects the object parameter to decide whether the notification was sent as a result of a login. - // If the object is non-nil, then the method considers the source a login. - // - // The code path in which we are is that of an invalid token, and that's neither a login nor a logout, it's more appropriate to consider it a logout. - // That's because if the token is invalid the app will soon received errors from the API and it's therefore better to force the user to login again. - [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:nil]; - } - }]; - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; - }); - } - } - return _wordPressComRestApi; - -} +//- (WordPressComRestApi *)wordPressComRestApi +//{ +// if (!_wordPressComRestApi) { +// if (self.authToken.length > 0) { +// __weak __typeof(self) weakSelf = self; +// _wordPressComRestApi = [WordPressComRestApi defaultApiWithOAuthToken:self.authToken +// userAgent:[WPUserAgent wordPressUserAgent] +// localeKey:[WordPressComRestApi LocaleKeyDefault]]; +// [_wordPressComRestApi setInvalidTokenHandler:^{ +// [weakSelf setAuthToken:nil]; +// [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; +// if (weakSelf.isDefaultWordPressComAccount) { +// // At the time of writing, there is an implicit assumption on what the object parameter value means. +// // For example, the WordPressAppDelegate.handleDefaultAccountChangedNotification(_:) subscriber inspects the object parameter to decide whether the notification was sent as a result of a login. +// // If the object is non-nil, then the method considers the source a login. +// // +// // The code path in which we are is that of an invalid token, and that's neither a login nor a logout, it's more appropriate to consider it a logout. +// // That's because if the token is invalid the app will soon received errors from the API and it's therefore better to force the user to login again. +// [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:nil]; +// } +// }]; +// } else { +// dispatch_async(dispatch_get_main_queue(), ^{ +// [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; +// }); +// } +// } +// return _wordPressComRestApi; +// +//} @end diff --git a/Modules/Tests/WordPressUIUnitTests/BasePost.m b/Modules/Tests/WordPressUIUnitTests/BasePost.m deleted file mode 100644 index 26bc21d8d08b..000000000000 --- a/Modules/Tests/WordPressUIUnitTests/BasePost.m +++ /dev/null @@ -1,102 +0,0 @@ -#import "BasePost.h" -#import "Media.h" -@import WordPressDataObjC; - -@import WordPressShared; - -@implementation BasePost - -@dynamic authorID; -@dynamic author; -@dynamic authorAvatarURL; -@dynamic content; -@dynamic date_created_gmt; -@dynamic postID; -@dynamic postTitle; -@dynamic password; -@dynamic remoteStatusNumber; -@dynamic permaLink; -@dynamic mt_excerpt; -@dynamic wp_slug; -@dynamic suggested_slug; -@dynamic pathForDisplayImage; - -- (NSDate *)dateCreated -{ - return self.date_created_gmt; -} - -- (void)setDateCreated:(NSDate *)localDate -{ - self.date_created_gmt = localDate; -} - -#pragma mark - PostContentProvider protocol - -- (NSString *)titleForDisplay -{ - NSString *title = [self.postTitle stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - if (title == nil || ([title length] == 0)) { - title = NSLocalizedString(@"(no title)", @""); - } - return [title stringByDecodingXMLCharacters]; -} - -- (NSString *)authorForDisplay -{ - return [self.author stringByDecodingXMLCharacters]; -} - -- (NSString *)blogNameForDisplay -{ - return @""; -} - -- (NSString *)contentForDisplay -{ - return self.content; -} - -- (NSString *)contentPreviewForDisplay -{ - return self.content; -} - -- (NSString *)gravatarEmailForDisplay -{ - return nil; -} - -- (NSURL *)avatarURLForDisplay -{ - return nil; -} - -- (NSDate *)dateForDisplay -{ - return [self dateCreated]; -} - -- (NSString *)slugForDisplay -{ - if (self.wp_slug.length > 0) { - return self.wp_slug; - } - return self.suggested_slug; -} - -- (BOOL)hasContent -{ - BOOL titleIsEmpty = self.postTitle ? self.postTitle.isEmpty : YES; - BOOL contentIsEmpty = [self isContentEmpty]; - - return !titleIsEmpty || !contentIsEmpty; -} - -- (BOOL)isContentEmpty -{ - BOOL isContentAnEmptyGBParagraph = [self.content isEqualToString:@"\n

\n"]; - return self.content ? (self.content.isEmpty || isContentAnEmptyGBParagraph) : YES; -} - -@end diff --git a/WordPress/Classes/Models/Blog/Blog.h b/WordPress/Classes/Models/Blog/Blog.h deleted file mode 100644 index a82a4225b10c..000000000000 --- a/WordPress/Classes/Models/Blog/Blog.h +++ /dev/null @@ -1,318 +0,0 @@ -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@class AbstractPost; -@class BlogSettings; -@class WPAccount; -@class WordPressComRestApi; -@class WordPressOrgRestApi; -@class WordPressOrgXMLRPCApi; -@class Role; -@class UserSuggestion; -@class SiteSuggestion; -@class PageTemplateCategory; -@class JetpackFeaturesRemovalCoordinator; -@class PublicizeInfo; - -extern NSString * const BlogEntityName; -extern NSString * const PostFormatStandard; - -typedef NS_ENUM(NSUInteger, BlogFeature) { - /// Can the blog be removed? - BlogFeatureRemovable, - /// Can the blog be hidden? - BlogFeatureVisibility, - /// Can the blog use the WordPress.com REST API? - BlogFeatureWPComRESTAPI, - /// Can we use an OAuth2 token with wp-login.php? - BlogFeatureOAuth2Login, - /// Does the blog support reblogs? - BlogFeatureReblog, - /// Does the blog support comment likes? - BlogFeatureCommentLikes, - /// Can we show stats for the blog? - BlogFeatureStats, - /// Can we show activity for the blog? - BlogFeatureActivity, - /// Does the blog support mentions? - BlogFeatureMentions, - /// Does the blog support xposts? - BlogFeatureXposts, - /// Does the blog support push notifications? - BlogFeaturePushNotifications, - /// Does the blog support theme browsing? - BlogFeatureThemeBrowsing, - /// Does the blog support custom themes? - BlogFeatureCustomThemes, - /// Does the blog support premium themes? - BlogFeaturePremiumThemes, - /// Does the blog support Menus management? - BlogFeatureMenus, - /// Does the blog support private visibility? - BlogFeaturePrivate, - /// Does the blog support sharing? - BlogFeatureSharing, - /// Does the blog support people management? - BlogFeaturePeople, - /// Can the blog's site be changed or deleted? - BlogFeatureSiteManagement, - /// Does the blog support different paid plans? - BlogFeaturePlans, - /// Does the blog support plugins? - BlogFeaturePluginManagement, - /// Does the blog support Jetpack image settings? - BlogFeatureJetpackImageSettings, - /// Does the blog support Jetpack settings - BlogFeatureJetpackSettings, - /// Does the blog support custom domains? - BlogFeatureDomains, - /// Does the blog support frame-nonce to authenticate previews? - BlogFeatureNoncePreviews, - /// Does the blog support editing media metadata? - BlogFeatureMediaMetadataEditing, - /// Does the blog support editing media alternative text? - BlogFeatureMediaAltEditing, - /// Does the blog support deleting media? - BlogFeatureMediaDeletion, - /// Does the blog support Stock Photos feature (free photos library) - BlogFeatureStockPhotos, - /// Does the blog support Tenor feature (free GIF library) - BlogFeatureTenor, - /// Does the blog support setting the homepage type and pages? - BlogFeatureHomepageSettings, - /// Does the blog support Jetpack contact info block? - BlogFeatureContactInfo, - BlogFeatureBlockEditorSettings, - /// Does the blog support the Layout grid block? - BlogFeatureLayoutGrid, - /// Does the blog support the tiled gallery block? - BlogFeatureTiledGallery, - /// Does the blog support the VideoPress block? - BlogFeatureVideoPress, - /// Does the blog support v5 of the VideoPress block? - BlogFeatureVideoPressV5, - /// Does the blog support Facebook embed block? - BlogFeatureFacebookEmbed, - /// Does the blog support Instagram embed block? - BlogFeatureInstagramEmbed, - /// Does the blog support Loom embed block? - BlogFeatureLoomEmbed, - /// Does the blog support Smartframe embed block? - BlogFeatureSmartframeEmbed, - /// Does the blog support File Downloads section in stats? - BlogFeatureFileDownloadsStats, - /// Does the blog support Blaze? - BlogFeatureBlaze, - /// Does the blog support listing and editing Pages? - BlogFeaturePages, - /// Does the blog support Site Monitoring? - BlogFeatureSiteMonitoring, -}; - -typedef NS_ENUM(NSInteger, SiteVisibility) { - SiteVisibilityPrivate = -1, - SiteVisibilityHidden = 0, - SiteVisibilityPublic = 1, - SiteVisibilityUnknown = NSIntegerMax -}; - -@interface Blog : NSManagedObject - -@property (nonatomic, strong, readwrite, nullable) NSNumber *blogID __deprecated_msg("Use dotComID instead"); -/// WordPress.com site ID stored as signed 32-bit integer. -@property (nonatomic, strong, readwrite, nullable) NSNumber *dotComID; -@property (nonatomic, strong, readwrite, nullable) NSString *xmlrpc; -@property (nonatomic, strong, readwrite, nullable) NSString *apiKey; -@property (nonatomic, strong, readwrite, nonnull) NSNumber *organizationID; -@property (nonatomic, strong, readwrite, nullable) NSNumber *hasOlderPosts; -@property (nonatomic, strong, readwrite, nullable) NSNumber *hasOlderPages; -@property (nonatomic, strong, readwrite, nullable) NSSet *posts; -@property (nonatomic, strong, readwrite, nullable) NSSet *categories; -@property (nonatomic, strong, readwrite, nullable) NSSet *tags; -@property (nonatomic, strong, readwrite, nullable) NSSet *comments; -@property (nonatomic, strong, readwrite, nullable) NSSet *connections; -@property (nonatomic, strong, readwrite, nullable) NSSet *inviteLinks; -@property (nonatomic, strong, readwrite, nullable) NSSet *domains; -@property (nonatomic, strong, readwrite, nullable) NSSet *themes; -@property (nonatomic, strong, readwrite, nullable) NSSet *media; -@property (nonatomic, strong, readwrite, nullable) NSSet *userSuggestions; -@property (nonatomic, strong, readwrite, nullable) NSSet *siteSuggestions; -@property (nonatomic, strong, readwrite, nullable) NSOrderedSet *menus; -@property (nonatomic, strong, readwrite, nullable) NSOrderedSet *menuLocations; -@property (nonatomic, strong, readwrite, nullable) NSSet *roles; -@property (nonatomic, strong, readwrite, nullable) NSString *currentThemeId; -@property (nonatomic, strong, readwrite, nullable) NSDate *lastPostsSync; -@property (nonatomic, strong, readwrite, nullable) NSDate *lastPagesSync; -@property (nonatomic, strong, readwrite, nullable) NSDate *lastCommentsSync; -@property (nonatomic, strong, readwrite, nullable) NSString *lastUpdateWarning; -@property (nonatomic, weak, readwrite, nullable) NSNumber *isActivated; -@property (nonatomic, strong, readwrite, nullable) NSDictionary *options; -@property (nonatomic, strong, readwrite, nullable) NSSet *postTypes; -@property (nonatomic, strong, readwrite, nullable) NSDictionary *postFormats; -@property (nonatomic, strong, readwrite, nullable) WPAccount *account; -@property (nonatomic, strong, readwrite, nullable) WPAccount *accountForDefaultBlog; -@property (nonatomic, assign, readwrite) BOOL videoPressEnabled; -@property (nonatomic, assign, readwrite) BOOL isMultiAuthor; -@property (nonatomic, assign, readwrite) BOOL isHostedAtWPcom; -@property (nonatomic, assign, readwrite) BOOL hasDomainCredit; -@property (nonatomic, strong, readwrite, nullable) NSString *icon; -@property (nonatomic, assign, readwrite) SiteVisibility siteVisibility; -@property (nonatomic, strong, readwrite, nullable) NSNumber *planID; -@property (nonatomic, strong, readwrite, nullable) NSString *planTitle; -@property (nonatomic, strong, readwrite, nullable) NSArray *planActiveFeatures; -@property (nonatomic, assign, readwrite) BOOL hasPaidPlan; -@property (nonatomic, strong, readwrite, nullable) NSSet *sharingButtons; -@property (nonatomic, strong, readwrite, nullable) NSDictionary *capabilities; -/// The blog's user ID for the current user -@property (nonatomic, strong, readwrite, nullable) NSNumber *userID; -/// Disk quota for site, this is only available for WP.com sites -@property (nonatomic, strong, readwrite, nullable) NSNumber *quotaSpaceAllowed; -@property (nonatomic, strong, readwrite, nullable) NSNumber *quotaSpaceUsed; -@property (nullable, nonatomic, retain) NSSet *pageTemplateCategories; - -/** - * @details Maps to a BlogSettings instance, which contains a collection of the available preferences, - * and their values. - */ -@property (nonatomic, strong, readwrite, nullable) BlogSettings *settings; - -/** - * @details Maps to a PublicizeInfo instance, which contains Jetpack Social auto-sharing information. - */ -@property (nonatomic, strong, readwrite, nullable) PublicizeInfo *publicizeInfo; - -/** - * @details Flags whether the current user is an admin on the blog. - */ -@property (nonatomic, assign, readwrite) BOOL isAdmin; - -/** - * @details Stores the username for self hosted sites - * - * @warn For WordPress.com or Jetpack Managed sites this will be nil. Use usernameForSite instead - */ -@property (nonatomic, strong, readwrite, nullable) NSString *username; -@property (nonatomic, strong, readwrite, nullable) NSString *password; - - -// Readonly Properties -@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormatNames; -@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormats; -@property (nonatomic, weak, readonly, nullable) NSArray *sortedConnections; -@property (nonatomic, readonly, nullable) NSArray *sortedRoles; -@property (nonatomic, strong, readonly, nullable) WordPressOrgXMLRPCApi *xmlrpcApi; -@property (nonatomic, strong, readonly, nullable) WordPressOrgRestApi *selfHostedSiteRestApi; -@property (nonatomic, weak, readonly, nullable) NSString *version; -@property (nonatomic, strong, readonly, nullable) NSString *authToken; -@property (nonatomic, strong, readonly, nullable) NSSet *allowedFileTypes; -@property (nonatomic, copy, readonly, nullable) NSString *usernameForSite; -@property (nonatomic, assign, readonly) BOOL canBlaze; - -/** - * @details URL properties (example: http://wp.koke.me/sub/xmlrpc.php) - */ - -// User to display the blog url to the user (IDN decoded, no http:) -// wp.koke.me/sub -@property (weak, readonly, nullable) NSString *displayURL; -// alias of displayURL -// kept for compatibilty, used as a key to store passwords -@property (weak, readonly, nullable) NSString *hostURL; -@property (weak, readonly, nullable) NSString *homeURL; -// http://wp.koke.me/sub -@property (nonatomic, strong, nullable) NSString *url; -// Used for reachability checks (IDN encoded) -// wp.koke.me -@property (weak, readonly, nullable) NSString *hostname; - -@property (weak, readonly, nullable) NSString *defaultPostFormatText; -// Used to check if the blog has an icon set up -@property (readonly) BOOL hasIcon; - -/** Determine timezone for blog from blog options. If no timezone information is stored on the device, then assume GMT+0 is the default. */ -@property (readonly, nullable) NSTimeZone *timeZone; - -#pragma mark - Blog information - -- (BOOL)isAtomic; -- (BOOL)isWPForTeams; -- (BOOL)isAutomatedTransfer; -- (BOOL)isPrivate; -- (BOOL)isPrivateAtWPCom; -- (nullable NSArray *)sortedCategories; -- (nullable id)getOptionValue:(NSString *) name; -- (void)setValue:(id)value forOption:(NSString *)name; -- (NSString *)loginUrl; -- (nullable NSString *)urlWithPath:(NSString *)path; -- (NSString *)adminUrlWithPath:(NSString *)path; -- (NSDictionary *) getImageResizeDimensions; -- (BOOL)supportsFeaturedImages; -- (BOOL)supports:(BlogFeature)feature; -- (BOOL)supportsPublicize; -- (BOOL)supportsShareButtons; -- (BOOL)isStatsActive; -- (BOOL)hasMappedDomain; - -/** - * Returnst the text description for a post format code - * - * @param postFormatCode of the post format you want to display - * - * @return a string with the post format description and if no description was found the postFormatCode sent. - */ -- (nullable NSString *)postFormatTextFromSlug:(nullable NSString *)postFormatSlug; -/** - Returns a human readable description for logging - - Instead of inspecting the core data object, this returns select information, more - useful for support. - */ -- (NSString *)logDescription; - -/** - Returns formatted Blog information to send to Support when user creates a new ticket. - */ -- (NSString *)supportDescription; - -/** - Returns formatted Blog State information to send to Support when user creates a new ticket. - */ -- (NSString *)stateDescription; - -/** - Returns a REST API client if available - - If the blog is a WordPress.com one or it has Jetpack it will return a REST API - client. Otherwise, the XML-RPC API should be used. - - @warning this method doesn't know if a Jetpack blog has the JSON API disabled - - @return a WordPressComRestApi object if available - */ -- (nullable WordPressComRestApi *)wordPressComRestApi; - -/** - Call this method to know if the blog is hosted at WPcom or accessed through Jetpack. - - @return YES if the blog is hosted at WPcom or if it's connected through Jetpack. - NO otherwise. - */ -- (BOOL)isAccessibleThroughWPCom; - -/** - Check if there is already a basic auth credential stored for this blog/site. - - @return YES if there is a credential - */ -- (BOOL)isBasicAuthCredentialStored; - -/// Checks the blogs installed WordPress version is more than or equal to the requiredVersion -/// @param requiredVersion The minimum version to check for -- (BOOL)hasRequiredWordPressVersion:(NSString *)requiredVersion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Models/WPAccount.m b/WordPress/Classes/Models/WPAccount.m deleted file mode 100644 index a40ea9fbf49e..000000000000 --- a/WordPress/Classes/Models/WPAccount.m +++ /dev/null @@ -1,195 +0,0 @@ -#import "WPAccount.h" -#import "WordPress-Swift.h" - -@interface WPAccount () - -@property (nonatomic, strong, readwrite) WordPressComRestApi *wordPressComRestApi; -@property (nonatomic, strong, readwrite) NSString *cachedToken; - -@end - -@implementation WPAccount - -@dynamic username; -@dynamic blogs; -@dynamic defaultBlog; -@dynamic primaryBlogID; -@dynamic uuid; -@dynamic dateCreated; -@dynamic email; -@dynamic emailVerified; -@dynamic displayName; -@dynamic userID; -@dynamic avatarURL; -@dynamic settings; -@synthesize wordPressComRestApi = _wordPressComRestApi; -@synthesize cachedToken; - -#pragma mark - NSManagedObject subclass methods - -- (void)prepareForDeletion -{ - // Only do these deletions in the primary context (no parent) - if (self.managedObjectContext.concurrencyType != NSMainQueueConcurrencyType) { - return; - } - - [_wordPressComRestApi invalidateAndCancelTasks]; - _wordPressComRestApi = nil; - self.authToken = nil; -} - -- (void)didTurnIntoFault -{ - [super didTurnIntoFault]; - _wordPressComRestApi = nil; - self.cachedToken = nil; -} - -+ (NSString *)entityName -{ - return @"Account"; -} - -#pragma mark - Custom accessors - -- (void)setUsername:(NSString *)username -{ - NSString *previousUsername = self.username; - - BOOL usernameChanged = ![previousUsername isEqualToString:username]; - NSString *authToken = nil; - - if (usernameChanged) { - authToken = self.authToken; - self.authToken = nil; - } - - [self willChangeValueForKey:@"username"]; - [self setPrimitiveValue:username forKey:@"username"]; - [self didChangeValueForKey:@"username"]; - - if (usernameChanged) { - self.authToken = authToken; - } -} - -- (NSString *)authToken -{ - if (self.cachedToken != nil) { - return self.cachedToken; - } - - NSString *token = [WPAccount tokenForUsername:self.username]; - self.cachedToken = token; - return token; -} - -- (void)setAuthToken:(NSString *)authToken -{ - self.cachedToken = nil; - - if (authToken) { - NSError *error = nil; - [SFHFKeychainUtils storeUsername:self.username - andPassword:authToken - forServiceName:[WPAccount authKeychainServiceName] - accessGroup:nil - updateExisting:YES - error:&error]; - - if (error) { - DDLogError(@"Error while updating WordPressComOAuthKeychainServiceName token: %@", error); - } - - } else { - NSError *error = nil; - [SFHFKeychainUtils deleteItemForUsername:self.username - andServiceName:[WPAccount authKeychainServiceName] - accessGroup:nil - error:&error]; - if (error) { - DDLogError(@"Error while deleting WordPressComOAuthKeychainServiceName token: %@", error); - } - } - - // Make sure to release any RestAPI alloc'ed, since it might have an invalid token - _wordPressComRestApi = nil; -} - -- (BOOL)hasAtomicSite { - for (Blog *blog in self.blogs) { - if ([blog isAtomic]) { - return YES; - } - } - return NO; -} - -#pragma mark - Static methods - -+ (NSString *)tokenForUsername:(NSString *)username -{ - NSError *error = nil; - [WPAccount migrateAuthKeyForUsername:username]; - NSString *authToken = [SFHFKeychainUtils getPasswordForUsername:username - andServiceName:[WPAccount authKeychainServiceName] - accessGroup:nil - error:&error]; - if (error) { - DDLogError(@"Error while retrieving WordPressComOAuthKeychainServiceName token: %@", error); - } - - return authToken; -} - -+ (void)migrateAuthKeyForUsername:(NSString *)username -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if ([AppConfiguration isJetpack]) { - SharedDataIssueSolver *sharedDataIssueSolver = [SharedDataIssueSolver instance]; - [sharedDataIssueSolver migrateAuthKeyFor:username]; - } - }); -} - -+ (NSString *)authKeychainServiceName -{ - return [AppConstants authKeychainServiceName]; -} - -#pragma mark - API Helpers - -- (WordPressComRestApi *)wordPressComRestApi -{ - if (!_wordPressComRestApi) { - if (self.authToken.length > 0) { - __weak __typeof(self) weakSelf = self; - _wordPressComRestApi = [WordPressComRestApi defaultApiWithOAuthToken:self.authToken - userAgent:[WPUserAgent wordPressUserAgent] - localeKey:[WordPressComRestApi LocaleKeyDefault]]; - [_wordPressComRestApi setInvalidTokenHandler:^{ - [weakSelf setAuthToken:nil]; - [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; - if (weakSelf.isDefaultWordPressComAccount) { - // At the time of writing, there is an implicit assumption on what the object parameter value means. - // For example, the WordPressAppDelegate.handleDefaultAccountChangedNotification(_:) subscriber inspects the object parameter to decide whether the notification was sent as a result of a login. - // If the object is non-nil, then the method considers the source a login. - // - // The code path in which we are is that of an invalid token, and that's neither a login nor a logout, it's more appropriate to consider it a logout. - // That's because if the token is invalid the app will soon received errors from the API and it's therefore better to force the user to login again. - [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:nil]; - } - }]; - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [WordPressAuthenticationManager showSigninForWPComFixingAuthToken]; - }); - } - } - return _wordPressComRestApi; - -} - -@end From 48f0802c4b58fd7ace7e99049c6212261598f197 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 18:18:54 +1100 Subject: [PATCH 16/25] WIP - Move more files - Build is broken --- Modules/Sources/WordPressData/Post.swift | 278 ++++++ .../WordPressDataObjC/AccountService.m | 452 ++++++++++ .../Sources/WordPressDataObjC/BlogService.m | 848 ++++++++++++++++++ .../WordPressDataObjC/Models/BasePost.m | 102 +++ .../Sources/WordPressDataObjC/Models/Blog.m | 1 + .../WordPressDataObjC/include/AbstractPost.h | 142 +++ .../include/AccountService.h | 133 +++ .../WordPressDataObjC/include/BlogService.h | 120 +++ .../include/PostContentProvider.h | 17 + 9 files changed, 2093 insertions(+) create mode 100644 Modules/Sources/WordPressData/Post.swift create mode 100644 Modules/Sources/WordPressDataObjC/AccountService.m create mode 100644 Modules/Sources/WordPressDataObjC/BlogService.m create mode 100644 Modules/Sources/WordPressDataObjC/Models/BasePost.m create mode 100644 Modules/Sources/WordPressDataObjC/include/AbstractPost.h create mode 100644 Modules/Sources/WordPressDataObjC/include/AccountService.h create mode 100644 Modules/Sources/WordPressDataObjC/include/BlogService.h create mode 100644 Modules/Sources/WordPressDataObjC/include/PostContentProvider.h diff --git a/Modules/Sources/WordPressData/Post.swift b/Modules/Sources/WordPressData/Post.swift new file mode 100644 index 000000000000..c6b87c321a61 --- /dev/null +++ b/Modules/Sources/WordPressData/Post.swift @@ -0,0 +1,278 @@ +import Foundation +import CoreData +import WordPressData + +@objc(Post) +class Post: AbstractPost { + @objc static let typeDefaultIdentifier = "post" + + struct Constants { + static let publicizeIdKey = "id" + static let publicizeValueKey = "value" + static let publicizeKeyKey = "key" + static let publicizeDisabledValue = "1" + static let publicizeEnabledValue = "0" + } + + enum PublicizeMetadataSkipPrefix: String { + case keyring = "_wpas_skip_" + case connection = "_wpas_skip_publicize_" + + /// Determines the prefix type from the given key. + /// + /// - Parameter key: String. + /// - Returns: A `PublicizeMetadataSkipPrefix` value, or nil if nothing matched. + static func prefix(of key: String) -> PublicizeMetadataSkipPrefix? { + // try to match the `keyring` format first, since it's a substring of the `connection` format. + guard key.hasPrefix(Self.keyring.rawValue) else { + return nil + } + return key.hasPrefix(Self.connection.rawValue) ? .connection : .keyring + } + } + + // MARK: - NSManagedObject + + override class func entityName() -> String { + return "Post" + } + + // MARK: - Format + + @objc func postFormatText() -> String? { + return blog.postFormatText(fromSlug: postFormat) + } + + @objc func setPostFormatText(_ postFormatText: String) { + + assert(blog.postFormats is [String: String]) + guard let postFormats = blog.postFormats as? [String: String] else { + DDLogError("Expected blog.postFormats to be \(String(describing: [String: String].self)).") + return + } + + var formatKey: String? + + for (key, value) in postFormats { + if value == postFormatText { + formatKey = key + break + } + } + + postFormat = formatKey + } + + // MARK: - Categories + + /// Returns categories as a comma-separated list + /// + @objc func categoriesText() -> String { + + guard let allStrings = categories?.map({ return $0.categoryName as String }) else { + return "" + } + + let orderedStrings = allStrings.sorted { (categoryName1, categoryName2) -> Bool in + return categoryName1.localizedCaseInsensitiveCompare(categoryName2) == .orderedAscending + } + + return orderedStrings.joined(separator: ", ") + } + + /// Set the categories for a post + /// + /// - Parameter categoryNames: a `NSArray` with the names of the categories for this post. If + /// a given category name doesn't exist it's ignored. + /// + @objc func setCategoriesFromNames(_ categoryNames: [String]) { + + var newCategories = Set() + + for categoryName in categoryNames { + + assert(blog.categories is Set) + guard let blogCategories = blog.categories as? Set else { + DDLogError("Expected blog.categories to be \(String(describing: Set.self)).") + return + } + + let matchingCategories = blogCategories.filter({ return $0.categoryName == categoryName }) + + if matchingCategories.count > 0 { + newCategories = newCategories.union(matchingCategories) + } + } + + categories = newCategories + } + + // MARK: - Sharing + + @objc func canEditPublicizeSettings() -> Bool { + return !self.hasRemote() || self.status != .publish + } + + // MARK: - PublicizeConnections + + @objc func publicizeConnectionDisabledForKeyringID(_ keyringID: NSNumber) -> Bool { + let isKeyringEntryDisabled = disabledPublicizeConnections?[keyringID]?[Constants.publicizeValueKey] == Constants.publicizeDisabledValue + + // try to check in case there's an entry for the PublicizeConnection that's keyed by the connectionID. + guard let connections = blog.connections as? Set, + let connection = connections.first(where: { $0.keyringConnectionID == keyringID }), + let existingValue = disabledPublicizeConnections?[connection.connectionID]?[Constants.publicizeValueKey] else { + // fall back to keyringID if there is no such entry with the connectionID. + return isKeyringEntryDisabled + } + + let isConnectionEntryDisabled = existingValue == Constants.publicizeDisabledValue + return isConnectionEntryDisabled || isKeyringEntryDisabled + } + + @objc func enablePublicizeConnectionWithKeyringID(_ keyringID: NSNumber) { + // if there's another entry keyed by connectionID references to the same connection, + // we need to make sure that the values are kept in sync. + if let connections = blog.connections as? Set, + let connection = connections.first(where: { $0.keyringConnectionID == keyringID }), + let _ = disabledPublicizeConnections?[connection.connectionID] { + enablePublicizeConnection(keyedBy: connection.connectionID) + } + + enablePublicizeConnection(keyedBy: keyringID) + } + + @objc func disablePublicizeConnectionWithKeyringID(_ keyringID: NSNumber) { + // if there's another entry keyed by connectionID references to the same connection, + // we need to make sure that the values are kept in sync. + if let connections = blog.connections as? Set, + let connectionID = connections.first(where: { $0.keyringConnectionID == keyringID })?.connectionID, + let _ = disabledPublicizeConnections?[connectionID] { + disablePublicizeConnection(keyedBy: connectionID) + + // additionally, if the keyring entry doesn't exist, there's no need create both formats. + // we can just update the dictionary's key from connectionID to keyringID instead. + if disabledPublicizeConnections?[keyringID] == nil, + let updatedEntry = disabledPublicizeConnections?[connectionID] { + disabledPublicizeConnections?.removeValue(forKey: connectionID) + disabledPublicizeConnections?[keyringID] = updatedEntry + return + } + } + + disablePublicizeConnection(keyedBy: keyringID) + } + + /// Marks the Publicize connection with the given id as enabled. + /// + /// - Parameter id: The dictionary key for `disabledPublicizeConnections`. + private func enablePublicizeConnection(keyedBy id: NSNumber) { + guard var connection = disabledPublicizeConnections?[id] else { + return + } + + // if the auto-sharing settings is not yet synced to remote, + // we can just remove the entry since all connections are enabled by default. + guard let _ = connection[Constants.publicizeIdKey] else { + _ = disabledPublicizeConnections?.removeValue(forKey: id) + return + } + + connection[Constants.publicizeValueKey] = Constants.publicizeEnabledValue + disabledPublicizeConnections?[id] = connection + } + + /// Marks the Publicize connection with the given id as disabled. + /// + /// - Parameter id: The dictionary key for `disabledPublicizeConnections`. + private func disablePublicizeConnection(keyedBy id: NSNumber) { + if let _ = disabledPublicizeConnections?[id] { + disabledPublicizeConnections?[id]?[Constants.publicizeValueKey] = Constants.publicizeDisabledValue + return + } + + if disabledPublicizeConnections == nil { + disabledPublicizeConnections = [NSNumber: [String: String]]() + } + + disabledPublicizeConnections?[id] = [Constants.publicizeValueKey: Constants.publicizeDisabledValue] + } + + // MARK: - Comments + + @objc func numberOfComments() -> Int { + return commentCount?.intValue ?? 0 + } + + // MARK: - Likes + + @objc func numberOfLikes() -> Int { + return likeCount?.intValue ?? 0 + } + + // MARK: - AbstractPost + + override func hasCategories() -> Bool { + categories?.isEmpty == false + } + + override func hasTags() -> Bool { + tags?.trim().isEmpty == false + } + + override func authorForDisplay() -> String? { + author ?? blog.account?.displayName + } + + // MARK: - BasePost + + override func contentPreviewForDisplay() -> String { + if let excerpt = mt_excerpt, excerpt.count > 0 { + if let preview = PostPreviewCache.shared.excerpt[excerpt] { + return preview + } + let preview = excerpt.makePlainText() + PostPreviewCache.shared.excerpt[excerpt] = preview + return preview + } else if let content { + if let preview = PostPreviewCache.shared.content[content] { + return preview + } + let preview = content.summarized() + PostPreviewCache.shared.content[content] = preview + return preview + } else { + return "" + } + } + + override func titleForDisplay() -> String { + var title = postTitle?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" + title = title + .stringByDecodingXMLCharacters() + .strippingHTML() + + if title.count == 0 && contentPreviewForDisplay().count == 0 && !hasRemote() { + title = NSLocalizedString("(no title)", comment: "Lets a user know that a local draft does not have a title.") + } + + return title + } +} + +private final class PostPreviewCache { + static let shared = PostPreviewCache() + + let excerpt = Cache() + let content = Cache() +} + +private final class Cache { + private let lock = NSLock() + private var dictionary: [Key: Value] = [:] + + subscript(key: Key) -> Value? { + get { lock.withLock { dictionary[key] } } + set { lock.withLock { dictionary[key] = newValue } } + } +} diff --git a/Modules/Sources/WordPressDataObjC/AccountService.m b/Modules/Sources/WordPressDataObjC/AccountService.m new file mode 100644 index 000000000000..45f19e57fa45 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/AccountService.m @@ -0,0 +1,452 @@ +#import "AccountService.h" +#import "WPAccount.h" +@import WordPressDataObjC; +#import "Blog.h" +#import "BlogService.h" + +@import WordPressKit; +@import WordPressShared; +#import "WordPress-Swift.h" + +static NSString * const DefaultDotcomAccountUUIDDefaultsKey = @"AccountDefaultDotcomUUID"; +static NSString * const DefaultDotcomAccountPasswordRemovedKey = @"DefaultDotcomAccountPasswordRemovedKey"; + +static NSString * const WordPressDotcomXMLRPCKey = @"https://wordpress.com/xmlrpc.php"; +NSNotificationName const WPAccountDefaultWordPressComAccountChangedNotification = @"WPAccountDefaultWordPressComAccountChangedNotification"; +NSString * const WPAccountEmailAndDefaultBlogUpdatedNotification = @"WPAccountEmailAndDefaultBlogUpdatedNotification"; + +@implementation AccountService + +///------------------------------------ +/// @name Default WordPress.com account +///------------------------------------ + +/** + Sets the default WordPress.com account + + @param account the account to set as default for WordPress.com + @see defaultWordPressComAccount + @see removeDefaultWordPressComAccount + */ +- (void)setDefaultWordPressComAccount:(WPAccount *)account +{ + NSParameterAssert(account != nil); + NSAssert(account.authToken.length > 0, @"Account should have an authToken for WP.com"); + + if ([account isDefaultWordPressComAccount]) { + return; + } + + [[UserPersistentStoreFactory userDefaultsInstance] setObject:account.uuid forKey:DefaultDotcomAccountUUIDDefaultsKey]; + + NSManagedObjectID *accountID = account.objectID; + void (^notifyAccountChange)(void) = ^{ + NSManagedObjectContext *mainContext = self.coreDataStack.mainContext; + NSManagedObject *accountInContext = [mainContext existingObjectWithID:accountID error:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:accountInContext]; + + [[PushNotificationsManager shared] setupRemoteNotifications]; + }; + if ([NSThread isMainThread]) { + // This is meant to help with testing account observers. + // Short version: dispatch_async and XCTest asynchronous helpers don't play nice with each other + // Long version: see the comment in https://github.com/wordpress-mobile/WordPress-iOS/blob/2f9a2100ca69d8f455acec47a1bbd6cbc5084546/WordPress/WordPressTest/AccountServiceRxTests.swift#L7 + notifyAccountChange(); + } else { + dispatch_async(dispatch_get_main_queue(), notifyAccountChange); + } +} + +/** + Removes the default WordPress.com account + + @see defaultWordPressComAccount + @see setDefaultWordPressComAccount: + */ +- (void)removeDefaultWordPressComAccount +{ + NSAssert([NSThread isMainThread], @"This method should only be called from the main thread"); + + [[PushNotificationsManager shared] unregisterDeviceToken]; + + WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; + if (account == nil) { + return; + } + + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + WPAccount *accountInContext = [context existingObjectWithID:account.objectID error:nil]; + [context deleteObject:accountInContext]; + }]; + + // Clear WordPress.com cookies + NSArray> *cookieJars = @[ + (id)[NSHTTPCookieStorage sharedHTTPCookieStorage], + (id)[[WKWebsiteDataStore defaultDataStore] httpCookieStore] + ]; + + for (id cookieJar in cookieJars) { + [cookieJar removeWordPressComCookiesWithCompletion:^{}]; + } + + [[NSURLCache sharedURLCache] removeAllCachedResponses]; + + // Remove defaults + [[UserPersistentStoreFactory userDefaultsInstance] removeObjectForKey:DefaultDotcomAccountUUIDDefaultsKey]; + + [WPAnalytics refreshMetadata]; + [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:nil]; + + [StatsCache clearCaches]; +} + +- (void)isEmailAvailable:(NSString *)email success:(void (^)(BOOL available))success failure:(void (^)(NSError *error))failure +{ + id remote = [self remoteForAnonymous]; + [remote isEmailAvailable:email success:^(BOOL available) { + if (success) { + success(available); + } + } failure:^(NSError *error) { + if (failure) { + failure(error); + } + }]; +} + +- (void)isUsernameAvailable:(NSString *)username + success:(void (^)(BOOL available))success + failure:(void (^)(NSError *error))failure +{ + id remote = [self remoteForAnonymous]; + [remote isUsernameAvailable:username success:^(BOOL available) { + if (success) { + success(available); + } + } failure:^(NSError *error) { + if (failure) { + failure(error); + } + }]; +} + +- (void)requestVerificationEmail:(void (^)(void))success failure:(void (^)(NSError * _Nonnull))failure +{ + NSAssert([NSThread isMainThread], @"This method should only be called from the main thread"); + + WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; + id remote = [self remoteForAccount:account]; + [remote requestVerificationEmailWithSucccess:^{ + if (success) { + success(); + } + } failure:^(NSError *error) { + if (failure) { + failure(error); + } + }]; +} + + +///----------------------- +/// @name Account creation +///----------------------- + +- (NSManagedObjectID *)createOrUpdateAccountWithUserDetails:(RemoteUser *)remoteUser authToken:(NSString *)authToken +{ + NSManagedObjectID * __block accountObjectID = nil; + [self.coreDataStack.mainContext performBlockAndWait:^{ + accountObjectID = [[WPAccount lookupWithUsername:remoteUser.username context:self.coreDataStack.mainContext] objectID]; + }]; + + if (accountObjectID) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + WPAccount *account = [context existingObjectWithID:accountObjectID error:nil]; + // Even if we find an account via its userID we should still update + // its authtoken, otherwise the Authenticator's authtoken fixer won't + // work. + account.authToken = authToken; + }]; + } else { + accountObjectID = [self createOrUpdateAccountWithUsername:remoteUser.username authToken:authToken]; + } + + [self updateAccountWithID:accountObjectID withUserDetails:remoteUser]; + + return accountObjectID; +} + +/** + Creates a new WordPress.com account or updates the password if there is a matching account + + There can only be one WordPress.com account per username, so if one already exists for the given `username` its password is updated + + Uses a background managed object context. + + @param username the WordPress.com account's username + @param authToken the OAuth2 token returned by signIntoWordPressDotComWithUsername:authToken: + @return The ID of the WordPress.com `WPAccount` object for the given `username` + @see createOrUpdateWordPressComAccountWithUsername:password:authToken: + */ +- (NSManagedObjectID *)createOrUpdateAccountWithUsername:(NSString *)username authToken:(NSString *)authToken +{ + NSManagedObjectID * __block objectID = nil; + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + WPAccount *account = [WPAccount lookupWithUsername:username context:context]; + if (!account) { + account = [NSEntityDescription insertNewObjectForEntityForName:@"Account" inManagedObjectContext:context]; + account.uuid = [[NSUUID new] UUIDString]; + account.username = username; + } + account.authToken = authToken; + [context obtainPermanentIDsForObjects:@[account] error:nil]; + objectID = account.objectID; + }]; + + [self.coreDataStack.mainContext performBlockAndWait:^{ + WPAccount *defaultAccount = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; + if (!defaultAccount) { + WPAccount *account = [self.coreDataStack.mainContext existingObjectWithID:objectID error:nil]; + [self setDefaultWordPressComAccount:account]; + dispatch_async(dispatch_get_main_queue(), ^{ + [WPAnalytics refreshMetadata]; + }); + } + }]; + + return objectID; +} + +/** + Checks an account to see if it is just used to connect to Jetpack. + + @param account The account to inspect. + @return True if used only for a Jetpack connection. + */ +- (BOOL)accountHasOnlyJetpackBlogs:(WPAccount *)account +{ + if ([account.blogs count] == 0) { + // Most likly, this is a blogless account used for the reader or commenting and not Jetpack. + return NO; + } + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.isHostedAtWPcom = true"]; + NSSet *wpcomBlogs = [account.blogs filteredSetUsingPredicate:predicate]; + if ([wpcomBlogs count] > 0) { + return NO; + } + + return YES; +} + +- (void)restoreDisassociatedAccountIfNecessary +{ + NSAssert([NSThread isMainThread], @"This method should only be called from the main thread"); + + if([WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext] != nil) { + return; + } + + // Attempt to restore a default account that has somehow been disassociated. + WPAccount *account = [self findDefaultAccountCandidateFromAccounts:[WPAccount lookupAllAccountsInContext:self.coreDataStack.mainContext]]; + if (account) { + // Assume we have a good candidate account and make it the default account in the app. + // Note that this should be the account with the most blogs. + // Updates user defaults here vs the setter method to avoid potential side-effects from dispatched notifications. + [[UserPersistentStoreFactory userDefaultsInstance] setObject:account.uuid forKey:DefaultDotcomAccountUUIDDefaultsKey]; + } +} + +- (WPAccount *)findDefaultAccountCandidateFromAccounts:(NSArray *)allAccounts +{ + NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"blogs.@count" ascending:NO]; + NSArray *accounts = [allAccounts sortedArrayUsingDescriptors:@[sort]]; + + for (WPAccount *account in accounts) { + // Skip accounts that were likely added to Jetpack-connected self-hosted + // sites, while there was an existing default wpcom account. + if ([self accountHasOnlyJetpackBlogs:account]) { + continue; + } + return account; + } + return nil; +} + +- (void)createOrUpdateAccountWithAuthToken:(NSString *)authToken + success:(void (^)(WPAccount * _Nonnull))success + failure:(void (^)(NSError * _Nonnull))failure +{ + WordPressComRestApi *api = [WordPressComRestApi defaultApiWithOAuthToken:authToken userAgent:[WPUserAgent defaultUserAgent] localeKey:[WordPressComRestApi LocaleKeyDefault]]; + AccountServiceRemoteREST *remote = [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:api]; + [remote getAccountDetailsWithSuccess:^(RemoteUser *remoteUser) { + NSManagedObjectID *objectID = [self createOrUpdateAccountWithUserDetails:remoteUser authToken:authToken]; + WPAccount * __block account = nil; + [self.coreDataStack.mainContext performBlockAndWait:^{ + account = [self.coreDataStack.mainContext existingObjectWithID:objectID error:nil]; + }]; + success(account); + } failure:^(NSError *error) { + failure(error); + }]; +} + +- (void)updateUserDetailsForAccount:(WPAccount *)account + success:(nullable void (^)(void))success + failure:(nullable void (^)(NSError * _Nonnull))failure +{ + NSAssert(account, @"Account can not be nil"); + NSAssert(account.username, @"account.username can not be nil"); + + id remote = [self remoteForAccount:account]; + [remote getAccountDetailsWithSuccess:^(RemoteUser *remoteUser) { + // account.objectID can be temporary, so fetch via username/xmlrpc instead. + [self updateAccountWithID:account.objectID withUserDetails:remoteUser]; + dispatch_async(dispatch_get_main_queue(), ^{ + [WPAnalytics refreshMetadata]; + if (success) { + success(); + } + }); + } failure:^(NSError *error) { + DDLogError(@"Failed to fetch user details for account %@. %@", account, error); + if (failure) { + failure(error); + } + }]; +} + +- (id)remoteForAnonymous +{ + WordPressComRestApi *api = [WordPressComRestApi defaultApiWithOAuthToken:nil + userAgent:nil + localeKey:[WordPressComRestApi LocaleKeyDefault]]; + return [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:api]; +} + +- (id)remoteForAccount:(WPAccount *)account +{ + if (account.wordPressComRestApi == nil) { + return nil; + } + + return [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:account.wordPressComRestApi]; +} + +- (void)updateAccountWithID:(NSManagedObjectID *)objectID withUserDetails:(RemoteUser *)userDetails +{ + NSParameterAssert(![objectID isTemporaryID]); + + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + WPAccount *account = [context existingObjectWithID:objectID error:nil]; + account.userID = userDetails.userID; + account.username = userDetails.username; + account.email = userDetails.email; + account.avatarURL = userDetails.avatarURL; + account.displayName = userDetails.displayName; + account.dateCreated = userDetails.dateCreated; + account.emailVerified = @(userDetails.emailVerified); + account.primaryBlogID = userDetails.primaryBlogID; + }]; + + // Make sure the account is saved before updating its default blog. + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + WPAccount *account = [context existingObjectWithID:objectID error:nil]; + [self updateDefaultBlogIfNeeded:account inContext:context]; + [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountEmailAndDefaultBlogUpdatedNotification object:nil]; + }]; +} + +- (void)updateDefaultBlogIfNeeded:(WPAccount *)account inContext:(NSManagedObjectContext *)context +{ + NSParameterAssert(account.managedObjectContext == context); + + if (!account.primaryBlogID || [account.primaryBlogID intValue] == 0) { + return; + } + + // Load the Default Blog + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"blogID = %@", account.primaryBlogID]; + Blog *defaultBlog = [[account.blogs filteredSetUsingPredicate:predicate] anyObject]; + + if (!defaultBlog) { + DDLogError(@"Error: The Default Blog could not be loaded"); + return; + } + + // Setup the Account + account.defaultBlog = defaultBlog; + + // Update app extensions if needed. + if ([account isDefaultWordPressComAccount]) { + [self setupAppExtensionsWithDefaultAccount:account inContext:context]; + } +} + +- (void)setupAppExtensionsWithDefaultAccount +{ + NSManagedObjectContext *context = self.coreDataStack.mainContext; + [context performBlockAndWait:^{ + WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:context]; + if (account == nil) { + return; + } + [self setupAppExtensionsWithDefaultAccount:account inContext:context]; + }]; +} + +- (void)setupAppExtensionsWithDefaultAccount:(WPAccount *)defaultAccount inContext:(NSManagedObjectContext *)context +{ + NSParameterAssert(defaultAccount.managedObjectContext == context); + + NSManagedObjectID *defaultAccountObjectID = defaultAccount.objectID; + Blog *defaultBlog = [defaultAccount defaultBlog]; + NSNumber *siteId = defaultBlog.dotComID; + NSString *blogName = defaultBlog.settings.name; + + if (defaultBlog == nil || defaultBlog.isDeleted) { + dispatch_async(dispatch_get_main_queue(), ^{ + [ShareExtensionService removeShareExtensionConfiguration]; + + [NotificationSupportService deleteServiceExtensionToken]; + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + WPAccount *defaultAccount = [self.coreDataStack.mainContext existingObjectWithID:defaultAccountObjectID error:nil]; + + [ShareExtensionService configureShareExtensionDefaultSiteID:siteId.integerValue defaultSiteName:blogName]; + [ShareExtensionService configureShareExtensionToken:defaultAccount.authToken]; + [ShareExtensionService configureShareExtensionUsername:defaultAccount.username]; + + [NotificationSupportService insertServiceExtensionToken:defaultAccount.authToken]; + [NotificationSupportService insertServiceExtensionUsername:defaultAccount.username]; + [NotificationSupportService insertServiceExtensionUserID:defaultAccount.userID.stringValue]; + }); + } +} + +- (void)purgeAccountIfUnused:(WPAccount *)account +{ + NSParameterAssert(account); + + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + WPAccount *accountInContext = [context existingObjectWithID:account.objectID error:nil]; + if (accountInContext == nil) { + return; + } + + BOOL purge = NO; + WPAccount *defaultAccount = [WPAccount lookupDefaultWordPressComAccountInContext:context]; + if ([accountInContext.blogs count] == 0 + && ![defaultAccount isEqual:accountInContext]) { + purge = YES; + } + + if (purge) { + DDLogWarn(@"Removing account since it has no blogs associated and it's not the default account: %@", accountInContext); + [context deleteObject:accountInContext]; + } + }]; +} + +@end diff --git a/Modules/Sources/WordPressDataObjC/BlogService.m b/Modules/Sources/WordPressDataObjC/BlogService.m new file mode 100644 index 000000000000..e945ce1f2da7 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/BlogService.m @@ -0,0 +1,848 @@ +#import "BlogService.h" +#import "Blog.h" +#import "WPAccount.h" +#import "AccountService.h" +@import WordPressDataObjC; +#import "WPError.h" +#import "Media.h" +#import "PostCategoryService.h" +#import "CommentService.h" +#import "PostService.h" +#import "WordPress-Swift.h" +#import "PostType.h" +@import WordPressKit; +@import WordPressShared; + +@class Comment; + +NSString *const WPComGetFeatures = @"wpcom.getFeatures"; +NSString *const VideopressEnabled = @"videopress_enabled"; +NSString *const WordPressMinimumVersion = @"4.0"; +NSString *const HttpsPrefix = @"https://"; +NSString *const WPBlogUpdatedNotification = @"WPBlogUpdatedNotification"; +NSString *const WPBlogSettingsUpdatedNotification = @"WPBlogSettingsUpdatedNotification"; + +@implementation BlogService + +- (void)syncBlogsForAccount:(WPAccount *)account + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + DDLogMethod(); + + id remote = [self remoteForAccount:account]; + + BOOL filterJetpackSites = false; + + [remote getBlogs:filterJetpackSites success:^(NSArray *blogs) { + [[[JetpackCapabilitiesService alloc] init] syncWithBlogs:blogs success:^(NSArray *blogs) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + [self mergeBlogs:blogs withAccountID:account.objectID inContext:context]; + } completion:success onQueue:dispatch_get_main_queue()]; + }]; + } failure:^(NSError *error) { + DDLogError(@"Error syncing blogs: %@", error); + + if (failure) { + failure(error); + } + }]; +} + +- (void)syncBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + id remote = [self remoteForBlog:blog]; + if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { + BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; + [xmlrpcRemote syncBlogOptionsWithSuccess:[self optionsHandlerWithBlogObjectID:blog.objectID + completionHandler:success] + failure:failure]; + } else if ([remote isKindOfClass:[BlogServiceRemoteREST class]]) { + BlogServiceRemoteREST *restRemote = remote; + [restRemote syncBlogWithSuccess:[self blogDetailsHandlerWithBlogObjectID:blog.objectID + completionHandler:success] + failure:failure]; + } +} + +- (void)syncBlogAndAllMetadata:(Blog *)blog completionHandler:(void (^)(void))completionHandler +{ + // Create a dispatch group. We'll use this to monitor completion of the various + // remote calls and to execute the completionHandler. + dispatch_group_t syncGroup = dispatch_group_create(); + + NSManagedObjectID *blogObjectID = blog.objectID; + id remote = [self remoteForBlog:blog]; + + if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { + dispatch_group_enter(syncGroup); + BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; + [xmlrpcRemote syncBlogOptionsWithSuccess:[self optionsHandlerWithBlogObjectID:blogObjectID + completionHandler:^{ + dispatch_group_leave(syncGroup); + }] + failure:^(NSError *error) { + DDLogError(@"Failed syncing options for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + } + + if ([remote isKindOfClass:[BlogServiceRemoteREST class]]) { + dispatch_group_enter(syncGroup); + BlogServiceRemoteREST *restRemote = remote; + [restRemote syncBlogWithSuccess:[self blogDetailsHandlerWithBlogObjectID:blogObjectID + completionHandler:^{ + dispatch_group_leave(syncGroup); + }] + failure:^(NSError *error) { + DDLogError(@"Failed syncing site details for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + dispatch_group_enter(syncGroup); + [restRemote syncBlogSettingsWithSuccess:^(RemoteBlogSettings *settings) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blogInContext = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; + if (blogInContext) { + [self updateSettings:blogInContext.settings withRemoteSettings:settings]; + } + } completion:^{ + dispatch_group_leave(syncGroup); + } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + } failure:^(NSError *error) { + DDLogError(@"Failed syncing settings for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + } + + dispatch_group_enter(syncGroup); + [remote syncPostFormatsWithSuccess:[self postFormatsHandlerWithBlogObjectID:blogObjectID + completionHandler:^{ + dispatch_group_leave(syncGroup); + }] + failure:^(NSError *error) { + DDLogError(@"Failed syncing post formats for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + PostCategoryService *categoryService = [[PostCategoryService alloc] initWithCoreDataStack:self.coreDataStack]; + dispatch_group_enter(syncGroup); + [categoryService syncCategoriesForBlog:blog + success:^{ + dispatch_group_leave(syncGroup); + } + failure:^(NSError *error) { + DDLogError(@"Failed syncing categories for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:self.coreDataStack]; + dispatch_group_enter(syncGroup); + [sharingService syncPublicizeConnectionsForBlog:blog + success:^{ + dispatch_group_leave(syncGroup); + } + failure:^(NSError *error) { + DDLogError(@"Failed syncing publicize connections for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + SharingService *publicizeService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; + dispatch_group_enter(syncGroup); + [publicizeService syncPublicizeServicesForBlog:blog success:^{ + dispatch_group_leave(syncGroup); + } failure:^(NSError * _Nullable error) { + DDLogError(@"Failed syncing publicize services for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + if ([RemoteFeature enabled:RemoteFeatureFlagJetpackSocialImprovements] && blog.dotComID != nil) { + JetpackSocialService *jetpackSocialService = [[JetpackSocialService alloc] initWithContextManager:ContextManager.sharedInstance]; + dispatch_group_enter(syncGroup); + [jetpackSocialService syncSharingLimitWithDotComID:blog.dotComID success:^{ + dispatch_group_leave(syncGroup); + } failure:^(NSError * _Nullable error) { + DDLogError(@"Failed syncing publicize sharing limit for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + } + + dispatch_group_enter(syncGroup); + [remote getAllAuthorsWithSuccess:^(NSArray *users) { + [self updateMultiAuthor:users forBlog:blogObjectID completionHandler:^{ + dispatch_group_leave(syncGroup); + }]; + } failure:^(NSError *error) { + DDLogError(@"Failed checking multi-author status for blog %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + PlanService *planService = [[PlanService alloc] initWithCoreDataStack:self.coreDataStack]; + dispatch_group_enter(syncGroup); + [planService getWpcomPlans:blog.account + success:^{ + dispatch_group_leave(syncGroup); + } failure:^(NSError *error) { + DDLogError(@"Failed updating plans: %@", error); + dispatch_group_leave(syncGroup); + }]; + + dispatch_group_enter(syncGroup); + [planService plansWithPricesForBlog:blog success:^{ + dispatch_group_leave(syncGroup); + } failure:^(NSError *error) { + DDLogError(@"Failed checking domain credit for site %@: %@", blog.url, error); + dispatch_group_leave(syncGroup); + }]; + + EditorSettingsService *editorService = [[EditorSettingsService alloc] initWithCoreDataStack:self.coreDataStack]; + dispatch_group_enter(syncGroup); + [editorService syncEditorSettingsForBlog:blog success:^{ + dispatch_group_leave(syncGroup); + } failure:^(NSError * _Nonnull __unused error) { + DDLogError(@"Failed to sync Editor settings"); + dispatch_group_leave(syncGroup); + }]; + + if ([FreeToPaidPlansDashboardCardHelper isFeatureEnabled]) { + dispatch_group_enter(syncGroup); + [self refreshDomainsFor:blog success:^{ + dispatch_group_leave(syncGroup); + } failure:^(NSError * _Nonnull error) { + DDLogError(@"Failed refreshing domains: %@", error); + dispatch_group_leave(syncGroup); + }]; + } + + // When everything has left the syncGroup (all calls have ended with success + // or failure) perform the completionHandler + dispatch_group_notify(syncGroup, dispatch_get_main_queue(),^{ + if (completionHandler) { + completionHandler(); + } + }); +} + +- (void)syncSettingsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *blogID = [blog objectID]; + [self.coreDataStack.mainContext performBlock:^{ + Blog *blogInContext = (Blog *)[self.coreDataStack.mainContext objectWithID:blogID]; + if (!blogInContext) { + if (success) { + success(); + } + return; + } + + void(^updateOnSuccess)(RemoteBlogSettings *) = ^(RemoteBlogSettings *remoteSettings) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blogInContext = (Blog *)[context objectWithID:blogID]; + [self updateSettings:blogInContext.settings withRemoteSettings:remoteSettings]; + } completion:success onQueue:dispatch_get_main_queue()]; + }; + id remote = [self remoteForBlog:blogInContext]; + if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { + + BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; + [xmlrpcRemote syncBlogOptionsWithSuccess:^(NSDictionary *options) { + RemoteBlogSettings *remoteSettings = [RemoteBlogOptionsHelper remoteBlogSettingsFromXMLRPCDictionaryOptions:options]; + updateOnSuccess(remoteSettings); + } failure:failure]; + + } else if ([remote isKindOfClass:[BlogServiceRemoteREST class]]) { + + BlogServiceRemoteREST *restRemote = remote; + [restRemote syncBlogSettingsWithSuccess:^(RemoteBlogSettings *settings) { + updateOnSuccess(settings); + } failure:failure]; + } + }]; +} + +- (void)syncAuthorsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *blogObjectID = blog.objectID; + id remote = [self remoteForBlog:blog]; + + [remote getAllAuthorsWithSuccess:^(NSArray *users) { + [self updateMultiAuthor:users forBlog:blogObjectID completionHandler:success]; + } failure:^(NSError *error) { + DDLogError(@"Failed checking multi-author status for blog %@: %@", blog.url, error); + failure(error); + }]; +} + +- (void)updateSettingsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *blogID = [blog objectID]; + NSManagedObjectContext *context = self.coreDataStack.mainContext; + [context performBlock:^{ + Blog *blogInContext = (Blog *)[context objectWithID:blogID]; + id remote = [self remoteForBlog:blogInContext]; + RemoteBlogSettings *remoteSettings = [self remoteSettingFromSettings:blogInContext.settings]; + + void(^onSuccess)(void) = ^() { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blogInContext = (Blog *)[context existingObjectWithID:blogID error:nil]; + if (blogInContext) { + [self updateSettings:blogInContext.settings withRemoteSettings:remoteSettings]; + } + } completion:^{ + if (success) { + success(); + } + } onQueue:dispatch_get_main_queue()]; + }; + + if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { + + BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; + [xmlrpcRemote updateBlogOptionsWith:[RemoteBlogOptionsHelper remoteOptionsForUpdatingBlogTitleAndTagline:remoteSettings] + success:onSuccess + failure:failure]; + + } else if([remote isKindOfClass:[BlogServiceRemoteREST class]]) { + + BlogServiceRemoteREST *restRemote = remote; + [restRemote updateBlogSettings:remoteSettings + success:onSuccess + failure:failure]; + } + }]; +} + +- (void)syncPostTypesForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *blogObjectID = blog.objectID; + id remote = [self remoteForBlog:blog]; + [remote syncPostTypesWithSuccess:^(NSArray *remotePostTypes) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + NSError *blogError; + Blog *blogInContext = (Blog *)[context existingObjectWithID:blogObjectID error:&blogError]; + if (!blogInContext || blogError) { + DDLogError(@"Error occurred fetching blog in context with: %@", blogError); + dispatch_async(dispatch_get_main_queue(), ^{ + if (failure) { + failure(blogError); + return; + } + }); + } + // Create new PostType entities with the RemotePostType objects. + NSMutableSet *postTypes = [NSMutableSet setWithCapacity:remotePostTypes.count]; + NSString *entityName = NSStringFromClass([PostType class]); + for (RemotePostType *remoteType in remotePostTypes) { + PostType *postType = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:context]; + postType.name = remoteType.name; + postType.label = remoteType.label; + postType.apiQueryable = remoteType.apiQueryable; + [postTypes addObject:postType]; + } + // Replace the current set of postTypes with new entities. + blogInContext.postTypes = [NSSet setWithSet:postTypes]; + } completion:success onQueue:dispatch_get_main_queue()]; + } failure:failure]; +} + +- (void)syncPostFormatsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + id remote = [self remoteForBlog:blog]; + [remote syncPostFormatsWithSuccess:[self postFormatsHandlerWithBlogObjectID:blog.objectID + completionHandler:success] + failure:failure]; +} + +///-------------------- +/// @name Blog creation +///-------------------- + +- (Blog *)findBlogWithDotComID:(NSNumber *)dotComID + inAccount:(WPAccount *)account +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"dotComID = %@", dotComID]; + return [[account.blogs filteredSetUsingPredicate:predicate] anyObject]; +} + +- (void)removeBlog:(Blog *)blog +{ + DDLogInfo(@" remove", blog.hostURL); + [blog.xmlrpcApi invalidateAndCancelTasks]; + [self unscheduleBloggingRemindersFor:blog]; + + WPAccount *account = blog.account; + + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blogInContext = [context existingObjectWithID:blog.objectID error:nil]; + [context deleteObject:blogInContext]; + }]; + + if (account) { + AccountService *accountService = [[AccountService alloc] initWithCoreDataStack:self.coreDataStack]; + [accountService purgeAccountIfUnused:account]; + } + + [WPAnalytics refreshMetadata]; +} + +- (void)associateSyncedBlogsToJetpackAccount:(WPAccount *)account + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + AccountServiceRemoteREST *remote = [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:account.wordPressComRestApi]; + + BOOL filterJetpackSites = false; + + [remote getBlogs:filterJetpackSites success:^(NSArray *remoteBlogs) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + NSMutableSet *accountBlogIDs = [NSMutableSet new]; + for (RemoteBlog *remoteBlog in remoteBlogs) { + [accountBlogIDs addObject:remoteBlog.blogID]; + } + + NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Blog class])]; + request.predicate = [NSPredicate predicateWithFormat:@"account = NULL"]; + NSArray *blogs = [context executeFetchRequest:request error:nil]; + blogs = [blogs filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * __unused bindings) { + Blog *blog = (Blog *)evaluatedObject; + NSNumber *jetpackBlogID = blog.jetpack.siteID; + return jetpackBlogID && [accountBlogIDs containsObject:jetpackBlogID]; + }]]; + + WPAccount *accountInContext = [context existingObjectWithID:account.objectID error:nil]; + [accountInContext addBlogs:[NSSet setWithArray:blogs]]; + } completion:success onQueue:dispatch_get_main_queue()]; + } failure:failure]; +} + +#pragma mark - Private methods + +- (void)mergeBlogs:(NSArray *)blogs withAccountID:(NSManagedObjectID *)accountID inContext:(NSManagedObjectContext *)context +{ + // Nuke dead blogs + NSError *error = nil; + WPAccount *account = [context existingObjectWithID:accountID error:&error]; + if (account == nil) { + DDLogInfo(@"Can't find the account. User may have signed out. Error: %@", error); + return; + } + + NSSet *remoteSet = [NSSet setWithArray:[blogs valueForKey:@"blogID"]]; + NSSet *localSet = [account.blogs valueForKey:@"dotComID"]; + NSMutableSet *toDelete = [localSet mutableCopy]; + [toDelete minusSet:remoteSet]; + + if ([toDelete count] > 0) { + for (Blog *blog in account.blogs) { + if ([toDelete containsObject:blog.dotComID]) { + [self unscheduleBloggingRemindersFor:blog]; + // Consider switching this to a call to removeBlog in the future + // to consolidate behaviour @frosty + [context deleteObject:blog]; + } + } + } + + // Go through each remote incoming blog and make sure we're up to date with titles, etc. + // Also adds any blogs we don't have + for (RemoteBlog *remoteBlog in blogs) { + [self updateBlogWithRemoteBlog:remoteBlog account:account inContext:context]; + [self updatePromptSettingsFor:remoteBlog context:context]; + } + + /* + Sometimes bad things happen and blogs get duplicated. 👭 + Hopefully we'll fix all the causes and this should never happen again 🤞🤞🤞 + But even if it never happens again, it has already happened so we need to clean up. 🧹 + Otherwise, users would have to reinstall the app to get rid of duplicates 🙅‍♀️ + + More context here: + https://github.com/wordpress-mobile/WordPress-iOS/issues/7886#issuecomment-524221031 + */ + [account deduplicateBlogs]; + + // Ensure that the account has a default blog defined (if there is one). + AccountService *service = [[AccountService alloc] initWithCoreDataStack:self.coreDataStack]; + [service updateDefaultBlogIfNeeded:account inContext:context]; +} + +- (void)updateBlogWithRemoteBlog:(RemoteBlog *)remoteBlog account:(WPAccount *)account inContext:(NSManagedObjectContext *)context +{ + Blog *blog = [self findBlogWithDotComID:remoteBlog.blogID inAccount:account]; + + if (!blog && remoteBlog.jetpack) { + blog = [self migrateRemoteJetpackBlog:remoteBlog forAccount:account inContext:context]; + } + + if (!blog) { + DDLogInfo(@"New blog from account %@: %@", account.username, remoteBlog); + if (account != nil) { + blog = [Blog createBlankBlogWithAccount:account]; + } else { + blog = [Blog createBlankBlogInContext:context]; + } + blog.xmlrpc = remoteBlog.xmlrpc; + } + + [self updateBlog:blog withRemoteBlog:remoteBlog]; +} + +- (void)updateBlog:(Blog *)blog withRemoteBlog:(RemoteBlog *)remoteBlog +{ + [blog addSettingsIfNecessary]; + + blog.url = remoteBlog.url; + blog.dotComID = remoteBlog.blogID; + blog.organizationID = remoteBlog.organizationID; + blog.isHostedAtWPcom = !remoteBlog.jetpack; + blog.icon = remoteBlog.icon; + blog.capabilities = remoteBlog.capabilities; + blog.isAdmin = remoteBlog.isAdmin; + blog.options = remoteBlog.options; + blog.planID = remoteBlog.planID; + blog.planTitle = remoteBlog.planTitle; + blog.planActiveFeatures = remoteBlog.planActiveFeatures; + blog.hasPaidPlan = remoteBlog.hasPaidPlan; + blog.quotaSpaceAllowed = remoteBlog.quotaSpaceAllowed; + blog.quotaSpaceUsed = remoteBlog.quotaSpaceUsed; + + // Update 'Top Level' Settings + BlogSettings *settings = blog.settings; + settings.name = [remoteBlog.name stringByDecodingXMLCharacters]; + settings.tagline = [remoteBlog.tagline stringByDecodingXMLCharacters]; + + [NSNotificationCenter.defaultCenter postNotificationName:WPBlogUpdatedNotification object:nil]; +} + +/** + Searches for Jetpack blog that has already been added as a self hosted to the app + and migrates it to use Jetpack REST. + + When a Jetpack blog appears on the results to sync blogs, we want to see if it's + already added in the app as a self hosted site. If that's the case, this method + will take the blog and transfer it to the account. + + It would be the equivalent of just syncing and removing the previous self hosted, + but this will preserve the synced blog objects and local drafts. + + @param remoteBlog the RemoteBlog object with the blog details + @param account the account that the blog should be migrated to + @returns the migrated blog if found, or nil otherwise + */ +- (Blog *)migrateRemoteJetpackBlog:(RemoteBlog *)remoteBlog + forAccount:(WPAccount *)account + inContext:(NSManagedObjectContext *)context +{ + assert(remoteBlog.xmlrpc != nil); + NSURL *xmlrpcURL = [NSURL URLWithString:remoteBlog.xmlrpc]; + NSURLComponents *components = [NSURLComponents componentsWithURL:xmlrpcURL resolvingAgainstBaseURL:NO]; + if ([components.scheme isEqualToString:@"https"]) { + components.scheme = @"http"; + } else { + components.scheme = @"https"; + } + NSURL *alternateXmlrpcURL = components.URL; + NSArray *blogsWithNoAccount = [Blog selfHostedInContext:context]; + Blog *jetpackBlog = [[blogsWithNoAccount wp_filter:^BOOL(Blog *blogToTest) { + return [blogToTest.xmlrpc caseInsensitiveCompare:xmlrpcURL.absoluteString] == NSOrderedSame + || [blogToTest.xmlrpc caseInsensitiveCompare:alternateXmlrpcURL.absoluteString] == NSOrderedSame; + }] firstObject]; + + if (jetpackBlog) { + DDLogInfo(@"Migrating %@ to wp.com account %@", [jetpackBlog hostURL], account.username); + jetpackBlog.account = account; + } + + return jetpackBlog; +} + +- (id)remoteForBlog:(Blog *)blog +{ + id remote; + if ([blog supports:BlogFeatureWPComRESTAPI]) { + if (blog.wordPressComRestApi) { + remote = [[BlogServiceRemoteREST alloc] initWithWordPressComRestApi:blog.wordPressComRestApi siteID:blog.dotComID]; + } + } else if (blog.xmlrpcApi) { + remote = [[BlogServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password]; + } + + return remote; +} + +- (id)remoteForAccount:(WPAccount *)account +{ + if (account.wordPressComRestApi == nil) { + return nil; + } + + return [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:account.wordPressComRestApi]; +} + +#pragma mark - Completion handlers + +- (void)updateMultiAuthor:(NSArray *)users forBlog:(NSManagedObjectID *)blogObjectID completionHandler:(void (^)(void))completion +{ + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + NSError *error; + Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:&error]; + if (error) { + DDLogError(@"%@", error); + } + if (!blog) { + return; + } + + [self updateBlogAuthorsForBlog:blog withRemoteUsers:users inContext:context]; + + blog.isMultiAuthor = users.count > 1; + /// Search for a matching user ID + /// - wp.com hosted: blog.account.userID + /// - Jetpack: user.linkedUserID == blog.account.userID + /// - self hosted: user.username == blog.username + if (blog.account) { + if ([blog isHostedAtWPcom]) { + blog.userID = blog.account.userID; + } else { + for (RemoteUser *user in users) { + if ([user.linkedUserID isEqual:blog.account.userID]) { + blog.userID = user.userID; + break; + } + } + } + } else if (blog.username != nil) { + for (RemoteUser *user in users) { + if ([user.username isEqualToString:blog.username]) { + blog.userID = user.userID; + break; + } + } + } + } completion: completion onQueue:dispatch_get_main_queue()]; +} + +- (BlogDetailsHandler)blogDetailsHandlerWithBlogObjectID:(NSManagedObjectID *)blogObjectID + completionHandler:(void (^)(void))completion +{ + return ^void(RemoteBlog *remoteBlog) { + [[[JetpackCapabilitiesService alloc] init] syncWithBlogs:@[remoteBlog] success:^(NSArray *blogs) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; + if (blog) { + [self updateBlog:blog withRemoteBlog:blogs.firstObject]; + [self updatePromptSettingsFor:blogs.firstObject context:context]; + } + } completion:completion onQueue:dispatch_get_main_queue()]; + }]; + }; +} + +- (OptionsHandler)optionsHandlerWithBlogObjectID:(NSManagedObjectID *)blogObjectID + completionHandler:(void (^)(void))completion +{ + return ^void(NSDictionary *options) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; + if (!blog) { + return; + } + + blog.options = [NSDictionary dictionaryWithDictionary:options]; + + RemoteBlogSettings *remoteSettings = [RemoteBlogOptionsHelper remoteBlogSettingsFromXMLRPCDictionaryOptions:options]; + [self updateSettings:blog.settings withRemoteSettings:remoteSettings]; + + + // NOTE: `[blog version]` can return nil. If this happens `version` will be `0` + CGFloat version = [[blog version] floatValue]; + if (version > 0 && version < [WordPressMinimumVersion floatValue]) { + if (blog.lastUpdateWarning == nil + || [blog.lastUpdateWarning floatValue] < [WordPressMinimumVersion floatValue]) + { + dispatch_async(dispatch_get_main_queue(), ^{ + // TODO :: Remove UI call from service layer + [WPError showAlertWithTitle:NSLocalizedString(@"WordPress version too old", @"") + message:[NSString stringWithFormat:NSLocalizedString(@"The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@", @""), [blog hostname], [blog version], WordPressMinimumVersion]]; + }); + blog.lastUpdateWarning = WordPressMinimumVersion; + } + } + } completion:completion onQueue:dispatch_get_main_queue()]; + }; +} + +- (PostFormatsHandler)postFormatsHandlerWithBlogObjectID:(NSManagedObjectID *)blogObjectID + completionHandler:(void (^)(void))completion +{ + return ^void(NSDictionary *postFormats) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; + if (blog) { + NSDictionary *formats = postFormats; + if (![formats objectForKey:PostFormatStandard]) { + NSMutableDictionary *mutablePostFormats = [formats mutableCopy]; + mutablePostFormats[PostFormatStandard] = NSLocalizedString(@"Standard", @"Standard post format label"); + formats = [NSDictionary dictionaryWithDictionary:mutablePostFormats]; + } + blog.postFormats = formats; + } + } completion:completion onQueue:dispatch_get_main_queue()]; + }; +} + +- (void)updateSettings:(BlogSettings *)settings withRemoteSettings:(RemoteBlogSettings *)remoteSettings +{ + NSParameterAssert(settings); + NSParameterAssert(remoteSettings); + + // Transformables + NSSet *separatedBlocklistKeys = [remoteSettings.commentsBlocklistKeys uniqueStringComponentsSeparatedByNewline]; + NSSet *separatedModerationKeys = [remoteSettings.commentsModerationKeys uniqueStringComponentsSeparatedByNewline]; + + // General + settings.name = remoteSettings.name; + settings.tagline = remoteSettings.tagline; + settings.privacy = remoteSettings.privacy ?: settings.privacy; + settings.languageID = remoteSettings.languageID ?: settings.languageID; + settings.iconMediaID = remoteSettings.iconMediaID; + settings.gmtOffset = remoteSettings.gmtOffset; + settings.timezoneString = remoteSettings.timezoneString; + + // Writing + settings.defaultCategoryID = remoteSettings.defaultCategoryID ?: settings.defaultCategoryID; + settings.defaultPostFormat = remoteSettings.defaultPostFormat ?: settings.defaultPostFormat; + settings.dateFormat = remoteSettings.dateFormat; + settings.timeFormat = remoteSettings.timeFormat; + settings.startOfWeek = remoteSettings.startOfWeek; + settings.postsPerPage = remoteSettings.postsPerPage; + + // Discussion + settings.commentsAllowed = [remoteSettings.commentsAllowed boolValue]; + settings.commentsBlocklistKeys = separatedBlocklistKeys; + settings.commentsCloseAutomatically = [remoteSettings.commentsCloseAutomatically boolValue]; + settings.commentsCloseAutomaticallyAfterDays = remoteSettings.commentsCloseAutomaticallyAfterDays; + settings.commentsFromKnownUsersAllowlisted = [remoteSettings.commentsFromKnownUsersAllowlisted boolValue]; + + settings.commentsMaximumLinks = remoteSettings.commentsMaximumLinks; + settings.commentsModerationKeys = separatedModerationKeys; + + settings.commentsPagingEnabled = [remoteSettings.commentsPagingEnabled boolValue]; + settings.commentsPageSize = remoteSettings.commentsPageSize; + + settings.commentsRequireManualModeration = [remoteSettings.commentsRequireManualModeration boolValue]; + settings.commentsRequireNameAndEmail = [remoteSettings.commentsRequireNameAndEmail boolValue]; + settings.commentsRequireRegistration = [remoteSettings.commentsRequireRegistration boolValue]; + + settings.commentsSortOrderAscending = remoteSettings.commentsSortOrderAscending; + + settings.commentsThreadingDepth = remoteSettings.commentsThreadingDepth; + settings.commentsThreadingEnabled = [remoteSettings.commentsThreadingEnabled boolValue]; + + settings.pingbackInboundEnabled = [remoteSettings.pingbackInboundEnabled boolValue]; + settings.pingbackOutboundEnabled = [remoteSettings.pingbackOutboundEnabled boolValue]; + + // Related Posts + settings.relatedPostsAllowed = [remoteSettings.relatedPostsAllowed boolValue]; + settings.relatedPostsEnabled = [remoteSettings.relatedPostsEnabled boolValue]; + settings.relatedPostsShowHeadline = [remoteSettings.relatedPostsShowHeadline boolValue]; + settings.relatedPostsShowThumbnails = [remoteSettings.relatedPostsShowThumbnails boolValue]; + + // AMP + settings.ampSupported = [remoteSettings.ampSupported boolValue]; + settings.ampEnabled = [remoteSettings.ampEnabled boolValue]; + + // Sharing + settings.sharingButtonStyle = remoteSettings.sharingButtonStyle; + settings.sharingLabel = remoteSettings.sharingLabel; + settings.sharingTwitterName = remoteSettings.sharingTwitterName; + settings.sharingCommentLikesEnabled = [remoteSettings.sharingCommentLikesEnabled boolValue]; + settings.sharingDisabledLikes = [remoteSettings.sharingDisabledLikes boolValue]; + settings.sharingDisabledReblogs = [remoteSettings.sharingDisabledReblogs boolValue]; +} + +- (RemoteBlogSettings *)remoteSettingFromSettings:(BlogSettings *)settings +{ + NSParameterAssert(settings); + RemoteBlogSettings *remoteSettings = [RemoteBlogSettings new]; + + // Transformables + NSString *joinedBlocklistKeys = [[settings.commentsBlocklistKeys allObjects] componentsJoinedByString:@"\n"]; + NSString *joinedModerationKeys = [[settings.commentsModerationKeys allObjects] componentsJoinedByString:@"\n"]; + + // General + remoteSettings.name = settings.name; + remoteSettings.tagline = settings.tagline; + remoteSettings.privacy = settings.privacy; + remoteSettings.languageID = settings.languageID; + remoteSettings.iconMediaID = settings.iconMediaID; + remoteSettings.gmtOffset = settings.gmtOffset; + remoteSettings.timezoneString = settings.timezoneString; + + // Writing + remoteSettings.defaultCategoryID = settings.defaultCategoryID; + remoteSettings.defaultPostFormat = settings.defaultPostFormat; + remoteSettings.dateFormat = settings.dateFormat; + remoteSettings.timeFormat = settings.timeFormat; + remoteSettings.startOfWeek = settings.startOfWeek; + remoteSettings.postsPerPage = settings.postsPerPage; + + // Discussion + remoteSettings.commentsAllowed = @(settings.commentsAllowed); + remoteSettings.commentsBlocklistKeys = joinedBlocklistKeys; + remoteSettings.commentsCloseAutomatically = @(settings.commentsCloseAutomatically); + remoteSettings.commentsCloseAutomaticallyAfterDays = settings.commentsCloseAutomaticallyAfterDays; + remoteSettings.commentsFromKnownUsersAllowlisted = @(settings.commentsFromKnownUsersAllowlisted); + + remoteSettings.commentsMaximumLinks = settings.commentsMaximumLinks; + remoteSettings.commentsModerationKeys = joinedModerationKeys; + + remoteSettings.commentsPagingEnabled = @(settings.commentsPagingEnabled); + remoteSettings.commentsPageSize = settings.commentsPageSize; + + remoteSettings.commentsRequireManualModeration = @(settings.commentsRequireManualModeration); + remoteSettings.commentsRequireNameAndEmail = @(settings.commentsRequireNameAndEmail); + remoteSettings.commentsRequireRegistration = @(settings.commentsRequireRegistration); + + remoteSettings.commentsSortOrderAscending = settings.commentsSortOrderAscending; + + remoteSettings.commentsThreadingDepth = settings.commentsThreadingDepth; + remoteSettings.commentsThreadingEnabled = @(settings.commentsThreadingEnabled); + + remoteSettings.pingbackInboundEnabled = @(settings.pingbackInboundEnabled); + remoteSettings.pingbackOutboundEnabled = @(settings.pingbackOutboundEnabled); + + // AMP + remoteSettings.ampEnabled = @(settings.ampEnabled); + + // Related Posts + remoteSettings.relatedPostsAllowed = @(settings.relatedPostsAllowed); + remoteSettings.relatedPostsEnabled = @(settings.relatedPostsEnabled); + remoteSettings.relatedPostsShowHeadline = @(settings.relatedPostsShowHeadline); + remoteSettings.relatedPostsShowThumbnails = @(settings.relatedPostsShowThumbnails); + + // Sharing + remoteSettings.sharingButtonStyle = settings.sharingButtonStyle; + remoteSettings.sharingLabel = settings.sharingLabel; + remoteSettings.sharingTwitterName = settings.sharingTwitterName; + remoteSettings.sharingCommentLikesEnabled = @(settings.sharingCommentLikesEnabled); + remoteSettings.sharingDisabledLikes = @(settings.sharingDisabledLikes); + remoteSettings.sharingDisabledReblogs = @(settings.sharingDisabledReblogs); + + return remoteSettings; +} + +@end diff --git a/Modules/Sources/WordPressDataObjC/Models/BasePost.m b/Modules/Sources/WordPressDataObjC/Models/BasePost.m new file mode 100644 index 000000000000..26bc21d8d08b --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/Models/BasePost.m @@ -0,0 +1,102 @@ +#import "BasePost.h" +#import "Media.h" +@import WordPressDataObjC; + +@import WordPressShared; + +@implementation BasePost + +@dynamic authorID; +@dynamic author; +@dynamic authorAvatarURL; +@dynamic content; +@dynamic date_created_gmt; +@dynamic postID; +@dynamic postTitle; +@dynamic password; +@dynamic remoteStatusNumber; +@dynamic permaLink; +@dynamic mt_excerpt; +@dynamic wp_slug; +@dynamic suggested_slug; +@dynamic pathForDisplayImage; + +- (NSDate *)dateCreated +{ + return self.date_created_gmt; +} + +- (void)setDateCreated:(NSDate *)localDate +{ + self.date_created_gmt = localDate; +} + +#pragma mark - PostContentProvider protocol + +- (NSString *)titleForDisplay +{ + NSString *title = [self.postTitle stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if (title == nil || ([title length] == 0)) { + title = NSLocalizedString(@"(no title)", @""); + } + return [title stringByDecodingXMLCharacters]; +} + +- (NSString *)authorForDisplay +{ + return [self.author stringByDecodingXMLCharacters]; +} + +- (NSString *)blogNameForDisplay +{ + return @""; +} + +- (NSString *)contentForDisplay +{ + return self.content; +} + +- (NSString *)contentPreviewForDisplay +{ + return self.content; +} + +- (NSString *)gravatarEmailForDisplay +{ + return nil; +} + +- (NSURL *)avatarURLForDisplay +{ + return nil; +} + +- (NSDate *)dateForDisplay +{ + return [self dateCreated]; +} + +- (NSString *)slugForDisplay +{ + if (self.wp_slug.length > 0) { + return self.wp_slug; + } + return self.suggested_slug; +} + +- (BOOL)hasContent +{ + BOOL titleIsEmpty = self.postTitle ? self.postTitle.isEmpty : YES; + BOOL contentIsEmpty = [self isContentEmpty]; + + return !titleIsEmpty || !contentIsEmpty; +} + +- (BOOL)isContentEmpty +{ + BOOL isContentAnEmptyGBParagraph = [self.content isEqualToString:@"\n

\n"]; + return self.content ? (self.content.isEmpty || isContentAnEmptyGBParagraph) : YES; +} + +@end diff --git a/Modules/Sources/WordPressDataObjC/Models/Blog.m b/Modules/Sources/WordPressDataObjC/Models/Blog.m index c33b8986cd5d..0ea6f5e7e0ab 100644 --- a/Modules/Sources/WordPressDataObjC/Models/Blog.m +++ b/Modules/Sources/WordPressDataObjC/Models/Blog.m @@ -2,6 +2,7 @@ #import "WPAccount.h" #import "AccountService.h" @import WordPressDataObjC; +// FIXME: These imports break the compilation and we've reached the point where they're so many it's time to address them, or at least try... #import "Constants.h" #import "WPUserAgent.h" #import "WordPress-Swift.h" diff --git a/Modules/Sources/WordPressDataObjC/include/AbstractPost.h b/Modules/Sources/WordPressDataObjC/include/AbstractPost.h new file mode 100644 index 000000000000..41ed30bc1b4d --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/AbstractPost.h @@ -0,0 +1,142 @@ +#import +#import +#import "BasePost.h" + +NS_ASSUME_NONNULL_BEGIN + +@class Media; +@class Comment; + +typedef NS_ENUM(NSUInteger, AbstractPostRemoteStatus) { + AbstractPostRemoteStatusPushing, // Uploading post + AbstractPostRemoteStatusFailed, // Upload failed + AbstractPostRemoteStatusLocal, // Only local version + AbstractPostRemoteStatusSync, // Post uploaded + AbstractPostRemoteStatusPushingMedia, // Push Media + AbstractPostRemoteStatusAutoSaved, // Post remote auto-saved + + // All the previous states were deprecated in 24.9 and are no longer used + // by the app. To get the status of the uploads, use `PostCoordinator`. + + /// The default state of the newly created local revision. + AbstractPostRemoteStatusLocalRevision, + /// The user saved the revision, and it needs to be uploaded to a server. + AbstractPostRemoteStatusSyncNeeded +}; + +@interface AbstractPost : BasePost + +// Relationships +@property (nonatomic, strong) Blog *blog; +@property (nonatomic, strong, nullable) NSDate * dateModified; +@property (nonatomic, strong) NSSet *media; +@property (weak, readonly) AbstractPost *original; +@property (weak, readonly) AbstractPost *revision; +@property (nonatomic, strong) NSSet *comments; +@property (nonatomic, strong, nullable) Media *featuredImage; + +/// This array will contain a list of revision IDs. +@property (nonatomic, strong, nullable) NSArray *revisions; +/// The default value of autoUploadAttemptsCount is 0. +@property (nonatomic, strong, nonnull) NSNumber *autoUploadAttemptsCount; + +/// Autosave attributes hold a snapshot of the post's content. +@property (nonatomic, copy, nullable) NSString *autosaveContent; +@property (nonatomic, copy, nullable) NSString *autosaveExcerpt; +@property (nonatomic, copy, nullable) NSString *autosaveTitle; +@property (nonatomic, copy, nullable) NSDate *autosaveModifiedDate; +@property (nonatomic, copy, nullable) NSNumber *autosaveIdentifier; + +/// Used to deduplicate new posts +@property (nonatomic, strong, nullable) NSUUID *foreignID; + +@property (nonatomic, strong, nullable) NSDate *confirmedChangesTimestamp; + +@property (nonatomic, strong, nullable) NSString *voiceContent; + +// Revision management +- (AbstractPost *)createRevision; +- (void)deleteRevision; +- (void)applyRevision; +- (AbstractPost *)updatePostFrom:(AbstractPost *)revision; +- (BOOL)isRevision; +- (BOOL)isOriginal; + +/// Returns the latest revision of a post. +/// +- (AbstractPost *)latest; +- (AbstractPost *)cloneFrom:(AbstractPost *)source; +- (BOOL)hasPhoto; +- (BOOL)hasVideo; +- (BOOL)hasCategories; +- (BOOL)hasTags; + +@property (nonatomic, assign, readonly) BOOL hasFailedMedia; + +/** + * @brief Call this method to know whether this post has a revision or not. + * + * @returns YES if this post has a revision, NO otherwise. + */ +- (BOOL)hasRevision; + +#pragma mark - Conveniece Methods +- (BOOL)shouldPublishImmediately; +- (NSString *)authorNameForDisplay; +- (NSString *)blavatarForDisplay; +- (NSString *)dateStringForDisplay; +- (BOOL)isMultiAuthorBlog; +- (BOOL)isPrivateAtWPCom; + + +#pragma mark - Unsaved Changes + +/** + Returns YES if the post is has a `future` post status + */ +- (BOOL)isScheduled; + +/** + Returns YES if the post is a draft + */ +- (BOOL)isDraft; + +/** + Returns YES if the post is a published. + */ +- (BOOL)isPublished; + +/** + Returns YES if the original post is a draft + */ +- (BOOL)originalIsDraft; + +// Does the post exist on the blog? +- (BOOL)hasRemote; + +// Save changes to disk +- (void)save; + +/** + * Updates the path for the display image by looking at the post content and trying to find an good image to use. + * If no appropiated image is found the path is set to nil. + */ +- (void)updatePathForDisplayImageBasedOnContent; + +@end + +@interface AbstractPost (CoreDataGeneratedAccessors) + +- (void)addMediaObject:(Media *)value; +- (void)removeMediaObject:(Media *)value; +- (void)addMedia:(NSSet *)values; +- (void)removeMedia:(NSSet *)values; + +- (void)addCommentsObject:(Comment *)value; +- (void)removeCommentsObject:(Comment *)value; +- (void)addComments:(NSSet *)values; +- (void)removeComments:(NSSet *)values; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Modules/Sources/WordPressDataObjC/include/AccountService.h b/Modules/Sources/WordPressDataObjC/include/AccountService.h new file mode 100644 index 000000000000..a739504de786 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/AccountService.h @@ -0,0 +1,133 @@ +#import +@import WordPressDataObjC; + +NS_ASSUME_NONNULL_BEGIN + +@class WPAccount; +@class RemoteUser; + +extern NSString *const WPAccountDefaultWordPressComAccountChangedNotification; +extern NSNotificationName const WPAccountEmailAndDefaultBlogUpdatedNotification; + +@interface AccountService : CoreDataService + +///------------------------------------ +/// @name Default WordPress.com account +///------------------------------------ + +/** + Sets the default WordPress.com account + + @param account the account to set as default for WordPress.com + @see defaultWordPressComAccount + @see removeDefaultWordPressComAccount + */ +- (void)setDefaultWordPressComAccount:(WPAccount *)account; + +/** + Removes the default WordPress.com account. Should only be called from the Main Thread + + @see defaultWordPressComAccount + @see setDefaultWordPressComAccount: + */ +- (void)removeDefaultWordPressComAccount; + +/** + Query to check if an email address is paired to a wpcom account. Used in the + magic links signup flow. + + @param email + @param success + @param failure + */ +- (void)isEmailAvailable:(NSString *)email success:(void (^)(BOOL available))success failure:(void (^)(NSError *error))failure; + +/** + Query to check if a username is available. Used in the signup flow. + + @param email + @param success + @param failure + */ +- (void)isUsernameAvailable:(NSString *)username + success:(void (^)(BOOL available))success + failure:(void (^)(NSError *error))failure; + +/** + Requests a verification email to be sent to the email address associated with the current account. + + @param success + @param failure + */ +- (void)requestVerificationEmail:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + + + +///----------------------- +/// @name Account creation +///----------------------- + +/** + Creates a new WordPress.com account or updates the password if there is a matching account + + There can only be one WordPress.com account per username, so if one already exists for the given `username` its password is updated + + Uses a background managed object context. + + @param username the WordPress.com account's username + @param authToken the OAuth2 token returned by signIntoWordPressDotComWithUsername:authToken: + @return The ID of the WordPress.com `WPAccount` object for the given `username` + */ +- (NSManagedObjectID *)createOrUpdateAccountWithUsername:(NSString *)username authToken:(NSString *)authToken; + +/** + Updates user details including username, email, userID, avatarURL, and default blog. + + @param account WPAccount to be updated + */ +- (void)updateUserDetailsForAccount:(WPAccount *)account + success:(nullable void (^)(void))success + failure:(nullable void (^)(NSError *error))failure; + +/** + Updates the default blog for the specified account. The default blog will be the one whose siteID matches + the accounts primaryBlogID. + */ +- (void)updateDefaultBlogIfNeeded:(WPAccount *)account inContext:(NSManagedObjectContext *)context; + +/** + Syncs the details for the account associated with the provided auth token, then + creates or updates a WPAccount with the synced information. + + @param authToken The auth token associated with the account being created/updated. + @param success A success block. + @param failure A failure block. + */ +- (void)createOrUpdateAccountWithAuthToken:(NSString *)authToken + success:(void (^)(WPAccount * _Nonnull))success + failure:(void (^)(NSError * _Nonnull))failure; + +- (NSManagedObjectID *)createOrUpdateAccountWithUserDetails:(RemoteUser *)remoteUser authToken:(NSString *)authToken; + +/** + Initializes the WordPress iOS Extensions with the WordPress.com Default Account. + */ +- (void)setupAppExtensionsWithDefaultAccount; + + +/** + Removes an account if it's not the default account and there are no associated blogs + */ +- (void)purgeAccountIfUnused:(WPAccount *)account; + +/** + Restores a disassociated default WordPress.com account if the current defaultWordPressCom account is nil + and another candidate account is found. This method bypasses the normal setter to avoid triggering unintended + side-effects from dispatching account changed notifications. + */ +- (void)restoreDisassociatedAccountIfNecessary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Modules/Sources/WordPressDataObjC/include/BlogService.h b/Modules/Sources/WordPressDataObjC/include/BlogService.h new file mode 100644 index 000000000000..c98741e5874b --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/BlogService.h @@ -0,0 +1,120 @@ +#import +#import "Blog.h" +@import WordPressDataObjC; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const WordPressMinimumVersion; +extern NSString *const WPBlogUpdatedNotification; +extern NSString *const WPBlogSettingsUpdatedNotification; + +@class WPAccount; +@class SiteInfo; + +@interface BlogService : CoreDataService + +/** + * Sync all available blogs for an acccount + * + * @param account the account for the associated blogs. + * @param success a block that is invoked when the sync is successful. + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncBlogsForAccount:(WPAccount *)account + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +/** + * Sync the blog and its top-level details such as the 'options' data and any jetpack configuration. + * + * @param blog the blog from where to read the information from + * @param success a block that is invoked when the sync is successful. + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +/** + * Sync the blog and all available metadata or configuration. Such as top-level details, postTypes, postFormats, categories, multi-author and jetpack configuration. + * + * @note Used for instances where the entire blog should be refreshed or initially downloaded. + * + * @param blog the blog from where to read the information from + * @param success a block that is invoked when the sync is successful. + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncBlogAndAllMetadata:(Blog *)blog + completionHandler:(void (^)(void))completionHandler; + +/** + * Sync the available postTypes configured for the blog. + * + * @param blog the blog from where to read the information from + * @param success a block that is invoked when the sync is successful. + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncPostTypesForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +/** + * Sync the available postFormats configured for the blog. + * + * @param blog the blog from where to read the information from + * @param success a block that is invoked when the sync is successful. + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncPostFormatsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +/** + * Sync blog settings from the server + * + * @param blog the blog from where to read the information from + * @param success a block that is invoked when the sync is successful + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncSettingsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +/** + * Sync authors from the server + * + * @param blog the blog from where to read the information from + * @param success a block that is invoked when the sync is successful + * @param failure a block that in invoked when the sync fails. + */ +- (void)syncAuthorsForBlog:(Blog *)blog + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +/** + * Update blog settings to the server + * + * @param blog the blog to update + * @param success a block that is invoked when the update is successful + * @param failure a block that in invoked when the update fails. + */ +- (void)updateSettingsForBlog:(Blog *)blog + success:(nullable void (^)(void))success + failure:(nullable void (^)(NSError *error))failure; + +/** + * Associate synced blogs to the specified Jetpack account. + * + * @param account the account + * @param success a block that is invoked when the update is successful + * @param failure a block that in invoked when the update fails. + */ +- (void)associateSyncedBlogsToJetpackAccount:(WPAccount *)account + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure; + +- (void)removeBlog:(Blog *)blog; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Modules/Sources/WordPressDataObjC/include/PostContentProvider.h b/Modules/Sources/WordPressDataObjC/include/PostContentProvider.h new file mode 100644 index 000000000000..a3d68ca2e442 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/PostContentProvider.h @@ -0,0 +1,17 @@ +#import + +@protocol PostContentProvider +- (NSString *)titleForDisplay; +- (NSString *)authorForDisplay; +- (NSString *)contentForDisplay; +- (NSString *)contentPreviewForDisplay; +- (NSURL *)avatarURLForDisplay; // Some providers use a hardcoded URL or blavatar URL +- (NSString *)gravatarEmailForDisplay; +- (NSDate *)dateForDisplay; +@optional +- (NSString *)blogNameForDisplay; +- (NSURL *)featuredImageURLForDisplay; +- (NSURL *)authorURL; +- (NSString *)slugForDisplay; +- (NSArray *)tagsForDisplay; +@end From 7275ae34808767830271e254ac208d2f2b1c377b Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 18:19:08 +1100 Subject: [PATCH 17/25] Make `entityName()` open to see if compilation error goes away... --- Modules/Sources/WordPressData/Extensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Sources/WordPressData/Extensions.swift b/Modules/Sources/WordPressData/Extensions.swift index b8675d655960..ca46ce6e1c7f 100644 --- a/Modules/Sources/WordPressData/Extensions.swift +++ b/Modules/Sources/WordPressData/Extensions.swift @@ -12,7 +12,7 @@ extension NSManagedObject { /// /// Note: entity().name returns nil as per iOS 10, in Unit Testing Targets. Awesome. /// - @objc public class func entityName() -> String { + @objc open class func entityName() -> String { return entity().name ?? classNameWithoutNamespaces() } From b0517a3b3fb6e9e584d1462a36c2f25c70f49fc2 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Mar 2025 21:02:31 +1100 Subject: [PATCH 18/25] Delete leftover versions of some of the files moved --- WordPress/Classes/Models/Media.h | 119 --- WordPress/Classes/Models/Media.m | 223 ----- WordPress/Classes/Models/PostCategory.h | 29 - WordPress/Classes/Models/PostCategory.m | 20 - WordPress/Classes/Services/AccountService.h | 133 --- WordPress/Classes/Services/AccountService.m | 452 ----------- WordPress/Classes/Services/BlogService.h | 120 --- WordPress/Classes/Services/BlogService.m | 848 -------------------- 8 files changed, 1944 deletions(-) delete mode 100644 WordPress/Classes/Models/Media.h delete mode 100644 WordPress/Classes/Models/Media.m delete mode 100644 WordPress/Classes/Models/PostCategory.h delete mode 100644 WordPress/Classes/Models/PostCategory.m delete mode 100644 WordPress/Classes/Services/AccountService.h delete mode 100644 WordPress/Classes/Services/AccountService.m delete mode 100644 WordPress/Classes/Services/BlogService.h delete mode 100644 WordPress/Classes/Services/BlogService.m diff --git a/WordPress/Classes/Models/Media.h b/WordPress/Classes/Models/Media.h deleted file mode 100644 index 79f71ba680da..000000000000 --- a/WordPress/Classes/Models/Media.h +++ /dev/null @@ -1,119 +0,0 @@ -#import -#import "Blog.h" -#import "AbstractPost.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, MediaRemoteStatus) { - MediaRemoteStatusSync, /* Post synced. */ - MediaRemoteStatusFailed, /* Upload failed. */ - MediaRemoteStatusLocal, /* Only local version. */ - MediaRemoteStatusPushing, /* Uploading post. */ - MediaRemoteStatusProcessing, /* Intermediate status before uploading. */ - MediaRemoteStatusStub, /* We only have the mediaID information from the server */ -}; - -typedef NS_ENUM(NSUInteger, MediaType) { - MediaTypeImage, - MediaTypeVideo, - MediaTypeDocument, - MediaTypePowerpoint, - MediaTypeAudio -}; - -@interface Media : NSManagedObject - -// Managed properties - -@property (nonatomic, strong, nullable) NSString *alt; -@property (nonatomic, strong, nullable) NSString *caption; -@property (nonatomic, strong, nullable) NSDate *creationDate; -@property (nonatomic, strong, nullable) NSString *desc; -@property (nonatomic, strong, nullable) NSString *filename; -@property (nonatomic, strong, nullable) NSNumber *filesize; -@property (nonatomic, strong, nullable) NSNumber *height; -@property (nonatomic, strong, nullable) NSNumber *length; -@property (nonatomic, strong, nullable) NSString *localThumbnailIdentifier; -@property (nonatomic, strong, nullable) NSString *localThumbnailURL; -@property (nonatomic, strong, nullable) NSString *localURL; -@property (nonatomic, strong, nullable) NSNumber *mediaID; -@property (nonatomic, strong, nullable) NSString *mediaTypeString; -@property (nonatomic, strong, nullable) NSNumber *postID; -@property (nonatomic, strong, nullable) NSNumber *remoteStatusNumber; -@property (nonatomic, strong, nullable) NSString *remoteThumbnailURL; -@property (nonatomic, strong, nullable) NSString *remoteURL; -@property (nonatomic, strong, nullable) NSString *remoteLargeURL; -@property (nonatomic, strong, nullable) NSString *remoteMediumURL; -@property (nonatomic, strong, nullable) NSString *shortcode; -@property (nonatomic, strong, nullable) NSString *title; -@property (nonatomic, strong, nullable) NSString *videopressGUID; -@property (nonatomic, strong, nullable) NSNumber *width; -@property (nonatomic, strong, nullable) NSError *error; -@property (nonatomic, strong, nonnull) NSNumber *autoUploadFailureCount; - -// Relationships - -@property (nonatomic, strong) Blog *blog; -@property (nonatomic, strong, nullable) NSSet *posts; -@property (nonatomic, strong, nullable) NSSet *featuredOnPosts; - -// Helper properties - -@property (nonatomic, assign) MediaType mediaType; -@property (nonatomic, assign) MediaRemoteStatus remoteStatus; - -/** - Local file URL for the Media's asset. e.g. an image, video, gif or other file. - */ -@property (nonatomic, strong, nullable) NSURL *absoluteLocalURL; - -/** - Local file URL for a preprocessed **large** thumbnail that can be used for - a full-screen presentation. - - - warning: Deprecated! Use ``MediaImageService`` to access the thumbnails. - */ -@property (nonatomic, strong, nullable) NSURL *absoluteThumbnailLocalURL; - -/// Returns true if the media object already exists on the server -@property (nonatomic, readonly) BOOL hasRemote; - -// Helper methods - -+ (NSString *)stringFromMediaType:(MediaType)mediaType; - -- (nullable NSString *)fileExtension; - -// CoreData helpers - -- (void)remove; -- (void)save; - -@end - -@class AbstractPost; - -@interface Media (CoreDataGeneratedAccessors) - -- (void)addPostsObject:(AbstractPost *)value; -- (void)removePostsObject:(AbstractPost *)value; -- (void)addPosts:(NSSet *)values; -- (void)removePosts:(NSSet *)values; - -- (void)addFeaturedOnPostsObject:(AbstractPost *)value; -- (void)removeFeaturedOnPostsObject:(AbstractPost *)value; -- (void)addFeaturedOnPosts:(NSSet *)values; -- (void)removeFeaturedOnPosts:(NSSet *)values; - -@end - -// TODO: Remove it; it was added for compatibility during the WPMediaPicker removal -typedef NS_OPTIONS(NSInteger, WPMediaType){ - WPMediaTypeImage = 1, - WPMediaTypeVideo = 1 << 1, - WPMediaTypeAudio = 1 << 2, - WPMediaTypeOther = 1 << 3, - WPMediaTypeAll= 0XFF -}; - -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Models/Media.m b/WordPress/Classes/Models/Media.m deleted file mode 100644 index 0a300b4fdcc6..000000000000 --- a/WordPress/Classes/Models/Media.m +++ /dev/null @@ -1,223 +0,0 @@ -#import "Media.h" -@import WordPressDataObjC; -#import "WordPress-Swift.h" - -@implementation Media - -@dynamic alt; -@dynamic mediaID; -@dynamic remoteURL; -@dynamic remoteLargeURL; -@dynamic remoteMediumURL; -@dynamic localURL; -@dynamic shortcode; -@dynamic width; -@dynamic length; -@dynamic title; -@dynamic height; -@dynamic filename; -@dynamic filesize; -@dynamic creationDate; -@dynamic blog; -@dynamic posts; -@dynamic remoteStatusNumber; -@dynamic caption; -@dynamic desc; -@dynamic mediaTypeString; -@dynamic videopressGUID; -@dynamic localThumbnailIdentifier; -@dynamic localThumbnailURL; -@dynamic remoteThumbnailURL; -@dynamic postID; -@dynamic error; -@dynamic featuredOnPosts; -@dynamic autoUploadFailureCount; - -#pragma mark - - -+ (NSString *)stringFromMediaType:(MediaType)mediaType -{ - switch (mediaType) { - case MediaTypeImage: - return @"image"; - break; - case MediaTypeVideo: - return @"video"; - break; - case MediaTypePowerpoint: - return @"powerpoint"; - break; - case MediaTypeDocument: - return @"document"; - break; - case MediaTypeAudio: - return @"audio"; - break; - } -} - -#pragma mark - - -- (NSString *)fileExtension -{ - NSString *extension = [self.filename pathExtension]; - if (extension.length) { - return extension; - } - extension = [self.localURL pathExtension]; - if (extension.length) { - return extension; - } - extension = [self.remoteURL pathExtension]; - return extension; -} - -#pragma mark - Media Types - -- (MediaType)mediaType -{ - if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeImage]]) { - return MediaTypeImage; - } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeVideo]]) { - return MediaTypeVideo; - } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypePowerpoint]]) { - return MediaTypePowerpoint; - } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeDocument]]) { - return MediaTypeDocument; - } else if ([self.mediaTypeString isEqualToString:[Media stringFromMediaType:MediaTypeAudio]]) { - return MediaTypeAudio; - } - - return MediaTypeDocument; -} - -- (void)setMediaType:(MediaType)mediaType -{ - self.mediaTypeString = [[self class] stringFromMediaType:mediaType]; -} - -#pragma mark - Remote Status - -- (MediaRemoteStatus)remoteStatus -{ - return (MediaRemoteStatus)[[self remoteStatusNumber] intValue]; -} - -- (void)setRemoteStatus:(MediaRemoteStatus)aStatus -{ - [self setRemoteStatusNumber:@(aStatus)]; -} - -- (NSString *)remoteStatusText -{ - switch (self.remoteStatus) { - case MediaRemoteStatusPushing: - return NSLocalizedString(@"Uploading", @"Status for Media object that is being uploaded."); - case MediaRemoteStatusFailed: - return NSLocalizedString(@"Failed", @"Status for Media object that is failed upload or export."); - case MediaRemoteStatusSync: - return NSLocalizedString(@"Uploaded", @"Status for Media object that is uploaded and sync with server."); - case MediaRemoteStatusProcessing: - return NSLocalizedString(@"Pending", @"Status for Media object that is being processed locally."); - case MediaRemoteStatusLocal: - return NSLocalizedString(@"Local", @"Status for Media object that is only exists locally."); - case MediaRemoteStatusStub: - return NSLocalizedString(@"Stub", @"Status for Media object that is only has the mediaID locally."); - } -} - -#pragma mark - Absolute URLs - -- (NSURL *)absoluteThumbnailLocalURL; -{ - if (!self.localThumbnailURL.length) { - return nil; - } - return [self absoluteURLForLocalPath:self.localThumbnailURL cacheDirectory:YES]; -} - -- (void)setAbsoluteThumbnailLocalURL:(NSURL *)absoluteLocalURL -{ - self.localThumbnailURL = absoluteLocalURL.lastPathComponent; -} - -- (NSURL *)absoluteLocalURL -{ - if (!self.localURL.length) { - return nil; - } - return [self absoluteURLForLocalPath:self.localURL cacheDirectory:NO]; -} - -- (void)setAbsoluteLocalURL:(NSURL *)absoluteLocalURL -{ - self.localURL = absoluteLocalURL.lastPathComponent; -} - -- (NSURL *)absoluteURLForLocalPath:(NSString *)localPath cacheDirectory:(BOOL)cacheDirectory -{ - NSError *error; - NSURL *mediaDirectory = nil; - if (cacheDirectory) { - mediaDirectory = [[MediaFileManager cacheManager] directoryURLAndReturnError:&error]; - } else { - mediaDirectory = [MediaFileManager uploadsDirectoryURLAndReturnError:&error]; - } - if (error) { - DDLogInfo(@"Error resolving Media directory: %@", error); - return nil; - } - return [mediaDirectory URLByAppendingPathComponent:localPath.lastPathComponent]; -} - -#pragma mark - CoreData Helpers - -- (void)prepareForDeletion -{ - NSError *error = nil; - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *absolutePath = self.absoluteLocalURL.path; - if ([fileManager fileExistsAtPath:absolutePath] && - ![fileManager removeItemAtPath:absolutePath error:&error]) { - DDLogInfo(@"Error removing media files:%@", error); - } - NSString *absoluteThumbnailPath = self.absoluteThumbnailLocalURL.path; - if ([fileManager fileExistsAtPath:absoluteThumbnailPath] && - ![fileManager removeItemAtPath:absoluteThumbnailPath error:&error]) { - DDLogInfo(@"Error removing media files:%@", error); - } - [super prepareForDeletion]; -} - -- (void)remove -{ - [self.managedObjectContext performBlockAndWait:^{ - [self.managedObjectContext deleteObject:self]; - [[ContextManager sharedInstance] saveContextAndWait:self.managedObjectContext]; - }]; -} - -- (void)save -{ - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; -} - -- (BOOL)hasRemote { - return self.mediaID.intValue != 0; -} - -- (void)setError:(NSError *)error -{ - if (error != nil) { - // Cherry pick keys that support secure coding. NSErrors thrown from the OS can - // contain types that don't adopt NSSecureCoding, leading to a Core Data exception and crash. - NSDictionary *userInfo = @{NSLocalizedDescriptionKey: error.localizedDescription}; - error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; - } - - [self willChangeValueForKey:@"error"]; - [self setPrimitiveValue:error forKey:@"error"]; - [self didChangeValueForKey:@"error"]; -} - -@end diff --git a/WordPress/Classes/Models/PostCategory.h b/WordPress/Classes/Models/PostCategory.h deleted file mode 100644 index 00501aef5e9c..000000000000 --- a/WordPress/Classes/Models/PostCategory.h +++ /dev/null @@ -1,29 +0,0 @@ -#import -#import "Blog.h" - -extern NSString * const PostCategoryEntityName; -extern NSString * const PostCategoryNameKey; -extern const NSInteger PostCategoryUncategorized; - -@interface PostCategory : NSManagedObject - -@property (nonatomic, strong) NSNumber *categoryID; -@property (nonatomic, strong) NSString *categoryName; -@property (nonatomic, strong) NSNumber *parentID; -@property (nonatomic, strong) NSSet *posts; -@property (nonatomic, strong) Blog *blog; - -+ (NSString *)entityName; - -@end - -@class Post; - -@interface PostCategory (CoreDataGeneratedAccessors) - -- (void)addPostsObject:(Post *)value; -- (void)removePostsObject:(Post *)value; -- (void)addPosts:(NSSet *)values; -- (void)removePosts:(NSSet *)values; - -@end diff --git a/WordPress/Classes/Models/PostCategory.m b/WordPress/Classes/Models/PostCategory.m deleted file mode 100644 index 79511f7be128..000000000000 --- a/WordPress/Classes/Models/PostCategory.m +++ /dev/null @@ -1,20 +0,0 @@ -#import "PostCategory.h" - -NSString * const PostCategoryEntityName = @"Category"; -NSString * const PostCategoryNameKey = @"categoryName"; -const NSInteger PostCategoryUncategorized = 1; - -@implementation PostCategory - -@dynamic categoryID; -@dynamic categoryName; -@dynamic parentID; -@dynamic posts; -@dynamic blog; - -+ (NSString *)entityName -{ - return @"Category"; -} - -@end diff --git a/WordPress/Classes/Services/AccountService.h b/WordPress/Classes/Services/AccountService.h deleted file mode 100644 index a739504de786..000000000000 --- a/WordPress/Classes/Services/AccountService.h +++ /dev/null @@ -1,133 +0,0 @@ -#import -@import WordPressDataObjC; - -NS_ASSUME_NONNULL_BEGIN - -@class WPAccount; -@class RemoteUser; - -extern NSString *const WPAccountDefaultWordPressComAccountChangedNotification; -extern NSNotificationName const WPAccountEmailAndDefaultBlogUpdatedNotification; - -@interface AccountService : CoreDataService - -///------------------------------------ -/// @name Default WordPress.com account -///------------------------------------ - -/** - Sets the default WordPress.com account - - @param account the account to set as default for WordPress.com - @see defaultWordPressComAccount - @see removeDefaultWordPressComAccount - */ -- (void)setDefaultWordPressComAccount:(WPAccount *)account; - -/** - Removes the default WordPress.com account. Should only be called from the Main Thread - - @see defaultWordPressComAccount - @see setDefaultWordPressComAccount: - */ -- (void)removeDefaultWordPressComAccount; - -/** - Query to check if an email address is paired to a wpcom account. Used in the - magic links signup flow. - - @param email - @param success - @param failure - */ -- (void)isEmailAvailable:(NSString *)email success:(void (^)(BOOL available))success failure:(void (^)(NSError *error))failure; - -/** - Query to check if a username is available. Used in the signup flow. - - @param email - @param success - @param failure - */ -- (void)isUsernameAvailable:(NSString *)username - success:(void (^)(BOOL available))success - failure:(void (^)(NSError *error))failure; - -/** - Requests a verification email to be sent to the email address associated with the current account. - - @param success - @param failure - */ -- (void)requestVerificationEmail:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - - - -///----------------------- -/// @name Account creation -///----------------------- - -/** - Creates a new WordPress.com account or updates the password if there is a matching account - - There can only be one WordPress.com account per username, so if one already exists for the given `username` its password is updated - - Uses a background managed object context. - - @param username the WordPress.com account's username - @param authToken the OAuth2 token returned by signIntoWordPressDotComWithUsername:authToken: - @return The ID of the WordPress.com `WPAccount` object for the given `username` - */ -- (NSManagedObjectID *)createOrUpdateAccountWithUsername:(NSString *)username authToken:(NSString *)authToken; - -/** - Updates user details including username, email, userID, avatarURL, and default blog. - - @param account WPAccount to be updated - */ -- (void)updateUserDetailsForAccount:(WPAccount *)account - success:(nullable void (^)(void))success - failure:(nullable void (^)(NSError *error))failure; - -/** - Updates the default blog for the specified account. The default blog will be the one whose siteID matches - the accounts primaryBlogID. - */ -- (void)updateDefaultBlogIfNeeded:(WPAccount *)account inContext:(NSManagedObjectContext *)context; - -/** - Syncs the details for the account associated with the provided auth token, then - creates or updates a WPAccount with the synced information. - - @param authToken The auth token associated with the account being created/updated. - @param success A success block. - @param failure A failure block. - */ -- (void)createOrUpdateAccountWithAuthToken:(NSString *)authToken - success:(void (^)(WPAccount * _Nonnull))success - failure:(void (^)(NSError * _Nonnull))failure; - -- (NSManagedObjectID *)createOrUpdateAccountWithUserDetails:(RemoteUser *)remoteUser authToken:(NSString *)authToken; - -/** - Initializes the WordPress iOS Extensions with the WordPress.com Default Account. - */ -- (void)setupAppExtensionsWithDefaultAccount; - - -/** - Removes an account if it's not the default account and there are no associated blogs - */ -- (void)purgeAccountIfUnused:(WPAccount *)account; - -/** - Restores a disassociated default WordPress.com account if the current defaultWordPressCom account is nil - and another candidate account is found. This method bypasses the normal setter to avoid triggering unintended - side-effects from dispatching account changed notifications. - */ -- (void)restoreDisassociatedAccountIfNecessary; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Services/AccountService.m b/WordPress/Classes/Services/AccountService.m deleted file mode 100644 index 45f19e57fa45..000000000000 --- a/WordPress/Classes/Services/AccountService.m +++ /dev/null @@ -1,452 +0,0 @@ -#import "AccountService.h" -#import "WPAccount.h" -@import WordPressDataObjC; -#import "Blog.h" -#import "BlogService.h" - -@import WordPressKit; -@import WordPressShared; -#import "WordPress-Swift.h" - -static NSString * const DefaultDotcomAccountUUIDDefaultsKey = @"AccountDefaultDotcomUUID"; -static NSString * const DefaultDotcomAccountPasswordRemovedKey = @"DefaultDotcomAccountPasswordRemovedKey"; - -static NSString * const WordPressDotcomXMLRPCKey = @"https://wordpress.com/xmlrpc.php"; -NSNotificationName const WPAccountDefaultWordPressComAccountChangedNotification = @"WPAccountDefaultWordPressComAccountChangedNotification"; -NSString * const WPAccountEmailAndDefaultBlogUpdatedNotification = @"WPAccountEmailAndDefaultBlogUpdatedNotification"; - -@implementation AccountService - -///------------------------------------ -/// @name Default WordPress.com account -///------------------------------------ - -/** - Sets the default WordPress.com account - - @param account the account to set as default for WordPress.com - @see defaultWordPressComAccount - @see removeDefaultWordPressComAccount - */ -- (void)setDefaultWordPressComAccount:(WPAccount *)account -{ - NSParameterAssert(account != nil); - NSAssert(account.authToken.length > 0, @"Account should have an authToken for WP.com"); - - if ([account isDefaultWordPressComAccount]) { - return; - } - - [[UserPersistentStoreFactory userDefaultsInstance] setObject:account.uuid forKey:DefaultDotcomAccountUUIDDefaultsKey]; - - NSManagedObjectID *accountID = account.objectID; - void (^notifyAccountChange)(void) = ^{ - NSManagedObjectContext *mainContext = self.coreDataStack.mainContext; - NSManagedObject *accountInContext = [mainContext existingObjectWithID:accountID error:nil]; - [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:accountInContext]; - - [[PushNotificationsManager shared] setupRemoteNotifications]; - }; - if ([NSThread isMainThread]) { - // This is meant to help with testing account observers. - // Short version: dispatch_async and XCTest asynchronous helpers don't play nice with each other - // Long version: see the comment in https://github.com/wordpress-mobile/WordPress-iOS/blob/2f9a2100ca69d8f455acec47a1bbd6cbc5084546/WordPress/WordPressTest/AccountServiceRxTests.swift#L7 - notifyAccountChange(); - } else { - dispatch_async(dispatch_get_main_queue(), notifyAccountChange); - } -} - -/** - Removes the default WordPress.com account - - @see defaultWordPressComAccount - @see setDefaultWordPressComAccount: - */ -- (void)removeDefaultWordPressComAccount -{ - NSAssert([NSThread isMainThread], @"This method should only be called from the main thread"); - - [[PushNotificationsManager shared] unregisterDeviceToken]; - - WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; - if (account == nil) { - return; - } - - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - WPAccount *accountInContext = [context existingObjectWithID:account.objectID error:nil]; - [context deleteObject:accountInContext]; - }]; - - // Clear WordPress.com cookies - NSArray> *cookieJars = @[ - (id)[NSHTTPCookieStorage sharedHTTPCookieStorage], - (id)[[WKWebsiteDataStore defaultDataStore] httpCookieStore] - ]; - - for (id cookieJar in cookieJars) { - [cookieJar removeWordPressComCookiesWithCompletion:^{}]; - } - - [[NSURLCache sharedURLCache] removeAllCachedResponses]; - - // Remove defaults - [[UserPersistentStoreFactory userDefaultsInstance] removeObjectForKey:DefaultDotcomAccountUUIDDefaultsKey]; - - [WPAnalytics refreshMetadata]; - [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountDefaultWordPressComAccountChangedNotification object:nil]; - - [StatsCache clearCaches]; -} - -- (void)isEmailAvailable:(NSString *)email success:(void (^)(BOOL available))success failure:(void (^)(NSError *error))failure -{ - id remote = [self remoteForAnonymous]; - [remote isEmailAvailable:email success:^(BOOL available) { - if (success) { - success(available); - } - } failure:^(NSError *error) { - if (failure) { - failure(error); - } - }]; -} - -- (void)isUsernameAvailable:(NSString *)username - success:(void (^)(BOOL available))success - failure:(void (^)(NSError *error))failure -{ - id remote = [self remoteForAnonymous]; - [remote isUsernameAvailable:username success:^(BOOL available) { - if (success) { - success(available); - } - } failure:^(NSError *error) { - if (failure) { - failure(error); - } - }]; -} - -- (void)requestVerificationEmail:(void (^)(void))success failure:(void (^)(NSError * _Nonnull))failure -{ - NSAssert([NSThread isMainThread], @"This method should only be called from the main thread"); - - WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; - id remote = [self remoteForAccount:account]; - [remote requestVerificationEmailWithSucccess:^{ - if (success) { - success(); - } - } failure:^(NSError *error) { - if (failure) { - failure(error); - } - }]; -} - - -///----------------------- -/// @name Account creation -///----------------------- - -- (NSManagedObjectID *)createOrUpdateAccountWithUserDetails:(RemoteUser *)remoteUser authToken:(NSString *)authToken -{ - NSManagedObjectID * __block accountObjectID = nil; - [self.coreDataStack.mainContext performBlockAndWait:^{ - accountObjectID = [[WPAccount lookupWithUsername:remoteUser.username context:self.coreDataStack.mainContext] objectID]; - }]; - - if (accountObjectID) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - WPAccount *account = [context existingObjectWithID:accountObjectID error:nil]; - // Even if we find an account via its userID we should still update - // its authtoken, otherwise the Authenticator's authtoken fixer won't - // work. - account.authToken = authToken; - }]; - } else { - accountObjectID = [self createOrUpdateAccountWithUsername:remoteUser.username authToken:authToken]; - } - - [self updateAccountWithID:accountObjectID withUserDetails:remoteUser]; - - return accountObjectID; -} - -/** - Creates a new WordPress.com account or updates the password if there is a matching account - - There can only be one WordPress.com account per username, so if one already exists for the given `username` its password is updated - - Uses a background managed object context. - - @param username the WordPress.com account's username - @param authToken the OAuth2 token returned by signIntoWordPressDotComWithUsername:authToken: - @return The ID of the WordPress.com `WPAccount` object for the given `username` - @see createOrUpdateWordPressComAccountWithUsername:password:authToken: - */ -- (NSManagedObjectID *)createOrUpdateAccountWithUsername:(NSString *)username authToken:(NSString *)authToken -{ - NSManagedObjectID * __block objectID = nil; - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - WPAccount *account = [WPAccount lookupWithUsername:username context:context]; - if (!account) { - account = [NSEntityDescription insertNewObjectForEntityForName:@"Account" inManagedObjectContext:context]; - account.uuid = [[NSUUID new] UUIDString]; - account.username = username; - } - account.authToken = authToken; - [context obtainPermanentIDsForObjects:@[account] error:nil]; - objectID = account.objectID; - }]; - - [self.coreDataStack.mainContext performBlockAndWait:^{ - WPAccount *defaultAccount = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; - if (!defaultAccount) { - WPAccount *account = [self.coreDataStack.mainContext existingObjectWithID:objectID error:nil]; - [self setDefaultWordPressComAccount:account]; - dispatch_async(dispatch_get_main_queue(), ^{ - [WPAnalytics refreshMetadata]; - }); - } - }]; - - return objectID; -} - -/** - Checks an account to see if it is just used to connect to Jetpack. - - @param account The account to inspect. - @return True if used only for a Jetpack connection. - */ -- (BOOL)accountHasOnlyJetpackBlogs:(WPAccount *)account -{ - if ([account.blogs count] == 0) { - // Most likly, this is a blogless account used for the reader or commenting and not Jetpack. - return NO; - } - - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.isHostedAtWPcom = true"]; - NSSet *wpcomBlogs = [account.blogs filteredSetUsingPredicate:predicate]; - if ([wpcomBlogs count] > 0) { - return NO; - } - - return YES; -} - -- (void)restoreDisassociatedAccountIfNecessary -{ - NSAssert([NSThread isMainThread], @"This method should only be called from the main thread"); - - if([WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext] != nil) { - return; - } - - // Attempt to restore a default account that has somehow been disassociated. - WPAccount *account = [self findDefaultAccountCandidateFromAccounts:[WPAccount lookupAllAccountsInContext:self.coreDataStack.mainContext]]; - if (account) { - // Assume we have a good candidate account and make it the default account in the app. - // Note that this should be the account with the most blogs. - // Updates user defaults here vs the setter method to avoid potential side-effects from dispatched notifications. - [[UserPersistentStoreFactory userDefaultsInstance] setObject:account.uuid forKey:DefaultDotcomAccountUUIDDefaultsKey]; - } -} - -- (WPAccount *)findDefaultAccountCandidateFromAccounts:(NSArray *)allAccounts -{ - NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"blogs.@count" ascending:NO]; - NSArray *accounts = [allAccounts sortedArrayUsingDescriptors:@[sort]]; - - for (WPAccount *account in accounts) { - // Skip accounts that were likely added to Jetpack-connected self-hosted - // sites, while there was an existing default wpcom account. - if ([self accountHasOnlyJetpackBlogs:account]) { - continue; - } - return account; - } - return nil; -} - -- (void)createOrUpdateAccountWithAuthToken:(NSString *)authToken - success:(void (^)(WPAccount * _Nonnull))success - failure:(void (^)(NSError * _Nonnull))failure -{ - WordPressComRestApi *api = [WordPressComRestApi defaultApiWithOAuthToken:authToken userAgent:[WPUserAgent defaultUserAgent] localeKey:[WordPressComRestApi LocaleKeyDefault]]; - AccountServiceRemoteREST *remote = [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:api]; - [remote getAccountDetailsWithSuccess:^(RemoteUser *remoteUser) { - NSManagedObjectID *objectID = [self createOrUpdateAccountWithUserDetails:remoteUser authToken:authToken]; - WPAccount * __block account = nil; - [self.coreDataStack.mainContext performBlockAndWait:^{ - account = [self.coreDataStack.mainContext existingObjectWithID:objectID error:nil]; - }]; - success(account); - } failure:^(NSError *error) { - failure(error); - }]; -} - -- (void)updateUserDetailsForAccount:(WPAccount *)account - success:(nullable void (^)(void))success - failure:(nullable void (^)(NSError * _Nonnull))failure -{ - NSAssert(account, @"Account can not be nil"); - NSAssert(account.username, @"account.username can not be nil"); - - id remote = [self remoteForAccount:account]; - [remote getAccountDetailsWithSuccess:^(RemoteUser *remoteUser) { - // account.objectID can be temporary, so fetch via username/xmlrpc instead. - [self updateAccountWithID:account.objectID withUserDetails:remoteUser]; - dispatch_async(dispatch_get_main_queue(), ^{ - [WPAnalytics refreshMetadata]; - if (success) { - success(); - } - }); - } failure:^(NSError *error) { - DDLogError(@"Failed to fetch user details for account %@. %@", account, error); - if (failure) { - failure(error); - } - }]; -} - -- (id)remoteForAnonymous -{ - WordPressComRestApi *api = [WordPressComRestApi defaultApiWithOAuthToken:nil - userAgent:nil - localeKey:[WordPressComRestApi LocaleKeyDefault]]; - return [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:api]; -} - -- (id)remoteForAccount:(WPAccount *)account -{ - if (account.wordPressComRestApi == nil) { - return nil; - } - - return [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:account.wordPressComRestApi]; -} - -- (void)updateAccountWithID:(NSManagedObjectID *)objectID withUserDetails:(RemoteUser *)userDetails -{ - NSParameterAssert(![objectID isTemporaryID]); - - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - WPAccount *account = [context existingObjectWithID:objectID error:nil]; - account.userID = userDetails.userID; - account.username = userDetails.username; - account.email = userDetails.email; - account.avatarURL = userDetails.avatarURL; - account.displayName = userDetails.displayName; - account.dateCreated = userDetails.dateCreated; - account.emailVerified = @(userDetails.emailVerified); - account.primaryBlogID = userDetails.primaryBlogID; - }]; - - // Make sure the account is saved before updating its default blog. - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - WPAccount *account = [context existingObjectWithID:objectID error:nil]; - [self updateDefaultBlogIfNeeded:account inContext:context]; - [[NSNotificationCenter defaultCenter] postNotificationName:WPAccountEmailAndDefaultBlogUpdatedNotification object:nil]; - }]; -} - -- (void)updateDefaultBlogIfNeeded:(WPAccount *)account inContext:(NSManagedObjectContext *)context -{ - NSParameterAssert(account.managedObjectContext == context); - - if (!account.primaryBlogID || [account.primaryBlogID intValue] == 0) { - return; - } - - // Load the Default Blog - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"blogID = %@", account.primaryBlogID]; - Blog *defaultBlog = [[account.blogs filteredSetUsingPredicate:predicate] anyObject]; - - if (!defaultBlog) { - DDLogError(@"Error: The Default Blog could not be loaded"); - return; - } - - // Setup the Account - account.defaultBlog = defaultBlog; - - // Update app extensions if needed. - if ([account isDefaultWordPressComAccount]) { - [self setupAppExtensionsWithDefaultAccount:account inContext:context]; - } -} - -- (void)setupAppExtensionsWithDefaultAccount -{ - NSManagedObjectContext *context = self.coreDataStack.mainContext; - [context performBlockAndWait:^{ - WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:context]; - if (account == nil) { - return; - } - [self setupAppExtensionsWithDefaultAccount:account inContext:context]; - }]; -} - -- (void)setupAppExtensionsWithDefaultAccount:(WPAccount *)defaultAccount inContext:(NSManagedObjectContext *)context -{ - NSParameterAssert(defaultAccount.managedObjectContext == context); - - NSManagedObjectID *defaultAccountObjectID = defaultAccount.objectID; - Blog *defaultBlog = [defaultAccount defaultBlog]; - NSNumber *siteId = defaultBlog.dotComID; - NSString *blogName = defaultBlog.settings.name; - - if (defaultBlog == nil || defaultBlog.isDeleted) { - dispatch_async(dispatch_get_main_queue(), ^{ - [ShareExtensionService removeShareExtensionConfiguration]; - - [NotificationSupportService deleteServiceExtensionToken]; - }); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - WPAccount *defaultAccount = [self.coreDataStack.mainContext existingObjectWithID:defaultAccountObjectID error:nil]; - - [ShareExtensionService configureShareExtensionDefaultSiteID:siteId.integerValue defaultSiteName:blogName]; - [ShareExtensionService configureShareExtensionToken:defaultAccount.authToken]; - [ShareExtensionService configureShareExtensionUsername:defaultAccount.username]; - - [NotificationSupportService insertServiceExtensionToken:defaultAccount.authToken]; - [NotificationSupportService insertServiceExtensionUsername:defaultAccount.username]; - [NotificationSupportService insertServiceExtensionUserID:defaultAccount.userID.stringValue]; - }); - } -} - -- (void)purgeAccountIfUnused:(WPAccount *)account -{ - NSParameterAssert(account); - - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - WPAccount *accountInContext = [context existingObjectWithID:account.objectID error:nil]; - if (accountInContext == nil) { - return; - } - - BOOL purge = NO; - WPAccount *defaultAccount = [WPAccount lookupDefaultWordPressComAccountInContext:context]; - if ([accountInContext.blogs count] == 0 - && ![defaultAccount isEqual:accountInContext]) { - purge = YES; - } - - if (purge) { - DDLogWarn(@"Removing account since it has no blogs associated and it's not the default account: %@", accountInContext); - [context deleteObject:accountInContext]; - } - }]; -} - -@end diff --git a/WordPress/Classes/Services/BlogService.h b/WordPress/Classes/Services/BlogService.h deleted file mode 100644 index c98741e5874b..000000000000 --- a/WordPress/Classes/Services/BlogService.h +++ /dev/null @@ -1,120 +0,0 @@ -#import -#import "Blog.h" -@import WordPressDataObjC; - -NS_ASSUME_NONNULL_BEGIN - -extern NSString *const WordPressMinimumVersion; -extern NSString *const WPBlogUpdatedNotification; -extern NSString *const WPBlogSettingsUpdatedNotification; - -@class WPAccount; -@class SiteInfo; - -@interface BlogService : CoreDataService - -/** - * Sync all available blogs for an acccount - * - * @param account the account for the associated blogs. - * @param success a block that is invoked when the sync is successful. - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncBlogsForAccount:(WPAccount *)account - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -/** - * Sync the blog and its top-level details such as the 'options' data and any jetpack configuration. - * - * @param blog the blog from where to read the information from - * @param success a block that is invoked when the sync is successful. - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -/** - * Sync the blog and all available metadata or configuration. Such as top-level details, postTypes, postFormats, categories, multi-author and jetpack configuration. - * - * @note Used for instances where the entire blog should be refreshed or initially downloaded. - * - * @param blog the blog from where to read the information from - * @param success a block that is invoked when the sync is successful. - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncBlogAndAllMetadata:(Blog *)blog - completionHandler:(void (^)(void))completionHandler; - -/** - * Sync the available postTypes configured for the blog. - * - * @param blog the blog from where to read the information from - * @param success a block that is invoked when the sync is successful. - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncPostTypesForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -/** - * Sync the available postFormats configured for the blog. - * - * @param blog the blog from where to read the information from - * @param success a block that is invoked when the sync is successful. - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncPostFormatsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -/** - * Sync blog settings from the server - * - * @param blog the blog from where to read the information from - * @param success a block that is invoked when the sync is successful - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncSettingsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -/** - * Sync authors from the server - * - * @param blog the blog from where to read the information from - * @param success a block that is invoked when the sync is successful - * @param failure a block that in invoked when the sync fails. - */ -- (void)syncAuthorsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -/** - * Update blog settings to the server - * - * @param blog the blog to update - * @param success a block that is invoked when the update is successful - * @param failure a block that in invoked when the update fails. - */ -- (void)updateSettingsForBlog:(Blog *)blog - success:(nullable void (^)(void))success - failure:(nullable void (^)(NSError *error))failure; - -/** - * Associate synced blogs to the specified Jetpack account. - * - * @param account the account - * @param success a block that is invoked when the update is successful - * @param failure a block that in invoked when the update fails. - */ -- (void)associateSyncedBlogsToJetpackAccount:(WPAccount *)account - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - -- (void)removeBlog:(Blog *)blog; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Services/BlogService.m b/WordPress/Classes/Services/BlogService.m deleted file mode 100644 index e945ce1f2da7..000000000000 --- a/WordPress/Classes/Services/BlogService.m +++ /dev/null @@ -1,848 +0,0 @@ -#import "BlogService.h" -#import "Blog.h" -#import "WPAccount.h" -#import "AccountService.h" -@import WordPressDataObjC; -#import "WPError.h" -#import "Media.h" -#import "PostCategoryService.h" -#import "CommentService.h" -#import "PostService.h" -#import "WordPress-Swift.h" -#import "PostType.h" -@import WordPressKit; -@import WordPressShared; - -@class Comment; - -NSString *const WPComGetFeatures = @"wpcom.getFeatures"; -NSString *const VideopressEnabled = @"videopress_enabled"; -NSString *const WordPressMinimumVersion = @"4.0"; -NSString *const HttpsPrefix = @"https://"; -NSString *const WPBlogUpdatedNotification = @"WPBlogUpdatedNotification"; -NSString *const WPBlogSettingsUpdatedNotification = @"WPBlogSettingsUpdatedNotification"; - -@implementation BlogService - -- (void)syncBlogsForAccount:(WPAccount *)account - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - DDLogMethod(); - - id remote = [self remoteForAccount:account]; - - BOOL filterJetpackSites = false; - - [remote getBlogs:filterJetpackSites success:^(NSArray *blogs) { - [[[JetpackCapabilitiesService alloc] init] syncWithBlogs:blogs success:^(NSArray *blogs) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - [self mergeBlogs:blogs withAccountID:account.objectID inContext:context]; - } completion:success onQueue:dispatch_get_main_queue()]; - }]; - } failure:^(NSError *error) { - DDLogError(@"Error syncing blogs: %@", error); - - if (failure) { - failure(error); - } - }]; -} - -- (void)syncBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - id remote = [self remoteForBlog:blog]; - if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { - BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; - [xmlrpcRemote syncBlogOptionsWithSuccess:[self optionsHandlerWithBlogObjectID:blog.objectID - completionHandler:success] - failure:failure]; - } else if ([remote isKindOfClass:[BlogServiceRemoteREST class]]) { - BlogServiceRemoteREST *restRemote = remote; - [restRemote syncBlogWithSuccess:[self blogDetailsHandlerWithBlogObjectID:blog.objectID - completionHandler:success] - failure:failure]; - } -} - -- (void)syncBlogAndAllMetadata:(Blog *)blog completionHandler:(void (^)(void))completionHandler -{ - // Create a dispatch group. We'll use this to monitor completion of the various - // remote calls and to execute the completionHandler. - dispatch_group_t syncGroup = dispatch_group_create(); - - NSManagedObjectID *blogObjectID = blog.objectID; - id remote = [self remoteForBlog:blog]; - - if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { - dispatch_group_enter(syncGroup); - BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; - [xmlrpcRemote syncBlogOptionsWithSuccess:[self optionsHandlerWithBlogObjectID:blogObjectID - completionHandler:^{ - dispatch_group_leave(syncGroup); - }] - failure:^(NSError *error) { - DDLogError(@"Failed syncing options for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - } - - if ([remote isKindOfClass:[BlogServiceRemoteREST class]]) { - dispatch_group_enter(syncGroup); - BlogServiceRemoteREST *restRemote = remote; - [restRemote syncBlogWithSuccess:[self blogDetailsHandlerWithBlogObjectID:blogObjectID - completionHandler:^{ - dispatch_group_leave(syncGroup); - }] - failure:^(NSError *error) { - DDLogError(@"Failed syncing site details for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - dispatch_group_enter(syncGroup); - [restRemote syncBlogSettingsWithSuccess:^(RemoteBlogSettings *settings) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blogInContext = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; - if (blogInContext) { - [self updateSettings:blogInContext.settings withRemoteSettings:settings]; - } - } completion:^{ - dispatch_group_leave(syncGroup); - } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; - } failure:^(NSError *error) { - DDLogError(@"Failed syncing settings for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - } - - dispatch_group_enter(syncGroup); - [remote syncPostFormatsWithSuccess:[self postFormatsHandlerWithBlogObjectID:blogObjectID - completionHandler:^{ - dispatch_group_leave(syncGroup); - }] - failure:^(NSError *error) { - DDLogError(@"Failed syncing post formats for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - PostCategoryService *categoryService = [[PostCategoryService alloc] initWithCoreDataStack:self.coreDataStack]; - dispatch_group_enter(syncGroup); - [categoryService syncCategoriesForBlog:blog - success:^{ - dispatch_group_leave(syncGroup); - } - failure:^(NSError *error) { - DDLogError(@"Failed syncing categories for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:self.coreDataStack]; - dispatch_group_enter(syncGroup); - [sharingService syncPublicizeConnectionsForBlog:blog - success:^{ - dispatch_group_leave(syncGroup); - } - failure:^(NSError *error) { - DDLogError(@"Failed syncing publicize connections for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - SharingService *publicizeService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - dispatch_group_enter(syncGroup); - [publicizeService syncPublicizeServicesForBlog:blog success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nullable error) { - DDLogError(@"Failed syncing publicize services for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - if ([RemoteFeature enabled:RemoteFeatureFlagJetpackSocialImprovements] && blog.dotComID != nil) { - JetpackSocialService *jetpackSocialService = [[JetpackSocialService alloc] initWithContextManager:ContextManager.sharedInstance]; - dispatch_group_enter(syncGroup); - [jetpackSocialService syncSharingLimitWithDotComID:blog.dotComID success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nullable error) { - DDLogError(@"Failed syncing publicize sharing limit for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - } - - dispatch_group_enter(syncGroup); - [remote getAllAuthorsWithSuccess:^(NSArray *users) { - [self updateMultiAuthor:users forBlog:blogObjectID completionHandler:^{ - dispatch_group_leave(syncGroup); - }]; - } failure:^(NSError *error) { - DDLogError(@"Failed checking multi-author status for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - PlanService *planService = [[PlanService alloc] initWithCoreDataStack:self.coreDataStack]; - dispatch_group_enter(syncGroup); - [planService getWpcomPlans:blog.account - success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError *error) { - DDLogError(@"Failed updating plans: %@", error); - dispatch_group_leave(syncGroup); - }]; - - dispatch_group_enter(syncGroup); - [planService plansWithPricesForBlog:blog success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError *error) { - DDLogError(@"Failed checking domain credit for site %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - EditorSettingsService *editorService = [[EditorSettingsService alloc] initWithCoreDataStack:self.coreDataStack]; - dispatch_group_enter(syncGroup); - [editorService syncEditorSettingsForBlog:blog success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nonnull __unused error) { - DDLogError(@"Failed to sync Editor settings"); - dispatch_group_leave(syncGroup); - }]; - - if ([FreeToPaidPlansDashboardCardHelper isFeatureEnabled]) { - dispatch_group_enter(syncGroup); - [self refreshDomainsFor:blog success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nonnull error) { - DDLogError(@"Failed refreshing domains: %@", error); - dispatch_group_leave(syncGroup); - }]; - } - - // When everything has left the syncGroup (all calls have ended with success - // or failure) perform the completionHandler - dispatch_group_notify(syncGroup, dispatch_get_main_queue(),^{ - if (completionHandler) { - completionHandler(); - } - }); -} - -- (void)syncSettingsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSManagedObjectID *blogID = [blog objectID]; - [self.coreDataStack.mainContext performBlock:^{ - Blog *blogInContext = (Blog *)[self.coreDataStack.mainContext objectWithID:blogID]; - if (!blogInContext) { - if (success) { - success(); - } - return; - } - - void(^updateOnSuccess)(RemoteBlogSettings *) = ^(RemoteBlogSettings *remoteSettings) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blogInContext = (Blog *)[context objectWithID:blogID]; - [self updateSettings:blogInContext.settings withRemoteSettings:remoteSettings]; - } completion:success onQueue:dispatch_get_main_queue()]; - }; - id remote = [self remoteForBlog:blogInContext]; - if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { - - BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; - [xmlrpcRemote syncBlogOptionsWithSuccess:^(NSDictionary *options) { - RemoteBlogSettings *remoteSettings = [RemoteBlogOptionsHelper remoteBlogSettingsFromXMLRPCDictionaryOptions:options]; - updateOnSuccess(remoteSettings); - } failure:failure]; - - } else if ([remote isKindOfClass:[BlogServiceRemoteREST class]]) { - - BlogServiceRemoteREST *restRemote = remote; - [restRemote syncBlogSettingsWithSuccess:^(RemoteBlogSettings *settings) { - updateOnSuccess(settings); - } failure:failure]; - } - }]; -} - -- (void)syncAuthorsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSManagedObjectID *blogObjectID = blog.objectID; - id remote = [self remoteForBlog:blog]; - - [remote getAllAuthorsWithSuccess:^(NSArray *users) { - [self updateMultiAuthor:users forBlog:blogObjectID completionHandler:success]; - } failure:^(NSError *error) { - DDLogError(@"Failed checking multi-author status for blog %@: %@", blog.url, error); - failure(error); - }]; -} - -- (void)updateSettingsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSManagedObjectID *blogID = [blog objectID]; - NSManagedObjectContext *context = self.coreDataStack.mainContext; - [context performBlock:^{ - Blog *blogInContext = (Blog *)[context objectWithID:blogID]; - id remote = [self remoteForBlog:blogInContext]; - RemoteBlogSettings *remoteSettings = [self remoteSettingFromSettings:blogInContext.settings]; - - void(^onSuccess)(void) = ^() { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blogInContext = (Blog *)[context existingObjectWithID:blogID error:nil]; - if (blogInContext) { - [self updateSettings:blogInContext.settings withRemoteSettings:remoteSettings]; - } - } completion:^{ - if (success) { - success(); - } - } onQueue:dispatch_get_main_queue()]; - }; - - if ([remote isKindOfClass:[BlogServiceRemoteXMLRPC class]]) { - - BlogServiceRemoteXMLRPC *xmlrpcRemote = remote; - [xmlrpcRemote updateBlogOptionsWith:[RemoteBlogOptionsHelper remoteOptionsForUpdatingBlogTitleAndTagline:remoteSettings] - success:onSuccess - failure:failure]; - - } else if([remote isKindOfClass:[BlogServiceRemoteREST class]]) { - - BlogServiceRemoteREST *restRemote = remote; - [restRemote updateBlogSettings:remoteSettings - success:onSuccess - failure:failure]; - } - }]; -} - -- (void)syncPostTypesForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSManagedObjectID *blogObjectID = blog.objectID; - id remote = [self remoteForBlog:blog]; - [remote syncPostTypesWithSuccess:^(NSArray *remotePostTypes) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - NSError *blogError; - Blog *blogInContext = (Blog *)[context existingObjectWithID:blogObjectID error:&blogError]; - if (!blogInContext || blogError) { - DDLogError(@"Error occurred fetching blog in context with: %@", blogError); - dispatch_async(dispatch_get_main_queue(), ^{ - if (failure) { - failure(blogError); - return; - } - }); - } - // Create new PostType entities with the RemotePostType objects. - NSMutableSet *postTypes = [NSMutableSet setWithCapacity:remotePostTypes.count]; - NSString *entityName = NSStringFromClass([PostType class]); - for (RemotePostType *remoteType in remotePostTypes) { - PostType *postType = [NSEntityDescription insertNewObjectForEntityForName:entityName - inManagedObjectContext:context]; - postType.name = remoteType.name; - postType.label = remoteType.label; - postType.apiQueryable = remoteType.apiQueryable; - [postTypes addObject:postType]; - } - // Replace the current set of postTypes with new entities. - blogInContext.postTypes = [NSSet setWithSet:postTypes]; - } completion:success onQueue:dispatch_get_main_queue()]; - } failure:failure]; -} - -- (void)syncPostFormatsForBlog:(Blog *)blog - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - id remote = [self remoteForBlog:blog]; - [remote syncPostFormatsWithSuccess:[self postFormatsHandlerWithBlogObjectID:blog.objectID - completionHandler:success] - failure:failure]; -} - -///-------------------- -/// @name Blog creation -///-------------------- - -- (Blog *)findBlogWithDotComID:(NSNumber *)dotComID - inAccount:(WPAccount *)account -{ - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"dotComID = %@", dotComID]; - return [[account.blogs filteredSetUsingPredicate:predicate] anyObject]; -} - -- (void)removeBlog:(Blog *)blog -{ - DDLogInfo(@" remove", blog.hostURL); - [blog.xmlrpcApi invalidateAndCancelTasks]; - [self unscheduleBloggingRemindersFor:blog]; - - WPAccount *account = blog.account; - - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blogInContext = [context existingObjectWithID:blog.objectID error:nil]; - [context deleteObject:blogInContext]; - }]; - - if (account) { - AccountService *accountService = [[AccountService alloc] initWithCoreDataStack:self.coreDataStack]; - [accountService purgeAccountIfUnused:account]; - } - - [WPAnalytics refreshMetadata]; -} - -- (void)associateSyncedBlogsToJetpackAccount:(WPAccount *)account - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - AccountServiceRemoteREST *remote = [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:account.wordPressComRestApi]; - - BOOL filterJetpackSites = false; - - [remote getBlogs:filterJetpackSites success:^(NSArray *remoteBlogs) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - NSMutableSet *accountBlogIDs = [NSMutableSet new]; - for (RemoteBlog *remoteBlog in remoteBlogs) { - [accountBlogIDs addObject:remoteBlog.blogID]; - } - - NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Blog class])]; - request.predicate = [NSPredicate predicateWithFormat:@"account = NULL"]; - NSArray *blogs = [context executeFetchRequest:request error:nil]; - blogs = [blogs filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * __unused bindings) { - Blog *blog = (Blog *)evaluatedObject; - NSNumber *jetpackBlogID = blog.jetpack.siteID; - return jetpackBlogID && [accountBlogIDs containsObject:jetpackBlogID]; - }]]; - - WPAccount *accountInContext = [context existingObjectWithID:account.objectID error:nil]; - [accountInContext addBlogs:[NSSet setWithArray:blogs]]; - } completion:success onQueue:dispatch_get_main_queue()]; - } failure:failure]; -} - -#pragma mark - Private methods - -- (void)mergeBlogs:(NSArray *)blogs withAccountID:(NSManagedObjectID *)accountID inContext:(NSManagedObjectContext *)context -{ - // Nuke dead blogs - NSError *error = nil; - WPAccount *account = [context existingObjectWithID:accountID error:&error]; - if (account == nil) { - DDLogInfo(@"Can't find the account. User may have signed out. Error: %@", error); - return; - } - - NSSet *remoteSet = [NSSet setWithArray:[blogs valueForKey:@"blogID"]]; - NSSet *localSet = [account.blogs valueForKey:@"dotComID"]; - NSMutableSet *toDelete = [localSet mutableCopy]; - [toDelete minusSet:remoteSet]; - - if ([toDelete count] > 0) { - for (Blog *blog in account.blogs) { - if ([toDelete containsObject:blog.dotComID]) { - [self unscheduleBloggingRemindersFor:blog]; - // Consider switching this to a call to removeBlog in the future - // to consolidate behaviour @frosty - [context deleteObject:blog]; - } - } - } - - // Go through each remote incoming blog and make sure we're up to date with titles, etc. - // Also adds any blogs we don't have - for (RemoteBlog *remoteBlog in blogs) { - [self updateBlogWithRemoteBlog:remoteBlog account:account inContext:context]; - [self updatePromptSettingsFor:remoteBlog context:context]; - } - - /* - Sometimes bad things happen and blogs get duplicated. 👭 - Hopefully we'll fix all the causes and this should never happen again 🤞🤞🤞 - But even if it never happens again, it has already happened so we need to clean up. 🧹 - Otherwise, users would have to reinstall the app to get rid of duplicates 🙅‍♀️ - - More context here: - https://github.com/wordpress-mobile/WordPress-iOS/issues/7886#issuecomment-524221031 - */ - [account deduplicateBlogs]; - - // Ensure that the account has a default blog defined (if there is one). - AccountService *service = [[AccountService alloc] initWithCoreDataStack:self.coreDataStack]; - [service updateDefaultBlogIfNeeded:account inContext:context]; -} - -- (void)updateBlogWithRemoteBlog:(RemoteBlog *)remoteBlog account:(WPAccount *)account inContext:(NSManagedObjectContext *)context -{ - Blog *blog = [self findBlogWithDotComID:remoteBlog.blogID inAccount:account]; - - if (!blog && remoteBlog.jetpack) { - blog = [self migrateRemoteJetpackBlog:remoteBlog forAccount:account inContext:context]; - } - - if (!blog) { - DDLogInfo(@"New blog from account %@: %@", account.username, remoteBlog); - if (account != nil) { - blog = [Blog createBlankBlogWithAccount:account]; - } else { - blog = [Blog createBlankBlogInContext:context]; - } - blog.xmlrpc = remoteBlog.xmlrpc; - } - - [self updateBlog:blog withRemoteBlog:remoteBlog]; -} - -- (void)updateBlog:(Blog *)blog withRemoteBlog:(RemoteBlog *)remoteBlog -{ - [blog addSettingsIfNecessary]; - - blog.url = remoteBlog.url; - blog.dotComID = remoteBlog.blogID; - blog.organizationID = remoteBlog.organizationID; - blog.isHostedAtWPcom = !remoteBlog.jetpack; - blog.icon = remoteBlog.icon; - blog.capabilities = remoteBlog.capabilities; - blog.isAdmin = remoteBlog.isAdmin; - blog.options = remoteBlog.options; - blog.planID = remoteBlog.planID; - blog.planTitle = remoteBlog.planTitle; - blog.planActiveFeatures = remoteBlog.planActiveFeatures; - blog.hasPaidPlan = remoteBlog.hasPaidPlan; - blog.quotaSpaceAllowed = remoteBlog.quotaSpaceAllowed; - blog.quotaSpaceUsed = remoteBlog.quotaSpaceUsed; - - // Update 'Top Level' Settings - BlogSettings *settings = blog.settings; - settings.name = [remoteBlog.name stringByDecodingXMLCharacters]; - settings.tagline = [remoteBlog.tagline stringByDecodingXMLCharacters]; - - [NSNotificationCenter.defaultCenter postNotificationName:WPBlogUpdatedNotification object:nil]; -} - -/** - Searches for Jetpack blog that has already been added as a self hosted to the app - and migrates it to use Jetpack REST. - - When a Jetpack blog appears on the results to sync blogs, we want to see if it's - already added in the app as a self hosted site. If that's the case, this method - will take the blog and transfer it to the account. - - It would be the equivalent of just syncing and removing the previous self hosted, - but this will preserve the synced blog objects and local drafts. - - @param remoteBlog the RemoteBlog object with the blog details - @param account the account that the blog should be migrated to - @returns the migrated blog if found, or nil otherwise - */ -- (Blog *)migrateRemoteJetpackBlog:(RemoteBlog *)remoteBlog - forAccount:(WPAccount *)account - inContext:(NSManagedObjectContext *)context -{ - assert(remoteBlog.xmlrpc != nil); - NSURL *xmlrpcURL = [NSURL URLWithString:remoteBlog.xmlrpc]; - NSURLComponents *components = [NSURLComponents componentsWithURL:xmlrpcURL resolvingAgainstBaseURL:NO]; - if ([components.scheme isEqualToString:@"https"]) { - components.scheme = @"http"; - } else { - components.scheme = @"https"; - } - NSURL *alternateXmlrpcURL = components.URL; - NSArray *blogsWithNoAccount = [Blog selfHostedInContext:context]; - Blog *jetpackBlog = [[blogsWithNoAccount wp_filter:^BOOL(Blog *blogToTest) { - return [blogToTest.xmlrpc caseInsensitiveCompare:xmlrpcURL.absoluteString] == NSOrderedSame - || [blogToTest.xmlrpc caseInsensitiveCompare:alternateXmlrpcURL.absoluteString] == NSOrderedSame; - }] firstObject]; - - if (jetpackBlog) { - DDLogInfo(@"Migrating %@ to wp.com account %@", [jetpackBlog hostURL], account.username); - jetpackBlog.account = account; - } - - return jetpackBlog; -} - -- (id)remoteForBlog:(Blog *)blog -{ - id remote; - if ([blog supports:BlogFeatureWPComRESTAPI]) { - if (blog.wordPressComRestApi) { - remote = [[BlogServiceRemoteREST alloc] initWithWordPressComRestApi:blog.wordPressComRestApi siteID:blog.dotComID]; - } - } else if (blog.xmlrpcApi) { - remote = [[BlogServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password]; - } - - return remote; -} - -- (id)remoteForAccount:(WPAccount *)account -{ - if (account.wordPressComRestApi == nil) { - return nil; - } - - return [[AccountServiceRemoteREST alloc] initWithWordPressComRestApi:account.wordPressComRestApi]; -} - -#pragma mark - Completion handlers - -- (void)updateMultiAuthor:(NSArray *)users forBlog:(NSManagedObjectID *)blogObjectID completionHandler:(void (^)(void))completion -{ - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - NSError *error; - Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:&error]; - if (error) { - DDLogError(@"%@", error); - } - if (!blog) { - return; - } - - [self updateBlogAuthorsForBlog:blog withRemoteUsers:users inContext:context]; - - blog.isMultiAuthor = users.count > 1; - /// Search for a matching user ID - /// - wp.com hosted: blog.account.userID - /// - Jetpack: user.linkedUserID == blog.account.userID - /// - self hosted: user.username == blog.username - if (blog.account) { - if ([blog isHostedAtWPcom]) { - blog.userID = blog.account.userID; - } else { - for (RemoteUser *user in users) { - if ([user.linkedUserID isEqual:blog.account.userID]) { - blog.userID = user.userID; - break; - } - } - } - } else if (blog.username != nil) { - for (RemoteUser *user in users) { - if ([user.username isEqualToString:blog.username]) { - blog.userID = user.userID; - break; - } - } - } - } completion: completion onQueue:dispatch_get_main_queue()]; -} - -- (BlogDetailsHandler)blogDetailsHandlerWithBlogObjectID:(NSManagedObjectID *)blogObjectID - completionHandler:(void (^)(void))completion -{ - return ^void(RemoteBlog *remoteBlog) { - [[[JetpackCapabilitiesService alloc] init] syncWithBlogs:@[remoteBlog] success:^(NSArray *blogs) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; - if (blog) { - [self updateBlog:blog withRemoteBlog:blogs.firstObject]; - [self updatePromptSettingsFor:blogs.firstObject context:context]; - } - } completion:completion onQueue:dispatch_get_main_queue()]; - }]; - }; -} - -- (OptionsHandler)optionsHandlerWithBlogObjectID:(NSManagedObjectID *)blogObjectID - completionHandler:(void (^)(void))completion -{ - return ^void(NSDictionary *options) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; - if (!blog) { - return; - } - - blog.options = [NSDictionary dictionaryWithDictionary:options]; - - RemoteBlogSettings *remoteSettings = [RemoteBlogOptionsHelper remoteBlogSettingsFromXMLRPCDictionaryOptions:options]; - [self updateSettings:blog.settings withRemoteSettings:remoteSettings]; - - - // NOTE: `[blog version]` can return nil. If this happens `version` will be `0` - CGFloat version = [[blog version] floatValue]; - if (version > 0 && version < [WordPressMinimumVersion floatValue]) { - if (blog.lastUpdateWarning == nil - || [blog.lastUpdateWarning floatValue] < [WordPressMinimumVersion floatValue]) - { - dispatch_async(dispatch_get_main_queue(), ^{ - // TODO :: Remove UI call from service layer - [WPError showAlertWithTitle:NSLocalizedString(@"WordPress version too old", @"") - message:[NSString stringWithFormat:NSLocalizedString(@"The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@", @""), [blog hostname], [blog version], WordPressMinimumVersion]]; - }); - blog.lastUpdateWarning = WordPressMinimumVersion; - } - } - } completion:completion onQueue:dispatch_get_main_queue()]; - }; -} - -- (PostFormatsHandler)postFormatsHandlerWithBlogObjectID:(NSManagedObjectID *)blogObjectID - completionHandler:(void (^)(void))completion -{ - return ^void(NSDictionary *postFormats) { - [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { - Blog *blog = (Blog *)[context existingObjectWithID:blogObjectID error:nil]; - if (blog) { - NSDictionary *formats = postFormats; - if (![formats objectForKey:PostFormatStandard]) { - NSMutableDictionary *mutablePostFormats = [formats mutableCopy]; - mutablePostFormats[PostFormatStandard] = NSLocalizedString(@"Standard", @"Standard post format label"); - formats = [NSDictionary dictionaryWithDictionary:mutablePostFormats]; - } - blog.postFormats = formats; - } - } completion:completion onQueue:dispatch_get_main_queue()]; - }; -} - -- (void)updateSettings:(BlogSettings *)settings withRemoteSettings:(RemoteBlogSettings *)remoteSettings -{ - NSParameterAssert(settings); - NSParameterAssert(remoteSettings); - - // Transformables - NSSet *separatedBlocklistKeys = [remoteSettings.commentsBlocklistKeys uniqueStringComponentsSeparatedByNewline]; - NSSet *separatedModerationKeys = [remoteSettings.commentsModerationKeys uniqueStringComponentsSeparatedByNewline]; - - // General - settings.name = remoteSettings.name; - settings.tagline = remoteSettings.tagline; - settings.privacy = remoteSettings.privacy ?: settings.privacy; - settings.languageID = remoteSettings.languageID ?: settings.languageID; - settings.iconMediaID = remoteSettings.iconMediaID; - settings.gmtOffset = remoteSettings.gmtOffset; - settings.timezoneString = remoteSettings.timezoneString; - - // Writing - settings.defaultCategoryID = remoteSettings.defaultCategoryID ?: settings.defaultCategoryID; - settings.defaultPostFormat = remoteSettings.defaultPostFormat ?: settings.defaultPostFormat; - settings.dateFormat = remoteSettings.dateFormat; - settings.timeFormat = remoteSettings.timeFormat; - settings.startOfWeek = remoteSettings.startOfWeek; - settings.postsPerPage = remoteSettings.postsPerPage; - - // Discussion - settings.commentsAllowed = [remoteSettings.commentsAllowed boolValue]; - settings.commentsBlocklistKeys = separatedBlocklistKeys; - settings.commentsCloseAutomatically = [remoteSettings.commentsCloseAutomatically boolValue]; - settings.commentsCloseAutomaticallyAfterDays = remoteSettings.commentsCloseAutomaticallyAfterDays; - settings.commentsFromKnownUsersAllowlisted = [remoteSettings.commentsFromKnownUsersAllowlisted boolValue]; - - settings.commentsMaximumLinks = remoteSettings.commentsMaximumLinks; - settings.commentsModerationKeys = separatedModerationKeys; - - settings.commentsPagingEnabled = [remoteSettings.commentsPagingEnabled boolValue]; - settings.commentsPageSize = remoteSettings.commentsPageSize; - - settings.commentsRequireManualModeration = [remoteSettings.commentsRequireManualModeration boolValue]; - settings.commentsRequireNameAndEmail = [remoteSettings.commentsRequireNameAndEmail boolValue]; - settings.commentsRequireRegistration = [remoteSettings.commentsRequireRegistration boolValue]; - - settings.commentsSortOrderAscending = remoteSettings.commentsSortOrderAscending; - - settings.commentsThreadingDepth = remoteSettings.commentsThreadingDepth; - settings.commentsThreadingEnabled = [remoteSettings.commentsThreadingEnabled boolValue]; - - settings.pingbackInboundEnabled = [remoteSettings.pingbackInboundEnabled boolValue]; - settings.pingbackOutboundEnabled = [remoteSettings.pingbackOutboundEnabled boolValue]; - - // Related Posts - settings.relatedPostsAllowed = [remoteSettings.relatedPostsAllowed boolValue]; - settings.relatedPostsEnabled = [remoteSettings.relatedPostsEnabled boolValue]; - settings.relatedPostsShowHeadline = [remoteSettings.relatedPostsShowHeadline boolValue]; - settings.relatedPostsShowThumbnails = [remoteSettings.relatedPostsShowThumbnails boolValue]; - - // AMP - settings.ampSupported = [remoteSettings.ampSupported boolValue]; - settings.ampEnabled = [remoteSettings.ampEnabled boolValue]; - - // Sharing - settings.sharingButtonStyle = remoteSettings.sharingButtonStyle; - settings.sharingLabel = remoteSettings.sharingLabel; - settings.sharingTwitterName = remoteSettings.sharingTwitterName; - settings.sharingCommentLikesEnabled = [remoteSettings.sharingCommentLikesEnabled boolValue]; - settings.sharingDisabledLikes = [remoteSettings.sharingDisabledLikes boolValue]; - settings.sharingDisabledReblogs = [remoteSettings.sharingDisabledReblogs boolValue]; -} - -- (RemoteBlogSettings *)remoteSettingFromSettings:(BlogSettings *)settings -{ - NSParameterAssert(settings); - RemoteBlogSettings *remoteSettings = [RemoteBlogSettings new]; - - // Transformables - NSString *joinedBlocklistKeys = [[settings.commentsBlocklistKeys allObjects] componentsJoinedByString:@"\n"]; - NSString *joinedModerationKeys = [[settings.commentsModerationKeys allObjects] componentsJoinedByString:@"\n"]; - - // General - remoteSettings.name = settings.name; - remoteSettings.tagline = settings.tagline; - remoteSettings.privacy = settings.privacy; - remoteSettings.languageID = settings.languageID; - remoteSettings.iconMediaID = settings.iconMediaID; - remoteSettings.gmtOffset = settings.gmtOffset; - remoteSettings.timezoneString = settings.timezoneString; - - // Writing - remoteSettings.defaultCategoryID = settings.defaultCategoryID; - remoteSettings.defaultPostFormat = settings.defaultPostFormat; - remoteSettings.dateFormat = settings.dateFormat; - remoteSettings.timeFormat = settings.timeFormat; - remoteSettings.startOfWeek = settings.startOfWeek; - remoteSettings.postsPerPage = settings.postsPerPage; - - // Discussion - remoteSettings.commentsAllowed = @(settings.commentsAllowed); - remoteSettings.commentsBlocklistKeys = joinedBlocklistKeys; - remoteSettings.commentsCloseAutomatically = @(settings.commentsCloseAutomatically); - remoteSettings.commentsCloseAutomaticallyAfterDays = settings.commentsCloseAutomaticallyAfterDays; - remoteSettings.commentsFromKnownUsersAllowlisted = @(settings.commentsFromKnownUsersAllowlisted); - - remoteSettings.commentsMaximumLinks = settings.commentsMaximumLinks; - remoteSettings.commentsModerationKeys = joinedModerationKeys; - - remoteSettings.commentsPagingEnabled = @(settings.commentsPagingEnabled); - remoteSettings.commentsPageSize = settings.commentsPageSize; - - remoteSettings.commentsRequireManualModeration = @(settings.commentsRequireManualModeration); - remoteSettings.commentsRequireNameAndEmail = @(settings.commentsRequireNameAndEmail); - remoteSettings.commentsRequireRegistration = @(settings.commentsRequireRegistration); - - remoteSettings.commentsSortOrderAscending = settings.commentsSortOrderAscending; - - remoteSettings.commentsThreadingDepth = settings.commentsThreadingDepth; - remoteSettings.commentsThreadingEnabled = @(settings.commentsThreadingEnabled); - - remoteSettings.pingbackInboundEnabled = @(settings.pingbackInboundEnabled); - remoteSettings.pingbackOutboundEnabled = @(settings.pingbackOutboundEnabled); - - // AMP - remoteSettings.ampEnabled = @(settings.ampEnabled); - - // Related Posts - remoteSettings.relatedPostsAllowed = @(settings.relatedPostsAllowed); - remoteSettings.relatedPostsEnabled = @(settings.relatedPostsEnabled); - remoteSettings.relatedPostsShowHeadline = @(settings.relatedPostsShowHeadline); - remoteSettings.relatedPostsShowThumbnails = @(settings.relatedPostsShowThumbnails); - - // Sharing - remoteSettings.sharingButtonStyle = settings.sharingButtonStyle; - remoteSettings.sharingLabel = settings.sharingLabel; - remoteSettings.sharingTwitterName = settings.sharingTwitterName; - remoteSettings.sharingCommentLikesEnabled = @(settings.sharingCommentLikesEnabled); - remoteSettings.sharingDisabledLikes = @(settings.sharingDisabledLikes); - remoteSettings.sharingDisabledReblogs = @(settings.sharingDisabledReblogs); - - return remoteSettings; -} - -@end From fb16306d426abf66dbf3cdc78af0d69c28a8295e Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 12 Mar 2025 11:34:01 +1100 Subject: [PATCH 19/25] Add `import UIKit` to some files shared with notification extension All those Swift files compile without importing UIKit because `SFHFKeychainUtils.h` did that for them and they got access to it via the bridging header. Next, we'll move SFHFKeychainUtils into the modules, and UIKit will no longer be available through it. Side note: UIKit should not have been available via SFHFKeychainUtils in the first place. On top of that, SFHFKeychainUtils does not need UIKit either. --- .../Actions/FormattableContentActionCommand.swift | 2 ++ .../Utility/FormattableContent/FormattableContentStyles.swift | 1 + .../ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift | 1 + .../ViewRelated/Aztec/Extensions/TextList+WordPress.swift | 1 + .../FormattableContent/NotificationTextContent.swift | 2 +- WordPress/Shared/SharePost.swift | 1 + .../UINavigationController+Extensions.swift | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/FormattableContent/Actions/FormattableContentActionCommand.swift b/WordPress/Classes/Utility/FormattableContent/Actions/FormattableContentActionCommand.swift index 0fab1a0c1713..a02c4f955194 100644 --- a/WordPress/Classes/Utility/FormattableContent/Actions/FormattableContentActionCommand.swift +++ b/WordPress/Classes/Utility/FormattableContent/Actions/FormattableContentActionCommand.swift @@ -1,3 +1,5 @@ +import UIKit + /// Abstracts the logic behind contextual actions that can be applied to FormattableContent. /// protocol FormattableContentActionCommand: CustomStringConvertible { diff --git a/WordPress/Classes/Utility/FormattableContent/FormattableContentStyles.swift b/WordPress/Classes/Utility/FormattableContent/FormattableContentStyles.swift index c9a9fae1bd37..694f22abcab6 100644 --- a/WordPress/Classes/Utility/FormattableContent/FormattableContentStyles.swift +++ b/WordPress/Classes/Utility/FormattableContent/FormattableContentStyles.swift @@ -1,3 +1,4 @@ +import UIKit /// Styles definition to be applied to a single FormattableContent entity. public protocol FormattableContentStyles { diff --git a/WordPress/Classes/ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift b/WordPress/Classes/ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift index 064cd2bff840..1a92f1b928af 100644 --- a/WordPress/Classes/ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift +++ b/WordPress/Classes/ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift @@ -1,6 +1,7 @@ import Foundation import Gridicons import Aztec +import UIKit protocol FormatBarItemProvider { var iconImage: UIImage { get } diff --git a/WordPress/Classes/ViewRelated/Aztec/Extensions/TextList+WordPress.swift b/WordPress/Classes/ViewRelated/Aztec/Extensions/TextList+WordPress.swift index 645951d19dee..5b273cba93c4 100644 --- a/WordPress/Classes/ViewRelated/Aztec/Extensions/TextList+WordPress.swift +++ b/WordPress/Classes/ViewRelated/Aztec/Extensions/TextList+WordPress.swift @@ -1,5 +1,6 @@ import Foundation import Aztec +import UIKit // MARK: - TextList.Style // diff --git a/WordPress/Classes/ViewRelated/Notifications/FormattableContent/NotificationTextContent.swift b/WordPress/Classes/ViewRelated/Notifications/FormattableContent/NotificationTextContent.swift index 43661fe3e678..3a130038ece0 100644 --- a/WordPress/Classes/ViewRelated/Notifications/FormattableContent/NotificationTextContent.swift +++ b/WordPress/Classes/ViewRelated/Notifications/FormattableContent/NotificationTextContent.swift @@ -1,5 +1,5 @@ - import Foundation +import UIKit extension FormattableContentKind { static let image = FormattableContentKind("image") diff --git a/WordPress/Shared/SharePost.swift b/WordPress/Shared/SharePost.swift index b5721ca3b707..17084297ccff 100644 --- a/WordPress/Shared/SharePost.swift +++ b/WordPress/Shared/SharePost.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import MobileCoreServices /// A simple shared model to represent a Site diff --git a/WordPress/WordPressShareExtension/UINavigationController+Extensions.swift b/WordPress/WordPressShareExtension/UINavigationController+Extensions.swift index ad35d7dd410b..6abd384fa21f 100644 --- a/WordPress/WordPressShareExtension/UINavigationController+Extensions.swift +++ b/WordPress/WordPressShareExtension/UINavigationController+Extensions.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit /// Encapsulates UINavigationController Helper Methods. /// From 8959db19183456747d341eaf98eef090eec42249 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 12 Mar 2025 14:32:58 +1100 Subject: [PATCH 20/25] Extract SFHFKeychainUtils in a dedicated Swift package We need it in a dedicated package because the Objective-C implementation does not use ARC. Therefore, we need to use the `-fno-objc-arc` compiler setting. But such a setting should be applied sparingly so instead of blanket using it in an existing Objective-C package like WordPressSharedObjC, we use it surgically just for this file. --- Modules/Package.swift | 11 ++++++ .../SFHFKeychainUtils}/SFHFKeychainUtils.m | 0 .../include}/SFHFKeychainUtils.h | 4 +- .../Sources/WordPressDataObjC/Models/Blog.m | 1 + .../WordPressDataObjC/Models/WPAccount.m | 2 +- .../Classes/Services/CredentialsService.swift | 2 + .../Services/NotificationSupportService.swift | 1 + .../Services/ShareExtensionService.swift | 1 + .../Classes/Stores/StatsWidgetsStore.swift | 1 + .../System/WordPress-Bridging-Header.h | 3 -- .../Classes/System/WordPressAppDelegate.swift | 1 + WordPress/Classes/Utility/KeychainUtils.swift | 2 + .../Utility/Migrations/10-11/BlogToAccount.m | 1 + .../Migrations/10-11/BlogToJetpackAccount.m | 1 + .../Migrations/32-33/BlogToBlog32to33.swift | 1 + .../WordPressAuthenticationManager.swift | 1 + .../Remote service/StatsWidgetsService.swift | 1 + .../JetpackStatsWidgets-Bridging-Header.h | 1 - WordPress/WordPress.xcodeproj/project.pbxproj | 38 ------------------- ...ressDraftActionExtension-Bridging-Header.h | 1 - .../Sources/NotificationService.swift | 1 + ...ficationServiceExtension-Bridging-Header.h | 3 -- .../WordPressShare-Bridging-Header.h | 1 - WordPress/WordPressTest/KeychainTest.m | 3 +- .../WordPressTest/KeychainUtilsTests.swift | 2 + 25 files changed, 33 insertions(+), 51 deletions(-) rename {WordPress/Classes/Utility => Modules/Sources/SFHFKeychainUtils}/SFHFKeychainUtils.m (100%) rename {WordPress/Classes/Utility => Modules/Sources/SFHFKeychainUtils/include}/SFHFKeychainUtils.h (98%) diff --git a/Modules/Package.swift b/Modules/Package.swift index 5685d279c75a..550ccd0edef6 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -11,6 +11,7 @@ let package = Package( .library(name: "AsyncImageKit", targets: ["AsyncImageKit"]), .library(name: "DesignSystem", targets: ["DesignSystem"]), .library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]), + .library(name: "SFHFKeychainUtils", targets: ["SFHFKeychainUtils"]), .library(name: "WordPressData", targets: ["WordPressData"]), .library(name: "WordPressFlux", targets: ["WordPressFlux"]), .library(name: "WordPressShared", targets: ["WordPressShared"]), @@ -60,6 +61,12 @@ let package = Package( ]), .target(name: "DesignSystem", swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]), + // SFHFKeychainUtils is an old Objective-C keychain wrapper. + // The implementatoin predates ARC, hence the dedicated target with ARC disabled, for the time being. + .target( + name: "SFHFKeychainUtils", + cSettings: [.unsafeFlags(["-fno-objc-arc"])] + ), .target(name: "UITestsFoundation", dependencies: [ .product(name: "ScreenObject", package: "ScreenObject"), .product(name: "XCUITestHelpers", package: "ScreenObject"), @@ -161,6 +168,7 @@ enum XcodeSupport { ] let shareAndDraftExtensionsDependencies: [Target.Dependency] = [ + "SFHFKeychainUtils", "WordPressShared", "WordPressUI", .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), @@ -183,6 +191,7 @@ enum XcodeSupport { .xcodeTarget("XcodeTarget_App", dependencies: [ "DesignSystem", "JetpackStatsWidgetsCore", + "SFHFKeychainUtils", "WordPressData", "WordPressFlux", "WordPressShared", @@ -221,6 +230,7 @@ enum XcodeSupport { .product(name: "WordPressEditor", package: "AztecEditor-iOS"), ]), .xcodeTarget("XcodeTarget_WordPressTests", dependencies: testDependencies + [ + "SFHFKeychainUtils", "WordPressShared", .product(name: "Gravatar", package: "Gravatar-SDK-iOS"), .product(name: "Nimble", package: "Nimble"), @@ -231,6 +241,7 @@ enum XcodeSupport { .xcodeTarget("XcodeTarget_ShareExtension", dependencies: shareAndDraftExtensionsDependencies), .xcodeTarget("XcodeTarget_DraftActionExtension", dependencies: shareAndDraftExtensionsDependencies), .xcodeTarget("XcodeTarget_NotificationServiceExtension", dependencies: [ + "SFHFKeychainUtils", "WordPressShared", ]), .xcodeTarget("XcodeTarget_StatsWidget", dependencies: [ diff --git a/WordPress/Classes/Utility/SFHFKeychainUtils.m b/Modules/Sources/SFHFKeychainUtils/SFHFKeychainUtils.m similarity index 100% rename from WordPress/Classes/Utility/SFHFKeychainUtils.m rename to Modules/Sources/SFHFKeychainUtils/SFHFKeychainUtils.m diff --git a/WordPress/Classes/Utility/SFHFKeychainUtils.h b/Modules/Sources/SFHFKeychainUtils/include/SFHFKeychainUtils.h similarity index 98% rename from WordPress/Classes/Utility/SFHFKeychainUtils.h rename to Modules/Sources/SFHFKeychainUtils/include/SFHFKeychainUtils.h index 0bf448028a60..60b1c228af40 100755 --- a/WordPress/Classes/Utility/SFHFKeychainUtils.h +++ b/Modules/Sources/SFHFKeychainUtils/include/SFHFKeychainUtils.h @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -#import +#import @interface SFHFKeychainUtils : NSObject { @@ -68,4 +68,4 @@ + (NSArray *> *)getAllPasswordsForAccessGroup:(NSString *)accessGroup error:(NSError **)error; -@end \ No newline at end of file +@end diff --git a/Modules/Sources/WordPressDataObjC/Models/Blog.m b/Modules/Sources/WordPressDataObjC/Models/Blog.m index 0ea6f5e7e0ab..a5cd07513eed 100644 --- a/Modules/Sources/WordPressDataObjC/Models/Blog.m +++ b/Modules/Sources/WordPressDataObjC/Models/Blog.m @@ -6,6 +6,7 @@ #import "Constants.h" #import "WPUserAgent.h" #import "WordPress-Swift.h" +@import SFHFKeychainUtils; @import NSObject_SafeExpectations; @import NSURL_IDN; diff --git a/Modules/Sources/WordPressDataObjC/Models/WPAccount.m b/Modules/Sources/WordPressDataObjC/Models/WPAccount.m index b7411228fbd2..fc5800f7ffc3 100644 --- a/Modules/Sources/WordPressDataObjC/Models/WPAccount.m +++ b/Modules/Sources/WordPressDataObjC/Models/WPAccount.m @@ -1,6 +1,6 @@ #import "WPAccount.h" -//#import "WordPress-Swift.h" @import CocoaLumberjack; +@import SFHFKeychainUtils; @interface WPAccount () diff --git a/WordPress/Classes/Services/CredentialsService.swift b/WordPress/Classes/Services/CredentialsService.swift index 3118b89ca3e9..09c2f86484cb 100644 --- a/WordPress/Classes/Services/CredentialsService.swift +++ b/WordPress/Classes/Services/CredentialsService.swift @@ -1,3 +1,5 @@ +import SFHFKeychainUtils + protocol CredentialsProvider { func getPassword(username: String, service: String) -> String? } diff --git a/WordPress/Classes/Services/NotificationSupportService.swift b/WordPress/Classes/Services/NotificationSupportService.swift index e7b2121b8e7a..6a1f9d61fb1b 100644 --- a/WordPress/Classes/Services/NotificationSupportService.swift +++ b/WordPress/Classes/Services/NotificationSupportService.swift @@ -1,4 +1,5 @@ import Foundation +import SFHFKeychainUtils @objc open class NotificationSupportService: NSObject { diff --git a/WordPress/Classes/Services/ShareExtensionService.swift b/WordPress/Classes/Services/ShareExtensionService.swift index cd02012c124e..01d0b2042b9a 100644 --- a/WordPress/Classes/Services/ShareExtensionService.swift +++ b/WordPress/Classes/Services/ShareExtensionService.swift @@ -1,4 +1,5 @@ import Foundation +import SFHFKeychainUtils @objc open class ShareExtensionService: NSObject { diff --git a/WordPress/Classes/Stores/StatsWidgetsStore.swift b/WordPress/Classes/Stores/StatsWidgetsStore.swift index 95d82a9066b5..fa9db9a0e554 100644 --- a/WordPress/Classes/Stores/StatsWidgetsStore.swift +++ b/WordPress/Classes/Stores/StatsWidgetsStore.swift @@ -1,4 +1,5 @@ import JetpackStatsWidgetsCore +import SFHFKeychainUtils import WidgetKit class StatsWidgetsStore { diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index 401dd2930737..c8c2a0a0d5fd 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -1,5 +1,3 @@ -#import "SFHFKeychainUtils.h" - // WordPress Module #import "ActivityLogViewController.h" @@ -55,7 +53,6 @@ #import "SettingTableViewCell.h" #import "SettingsTextViewController.h" #import "SharingViewController.h" -#import "SFHFKeychainUtils.h" #import "SiteSettingsViewController.h" #import "SourcePostAttribution.h" #import "StatsViewController.h" diff --git a/WordPress/Classes/System/WordPressAppDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate.swift index c060b1448aa2..ecbd694336f4 100644 --- a/WordPress/Classes/System/WordPressAppDelegate.swift +++ b/WordPress/Classes/System/WordPressAppDelegate.swift @@ -1,3 +1,4 @@ +import SFHFKeychainUtils import UIKit import CocoaLumberjackSwift import Reachability diff --git a/WordPress/Classes/Utility/KeychainUtils.swift b/WordPress/Classes/Utility/KeychainUtils.swift index ee71c72dc7c3..95a84b60e0d3 100644 --- a/WordPress/Classes/Utility/KeychainUtils.swift +++ b/WordPress/Classes/Utility/KeychainUtils.swift @@ -1,3 +1,5 @@ +import SFHFKeychainUtils + @objcMembers class KeychainUtils: NSObject { diff --git a/WordPress/Classes/Utility/Migrations/10-11/BlogToAccount.m b/WordPress/Classes/Utility/Migrations/10-11/BlogToAccount.m index e5f484167d85..b48ad63d71c6 100644 --- a/WordPress/Classes/Utility/Migrations/10-11/BlogToAccount.m +++ b/WordPress/Classes/Utility/Migrations/10-11/BlogToAccount.m @@ -5,6 +5,7 @@ @import NSObject_SafeExpectations; @import NSURL_IDN; +@import SFHFKeychainUtils; @implementation BlogToAccount { NSString *_defaultWpcomUsername; diff --git a/WordPress/Classes/Utility/Migrations/10-11/BlogToJetpackAccount.m b/WordPress/Classes/Utility/Migrations/10-11/BlogToJetpackAccount.m index eae7ffca000a..af29a84cf41d 100644 --- a/WordPress/Classes/Utility/Migrations/10-11/BlogToJetpackAccount.m +++ b/WordPress/Classes/Utility/Migrations/10-11/BlogToJetpackAccount.m @@ -3,6 +3,7 @@ #import "WordPress-Swift.h" @import NSObject_SafeExpectations; +@import SFHFKeychainUtils; static NSString * const BlogJetpackKeychainPrefix = @"jetpackblog-"; diff --git a/WordPress/Classes/Utility/Migrations/32-33/BlogToBlog32to33.swift b/WordPress/Classes/Utility/Migrations/32-33/BlogToBlog32to33.swift index cecd4ba7ccde..35e5b98d19c1 100644 --- a/WordPress/Classes/Utility/Migrations/32-33/BlogToBlog32to33.swift +++ b/WordPress/Classes/Utility/Migrations/32-33/BlogToBlog32to33.swift @@ -1,3 +1,4 @@ +import SFHFKeychainUtils import UIKit class BlogToBlog32to33: NSEntityMigrationPolicy { diff --git a/WordPress/Classes/ViewRelated/NUX/Helpers/WordPressAuthenticationManager.swift b/WordPress/Classes/ViewRelated/NUX/Helpers/WordPressAuthenticationManager.swift index 8790d85414b7..122567801162 100644 --- a/WordPress/Classes/ViewRelated/NUX/Helpers/WordPressAuthenticationManager.swift +++ b/WordPress/Classes/ViewRelated/NUX/Helpers/WordPressAuthenticationManager.swift @@ -1,4 +1,5 @@ import Foundation +import SFHFKeychainUtils import WordPressAuthenticator import Gridicons import UIKit diff --git a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift index 7f4f1bef5b53..70f42c254808 100644 --- a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift +++ b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift @@ -1,3 +1,4 @@ +import SFHFKeychainUtils import WordPressKit import JetpackStatsWidgetsCore import WordPressShared diff --git a/WordPress/JetpackStatsWidgets/Supporting Files/JetpackStatsWidgets-Bridging-Header.h b/WordPress/JetpackStatsWidgets/Supporting Files/JetpackStatsWidgets-Bridging-Header.h index ff4dac2f06fc..4bda21fed193 100644 --- a/WordPress/JetpackStatsWidgets/Supporting Files/JetpackStatsWidgets-Bridging-Header.h +++ b/WordPress/JetpackStatsWidgets/Supporting Files/JetpackStatsWidgets-Bridging-Header.h @@ -1,2 +1 @@ -#import "SFHFKeychainUtils.h" #import "Constants.h" diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3010865ada24..3262be4d13d1 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -3293,9 +3293,6 @@ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 24CE68AE2CD3375300C7B37D /* Exceptions for "Classes" folder in "WordPress" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Utility/Migrations/20-21/SafeReaderTopicToReaderTopic.m", "ViewRelated/Me/All Domains/Coordinators/AllDomainsAddDomainCoordinator.swift", @@ -3310,9 +3307,6 @@ }; 24CE68AF2CD3375300C7B37D /* Exceptions for "Classes" folder in "WordPressShareExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift", "Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift", @@ -3333,7 +3327,6 @@ Utility/BuildInformation/FeatureFlagRolloutStore.swift, Utility/KeychainUtils.swift, Utility/Logging/CocoaLumberjack.swift, - Utility/SFHFKeychainUtils.m, ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift, "ViewRelated/Aztec/Extensions/Header+WordPress.swift", "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", @@ -3347,9 +3340,6 @@ }; 24CE68B02CD3375300C7B37D /* Exceptions for "Classes" folder in "WordPressDraftActionExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift", "Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift", @@ -3370,7 +3360,6 @@ Utility/BuildInformation/FeatureFlagRolloutStore.swift, Utility/KeychainUtils.swift, Utility/Logging/CocoaLumberjack.swift, - Utility/SFHFKeychainUtils.m, ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift, "ViewRelated/Aztec/Extensions/Header+WordPress.swift", "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", @@ -3384,9 +3373,6 @@ }; 24CE68B22CD3375300C7B37D /* Exceptions for "Classes" folder in "WordPressNotificationServiceExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Extensions/String+Helpers.swift", "Extensions/UIImage+Extensions.swift", @@ -3412,7 +3398,6 @@ Utility/FormattableContent/FormattableRangesFactory.swift, Utility/FormattableContent/FormattableTextContent.swift, Utility/KeychainUtils.swift, - Utility/SFHFKeychainUtils.m, ViewRelated/Notifications/FormattableContent/FormattableCommentContent.swift, ViewRelated/Notifications/FormattableContent/FormattableUserContent.swift, ViewRelated/Notifications/FormattableContent/Notifiable.swift, @@ -3442,9 +3427,6 @@ }; 24CE68B52CD3375300C7B37D /* Exceptions for "Classes" folder in "Jetpack" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Utility/App Configuration/AppConfiguration.swift", "Utility/App Configuration/AppConstants.swift", @@ -3456,9 +3438,6 @@ }; 24CE68B62CD3375300C7B37D /* Exceptions for "Classes" folder in "JetpackShareExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift", "Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift", @@ -3477,7 +3456,6 @@ Utility/BuildInformation/FeatureFlagRolloutStore.swift, Utility/KeychainUtils.swift, Utility/Logging/CocoaLumberjack.swift, - Utility/SFHFKeychainUtils.m, ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift, "ViewRelated/Aztec/Extensions/Header+WordPress.swift", "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", @@ -3491,9 +3469,6 @@ }; 24CE68B72CD3375300C7B37D /* Exceptions for "Classes" folder in "JetpackDraftActionExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift", "Extensions/Colors and Styles/WPStyleGuide+Gridicon.swift", @@ -3512,7 +3487,6 @@ Utility/BuildInformation/FeatureFlagRolloutStore.swift, Utility/KeychainUtils.swift, Utility/Logging/CocoaLumberjack.swift, - Utility/SFHFKeychainUtils.m, ViewRelated/Aztec/Extensions/FormatBarItemProviders.swift, "ViewRelated/Aztec/Extensions/Header+WordPress.swift", "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", @@ -3526,9 +3500,6 @@ }; 24CE68B82CD3375300C7B37D /* Exceptions for "Classes" folder in "JetpackNotificationServiceExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( "Extensions/String+Helpers.swift", "Extensions/UIImage+Extensions.swift", @@ -3552,7 +3523,6 @@ Utility/FormattableContent/FormattableRangesFactory.swift, Utility/FormattableContent/FormattableTextContent.swift, Utility/KeychainUtils.swift, - Utility/SFHFKeychainUtils.m, ViewRelated/Notifications/FormattableContent/FormattableCommentContent.swift, ViewRelated/Notifications/FormattableContent/FormattableUserContent.swift, ViewRelated/Notifications/FormattableContent/Notifiable.swift, @@ -3567,9 +3537,6 @@ }; 24CE68B92CD3375300C7B37D /* Exceptions for "Classes" folder in "JetpackStatsWidgets" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( Services/KeyValueDatabase.swift, System/Constants/Constants.m, @@ -3582,7 +3549,6 @@ Utility/BuildInformation/FeatureFlagRolloutStore.swift, Utility/KeychainUtils.swift, Utility/Logging/CocoaLumberjack.swift, - Utility/SFHFKeychainUtils.m, "ViewRelated/Stats/Extensions/Double+Stats.swift", "ViewRelated/Stats/Extensions/NumberFormatter+Stats.swift", ); @@ -3590,13 +3556,9 @@ }; 24CE68BA2CD3375300C7B37D /* Exceptions for "Classes" folder in "JetpackIntents" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - additionalCompilerFlagsByRelativePath = { - Utility/SFHFKeychainUtils.m = "-fno-objc-arc"; - }; membershipExceptions = ( System/Constants/Constants.m, Utility/Logging/CocoaLumberjack.swift, - Utility/SFHFKeychainUtils.m, ); target = 0107E13828FE9DB200DE87DB /* JetpackIntents */; }; diff --git a/WordPress/WordPressDraftActionExtension/WordPressDraftActionExtension-Bridging-Header.h b/WordPress/WordPressDraftActionExtension/WordPressDraftActionExtension-Bridging-Header.h index 670e97f7e3e3..285c18bfd1ae 100644 --- a/WordPress/WordPressDraftActionExtension/WordPressDraftActionExtension-Bridging-Header.h +++ b/WordPress/WordPressDraftActionExtension/WordPressDraftActionExtension-Bridging-Header.h @@ -2,5 +2,4 @@ #import "Constants.h" // Shared Helpers -#import "SFHFKeychainUtils.h" #import "TextBundleWrapper.h" diff --git a/WordPress/WordPressNotificationServiceExtension/Sources/NotificationService.swift b/WordPress/WordPressNotificationServiceExtension/Sources/NotificationService.swift index 43bbdda47c9b..3b563afdecc4 100644 --- a/WordPress/WordPressNotificationServiceExtension/Sources/NotificationService.swift +++ b/WordPress/WordPressNotificationServiceExtension/Sources/NotificationService.swift @@ -1,3 +1,4 @@ +import SFHFKeychainUtils import UserNotifications import WordPressKit diff --git a/WordPress/WordPressNotificationServiceExtension/Sources/WordPressNotificationServiceExtension-Bridging-Header.h b/WordPress/WordPressNotificationServiceExtension/Sources/WordPressNotificationServiceExtension-Bridging-Header.h index 4a84db88f642..308d6e64ad55 100644 --- a/WordPress/WordPressNotificationServiceExtension/Sources/WordPressNotificationServiceExtension-Bridging-Header.h +++ b/WordPress/WordPressNotificationServiceExtension/Sources/WordPressNotificationServiceExtension-Bridging-Header.h @@ -1,5 +1,2 @@ // Main App Helpers #import "Constants.h" - -// Shared Helpers -#import "SFHFKeychainUtils.h" diff --git a/WordPress/WordPressShareExtension/WordPressShare-Bridging-Header.h b/WordPress/WordPressShareExtension/WordPressShare-Bridging-Header.h index 670e97f7e3e3..285c18bfd1ae 100644 --- a/WordPress/WordPressShareExtension/WordPressShare-Bridging-Header.h +++ b/WordPress/WordPressShareExtension/WordPressShare-Bridging-Header.h @@ -2,5 +2,4 @@ #import "Constants.h" // Shared Helpers -#import "SFHFKeychainUtils.h" #import "TextBundleWrapper.h" diff --git a/WordPress/WordPressTest/KeychainTest.m b/WordPress/WordPressTest/KeychainTest.m index e85a544eed2b..76129e8bd137 100644 --- a/WordPress/WordPressTest/KeychainTest.m +++ b/WordPress/WordPressTest/KeychainTest.m @@ -1,6 +1,7 @@ +// TODO: Port to yet to create SFHFKeychainUtils test package #import #import -#import "SFHFKeychainUtils.h" +@import SFHFKeychainUtils; @interface KeychainTest : XCTestCase { diff --git a/WordPress/WordPressTest/KeychainUtilsTests.swift b/WordPress/WordPressTest/KeychainUtilsTests.swift index 35c62c8da4de..4254cedef338 100644 --- a/WordPress/WordPressTest/KeychainUtilsTests.swift +++ b/WordPress/WordPressTest/KeychainUtilsTests.swift @@ -1,3 +1,5 @@ +// TODO: Port to yet to create SFHFKeychainUtils test package +import SFHFKeychainUtils import XCTest @testable import WordPress From b86a99ce3b70884bef836ff1de737fc972144cf3 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 12 Mar 2025 15:04:04 +1100 Subject: [PATCH 21/25] Remove duplicated `CoreDataStackMock` class --- .../WordPressTest/DataMigratorTests.swift | 21 ------------------- .../SharedDataIssueSolverTests.swift | 4 ++-- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/WordPress/WordPressTest/DataMigratorTests.swift b/WordPress/WordPressTest/DataMigratorTests.swift index c834c515b674..916119e2f36b 100644 --- a/WordPress/WordPressTest/DataMigratorTests.swift +++ b/WordPress/WordPressTest/DataMigratorTests.swift @@ -209,27 +209,6 @@ class DataMigratorTests: XCTestCase { } } -// MARK: - CoreDataStackMock - -private final class CoreDataStackMock: CoreDataStack { - var mainContext: NSManagedObjectContext - - init(mainContext: NSManagedObjectContext) { - self.mainContext = mainContext - } - - func newDerivedContext() -> NSManagedObjectContext { - return mainContext - } - - func saveContextAndWait(_ context: NSManagedObjectContext) {} - func save(_ context: NSManagedObjectContext) {} - func save(_ context: NSManagedObjectContext, completion completionBlock: (() -> Void)?, on queue: DispatchQueue) {} - - func performAndSave(_ aBlock: @escaping (NSManagedObjectContext) -> Void) {} - func performAndSave(_ aBlock: @escaping (NSManagedObjectContext) -> Void, completion: (() -> Void)?, on queue: DispatchQueue) {} -} - // MARK: - KeychainUtilsMock private final class KeychainUtilsMock: KeychainUtils { diff --git a/WordPress/WordPressTest/SharedDataIssueSolverTests.swift b/WordPress/WordPressTest/SharedDataIssueSolverTests.swift index c857ac660c9d..d17f61ea8ddc 100644 --- a/WordPress/WordPressTest/SharedDataIssueSolverTests.swift +++ b/WordPress/WordPressTest/SharedDataIssueSolverTests.swift @@ -126,8 +126,8 @@ private extension SharedDataIssueSolverTests { // MARK: - CoreDataStackMock -private final class CoreDataStackMock: CoreDataStack { - var mainContext: NSManagedObjectContext +final class CoreDataStackMock: CoreDataStack { + private(set) var mainContext: NSManagedObjectContext init(mainContext: NSManagedObjectContext) { self.mainContext = mainContext From 2f70b942e65bd7f2886bb582a51726504d658d12 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 12 Mar 2025 08:45:24 +1100 Subject: [PATCH 22/25] Declare a `sharedInstance` in `CoreDataStack` This is to decouple the Objective-C implementation from knowing about components (i.e. `ContextManager`) in the Swift layer. --- Modules/Sources/WordPressData/ContextManager.swift | 6 +++--- .../Sources/WordPressDataObjC/include/CoreDataStack.h | 2 ++ WordPress/Classes/Models/Blog/Blog+SelfHosted.swift | 2 +- WordPress/Classes/Services/JetpackSocialService.swift | 7 +++++-- WordPress/Classes/Services/SharingService.swift | 9 +++++---- .../Root View/SplitViewRootPresenter+Welcome.swift | 3 ++- .../Classes/System/UITesting/UITestConfigurator.swift | 4 +++- .../BackgroundTasks/WeeklyRoundupBackgroundTask.swift | 4 ++-- .../Classes/Utility/Logging/WPCrashLoggingProvider.swift | 5 +++-- .../ViewRelated/Blog/Site Picker/AddSiteController.swift | 3 ++- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 5 +++-- .../EEUUSCompliance/CompliancePopoverViewModel.swift | 5 +++-- WordPress/WordPressTest/SharedDataIssueSolverTests.swift | 5 +++++ 13 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Modules/Sources/WordPressData/ContextManager.swift b/Modules/Sources/WordPressData/ContextManager.swift index 7124fcdc1df2..66ebd0994c3a 100644 --- a/Modules/Sources/WordPressData/ContextManager.swift +++ b/Modules/Sources/WordPressData/ContextManager.swift @@ -288,7 +288,7 @@ extension ContextManager { /// Tests purpose only nonisolated(unsafe) static var overrideInstance: ContextManager? - @objc class func sharedInstance() -> ContextManager { + @objc public class func sharedInstance() -> CoreDataStack { if let overrideInstance { return overrideInstance } @@ -296,8 +296,8 @@ extension ContextManager { return ContextManager.internalSharedInstance } - public static var shared: ContextManager { - return sharedInstance() + static var shared: CoreDataStackSwift { + return sharedInstance() as! CoreDataStackSwift } } diff --git a/Modules/Sources/WordPressDataObjC/include/CoreDataStack.h b/Modules/Sources/WordPressDataObjC/include/CoreDataStack.h index 69c79f3a95ab..27cc2076e862 100644 --- a/Modules/Sources/WordPressDataObjC/include/CoreDataStack.h +++ b/Modules/Sources/WordPressDataObjC/include/CoreDataStack.h @@ -7,6 +7,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, strong) NSManagedObjectContext *mainContext; ++ (id)sharedInstance; + - (NSManagedObjectContext *const)newDerivedContext DEPRECATED_MSG_ATTRIBUTE("Use `performAndSave` instead"); - (void)saveContextAndWait:(NSManagedObjectContext *)context; diff --git a/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift b/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift index c2243b8f40e7..64b422f916a4 100644 --- a/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift +++ b/WordPress/Classes/Models/Blog/Blog+SelfHosted.swift @@ -18,7 +18,7 @@ extension Blog { static func createRestApiBlog( with details: WpApiApplicationPasswordDetails, - in contextManager: ContextManager, + in contextManager: CoreDataStackSwift, using keychainImplementation: KeychainAccessible = KeychainUtils() ) async throws -> String { try await contextManager.performAndSave { context in diff --git a/WordPress/Classes/Services/JetpackSocialService.swift b/WordPress/Classes/Services/JetpackSocialService.swift index 579c3473683b..bcf8c59cbcc8 100644 --- a/WordPress/Classes/Services/JetpackSocialService.swift +++ b/WordPress/Classes/Services/JetpackSocialService.swift @@ -19,8 +19,11 @@ import WordPressKit /// Init method for Objective-C. /// - @objc init(contextManager: ContextManager) { - self.coreDataStack = contextManager + @objc init(contextManager: CoreDataStack) { + guard let typeCastStack = contextManager as? CoreDataStackSwift else { + fatalError("Expected a CoreDataStackSwift-conforming type even though this initializer is marked @objc.") + } + self.coreDataStack = typeCastStack } init(coreDataStack: CoreDataStackSwift = ContextManager.shared) { diff --git a/WordPress/Classes/Services/SharingService.swift b/WordPress/Classes/Services/SharingService.swift index 35be9f1ce855..2cd397f2636d 100644 --- a/WordPress/Classes/Services/SharingService.swift +++ b/WordPress/Classes/Services/SharingService.swift @@ -12,11 +12,12 @@ import WordPressKit private let coreDataStack: CoreDataStackSwift /// The initialiser for Objective-C code. - /// - /// Using `ContextManager` as the argument becuase `CoreDataStackSwift` is not accessible from Objective-C code. @objc - init(contextManager: ContextManager) { - self.coreDataStack = contextManager + init(contextManager: CoreDataStack) { + guard let typeCastStack = contextManager as? CoreDataStackSwift else { + fatalError("Expected a CoreDataStackSwift-conforming type even though this initializer is marked @objc.") + } + self.coreDataStack = typeCastStack } init(coreDataStack: CoreDataStackSwift) { diff --git a/WordPress/Classes/System/Root View/SplitViewRootPresenter+Welcome.swift b/WordPress/Classes/System/Root View/SplitViewRootPresenter+Welcome.swift index 9589f6e4726e..f34b3cdcdf5c 100644 --- a/WordPress/Classes/System/Root View/SplitViewRootPresenter+Welcome.swift +++ b/WordPress/Classes/System/Root View/SplitViewRootPresenter+Welcome.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import SwiftUI +import WordPressData class WelcomeSplitViewContent: SplitViewDisplayable { let supplementary: UINavigationController @@ -9,7 +10,7 @@ class WelcomeSplitViewContent: SplitViewDisplayable { init(addSite: @escaping (AddSiteMenuViewModel.Selection) -> Void) { supplementary = UINavigationController(rootViewController: UnifiedPrologueViewController()) - let addSiteViewModel = AddSiteMenuViewModel(context: .shared, onSelection: addSite) + let addSiteViewModel = AddSiteMenuViewModel(context: ContextManager.shared, onSelection: addSite) let noSitesViewModel = NoSitesViewModel(appUIType: JetpackFeaturesRemovalCoordinator.currentAppUIType, account: nil) let noSiteView = NoSitesView(addSiteViewModel: addSiteViewModel, viewModel: noSitesViewModel) let noSitesVC = UIHostingController(rootView: noSiteView) diff --git a/WordPress/Classes/System/UITesting/UITestConfigurator.swift b/WordPress/Classes/System/UITesting/UITestConfigurator.swift index 5651cf6a8188..cc2d22ec1d12 100644 --- a/WordPress/Classes/System/UITesting/UITestConfigurator.swift +++ b/WordPress/Classes/System/UITesting/UITestConfigurator.swift @@ -37,7 +37,9 @@ struct UITestConfigurator { private static func resetEverything() { // Remove CoreData DB - ContextManager.shared.resetEverything() + // + // We can afford to force cast here because we are in a test-specific code + (ContextManager.shared as! ContextManager).resetEverything() // Clear user defaults. for key in UserDefaults.standard.dictionaryRepresentation().keys { diff --git a/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift b/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift index bd61d575de06..bd04075eda9d 100644 --- a/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift +++ b/WordPress/Classes/Utility/BackgroundTasks/WeeklyRoundupBackgroundTask.swift @@ -330,14 +330,14 @@ class WeeklyRoundupBackgroundTask: BackgroundTask { private let eventTracker: NotificationEventTracker let runDateComponents: DateComponents let notificationScheduler: WeeklyRoundupNotificationScheduler - let coreDataStack: ContextManager + let coreDataStack: CoreDataStackSwift init( eventTracker: NotificationEventTracker = NotificationEventTracker(), runDateComponents: DateComponents? = nil, staticNotificationDateComponents: DateComponents? = nil, store: Store = Store(), - coreDataStack: ContextManager = .shared + coreDataStack: CoreDataStackSwift = ContextManager.shared ) { self.coreDataStack = coreDataStack self.eventTracker = eventTracker diff --git a/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift b/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift index ffb960159f95..3142a83cc955 100644 --- a/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift +++ b/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift @@ -2,6 +2,7 @@ import UIKit import Combine import AutomatticTracks import AutomatticEncryptedLogs +import WordPressData /// A wrapper around the logging stack – provides shared initialization and configuration for Tracks Crash and Event Logging struct WPLoggingStack { @@ -38,9 +39,9 @@ struct WPLoggingStack { } struct WPCrashLoggingDataProvider: CrashLoggingDataProvider { - private let contextManager: ContextManager + private let contextManager: CoreDataStackSwift - init(contextManager: ContextManager = .shared) { + init(contextManager: CoreDataStackSwift = ContextManager.shared) { self.contextManager = contextManager } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/AddSiteController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/AddSiteController.swift index 0772d442d9fd..e7d2f041f6a4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/AddSiteController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/AddSiteController.swift @@ -1,6 +1,7 @@ import UIKit import SwiftUI import WordPressAuthenticator +import WordPressData /// Manages the site creation flows. struct AddSiteController { @@ -80,7 +81,7 @@ struct AddSiteMenuViewModel { } } - init(context: ContextManager = .shared, onSelection: @escaping (Selection) -> Void) { + init(context: CoreDataStackSwift = ContextManager.shared, onSelection: @escaping (Selection) -> Void) { let defaultAccount = try? WPAccount.lookupDefaultWordPressComAccount(in: context.mainContext) let canAddSelfHostedSite = AppConfiguration.showAddSelfHostedSiteButton diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 8ed9d257aa22..540d6d30aa19 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -1,5 +1,6 @@ import CoreData import SwiftUI +import WordPressData import WordPressShared final class BlogListViewModel: NSObject, ObservableObject { @@ -15,7 +16,7 @@ final class BlogListViewModel: NSObject, ObservableObject { private let configuration: BlogListConfiguration private var rawSites: [Blog] = [] private let fetchedResultsController: NSFetchedResultsController - private let contextManager: ContextManager + private let contextManager: CoreDataStackSwift private let blogService: BlogService private let eventTracker: EventTracker private let recentSitesService: RecentSitesService @@ -24,7 +25,7 @@ final class BlogListViewModel: NSObject, ObservableObject { var onAddSiteTapped: (AddSiteMenuViewModel.Selection) -> Void = { _ in } init(configuration: BlogListConfiguration = .defaultConfig, - contextManager: ContextManager = ContextManager.shared, + contextManager: CoreDataStackSwift = ContextManager.shared, recentSitesService: RecentSitesService = RecentSitesService(), eventTracker: EventTracker = DefaultEventTracker()) { self.configuration = configuration diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift index c7b15044b923..e7b45f98db08 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import WordPressData import WordPressUI class CompliancePopoverViewModel: ObservableObject { @@ -13,12 +14,12 @@ class CompliancePopoverViewModel: ObservableObject { private let analyticsTracker: PrivacySettingsAnalyticsTracking private let defaults: UserDefaults - private let contextManager: ContextManager + private let contextManager: CoreDataStackSwift // MARK: - Init init(defaults: UserDefaults, - contextManager: ContextManager, + contextManager: CoreDataStackSwift, analyticsTracker: PrivacySettingsAnalyticsTracking = PrivacySettingsAnalyticsTracker()) { self.defaults = defaults self.analyticsTracker = analyticsTracker diff --git a/WordPress/WordPressTest/SharedDataIssueSolverTests.swift b/WordPress/WordPressTest/SharedDataIssueSolverTests.swift index d17f61ea8ddc..fb45820d88e1 100644 --- a/WordPress/WordPressTest/SharedDataIssueSolverTests.swift +++ b/WordPress/WordPressTest/SharedDataIssueSolverTests.swift @@ -129,10 +129,15 @@ private extension SharedDataIssueSolverTests { final class CoreDataStackMock: CoreDataStack { private(set) var mainContext: NSManagedObjectContext + private static var shared: CoreDataStack? + init(mainContext: NSManagedObjectContext) { self.mainContext = mainContext + CoreDataStackMock.shared = self } + static func sharedInstance() -> any CoreDataStack { CoreDataStackMock.shared! } + func newDerivedContext() -> NSManagedObjectContext { return mainContext } From 53f2e19ff4dc1730401476e4012896466ccb3b55 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 6 Mar 2025 10:54:40 +1100 Subject: [PATCH 23/25] Update `.editorconfig` to indent Swift with 4 spaces outside Xcode --- .editorconfig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.editorconfig b/.editorconfig index db1bf43b9db9..7f956a19cc9d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,23 @@ +root = true + # Apply to all files [*] +indent_style = space +indent_size = 2 end_of_line = lf insert_final_newline = true +charset = utf-8 trim_trailing_whitespace = true +[*.md] +trim_trailing_whitespace = false + +[*.swift] +indent_size = 4 + +[{*.h,*.m}] +indent_size = 4 + # Ruby specific rules [{*.rb,Fastfile,Gemfile}] indent_style = space From 71059b1fdb18b147b3e60752f217f9789bf017eb Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 12 Mar 2025 15:14:39 +1100 Subject: [PATCH 24/25] Hack code in `Blog.m` to attempt compilation --- .../Sources/WordPressDataObjC/Models/Blog.m | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/Modules/Sources/WordPressDataObjC/Models/Blog.m b/Modules/Sources/WordPressDataObjC/Models/Blog.m index a5cd07513eed..a856ec7591da 100644 --- a/Modules/Sources/WordPressDataObjC/Models/Blog.m +++ b/Modules/Sources/WordPressDataObjC/Models/Blog.m @@ -3,9 +3,9 @@ #import "AccountService.h" @import WordPressDataObjC; // FIXME: These imports break the compilation and we've reached the point where they're so many it's time to address them, or at least try... -#import "Constants.h" -#import "WPUserAgent.h" -#import "WordPress-Swift.h" +//#import "Constants.h" +//#import "WPUserAgent.h" +//#import "WordPress-Swift.h" @import SFHFKeychainUtils; @import NSObject_SafeExpectations; @@ -123,8 +123,9 @@ - (void)prepareForDeletion [self deleteApplicationToken]; } - [_xmlrpcApi invalidateAndCancelTasks]; - [_selfHostedSiteRestApi invalidateAndCancelTasks]; + // FIXME: +// [_xmlrpcApi invalidateAndCancelTasks]; +// [_selfHostedSiteRestApi invalidateAndCancelTasks]; // Remove the self-hosted site cookies from the shared cookie storage. if (self.account == nil && self.url != nil) { @@ -574,9 +575,9 @@ - (BOOL)supports:(BlogFeature)feature return [self supportsSharing]; case BlogFeatureOAuth2Login: return [self isHostedAtWPcom]; - case BlogFeatureMentions: + case BlogFeatureMentions: // TODO: is this used? return [self isAccessibleThroughWPCom]; - case BlogFeatureXposts: + case BlogFeatureXposts: // TODO: is this used? return [self isAccessibleThroughWPCom]; case BlogFeatureReblog: case BlogFeaturePlans: @@ -898,36 +899,39 @@ - (NSString *)stateDescription #pragma mark - api accessor -- (WordPressOrgXMLRPCApi *)xmlrpcApi -{ - NSURL *xmlRPCEndpoint = [NSURL URLWithString:self.xmlrpc]; - if (_xmlrpcApi == nil) { - if (xmlRPCEndpoint != nil) { - _xmlrpcApi = [[WordPressOrgXMLRPCApi alloc] initWithEndpoint:xmlRPCEndpoint - userAgent:[WPUserAgent wordPressUserAgent]]; - } - } - return _xmlrpcApi; -} - -- (WordPressOrgRestApi *)selfHostedSiteRestApi -{ - if (_selfHostedSiteRestApi == nil) { - _selfHostedSiteRestApi = self.account == nil ? [[WordPressOrgRestApi alloc] initWithBlog:self] : nil; - } - return _selfHostedSiteRestApi; -} - -- (WordPressComRestApi *)wordPressComRestApi -{ - if (self.account) { - return self.account.wordPressComRestApi; - } - return nil; -} +// FIXME: +//- (WordPressOrgXMLRPCApi *)xmlrpcApi +//{ +// NSURL *xmlRPCEndpoint = [NSURL URLWithString:self.xmlrpc]; +// if (_xmlrpcApi == nil) { +// if (xmlRPCEndpoint != nil) { +// _xmlrpcApi = [[WordPressOrgXMLRPCApi alloc] initWithEndpoint:xmlRPCEndpoint +// userAgent:[WPUserAgent wordPressUserAgent]]; +// } +// } +// return _xmlrpcApi; +//} +// +//- (WordPressOrgRestApi *)selfHostedSiteRestApi +//{ +// if (_selfHostedSiteRestApi == nil) { +// _selfHostedSiteRestApi = self.account == nil ? [[WordPressOrgRestApi alloc] initWithBlog:self] : nil; +// } +// return _selfHostedSiteRestApi; +//} +// +//- (WordPressComRestApi *)wordPressComRestApi +//{ +// if (self.account) { +// return self.account.wordPressComRestApi; +// } +// return nil; +//} - (BOOL)isAccessibleThroughWPCom { - return self.wordPressComRestApi != nil; + // FIXME: + // return self.wordPressComRestApi != nil; + return YES; } - (BOOL)supportsRestApi { From 2421b896b9d5c2e95a7d923282825459d45f18a3 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 12 Mar 2025 15:18:50 +1100 Subject: [PATCH 25/25] =?UTF-8?q?Move=20even=20more=20files=20=E2=80=94=20?= =?UTF-8?q?App=20still=20does=20not=20compile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Modules/Package.swift | 1 + .../{ => Services}/AccountService.m | 2 +- .../{ => Services}/BlogService.m | 4 +- .../Services/CommentService.m | 1375 +++++++++++++++++ .../{ => Services}/CoreDataService.m | 0 .../Services/PostCategoryService.m | 0 .../include/CommentService.h | 207 +++ .../include}/PostCategoryService.h | 0 8 files changed, 1586 insertions(+), 3 deletions(-) rename Modules/Sources/WordPressDataObjC/{ => Services}/AccountService.m (99%) rename Modules/Sources/WordPressDataObjC/{ => Services}/BlogService.m (99%) create mode 100644 Modules/Sources/WordPressDataObjC/Services/CommentService.m rename Modules/Sources/WordPressDataObjC/{ => Services}/CoreDataService.m (100%) rename {WordPress/Classes => Modules/Sources/WordPressDataObjC}/Services/PostCategoryService.m (100%) create mode 100644 Modules/Sources/WordPressDataObjC/include/CommentService.h rename {WordPress/Classes/Services => Modules/Sources/WordPressDataObjC/include}/PostCategoryService.h (100%) diff --git a/Modules/Package.swift b/Modules/Package.swift index 550ccd0edef6..f1dc01182051 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -74,6 +74,7 @@ let package = Package( .target( name: "WordPressDataObjC", dependencies: [ + "SFHFKeychainUtils", .target(name: "WordPressShared"), .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), ] diff --git a/Modules/Sources/WordPressDataObjC/AccountService.m b/Modules/Sources/WordPressDataObjC/Services/AccountService.m similarity index 99% rename from Modules/Sources/WordPressDataObjC/AccountService.m rename to Modules/Sources/WordPressDataObjC/Services/AccountService.m index 45f19e57fa45..3f51dbb9a063 100644 --- a/Modules/Sources/WordPressDataObjC/AccountService.m +++ b/Modules/Sources/WordPressDataObjC/Services/AccountService.m @@ -6,7 +6,7 @@ @import WordPressKit; @import WordPressShared; -#import "WordPress-Swift.h" +//#import "WordPress-Swift.h" static NSString * const DefaultDotcomAccountUUIDDefaultsKey = @"AccountDefaultDotcomUUID"; static NSString * const DefaultDotcomAccountPasswordRemovedKey = @"DefaultDotcomAccountPasswordRemovedKey"; diff --git a/Modules/Sources/WordPressDataObjC/BlogService.m b/Modules/Sources/WordPressDataObjC/Services/BlogService.m similarity index 99% rename from Modules/Sources/WordPressDataObjC/BlogService.m rename to Modules/Sources/WordPressDataObjC/Services/BlogService.m index e945ce1f2da7..74e41429537c 100644 --- a/Modules/Sources/WordPressDataObjC/BlogService.m +++ b/Modules/Sources/WordPressDataObjC/Services/BlogService.m @@ -3,12 +3,12 @@ #import "WPAccount.h" #import "AccountService.h" @import WordPressDataObjC; -#import "WPError.h" +//#import "WPError.h" #import "Media.h" #import "PostCategoryService.h" #import "CommentService.h" #import "PostService.h" -#import "WordPress-Swift.h" +//#import "WordPress-Swift.h" #import "PostType.h" @import WordPressKit; @import WordPressShared; diff --git a/Modules/Sources/WordPressDataObjC/Services/CommentService.m b/Modules/Sources/WordPressDataObjC/Services/CommentService.m new file mode 100644 index 000000000000..a2262b2520b1 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/Services/CommentService.m @@ -0,0 +1,1375 @@ +#import "CommentService.h" +#import "AccountService.h" +#import "Blog.h" +@import WordPressDataObjC; +#import "ReaderPost.h" +#import "WPAccount.h" +#import "PostService.h" +#import "AbstractPost.h" +#import "WordPress-Swift.h" + +@import WordPressShared; + +NSUInteger const WPTopLevelHierarchicalCommentsPerPage = 20; +NSInteger const WPNumberOfCommentsToSync = 100; +static NSTimeInterval const CommentsRefreshTimeoutInSeconds = 60 * 5; // 5 minutes + +@interface CommentService () + +@property (nonnull, strong, nonatomic) CommentServiceRemoteFactory *remoteFactory; + +@end + +@implementation CommentService + +- (instancetype)initWithCoreDataStack:(id)coreDataStack +{ + return [self initWithCoreDataStack:coreDataStack commentServiceRemoteFactory:[CommentServiceRemoteFactory new]]; +} + +- (instancetype)initWithCoreDataStack:(id)coreDataStack + commentServiceRemoteFactory:(CommentServiceRemoteFactory *)remoteFactory +{ + self = [super initWithCoreDataStack:coreDataStack]; + if (self) { + self.remoteFactory = remoteFactory; + } + return self; +} + ++ (NSMutableSet *)syncingCommentsLocks +{ + static NSMutableSet *syncingCommentsLocks; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + syncingCommentsLocks = [NSMutableSet set]; + }); + return syncingCommentsLocks; +} + ++ (BOOL)isSyncingCommentsForBlog:(Blog *)blog { + return [self isSyncingCommentsForBlogID:blog.objectID]; +} + ++ (BOOL)isSyncingCommentsForBlogID:(NSManagedObjectID *)blogID +{ + NSParameterAssert(blogID); + return [[self syncingCommentsLocks] containsObject:blogID]; +} + ++ (BOOL)startSyncingCommentsForBlog:(NSManagedObjectID *)blogID +{ + NSParameterAssert(blogID); + @synchronized([self syncingCommentsLocks]) { + if ([self isSyncingCommentsForBlogID:blogID]){ + return NO; + } + [[self syncingCommentsLocks] addObject:blogID]; + return YES; + } +} + ++ (void)stopSyncingCommentsForBlog:(NSManagedObjectID *)blogID +{ + NSParameterAssert(blogID); + @synchronized([self syncingCommentsLocks]) { + [[self syncingCommentsLocks] removeObject:blogID]; + } +} + ++ (BOOL)shouldRefreshCacheFor:(Blog *)blog +{ + NSDate *lastSynced = blog.lastCommentsSync; + BOOL isSyncing = [self isSyncingCommentsForBlog:blog]; + return !isSyncing && (lastSynced == nil || ABS(lastSynced.timeIntervalSinceNow) > CommentsRefreshTimeoutInSeconds); +} + +#pragma mark Public methods + +#pragma mark Blog-centric methods + +// Create comment +- (Comment *)createCommentForBlog:(Blog *)blog +{ + NSParameterAssert(blog.managedObjectContext != nil); + + Comment *comment = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Comment class]) + inManagedObjectContext:blog.managedObjectContext]; + comment.dateCreated = [NSDate new]; + comment.blog = blog; + return comment; +} + +// Create reply +- (void)createReplyForComment:(Comment *)comment content:(NSString *)content completion:(void (^)(Comment *reply))completion +{ + NSManagedObjectID *parentCommentID = comment.objectID; + NSManagedObjectID * __block replyID = nil; + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *comment = [context existingObjectWithID:parentCommentID error:nil]; + Comment *reply = [self createCommentForBlog:comment.blog]; + reply.postID = comment.postID; + reply.post = comment.post; + reply.parentID = comment.commentID; + reply.status = [Comment descriptionFor:CommentStatusTypeApproved]; + reply.content = content; + [context obtainPermanentIDsForObjects:@[reply] error:nil]; + replyID = reply.objectID; + } completion:^{ + if (completion) { + completion([self.coreDataStack.mainContext existingObjectWithID:replyID error:nil]); + } + } onQueue:dispatch_get_main_queue()]; +} + +// Sync comments +- (void)syncCommentsForBlog:(Blog *)blog + withStatus:(CommentStatusFilter)status + success:(void (^)(BOOL hasMore))success + failure:(void (^)(NSError *error))failure +{ + [self syncCommentsForBlog:blog withStatus:status filterUnreplied:NO success:success failure:failure]; +} + +- (void)syncCommentsForBlog:(Blog *)blog + withStatus:(CommentStatusFilter)status + filterUnreplied:(BOOL)filterUnreplied + success:(void (^)(BOOL hasMore))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *blogID = blog.objectID; + if (![[self class] startSyncingCommentsForBlog:blogID]){ + // We assume success because a sync is already running and it will change the comments + if (success) { + success(YES); + } + return; + } + + // If the comment status is not specified, default to all. + CommentStatusFilter commentStatus = status ?: CommentStatusFilterAll; + NSDictionary *options = @{ @"status": [NSNumber numberWithInt:commentStatus] }; + + id remote = [self remoteForBlog:blog]; + + [remote getCommentsWithMaximumCount:WPNumberOfCommentsToSync + options:options + success:^(NSArray *comments) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = [context existingObjectWithID:blogID error:nil]; + if (!blog) { + return; + } + + NSArray *fetchedComments = comments; + if (filterUnreplied) { + NSString *author = @""; + if (blog.account) { + // See if there is a linked Jetpack user that we should use. + BlogAuthor *blogAuthor = [blog getAuthorWithLinkedID:blog.account.userID]; + author = (blogAuthor) ? blogAuthor.email : blog.account.email; + } else { + BlogAuthor *blogAuthor = [blog getAuthorWithId:blog.userID]; + author = (blogAuthor) ? blogAuthor.email : author; + } + fetchedComments = [self filterUnrepliedComments:comments forAuthor:author]; + } + [self mergeComments:fetchedComments forBlog:blog purgeExisting:YES]; + blog.lastCommentsSync = [NSDate date]; + } completion:^{ + [[self class] stopSyncingCommentsForBlog:blogID]; + + if (success) { + // Note: + // We'll assume that if the requested page size couldn't be filled, there are no + // more comments left to retrieve. However, for unreplied comments, we only fetch the first page (for now). + BOOL hasMore = comments.count >= WPNumberOfCommentsToSync && !filterUnreplied; + success(hasMore); + } + } onQueue:dispatch_get_main_queue()]; + } failure:^(NSError *error) { + [[self class] stopSyncingCommentsForBlog:blogID]; + + if (failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + } + }]; +} + +- (NSArray *)filterUnrepliedComments:(NSArray *)comments forAuthor:(NSString *)author { + NSMutableArray *marr = [comments mutableCopy]; + + NSMutableArray *foundIDs = [NSMutableArray array]; + NSMutableArray *discardables = [NSMutableArray array]; + + // get ids of comments that user has replied to. + for (RemoteComment *comment in marr) { + if (![comment.authorEmail isEqualToString:author] || !comment.parentID) { + continue; + } + [foundIDs addObject:comment.parentID]; + [discardables addObject:comment]; + } + // Discard the replies, they aren't needed. + [marr removeObjectsInArray:discardables]; + [discardables removeAllObjects]; + + // Get the parents, grandparents etc. and discard those too. + while ([foundIDs count] > 0) { + NSArray *needles = [foundIDs copy]; + [foundIDs removeAllObjects]; + for (RemoteComment *comment in marr) { + if ([needles containsObject:comment.commentID]) { + if (comment.parentID) { + [foundIDs addObject:comment.parentID]; + } + [discardables addObject:comment]; + } + } + // Discard the matches, and keep looking if items were found. + [marr removeObjectsInArray:discardables]; + [discardables removeAllObjects]; + } + + // remove any remaining child comments. + // remove any remaining root comments made by the user. + for (RemoteComment *comment in marr) { + if (comment.parentID.intValue != 0) { + [discardables addObject:comment]; + } else if ([comment.authorEmail isEqualToString:author]) { + [discardables addObject:comment]; + } + } + [marr removeObjectsInArray:discardables]; + + // these are the most recent unreplied comments from other users. + return [NSArray arrayWithArray:marr]; +} + +- (Comment *)oldestCommentForBlog:(Blog *)blog { + NSParameterAssert(blog.managedObjectContext != nil); + + NSString *entityName = NSStringFromClass([Comment class]); + NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName]; + request.predicate = [NSPredicate predicateWithFormat:@"dateCreated != NULL && blog=%@", blog]; + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"dateCreated" ascending:YES]; + request.sortDescriptors = @[sortDescriptor]; + + Comment * __block oldestComment = nil; + [blog.managedObjectContext performBlockAndWait:^{ + oldestComment = [[blog.managedObjectContext executeFetchRequest:request error:nil] firstObject]; + }]; + return oldestComment; +} + +- (void)loadMoreCommentsForBlog:(Blog *)blog + withStatus:(CommentStatusFilter)status + success:(void (^)(BOOL hasMore))success + failure:(void (^)(NSError *))failure +{ + NSManagedObjectID *blogID = blog.objectID; + if (![[self class] startSyncingCommentsForBlog:blogID]){ + // We assume success because a sync is already running and it will change the comments + if (success) { + success(YES); + } + } + + NSMutableDictionary *options = [NSMutableDictionary dictionary]; + + // If the comment status is not specified, default to all. + CommentStatusFilter commentStatus = status ?: CommentStatusFilterAll; + options[@"status"] = [NSNumber numberWithInt:commentStatus]; + + id remote = [self remoteForBlog:blog]; + + if ([remote isKindOfClass:[CommentServiceRemoteREST class]]) { + Comment *oldestComment = [self oldestCommentForBlog:blog]; + if (oldestComment.dateCreated) { + options[@"before"] = [oldestComment.dateCreated WordPressComJSONString]; + options[@"order"] = @"desc"; + } + } else if ([remote isKindOfClass:[CommentServiceRemoteXMLRPC class]]) { + NSUInteger commentCount = [blog.comments count]; + options[@"offset"] = @(commentCount); + } + + [remote getCommentsWithMaximumCount:WPNumberOfCommentsToSync + options:options + success:^(NSArray *comments) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = [context existingObjectWithID:blogID error:nil]; + if (!blog) { + return; + } + [self mergeComments:comments forBlog:blog purgeExisting:NO]; + } completion:^{ + [[self class] stopSyncingCommentsForBlog:blogID]; + if (success) { + success(comments.count > 1); + } + } onQueue:dispatch_get_main_queue()]; + } failure:^(NSError *error) { + [[self class] stopSyncingCommentsForBlog:blogID]; + if (failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + } + }]; +} + +- (void)loadCommentWithID:(NSNumber *)commentID + forBlog:(Blog *)blog + success:(void (^)(Comment *comment))success + failure:(void (^)(NSError *))failure { + + NSManagedObjectID *blogID = blog.objectID; + id remote = [self remoteForBlog:blog]; + + [remote getCommentWithID:commentID + success:^(RemoteComment *remoteComment) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = [context existingObjectWithID:blogID error:nil]; + if (!blog) { + return; + } + + Comment *comment = [blog commentWithID:remoteComment.commentID]; + if (!comment) { + comment = [self createCommentForBlog:blog]; + } + + [self updateComment:comment withRemoteComment:remoteComment]; + } completion:^{ + if (success) { + [self.coreDataStack.mainContext performBlock:^{ + Blog *blog = [self.coreDataStack.mainContext existingObjectWithID:blogID error:nil]; + success([blog commentWithID:remoteComment.commentID]); + }]; + } + } onQueue:dispatch_get_main_queue()]; + } failure:^(NSError *error) { + DDLogError(@"Error loading comment for blog: %@", error); + if (failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + } + }]; +} + +- (void)loadCommentWithID:(NSNumber *)commentID + forPost:(ReaderPost *)post + success:(void (^)(Comment *comment))success + failure:(void (^)(NSError *))failure { + + NSManagedObjectID *postID = post.objectID; + CommentServiceRemoteREST *service = [self restRemoteForSite:post.siteID]; + + [service getCommentWithID:commentID + success:^(RemoteComment *remoteComment) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + ReaderPost *post = [context existingObjectWithID:postID error:nil]; + if (!post) { + return; + } + + Comment *comment = [post commentWithID:remoteComment.commentID]; + + if (!comment) { + comment = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Comment class]) inManagedObjectContext:context]; + comment.dateCreated = [NSDate new]; + } + + comment.post = post; + [self updateComment:comment withRemoteComment:remoteComment]; + } completion:^{ + if (success) { + [self.coreDataStack.mainContext performBlock:^{ + ReaderPost *post = [self.coreDataStack.mainContext existingObjectWithID:postID error:nil]; + success([post commentWithID:remoteComment.commentID]); + }]; + } + } onQueue:dispatch_get_main_queue()]; + } failure:^(NSError *error) { + DDLogError(@"Error loading comment for post: %@", error); + if (failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + } + }]; +} + +// Upload comment +- (void)uploadComment:(Comment *)comment + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + id remote = [self remoteForComment:comment]; + RemoteComment *remoteComment = [self remoteCommentWithComment:comment]; + + NSManagedObjectID *commentObjectID = comment.objectID; + void (^successBlock)(RemoteComment *comment) = ^(RemoteComment *comment) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:commentObjectID error:nil]; + if (commentInContext) { + [self updateComment:commentInContext withRemoteComment:comment]; + } + } completion:success onQueue:dispatch_get_main_queue()]; + }; + + if (comment.commentID != 0) { + [remote updateComment:remoteComment + success:successBlock + failure:failure]; + } else { + [remote createComment:remoteComment + success:successBlock + failure:failure]; + } +} + +// Approve +- (void)approveComment:(Comment *)comment + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + [self moderateComment:comment + withStatus:CommentStatusTypeApproved + success:success + failure:failure]; +} + +// Unapprove +- (void)unapproveComment:(Comment *)comment + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + [self moderateComment:comment + withStatus:CommentStatusTypePending + success:success + failure:failure]; +} + +// Spam +- (void)spamComment:(Comment *)comment + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + + // If the Comment is not permanently deleted, don't remove it from the local cache as it can still be displayed. + if (!comment.deleteWillBePermanent) { + [self moderateComment:comment + withStatus:CommentStatusTypeSpam + success:success + failure:failure]; + + return; + } + + NSManagedObjectID *commentID = comment.objectID; + + [self moderateComment:comment + withStatus:CommentStatusTypeSpam + success:^{ + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:commentID error:nil]; + if (commentInContext != nil){ + [context deleteObject:commentInContext]; + } + } completion:success onQueue:dispatch_get_main_queue()]; + } failure: failure]; +} + +// Trash comment +- (void)trashComment:(Comment *)comment + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + [self moderateComment:comment + withStatus:CommentStatusTypeUnapproved + success:success + failure:failure]; +} + +// Delete comment +- (void)deleteComment:(Comment *)comment + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + // If this comment is local only, just delete. No need to query the endpoint or do any other work. + if (comment.commentID == 0) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:comment.objectID error:nil]; + if (commentInContext != nil) { + [context deleteObject:commentInContext]; + } + } completion:success onQueue:dispatch_get_main_queue()]; + return; + } + + RemoteComment *remoteComment = [self remoteCommentWithComment:comment]; + id remote = [self remoteForBlog:comment.blog]; + + // If the Comment is not permanently deleted, don't remove it from the local cache as it can still be displayed. + if (!comment.deleteWillBePermanent) { + [remote trashComment:remoteComment success:success failure:failure]; + return; + } + + // For the best user experience we want to optimistically delete the comment. + // However, if there is an error we need to restore it. + NSManagedObjectID *blogObjID = comment.blog.objectID; + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:comment.objectID error:nil]; + if (commentInContext != nil) { + [context deleteObject:commentInContext]; + } + } completion:^{ + [remote trashComment:remoteComment success:success failure:^(NSError *error) { + // Failure. Restore the comment. + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Blog *blog = [context objectWithID:blogObjID]; + if (!blog) { + return; + } + + Comment *comment = [self createCommentForBlog:blog]; + [self updateComment:comment withRemoteComment:remoteComment]; + } completion:^{ + if (failure) { + failure(error); + } + } onQueue:dispatch_get_main_queue()]; + }]; + } onQueue:dispatch_get_main_queue()]; +} + +#pragma mark - Post-centric methods + +- (void)syncHierarchicalCommentsForPost:(ReaderPost *)post + page:(NSUInteger)page + success:(void (^)(BOOL hasMore, NSNumber *totalComments))success + failure:(void (^)(NSError *error))failure +{ + [self syncHierarchicalCommentsForPost:post + page:page + topLevelComments:WPTopLevelHierarchicalCommentsPerPage + success:success + failure:failure]; +} + +- (void)syncHierarchicalCommentsForPost:(ReaderPost *)post + topLevelComments:(NSUInteger)number + success:(void (^)(BOOL hasMore, NSNumber *totalComments))success + failure:(void (^)(NSError *error))failure +{ + [self syncHierarchicalCommentsForPost:post + page:1 + topLevelComments:number + success:success + failure:failure]; +} + +- (void)syncHierarchicalCommentsForPost:(ReaderPost *)post + page:(NSUInteger)page + topLevelComments:(NSUInteger)number + success:(void (^)(BOOL hasMore, NSNumber *totalComments))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *postObjectID = post.objectID; + NSNumber *siteID = post.siteID; + NSNumber *postID = post.postID; + + NSUInteger commentsPerPage = number ?: WPTopLevelHierarchicalCommentsPerPage; + NSUInteger pageNumber = page ?: 1; + + CommentServiceRemoteREST *service = [self restRemoteForSite:siteID]; + [service syncHierarchicalCommentsForPost:postID + page:pageNumber + number:commentsPerPage + success:^(NSArray *comments, NSNumber *totalComments) { + BOOL __block includesNewComments = NO; + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + NSError *error; + ReaderPost *aPost = [context existingObjectWithID:postObjectID error:&error]; + if (!aPost) { + if (failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + } + return; + } + + includesNewComments = [self mergeHierarchicalComments:comments forPage:page forPost:aPost]; + } completion:^{ + if (!success) { + return; + } + + [self.coreDataStack.mainContext performBlock:^{ + NSError *error; + ReaderPost *aPost = [self.coreDataStack.mainContext existingObjectWithID:postObjectID error:&error]; + if (!aPost) { + if (failure) { + failure(error); + } + return; + } + + // There are no more comments when: + // - There are fewer top level comments in the results than requested + // - Page > 1, the number of top level comments matches those requested, but there are no new comments + // We check this way because the API can return the last page of results instead + // of returning zero results when the requested page is the last + 1. + NSArray *parents = [self topLevelCommentsForPage:page forPost:aPost]; + BOOL hasMore = YES; + if (([parents count] < WPTopLevelHierarchicalCommentsPerPage) || (page > 1 && !includesNewComments)) { + hasMore = NO; + } + + success(hasMore, totalComments); + }]; + } onQueue:dispatch_get_main_queue()]; + } failure:^(NSError *error) { + if (failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + } + }]; +} + +- (NSInteger)numberOfHierarchicalPagesSyncedforPost:(ReaderPost *)post +{ + NSSet *topComments = [post.comments filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"parentID = 0"]]; + CGFloat page = [topComments count] / WPTopLevelHierarchicalCommentsPerPage; + return (NSInteger)page; +} + +#pragma mark - REST Helpers + +- (NSString *)sanitizeCommentContent:(NSString *)string isPrivateSite:(BOOL)isPrivateSite +{ + NSString *content = string; + content = [RichContentFormatter removeTrailingBreakTags:content]; + content = [RichContentFormatter formatContentString:content isPrivateSite:isPrivateSite]; + return content; +} + +// Edition +- (void)updateCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + content:(NSString *)content + success:(void (^)(RemoteComment *comment))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote updateCommentWithID:commentID + content:content + success:success + failure:failure]; +} + +// Replies +- (void)replyToPost:(ReaderPost *)post + content:(NSString *)content + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + // Create and optimistically save a comment, based on the current wpcom acct + // post and content provided. + BOOL isPrivateSite = post.isBlogPrivate; + [self createHierarchicalCommentWithContent:content withParent:nil postObjectID:post.objectID siteID:post.siteID completion:^(NSManagedObjectID *commentID) { + if (!commentID) { + NSError *error = [NSError errorWithDomain:WKErrorDomain code:WKErrorUnknown userInfo:@{NSDebugDescriptionErrorKey: @"Failed to create a comment for a post"}]; + if (failure) { + failure(error); + } + [WordPressAppDelegate logError:error]; + return; + } + void (^successBlock)(RemoteComment *remoteComment) = ^void(RemoteComment *remoteComment) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *comment = [context existingObjectWithID:commentID error:nil]; + if (!comment) { + return; + } + + remoteComment.content = [self sanitizeCommentContent:remoteComment.content isPrivateSite:isPrivateSite]; + + [self updateHierarchicalComment:comment withRemoteComment:remoteComment]; + } completion:success onQueue:dispatch_get_main_queue()]; + }; + + void (^failureBlock)(NSError *error) = ^void(NSError *error) { + // Remove the optimistically saved comment. + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:commentID error:nil]; + if (commentInContext != nil) { + [context deleteObject:commentInContext]; + } + } completion:^{ + if (failure) { + failure(error); + } + } onQueue:dispatch_get_main_queue()]; + }; + + CommentServiceRemoteREST *remote = [self restRemoteForSite:post.siteID]; + [remote replyToPostWithID:post.postID + content:content + success:successBlock + failure:failureBlock]; + }]; +} + +- (void)replyToHierarchicalCommentWithID:(NSNumber *)commentID + post:(ReaderPost *)post + content:(NSString *)content + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + // Create and optimistically save a comment, based on the current wpcom acct + // post and content provided. + BOOL isPrivateSite = post.isBlogPrivate; + [self createHierarchicalCommentWithContent:content withParent:nil postObjectID:post.objectID siteID:post.siteID completion:^(NSManagedObjectID *commentObjectID) { + if (!commentObjectID) { + NSError *error = [NSError errorWithDomain:WKErrorDomain code:WKErrorUnknown userInfo:@{NSDebugDescriptionErrorKey: @"Failed to create a comment for a post"}]; + if (failure) { + failure(error); + } + [WordPressAppDelegate logError:error]; + return; + } + void (^successBlock)(RemoteComment *remoteComment) = ^void(RemoteComment *remoteComment) { + // Update and save the comment + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *comment = [context existingObjectWithID:commentObjectID error:nil]; + if (!comment) { + return; + } + + remoteComment.content = [self sanitizeCommentContent:remoteComment.content isPrivateSite:isPrivateSite]; + + [self updateHierarchicalComment:comment withRemoteComment:remoteComment]; + } completion:success onQueue:dispatch_get_main_queue()]; + }; + + void (^failureBlock)(NSError *error) = ^void(NSError *error) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:commentObjectID error:nil]; + if (!commentInContext) { + return; + } + // Remove the optimistically saved comment. + [context deleteObject:commentInContext]; + ReaderPost *post = (ReaderPost *)commentInContext.post; + post.commentCount = @([post.commentCount integerValue] - 1); + } completion:^{ + if (failure) { + failure(error); + } + } onQueue:dispatch_get_main_queue()]; + }; + + CommentServiceRemoteREST *remote = [self restRemoteForSite:post.siteID]; + [remote replyToCommentWithID:commentID + content:content + success:successBlock + failure:failureBlock]; + }]; +} + +- (void)replyToCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + content:(NSString *)content + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote replyToCommentWithID:commentID + content:content + success:^(RemoteComment * __unused comment){ + if (success){ + success(); + } + } + failure:failure]; +} + + +// Likes +- (void)likeCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote likeCommentWithID:commentID + success:success + failure:failure]; +} + +- (void)unlikeCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote unlikeCommentWithID:commentID + success:success + failure:failure]; +} + +// Moderation +- (void)approveCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote moderateCommentWithID:commentID + status:@"approved" + success:success + failure:failure]; +} + +- (void)unapproveCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote moderateCommentWithID:commentID + status:@"unapproved" + success:success + failure:failure]; +} + +- (void)spamCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote moderateCommentWithID:commentID + status:[Comment descriptionFor:CommentStatusTypeSpam] + success:success + failure:failure]; +} + +- (void)deleteCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + CommentServiceRemoteREST *remote = [self restRemoteForSite:siteID]; + [remote trashCommentWithID:commentID + success:success + failure:failure]; +} + +- (void)toggleLikeStatusForComment:(Comment *)comment + siteID:(NSNumber *)siteID + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + NSManagedObjectID *commentObjectID = comment.objectID; + BOOL isLikedOriginally = comment.isLiked; + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + // toggle the like status and change the like count and save it + Comment *comment = [context existingObjectWithID:commentObjectID error:nil]; + comment.isLiked = !isLikedOriginally; + comment.likeCount = comment.likeCount + (comment.isLiked ? 1 : -1); + } completion:^{ + // This block will reverse the like/unlike action + void (^failureBlock)(NSError *) = ^(NSError *error) { + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *comment = [context existingObjectWithID:commentObjectID error:nil]; + DDLogError(@"Error while %@ comment: %@", comment.isLiked ? @"liking" : @"unliking", error); + + comment.isLiked = isLikedOriginally; + comment.likeCount = comment.likeCount + (comment.isLiked ? 1 : -1); + } completion:^{ + if (failure) { + failure(error); + } + } onQueue:dispatch_get_main_queue()]; + }; + + NSNumber *commentID = [NSNumber numberWithInt:comment.commentID]; + + if (!isLikedOriginally) { + [self likeCommentWithID:commentID siteID:siteID success:success failure:failureBlock]; + } + else { + [self unlikeCommentWithID:commentID siteID:siteID success:success failure:failureBlock]; + } + } onQueue:dispatch_get_main_queue()]; +} + + +#pragma mark - Private methods + +// Deletes orphaned comments. Does not save context. +- (void)deleteUnownedCommentsInContext:(NSManagedObjectContext *)context +{ + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([Comment class])]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"post = NULL && blog = NULL"]; + + NSError *error; + NSArray *results = [context executeFetchRequest:fetchRequest error:&error]; + if (error) { + DDLogError(@"Error fetching orphaned comments: %@", error); + } + for (Comment *comment in results) { + [context deleteObject:comment]; + } +} + + + + +#pragma mark - Blog centric methods +// Generic moderation +- (void)moderateComment:(Comment *)comment + withStatus:(CommentStatusType)status + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + NSString *currentStatus = [Comment descriptionFor:status]; + NSString *prevStatus = comment.status; + + if ([prevStatus isEqualToString:currentStatus]) { + if (success) { + success(); + } + return; + } + + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:comment.objectID error:nil]; + commentInContext.status = currentStatus; + }]; + + comment.status = currentStatus; + + id remote = [self remoteForComment:comment]; + RemoteComment *remoteComment = [self remoteCommentWithComment:comment]; + [remote moderateComment:remoteComment + success:^(RemoteComment * __unused comment) { + if (success) { + success(); + } + } failure:^(NSError *error) { + DDLogError(@"Error moderating comment: %@", error); + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + Comment *commentInContext = [context existingObjectWithID:comment.objectID error:nil]; + commentInContext.status = prevStatus; + } completion:^{ + if (failure) { + failure(error); + } + } onQueue:dispatch_get_main_queue()]; + }]; +} + +- (void)mergeComments:(NSArray *)comments + forBlog:(Blog *)blog + purgeExisting:(BOOL)purgeExisting +{ + NSParameterAssert(blog.managedObjectContext != nil); + + NSMutableArray *commentsToKeep = [NSMutableArray array]; + for (RemoteComment *remoteComment in comments) { + Comment *comment = [blog commentWithID:remoteComment.commentID]; + if (!comment) { + comment = [self createCommentForBlog:blog]; + } + [self updateComment:comment withRemoteComment:remoteComment]; + [commentsToKeep addObject:comment]; + } + + if (purgeExisting) { + NSSet *existingComments = blog.comments; + if (existingComments.count > 0) { + for (Comment *comment in existingComments) { + // Don't delete unpublished comments + if (![commentsToKeep containsObject:comment] && comment.commentID != 0) { + DDLogInfo(@"Deleting Comment: %@", comment); + [blog.managedObjectContext deleteObject:comment]; + } + } + } + } + + [self deleteUnownedCommentsInContext:blog.managedObjectContext]; +} + +#pragma mark - Post centric methods + +- (NSMutableArray *)ancestorsForCommentWithParentID:(NSNumber *)parentID andCurrentAncestors:(NSArray *)currentAncestors +{ + NSMutableArray *ancestors = [currentAncestors mutableCopy]; + + // Calculate hierarchy and depth. + if (parentID.intValue != 0) { + if ([ancestors containsObject:parentID]) { + NSUInteger index = [ancestors indexOfObject:parentID] + 1; + NSArray *subarray = [ancestors subarrayWithRange:NSMakeRange(0, index)]; + [ancestors removeAllObjects]; + [ancestors addObjectsFromArray:subarray]; + } else { + [ancestors addObject:parentID]; + } + } else { + [ancestors removeAllObjects]; + } + return ancestors; +} + +- (NSString *)hierarchyFromAncestors:(NSArray *)ancestors andCommentID:(NSNumber *)commentID +{ + NSArray *arr = [ancestors arrayByAddingObject:commentID]; + arr = [self formatHierarchyElements:arr]; + return [arr componentsJoinedByString:@"."]; +} + +- (NSArray *)formatHierarchyElements:(NSArray *)hierarchy +{ + NSMutableArray *arr = [NSMutableArray array]; + for (NSNumber *commentID in hierarchy) { + [arr addObject:[self formattedHierarchyElement:commentID]]; + } + return arr; +} + +- (NSString *)formattedHierarchyElement:(NSNumber *)commentID +{ + return [NSString stringWithFormat:@"%010u", [commentID integerValue]]; +} + +- (void)createHierarchicalCommentWithContent:(NSString *)content + withParent:(NSNumber *)parentID + postObjectID:(NSManagedObjectID *)postObjectID + siteID:(NSNumber *)siteID + completion:(void (^)(NSManagedObjectID *commentID))completion +{ + NSManagedObjectID * __block objectID = nil; + [self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) { + ReaderPost *post = [context existingObjectWithID:postObjectID error:nil]; + Comment *comment = [self createHierarchicalCommentWithContent:content withParent:parentID postID:post.postID siteID:siteID inContext:context]; + objectID = comment.objectID; + // This fixes an issue where the comment may not appear for some posts after a successful posting + // More information: https://github.com/wordpress-mobile/WordPress-iOS/issues/13259 + comment.post = post; + } completion:^{ + completion(objectID); + } onQueue:dispatch_get_main_queue()]; +} + +- (Comment *)createHierarchicalCommentWithContent:(NSString *)content withParent:(NSNumber *)parentID postID:(NSNumber *)postID siteID:(NSNumber *)siteID inContext:(NSManagedObjectContext *)context +{ + // Fetch the relevant ReaderPost + NSError *error; + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([ReaderPost class])]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"postID = %@ AND siteID = %@", postID, siteID]; + fetchRequest.fetchLimit = 1; + NSArray *results = [context executeFetchRequest:fetchRequest error:&error]; + if (error) { + DDLogError(@"Error fetching post with id %@ and site %@. %@", postID, siteID, error); + return nil; + } + + ReaderPost *post = [results firstObject]; + if (!post) { + return nil; + } + + // (Insert a new comment into core data. Check for its existance first for paranoia sake. + // In theory a sync could include a newly created comment before the request that created it returned. + Comment *comment = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Comment class]) inManagedObjectContext:context]; + + WPAccount *account = [WPAccount lookupDefaultWordPressComAccountInContext:context]; + comment.author = [account username]; + comment.authorID = [account.userID intValue]; + comment.content = content; + comment.dateCreated = [NSDate date]; + comment.parentID = [parentID intValue]; + comment.postID = [postID intValue]; + comment.postTitle = post.postTitle; + comment.status = [Comment descriptionFor:CommentStatusTypeDraft]; + comment.post = post; + + // Increment the post's comment count. + post.commentCount = @([post.commentCount integerValue] + 1); + + // Find its parent comment (if it exists) + Comment *parentComment; + if (parentID.intValue != 0) { + parentComment = [post commentWithID:parentID]; + } + + // Update depth and hierarchy + [self setHierarchyAndDepthOnComment:comment withParentComment:parentComment]; + + [context obtainPermanentIDsForObjects:@[comment] error:&error]; + if (error) { + DDLogError(@"%@ error obtaining permanent ID for a hierarchical comment %@: %@", NSStringFromSelector(_cmd), comment, error); + } + + return comment; +} + +- (void)setHierarchyAndDepthOnComment:(Comment *)comment withParentComment:(Comment *)parentComment +{ + NSParameterAssert(comment.managedObjectContext != nil); + + // Update depth and hierarchy + NSNumber *commentID = [NSNumber numberWithInt:comment.commentID]; + + if (commentID != 0) { + // A new comment will have a 0 commentID. If 0 is used when formatting the hierarchy, + // the comment will preceed any other comment in its level of the hierarchy. + // Instead we'll pass a number so large as to ensure the comment will appear last in a list. + commentID = @9999999; + } + + if (parentComment) { + comment.hierarchy = [NSString stringWithFormat:@"%@.%@", parentComment.hierarchy, [self formattedHierarchyElement:commentID]]; + comment.depth = parentComment.depth + 1; + } else { + comment.hierarchy = [self formattedHierarchyElement:commentID]; + comment.depth = 0; + } +} + +- (void)updateHierarchicalComment:(Comment *)comment withRemoteComment:(RemoteComment *)remoteComment +{ + NSParameterAssert(comment.managedObjectContext != nil); + + [self updateComment:comment withRemoteComment:remoteComment]; + // Find its parent comment (if it exists) + Comment *parentComment; + if (comment.parentID != 0) { + NSNumber *parentID = [NSNumber numberWithInt:comment.parentID]; + parentComment = [(ReaderPost *)comment.post commentWithID:parentID]; + } + + // Update depth and hierarchy + [self setHierarchyAndDepthOnComment:comment withParentComment:parentComment]; +} + +- (BOOL)mergeHierarchicalComments:(NSArray *)comments forPage:(NSUInteger)page forPost:(ReaderPost *)post +{ + NSParameterAssert(post.managedObjectContext != nil); + + if (![comments count]) { + return NO; + } + + NSMutableSet *visibleCommentIds = [NSMutableSet new]; + NSMutableArray *ancestors = [NSMutableArray array]; + NSMutableArray *commentsToKeep = [NSMutableArray array]; + NSString *entityName = NSStringFromClass([Comment class]); + NSUInteger newCommentCount = 0; + + for (RemoteComment *remoteComment in comments) { + Comment *comment = [post commentWithID:remoteComment.commentID]; + if (!comment) { + newCommentCount++; + comment = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:post.managedObjectContext]; + } + + [self updateComment:comment withRemoteComment:remoteComment]; + + // Calculate hierarchy and depth. + ancestors = [self ancestorsForCommentWithParentID:[NSNumber numberWithInt:comment.parentID] andCurrentAncestors:ancestors]; + comment.hierarchy = [self hierarchyFromAncestors:ancestors andCommentID:[NSNumber numberWithInt:comment.commentID]]; + + // Comments are shown on the thread when (1) it is approved, and (2) its ancestors are approved. + // Having the comments sorted hierarchically ascending ensures that each comment's predecessors will be visited first. + // Therefore, we only need to check if the comment and its direct parent are approved. + // Ref: https://github.com/wordpress-mobile/WordPress-iOS/issues/18081 + BOOL hasValidParent = comment.parentID > 0 && [visibleCommentIds containsObject:@(comment.parentID)]; + if ([comment isApproved] && ([comment isTopLevelComment] || hasValidParent)) { + [visibleCommentIds addObject:@(comment.commentID)]; + } + comment.visibleOnReader = [visibleCommentIds containsObject:@(comment.commentID)]; + + comment.depth = ancestors.count; + comment.post = post; + comment.content = [self sanitizeCommentContent:comment.content isPrivateSite:post.isBlogPrivate]; + [commentsToKeep addObject:comment]; + } + + // Remove deleted comments + // When merging the first fetched page of comments, clear out anything that was previously + // cached and missing from the comments just synced. This provides for a clean slate and + // helps avoid certain cases where some pages might not be resynced, creating gaps in the content. + if (page == 1) { + [self deleteCommentsMissingFromHierarchicalComments:commentsToKeep forPost:post]; + [self deleteUnownedCommentsInContext:post.managedObjectContext]; + } + + // Make sure the post's comment count is at least the number of comments merged. + if ([post.commentCount integerValue] < [commentsToKeep count]) { + post.commentCount = @([commentsToKeep count]); + } + + return newCommentCount > 0; +} + +// Does not save context +- (void)deleteCommentsMissingFromHierarchicalComments:(NSArray *)commentsToKeep forPost:(ReaderPost *)post +{ + for (Comment *comment in post.comments) { + if (![commentsToKeep containsObject:comment]) { + [post.managedObjectContext deleteObject:comment]; + } + } +} + +- (NSArray *)topLevelCommentsForPage:(NSUInteger)page forPost:(ReaderPost *)post +{ + NSString *entityName = NSStringFromClass([Comment class]); + + // Retrieve the starting and ending comments for the specified page. + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:entityName]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"post = %@ AND parentID = 0", post]; + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"hierarchy" ascending:YES]; + fetchRequest.sortDescriptors = @[sortDescriptor]; + [fetchRequest setFetchLimit:WPTopLevelHierarchicalCommentsPerPage]; + NSUInteger offset = WPTopLevelHierarchicalCommentsPerPage * (page - 1); + [fetchRequest setFetchOffset:offset]; + + NSError *error = nil; + NSArray *fetchedObjects = [post.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (error) { + DDLogError(@"Error fetching top level comments for page %i : %@", page, error); + } + return fetchedObjects; +} + +- (NSArray *)topLevelComments:(NSUInteger)number forPost:(ReaderPost *)post +{ + NSParameterAssert(post.managedObjectContext != nil); + NSArray * __block commentsToReturn = nil; + [post.managedObjectContext performBlockAndWait:^{ + NSArray *comments = [self topLevelCommentsForPage:1 forPost:post]; + NSInteger count = MIN(comments.count, number); + commentsToReturn = [comments subarrayWithRange:NSMakeRange(0, count)]; + }]; + return commentsToReturn; +} + +#pragma mark - Transformations + +- (void)updateComment:(Comment *)comment withRemoteComment:(RemoteComment *)remoteComment +{ + NSParameterAssert(comment.managedObjectContext != nil); + + comment.commentID = [remoteComment.commentID intValue]; + comment.authorID = [remoteComment.authorID intValue]; + comment.author = remoteComment.author; + comment.author_email = remoteComment.authorEmail; + comment.author_url = remoteComment.authorUrl; + comment.authorAvatarURL = remoteComment.authorAvatarURL; + comment.author_ip = remoteComment.authorIP; + comment.content = remoteComment.content; + comment.rawContent = remoteComment.rawContent; + comment.dateCreated = remoteComment.date; + comment.link = remoteComment.link; + comment.parentID = [remoteComment.parentID intValue]; + comment.postID = [remoteComment.postID intValue]; + comment.postTitle = remoteComment.postTitle; + comment.status = remoteComment.status; + comment.type = remoteComment.type; + comment.isLiked = remoteComment.isLiked; + comment.likeCount = [remoteComment.likeCount intValue]; + comment.canModerate = remoteComment.canModerate; + + // if the post for the comment is not set, check if that post is already stored and associate them + if (!comment.post) { + comment.post = [comment.blog lookupPostWithID:[NSNumber numberWithInt:comment.postID] inContext:comment.managedObjectContext]; + } +} + +- (RemoteComment *)remoteCommentWithComment:(Comment *)comment +{ + RemoteComment *remoteComment = [RemoteComment new]; + remoteComment.commentID = [NSNumber numberWithInt:comment.commentID]; + remoteComment.authorID = [NSNumber numberWithInt:comment.authorID]; + remoteComment.author = comment.author; + remoteComment.authorEmail = comment.author_email; + remoteComment.authorUrl = comment.author_url; + remoteComment.authorAvatarURL = comment.authorAvatarURL; + remoteComment.authorIP = comment.author_ip; + remoteComment.content = comment.content; + remoteComment.date = comment.dateCreated; + remoteComment.link = comment.link; + remoteComment.parentID = [NSNumber numberWithInt:comment.parentID]; + remoteComment.postID = [NSNumber numberWithInt:comment.postID]; + remoteComment.postTitle = comment.postTitle; + remoteComment.status = comment.status; + remoteComment.type = comment.type; + remoteComment.isLiked = comment.isLiked; + remoteComment.likeCount = [NSNumber numberWithInt:comment.likeCount]; + remoteComment.canModerate = comment.canModerate; + return remoteComment; +} + + +#pragma mark - Remotes + +- (id)remoteForComment:(Comment *)comment +{ + NSParameterAssert(comment.managedObjectContext != nil); + + // If the comment is fetched through the Reader API, the blog will always be nil. + // Try to find the Blog locally first, as it should exist if the user has a role on the site. + if (comment.post && [comment.post isKindOfClass:[ReaderPost class]]) { + ReaderPost *readerPost = (ReaderPost *)comment.post; + return [self remoteForBlog:[Blog lookupWithHostname:readerPost.blogURL inContext:comment.managedObjectContext]]; + } + + return [self remoteForBlog:comment.blog]; +} + +- (id)remoteForBlog:(Blog *)blog +{ + return [self.remoteFactory remoteWithBlog:blog]; +} + +- (CommentServiceRemoteREST *)restRemoteForSite:(NSNumber *)siteID +{ + return [self.remoteFactory restRemoteWithSiteID:siteID api:[self apiForRESTRequest]]; +} + +/** + Get the api to use for the request. + */ +- (WordPressComRestApi *)apiForRESTRequest +{ + WPAccount * __block defaultAccount = nil; + [self.coreDataStack.mainContext performBlockAndWait:^{ + defaultAccount = [WPAccount lookupDefaultWordPressComAccountInContext:self.coreDataStack.mainContext]; + }]; + + WordPressComRestApi *api = [defaultAccount wordPressComRestApi]; + //Sergio Estevao: Do we really want to do this? If the call going to be valid if no credential is available? + if (![api hasCredentials]) { + api = [WordPressComRestApi defaultApiWithOAuthToken:nil + userAgent:[WPUserAgent wordPressUserAgent] + localeKey:[WordPressComRestApi LocaleKeyDefault]]; + } + return api; +} + +@end diff --git a/Modules/Sources/WordPressDataObjC/CoreDataService.m b/Modules/Sources/WordPressDataObjC/Services/CoreDataService.m similarity index 100% rename from Modules/Sources/WordPressDataObjC/CoreDataService.m rename to Modules/Sources/WordPressDataObjC/Services/CoreDataService.m diff --git a/WordPress/Classes/Services/PostCategoryService.m b/Modules/Sources/WordPressDataObjC/Services/PostCategoryService.m similarity index 100% rename from WordPress/Classes/Services/PostCategoryService.m rename to Modules/Sources/WordPressDataObjC/Services/PostCategoryService.m diff --git a/Modules/Sources/WordPressDataObjC/include/CommentService.h b/Modules/Sources/WordPressDataObjC/include/CommentService.h new file mode 100644 index 000000000000..8c6f136eaae9 --- /dev/null +++ b/Modules/Sources/WordPressDataObjC/include/CommentService.h @@ -0,0 +1,207 @@ +#import +#import "CoreDataService.h" +@import WordPressDataObjC; + +NS_ASSUME_NONNULL_BEGIN + +@import WordPressKit; + +extern NSUInteger const WPTopLevelHierarchicalCommentsPerPage; + +@class Blog; +@class Comment; +@class ReaderPost; +@class BasePost; +@class RemoteUser; +@class CommentServiceRemoteFactory; + +@interface CommentService : CoreDataService + +/// Initializes the instance with a custom service remote provider. +/// +/// @param coreDataStack The `CoreDataStack` this instance will use for interacting with CoreData. +/// @param commentServiceRemoteFactory The factory this instance will use to get service remote instances from. +- (instancetype)initWithCoreDataStack:(id)coreDataStack + commentServiceRemoteFactory:(CommentServiceRemoteFactory *)remoteFactory NS_DESIGNATED_INITIALIZER; + +// Create reply +- (void)createReplyForComment:(Comment *)comment content:(NSString *)content completion:(void (^)(Comment *reply))completion; + +// Sync comments +- (void)syncCommentsForBlog:(Blog *)blog + withStatus:(CommentStatusFilter)status + success:(void (^ _Nullable)(BOOL hasMore))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +- (void)syncCommentsForBlog:(Blog *)blog + withStatus:(CommentStatusFilter)status + filterUnreplied:(BOOL)filterUnreplied + success:(void (^ _Nullable)(BOOL hasMore))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Determine if a recent cache is available ++ (BOOL)shouldRefreshCacheFor:(Blog *)blog; + +// Load extra comments +- (void)loadMoreCommentsForBlog:(Blog *)blog + withStatus:(CommentStatusFilter)status + success:(void (^ _Nullable)(BOOL hasMore))success + failure:(void (^ _Nullable)(NSError * _Nullable))failure; + +// Load a single comment +- (void)loadCommentWithID:(NSNumber *_Nonnull)commentID + forBlog:(Blog *_Nonnull)blog + success:(void (^_Nullable)(Comment *_Nullable))success + failure:(void (^_Nullable)(NSError *_Nullable))failure; + +- (void)loadCommentWithID:(NSNumber *_Nonnull)commentID + forPost:(ReaderPost *_Nonnull)post + success:(void (^_Nullable)(Comment *_Nullable))success + failure:(void (^_Nullable)(NSError *_Nullable))failure; + +// Upload comment +- (void)uploadComment:(Comment *)comment + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Approve comment +- (void)approveComment:(Comment *)comment + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Unapprove (Pending) comment +- (void)unapproveComment:(Comment *)comment + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Spam comment +- (void)spamComment:(Comment *)comment + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Trash comment +- (void)trashComment:(Comment *)comment + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Delete comment +- (void)deleteComment:(Comment *)comment + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Sync a list of comments sorted by hierarchy, fetched by page number. +- (void)syncHierarchicalCommentsForPost:(ReaderPost *)post + page:(NSUInteger)page + success:(void (^ _Nullable)(BOOL hasMore, NSNumber * _Nullable totalComments))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Sync a list of comments sorted by hierarchy, restricted by the specified number of _top level_ comments. +// This method is intended to get a small number of comments. +// Therefore it is restricted to page 1 only. +- (void)syncHierarchicalCommentsForPost:(ReaderPost *)post + topLevelComments:(NSUInteger)number + success:(void (^ _Nullable)(BOOL hasMore, NSNumber * _Nullable totalComments))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Get the specified number of top level comments for the specified post. +// This method is intended to get a small number of comments. +// Therefore it is restricted to page 1 only. +- (NSArray *)topLevelComments:(NSUInteger)number forPost:(ReaderPost *)post; + +// Counts and returns the number of full pages of hierarchcial comments synced for a post. +// A partial set does not count toward the total number of pages. +- (NSInteger)numberOfHierarchicalPagesSyncedforPost:(ReaderPost *)post; + + +/** + REST Helpers: + ============= + + Decoupled from the CoreData Model, the main goal of the following helpers is to + to allow Comment Interaction in scenarios in which the Comment / Blog instances may not be available. +*/ + +// Edit comment +- (void)updateCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + content:(NSString *)content + success:(void (^ _Nullable)(RemoteComment * _Nullable comment))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Replies +- (void)replyToPost:(ReaderPost *)post + content:(NSString *)content + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +- (void)replyToHierarchicalCommentWithID:(NSNumber *)commentID + post:(ReaderPost *)post + content:(NSString *)content + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +- (void)replyToCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + content:(NSString *)content + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Like comment +- (void)likeCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Unlike comment +- (void)unlikeCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Approve comment +- (void)approveCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Unapprove comment +- (void)unapproveCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Spam comment +- (void)spamCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +// Delete comment +- (void)deleteCommentWithID:(NSNumber *)commentID + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +/** + This method will toggle the like status for a comment and optimistically save it. It will also + trigger either likeCommentWithID or unlikeCommentWithID. In case the request fails, like status + will be reverted back. + + @param siteID is used since the blog might be nil for comment. It's not optional! + */ +- (void)toggleLikeStatusForComment:(Comment *)comment + siteID:(NSNumber *)siteID + success:(void (^ _Nullable)(void))success + failure:(void (^ _Nullable)(NSError * _Nullable error))failure; + +/** + Get a CommentServiceRemoteREST for the given site. + This is public so it can be accessed from Swift extensions. + + @param siteID The ID of the site the remote will be used for. + */ +- (CommentServiceRemoteREST *_Nullable)restRemoteForSite:(NSNumber *_Nonnull)siteID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/Services/PostCategoryService.h b/Modules/Sources/WordPressDataObjC/include/PostCategoryService.h similarity index 100% rename from WordPress/Classes/Services/PostCategoryService.h rename to Modules/Sources/WordPressDataObjC/include/PostCategoryService.h