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
extendify / app / AdminPageRouter.php

AdminPageRouter.php in Extendify 3.2.1, at app/AdminPageRouter.php

372 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Simple router to handle admin page loading
5 */
6
7 namespace Extendify;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\Assist\AdminPage as AssistAdminPage;
12 use Extendify\Launch\AdminPage as LaunchAdminPage;
13 use Extendify\AutoLaunch\AdminPage as AutoLaunchAdminPage;
14
15 /**
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.
19 */
20
21 class AdminPageRouter
22 {
23 /**
24 * Adds various actions to set up the page
25 *
26 * @return void
27 */
28 public function __construct()
29 {
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);
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
50 // When Launch is finished, fire this to set the correct permalinks.
51 // phpcs:ignore WordPress.Security.NonceVerification
52 if (isset($_GET['extendify-launch-success'])) {
53 \add_action('admin_init', function () {
54 \flush_rewrite_rules();
55 });
56 }
57
58 \add_action('admin_menu', function () {
59 // If no partner, we don't show any menu.
60 if (!PartnerData::$id) {
61 return;
62 }
63
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 );
71
72 if (!Config::$showLaunch) {
73 return;
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 );
90 });
91
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 }
97
98 // Check for extendify-launch-success as other plugins will not override
99 // this as they intercept the request.
100 // phpcs:ignore WordPress.Security.NonceVerification
101 if (isset($_GET['extendify-launch-success'])) {
102 return \admin_url() . $this->getRoute();
103 }
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
115 return $url;
116 }, 9999);
117 }
118
119 /**
120 * A helper for handling sub menus
121 *
122 * @param string $name - The menu name.
123 * @param string $slug - The menu slug.
124 * @param callable $callback - The callback to render the page.
125 *
126 * @return void
127 */
128 public function addSubMenu($name, $slug, $callback = '')
129 {
130 \add_submenu_page(
131 // Uses a "dummy" page for adding pages without a menu.
132 (constant('EXTENDIFY_DEVMODE') ? 'extendify-assist' : 'extendify-page'),
133 $name,
134 $name,
135 Config::$requiredCapability,
136 $slug,
137 $callback
138 );
139 }
140
141 /**
142 * Adds Extendify top menu
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.
147 * @return void
148 */
149 public function addAdminMenu($label, $slug, $callback)
150 {
151 $menuLabel = sprintf(
152 '%1$s <span class="extendify-assist-badge-count" data-test="assist-badge-count"></span>',
153 $label
154 );
155
156 \add_menu_page(
157 'Extendify',
158 $menuLabel,
159 Config::$requiredCapability,
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
165 );
166 }
167
168 /**
169 * Routes pages accordingly
170 *
171 * @return string
172 */
173 public function getRoute()
174 {
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;
189 }
190
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;
200 }
201
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;
209 }
210
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 }
370 }
371 }
372