Skip to content

Commit

Permalink
Merge pull request #16675 from wordpress-mobile/task/16595-self-hoste…
Browse files Browse the repository at this point in the history
…d-plugins

Adds ability to manage plugins for self hosted sites that aren't connected to Jetpack
  • Loading branch information
Emily Laguna authored Jun 18, 2021
2 parents af9456f + 2fcfc27 commit 6cf191a
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 61 deletions.
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ end
def wordpress_kit
pod 'WordPressKit', '~> 4.36.0-beta'
# pod 'WordPressKit', :git => 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', :tag => ''
# pod 'WordPressKit', :git => 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', :branch => 'issues/16599-add-new-reader-post-fields'
# pod 'WordPressKit', :git => 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', :branch => ''
# pod 'WordPressKit', :git => 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', :commit => ''
# pod 'WordPressKit', :path => '../WordPressKit-iOS'
end
Expand Down
8 changes: 4 additions & 4 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ PODS:
- WordPressKit (~> 4.18-beta)
- WordPressShared (~> 1.12-beta)
- WordPressUI (~> 1.7-beta)
- WordPressKit (4.36.0-beta.1):
- WordPressKit (4.36.0-beta.2):
- Alamofire (~> 4.8.0)
- CocoaLumberjack (~> 3.4)
- NSObject-SafeExpectations (= 0.0.4)
Expand Down Expand Up @@ -565,7 +565,6 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/wordpress-mobile/cocoapods-specs.git:
- WordPressAuthenticator
- WordPressKit
trunk:
- 1PasswordExtension
- Alamofire
Expand Down Expand Up @@ -605,6 +604,7 @@ SPEC REPOS:
- UIDeviceIdentifier
- WordPress-Aztec-iOS
- WordPress-Editor-iOS
- WordPressKit
- WordPressMocks
- WordPressShared
- WordPressUI
Expand Down Expand Up @@ -810,7 +810,7 @@ SPEC CHECKSUMS:
WordPress-Aztec-iOS: 870c93297849072aadfc2223e284094e73023e82
WordPress-Editor-iOS: 068b32d02870464ff3cb9e3172e74234e13ed88c
WordPressAuthenticator: 4ccd7f41bae37247883922ad92a14f18cc759bf3
WordPressKit: 5f75eae2ad44a39a98391306394ced9f3bbf899e
WordPressKit: f9ce5b4aa9854facc409dfd2bcca62e2733f6323
WordPressMocks: dfac50a938ac74dddf5f7cce5a9110126408dd19
WordPressShared: 4fd83a8f572dfd8209fa6ca5c891194b29d15961
WordPressUI: cf3b3ef744663811a8d8a9844f42386e1826f512
Expand All @@ -826,6 +826,6 @@ SPEC CHECKSUMS:
ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba
ZIPFoundation: e27423c004a5a1410c15933407747374e7c6cb6e

PODFILE CHECKSUM: 2ef2682060b199ad5dd4a47a0846fdb741d13cbd
PODFILE CHECKSUM: 556e084a166bd2cd6ff33a3e6d07e2865c7e368b

COCOAPODS: 1.10.1
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
17.7
-----
* [**] (Don't apply to Jetpack app): Self hosted sites that do not use Jetpack can now manage (install, uninstall, activate, and deactivate) their plugins [#16675]

* [*] Upgraded the Zendesk SDK to version 5.3.0

Expand Down
24 changes: 21 additions & 3 deletions WordPress/Classes/Models/Blog.m
Original file line number Diff line number Diff line change
Expand Up @@ -628,11 +628,22 @@ - (BOOL)supportsPluginManagement
BOOL hasRequiredJetpack = [self hasRequiredJetpackVersion:@"5.6"];

BOOL isTransferrable = self.isHostedAtWPcom
&& self.hasBusinessPlan
&& self.siteVisibility != SiteVisibilityPrivate
&& self.hasBusinessPlan
&& self.siteVisibility != SiteVisibilityPrivate
&& self.isAdmin;

BOOL supports = isTransferrable || hasRequiredJetpack;

// If the site is not hosted on WP.com we can still manage plugins directly using the WP.org rest API
// Reference: https://make.wordpress.org/core/2020/07/16/new-and-modified-rest-api-endpoints-in-wordpress-5-5/
if(!supports && !self.account){
supports = !self.isHostedAtWPcom
&& self.wordPressOrgRestApi
&& [self hasRequiredWordPressVersion:@"5.5"]
&& self.isAdmin;
}

return isTransferrable || hasRequiredJetpack;
return supports;
}

- (BOOL)supportsStories
Expand Down Expand Up @@ -853,6 +864,13 @@ - (BOOL)hasRequiredJetpackVersion:(NSString *)requiredJetpackVersion
&& [self.jetpack.version compare:requiredJetpackVersion options:NSNumericSearch] != NSOrderedAscending;
}

/// 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
{
return [self.version compare:requiredVersion options:NSNumericSearch] != NSOrderedAscending;
}

#pragma mark - Private Methods

- (id)getOptionValue:(NSString *)name
Expand Down
55 changes: 46 additions & 9 deletions WordPress/Classes/Models/JetpackSiteRef.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,53 @@ struct JetpackSiteRef: Hashable, Codable {
let homeURL: String

private var hasBackup = false

private var hasPaidPlan = false

// Self Hosted Non Jetpack Support
// Ideally this would be a different "ref" object but the JetpackSiteRef
// is so coupled into the plugin management that the amount of changes and work needed to change
// would be very large. This is a workaround for that.
let isSelfHostedWithoutJetpack: Bool

/// The XMLRPC path for the site, only applies to self hosted sites with no Jetpack connected
var xmlRPC: String? = nil

init?(blog: Blog) {
guard let username = blog.account?.username,
let siteID = blog.dotComID as? Int,
let homeURL = blog.homeURL as String? else {

// Init for self hosted and no Jetpack
if blog.account == nil, !blog.isHostedAtWPcom {
guard
let username = blog.username,
let homeURL = blog.homeURL as String?,
let xmlRPC = blog.xmlrpc
else {
return nil
}

self.isSelfHostedWithoutJetpack = true
self.username = username
self.siteID = Constants.selfHostedSiteID
self.homeURL = homeURL
self.xmlRPC = xmlRPC
}

// Init for normal Jetpack connected sites
else {
guard
let username = blog.account?.username,
let siteID = blog.dotComID as? Int,
let homeURL = blog.homeURL as String?
else {
return nil
}

self.isSelfHostedWithoutJetpack = false
self.siteID = siteID
self.username = username
self.homeURL = homeURL
self.hasBackup = blog.isBackupsAllowed()
self.hasPaidPlan = blog.hasPaidPlan
}
self.siteID = siteID
self.username = username
self.homeURL = homeURL
self.hasBackup = blog.isBackupsAllowed()
self.hasPaidPlan = blog.hasPaidPlan
}

public func hash(into hasher: inout Hasher) {
Expand All @@ -48,4 +81,8 @@ struct JetpackSiteRef: Hashable, Codable {
func shouldShowActivityLogFilter() -> Bool {
hasBackup || hasPaidPlan
}

struct Constants {
static let selfHostedSiteID = -1
}
}
2 changes: 1 addition & 1 deletion WordPress/Classes/Models/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

struct Plugin: Equatable {
let state: PluginState
let directoryEntry: PluginDirectoryEntry?
var directoryEntry: PluginDirectoryEntry?

var id: String {
return state.id
Expand Down
11 changes: 10 additions & 1 deletion WordPress/Classes/Services/BlogService+JetpackConvenience.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
extension BlogService {
static func blog(with site: JetpackSiteRef, context: NSManagedObjectContext = ContextManager.shared.mainContext) -> Blog? {
let service = BlogService(managedObjectContext: context)
return service.blog(byBlogId: site.siteID as NSNumber, andUsername: site.username)

let blog: Blog?

if site.isSelfHostedWithoutJetpack, let xmlRPC = site.xmlRPC {
blog = service.findBlog(withXmlrpc: xmlRPC, andUsername: site.username)
} else {
blog = service.blog(byBlogId: site.siteID as NSNumber, andUsername: site.username)
}

return blog
}
}
77 changes: 46 additions & 31 deletions WordPress/Classes/Stores/PluginStore.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import WordPressFlux
import WordPressKit

enum PluginAction: Action {
case activate(id: String, site: JetpackSiteRef)
Expand Down Expand Up @@ -340,7 +341,7 @@ extension PluginStore {
}

func getPlugin(slug: String, site: JetpackSiteRef) -> Plugin? {
return getPlugins(site: site)?.plugins.first(where: { $0.state.slug == slug })
return getPlugins(site: site)?.plugins.first(where: { $0.state.slug.hasPrefix(slug) })
}

func getFeaturedPlugins() -> [PluginDirectoryEntry]? {
Expand Down Expand Up @@ -414,11 +415,10 @@ private extension PluginStore {
plugin.active = true
}

WPAppAnalytics.track(.pluginActivated, withBlogID: site.siteID as NSNumber)
track(.pluginActivated, with: site)

remote(site: site)?.activatePlugin(
pluginID: pluginID,
siteID: site.siteID,
pluginID: plugin.state.id,
success: {},
failure: { [weak self] (error) in
let message = String(format: NSLocalizedString("Error activating %@.", comment: "There was an error activating a plugin, placeholder is the plugin name"), plugin.name)
Expand All @@ -437,11 +437,10 @@ private extension PluginStore {
plugin.active = false
}

WPAppAnalytics.track(.pluginDeactivated, withBlogID: site.siteID as NSNumber)
track(.pluginDeactivated, with: site)

remote(site: site)?.deactivatePlugin(
pluginID: pluginID,
siteID: site.siteID,
pluginID: plugin.state.id,
success: {},
failure: { [weak self] (error) in
let message = String(format: NSLocalizedString("Error deactivating %@.", comment: "There was an error deactivating a plugin, placeholder is the plugin name"), plugin.name)
Expand All @@ -460,11 +459,10 @@ private extension PluginStore {
plugin.autoupdate = true
}

WPAppAnalytics.track(.pluginAutoupdateEnabled, withBlogID: site.siteID as NSNumber)
track(.pluginAutoupdateEnabled, with: site)

remote(site: site)?.enableAutoupdates(
pluginID: pluginID,
siteID: site.siteID,
pluginID: plugin.state.id,
success: {},
failure: { [weak self] (error) in
let message = String(format: NSLocalizedString("Error enabling autoupdates for %@.", comment: "There was an error enabling autoupdates for a plugin, placeholder is the plugin name"), plugin.name)
Expand All @@ -483,11 +481,10 @@ private extension PluginStore {
plugin.autoupdate = false
}

WPAppAnalytics.track(.pluginAutoupdateDisabled, withBlogID: site.siteID as NSNumber)
track(.pluginAutoupdateDisabled, with: site)

remote(site: site)?.disableAutoupdates(
pluginID: pluginID,
siteID: site.siteID,
pluginID: plugin.state.id,
success: {},
failure: { [weak self] (error) in
let message = String(format: NSLocalizedString("Error disabling autoupdates for %@.", comment: "There was an error disabling autoupdates for a plugin, placeholder is the plugin name"), plugin.name)
Expand All @@ -499,15 +496,14 @@ private extension PluginStore {
}

func activateAndEnableAutoupdatesPlugin(pluginID: String, site: JetpackSiteRef) {
guard getPlugin(id: pluginID, site: site) != nil else {
guard let plugin = getPlugin(id: pluginID, site: site) else {
return
}
state.modifyPlugin(id: pluginID, site: site) { plugin in
plugin.autoupdate = true
plugin.active = true
}
remote(site: site)?.activateAndEnableAutoupdated(pluginID: pluginID,
siteID: site.siteID,
remote(site: site)?.activateAndEnableAutoupdates(pluginID: plugin.state.id,
success: {},
failure: { [weak self] error in
self?.state.modifyPlugin(id: pluginID, site: site) { plugin in
Expand All @@ -524,15 +520,13 @@ private extension PluginStore {
}

state.updatesInProgress[site, default: Set()].insert(plugin.slug)
WPAppAnalytics.track(.pluginInstalled, withBlogID: site.siteID as NSNumber)

track(.pluginInstalled, with: site)
remote.install(
pluginSlug: plugin.slug,
siteID: site.siteID,
success: { [weak self] installedPlugin in
self?.transaction { state in
state.upsertPlugin(id: installedPlugin.id, site: site, newPlugin: installedPlugin)
state.updatesInProgress[site]?.remove(installedPlugin.slug)
state.updatesInProgress[site]?.remove(installedPlugin.id)
}

let message = String(format: NSLocalizedString("Successfully installed %@.", comment: "Notice displayed after installing a plug-in."), installedPlugin.name)
Expand All @@ -559,10 +553,10 @@ private extension PluginStore {
plugin.updateState = .updating(version)
})
}
WPAppAnalytics.track(.pluginUpdated, withBlogID: site.siteID as NSNumber)
track(.pluginUpdated, with: site)

remote(site: site)?.updatePlugin(
pluginID: pluginID,
siteID: site.siteID,
pluginID: plugin.state.id,
success: { [weak self] (plugin) in
self?.transaction({ (state) in
state.modifyPlugin(id: pluginID, site: site, change: { (updatedPlugin) in
Expand Down Expand Up @@ -590,7 +584,7 @@ private extension PluginStore {
return
}
state.plugins[site]?.plugins.remove(at: index)
WPAppAnalytics.track(.pluginRemoved, withBlogID: site.siteID as NSNumber)
track(.pluginRemoved, with: site)

guard let remote = self.remote(site: site) else {
return
Expand All @@ -604,15 +598,13 @@ private extension PluginStore {

let remove = {
remote.remove(
pluginID: pluginID,
siteID: site.siteID,
pluginID: plugin.state.id,
success: {},
failure: failure)
}

if plugin.state.active {
remote.deactivatePlugin(pluginID: pluginID,
siteID: site.siteID,
remote.deactivatePlugin(pluginID: plugin.state.id,
success: remove,
failure: failure)
} else {
Expand Down Expand Up @@ -650,7 +642,6 @@ private extension PluginStore {
}
state.fetching[site] = true
remote.getPlugins(
siteID: site.siteID,
success: { [actionDispatcher] (plugins) in
actionDispatcher.dispatch(PluginAction.receivePlugins(site: site, plugins: plugins))
},
Expand Down Expand Up @@ -796,12 +787,36 @@ private extension PluginStore {
ActionDispatcher.dispatch(NoticeAction.post(Notice(title: message)))
}

func remote(site: JetpackSiteRef) -> PluginServiceRemote? {
func remote(site: JetpackSiteRef) -> PluginManagementClient? {
guard site.isSelfHostedWithoutJetpack else {
return jetpackRemoteClient(site: site)
}

return selfHostedRemoteClient(site: site)
}

private func jetpackRemoteClient(site: JetpackSiteRef) -> PluginManagementClient? {
guard let token = CredentialsService().getOAuthToken(site: site) else {
return nil
}

let api = WordPressComRestApi.defaultApi(oAuthToken: token, userAgent: WPUserAgent.wordPress())
let pluginRemote = PluginServiceRemote(wordPressComRestApi: api)

return JetpackPluginManagementClient(with: site.siteID, remote: pluginRemote)
}

private func selfHostedRemoteClient(site: JetpackSiteRef) -> PluginManagementClient? {
guard let remote = BlogService.blog(with: site)?.wordPressOrgRestApi else {
return nil
}

return SelfHostedPluginManagementClient(with: remote)
}

func track(_ statName: WPAnalyticsStat, with site: JetpackSiteRef) {
let siteID: NSNumber? = (site.isSelfHostedWithoutJetpack ? nil : site.siteID) as NSNumber?

return PluginServiceRemote(wordPressComRestApi: api)
WPAppAnalytics.track(statName, withBlogID: siteID)
}
}
Loading

0 comments on commit 6cf191a

Please sign in to comment.