PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 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 All 65 releases
← All changes | core/addon/api-addon.php +191 -5 1.0.56 → 1.0.64 View file →
@@ -156,8 +156,18 @@
156 156 [ 'status' => 422 ]
157 157 );
158 158 }
159 159
160 + // The Ask AI setup dialog lets the user edit the account email. Reject a bad one before anything is installed.
161 + $email = isset( $params['email'] ) ? sanitize_email( $params['email'] ) : '';
162 +
163 + if ( isset( $params['email'] ) && ! is_email( $email ) ) {
164 + return $this->send_error(
165 + __( 'Enter a valid email address.', 'timetics' ),
166 + [ 'status' => 422 ]
167 + );
168 + }
169 +
160 170 $extension = timetics_extension()->find( $name );
161 171
162 172 if ( ! $extension ) {
163 173 return $this->send_error(
@@ -180,8 +190,22 @@
180 190
181 191 // All registered extensions are type=plugin — delegate to PluginManager.
182 192 $slug = isset( $extension['slug'] ) ? $extension['slug'] : $name;
183 193
194 + // Our-Plugins download_url wins over the wordpress.org slug lookup, so a
195 + // non-wordpress.org URL (e.g. GitHub release zip) is not shadowed.
196 + $download_url = ! empty( $extension['download_url'] ) ? $extension['download_url'] : '';
197 +
198 + // PluginManager checks no capabilities, so require what doing this by hand in Plugins needs.
199 + $needs_install = 'install' === $status || ( 'activate' === $status && ! PluginManager::is_installed( $slug ) );
200 +
201 + if ( ! current_user_can( $needs_install ? 'install_plugins' : 'activate_plugins' ) ) {
202 + return $this->send_error(
203 + __( 'Sorry, you are not allowed to manage plugins on this site.', 'timetics' ),
204 + [ 'status' => 403 ]
205 + );
206 + }
207 +
184 208 switch ( $status ) {
185 209 case 'install':
186 210 if ( ! function_exists( 'WP_Filesystem' ) ) {
187 211 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -186,11 +210,34 @@
186 210 if ( ! function_exists( 'WP_Filesystem' ) ) {
187 211 require_once ABSPATH . 'wp-admin/includes/file.php';
188 212 }
189 213 WP_Filesystem();
190 - $result = PluginManager::install_plugin( $slug );
214 + $result = $download_url
215 + ? $this->install_from_url( $download_url )
216 + : PluginManager::install_plugin( $slug );
191 217 break;
192 218 case 'activate':
219 + // Activate can be reached on a plugin that was never installed
220 + // (onboarding offers it in one click), so install on demand.
221 + if ( ! PluginManager::is_installed( $slug ) ) {
222 + if ( ! function_exists( 'WP_Filesystem' ) ) {
223 + require_once ABSPATH . 'wp-admin/includes/file.php';
224 + }
225 + WP_Filesystem();
226 + $install = $download_url
227 + ? $this->install_from_url( $download_url )
228 + : PluginManager::install_plugin( $slug );
229 +
230 + if ( false === $install || is_wp_error( $install ) ) {
231 + return $this->send_error(
232 + is_wp_error( $install )
233 + ? $install->get_error_message()
234 + : __( 'Plugin installation failed.', 'timetics' ),
235 + [ 'status' => 500 ]
236 + );
237 + }
238 + }
239 +
193 240 $result = PluginManager::activate_plugin( $slug );
194 241 break;
195 242 case 'deactivate':
196 243 $result = PluginManager::deactivate_plugin( $slug );
@@ -207,19 +254,158 @@
207 254
208 255 return $this->send_error( $message, [ 'status' => 500 ] );
209 256 }
210 257
258 + $data = [
259 + 'name' => $name,
260 + 'status' => $status,
261 + ];
262 +
263 + /*
264 + * Registration only runs when the caller sent explicit consent, which
265 + * today means the onboarding checkbox, the dashboard banner button or
266 + * the Ask AI setup dialog. Activating from About Us installs the plugin
267 + * and stops there, so no identity leaves the site without the user
268 + * opting in. Strict: a "1" or "true" string never counts as agreement.
269 + */
270 + if ( 'aisentic' === $name && 'activate' === $status && true === ( $params['consent'] ?? null ) && PluginManager::is_activated( $slug ) ) {
271 + // Snapshot before the handshake so the caller can tell a fresh
272 + // registration (tokens just granted) from re-activating a site that
273 + // was already connected (no new tokens).
274 + $was_registered = timetics_aisentic_is_registered();
275 +
276 + $this->register_aisentic_site( $email );
277 +
278 + $is_registered = timetics_aisentic_is_registered();
279 +
280 + // The banner needs to know whether the handshake actually landed so
281 + // it can show an error instead of silently disappearing.
282 + $data['aisentic_registered'] = $is_registered;
283 +
284 + // True only when this request is what connected the site, so the
285 + // "150K tokens added" message never fires on a plain re-activation.
286 + $data['aisentic_newly_registered'] = $is_registered && ! $was_registered;
287 + }
288 +
211 289 return rest_ensure_response(
212 290 [
213 291 'success' => true,
214 - 'data' => [
215 - 'name' => $name,
216 - 'status' => $status,
217 - ],
292 + 'data' => $data,
218 293 /* translators: %s: action name */
219 294 'message' => sprintf( __( 'Extension %s successfully.', 'timetics' ), $status . 'd' ),
220 295 ]
221 296 );
297 + }
298 +
299 + /**
300 + * Record the user's consent and hand the identity to Aisentic.
301 + *
302 + * Values come from timetics_aisentic_identity() so they match what the
303 + * consent UI showed. Aisentic swallows provider errors and skips the call
304 + * when it already has an api key, so this never affects the activation
305 + * response.
306 + *
307 + * @param string $email Email the user typed, empty to use their account email.
308 + * @return void
309 + */
310 + private function register_aisentic_site( $email = '' ) {
311 + // Older Aisentic builds have no listener for the action below, so the
312 + // handshake would go nowhere. Skip instead of storing consent for a
313 + // registration that cannot happen.
314 + if ( ! class_exists( 'Aisentic\Api\Services\Registration_Service' ) ) {
315 + return;
316 + }
317 +
318 + $identity = timetics_aisentic_identity( $email );
319 +
320 + // No email means nothing to register with, and Aisentic would reject
321 + // the call anyway. Fail closed rather than inventing a value.
322 + if ( empty( $identity['email'] ) ) {
323 + return;
324 + }
325 +
326 + // Proof of consent: who agreed, when, and for which email. Also lets
327 + // the banner tell "declined" apart from "never asked".
328 + update_option(
329 + 'timetics_aisentic_consent',
330 + [
331 + 'agreed' => true,
332 + 'time' => gmdate( 'c' ),
333 + 'user_id' => get_current_user_id(),
334 + 'email' => $identity['email'],
335 + ],
336 + false
337 + );
338 +
339 + /**
340 + * Fires after the user opts in to connecting the site with Aisentic.
341 + *
342 + * Aisentic's Timetics integration listens for this, registers the site
343 + * with its provider and marks itself connected.
344 + *
345 + * @param string $account_name Account name shown in the consent UI.
346 + * @param string $email Account email shown in the consent UI.
347 + * @param string $site_url Site URL to register with the provider.
348 + */
349 + do_action( 'timetics/aisentic/register_site', $identity['name'], $identity['email'], $identity['site_url'] );
350 + }
351 +
352 + /**
353 + * Install a plugin from an explicit download URL.
354 + *
355 + * The URL must be HTTPS and its host (or a subdomain of it) must be in the
356 + * trusted-domain allowlist.
357 + *
358 + * @param string $url Absolute HTTPS download URL.
359 + * @return bool|\WP_Error True on success, WP_Error on failure.
360 + */
361 + private function install_from_url( string $url ) {
362 + $allowed_hosts = [
363 + 'wordpress.org',
364 + 'downloads.wordpress.org',
365 + 'arraytics.com',
366 + 'themewinter.com',
367 + ];
368 +
369 + $parsed = wp_parse_url( $url );
370 +
371 + if ( empty( $parsed['scheme'] ) || 'https' !== strtolower( $parsed['scheme'] ) || empty( $parsed['host'] ) ) {
372 + return new \WP_Error(
373 + 'invalid_download_url',
374 + __( 'Download URL must use HTTPS from a trusted domain.', 'timetics' )
375 + );
376 + }
377 +
378 + $host = strtolower( $parsed['host'] );
379 + $trusted = false;
380 +
381 + foreach ( $allowed_hosts as $allowed ) {
382 + if ( $host === $allowed || substr( $host, - ( strlen( $allowed ) + 1 ) ) === '.' . $allowed ) {
383 + $trusted = true;
384 + break;
385 + }
386 + }
387 +
388 + if ( ! $trusted ) {
389 + return new \WP_Error(
390 + 'invalid_download_url',
391 + __( 'Download URL must use HTTPS from a trusted domain.', 'timetics' )
392 + );
393 + }
394 +
395 + include_once ABSPATH . 'wp-admin/includes/file.php';
396 + include_once ABSPATH . 'wp-admin/includes/misc.php';
397 + include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
398 +
399 + $skin = new \Automatic_Upgrader_Skin();
400 + $upgrader = new \Plugin_Upgrader( $skin );
401 + $result = $upgrader->install( $url );
402 +
403 + if ( is_wp_error( $result ) ) {
404 + return $result;
405 + }
406 +
407 + return $result ? true : false;
222 408 }
223 409
224 410 /**
225 411 * Return a standardised error response.