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 +127 -7 1.0.61 → 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(
@@ -184,8 +194,18 @@
184 194 // Our-Plugins download_url wins over the wordpress.org slug lookup, so a
185 195 // non-wordpress.org URL (e.g. GitHub release zip) is not shadowed.
186 196 $download_url = ! empty( $extension['download_url'] ) ? $extension['download_url'] : '';
187 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 +
188 208 switch ( $status ) {
189 209 case 'install':
190 210 if ( ! function_exists( 'WP_Filesystem' ) ) {
191 211 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -195,8 +215,29 @@
195 215 ? $this->install_from_url( $download_url )
196 216 : PluginManager::install_plugin( $slug );
197 217 break;
198 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 +
199 240 $result = PluginManager::activate_plugin( $slug );
200 241 break;
201 242 case 'deactivate':
202 243 $result = PluginManager::deactivate_plugin( $slug );
@@ -213,15 +254,43 @@
213 254
214 255 return $this->send_error( $message, [ 'status' => 500 ] );
215 256 }
216 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 +
217 289 return rest_ensure_response(
218 290 [
219 291 'success' => true,
220 - 'data' => [
221 - 'name' => $name,
222 - 'status' => $status,
223 - ],
292 + 'data' => $data,
224 293 /* translators: %s: action name */
225 294 'message' => sprintf( __( 'Extension %s successfully.', 'timetics' ), $status . 'd' ),
226 295 ]
227 296 );
@@ -227,13 +296,65 @@
227 296 );
228 297 }
229 298
230 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 + /**
231 353 * Install a plugin from an explicit download URL.
232 354 *
233 355 * 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).
356 + * trusted-domain allowlist.
236 357 *
237 358 * @param string $url Absolute HTTPS download URL.
238 359 * @return bool|\WP_Error True on success, WP_Error on failure.
239 360 */
@@ -242,9 +363,8 @@
242 363 'wordpress.org',
243 364 'downloads.wordpress.org',
244 365 'arraytics.com',
245 366 'themewinter.com',
246 - 'github.com',
247 367 ];
248 368
249 369 $parsed = wp_parse_url( $url );
250 370