PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 All 138 releases
wpforo / admin / pages / news / src / AdminPage.php

AdminPage.php in wpForo Forum 3.1.6, at admin/pages/news/src/AdminPage.php

311 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace gVectors\News;
4
5 use gVectors\News\Services\ConsentService;
6 use gVectors\News\Services\PrefsService;
7
8 // Exit if accessed directly
9 if( ! defined( 'ABSPATH' ) ) exit;
10
11 /**
12 * News & Emails settings page.
13 *
14 * Every registered gVectors plugin gets its own menu entry, but all entries
15 * render the SAME page over the SAME shared options.
16 *
17 * Two mirrored sections, each a channel × category matrix:
18 * A) My Preferences — the logged-in administrator's own choices (user meta):
19 * master email unsubscribe + per-category checkboxes for dashboard notices
20 * and for emails, including installed/not-installed addon relevance.
21 * B) Site-Wide — the same matrix applied to the whole site for all
22 * admins, plus the master service switch (full opt-out after opt-in).
23 */
24 class AdminPage {
25
26 /** category key → [label, description] */
27 private const CATEGORY_LABELS = [
28 'expiry_reminder' => [ 'License expiry & billing reminders', 'Transactional — expiring/expired license warnings and failed-payment notices. Email only.' ],
29 'renewal_offer' => [ 'Renewal discount offers', 'Reminders about unused personal renewal discounts reserved for this site, before they expire. Email only.' ],
30 'abandoned_checkout' => [ 'Unfinished purchase reminders', 'Reminders when a checkout you started was not completed, including any recovery discount. Sent only to the administrator who started the purchase. Email only.' ],
31 'recommendations' => [ 'Addon recommendations', 'Personalized suggestions of addons other sites with a similar setup use. Each recommendation is sent at most once. Email only.' ],
32 'new_addon' => [ 'New addon announcements', 'Marketing — when a new addon is released.' ],
33 'new_feature' => [ 'New feature announcements', 'Marketing — when an addon gets a major new feature.' ],
34 'new_version' => [ 'New version releases', 'Version releases — changelogs, scheduled release dates, and actions required before or after an update.' ],
35 'discount' => [ 'Discounts & promotions', 'Marketing — sales, coupon codes and limited-time offers.' ],
36 'announcement' => [ 'General announcements', 'Marketing — other news from gVectors.' ],
37 'addons_installed' => [ 'News about installed addons', 'Addon-targeted news for addons this site has installed.' ],
38 'addons_not_installed' => [ 'News about addons not installed here', 'Addon-targeted news for addons this site does not have — disable to skip irrelevant news.' ],
39 ];
40
41 private $config;
42 private $consent;
43 private $prefs;
44
45 public function __construct( Config $config, ConsentService $consent, PrefsService $prefs ) {
46 $this->config = $config;
47 $this->consent = $consent;
48 $this->prefs = $prefs;
49
50 // Form handlers are shared — registered once, whichever menu the form was opened from
51 add_action( 'admin_post_' . $this->config->get_prefix() . 'save_my_email_prefs', [ $this, 'handle_save_my_prefs' ] );
52 add_action( 'admin_post_' . $this->config->get_prefix() . 'save_site_email_settings', [ $this, 'handle_save_site_settings' ] );
53
54 $this->add_context( $config );
55 }
56
57 /**
58 * Register one gVectors plugin's "News & Emails" menu item.
59 * Every registered plugin gets its own menu entry under its own admin menu,
60 * but they all render the SAME page and store the SAME shared options.
61 */
62 public function add_context( Config $config ): void {
63 add_action( $config->get_dashboard_menu_hook(), function( $parent_slug ) use ( $config ) {
64 add_submenu_page(
65 $parent_slug,
66 __( 'News & Emails', 'gvectors' ),
67 __( 'News & Emails', 'gvectors' ),
68 'activate_plugins',
69 $config->get_settings_page_slug(),
70 function() {
71 $this->render_page();
72 }
73 );
74 } );
75 }
76
77 // ==========================================
78 // Form handlers (admin-post.php)
79 // ==========================================
80
81 /**
82 * Save the current administrator's own preference matrix.
83 */
84 public function handle_save_my_prefs(): void {
85 if( ! current_user_can( 'activate_plugins' ) ) {
86 wp_die( esc_html__( 'Permission denied', 'gvectors' ) );
87 }
88 check_admin_referer( $this->config->get_prefix() . 'save_my_email_prefs' );
89
90 $this->prefs->save_user_prefs(
91 get_current_user_id(),
92 ! empty( $_POST['gv_unsubscribed'] ),
93 $this->read_matrix_input( 'my' )
94 );
95
96 $this->redirect_back( 'prefs' );
97 }
98
99 /**
100 * Save the site-wide matrix + the master service switch.
101 */
102 public function handle_save_site_settings(): void {
103 if( ! current_user_can( 'activate_plugins' ) ) {
104 wp_die( esc_html__( 'Permission denied', 'gvectors' ) );
105 }
106 check_admin_referer( $this->config->get_prefix() . 'save_site_email_settings' );
107
108 $was_enabled = $this->consent->is_enabled();
109 $enable = ! empty( $_POST['gv_service_enabled'] );
110
111 update_option( $this->config->get_service_enabled_option(), $enable );
112 $this->prefs->save_site_prefs( $this->read_matrix_input( 'site' ) );
113
114 if( $enable && ! $was_enabled ) {
115 // Opt-in from this page counts as consent — suppress the opt-in notice
116 // and run the first sync right away instead of waiting for the daily cron.
117 update_option( $this->config->get_optin_notice_dismissed_option(), true );
118 if( ! wp_next_scheduled( $this->config->get_cron_hook() ) ) {
119 wp_schedule_single_event( time() + 10, $this->config->get_cron_hook() );
120 }
121 } elseif( ! $enable && $was_enabled ) {
122 // Opt-out: stop the daily sync immediately (the scheduler on `init`
123 // would also clear it, but don't leave a scheduled event behind).
124 wp_clear_scheduled_hook( $this->config->get_cron_hook() );
125 }
126
127 $this->redirect_back( 'site' );
128 }
129
130 /**
131 * Collect checked channel/category checkboxes from the submitted form.
132 * Field names: gv_{scope}_{channel}_{category}, e.g. gv_my_notices_new_addon.
133 */
134 private function read_matrix_input( string $scope ): array {
135 $enabled = [];
136 foreach( PrefsService::CHANNELS as $channel ) {
137 $enabled[ $channel ] = [];
138 foreach( PrefsService::channel_categories( $channel ) as $category ) {
139 if( ! empty( $_POST[ 'gv_' . $scope . '_' . $channel . '_' . $category ] ) ) {
140 $enabled[ $channel ][] = $category;
141 }
142 }
143 }
144
145 return $enabled;
146 }
147
148 /**
149 * Settings page URL to return to after saving — the page the form was
150 * submitted from (each registered plugin has its own menu entry).
151 */
152 private function get_return_url(): string {
153 $requested = isset( $_POST['gv_return'] ) ? sanitize_key( $_POST['gv_return'] ) : '';
154 foreach( NewsModule::get_contexts() as $config ) {
155 if( $requested === $config->get_settings_page_slug() ) {
156 return $config->get_settings_page_url();
157 }
158 }
159
160 return $this->config->get_settings_page_url();
161 }
162
163 private function redirect_back( string $section ): void {
164 wp_safe_redirect( add_query_arg( 'updated', $section, $this->get_return_url() ) );
165 exit;
166 }
167
168 // ==========================================
169 // Page rendering
170 // ==========================================
171
172 /**
173 * One channel × category checkbox matrix.
174 *
175 * @param string $scope 'my' | 'site' — field name prefix
176 * @param array $matrix ['notices' => [cat => bool], 'emails' => [cat => bool]]
177 */
178 private function render_matrix( string $scope, array $matrix ): void {
179 $all_categories = array_merge(
180 PrefsService::EMAIL_ONLY_CATEGORIES,
181 PrefsService::NEWS_CATEGORIES,
182 PrefsService::RELEVANCE_CATEGORIES
183 );
184 ?>
185 <table class="widefat striped" style="max-width:860px;">
186 <thead>
187 <tr>
188 <th><?php esc_html_e( 'Category', 'gvectors' ); ?></th>
189 <th style="width:130px;text-align:center;"><?php esc_html_e( 'Dashboard notices', 'gvectors' ); ?></th>
190 <th style="width:130px;text-align:center;"><?php esc_html_e( 'Emails', 'gvectors' ); ?></th>
191 </tr>
192 </thead>
193 <tbody>
194 <?php foreach( $all_categories as $category ) :
195 $label = self::CATEGORY_LABELS[ $category ] ?? [ $category, '' ];
196 ?>
197 <tr>
198 <td>
199 <strong><?php echo esc_html( __( $label[0], 'gvectors' ) ); ?></strong>
200 <?php if( $label[1] ) : ?>
201 <br><span class="description"><?php echo esc_html( __( $label[1], 'gvectors' ) ); ?></span>
202 <?php endif; ?>
203 </td>
204 <?php foreach( PrefsService::CHANNELS as $channel ) : ?>
205 <td style="text-align:center;">
206 <?php if( in_array( $category, PrefsService::channel_categories( $channel ), true ) ) : ?>
207 <input type="checkbox"
208 name="gv_<?php echo esc_attr( $scope . '_' . $channel . '_' . $category ); ?>"
209 value="1" <?php checked( ! empty( $matrix[ $channel ][ $category ] ) ); ?>>
210 <?php else : ?>
211 <span aria-hidden="true">&mdash;</span>
212 <?php endif; ?>
213 </td>
214 <?php endforeach; ?>
215 </tr>
216 <?php endforeach; ?>
217 </tbody>
218 </table>
219 <?php
220 }
221
222 private function render_page(): void {
223 if( ! current_user_can( 'activate_plugins' ) ) return;
224
225 $user = wp_get_current_user();
226 $user_prefs = $this->prefs->get_user_prefs( $user->ID );
227 $site_prefs = $this->prefs->get_site_prefs();
228 $updated = isset( $_GET['updated'] ) ? sanitize_key( $_GET['updated'] ) : '';
229 $prefix = $this->config->get_prefix();
230 $current = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
231 $products = array_keys( NewsModule::get_contexts() );
232 ?>
233 <div class="wrap gvectors-news-settings">
234 <h1><?php esc_html_e( 'News & Emails', 'gvectors' ); ?></h1>
235
236 <?php if( $updated === 'prefs' ) : ?>
237 <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Your preferences have been saved.', 'gvectors' ); ?></p></div>
238 <?php elseif( $updated === 'site' ) : ?>
239 <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Site-wide settings have been saved.', 'gvectors' ); ?></p></div>
240 <?php endif; ?>
241
242 <!-- ── Section A: this administrator's own preferences ── -->
243 <h2><?php esc_html_e( 'My Preferences', 'gvectors' ); ?></h2>
244 <p class="description">
245 <?php
246 printf(
247 /* translators: %s: the administrator's email address */
248 esc_html__( 'These settings apply only to your own account (%s). Other administrators manage their preferences on this same page. A dashboard notice you dismiss is dismissed only for you.', 'gvectors' ),
249 '<strong>' . esc_html( $user->user_email ) . '</strong>'
250 );
251 ?>
252 </p>
253 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
254 <input type="hidden" name="action" value="<?php echo esc_attr( $prefix . 'save_my_email_prefs' ); ?>">
255 <input type="hidden" name="gv_return" value="<?php echo esc_attr( $current ); ?>">
256 <?php wp_nonce_field( $prefix . 'save_my_email_prefs' ); ?>
257
258 <p>
259 <label>
260 <input type="checkbox" name="gv_unsubscribed" value="1" <?php checked( $user_prefs['unsubscribed'] ); ?>>
261 <strong><?php esc_html_e( 'Unsubscribe my email from ALL news module emails', 'gvectors' ); ?></strong>
262 </label>
263 <br><span class="description"><?php esc_html_e( 'When checked, you receive no emails from this module regardless of the matrix below. Dashboard notices are not affected.', 'gvectors' ); ?></span>
264 </p>
265
266 <?php $this->render_matrix( 'my', $user_prefs ); ?>
267
268 <p class="submit"><input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Save My Preferences', 'gvectors' ); ?>"></p>
269 </form>
270
271 <hr>
272
273 <!-- ── Section B: site-wide switches (all administrators) ── -->
274 <h2><?php esc_html_e( 'Site-Wide Settings', 'gvectors' ); ?></h2>
275 <p class="description">
276 <?php esc_html_e( 'These settings apply to the whole site and all administrators: a category disabled here is off for everyone, regardless of personal preferences.', 'gvectors' ); ?>
277 <?php
278 if( count( $products ) > 1 ) {
279 printf(
280 /* translators: %s: comma-separated list of gVectors plugin slugs */
281 esc_html__( 'They are shared by all installed gVectors products (%s) — news, emails and license reminders are managed once for all of them.', 'gvectors' ),
282 '<strong>' . esc_html( implode( ', ', $products ) ) . '</strong>'
283 );
284 }
285 ?>
286 </p>
287 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
288 <input type="hidden" name="action" value="<?php echo esc_attr( $prefix . 'save_site_email_settings' ); ?>">
289 <input type="hidden" name="gv_return" value="<?php echo esc_attr( $current ); ?>">
290 <?php wp_nonce_field( $prefix . 'save_site_email_settings' ); ?>
291
292 <p>
293 <label>
294 <input type="checkbox" name="gv_service_enabled" value="1" <?php checked( $this->consent->is_enabled() ); ?>>
295 <strong><?php esc_html_e( 'Enable the addon news service', 'gvectors' ); ?></strong>
296 </label>
297 <br><span class="description">
298 <?php esc_html_e( 'Master switch. When enabled, this site connects to the gVectors server once a day to fetch addon news and check for expiring licenses. When disabled, the daily sync stops and no outbound requests are made.', 'gvectors' ); ?>
299 <a href="<?php echo esc_url( $this->config->get_privacy_policy_url() ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Privacy policy', 'gvectors' ); ?></a>
300 </span>
301 </p>
302
303 <?php $this->render_matrix( 'site', $site_prefs ); ?>
304
305 <p class="submit"><input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Save Site-Wide Settings', 'gvectors' ); ?>"></p>
306 </form>
307 </div>
308 <?php
309 }
310 }
311