PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.61
Timetics – Appointment Booking Calendar & Scheduling v1.0.61
1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 All 62 releases
timetics / core / addon / api-addon.php

api-addon.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.61, at core/addon/api-addon.php

307 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Addon REST API Controller
4 *
5 * @package Timetics
6 */
7
8 namespace Timetics\Core\Addon;
9
10 defined( 'ABSPATH' ) || exit;
11
12 use Timetics\Base\Api;
13 use Timetics\Utils\Singleton;
14 use Arraytics\ToolsSdk\PluginManager;
15 use WP_REST_Request;
16
17 /**
18 * Class Api_Addon
19 *
20 * Handles GET (list) and PUT (status update) for Arraytics plugins
21 * displayed on the About Us page.
22 *
23 * @since 1.0.0
24 */
25 class Api_Addon extends Api {
26
27 use Singleton;
28
29 /**
30 * REST namespace.
31 *
32 * @var string
33 */
34 protected $namespace = 'timetics/v1';
35
36 /**
37 * REST base route.
38 *
39 * @var string
40 */
41 protected $rest_base = 'addons';
42
43 /**
44 * Register REST routes.
45 *
46 * @return void
47 */
48 public function register_routes() {
49 register_rest_route(
50 $this->namespace,
51 '/' . $this->rest_base,
52 [
53 [
54 'methods' => \WP_REST_Server::READABLE,
55 'callback' => [ $this, 'get_items' ],
56 'permission_callback' => [ $this, 'get_items_permissions_check' ],
57 'args' => [
58 'type' => [
59 'description' => __( 'Filter by extension type: module, addon, plugin, or all.', 'timetics' ),
60 'type' => 'string',
61 'enum' => [ 'module', 'addon', 'plugin', 'all' ],
62 'default' => 'all',
63 ],
64 ],
65 ],
66 [
67 'methods' => \WP_REST_Server::EDITABLE,
68 'callback' => [ $this, 'update_item' ],
69 'permission_callback' => [ $this, 'update_item_permissions_check' ],
70 ],
71 ]
72 );
73 }
74
75 /**
76 * Permission check for GET.
77 *
78 * @return bool
79 */
80 public function get_items_permissions_check( $request ) {
81 return current_user_can( 'manage_options' );
82 }
83
84 /**
85 * Permission check for PUT/POST.
86 *
87 * @return bool
88 */
89 public function update_item_permissions_check( $request ) {
90 return current_user_can( 'manage_options' );
91 }
92
93 /**
94 * GET /timetics/v1/addons
95 *
96 * Returns the addon list filtered by ?type=module|addon|plugin|all.
97 *
98 * @param WP_REST_Request $request
99 * @return \WP_REST_Response
100 */
101 public function get_items( $request ) {
102 $type = ! empty( $request['type'] ) ? sanitize_key( $request['type'] ) : 'all';
103 $extensions = timetics_extension();
104
105 $type_map = [
106 'module' => [ $extensions, 'get_modules' ],
107 'addon' => [ $extensions, 'get_addons' ],
108 'plugin' => [ $extensions, 'get_plugins' ],
109 'all' => [ $extensions, 'get' ],
110 ];
111
112 if ( ! isset( $type_map[ $type ] ) ) {
113 return $this->send_error(
114 __( 'Invalid extension type.', 'timetics' ),
115 [ 'status' => 400 ]
116 );
117 }
118
119 $items = array_values( call_user_func( $type_map[ $type ] ) );
120
121 return rest_ensure_response(
122 [
123 'success' => true,
124 'data' => $items,
125 ]
126 );
127 }
128
129 /**
130 * PUT /timetics/v1/addons
131 *
132 * Updates the status of an Arraytics plugin (install/activate/deactivate/upgrade).
133 *
134 * @param WP_REST_Request $request
135 * @return \WP_REST_Response
136 */
137 public function update_item( $request ) {
138 $params = json_decode( $request->get_body(), true );
139
140 $name = isset( $params['name'] ) ? sanitize_text_field( $params['name'] ) : '';
141 $status = isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : '';
142
143 $valid_statuses = [ 'install', 'activate', 'deactivate', 'upgrade' ];
144
145 if ( empty( $name ) ) {
146 return $this->send_error(
147 __( 'Please enter an extension name.', 'timetics' ),
148 [ 'status' => 422 ]
149 );
150 }
151
152 if ( empty( $status ) || ! in_array( $status, $valid_statuses, true ) ) {
153 return $this->send_error(
154 /* translators: %s: status value */
155 sprintf( __( 'Invalid status "%s" provided.', 'timetics' ), $status ),
156 [ 'status' => 422 ]
157 );
158 }
159
160 $extension = timetics_extension()->find( $name );
161
162 if ( ! $extension ) {
163 return $this->send_error(
164 /* translators: %s: plugin name */
165 sprintf( __( 'Extension "%s" not found.', 'timetics' ), $name ),
166 [ 'status' => 404 ]
167 );
168 }
169
170 // Redirect for upgrade (premium) actions.
171 if ( 'upgrade' === $status ) {
172 return rest_ensure_response(
173 [
174 'success' => true,
175 'data' => [ 'redirect_url' => $extension['upgrade_link'] ],
176 'message' => __( 'Redirecting to upgrade page.', 'timetics' ),
177 ]
178 );
179 }
180
181 // All registered extensions are type=plugin — delegate to PluginManager.
182 $slug = isset( $extension['slug'] ) ? $extension['slug'] : $name;
183
184 // Our-Plugins download_url wins over the wordpress.org slug lookup, so a
185 // non-wordpress.org URL (e.g. GitHub release zip) is not shadowed.
186 $download_url = ! empty( $extension['download_url'] ) ? $extension['download_url'] : '';
187
188 switch ( $status ) {
189 case 'install':
190 if ( ! function_exists( 'WP_Filesystem' ) ) {
191 require_once ABSPATH . 'wp-admin/includes/file.php';
192 }
193 WP_Filesystem();
194 $result = $download_url
195 ? $this->install_from_url( $download_url )
196 : PluginManager::install_plugin( $slug );
197 break;
198 case 'activate':
199 $result = PluginManager::activate_plugin( $slug );
200 break;
201 case 'deactivate':
202 $result = PluginManager::deactivate_plugin( $slug );
203 break;
204 default:
205 $result = false;
206 }
207
208 if ( false === $result || is_wp_error( $result ) ) {
209 $message = is_wp_error( $result )
210 ? $result->get_error_message()
211 /* translators: %s: action name */
212 : sprintf( __( 'Could not %s the extension.', 'timetics' ), $status );
213
214 return $this->send_error( $message, [ 'status' => 500 ] );
215 }
216
217 return rest_ensure_response(
218 [
219 'success' => true,
220 'data' => [
221 'name' => $name,
222 'status' => $status,
223 ],
224 /* translators: %s: action name */
225 'message' => sprintf( __( 'Extension %s successfully.', 'timetics' ), $status . 'd' ),
226 ]
227 );
228 }
229
230 /**
231 * Install a plugin from an explicit download URL.
232 *
233 * The URL must be HTTPS and its host (or a subdomain of it) must be in the
234 * trusted-domain allowlist. This lets us install Arraytics plugins hosted
235 * outside wordpress.org (e.g. GitHub release zips).
236 *
237 * @param string $url Absolute HTTPS download URL.
238 * @return bool|\WP_Error True on success, WP_Error on failure.
239 */
240 private function install_from_url( string $url ) {
241 $allowed_hosts = [
242 'wordpress.org',
243 'downloads.wordpress.org',
244 'arraytics.com',
245 'themewinter.com',
246 'github.com',
247 ];
248
249 $parsed = wp_parse_url( $url );
250
251 if ( empty( $parsed['scheme'] ) || 'https' !== strtolower( $parsed['scheme'] ) || empty( $parsed['host'] ) ) {
252 return new \WP_Error(
253 'invalid_download_url',
254 __( 'Download URL must use HTTPS from a trusted domain.', 'timetics' )
255 );
256 }
257
258 $host = strtolower( $parsed['host'] );
259 $trusted = false;
260
261 foreach ( $allowed_hosts as $allowed ) {
262 if ( $host === $allowed || substr( $host, - ( strlen( $allowed ) + 1 ) ) === '.' . $allowed ) {
263 $trusted = true;
264 break;
265 }
266 }
267
268 if ( ! $trusted ) {
269 return new \WP_Error(
270 'invalid_download_url',
271 __( 'Download URL must use HTTPS from a trusted domain.', 'timetics' )
272 );
273 }
274
275 include_once ABSPATH . 'wp-admin/includes/file.php';
276 include_once ABSPATH . 'wp-admin/includes/misc.php';
277 include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
278
279 $skin = new \Automatic_Upgrader_Skin();
280 $upgrader = new \Plugin_Upgrader( $skin );
281 $result = $upgrader->install( $url );
282
283 if ( is_wp_error( $result ) ) {
284 return $result;
285 }
286
287 return $result ? true : false;
288 }
289
290 /**
291 * Return a standardised error response.
292 *
293 * @param string $message Human-readable error message.
294 * @param array $data Additional data (e.g. ['status' => 422]).
295 * @return \WP_REST_Response
296 */
297 private function send_error( string $message, array $data = [] ): \WP_REST_Response {
298 return rest_ensure_response(
299 [
300 'success' => false,
301 'message' => $message,
302 'data' => $data,
303 ]
304 );
305 }
306 }
307