Repository: Automattic/jetpack (PHP)

Issue:
My Jetpack: Update product endpoint for standalone
We have the [product endpoint ](https://github.com/Automattic/jetpack/blob/trunk/projects/packages/my-jetpack/src/class-rest-products.php#L53-L60) that activates (and installs if needed) the plugin.

This endpoint calls the [`activate`](https://github.com/Automattic/jetpack/blob/trunk/projects/packages/my-jetpack/src/products/class-product.php#L390-L407) method, which looks if the [plugin is installed](https://github.com/Automattic/jetpack/blob/trunk/projects/packages/my-jetpack/src/products/class-product.php#L366).

But as soon we return to hybrid, this [method is overridden](https://github.com/Automattic/jetpack/blob/trunk/projects/packages/my-jetpack/src/products/class-hybrid-product.php#L46-L48), and look for the `Jetpack` plugin too.

We need to change or add an option to look only for the standalone plugin on this call.


Discussion: I was checking why both plugins are checked here, and seems like it's intended to fix a 'plugin_absent' status for Backup when only Jetpack is installed.

Since we are fixing the status handling, seems safe to remove the check for Jetpack installation. Makes sense?
> Since we are fixing the status handling, seems safe to remove the check for Jetpack installation. Makes sense?

Yes. My only concern is `class-extras`, if it works as expected (installing Jetpack plugin), I think it is worth removing it, since we want to push user to install standalone
Seems like `class-extras` is a product, not a hybrid-product, so changing the check on the `class-hybrid-product` would not affect it.
@renatoagds Thinking about it, we are going to offer two options to the user:

**1) activate the product**

If the standalone plugin is present, we activate it. If it's not present, we activate the module inside Jetpack.

**2) install the standalone plugin for the product**

This will trigger the plugin installation + activation, in the case it's absent.

Is this description correct? If so, what we need are:

- change the activation endpoint to make sure it only activate what is present, without sideloading any standalone plugin
- create an endpoint to install and activate the standalone plugin

Does it makes sense?
> Does it makes sense?

Yes, it makes sense. It could be good to split the endpoints. It will already fix a point that we're pushing the user for interstitial in this case, and we shouldn't. With this one, we don't need to push the user for another page to use the same endpoint, we just install it based on the new endpoint.


I was reviewing the code for the `Hybrid_Product`:

> change the activation endpoint to make sure it only activate what is present, without sideloading any standalone plugin

There is no need to change anything. This is how it works currently.

> create an endpoint to install and activate the standalone plugin

This is the action that we need to implement. I am going to focus on this.



Fix (patch):
diff --git a/projects/packages/my-jetpack/changelog/add-my-jetpack-endpoint-to-install-standalone-plugin b/projects/packages/my-jetpack/changelog/add-my-jetpack-endpoint-to-install-standalone-plugin
new file mode 100644
index 0000000000000..44b64279111e2
--- /dev/null
+++ b/projects/packages/my-jetpack/changelog/add-my-jetpack-endpoint-to-install-standalone-plugin
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+My Jetpack: Add endpoint to install the standalone plugin for hybrid products.
diff --git a/projects/packages/my-jetpack/src/class-rest-products.php b/projects/packages/my-jetpack/src/class-rest-products.php
index a0300a84303a8..4ecb94283a624 100644
--- a/projects/packages/my-jetpack/src/class-rest-products.php
+++ b/projects/packages/my-jetpack/src/class-rest-products.php
@@ -68,6 +68,21 @@ public function __construct() {
 				),
 			)
 		);
+
+		register_rest_route(
+			'my-jetpack/v1',
+			'site/products/(?P<product>[a-z\-]+)/install-standalone',
+			array(
+				array(
+					'methods'             => \WP_REST_Server::EDITABLE,
+					'callback'            => __CLASS__ . '::install_standalone',
+					'permission_callback' => __CLASS__ . '::edit_permissions_callback',
+					'args'                => array(
+						'product' => $product_arg,
+					),
+				),
+			)
+		);
 	}
 
 	/**
@@ -205,4 +220,40 @@ public static function deactivate_product( $request ) {
 		return rest_ensure_response( Products::get_product( $product_slug ), 200 );
 	}
 
+	/**
+	 * Callback for installing the standalone plugin on a Hybrid Product.
+	 *
+	 * @param \WP_REST_Request $request The request object.
+	 * @return \WP_REST_Response
+	 */
+	public static function install_standalone( $request ) {
+		$product_slug = $request->get_param( 'product' );
+		$product      = Products::get_product( $product_slug );
+		if ( ! isset( $product['class'] ) ) {
+			return new \WP_Error(
+				'not_implemented',
+				__( 'The product class handler is not implemented', 'jetpack-my-jetpack' ),
+				array( 'status' => 501 )
+			);
+		}
+
+		/**
+		 * If the product is not hybrid, there is no need to deal with a standalone plugin.
+		 */
+		if ( ! is_subclass_of( $product['class'], Hybrid_Product::class ) ) {
+			return new \WP_Error(
+				'not_hybrid',
+				__( 'This product does not have a standalone plugin to install', 'jetpack-my-jetpack' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		$install_product_result = call_user_func( array( $product['class'], 'install_and_activate_standalone' ) );
+		if ( is_wp_error( $install_product_result ) ) {
+			$install_product_result->add_data( array( 'status' => 400 ) );
+			return $install_product_result;
+		}
+
+		return rest_ensure_response( Products::get_product( $product_slug ), 200 );
+	}
 }
diff --git a/projects/packages/my-jetpack/src/products/class-hybrid-product.php b/projects/packages/my-jetpack/src/products/class-hybrid-product.php
index a342888e14960..a5f76f589f076 100644
--- a/projects/packages/my-jetpack/src/products/class-hybrid-product.php
+++ b/projects/packages/my-jetpack/src/products/class-hybrid-product.php
@@ -22,6 +22,13 @@
  */
 abstract class Hybrid_Product extends Product {
 
+	/**
+	 * All hybrid products have a standalone plugin
+	 *
+	 * @var bool
+	 */
+	public static $has_standalone_plugin = true;
+
 	/**
 	 * Checks whether the Product is active
 	 *
@@ -31,6 +38,15 @@ public static function is_plugin_active() {
 		return parent::is_plugin_active() || parent::is_jetpack_plugin_active();
 	}
 
+	/**
+	 * Checks whether the standalone plugin for this product is active
+	 *
+	 * @return boolean
+	 */
+	public static function is_standalone_plugin_active() {
+		return parent::is_plugin_active();
+	}
+
 	/**
 	 * Checks whether the plugin is installed
 	 *
@@ -122,4 +138,42 @@ public static function do_product_specific_activation( $product_activation ) {
 		return true;
 	}
 
+	/**
+	 * Install and activate the standalone plugin in the case it's missing.
+	 *
+	 * @return boolean|WP_Error
+	 */
+	final public static function install_and_activate_standalone() {
+		/**
+		 * Check for the presence of the standalone plugin, ignoring Jetpack presence.
+		 *
+		 * If the standalone plugin is not installed and the user can install plugins, proceed with the installation.
+		 */
+		if ( ! parent::is_plugin_installed() ) {
+			/**
+			 * Check for permissions
+			 */
+			if ( ! current_user_can( 'install_plugins' ) ) {
+				return new WP_Error( 'not_allowed', __( 'You are not allowed to install plugins on this site.', 'jetpack-my-jetpack' ) );
+			}
+
+			/**
+			 * Install the plugin
+			 */
+			$installed = Plugins_Installer::install_plugin( static::get_plugin_slug() );
+			if ( is_wp_error( $installed ) ) {
+				return $installed;
+			}
+		}
+
+		/**
+		 * Activate the installed plugin
+		 */
+		$result = static::activate_plugin();
+		if ( is_wp_error( $result ) ) {
+			return $result;
+		}
+
+		return true;
+	}
 }