PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/AdminPageRouter.php +276 -64 0.11.13.2.1 View file →
@@ -1,5 +1,6 @@
1 1 <?php
2 +
2 3 /**
3 4 * Simple router to handle admin page loading
4 5 */
5 6
@@ -4,18 +5,22 @@
4 5 */
5 6
6 7 namespace Extendify;
7 8
8 -use Extendify\Config;
9 -use Extendify\Library\AdminPage as LibraryAdminPage;
9 +defined('ABSPATH') || die('No direct access.');
10 +
10 11 use Extendify\Assist\AdminPage as AssistAdminPage;
12 +use Extendify\Launch\AdminPage as LaunchAdminPage;
13 +use Extendify\AutoLaunch\AdminPage as AutoLaunchAdminPage;
11 14
12 15 /**
13 16 * This class handles routing when the main admin button is pressed.
17 + * This used to be more robust but now just routes to Assist. And
18 + * possibly loads Launch depending on the state.
14 19 */
20 +
15 21 class AdminPageRouter
16 22 {
17 -
18 23 /**
19 24 * Adds various actions to set up the page
20 25 *
21 26 * @return void
@@ -21,60 +26,76 @@
21 26 * @return void
22 27 */
23 28 public function __construct()
24 29 {
25 - // Don't show the admin page if not using the standalone version (e.g. Redux).
26 - if (!Config::$standalone) {
27 - return;
28 - }
30 + // This does the initial redirect to Launch.
31 + \add_action('admin_init', [$this, 'redirectOnce'], 10);
32 + \add_action('admin_init', [$this, 'maybeForceFlush'], 5);
33 + \add_action('admin_init', [$this, 'handleLaunchRedirect'], 1);
29 34
35 + // Add a dropdown above Dashboard in the admin toolbar.
36 + \add_action('admin_bar_menu', function ($wpAdminBar) {
37 + if (!PartnerData::$id || is_admin()) {
38 + return;
39 + }
40 +
41 + $wpAdminBar->add_node([
42 + 'id' => 'extendify-site-assistant',
43 + 'title' => \__('Site Assistant', 'extendify-local'),
44 + 'href' => \admin_url() . $this->getRoute(),
45 + 'parent' => 'site-name',
46 + 'meta' => ['position' => 1],
47 + ]);
48 + }, 11);
49 +
30 50 // When Launch is finished, fire this to set the correct permalinks.
31 51 // phpcs:ignore WordPress.Security.NonceVerification
32 52 if (isset($_GET['extendify-launch-success'])) {
33 - \add_action( 'admin_init', function () {
53 + \add_action('admin_init', function () {
34 54 \flush_rewrite_rules();
35 55 });
36 56 }
37 57
38 - // Add top admin page to handle redirects. Everything
39 - // else will be a subpage of this.
40 - \add_action('admin_menu', [ $this, 'addAdminMenu' ]);
41 -
42 58 \add_action('admin_menu', function () {
43 - // Load the Assist page when Launch is finished.
44 - if (Config::$showAssist && Config::$launchCompleted) {
45 - $assist = new AssistAdminPage();
46 - $cb = [$assist, 'pageContent'];
47 - $this->addSubMenu('Assist', $assist->slug, $cb);
59 + // If no partner, we don't show any menu.
60 + if (!PartnerData::$id) {
61 + return;
48 62 }
49 63
50 - // Always load the Library page.
51 - $library = new LibraryAdminPage();
52 - $cb = [$library, 'pageContent'];
53 - $this->addSubMenu('Library', $library->slug, $cb);
64 + $assist = new AssistAdminPage();
65 + // Adds the top Assist menu.
66 + $this->addAdminMenu(
67 + __('Site Assistant', 'extendify-local'),
68 + $assist->slug,
69 + [$assist, 'pageContent']
70 + );
54 71
55 - // Show the Launch menu for dev users.
56 - if (Config::$environment === 'DEVELOPMENT') {
57 - $this->addSubMenu('Launch', 'post-new.php?extendify=onboarding');
72 + if (!Config::$showLaunch) {
73 + return;
58 74 }
75 +
76 + $launch = new LaunchAdminPage();
77 + $this->addSubMenu(
78 + // translators: Launch is a noun.
79 + __('Launch', 'extendify-local'),
80 + $launch->slug,
81 + [$launch, 'pageContent']
82 + );
83 + $autoLaunch = new AutoLaunchAdminPage();
84 + $this->addSubMenu(
85 + // translators: Launch is a noun.
86 + __('Auto Launch', 'extendify-local'),
87 + $autoLaunch->slug,
88 + [$autoLaunch, 'pageContent']
89 + );
59 90 });
60 91
61 - // Hide the menu items unless in dev mode.
62 - if (Config::$environment === 'PRODUCTION') {
63 - add_action('admin_head', function () {
64 - echo '<style>
65 - #toplevel_page_extendify-admin-page .wp-submenu {
66 - display:none!important;
67 - }
68 - #toplevel_page_extendify-admin-page::after {
69 - content:none!important;
70 - }
71 - </style>';
72 - });
73 - }
92 + // If the user is redirected to this while visiting our url, intercept it.
93 + \add_filter('wp_redirect', function ($url) {
94 + if (!PartnerData::$id) {
95 + return $url;
96 + }
74 97
75 - // If the user is redirected to this while visitng our url, intercept it.
76 - \add_filter('wp_redirect', function ($url) {
77 98 // Check for extendify-launch-success as other plugins will not override
78 99 // this as they intercept the request.
79 100 // phpcs:ignore WordPress.Security.NonceVerification
80 101 if (isset($_GET['extendify-launch-success'])) {
@@ -80,17 +101,20 @@
80 101 if (isset($_GET['extendify-launch-success'])) {
81 102 return \admin_url() . $this->getRoute();
82 103 }
83 104
105 + // Special treatment for Yoast to disable their redirect when installing.
106 + if ($url === \admin_url() . 'admin.php?page=wpseo_installation_successful_free') {
107 + return \admin_url() . $this->getRoute();
108 + }
109 +
110 + // Special treatment for Germanized for WooCommerce to disable their redirect when installing.
111 + if ($url === \admin_url() . 'admin.php?page=wc-gzd-setup') {
112 + return \admin_url() . $this->getRoute();
113 + }
114 +
84 115 return $url;
85 116 }, 9999);
86 -
87 - // Intercept requests and redirect as needed.
88 - // phpcs:ignore WordPress.Security.NonceVerification
89 - if (isset($_GET['page']) && $_GET['page'] === 'extendify-admin-page') {
90 - header('Location: ' . \admin_url() . $this->getRoute(), true, 302);
91 - exit;
92 - }
93 117 }
94 118
95 119 /**
96 120 * A helper for handling sub menus
@@ -103,9 +127,10 @@
103 127 */
104 128 public function addSubMenu($name, $slug, $callback = '')
105 129 {
106 130 \add_submenu_page(
107 - 'extendify-admin-page',
131 + // Uses a "dummy" page for adding pages without a menu.
132 + (constant('EXTENDIFY_DEVMODE') ? 'extendify-assist' : 'extendify-page'),
108 133 $name,
109 134 $name,
110 135 Config::$requiredCapability,
111 136 $slug,
@@ -115,20 +140,29 @@
115 140
116 141 /**
117 142 * Adds Extendify top menu
118 143 *
144 + * @param string $label - The menu label.
145 + * @param string $slug - The menu slug.
146 + * @param string|callable $callback - The callback to render the page.
119 147 * @return void
120 148 */
121 - public function addAdminMenu()
149 + public function addAdminMenu($label, $slug, $callback)
122 150 {
151 + $menuLabel = sprintf(
152 + '%1$s <span class="extendify-assist-badge-count" data-test="assist-badge-count"></span>',
153 + $label
154 + );
155 +
123 156 \add_menu_page(
124 157 'Extendify',
125 - 'Extendify',
158 + $menuLabel,
126 159 Config::$requiredCapability,
127 - 'extendify-admin-page',
128 - '__return_null',
129 - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
130 - 'data:image/svg+xml;base64,' . base64_encode('<svg width="20" height="20" viewBox="0 0 60 62" fill="black" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M36.0201 0H49.2377C52.9815 0 54.3365 0.391104 55.7061 1.12116C57.0756 1.85412 58.1469 2.92893 58.8795 4.29635C59.612 5.66666 60 7.02248 60 10.7684V23.9935C60 27.7394 59.6091 29.0952 58.8795 30.4655C58.1469 31.8358 57.0727 32.9078 55.7061 33.6407C55.0938 33.9684 54.4831 34.2381 53.661 34.4312V44.9564C53.661 50.7417 53.0573 52.8356 51.9305 54.952C50.7991 57.0683 49.1401 58.7238 47.0294 59.8558C44.9143 60.9878 42.8215 61.5873 37.0395 61.5873H16.626C10.844 61.5873 8.75122 60.9833 6.63608 59.8558C4.52094 58.7238 2.86639 57.0638 1.73504 54.952C0.603687 52.8401 0 50.7417 0 44.9564V24.5358C0 18.7506 0.603687 16.6566 1.73057 14.5403C2.86192 12.424 4.52094 10.764 6.63608 9.63201C8.74675 8.5045 10.844 7.90047 16.626 7.90047H25.3664C25.5303 6.18172 25.8724 5.24393 26.3754 4.29924C27.1079 2.92893 28.1821 1.85412 29.5517 1.12116C30.9183 0.391104 32.2763 0 36.0201 0ZM29.2266 8.41812C29.2266 5.96352 31.2155 3.97368 33.6689 3.97368H51.5859C54.0393 3.97368 56.0282 5.96352 56.0282 8.41812V26.3438C56.0282 28.7984 54.0393 30.7882 51.5859 30.7882H33.6689C31.2155 30.7882 29.2266 28.7984 29.2266 26.3438V8.41812Z" fill="black"/></svg>')
160 + $slug,
161 + $callback,
162 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode, Generic.Files.LineLength.TooLong
163 + 'data:image/svg+xml;base64,' . base64_encode('<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.773,9.373L16.679,10.467L10,3.788L3.321,10.467L2.227,9.373L10,1.6L14.129,5.729L14.129,3.665L16.194,3.665L16.194,7.794L17.773,9.373ZM16.194,11.437L16.194,17.6L3.806,17.6L3.806,11.437L10,5.254L16.194,11.437ZM13.297,11.525C13.394,11.437 13.432,11.301 13.387,11.178L13.388,11.176C13.326,11.008 13.251,10.847 13.165,10.692L13.099,10.578C13.006,10.423 12.901,10.276 12.787,10.138C12.704,10.037 12.566,10.002 12.441,10.042L11.881,10.219C11.74,10.263 11.586,10.235 11.462,10.155C11.393,10.111 11.322,10.07 11.249,10.032C11.116,9.964 11.015,9.845 10.984,9.702L10.858,9.127C10.83,8.999 10.731,8.897 10.601,8.876C10.406,8.843 10.206,8.827 10.001,8.827C9.797,8.827 9.596,8.843 9.402,8.877C9.272,8.899 9.173,9 9.145,9.129L9.019,9.703C8.987,9.847 8.885,9.966 8.754,10.033C8.68,10.07 8.61,10.112 8.541,10.156C8.415,10.236 8.263,10.266 8.121,10.221L7.561,10.043C7.437,10.004 7.299,10.037 7.215,10.139C7.101,10.277 6.997,10.424 6.904,10.579L6.837,10.694C6.751,10.849 6.676,11.01 6.614,11.178C6.569,11.301 6.607,11.437 6.705,11.525L7.141,11.923C7.251,12.022 7.302,12.168 7.294,12.317C7.293,12.358 7.292,12.399 7.292,12.441C7.292,12.483 7.293,12.524 7.294,12.565C7.302,12.712 7.249,12.859 7.141,12.959L6.705,13.355C6.607,13.443 6.569,13.58 6.614,13.703C6.676,13.871 6.751,14.031 6.837,14.187L6.904,14.301C6.997,14.456 7.101,14.603 7.215,14.741C7.299,14.841 7.437,14.877 7.561,14.837L8.12,14.658C8.261,14.613 8.415,14.642 8.539,14.723C8.608,14.767 8.679,14.808 8.752,14.846C8.884,14.913 8.987,15.032 9.018,15.176L9.143,15.75C9.171,15.879 9.27,15.98 9.4,16.002C9.595,16.034 9.795,16.051 10,16.051C10.205,16.051 10.405,16.034 10.6,16.002C10.729,15.98 10.828,15.879 10.857,15.75L10.982,15.176C11.015,15.032 11.116,14.913 11.247,14.846C11.321,14.809 11.391,14.767 11.461,14.723C11.586,14.642 11.739,14.613 11.88,14.658L12.44,14.836C12.564,14.875 12.703,14.841 12.786,14.74C12.9,14.602 13.005,14.455 13.098,14.3L13.164,14.185C13.25,14.03 13.325,13.869 13.387,13.701C13.432,13.578 13.394,13.441 13.297,13.354L12.861,12.957C12.751,12.859 12.7,12.712 12.707,12.564C12.708,12.523 12.71,12.482 12.71,12.439C12.71,12.397 12.708,12.356 12.707,12.315C12.7,12.168 12.752,12.022 12.861,11.922L13.297,11.525ZM10.8,13.238C10.588,13.449 10.301,13.569 10.001,13.569C9.702,13.569 9.415,13.449 9.203,13.238C8.991,13.026 8.872,12.739 8.872,12.439C8.872,12.14 8.991,11.853 9.203,11.641C9.415,11.429 9.702,11.31 10.001,11.31C10.301,11.31 10.588,11.429 10.8,11.641C11.011,11.853 11.13,12.14 11.13,12.439C11.13,12.739 11.011,13.026 10.8,13.238Z" style="fill:#a7aaad;" fill="currentColor" /></svg>'),
164 + PartnerData::$id ? 0 : null
131 165 );
132 166 }
133 167
134 168 /**
@@ -137,23 +171,201 @@
137 171 * @return string
138 172 */
139 173 public function getRoute()
140 174 {
141 - // If dev, redirect to assist always.
142 - if (Config::$environment === 'DEVELOPMENT') {
143 - return 'admin.php?page=extendify-assist';
175 + // This router use to handle more cases but now everyone goes to Assist.
176 + return 'admin.php?page=extendify-assist';
177 + }
178 +
179 + /**
180 + * Routes /wp-admin/?launch-redirect based on Launch and Agent state.
181 + *
182 + * @return void
183 + */
184 + public function handleLaunchRedirect()
185 + {
186 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
187 + if (!isset($_GET['launch-redirect']) || \wp_doing_ajax() || !PartnerData::$id) {
188 + return;
144 189 }
145 190
146 - // If Launch/Assist isn't enabled, show the Library page.
147 - if (!Config::$showOnboarding) {
148 - return 'admin.php?page=extendify-welcome';
191 + // If launch hasn't yet completed, they go to launch/auto-launch
192 + if (!Config::$launchCompleted) {
193 + $useAgentOnboarding = PartnerData::setting('useAgentOnboarding') ||
194 + Config::preview('agent-onboarding') ||
195 + constant('EXTENDIFY_DEVMODE');
196 + $page = $useAgentOnboarding ? 'extendify-auto-launch' : 'extendify-launch';
197 + $query_params = $this->presentLaunchParams(['page' => $page]);
198 + \wp_safe_redirect(\add_query_arg($query_params, \admin_url('admin.php')));
199 + exit;
149 200 }
150 201
151 - // If they've yet to complete launch, send them back to Launch.
152 - if (!Config::$launchCompleted) {
153 - return 'post-new.php?extendify=onboarding';
202 + // If they have the Agent onboarding enabled, redirect to home.
203 + if (PartnerData::setting('showAIAgents')) {
204 + \wp_safe_redirect(\add_query_arg(
205 + ['extendify-open-agent' => '1'],
206 + \home_url('/')
207 + ));
208 + exit;
154 209 }
155 210
156 - // If they made it this far, they can go to Assist.
157 - return 'admin.php?page=extendify-assist';
211 + // Otherwise, they go to Assist.
212 + \wp_safe_redirect(\admin_url() . $this->getRoute());
213 + exit;
214 + }
215 +
216 + /**
217 + * Redirect once to Launch, only once (at least once) when
218 + * the email matches the entry in WP Admin > Settings > General.
219 + * Requests carrying a build-id keep redirecting until onboarding
220 + * completes.
221 + *
222 + * @return void
223 + */
224 + public function redirectOnce()
225 + {
226 + if (wp_doing_ajax()) {
227 + return;
228 + }
229 +
230 + if (defined('EXTENDIFY_IS_THEME_EXTENDABLE') && !EXTENDIFY_IS_THEME_EXTENDABLE) {
231 + return;
232 + }
233 +
234 + // Partner preview links must work on every admin visit, not just the first.
235 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
236 + $buildIdRedirect = !empty(sanitize_text_field(wp_unslash($_GET['build-id'] ?? '')))
237 + && !Config::$launchCompleted;
238 +
239 + if (!$buildIdRedirect && \get_option('extendify_launch_loaded', false)) {
240 + return;
241 + }
242 +
243 + if (!Config::$showLaunch) {
244 + return;
245 + }
246 +
247 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
248 + if (isset($_GET['page']) && $_GET['page'] === 'extendify-auto-launch') {
249 + return;
250 + }
251 +
252 + // Only redirect if we aren't already on the page.
253 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
254 + if (isset($_GET['page']) && $_GET['page'] === 'extendify-launch') {
255 + $agentOnboarding = PartnerData::setting('useAgentOnboarding') ||
256 + Config::preview('agent-onboarding') ||
257 + constant('EXTENDIFY_DEVMODE');
258 + if ($agentOnboarding) {
259 + // If they landed on launch but have the Agent onboarding enabled, redirect to
260 + // auto-launch — carrying their deep-link params, which a bare page-only redirect drops.
261 + $query_params = $this->presentLaunchParams(['page' => 'extendify-auto-launch']);
262 + \wp_safe_redirect(\add_query_arg($query_params, \admin_url('admin.php')));
263 + exit;
264 + }
265 + return;
266 + }
267 +
268 + if ($buildIdRedirect) {
269 + \update_option('permalink_structure', '/%postname%/');
270 + \update_option('extendify_needs_rewrite_flush', true);
271 +
272 + // Only AutoLaunch consumes build-id.
273 + $query_params = $this->presentLaunchParams(['page' => 'extendify-auto-launch']);
274 + \wp_safe_redirect(\add_query_arg($query_params, \admin_url('admin.php')));
275 + exit;
276 + }
277 +
278 + $user = \wp_get_current_user();
279 + if (
280 + $user
281 + // Check the main admin email, and they have an admin role.
282 + && \get_option('admin_email') === $user->user_email
283 + && in_array('administrator', $user->roles, true)
284 + ) {
285 + // Only redirect 3 times.
286 + $currentCount = \get_option('extendify_attempted_redirect_count', 0);
287 + if ($currentCount >= 3) {
288 + return;
289 + }
290 +
291 + \update_option('extendify_attempted_redirect_count', ($currentCount + 1));
292 + \update_option('extendify_attempted_redirect', gmdate('Y-m-d H:i:s'));
293 +
294 + // Update permalink structure to postname when auto-redirecting to Launch
295 + \update_option('permalink_structure', '/%postname%/');
296 + \update_option('extendify_needs_rewrite_flush', true);
297 +
298 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
299 + $autoLaunch = isset($_GET['auto-launch']) && filter_var($_GET['auto-launch'], FILTER_VALIDATE_BOOLEAN);
300 + $query_params = $this->presentLaunchParams([
301 + 'page' => $autoLaunch ? 'extendify-auto-launch' : 'extendify-launch',
302 + ]);
303 +
304 + $redirect_url = \add_query_arg($query_params, \admin_url('admin.php'));
305 + \wp_safe_redirect($redirect_url);
306 + exit;
307 + }
308 + }
309 +
310 + /**
311 + * Merges the allowlisted launch deep-link params present in the request onto
312 + * $base, so a redirect into Launch/AutoLaunch carries the partner's params
313 + * through instead of dropping them.
314 + *
315 + * @param array $base Seed query args (typically the destination `page`).
316 + * @return array
317 + */
318 + private function presentLaunchParams(array $base)
319 + {
320 + $allowed = [
321 + 'objective',
322 + 'title',
323 + 'description',
324 + 'structure',
325 + 'tone',
326 + 'skip',
327 + 'type',
328 + 'category',
329 + 'products',
330 + 'appointments',
331 + 'events',
332 + 'donations',
333 + 'multilingual',
334 + 'contact',
335 + 'address',
336 + 'blog',
337 + 'landing-page',
338 + 'cta-link',
339 + 'build-id',
340 + 'go',
341 + ];
342 +
343 + foreach ($allowed as $param) {
344 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
345 + $value = sanitize_text_field(wp_unslash($_GET[$param] ?? ''));
346 + if (!empty($value)) {
347 + $base[$param] = $value;
348 + }
349 + }
350 +
351 + return $base;
352 + }
353 +
354 + /**
355 + * Flushes WordPress rewrite rules if previously scheduled by `redirectOnce()`.
356 + *
357 + * This is triggered based on a flag (`extendify_needs_rewrite_flush`) that is set
358 + * when the permalink structure is modified. Flushing rewrite rules in the same
359 + * request where the permalink structure is updated can be ineffective due to
360 + * internal WordPress caching, so the flush is deferred to a subsequent request.
361 + *
362 + * @return void
363 + */
364 + public function maybeForceFlush()
365 + {
366 + if (\get_option('extendify_needs_rewrite_flush', false)) {
367 + \flush_rewrite_rules(true);
368 + \delete_option('extendify_needs_rewrite_flush');
369 + }
158 370 }
159 371 }