JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
namespace WPForms\SetupWizard\Service;
/**
* Suppresses the onboarding redirect a cross-plugin arms when WPForms activates it.
*
* Several plugins WPForms can install on the user's behalf arm a "just activated,
* go to my onboarding" flag and consume it on `admin_init` at a priority ahead of
* the Setup Wizard's own launch hook (`PHP_INT_MAX`), throwing the user out of our
* onboarding and into theirs. This listener writes each plugin's opt-out flag the
* moment WPForms activates it.
*
* Hooked on `wpforms_plugin_activated`, and registered by `SetupWizard::hooks()`
* only when `RestApi::is_install_request()` says the current request is the
* wizard's own `install-plugins` REST call. Outside that request the listener is
* never registered, so installs from the Setup Checklist promos or the Education
* one-click links leave each plugin's own onboarding intact — those users never
* entered our wizard, and suppression is permanent. On this path the action fires
* from `WPForms\Helpers\Plugin::activate()` immediately after `activate_plugin()`
* succeeds, so a flag is never written for a plugin whose install failed.
*
* Suppression is permanent and never restored: WPForms' own onboarding is the one
* the user chose, and each plugin's wizard stays reachable from its own admin menu.
*
* @since 2.0.1
*/
class CrossPluginOnboarding {
/**
* Suppression registry, keyed by the activated plugin's directory slug.
*
* Keyed by slug rather than by full plugin file because the main-file
* basename reported to `wpforms_plugin_activated` can differ from the
* catalog's canonical file, and because each Pro variant lives in its own
* directory. `Education\WPConsent\InstallTracker` matches the same way.
*
* Each vendor reference below was verified by scanning every PHP file in the
* plugin's then-current wp.org release on 2026-08-11. A vendor renaming a flag
* degrades silently to an unsuppressed redirect, so re-verify these citations
* when touching this list.
*
* @since 2.0.1
*
* @var array<string, array{op: string, flag?: string}>
*/
private const SUPPRESSIONS = [
// Documented opt-out option, checked at admin_init:9999 in
// src/Admin/SetupWizard.php:132. Armed as a 30s transient in src/Core.php:678.
'wp-mail-smtp' => [
'op' => 'enable_option',
'flag' => 'wp_mail_smtp_activation_prevent_redirect',
],
// Pro ships the same Setup Wizard as Lite, so the option name is shared.
'wp-mail-smtp-pro' => [
'op' => 'enable_option',
'flag' => 'wp_mail_smtp_activation_prevent_redirect',
],
// Truthy value means "suppress" despite the name reading the other way:
// app/Common/Standalone/SetupWizard.php:62 returns early when it is set.
'all-in-one-seo-pack' => [
'op' => 'enable_option',
'flag' => 'aioseo_activation_redirect',
],
// Dedicated disable option, checked at OMAPI/Welcome.php:112.
'optinmonster' => [
'op' => 'enable_option',
'flag' => 'optin_monster_api_activation_redirect_disabled',
],
// One-shot 30s transient armed in the activation hook at
// googleanalytics.php:615, consumed at lite/includes/admin/welcome.php:37.
'google-analytics-for-wordpress' => [
'op' => 'delete_transient',
'flag' => '_monsterinsights_activation_redirect',
],
// One-shot 60s transient armed in the activation hook at
// app/Onboarding.php:116, consumed at app/Onboarding.php:45 on priority 10.
'universally-language-translation-multilingual-tool' => [
'op' => 'delete_transient',
'flag' => 'universally_onboarding_redirect',
],
// Durable option armed in the activation hook via
// classes/class.plugin.upgrade.php:111, gated at
// src/Controllers/WelcomeController.php:96. Its documented
// `duplicator_disable_onboarding_redirect` filter is not usable here: it is
// evaluated on a later request, so it would need an always-on registration
// that also suppressed installs WPForms had nothing to do with.
'duplicator' => [
'op' => 'delete_option',
'flag' => 'duplicator_redirect_to_welcome',
],
// The only plugin here with no reachable install-time flag. It arms
// `wpconsent_onboarding_redirect` from its own admin_init handler
// (includes/class-wpconsent-install.php:54), which bails on non-admin
// requests — and the Setup Wizard installs over REST, where admin_init never
// fires. So the transient does not exist yet and deleting it would find
// nothing; it gets armed on the next admin page the user opens and consumed
// in that same request at priority 9999 (includes/admin/onboarding.php:12).
// Seeding the option its arming branch is gated on is what prevents it.
'wpconsent-cookies-banner-privacy-suite' => [
'op' => 'seed_wpconsent',
],
// WPConsent Premium has no entry: its activation timestamp is stored under
// `wpconsent_pro`, not `wpconsent`, so seeding the Lite key would neither
// suppress it nor be truthful — and the wizard cannot install Premium anyway.
];
/**
* Register hooks.
*
* Called by `SetupWizard::hooks()` only for the wizard's own install request,
* so simply being loaded does not arm the listener.
*
* @since 2.0.1
*/
public function hooks(): void {
add_action( 'wpforms_plugin_activated', [ $this, 'suppress' ] );
}
/**
* Suppress the activated plugin's onboarding redirect, when it has one.
*
* The parameter is deliberately untyped: the action also fires for every
* WPForms addon activation and from third-party code, so the value is
* validated rather than type-hinted.
*
* @since 2.0.1
*
* @param string $plugin_basename Path to the activated plugin file relative to the plugins directory.
*
* @noinspection PhpMissingParamTypeInspection
*/
public function suppress( $plugin_basename ): void {
if ( ! is_string( $plugin_basename ) || $plugin_basename === '' ) {
return;
}
// Split rather than dirname()/basename(): those are locale-aware and have
// produced results that differ between local runs and CI.
$slug = explode( '/', $plugin_basename )[0];
$entry = self::SUPPRESSIONS[ $slug ] ?? null;
if ( $entry === null ) {
return;
}
$this->apply( $entry );
}
/**
* Apply a single registry entry.
*
* Every operation is best-effort: a failed write cannot break the install that
* triggered it, and every entry is idempotent under re-activation.
*
* @since 2.0.1
*
* @param array $entry Registry entry for the activated plugin.
*/
private function apply( array $entry ): void {
switch ( $entry['op'] ) {
case 'enable_option':
update_option( $entry['flag'], true );
break;
case 'delete_option':
delete_option( $entry['flag'] );
break;
case 'delete_transient':
delete_transient( $entry['flag'] );
break;
case 'seed_wpconsent':
$this->seed_wpconsent();
break;
// Unreachable while `SUPPRESSIONS` is the only source of `op`, and every
// value it uses has a case above. A registry entry naming an operation
// that does not exist here fails that entry's own unit test rather than
// silently suppressing nothing.
default:
break;
}
}
/**
* Seed WPConsent's first-activation option so its onboarding redirect is
* never armed.
*
* Writes exactly what `WPConsent_Install::maybe_run_install()` would have
* written, which keeps its fresh-install path intact: table creation still runs
* off the separate `wpconsent_install` option via its activation hook, the `0`
* version still triggers its `update_1_0_5()` cache clear, and
* includes/class-wpconsent-install.php:79 stamps the real version afterwards.
*
* @since 2.0.1
*/
private function seed_wpconsent(): void {
// Clears a transient armed by an earlier admin request that ended before the
// redirect at priority 9999 could fire. The seeding below cannot cover that
// case, because it no-ops once `wpconsent_activated` is set — and that same
// earlier request is what wrote it. Deleting an absent transient is a no-op,
// so this is free on the ordinary fresh-install path.
delete_transient( 'wpconsent_onboarding_redirect' );
$activated = (array) get_option( 'wpconsent_activated', [] );
// A pre-existing install has already spent its onboarding redirect, and its
// first-activation timestamp must not move.
if ( ! empty( $activated['wpconsent'] ) ) {
return;
}
$activated['wpconsent'] = time();
$activated['version'] = '0';
update_option( 'wpconsent_activated', $activated );
}
}