PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / api / plugin-update-rest-api.php

plugin-update-rest-api.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at inc/api/plugin-update-rest-api.php

214 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Update REST API — lets a ZIP AI screen offer a one-click "Update"
4 * when something it needs is installed but too old to work with.
5 *
6 * Cookie/nonce authenticated and gated on `update_plugins`, the same capability
7 * WordPress's own Plugins screen requires. This route can only UPDATE a plugin
8 * that is already installed — there is no install path here, so it cannot be
9 * used to pull new code onto the site.
10 *
11 * @since 0.0.9
12 * @package zip-ai
13 */
14
15 namespace ZipAI\MCP\Classes\Api;
16
17 use ZipAI\MCP\Classes\Core\Helper;
18 use ZipAI\MCP\Classes\Core\Response;
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Registers the one-click plugin update route.
24 */
25 class Plugin_Update_REST_API {
26
27 /**
28 * Hook route registration.
29 *
30 * @return void
31 */
32 public function __construct() {
33 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
34 }
35
36 /**
37 * Register the route.
38 *
39 * @return void
40 */
41 public function register_routes() {
42 register_rest_route(
43 'zip-ai/v1',
44 '/plugin/update',
45 array(
46 'methods' => 'POST',
47 'callback' => array( $this, 'update_plugin' ),
48 'permission_callback' => array( $this, 'check_permission' ),
49 'args' => array(
50 'slug' => array(
51 'type' => 'string',
52 'required' => true,
53 ),
54 ),
55 )
56 );
57 }
58
59 /**
60 * Only a user who could run this update from the Plugins screen may run it
61 * from here.
62 *
63 * @return bool
64 */
65 public function check_permission() {
66 return current_user_can( 'update_plugins' );
67 }
68
69 /**
70 * Update one already-installed plugin to its latest published version.
71 *
72 * @param \WP_REST_Request $request The REST request object.
73 * @return \WP_REST_Response
74 */
75 public function update_plugin( $request ) {
76 $raw_slug = $request->get_param( 'slug' );
77 $raw_slug = is_string( $raw_slug ) ? $raw_slug : '';
78
79 // Same wp.org slug shape the install ability enforces. Validate the RAW
80 // value before sanitising: sanitize_key() would quietly strip a slash or
81 // dot and turn "foo/bar.php" into something that looks valid.
82 if ( ! preg_match( '/^[a-z0-9][a-z0-9-]{0,62}$/', $raw_slug ) ) {
83 return rest_ensure_response(
84 Response::error( 'That plugin name doesn’t look right, so nothing was changed.' )
85 );
86 }
87 $slug = sanitize_key( $raw_slug );
88
89 if ( ! wp_is_file_mod_allowed( 'zipai_update_plugin' ) ) {
90 return rest_ensure_response(
91 Response::error( 'This site is set up so plugins can’t be changed from the dashboard. Ask whoever manages your hosting to update it for you.' )
92 );
93 }
94
95 if ( is_multisite() && ! is_super_admin() ) {
96 return rest_ensure_response(
97 Response::error( 'Only a network administrator can update plugins on this site.' )
98 );
99 }
100
101 $this->load_upgrader_dependencies();
102
103 $plugin_file = Helper::find_plugin_file_for_slug( $slug );
104 if ( null === $plugin_file ) {
105 return rest_ensure_response(
106 Response::error( 'That plugin isn’t installed on this site, so there’s nothing to update.' )
107 );
108 }
109
110 // Capture the active state BEFORE upgrading. Outside cron,
111 // Plugin_Upgrader::upgrade() hooks `deactivate_plugin_before_upgrade`,
112 // which silently deactivates an active plugin for the file swap and does
113 // not turn it back on. We restore it below — but only if it was on to
114 // begin with, or we would switch on a plugin the user deliberately
115 // disabled. Network activation is tracked SEPARATELY: the deactivation
116 // core runs also strips the network entry, so restoring a
117 // network-activated plugin with a plain (single-site) activate would
118 // leave a 30-site network with SureForms active on one site only.
119 $was_network_active = is_plugin_active_for_network( $plugin_file );
120 $was_active = is_plugin_active( $plugin_file );
121
122 // Refresh the update data so the upgrader does not read a stale "no
123 // update available" and report success while changing nothing.
124 // `wp_update_plugins()` alone re-checks wp.org and updates the transient
125 // in place; we deliberately do NOT call `wp_clean_plugins_cache( true )`
126 // first. That wipe deletes the site-wide `update_plugins` transient for
127 // EVERY plugin, and `wp_update_plugins()` bails without writing anything
128 // back if the wp.org request errors — so a host firewall or a wp.org
129 // blip would leave the whole site showing zero pending updates for up to
130 // 12 hours, sending the user to a Plugins screen this very click emptied.
131 // Core's own wp_ajax_update_plugin() refreshes without the wipe for the
132 // same reason.
133 wp_update_plugins();
134
135 $upgrader = new \Plugin_Upgrader( new \WP_Ajax_Upgrader_Skin() );
136 $result = $upgrader->upgrade( $plugin_file );
137
138 if ( is_wp_error( $result ) ) {
139 return rest_ensure_response(
140 Response::error( 'The update couldn’t be completed. You can update this plugin from your Plugins screen instead.' )
141 );
142 }
143
144 // upgrade() returns false when there was nothing to do, and null when the
145 // filesystem was unavailable. Neither is an exception, and neither left
146 // the site updated — so neither should be reported as success.
147 if ( true !== $result ) {
148 return rest_ensure_response(
149 Response::error( 'No update was available to install. Check your Plugins screen for a pending update.' )
150 );
151 }
152
153 // Re-read the header so the caller learns the version it actually got,
154 // rather than the one we assumed was published. `false` clears only the
155 // get_plugins() list cache, NOT the site-wide update transient — the
156 // upgrade already cleared this plugin's own pending-update entry, and
157 // wiping the transient wholesale would drop every OTHER plugin's pending
158 // flag too (the same wp.org-outage trap as the pre-upgrade path).
159 wp_clean_plugins_cache( false );
160 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false );
161 $version = $plugin_data['Version'];
162 $name = '' !== $plugin_data['Name'] ? $plugin_data['Name'] : $slug;
163
164 // Restore the pre-update state. Only when it WAS active — a plugin the
165 // user had switched off must stay off, or an update silently turns it on.
166 // `$was_network_active` carries the network scope through so a
167 // network-activated plugin is restored network-wide, not just here.
168 // `$silent = true` matches core (wp-admin/update.php): the deactivation
169 // was silent, so firing the activation hook against the pre-swap code
170 // that is still in memory is the asymmetry to avoid.
171 if ( ( $was_active || $was_network_active ) && ! is_plugin_active( $plugin_file ) && ! is_plugin_active_for_network( $plugin_file ) ) {
172 $reactivated = activate_plugin( $plugin_file, '', $was_network_active, true );
173 if ( is_wp_error( $reactivated ) ) {
174 // Files updated but the plugin is now OFF — worse than not
175 // updating, and the caller must not treat this as success.
176 return rest_ensure_response(
177 Response::error( sprintf( '%s was updated but could not be switched back on. Please activate it from your Plugins screen.', $name ) )
178 );
179 }
180 }
181
182 return rest_ensure_response(
183 Response::success(
184 sprintf( '%s is now up to date.', $name ),
185 array(
186 'slug' => $slug,
187 'version' => $version,
188 )
189 )
190 );
191 }
192
193 /**
194 * Pull in the admin-only upgrader pieces. These live in wp-admin and are not
195 * loaded during a REST request.
196 *
197 * @return void
198 */
199 private function load_upgrader_dependencies() {
200 if ( ! function_exists( 'get_plugins' ) ) {
201 require_once ABSPATH . 'wp-admin/includes/plugin.php';
202 }
203 if ( ! function_exists( 'request_filesystem_credentials' ) ) {
204 require_once ABSPATH . 'wp-admin/includes/file.php';
205 }
206 if ( ! class_exists( 'Plugin_Upgrader' ) ) {
207 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
208 }
209 if ( ! class_exists( 'WP_Ajax_Upgrader_Skin' ) ) {
210 require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
211 }
212 }
213 }
214