PluginProbe
wpForo Forum / 3.2.1
wpForo Forum v3.2.1
3.2.1 3.2.0 3.1.7 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 All 141 releases
wpforo / admin / pages / news / src / AdminPage.php

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

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