| @@ -375,8 +375,107 @@ | ||
| 375 | 375 | } |
| 376 | 376 | } |
| 377 | 377 | |
| 378 | 378 | /** |
| 379 | + * Updates that can't be installed — fired by the license module (in cron) when | |
| 380 | + * WordPress's own update check found new versions of licensed addons while this | |
| 381 | + * site blocks plugin installs (DISALLOW_FILE_MODS / read-only plugins folder), so | |
| 382 | + * neither the auto-updater nor the Updates screen can install them. | |
| 383 | + * | |
| 384 | + * Every administrator gets ONE email listing the versions they were not told | |
| 385 | + * about yet — per-admin keys "plugin_slug:version", so each release notifies once | |
| 386 | + * and a later release notifies again. Own email-only category 'blocked_update' | |
| 387 | + * (allows() also covers the master unsubscribe). No proxy request: templates come | |
| 388 | + * from the cached daily payload or the local fallback, so consent is not involved. | |
| 389 | + * | |
| 390 | + * @param array $updates plugin_slug => [name, current_version, new_version] | |
| 391 | + */ | |
| 392 | + public function handle_blocked_updates( array $updates ): void { | |
| 393 | + $clean = []; | |
| 394 | + foreach( $updates as $slug => $update ) { | |
| 395 | + if( ! is_array( $update ) || empty( $update['new_version'] ) ) continue; | |
| 396 | + $slug = sanitize_key( (string) $slug ); | |
| 397 | + if( $slug === '' ) continue; | |
| 398 | + $new = sanitize_text_field( (string) $update['new_version'] ); | |
| 399 | + $clean[ $slug ] = [ | |
| 400 | + 'key' => $slug . ':' . $new, | |
| 401 | + 'name' => sanitize_text_field( (string) ( $update['name'] ?? $slug ) ), | |
| 402 | + 'current' => sanitize_text_field( (string) ( $update['current_version'] ?? '' ) ), | |
| 403 | + 'new' => $new, | |
| 404 | + ]; | |
| 405 | + } | |
| 406 | + if( ! $clean ) return; | |
| 407 | + | |
| 408 | + $templates = $this->get_blocked_update_templates(); | |
| 409 | + $meta_key = $this->config->get_emailed_blocked_updates_meta(); | |
| 410 | + | |
| 411 | + foreach( $this->get_recipients() as $user ) { | |
| 412 | + if( empty( $user->user_email ) ) continue; | |
| 413 | + if( ! $this->prefs->allows( $user->ID, PrefsService::CHANNEL_EMAILS, 'blocked_update' ) ) continue; | |
| 414 | + | |
| 415 | + $sent = get_user_meta( $user->ID, $meta_key, true ); | |
| 416 | + $sent = is_array( $sent ) ? $sent : []; | |
| 417 | + $fresh = array_filter( $clean, fn( $u ) => ! in_array( $u['key'], $sent, true ) ); | |
| 418 | + if( ! $fresh ) continue; | |
| 419 | + | |
| 420 | + $rows = ''; | |
| 421 | + foreach( $fresh as $u ) { | |
| 422 | + $rows .= str_replace( | |
| 423 | + [ '{product_name}', '{current_version}', '{new_version}' ], | |
| 424 | + [ esc_html( $u['name'] ), esc_html( $u['current'] !== '' ? $u['current'] : '—' ), esc_html( $u['new'] ) ], | |
| 425 | + $templates['row'] | |
| 426 | + ); | |
| 427 | + } | |
| 428 | + $count = (string) count( $fresh ); | |
| 429 | + $body = str_replace( [ '{update_rows}', '{update_count}' ], [ $rows, $count ], $templates['body_html'] ); | |
| 430 | + $subject = str_replace( '{update_count}', $count, $templates['subject'] ); | |
| 431 | + | |
| 432 | + if( $this->send( $user, $subject, $body ) ) { | |
| 433 | + $sent = array_merge( $sent, array_column( $fresh, 'key' ) ); | |
| 434 | + update_user_meta( $user->ID, $meta_key, array_slice( array_values( array_unique( $sent ) ), -100 ) ); | |
| 435 | + } | |
| 436 | + } | |
| 437 | + } | |
| 438 | + | |
| 439 | + /** | |
| 440 | + * Blocked-update templates: proxy-editable versions from the cached daily news | |
| 441 | + * payload (`blocked_update_email`), with a complete local fallback. | |
| 442 | + */ | |
| 443 | + private function get_blocked_update_templates(): array { | |
| 444 | + $payload = get_transient( $this->config->get_news_transient() ); | |
| 445 | + $remote = is_array( $payload ) && ! empty( $payload['blocked_update_email']['body_html'] ) ? $payload['blocked_update_email'] : null; | |
| 446 | + $row = '<div style="border:1px solid #e2e4e8;border-left:4px solid #996800;border-radius:4px;padding:12px 18px;margin:0 0 12px;">' | |
| 447 | + . '<div style="font-size:14px;color:#1d2327;"><strong>{product_name}</strong></div>' | |
| 448 | + . '<div style="font-size:13px;color:#50575e;margin-top:4px;">Installed: {current_version} → new version: <strong>{new_version}</strong></div>' | |
| 449 | + . '</div>'; | |
| 450 | + | |
| 451 | + if( $remote ) { | |
| 452 | + return [ | |
| 453 | + 'subject' => ! empty( $remote['subject'] ) ? $remote['subject'] : __( 'New addon versions can\'t be installed on {site_name}', 'gvectors' ), | |
| 454 | + 'body_html' => $remote['body_html'], | |
| 455 | + 'row' => ! empty( $remote['row'] ) ? $remote['row'] : $row, | |
| 456 | + ]; | |
| 457 | + } | |
| 458 | + | |
| 459 | + return [ | |
| 460 | + 'subject' => __( 'New addon versions can\'t be installed on {site_name}', 'gvectors' ), | |
| 461 | + 'body_html' => '<div style="background:#f4f5f7;padding:24px 0;font-family:Arial,Helvetica,sans-serif;">' | |
| 462 | + . '<div style="max-width:600px;margin:0 auto;background:#ffffff;border-radius:8px;border:1px solid #e2e4e8;padding:28px;">' | |
| 463 | + . '<p style="margin:0 0 16px;color:#1d2327;font-size:15px;">Hi {admin_name},</p>' | |
| 464 | + . '<p style="margin:0 0 20px;color:#50575e;font-size:14px;line-height:1.6;">WordPress found new versions of your licensed gVectors addons on {site_name}, but it could not install them: this website does not allow WordPress to download and install plugins (for example <code>DISALLOW_FILE_MODS</code> in <code>wp-config.php</code> or a read-only plugins folder).</p>' | |
| 465 | + . '{update_rows}' | |
| 466 | + . '<div style="margin-top:20px;font-size:13px;line-height:1.6;color:#1d4d2b;background:#edfaef;border:1px solid #b8e6bf;border-radius:4px;padding:12px 16px;">' | |
| 467 | + . '<strong>Recommended — install the updates immediately:</strong> restore the default WordPress permissions: remove <code>DISALLOW_FILE_MODS</code> from <code>wp-config.php</code> (or set it to <code>false</code>) and make sure WordPress can write to <code>wp-content/plugins/</code> — your hosting provider can help with that. ' | |
| 468 | + . 'The updates then appear in Dashboard → Updates and install with one click, and future releases arrive the same natural way.' | |
| 469 | + . '<div style="margin-top:6px;font-size:12px;">Tip: if you only want to block code editing in the dashboard, <code>DISALLOW_FILE_EDIT</code> does that without blocking updates.</div>' | |
| 470 | + . '</div>' | |
| 471 | + . '<p style="margin:16px 0 0;color:#8c8f94;font-size:12px;line-height:1.6;">If you can\'t change these settings, contact gVectors support to receive a manual download link for the new version(s).</p>' | |
| 472 | + . '</div></div>', | |
| 473 | + 'row' => $row, | |
| 474 | + ]; | |
| 475 | + } | |
| 476 | + | |
| 477 | + /** | |
| 379 | 478 | * Purchase templates: proxy-editable versions from the cached daily news |
| 380 | 479 | * payload, with a complete local fallback (a purchase must never lose its |
| 381 | 480 | * confirmation email because the cache is cold). |
| 382 | 481 | */ |