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

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

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