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 / Services / PrefsService.php

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

208 lines 8.0 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\Services;
4
5 use gVectors\News\Config;
6
7 // Exit if accessed directly
8 if( ! defined( 'ABSPATH' ) ) exit;
9
10 /**
11 * Central preference matrix: channel × category, at two scopes.
12 *
13 * Channels:
14 * - notices : dashboard admin notices
15 * - emails : emails sent via wp_mail
16 *
17 * Categories:
18 * - expiry_reminder (emails only — transactional)
19 * - renewal_offer (emails only — unused personal renewal
20 * discount reminders, unsubscribable separately from expiry warnings)
21 * - blocked_update (emails only — WordPress found new versions of
22 * licensed addons but this site blocks plugin installs)
23 * - new_addon, new_feature, announcement (news content types)
24 * - addons_installed / addons_not_installed (relevance filter — applies only
25 * to news items targeted at a specific addon via plugin_slug: news about an
26 * addon the site has installed vs news about addons it doesn't have)
27 *
28 * Scopes:
29 * - site : one shared option, applies to everyone (option: site prefs)
30 * - user : each administrator's own choices (user meta), incl. a master
31 * email unsubscribe
32 *
33 * A news item reaches an admin through a channel only when BOTH scopes allow
34 * its content category AND (for addon-targeted items) its relevance category.
35 * Everything defaults to enabled — only an explicit false disables.
36 */
37 class PrefsService {
38
39 public const CHANNEL_NOTICES = 'notices';
40 public const CHANNEL_EMAILS = 'emails';
41 public const CHANNELS = [ self::CHANNEL_NOTICES, self::CHANNEL_EMAILS ];
42
43 /** News content categories (map 1:1 from news item types). */
44 public const NEWS_CATEGORIES = [ 'new_addon', 'new_feature', 'new_version', 'discount', 'announcement' ];
45
46 /** Relevance filter for addon-targeted news (items with a plugin_slug). */
47 public const RELEVANCE_CATEGORIES = [ 'addons_installed', 'addons_not_installed' ];
48
49 /** Categories that only exist on the emails channel. */
50 public const EMAIL_ONLY_CATEGORIES = [ 'expiry_reminder', 'renewal_offer', 'abandoned_checkout', 'blocked_update', 'recommendations' ];
51
52 private $config;
53
54 public function __construct( Config $config ) {
55 $this->config = $config;
56 }
57
58 /**
59 * All category keys available on a channel.
60 */
61 public static function channel_categories( string $channel ): array {
62 $categories = array_merge( self::NEWS_CATEGORIES, self::RELEVANCE_CATEGORIES );
63 if( $channel === self::CHANNEL_EMAILS ) {
64 $categories = array_merge( self::EMAIL_ONLY_CATEGORIES, $categories );
65 }
66
67 return $categories;
68 }
69
70 // ==========================================
71 // Per-admin preferences (user meta)
72 // ==========================================
73
74 /**
75 * Shape: ['unsubscribed' => bool, 'notices' => [cat => bool], 'emails' => [cat => bool]]
76 * Missing meta or missing keys mean enabled.
77 */
78 public function get_user_prefs( int $user_id ): array {
79 $raw = get_user_meta( $user_id, $this->config->get_user_prefs_meta(), true );
80 $raw = is_array( $raw ) ? $raw : [];
81
82 $prefs = [ 'unsubscribed' => ! empty( $raw['unsubscribed'] ) ];
83 foreach( self::CHANNELS as $channel ) {
84 foreach( self::channel_categories( $channel ) as $category ) {
85 $prefs[ $channel ][ $category ] = ! isset( $raw[ $channel ][ $category ] ) || ! empty( $raw[ $channel ][ $category ] );
86 }
87 }
88
89 return $prefs;
90 }
91
92 /**
93 * @param array $enabled_by_channel ['notices' => ['new_addon', ...], 'emails' => [...]] — enabled category keys per channel
94 */
95 public function save_user_prefs( int $user_id, bool $unsubscribed, array $enabled_by_channel ): void {
96 update_user_meta(
97 $user_id,
98 $this->config->get_user_prefs_meta(),
99 [ 'unsubscribed' => $unsubscribed ] + $this->normalize_matrix( $enabled_by_channel )
100 );
101 }
102
103 // ==========================================
104 // Site-wide preferences (shared option)
105 // ==========================================
106
107 /**
108 * Shape: ['notices' => [cat => bool], 'emails' => [cat => bool]] — missing keys mean enabled.
109 */
110 public function get_site_prefs(): array {
111 $raw = get_option( $this->config->get_site_prefs_option(), [] );
112 $raw = is_array( $raw ) ? $raw : [];
113
114 $prefs = [];
115 foreach( self::CHANNELS as $channel ) {
116 foreach( self::channel_categories( $channel ) as $category ) {
117 $prefs[ $channel ][ $category ] = ! isset( $raw[ $channel ][ $category ] ) || ! empty( $raw[ $channel ][ $category ] );
118 }
119 }
120
121 return $prefs;
122 }
123
124 /**
125 * @param array $enabled_by_channel ['notices' => [enabled keys], 'emails' => [enabled keys]]
126 */
127 public function save_site_prefs( array $enabled_by_channel ): void {
128 update_option( $this->config->get_site_prefs_option(), $this->normalize_matrix( $enabled_by_channel ) );
129 }
130
131 private function normalize_matrix( array $enabled_by_channel ): array {
132 $matrix = [];
133 foreach( self::CHANNELS as $channel ) {
134 $enabled = isset( $enabled_by_channel[ $channel ] ) && is_array( $enabled_by_channel[ $channel ] )
135 ? $enabled_by_channel[ $channel ]
136 : [];
137 foreach( self::channel_categories( $channel ) as $category ) {
138 $matrix[ $channel ][ $category ] = in_array( $category, $enabled, true );
139 }
140 }
141
142 return $matrix;
143 }
144
145 // ==========================================
146 // Decision helpers
147 // ==========================================
148
149 public function site_allows( string $channel, string $category ): bool {
150 $site = $this->get_site_prefs();
151
152 return ! empty( $site[ $channel ][ $category ] );
153 }
154
155 /**
156 * Per-admin check. On the emails channel a master unsubscribe blocks everything.
157 */
158 public function user_allows( int $user_id, string $channel, string $category ): bool {
159 $prefs = $this->get_user_prefs( $user_id );
160 if( $channel === self::CHANNEL_EMAILS && $prefs['unsubscribed'] ) return false;
161
162 return ! empty( $prefs[ $channel ][ $category ] );
163 }
164
165 /**
166 * Combined site + user check — the only gate senders/renderers should use.
167 */
168 public function allows( int $user_id, string $channel, string $category ): bool {
169 return $this->site_allows( $channel, $category ) && $this->user_allows( $user_id, $channel, $category );
170 }
171
172 /**
173 * Full decision for one news item on one channel for one admin:
174 * content category must be allowed, and for addon-targeted items the
175 * relevance category (installed / not installed) must be allowed too.
176 */
177 public function news_item_allowed( array $item, string $channel, int $user_id ): bool {
178 $category = in_array( $item['type'] ?? '', self::NEWS_CATEGORIES, true ) ? $item['type'] : 'announcement';
179 if( ! $this->allows( $user_id, $channel, $category ) ) return false;
180
181 if( ! empty( $item['plugin_slug'] ) ) {
182 $relevance = $this->is_addon_installed( (string) $item['plugin_slug'] ) ? 'addons_installed' : 'addons_not_installed';
183 if( ! $this->allows( $user_id, $channel, $relevance ) ) return false;
184 }
185
186 return true;
187 }
188
189 /**
190 * Is an addon with this plugin slug installed on the site (any activation state)?
191 * Matched by plugin directory name; result cached per request.
192 */
193 public function is_addon_installed( string $slug ): bool {
194 static $installed = null;
195 if( $installed === null ) {
196 if( ! function_exists( 'get_plugins' ) ) {
197 require_once ABSPATH . 'wp-admin/includes/plugin.php';
198 }
199 $installed = [];
200 foreach( array_keys( get_plugins() ) as $plugin_file ) {
201 $installed[ dirname( $plugin_file ) ] = true;
202 }
203 }
204
205 return isset( $installed[ $slug ] );
206 }
207 }
208