PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
fluent-support / app / Services / FluentAuthBridge.php

FluentAuthBridge.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.5, at app/Services/FluentAuthBridge.php

268 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Services;
4
5 use FluentSupport\App\Hooks\Handlers\AuthHandler;
6 use FluentSupport\App\Http\Controllers\AuthController;
7
8 /**
9 * Hands the portal's login, signup and reset forms to FluentAuth when it is installed.
10 *
11 * From FluentAuth 3.0 a plugin can adopt its forms through LoginBridge even when the
12 * site's own FluentAuth forms setting is off. Without that, our wp_login_form() picks up
13 * FluentAuth's magic login, social and passkey buttons, and our REST login cannot show
14 * FluentAuth's second factor. Older FluentAuth versions are used only when that setting
15 * is on, as before.
16 */
17 class FluentAuthBridge
18 {
19 const HOST = 'fluent-support';
20
21 /**
22 * Set while one of our shortcodes renders a FluentAuth form.
23 *
24 * @var bool
25 */
26 private static $rendering = false;
27
28 /**
29 * The submitted signup data, kept from validation until the user is created.
30 *
31 * @var array|null
32 */
33 private static $signupData = null;
34
35 public static function register()
36 {
37 if (!defined('FLUENT_AUTH_VERSION')) {
38 return;
39 }
40
41 // On init, so an opt-out filter added by the theme is seen.
42 add_action('init', [__CLASS__, 'registerHost'], 1);
43
44 add_filter('fluent_auth/registration_form_fields', [__CLASS__, 'addSignupFields']);
45 add_action('fluent_auth/after_signup_validation', [__CLASS__, 'captureSignupData']);
46 add_action('fluent_auth/after_creating_user', [__CLASS__, 'saveSignupMeta']);
47 add_filter('fluent_auth/signup_default_role', [__CLASS__, 'signupRole']);
48 add_filter('fluent_auth/login_redirect_url', [__CLASS__, 'loginRedirectUrl']);
49 }
50
51 /**
52 * Tells FluentAuth which posts come from our forms, so its endpoints answer them.
53 * Only the signed marker FluentAuth prints in forms it renders for us counts.
54 */
55 public static function registerHost()
56 {
57 if (self::shouldAdopt()) {
58 \FluentAuth\App\Services\LoginBridge::register(self::HOST, [__CLASS__, 'hasSignedMarker']);
59 }
60 }
61
62 /**
63 * Whether FluentAuth should render our forms even with its forms setting off.
64 *
65 * @return bool
66 */
67 public static function shouldAdopt()
68 {
69 if (!self::hasBridge()) {
70 return false;
71 }
72
73 // A site that turned on our portal 2FA keeps it: adopting FluentAuth's forms would
74 // switch it off without a word, and FluentAuth's 2FA may not cover customer roles.
75 // Raw option: this runs on init priority 1, before translated defaults should be cached.
76 $adopt = \FluentSupport\Framework\Support\Arr::get(Helper::getOption('global_business_settings', []), 'enable_two_fa') !== 'yes';
77
78 /*
79 * Filter whether the portal forms use FluentAuth when it is installed
80 *
81 * @since v2.4.1
82 *
83 * @param bool $adopt false by default while Fluent Support's own 2FA is on
84 */
85 return (bool) apply_filters('fluent_support/use_fluent_auth_forms', $adopt);
86 }
87
88 /**
89 * Renders a FluentAuth form as ours: adopts the screen when the bridge exists and
90 * adds our signup fields while the form is built.
91 *
92 * @param callable $render returns the form html
93 * @param string $redirectTo
94 * @return string
95 */
96 public static function render(callable $render, $redirectTo = '')
97 {
98 if (self::hasBridge()) {
99 \FluentAuth\App\Services\LoginBridge::adopt([
100 'host' => self::HOST,
101 'redirect_to' => $redirectTo
102 ]);
103 }
104
105 self::$rendering = true;
106
107 try {
108 return $render();
109 } finally {
110 self::$rendering = false;
111 }
112 }
113
114 public static function addSignupFields($fields)
115 {
116 if (!self::isOurRequest()) {
117 return $fields;
118 }
119
120 $customFields = AuthHandler::allCustomFields();
121 $supportsSelect = self::supportsSelectFields();
122
123 foreach (Helper::getCustomRegistrationFieldKeys() as $key) {
124 if (!isset($customFields[$key])) {
125 continue;
126 }
127
128 $field = $customFields[$key];
129
130 // FluentAuth versions without select support leave the field out of the form.
131 if (($field['type'] ?? '') === 'select' && !$supportsSelect) {
132 $field['type'] = 'text';
133 unset($field['options']);
134 }
135
136 $fields[$key] = $field;
137 }
138
139 return $fields;
140 }
141
142 public static function captureSignupData($formData)
143 {
144 // Older FluentAuth signup forms carry no marker, so their posts cannot be told apart.
145 if (self::isOurRequest() || !self::hasBridge()) {
146 self::$signupData = (array)$formData;
147 }
148 }
149
150 public static function saveSignupMeta($userId)
151 {
152 if (self::$signupData === null || !$userId) {
153 return;
154 }
155
156 (new AuthController())->addUserMetaData($userId, self::$signupData);
157 self::$signupData = null;
158 }
159
160 public static function signupRole($role)
161 {
162 if (!self::isOurRequest()) {
163 return $role;
164 }
165
166 /*
167 * Filter user assignable role after signup
168 *
169 * @since v1.0.0
170 * @param string $setRole WordPress user role key
171 */
172 return apply_filters('fluent_support/user_role', 'subscriber');
173 }
174
175 /**
176 * FluentAuth's signup posts its redirect as `__redirect_to` but reads `redirect_to`,
177 * so without this a portal signup would land in wp-admin.
178 *
179 * @param string $redirectUrl
180 * @return string
181 */
182 public static function loginRedirectUrl($redirectUrl)
183 {
184 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- FluentAuth verified the request before redirecting.
185 if (!self::isOurRequest() || empty($_REQUEST['__redirect_to'])) {
186 return $redirectUrl;
187 }
188
189 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
190 $target = sanitize_url(wp_unslash($_REQUEST['__redirect_to']));
191
192 return wp_validate_redirect($target, Helper::getPortalBaseUrl());
193 }
194
195 /**
196 * Whether this request is one of our forms: rendering it, or posting it back.
197 *
198 * @return bool
199 */
200 private static function isOurRequest()
201 {
202 return self::$rendering || self::hasSignedMarker();
203 }
204
205 /**
206 * Whether the request carries the signed marker FluentAuth prints in every form it
207 * renders for us. A visitor cannot forge it, unlike a plain request field.
208 *
209 * The marker is a nonce for the visitor who loaded the form. A signup logs the new
210 * user in before FluentAuth applies its redirect filter, and the nonce no longer
211 * verifies for that user, so the first answer for a signature is kept.
212 *
213 * @return bool
214 */
215 public static function hasSignedMarker()
216 {
217 static $verified = [];
218
219 if (!self::hasBridge()) {
220 return false;
221 }
222
223 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- the marker is itself a nonce, verified below.
224 $markerField = \FluentAuth\App\Services\LoginBridge::MARKER_FIELD;
225
226 if (empty($_REQUEST[$markerField]) || empty($_REQUEST[$markerField . '_sig'])) {
227 return false;
228 }
229
230 if (sanitize_key(wp_unslash($_REQUEST[$markerField])) !== self::HOST) {
231 return false;
232 }
233
234 $signature = sanitize_text_field(wp_unslash($_REQUEST[$markerField . '_sig']));
235 // phpcs:enable WordPress.Security.NonceVerification.Recommended
236
237 if (!isset($verified[$signature])) {
238 $verified[$signature] = (bool)wp_verify_nonce($signature, \FluentAuth\App\Services\LoginBridge::MARKER_ACTION . self::HOST);
239 }
240
241 return $verified[$signature];
242 }
243
244 /**
245 * LoginBridge ships from FluentAuth 3.0.0. The version is checked before the class,
246 * because FluentAuth 2.x's autoloader requires a missing file instead of returning
247 * false, so class_exists() alone would be a fatal error there.
248 *
249 * @return bool
250 */
251 private static function hasBridge()
252 {
253 return defined('FLUENT_AUTH_VERSION')
254 && version_compare(FLUENT_AUTH_VERSION, '3.0.0', '>=')
255 && class_exists('\FluentAuth\App\Services\LoginBridge');
256 }
257
258 /**
259 * @return bool
260 */
261 private static function supportsSelectFields()
262 {
263 $constant = '\FluentAuth\App\Hooks\Handlers\CustomAuthHandler::SIGNUP_FIELD_TYPES';
264
265 return defined($constant) && in_array('select', (array)constant($constant), true);
266 }
267 }
268