PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / app / Hooks / Handlers / PortalHandler.php

PortalHandler.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.11.0, at app/Hooks/Handlers/PortalHandler.php

1,377 lines 73.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Hooks\Handlers;
4
5 use FluentCommunity\App\App;
6 use FluentCommunity\App\Functions\Utility;
7 use FluentCommunity\App\Models\Space;
8 use FluentCommunity\App\Models\Feed;
9 use FluentCommunity\App\Models\BaseSpace;
10 use FluentCommunity\App\Models\SpaceGroup;
11 use FluentCommunity\App\Models\User;
12 use FluentCommunity\App\Models\XProfile;
13 use FluentCommunity\App\Services\CustomSanitizer;
14 use FluentCommunity\App\Services\Helper;
15 use FluentCommunity\App\Services\ProfileHelper;
16 use FluentCommunity\App\Services\TransStrings;
17 use FluentCommunity\App\Vite;
18 use FluentCommunity\App\Models\NotificationSubscriber;
19 use FluentCommunity\Framework\Support\Arr;
20 use FluentCommunity\App\Services\FeedsHelper;
21 use FluentCommunity\Modules\Auth\AuthHelper;
22 use FluentCommunity\Modules\Course\Model\CourseLesson;
23
24 class PortalHandler
25 {
26 /**
27 * Route + model resolved while building the head meta, handed to the view so
28 * `fluent_community/portal_content_pre_render` listeners can render a
29 * server-side first paint without querying for the same record again.
30 *
31 * @var array{route: string, model: mixed}|array
32 */
33 protected $preRenderContext = [];
34
35 protected $currentPath = '';
36
37 protected $slug = null;
38
39 public function register()
40 {
41 /**
42 * Register the portal route
43 */
44 add_action('init', function () {
45 $this->slug = Helper::getPortalSlug();
46 if ($this->slug !== '') {
47 add_rewrite_rule('^' . $this->slug . '/?$', 'index.php?fcom_route=portal_home', 'top'); // For /hooks
48 add_rewrite_rule('^' . $this->slug . '/(.+)/?', 'index.php?fcom_route=$matches[1]', 'top');
49 }
50 });
51
52 add_filter('query_vars', function ($vars) {
53 $vars[] = 'fcom_route';
54 return $vars;
55 });
56
57 add_action('template_redirect', function () {
58 $hook_path = get_query_var('fcom_route');
59
60 if ($hook_path) {
61 $this->currentPath = $hook_path;
62 $this->renderFullApp();
63 }
64
65 if ($this->slug === '') {
66 global $wp;
67 $currentRequest = $wp->request;
68 if ($hookPath = Helper::getPortalRequestPath($currentRequest)) {
69 $this->currentPath = $hookPath;
70 $this->renderFullApp();
71 }
72 }
73 }, 1);
74
75 add_action('fluent_community/portal_html', function () {
76 App::make('view')->render('portal.portal', [
77 'preRenderContext' => $this->preRenderContext,
78 ]);
79 });
80
81 add_action('fluent_community/portal_header', function ($context) {
82 $this->getPortalHeader(true, $context);
83 });
84
85 add_action('fluent_community/portal_sidebar', function ($context) {
86 $this->getPortalSidebar(true, $context);
87 });
88
89 add_action('fluent_community/enqueue_global_assets', function ($useDefaultTheme = true) {
90 $isRtl = Helper::isRtl();
91
92 wp_enqueue_style('fluent_community_global', Vite::getStaticSrcUrl('global.css', $isRtl), [], FLUENT_COMMUNITY_PLUGIN_VERSION, 'screen');
93
94 if ($useDefaultTheme) {
95 wp_enqueue_style('fluent_community_default_theme', Vite::getStaticSrcUrl('theme-default.css', $isRtl), [], FLUENT_COMMUNITY_PLUGIN_VERSION, 'screen');
96 }
97 $css = Utility::getColorCssVariables();
98 wp_add_inline_style('fluent_community_global', $css);
99 wp_enqueue_script('portal_general', Vite::getStaticSrcUrl('portal_general.js'), [], FLUENT_COMMUNITY_PLUGIN_VERSION, [
100 'in_footer' => true
101 ]);
102 wp_localize_script('portal_general', 'fcom_portal_general', $this->getGlobalScriptVars());
103
104 if (Utility::isCustomizationEnabled('enable_sidebar_toggle')) {
105 add_action('fluent_community/before_portal_dom', [$this, 'renderSidebarCollapseFouc'], 1);
106 }
107 });
108
109 add_action('admin_bar_menu', function ($wp_admin_bar) {
110 if (!\FluentCommunity\App\Services\Helper::canAccessPortal()) {
111 return;
112 }
113
114 // Add custom link within site name dropdown
115 $wp_admin_bar->add_node(array(
116 'id' => 'fluent-community-link',
117 'title' => __('Visit Community Portal', 'fluent-community'),
118 'href' => \FluentCommunity\App\Services\Helper::baseUrl('/'),
119 'parent' => 'site-name'
120 ));
121 }, 99);
122
123 add_action('fluent_community/top_menu_right_items', [$this, 'renderTopMenuRightItems']);
124
125 add_action('fluent_community/after_portal_sidebar', function () {
126
127 if (did_action('fluent_community/after_header_right_menu_items')) {
128 echo '<div role="region" aria-label="Portal Settings" class="fcom_side_footer">';
129 if (Helper::isSiteAdmin()) {
130 ?>
131 <div style="display: flex;justify-content: space-between;" class="fcom_admin_menu">
132 <?php if (!defined('FLUENT_COMMUNITY_PRO')): ?>
133 <a target="_blank" rel="noopener" class="fcom_inline_icon_link_item"
134 href="<?php echo esc_url(Utility::getProUpgradeUrl('sidebar_cta')) ?>">
135 <span class="el-icon">
136 <svg width="126" height="125" viewBox="0 0 126 125" fill="none"><rect x="0.22139"
137 width="125"
138 height="125"
139 rx="17.8571"
140 fill="#4A5FE0"/>
141 <path
142 d="M47.9424 75.1832L61.6888 67.2467L69.6253 80.9931C62.0334 85.3763 52.3256 82.7751 47.9424 75.1832Z"
143 fill="white"/>
144 <path fill-rule="evenodd" clip-rule="evenodd"
145 d="M75.4348 59.3101L61.6884 67.2466L69.6249 80.993L83.3713 73.0565L75.4348 59.3101ZM89.1821 51.3734L75.4356 59.3099L83.3721 73.0564L97.1186 65.1199L89.1821 51.3734Z"
146 fill="white"/>
147 <path
148 d="M89.182 51.3736C92.978 49.182 97.8319 50.4826 100.023 54.2786L103.992 61.1518L97.1185 65.12L89.182 51.3736Z"
149 fill="white"/>
150 <path
151 d="M64.593 56.4052L50.8466 64.3417L42.9101 50.5953L56.6565 42.6588L64.593 56.4052Z"
152 fill="white"/>
153 <path
154 d="M78.3397 48.4683L64.5933 56.4048L56.6568 42.6584C64.2487 38.2752 73.9565 40.8764 78.3397 48.4683Z"
155 fill="white"/>
156 <path
157 d="M50.847 64.3418L37.1006 72.2783L29.1641 58.5318L42.9105 50.5953L50.847 64.3418Z"
158 fill="white"/>
159 <path
160 d="M37.1011 72.2783C33.3051 74.4699 28.4512 73.1693 26.2596 69.3733L22.2913 62.5001L29.1646 58.5319L37.1011 72.2783Z"
161 fill="white"/>
162 </svg>
163 </span>
164 <span><?php esc_html_e('Upgrade', 'fluent-community'); ?></span>
165 </a>
166 <?php else: ?>
167 <a aria-label="<?php echo esc_attr__('Go to WordPress admin', 'fluent-community'); ?>" class="fcom_inline_icon_link_item fcom_wp_admin_link"
168 href="<?php echo esc_url(admin_url()); ?>">
169 <span class="el-icon">
170 <svg viewBox="0 0 122.52 122.523"><g fill="currentColor">
171 <path
172 d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z"/>
173 <path
174 d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z"/>
175 <path
176 d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.141-.225-.269-.464-.374-.724z"/>
177 <path
178 d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z"/>
179 <path
180 d="m61.262 0c-33.779 0-61.262 27.481-61.262 61.26 0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263-.001-33.779-27.487-61.26-61.265-61.26zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z"/></g>
181 </svg>
182 </span>
183 </a>
184 <?php endif; ?>
185 <?php // The label text is hidden while the sidebar is collapsed, so the link carries its own name. ?>
186 <a class="fcom_inline_icon_link_item"
187 aria-label="<?php echo esc_attr__('Settings', 'fluent-community'); ?>"
188 href="<?php echo esc_url(Helper::baseUrl('admin/settings')); ?>">
189 <span class="el-icon">
190 <svg viewBox="0 0 1024 1024" data-v-d2e47025="">
191 <path fill="currentColor"
192 d="M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357.12 357.12 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a351.616 351.616 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357.12 357.12 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294.113 294.113 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293.12 293.12 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294.113 294.113 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288.282 288.282 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293.12 293.12 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a287.616 287.616 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256"></path>
193 </svg>
194 </span>
195 <span><?php esc_html_e('Settings', 'fluent-community'); ?></span>
196 </a>
197 </div>
198 <?php
199 } else if (Utility::isCustomizationEnabled('show_powered_by')) {
200 ?>
201 <a target="_blank" rel="noopener" style="font-size: 13px; cursor: pointer;"
202 class="fcom_inline_icon_link_item" href="<?php echo esc_url(Utility::getProductUrl(true)) ?>">
203 <?php echo esc_html__('Powered by FluentCommunity', 'fluent-community'); ?>
204 </a>
205 <?php
206 }
207 echo '</div>';
208 return;
209 }
210
211 if (Helper::isSiteAdmin()) {
212 add_action('fluent_community/before_header_menu_items', function () {
213 ?>
214 <li class="top_menu_item fcom_icon_link">
215 <a aria-label="<?php echo esc_attr__('Settings', 'fluent-community'); ?>" href="<?php echo esc_url(Helper::baseUrl('admin/settings')); ?>">
216 <span class="el-icon">
217 <svg viewBox="0 0 1024 1024" data-v-d2e47025="">
218 <path fill="currentColor"
219 d="M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357.12 357.12 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a351.616 351.616 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357.12 357.12 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294.113 294.113 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293.12 293.12 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294.113 294.113 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288.282 288.282 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293.12 293.12 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a287.616 287.616 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256"></path>
220 </svg>
221 </a>
222 </li>
223 <?php
224 });
225 }
226
227 // echo '<div class="fcom_side_footer">';
228 // $this->renderTopMenuRightItems();
229 // echo '</div>';
230 });
231
232 add_action('fluent_community/portal_action_reactivate_account', function ($data) {
233 $xprofile = Helper::getCurrentProfile();
234
235 if ($xprofile && $xprofile->status == '' && Utility::getPrivacySetting('can_deactive_account') === 'yes') {
236
237 $nonce = Arr::get($data, '__nonce', '');
238 if (!wp_verify_nonce($nonce, 'fcom_reactivate_account')) {
239 $this->viewErrorPage(__('Invalid request', 'fluent-community'), __('Security check failed. Please try again.', 'fluent-community'));
240 }
241
242 // we need to reactivate the account
243 $xprofile->status = 'active';
244 $xprofile->save();
245 do_action('fluent_community/reactivate_account', $xprofile);
246 delete_user_meta($xprofile->user_id, '_fcom_deactivated_at');
247 }
248
249 // redirect to the portal home
250 wp_safe_redirect(Helper::baseUrl('/'));
251 exit;
252 });
253 }
254
255 public function renderTopMenuRightItems($context = 'headless')
256 {
257 $auth_url = $this->getAuthUrl();
258 $auth = Helper::getCurrentProfile();
259 $has_color_scheme = Helper::hasColorScheme();
260 $profileLinks = $this->getProfileLinks($auth);
261 ?>
262 <ul class="fcom_user_context_menu_items">
263 <?php do_action('fluent_community/before_header_right_menu_items', $auth); ?>
264 <?php if ($has_color_scheme): ?>
265 <li class="top_menu_item fcom_color_mode">
266 <button type="button" aria-label="<?php echo esc_attr__('Toggle dark mode', 'fluent-community'); ?>" class="fcom_color_mode_core fcom_mode_switch fcom_menu_button">
267 <span class="fcom_color_mode_action el-icon">
268 <svg class="show_on_light" width="20" height="20" viewBox="0 0 20 20" fill="none"
269 xmlns="http://www.w3.org/2000/svg">
270 <path
271 d="M17.9163 11.7317C16.9166 12.2654 15.7748 12.5681 14.5623 12.5681C10.6239 12.5681 7.43128 9.37543 7.43128 5.43705C7.43128 4.22456 7.73388 3.08274 8.2677 2.08301C4.72272 2.91382 2.08301 6.09562 2.08301 9.89393C2.08301 14.3246 5.67476 17.9163 10.1054 17.9163C13.9038 17.9163 17.0855 15.2767 17.9163 11.7317Z"
272 stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
273 stroke-linejoin="round"/>
274 </svg>
275 <svg class="show_on_dark" width="20" height="20" viewBox="0 0 20 20" fill="none"
276 xmlns="http://www.w3.org/2000/svg">
277 <g clip-path="url(#clip0_2906_20675)">
278 <path
279 d="M14.1663 10.0007C14.1663 12.3018 12.3009 14.1673 9.99967 14.1673C7.69849 14.1673 5.83301 12.3018 5.83301 10.0007C5.83301 7.69946 7.69849 5.83398 9.99967 5.83398C12.3009 5.83398 14.1663 7.69946 14.1663 10.0007Z"
280 stroke="currentColor" stroke-width="1.5"/>
281 <path
282 d="M9.99984 1.66699V2.91699M9.99984 17.0837V18.3337M15.8922 15.8931L15.0083 15.0092M4.99089 4.99137L4.107 4.10749M18.3332 10.0003H17.0832M2.9165 10.0003H1.6665M15.8926 4.10758L15.0087 4.99147M4.99129 15.0093L4.10741 15.8932"
283 stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
284 </g>
285 <defs>
286 <clipPath id="clip0_2906_20675">
287 <rect width="20" height="20" fill="currentColor"/>
288 </clipPath>
289 </defs>
290 </svg>
291 </span>
292 </button>
293 </li>
294 <?php endif; ?>
295 <li class="top_menu_item fcom_search_holder"></li>
296 <?php if ($auth): ?>
297 <li class="top_menu_item fcom_notification_holder fcom_countable_notification_holder">
298 <?php if ($context != 'headless'): ?>
299 <?php
300 $notificationCount = NotificationSubscriber::unread()
301 ->where('user_id', $auth->user_id)
302 ->count();
303 ?>
304 <a href="<?php echo esc_url(Helper::baseUrl('/notifications')); ?>" type="button"
305 aria-label="<?php echo esc_attr__('Notifications', 'fluent-community'); ?>"
306 class="fcom_menu_button fcom_theme_button">
307 <i class="el-icon">
308 <svg width="20" height="20" viewBox="0 0 20 20" fill="none"
309 xmlns="http://www.w3.org/2000/svg">
310 <path
311 d="M2.10794 11.9954C1.93073 13.1229 2.72301 13.9055 3.69305 14.2955C7.41201 15.7908 12.5873 15.7908 16.3063 14.2955C17.2763 13.9055 18.0686 13.1229 17.8914 11.9954C17.7825 11.3024 17.244 10.7254 16.845 10.162C16.3224 9.41493 16.2705 8.60009 16.2704 7.73317C16.2704 4.38291 13.4629 1.66699 9.99968 1.66699C6.53645 1.66699 3.72895 4.38291 3.72895 7.73317C3.72887 8.60009 3.67694 9.41493 3.15435 10.162C2.75537 10.7254 2.21685 11.3024 2.10794 11.9954Z"
312 stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
313 stroke-linejoin="round"></path>
314 <path
315 d="M7.5 17.5C8.16345 18.0182 9.03956 18.3333 10 18.3333C10.9604 18.3333 11.8366 18.0182 12.5 17.5"
316 stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
317 stroke-linejoin="round"></path>
318 </svg>
319 </i>
320 <?php if ($notificationCount): ?>
321 <sup
322 class="el-badge__content fcomc_unread_badge el-badge__content--danger is-fixed"><?php echo $notificationCount > 20 ? '20+' : esc_html($notificationCount); ?></sup>
323 <?php endif; ?>
324 </a>
325 <?php endif; ?>
326 </li>
327 <?php endif; ?>
328 <?php do_action('fluent_community/before_header_menu_items', $auth, $context); ?>
329 <?php if ($auth): ?>
330 <li class="top_menu_item fcom_menu_item_user">
331 <div class="fcom_user_menu_item">
332 <div class="fcom_profile_extend">
333 <div class="fcom_profile_menu">
334 <div class="user_avatar" role="button" tabindex="0" aria-expanded="false" aria-controls="fcom_profile_sub_menu" aria-label="<?php echo esc_attr__('Account menu', 'fluent-community'); ?>">
335 <img alt="<?php echo esc_attr($auth->display_name); ?>" src="<?php echo esc_url($auth->avatar); ?>"/>
336 <span class="avatar_icon">
337 <svg xmlns="http://www.w3.org/2000/svg" height="8" width="8"
338 viewBox="0 0 1024 1024">
339 <path fill="currentColor"
340 d="M104.704 338.752a64 64 0 0 1 90.496 0l316.8 316.8 316.8-316.8a64 64 0 0 1 90.496 90.496L557.248 791.296a64 64 0 0 1-90.496 0L104.704 429.248a64 64 0 0 1 0-90.496z"></path>
341 </svg>
342 </span>
343 </div>
344 <span class="user_name">
345 <?php echo esc_html($auth->display_name); ?>
346 </span>
347 <ul id="fcom_profile_sub_menu" class="fcom_profile_sub_menu">
348 <li class="fcom_profile_block">
349 <a href="<?php echo esc_url(Helper::baseUrl('/u/' . $auth->username)); ?>">
350 <div class="fcom_profile_avatar">
351 <img src="<?php echo esc_url($auth->avatar); ?>"
352 alt="<?php echo esc_attr($auth->display_name); ?>">
353 </div>
354 <div class="fcom_user_info">
355 <p class="fcom_user_name"><?php echo esc_html($auth->display_name); ?></p>
356 <p class="fcom_user_email"><?php echo esc_html($auth->user->user_email); ?></p>
357 </div>
358 </a>
359 </li>
360 <?php foreach ($profileLinks as $link): ?>
361 <li>
362 <?php Helper::renderLink($link, 'fcom_menu_link', ''); ?>
363 </li>
364 <?php endforeach; ?>
365 </ul>
366 </div>
367 </div>
368 </li>
369 <?php else: ?>
370 <?php
371 if (AuthHelper::isRegistrationEnabled()) {
372 $btnText = __('Login / Signup', 'fluent-community');
373 } else {
374 $btnText = __('Login', 'fluent-community');
375 }
376 ?>
377
378 <li style="margin-right: 5px" class="top_menu_item fcom_login_li">
379 <a class="fcom_login_btn el-button fcom_primary_button" href="<?php echo esc_url($auth_url); ?>">
380 <span><?php echo esc_html($btnText); ?></span>
381 </a>
382 </li>
383 <?php endif; ?>
384 <?php do_action('fluent_community/after_header_right_menu_items', $auth); ?>
385 </ul>
386 <?php
387 }
388
389 public function appVars()
390 {
391 $userModel = User::where('ID', get_current_user_id())->first();
392 $xprofile = null;
393
394 if ($userModel) {
395 $xprofile = XProfile::where('user_id', get_current_user_id())->first();
396 if (!$xprofile) {
397 $xprofile = $userModel->syncXProfile();
398 }
399 }
400
401 $authData = null;
402 $spaceSlugs = [];
403 $spaceGroups = [];
404
405 if ($userModel && $xprofile) {
406 $wpUser = get_user_by('ID', $userModel->ID);
407
408 if ($userModel->isCommunityModerator()) {
409 $userSpaces = Space::orderBy('serial', 'ASC')->get();
410 $spaceGroups = SpaceGroup::orderBy('serial', 'ASC')
411 ->select(['id', 'title', 'slug', 'settings'])
412 ->get();
413 } else {
414 $userSpaces = Space::orderBy('serial', 'ASC')
415 ->whereHas('members', function ($q) {
416 $q->where('user_id', get_current_user_id())
417 ->where('status', 'active');
418 })
419 ->get();
420
421 $userSpaceParentIds = $userSpaces->pluck('parent_id')->filter()->unique()->toArray();
422
423 $spaceGroups = SpaceGroup::orderBy('serial', 'ASC')
424 ->select(['id', 'title', 'slug', 'settings'])
425 ->whereIn('id', $userSpaceParentIds)
426 ->get();
427 }
428
429 BaseSpace::preloadMemberships($userSpaces, $userModel->ID);
430
431 $userSpaces->each(function ($space) use ($userModel) {
432 $space = $space->formatSpaceData($userModel);
433 do_action_ref_array('fluent_community/space', [&$space]);
434 });
435
436 $userSpaces = $userSpaces->keyBy('slug');
437
438 $userModel->cacheAccessSpaces();
439
440 $spaceSlugs = $userSpaces->pluck('slug')->toArray();
441
442 $authData = [
443 'id' => $xprofile->user_id,
444 'user_id' => $xprofile->user_id,
445 'username' => $xprofile->username,
446 'display_name' => $xprofile->display_name,
447 'first_name' => $wpUser->first_name ? $wpUser->first_name : $xprofile->getFirstName(),
448 'avatar' => $xprofile->avatar,
449 'total_points' => $xprofile->total_points,
450 'last_activity' => $xprofile->last_activity,
451 'email' => $userModel->user_email,
452 'spaces' => $userSpaces,
453 'community_roles' => $userModel->getCommunityRoles(),
454 'is_verified' => $xprofile->is_verified,
455 'compilation_score' => $xprofile->getCompletionScore(),
456 'meta' => $xprofile->meta
457 ];
458 } else {
459 $authData = [
460 'id' => 0,
461 'user_id' => 0,
462 'spaces' => (object)[],
463 'community_roles' => [],
464 'compilation_score' => 100
465 ];
466 }
467
468 $settings = Helper::generalSettings();
469
470 $onboardSettings = Helper::getOnboardingSettings();
471
472 global $wp_version;
473
474 $isRtl = Helper::isRtl();
475
476 $isAbsoluteUrl = defined('FLUENT_COMMUNITY_PORTAL_SLUG') && FLUENT_COMMUNITY_PORTAL_SLUG == '';
477
478 $editorFrameUrl = site_url('?fluent_community_block_editor=1');
479
480 if (current_user_can('edit_posts')) {
481 $editorFrameUrl = admin_url('edit.php?fluent_community_block_editor=1');
482 }
483
484 $portalVars = apply_filters('fluent_community/portal_vars', [
485 'portal_notices' => apply_filters('fluent_community/portal_notices', []),
486 'i18n' => TransStrings::getStrings(),
487 'auth' => $authData,
488 'ajaxurl' => admin_url('admin-ajax.php'),
489 'ajax_nonce' => wp_create_nonce('fluent_community_ajax_nonce'),
490 'slug' => 'fluent-community',
491 'rest' => $this->getRestInfo(),
492 'user_id' => $userModel ? $userModel->ID : null,
493 'assets_url' => FLUENT_COMMUNITY_PLUGIN_URL . 'assets/',
494 'permissions' => $userModel ? $userModel->getPermissions() : ['read' => true],
495 'logo' => Arr::get($settings, 'logo'),
496 'site_title' => Arr::get($settings, 'site_title'),
497 'access_level' => Arr::get($settings, 'access.acess_level'),
498 'user_membership_slugs' => $spaceSlugs,
499 'block_editor_assets' => [
500 'scripts' => [
501 'react' => Vite::getStaticSrcUrl('libs/isolated-editor/react.production.min.js'),
502 'react-dom' => Vite::getStaticSrcUrl(
503 'libs/isolated-editor/react-dom.production.min.js'
504 ),
505 'isolated-block-editor' => Vite::getStaticSrcUrl('libs/isolated-editor/isolated-block-editor.min.js')
506 ],
507 'styles' => [
508 $isRtl ? Vite::getStaticSrcUrl('libs/isolated-editor/core.rtl.css') : Vite::getStaticSrcUrl('libs/isolated-editor/core.css'),
509 $isRtl ? Vite::getStaticSrcUrl('libs/isolated-editor/isolated-block-editor.rtl.css') : Vite::getStaticSrcUrl('libs/isolated-editor/isolated-block-editor.css'),
510 includes_url('css/dist/block-editor/content.min.css?ver=' . $wp_version),
511 ]
512 ],
513 'features' => [
514 'disable_global_posts' => Arr::get($settings, 'disable_global_posts', '') == 'yes',
515 'has_survey_poll' => true,
516 'is_onboarding_enabled' => Arr::get($onboardSettings, 'is_onboarding_enabled', 'no') == 'yes',
517 'can_switch_layout' => Utility::getCustomizationSetting('disable_feed_layout') !== 'yes',
518 'mention_mail' => Utility::hasEmailAnnouncementEnabled(),
519 'max_media_per_post' => apply_filters('fluent_community/max_media_per_post', Utility::getCustomizationSetting('max_media_per_post')),
520 'has_post_title' => Utility::postTitlePref(),
521 'has_course' => Helper::isFeatureEnabled('course_module'),
522 'followers_module' => Helper::isFeatureEnabled('followers_module'),
523 'skicky_sidebar' => Utility::isCustomizationEnabled('fixed_sidebar'),
524 'post_layout' => Utility::getCustomizationSetting('rich_post_layout'),
525 'member_list_layout' => Utility::getCustomizationSetting('member_list_layout'),
526 'default_feed_layout' => Utility::getCustomizationSetting('default_feed_layout'),
527 'disable_feed_sort_by' => Utility::getCustomizationSetting('disable_feed_sort_by'),
528 'default_feed_sort_by' => Utility::getCustomizationSetting('default_feed_sort_by'),
529 'video_embeder' => apply_filters('fluent_community/has_video_embeder', true),
530 'has_inline_image_upload' => apply_filters('fluent_community/has_inline_image_upload', 'yes'),
531 'has_topics' => !!Utility::getTopics(),
532 'show_post_modal' => Utility::isCustomizationEnabled('show_post_modal'),
533 'has_analytics' => Utility::hasAnalyticsEnabled(),
534 'can_deactivate_account' => Utility::getPrivacySetting('can_deactive_account') === 'yes',
535 ],
536 'route_classes' => array_filter([
537 'fcom_sticky_header' => Utility::isCustomizationEnabled('fixed_page_header'),
538 'fcom_sticky_sidebar' => Utility::isCustomizationEnabled('fixed_sidebar'),
539 'fcom_has_icon_on_header_menu' => Utility::isCustomizationEnabled('icon_on_header_menu')
540 ]),
541 'urls' => [
542 'site_url' => home_url(),
543 'portal_base' => Helper::baseUrl('/'),
544 'global_search' => Helper::baseUrl(),
545 ],
546 'last_feed_id' => FeedsHelper::getLastFeedId(),
547 'unread_notification_count' => $userModel ? $userModel->getUnreadNotificationCount() : 0,
548 'unread_feed_ids' => $userModel ? $userModel->getUnreadNotificationFeedIds() : [],
549 'date_offset' => time() - current_time('timestamp'),
550 'date_formatter' => Helper::getDateFormatter(true),
551 'time_formatter' => Helper::getTimeFormatter(true),
552 'date_time_formatter' => Helper::getDateFormatter(true) . ' ' . Helper::getTimeFormatter(true),
553 'portal_slug' => Helper::getPortalSlug(true),
554 'socialLinkProviders' => ProfileHelper::socialLinkProviders(true),
555 'space_groups' => $spaceGroups,
556 'mobileMenuItems' => Helper::getMobileMenuItems(),
557 'feed_links' => Helper::getEnabledFeedLinks(),
558 'post_order_by_options' => Helper::getPostOrderOptions('feed'),
559 'comment_order_by_options' => Helper::getCommentOrderOptions('comment'),
560 'user_post_order_by_options' => Helper::getPostOrderOptions('user'),
561 'routing_system' => Helper::getPortalRouteType(),
562 'portal_url' => Helper::baseUrl('/'),
563 'upgrade_url' => Utility::getProUpgradeUrl('upgrade_page'),
564 'dateTime18n' => apply_filters('fluent_community/date_time_i18n', [
565 /* translators: weekday. Please keep the serial and format */
566 'weekdays' => __('Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday', 'fluent-community'),
567 /* translators: Months Please keep the serial and format*/
568 'months' => __('January_February_March_April_May_June_July_August_September_October_November_December', 'fluent-community'),
569 /* translators: weekday short Please keep the serial and format*/
570 'weekdaysShort' => __('Sun_Mon_Tue_Wed_Thu_Fri_Sat', 'fluent-community'),
571 /* translators: Months short Please keep the serial and format*/
572 'monthsShort' => __('Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec', 'fluent-community'),
573 /* translators: weekday min Please keep the serial and format*/
574 'weekdaysMin' => __('Su_Mo_Tu_We_Th_Fr_Sa', 'fluent-community'),
575 'relativeTime' => [
576 /* translators: Relative Date Formats. Please do not alter %s*/
577 'future' => __('in %s', 'fluent-community'),
578 /* translators: Relative Date Formats. Please do not alter %s*/
579 'past' => __('%s ago', 'fluent-community'),
580 /* translators: Relative Date Formats.*/
581 's' => __('a few seconds', 'fluent-community'),
582 /* translators: Relative Date Formats.*/
583 'm' => __('a minute', 'fluent-community'),
584 /* translators: Relative Date Formats. Don't alter %d*/
585 'mm' => __('%d minutes', 'fluent-community'),
586 /* translators: Relative Date Formats*/
587 'h' => __('an hour', 'fluent-community'),
588 /* translators: Relative Date Formats. Don't alter %d*/
589 'hh' => __('%d hours', 'fluent-community'),
590 /* translators: Relative Date Formats*/
591 'd' => __('a day', 'fluent-community'),
592 /* translators: Relative Date Formats. Don't alter %d*/
593 'dd' => __('%d days', 'fluent-community'),
594 /* translators: Relative Date Formats*/
595 'M' => __('a month', 'fluent-community'),
596 /* translators: Relative Date Formats. Don't alter %d*/
597 'MM' => __('%d months', 'fluent-community'),
598 /* translators: Relative Date Formats*/
599 'y' => __('a year', 'fluent-community'),
600 /* translators: Relative Date Formats. Don't alter %d*/
601 'yy' => __('%d years', 'fluent-community')
602 ],
603 'relativeTimeMobile' => [
604 /* translators: Relative Date Formats. Please do not alter %s*/
605 'future' => __('in %s', 'fluent-community'),
606 /* translators: Relative Date Formats. Please do not alter %s*/
607 'past' => __('%s ago', 'fluent-community'),
608 /* translators: Relative Date Formats.*/
609 's' => __('few sec', 'fluent-community'),
610 /* translators: Relative Date Formats.*/
611 'm' => __('1min', 'fluent-community'),
612 /* translators: Relative Date Formats. Don't alter %d*/
613 'mm' => __('%dmin', 'fluent-community'),
614 /* translators: Relative Date Formats*/
615 'h' => __('1h', 'fluent-community'),
616 /* translators: Relative Date Formats. Don't alter %d*/
617 'hh' => __('%dh', 'fluent-community'),
618 /* translators: Relative Date Formats*/
619 'd' => __('1d', 'fluent-community'),
620 /* translators: Relative Date Formats. Don't alter %d*/
621 'dd' => __('%dd', 'fluent-community'),
622 /* translators: Relative Date Formats*/
623 'M' => __('1mo', 'fluent-community'),
624 /* translators: Relative Date Formats. Don't alter %d*/
625 'MM' => __('%dmo', 'fluent-community'),
626 /* translators: Relative Date Formats*/
627 'y' => __('1y', 'fluent-community'),
628 /* translators: Relative Date Formats. Don't alter %d*/
629 'yy' => __('%dy', 'fluent-community')
630 ]
631 ]),
632 'topicsConfig' => Helper::getTopicsConfig(),
633 'moderationConfig' => Helper::getModerationConfig(),
634 'is_absolute_url' => $isAbsoluteUrl,
635 'portal_paths' => $isAbsoluteUrl ? Helper::portalRoutePaths() : [],
636 'suggestedColors' => Utility::getSuggestedColors(),
637 'view_leaderboard_members' => Utility::canViewLeaderboardMembers(),
638 'report_reasons' => Helper::getReportReasons(),
639 'el_i18n' => [
640 'pagination' => [
641 /* translators: %s is replaced by the page number */
642 'currentPage' => \sprintf(__('page %s', 'fluent-community'), '{pager}'),
643 'deprecationWarning' => 'Deprecated usages detected',
644 'goto' => __('Go to', 'fluent-community'),
645 'next' => __('Go to next page', 'fluent-community'),
646 /* translators: %s is replaced by the number of pages */
647 'nextPages' => \sprintf(__('Next %s pages', 'fluent-community'), ' {pager}'),
648 'page' => __('Page', 'fluent-community'),
649 'pageClassifier' => '',
650 'pagesize' => __('/page', 'fluent-community'),
651 'prev' => __('Go to previous page', 'fluent-community'),
652 /* translators: %s is replaced by the number of pages */
653 'prevPages' => \sprintf(__('Previous %s pages', 'fluent-community'), '{pager}'),
654 /* translators: %s is replaced by the total number of items */
655 'total' => \sprintf(__('Total %s', 'fluent-community'), '{total}'),
656 ],
657 'table' => [
658 'clearFilter' => __('All', 'fluent-community'),
659 'confirmFilter' => __('Confirm', 'fluent-community'),
660 'emptyText' => __('No Data', 'fluent-community'),
661 'resetFilter' => __('Reset', 'fluent-community'),
662 'sumText' => __('Sum', 'fluent-community'),
663 ],
664 'image' => [
665 'error' => __('Failed to Load', 'fluent-community'),
666 ],
667 'upload' => [
668 'continue' => __('Continue', 'fluent-community'),
669 'delete' => __('Delete', 'fluent-community'),
670 'deleteTip' => __('press delete to remove', 'fluent-community'),
671 'preview' => __('Preview', 'fluent-community'),
672 ],
673 'select' => [
674 'loading' => __('Loading', 'fluent-community'),
675 'noData' => __('No data', 'fluent-community'),
676 'noMatch' => __('No matching data', 'fluent-community'),
677 'placeholder' => __('Select', 'fluent-community'),
678 ],
679 'datepicker' => [
680 'now' => __('Now', 'fluent-community'),
681 'today' => __('Today', 'fluent-community'),
682 'cancel' => __('Cancel', 'fluent-community'),
683 'clear' => __('Clear', 'fluent-community'),
684 'confirm' => __('OK', 'fluent-community'),
685 'dateTablePrompt' => __('Use the arrow keys and enter to select the day of the month', 'fluent-community'),
686 'monthTablePrompt' => __('Use the arrow keys and enter to select the month', 'fluent-community'),
687 'yearTablePrompt' => __('Use the arrow keys and enter to select the year', 'fluent-community'),
688 'selectedDate' => __('Selected date', 'fluent-community'),
689 'selectDate' => __('Select date', 'fluent-community'),
690 'selectTime' => __('Select time', 'fluent-community'),
691 'startDate' => __('Start Date', 'fluent-community'),
692 'startTime' => __('Start Time', 'fluent-community'),
693 'endDate' => __('End Date', 'fluent-community'),
694 'endTime' => __('End Time', 'fluent-community'),
695 'prevYear' => __('Previous Year', 'fluent-community'),
696 'nextYear' => __('Next Year', 'fluent-community'),
697 'prevMonth' => __('Previous Month', 'fluent-community'),
698 'nextMonth' => __('Next Month', 'fluent-community'),
699 'year' => __('year', 'fluent-community'),
700 'month1' => __('January', 'fluent-community'),
701 'month2' => __('February', 'fluent-community'),
702 'month3' => __('March', 'fluent-community'),
703 'month4' => __('April', 'fluent-community'),
704 'month5' => __('May', 'fluent-community'),
705 'month6' => __('June', 'fluent-community'),
706 'month7' => __('July', 'fluent-community'),
707 'month8' => __('August', 'fluent-community'),
708 'month9' => __('September', 'fluent-community'),
709 'month10' => __('October', 'fluent-community'),
710 'month11' => __('November', 'fluent-community'),
711 'month12' => __('December', 'fluent-community'),
712 'week' => __('week', 'fluent-community'),
713 'weeks' => [
714 'sun' => __('Sun', 'fluent-community'),
715 'mon' => __('Mon', 'fluent-community'),
716 'tue' => __('Tue', 'fluent-community'),
717 'wed' => __('Wed', 'fluent-community'),
718 'thu' => __('Thu', 'fluent-community'),
719 'fri' => __('Fri', 'fluent-community'),
720 'sat' => __('Sat', 'fluent-community'),
721 ],
722 'weeksFull' => [
723 'sun' => __('Sunday', 'fluent-community'),
724 'mon' => __('Monday', 'fluent-community'),
725 'tue' => __('Tuesday', 'fluent-community'),
726 'wed' => __('Wednesday', 'fluent-community'),
727 'thu' => __('Thursday', 'fluent-community'),
728 'fri' => __('Friday', 'fluent-community'),
729 'sat' => __('Saturday', 'fluent-community'),
730 ],
731 'months' => [
732 'jan' => __('Jan', 'fluent-community'),
733 'feb' => __('Feb', 'fluent-community'),
734 'mar' => __('Mar', 'fluent-community'),
735 'apr' => __('Apr', 'fluent-community'),
736 'may' => __('May', 'fluent-community'),
737 'jun' => __('Jun', 'fluent-community'),
738 'jul' => __('Jul', 'fluent-community'),
739 'aug' => __('Aug', 'fluent-community'),
740 'sep' => __('Sep', 'fluent-community'),
741 'oct' => __('Oct', 'fluent-community'),
742 'nov' => __('Nov', 'fluent-community'),
743 'dec' => __('Dec', 'fluent-community'),
744 ],
745 ]
746 ],
747 'course_sections_collapsed' => apply_filters('fluent_community/course_section_collapse_default', 'no'),
748 'course_lesson_fullscreen' => apply_filters('fluent_community/course_lesson_fullscreen_default', 'no'),
749 'default_profile_tab' => apply_filters('fluent_community/default_profile_tab_route', ''),
750 'default_post_space' => apply_filters('fluent_community/create_post_default_space', ''),
751 'wp_lesson_editor_frame' => $editorFrameUrl,
752 'has_fluentcrm' => defined('FLUENTCRM'),
753 'lazy_styles' => [
754 'wp-block-library-css' => includes_url('css/dist/block-library/style.min.css?version=' . $wp_version),
755 'fcom-block-content-styling-css' => FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/content_styling.css?version=' . FLUENT_COMMUNITY_PLUGIN_VERSION
756 ],
757 'debug_mode' => 1
758 ]);
759
760 $portalVars['welcome_banner'] = Helper::getWelcomeBanner($userModel ? 'login' : 'logout');
761
762 if (!$xprofile) {
763 $portalVars['auth_url'] = $this->getAuthUrl();
764 $portalVars['allow_signup'] = !!AuthHelper::isRegistrationEnabled();
765 }
766
767 return $portalVars;
768 }
769
770 public function renderSidebarCollapseFouc()
771 {
772 ?>
773 <script>
774 (function () {
775 var m = document.cookie.match(/(?:^|; )fcom_sidebar_collapsed=([^;]+)/);
776 if (m && m[1] === 'true') {
777 document.documentElement.classList.add('fcom_sidebar_collapsed_desktop');
778 }
779 })();
780 </script>
781 <?php
782 }
783
784 public function getGlobalScriptVars($scope = 'wp')
785 {
786 return apply_filters('fluent_community/general_portal_vars', [
787 'scope' => $scope,
788 'theme' => get_option('template'),
789 'default_color' => 'light',
790 'color_switch_cookie_name' => '',
791 'has_color_scheme' => Helper::hasColorScheme(),
792 'default_theme_mode' => Helper::getDefaultThemeMode(),
793 'collapse_sidebar_groups' => Utility::isCustomizationEnabled('collapse_sidebar_groups'),
794 'hide_header_on_scroll' => Utility::isCustomizationEnabled('hide_header_on_scroll'),
795 'enable_sidebar_toggle' => Utility::isCustomizationEnabled('enable_sidebar_toggle'),
796 'sidebar_toggle_label' => __('Toggle sidebar', 'fluent-community'),
797 ]);
798 }
799
800 protected function getRestInfo()
801 {
802 $app = fluentCommunityApp();
803 $ns = $app->config->get('app.rest_namespace');
804 $v = $app->config->get('app.rest_version');
805
806 return [
807 'base_url' => esc_url_raw(rest_url()),
808 'url' => rest_url($ns . '/' . $v),
809 'nonce' => wp_create_nonce('wp_rest'),
810 'namespace' => $ns,
811 'version' => $v,
812 ];
813 }
814
815 protected function renderFullApp()
816 {
817 do_action('litespeed_control_set_nocache', 'fluentcommunity api request'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
818 // set no cache headers
819 nocache_headers();
820
821 if (isset($_REQUEST['fcom_action'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
822 $action = sanitize_text_field(wp_unslash($_REQUEST['fcom_action'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
823 do_action('fluent_community/portal_action_' . $action, $_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
824 }
825
826 $pathParts = explode('/', $this->currentPath);
827 do_action('fluent_community/rendering_path_ssr_' . $pathParts[0], $pathParts);
828
829 $userId = get_current_user_id();
830
831 if (!$userId && !Helper::isPublicAccessible()) {
832 $adminAuthUrl = Arr::get(Helper::generalSettings(), 'auth_url', '');
833 $authPath = wp_parse_url($adminAuthUrl, PHP_URL_PATH) ?: '';
834 $portalPath = wp_parse_url(Helper::baseUrl(), PHP_URL_PATH) ?: '';
835
836 // Drop admin's URL if it points back into the portal (would infinite loop)
837 $isPortalLoop = $authPath && $portalPath && strpos(trim($authPath, '/'), trim($portalPath, '/')) === 0;
838
839 if ($adminAuthUrl && !$isPortalLoop) {
840 $authUrl = $adminAuthUrl;
841 } else {
842 $adminAuthUrl = '';
843 $parsedUrl = wp_parse_url(home_url(add_query_arg($_REQUEST, $GLOBALS['wp']->request))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
844 $redirectPath = ($parsedUrl['path'] ?? '/') . (!empty($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '');
845 $internalUrl = $this->getAuthUrl();
846 $authUrl = $internalUrl ? add_query_arg(['redirect_to' => $redirectPath], $internalUrl) : wp_login_url($redirectPath);
847 }
848
849 do_action('fluent_community/portal/not_logged_in', $authUrl);
850 // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- intentional external admin auth redirect, followed by exit()
851 $adminAuthUrl ? wp_redirect($authUrl) : wp_safe_redirect($authUrl);
852 exit();
853 }
854
855 do_action('fluent_community/portal/viewed');
856 $xprofile = Helper::getCurrentProfile();
857
858 if ($xprofile && $xprofile->status != 'active') {
859
860 if ($xprofile->status == 'pending') {
861 $this->viewErrorPage(__('Your request is pending', 'fluent-community'), __('An admin needs to approve your join request. Please contact an admin to get approval', 'fluent-community'));
862 } else if ($xprofile->status == '') { // it's a self deactiavted account
863 $this->viewErrorPage(__('Your account is deactivated', 'fluent-community'), __('You have deactivated your community profile.', 'fluent-community'), true, [
864 'btn_url' => Helper::baseUrl('?fcom_action=reactivate_account&__nonce=' . wp_create_nonce('fcom_reactivate_account')),
865 'btn_text' => __('Activate my account', 'fluent-community')
866 ]);
867 } else {
868 $this->viewErrorPage(__('Access denied', 'fluent-community'), __('Sorry, you cannot access this community', 'fluent-community'));
869 }
870 }
871
872 // Gate by role before render: appVars() creates the profile lazily, so checking it
873 // afterward let an unauthorized user see the portal once (and get a profile) before
874 // being denied. Pass false to authorize by role without requiring a profile yet.
875 if (!Helper::canAccessPortal($userId, false)) {
876 $generalSettings = Helper::generalSettings();
877 $this->viewErrorPage(__('Access Denied', 'fluent-community'), Arr::get($generalSettings, 'restricted_role_content'));
878 }
879
880 if ($xprofile) {
881 if ($xprofile->user) {
882 $xprofile->user->cacheAccessSpaces();
883 }
884
885 /*
886 * Just in case if username is not sanitized
887 */
888 if ($xprofile->username != CustomSanitizer::sanitizeUserName($xprofile->username)) {
889 $xprofile->username = ProfileHelper::generateUserName($xprofile->user_id);
890 $xprofile->save();
891 }
892
893 do_action('fluent_community/portal_render_for_user', $xprofile);
894 }
895
896 $data = $this->getAppData();
897 $data = $this->maybePushDynamicMetaData($data);
898
899 /**
900 * Lets a listener adjust the resolved head meta (canonical_url, title,
901 * description, json_ld) with knowledge the handler does not have - for
902 * example a pre-renderer that paints ?page=N of a space listing and needs
903 * the canonical to point at that page rather than at page one.
904 */
905 $data = apply_filters('fluent_community/portal_page_meta', $data, $this->currentPath, $this->preRenderContext);
906
907 $data['isHeadless'] = Helper::isHeadless();
908
909 if (!$data['isHeadless']) {
910 $this->loadClassicPortalAssets($data);
911 } else {
912 do_action('fluent_community/rendering_headless_portal', $data);
913 }
914
915 $data['html_class'] = '';
916 if (Utility::isCustomizationEnabled('enable_sidebar_toggle') && isset($_COOKIE['fcom_sidebar_collapsed']) && $_COOKIE['fcom_sidebar_collapsed'] === 'true') {
917 $data['html_class'] = 'fcom_sidebar_collapsed_desktop';
918 }
919
920 do_action('fluent_community/before_portal_rendered', $data);
921
922 status_header(200);
923 App::make('view')->render('portal_page', $data);
924 exit();
925 }
926
927 public function loadClassicPortalAssets($data)
928 {
929 $cssFiles = $data['css_files'];
930
931 unset($cssFiles['fcom_theme_default']);
932
933 foreach ($cssFiles as $fileName => $cssFile) {
934 wp_enqueue_style($fileName, $cssFile['url'], [], FLUENT_COMMUNITY_PLUGIN_VERSION, 'screen');
935 }
936
937 $jsFiles = $data['js_files'];
938 $jsTags = array_keys($jsFiles);
939 add_filter('script_loader_tag', function ($tag, $handle) use ($jsTags) {
940 if (!in_array($handle, $jsTags)) {
941 return $tag;
942 }
943 $tag = str_replace(' src', ' type="module" src', $tag);
944 return $tag;
945 }, 10, 2);
946 foreach ($jsFiles as $fileName => $jsFile) {
947 wp_enqueue_script($fileName, $jsFile['url'], $jsFile['deps'], FLUENT_COMMUNITY_PLUGIN_VERSION, [
948 'in_footer' => true,
949 'strategy' => 'defer'
950 ]);
951 }
952
953 foreach ($data['js_vars'] as $varKey => $values) {
954 wp_localize_script('fcom_start', $varKey, $values);
955 }
956 }
957
958 public function getAppData()
959 {
960 $isRtl = Helper::isRtl();
961
962 $userId = get_current_user_id();
963 $url = home_url(add_query_arg($_REQUEST, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
964
965 $appVars = $this->appVars();
966 $generalSettings = Helper::generalSettings();
967 $isDev = Utility::isDev();
968
969 $siteTitle = Arr::get($generalSettings, 'site_title');
970
971 $dataVars = [
972 'title' => $siteTitle,
973 'og_title' => $siteTitle,
974 'description' => get_bloginfo('description'),
975 'featured_image' => Arr::get($generalSettings, 'featured_image', ''),
976 'header_js_files' => [],
977 'js_vars' => [
978 'fluentComAdmin' => $appVars,
979 'fcom_portal_general' => $this->getGlobalScriptVars('portal'),
980 ],
981 'url' => $url,
982 'current_route' => $this->currentPath,
983 'contact' => $appVars['auth'],
984 'user' => $userId ? get_user_by('ID', $userId) : null,
985 'route_group' => Helper::getRouteNameByRequestPath($this->currentPath),
986 'is_dev' => $isDev,
987 'theme_color' => Utility::getThemeColor()
988 ];
989
990 if ($isDev) {
991 $dataVars['css_files'] = [
992 'fcom_theme_default' => [
993 'url' => Vite::getDynamicSrcUrl('theme-default.scss')
994 ],
995 'fcom_global' => [
996 'url' => Vite::getDynamicSrcUrl('global.scss')
997 ],
998 'fcom_vendor' => [
999 'url' => Vite::getDynamicSrcUrl('app_css.scss')
1000 ]
1001 ];
1002 $dataVars['js_files'] = [
1003 'fcom_general' => [
1004 'url' => Vite::getDynamicSrcUrl('portal_general.js'),
1005 'deps' => []
1006 ],
1007 'fcom_start' => [
1008 'url' => Vite::getDynamicSrcUrl('start.js'),
1009 'deps' => []
1010 ],
1011 'fcom_app' => [
1012 'url' => Vite::getDynamicSrcUrl('app.js'),
1013 'deps' => []
1014 ]
1015 ];
1016
1017 } else {
1018 $dataVars['js_files'] = [
1019 'fcom_general' => [
1020 'url' => Vite::getStaticSrcUrl('portal_general.js'),
1021 'deps' => []
1022 ],
1023 'fcom_start' => [
1024 'url' => Vite::getStaticSrcUrl('start.js'),
1025 'deps' => []
1026 ],
1027 'fcom_app' => [
1028 'url' => Vite::getStaticSrcUrl('app.js'),
1029 'deps' => []
1030 ]
1031 ];
1032 $dataVars['css_files'] = [
1033 'fcom_theme_default' => [
1034 'url' => Vite::getStaticSrcUrl('theme-default.css', $isRtl)
1035 ],
1036 'fcom_global' => [
1037 'url' => Vite::getStaticSrcUrl('global.css', $isRtl)
1038 ],
1039 'fcom_app_css' => [
1040 'url' => Vite::getStaticSrcUrl('app.css', $isRtl)
1041 ],
1042 'fcom_vendor' => [
1043 'url' => Vite::getStaticSrcUrl('app_css.css', $isRtl)
1044 ],
1045 ];
1046 }
1047
1048 return apply_filters('fluent_community/portal_data_vars', $dataVars);
1049 }
1050
1051 protected function maybePushDynamicMetaData($data)
1052 {
1053 $dynamicRoute = Arr::get($data, 'route_group');
1054
1055 $data['landing_route'] = $dynamicRoute;
1056
1057 $validGroups = [
1058 'user_profile',
1059 'community_view',
1060 'course_view',
1061 'feed_view',
1062 'lesson_view'
1063 ];
1064
1065 if (!in_array($dynamicRoute, $validGroups)) {
1066 return $data;
1067 }
1068
1069 if ($dynamicRoute == 'user_profile') {
1070 $uriParts = explode('/', $this->currentPath);
1071 if (count($uriParts) >= 2) {
1072 $userName = $uriParts[1];
1073 $xProfile = XProfile::where('username', $userName)->first();
1074 if ($xProfile && $xProfile->status === 'active' && Utility::canViewUserProfile($xProfile->user_id)) {
1075 $data['title'] = 'User ' . esc_html($xProfile->display_name) . ' - ' . $data['title'];
1076 $data['og_title'] = esc_html($xProfile->display_name) . ' - ' . $data['og_title'];
1077 if ($xProfile->short_description) {
1078 $data['description'] = Helper::getHumanExcerpt($xProfile->short_description, 100);
1079 }
1080 if ($coverPhoto = Arr::get($xProfile->meta, 'cover_photo')) {
1081 $data['featured_image'] = $coverPhoto;
1082 }
1083 }
1084 }
1085 return $data;
1086 }
1087
1088 if ($dynamicRoute == 'community_view' || $dynamicRoute == 'course_view') {
1089 $uriParts = explode('/', $this->currentPath);
1090 if (count($uriParts) >= 2) {
1091 $paceSlug = $uriParts[1];
1092 $space = BaseSpace::query()->onlyMain()->where('slug', $paceSlug)->first();
1093 if ($space && $space->privacy != 'secret') {
1094
1095 // matches the URL the sitemap submits, so tab variants consolidate onto it
1096 $data['canonical_url'] = $space->getPermalink();
1097
1098 $this->preRenderContext = [
1099 'route' => $dynamicRoute,
1100 'model' => $space,
1101 'path' => $this->currentPath,
1102 ];
1103
1104 if ($dynamicRoute == 'course_view') {
1105 /* translators: %s is replaced by the title of the space */
1106 $data['title'] = sprintf(__('Enroll %s', 'fluent-community'), esc_html($space->title) . ' - ' . $data['title']);
1107 /* translators: %s is replaced by the title of the space */
1108 $data['og_title'] = sprintf(__('Enroll %s', 'fluent-community'), esc_html($space->title));
1109 } else {
1110 /* translators: %s is replaced by the title of the space */
1111 $data['title'] = sprintf(__('Join %s', 'fluent-community'), esc_html($space->title) . ' - ' . $data['title']);
1112 /* translators: %s is replaced by the title of the space */
1113 $data['og_title'] = sprintf(__('Join %s', 'fluent-community'), esc_html($space->title));
1114 }
1115
1116 if ($space->description) {
1117 $data['description'] = Helper::getHumanExcerpt($space->description, 100);
1118 }
1119 if ($ogImage = Arr::get($space->settings, 'og_image')) {
1120 $data['featured_image'] = $ogImage;
1121 } else if ($space->cover_photo) {
1122 $data['featured_image'] = $space->cover_photo;
1123 }
1124
1125 if ($dynamicRoute == 'course_view') {
1126 $data['json_ld'] = apply_filters('fluent_community/course_view_json_ld', [], $space, $data);
1127 }
1128 }
1129 }
1130 return $data;
1131 }
1132
1133 if ($dynamicRoute == 'feed_view') {
1134 $uriParts = explode('/', $this->currentPath);
1135 if (count($uriParts) >= 2) {
1136 $postSlug = end($uriParts);
1137 // Scopes are dropped to reach image and article posts, so the type
1138 // has to be narrowed by hand - see Feed::$feedViewTypes.
1139 $feed = Feed::query()->withoutGlobalScopes()
1140 ->whereIn('type', Feed::$feedViewTypes)
1141 ->where('status', 'published')
1142 ->where('slug', $postSlug)
1143 ->with([
1144 'xprofile' => function ($q) {
1145 $q->select(ProfileHelper::getXProfilePublicFields());
1146 },
1147 'space'
1148 ])
1149 ->whereDoesntHave('space', function ($q) {
1150 $q->whereIn('privacy', ['secret', 'private']);
1151 })
1152 ->first();
1153
1154 if (!$feed) {
1155 return $data;
1156 }
1157
1158 if ($feed->title) {
1159 $data['title'] = esc_html($feed->title) . ' - ' . $data['title'];
1160 $data['og_title'] = esc_html($feed->title);
1161 } else {
1162 $authorName = esc_html($feed->xprofile ? $feed->xprofile->display_name : 'Someone');
1163 $data['title'] = $authorName . ' posted at ' . $data['title'];
1164 $data['og_title'] = $authorName . ' posted at ' . $data['og_title'];
1165 }
1166
1167 $feedUrl = $feed->getPermalink();
1168 $data['canonical_url'] = $feedUrl;
1169
1170 if ($feed->message) {
1171 $data['description'] = esc_html(Helper::getHumanExcerpt($feed->message, 160));
1172 }
1173
1174 if ($mediaPreview = Arr::get($feed->meta, 'media_preview.image')) {
1175 $data['featured_image'] = esc_url($mediaPreview);
1176 } else if ($firstImage = Arr::get($feed->meta, 'media_items.0.url')) {
1177 $data['featured_image'] = esc_url($firstImage);
1178 } else if ($feed->space) {
1179 $space = $feed->space;
1180 if ($ogImage = Arr::get($space->settings, 'og_image')) {
1181 $data['featured_image'] = $ogImage;
1182 } else if ($space->cover_photo) {
1183 $data['featured_image'] = $space->cover_photo;
1184 }
1185 }
1186
1187 $this->preRenderContext = [
1188 'route' => 'feed_view',
1189 'model' => $feed,
1190 'path' => $this->currentPath,
1191 ];
1192
1193 $data['json_ld'] = apply_filters('fluent_community/feed_view_json_ld', [], $feed, $data);
1194 }
1195 return $data;
1196 }
1197
1198 if ($dynamicRoute == 'lesson_view') {
1199 $uriParts = explode('/', $this->currentPath);
1200
1201 $lessonSlug = Arr::get($uriParts, 3);
1202 $courseSlug = Arr::get($uriParts, 1);
1203 if (!$lessonSlug || !$courseSlug) {
1204 return $data;
1205 }
1206
1207 // Resolve the course first: lesson slugs are only unique within one, so a
1208 // slug-first lookup would answer with whichever course was created first
1209 // and then fail the course comparison, costing the real lesson its metadata.
1210 $course = BaseSpace::query()->onlyMain()
1211 ->where('slug', $courseSlug)
1212 ->where('status', 'published')
1213 ->where('privacy', 'public')
1214 ->first();
1215
1216 if (!$course) {
1217 return $data;
1218 }
1219
1220 $lesson = CourseLesson::query()
1221 ->where('space_id', $course->id)
1222 ->where('slug', $lessonSlug)
1223 ->where('status', 'published')
1224 ->first();
1225
1226 if (!$lesson) {
1227 return $data;
1228 }
1229
1230 $data['title'] = esc_html($lesson->title) . ' - ' . $data['title'];
1231 $data['og_title'] = esc_html($lesson->title);
1232 $data['description'] = esc_html(Helper::getHumanExcerpt($lesson->message, 120));
1233 $data['canonical_url'] = $lesson->getPermalink();
1234
1235 $data['json_ld'] = apply_filters('fluent_community/lesson_view_json_ld', [], $lesson, $course, $data);
1236
1237 return $data;
1238 }
1239
1240 return $data;
1241 }
1242
1243 protected function getMainMenuItems($scope = 'sidebar')
1244 {
1245 $menuGroups = Helper::getMenuItemsGroup('view');
1246
1247 $items = Arr::get($menuGroups, 'mainMenuItems', []);
1248
1249 return apply_filters('fluent_community/main_menu_items', $items, $scope);
1250 }
1251
1252 public function getPortalHeader($echo = false, $context = 'headless')
1253 {
1254 $settings = Helper::generalSettings();
1255 $userId = get_current_user_id();
1256
1257 $xprofile = null;
1258 if ($userId && Helper::canAccessPortal($userId, false)) {
1259 $userModel = Helper::getCurrentUser();
1260 if ($userModel) {
1261 $xprofile = $userModel->syncXProfile(false);
1262 }
1263 }
1264
1265 $authUrl = $this->getAuthUrl();
1266
1267 if ($xprofile) {
1268 $xprofile->logout_url = wp_logout_url(Helper::baseUrl());
1269 }
1270
1271 $logo = Arr::get($settings, 'logo', '');
1272 $whiteLogo = Arr::get($settings, 'white_logo', '');
1273 if (!$whiteLogo) {
1274 $whiteLogo = $logo;
1275 }
1276
1277 $logoPermalink = Helper::baseUrl('/');
1278 $logoPermalinkType = Arr::get($settings, 'logo_permalink_type');
1279 if ($logoPermalinkType == 'custom') {
1280 $logoPermalink = Arr::get($settings, 'logo_permalink');
1281 }
1282
1283 $data = apply_filters('fluent_community/header_vars', [
1284 'portal_url' => Helper::baseUrl('/'),
1285 'logo' => $logo,
1286 'white_logo' => $whiteLogo,
1287 'logo_permalink' => $logoPermalink,
1288 'site_title' => Arr::get($settings, 'site_title'),
1289 'profile_url' => $xprofile ? Helper::baseUrl('u/' . $xprofile->username . '/') : '',
1290 'auth' => $xprofile ? $xprofile : null,
1291 'auth_url' => $authUrl,
1292 'menuItems' => $this->getMainMenuItems('header'),
1293 'context' => $context
1294 ]);
1295
1296 if ($echo) {
1297 App::make('view')->render('portal.header', $data);
1298 return;
1299 }
1300
1301 return (string)App::make('view')->make('portal.header', $data);
1302 }
1303
1304 private function getProfileLinks($xprofile = null)
1305 {
1306 if (!$xprofile) {
1307 $xprofile = Helper::getCurrentProfile();
1308 }
1309
1310 $profileLinks = [];
1311 $menuGroups = Helper::getMenuItemsGroup('view');
1312 if ($xprofile) {
1313 $profileLinks = Arr::get($menuGroups, 'profileDropdownItems', []);
1314
1315 $replaces = [
1316 '#{{user_url}}' => Helper::baseUrl('u/' . $xprofile->username),
1317 '#user_url' => Helper::baseUrl('u/' . $xprofile->username),
1318 '#{{logout_url}}' => wp_logout_url(Helper::baseUrl()),
1319 '#logout_url' => wp_logout_url(Helper::baseUrl())
1320 ];
1321
1322 foreach ($profileLinks as &$link) {
1323 $slug = Arr::get($link, 'slug', '');
1324
1325 if ($slug == 'my_spaces') {
1326 $link['permalink'] = Helper::baseUrl('u/' . $xprofile->username . '/spaces');
1327 } else if ($slug == 'logout') {
1328 $link['permalink'] = wp_logout_url(Helper::baseUrl());
1329 } else if ($slug == 'profile') {
1330 $link['permalink'] = Helper::baseUrl('u/' . $xprofile->username);
1331 }
1332
1333 $link['permalink'] = str_replace(array_keys($replaces), array_values($replaces), $link['permalink']);
1334 }
1335 }
1336
1337 return $profileLinks;
1338 }
1339
1340 public function getPortalSidebar($echo = false, $context = 'headless')
1341 {
1342 $data = Utility::getPortalSidebarData('sidebar');
1343
1344 $data['context'] = $context;
1345
1346 if ($echo) {
1347 App::make('view')->render('portal.main_sidebar', $data);
1348 return;
1349 }
1350
1351 return (string)App::make('view')->make('portal.main_sidebar', $data);
1352 }
1353
1354 protected function getAuthUrl()
1355 {
1356 return apply_filters('fluent_community/auth/login_url', Helper::getAuthUrl());
1357 }
1358
1359 public function viewErrorPage($title, $message = '', $showBtn = true, $btnData = [])
1360 {
1361 $data = [
1362 'title' => $title,
1363 'description' => $message,
1364 'btn_txt' => Arr::get($btnData, 'btn_text', __('Go to home page', 'fluent-community')),
1365 'url' => Arr::get($btnData, 'btn_url', home_url())
1366 ];
1367
1368 if (!$showBtn) {
1369 $data['btn_txt'] = '';
1370 }
1371
1372 status_header(200);
1373 App::make('view')->render('error_page', $data);
1374 exit();
1375 }
1376 }
1377