PluginProbe
Filter Everything — WordPress & WooCommerce Filters / trunk
Filter Everything — WordPress & WooCommerce Filters vtrunk
1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 All 51 releases
filter-everything / src / Admin / WhatsNew.php

WhatsNew.php in Filter Everything — WordPress & WooCommerce Filters trunk, at src/Admin/WhatsNew.php

266 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FilterEverything\Filter;
4
5 if ( ! defined('ABSPATH') ) {
6 exit;
7 }
8
9 /**
10 * «What's new» — release notes inside the admin, plus the badge that points to them.
11 *
12 * The problem it solves: after an update (increasingly an automatic one) nobody
13 * opens the plugin settings to discover what changed, yet a redirect or a modal
14 * would be obnoxious. So:
15 *
16 * - a «What's new» submenu page under the Filters menu renders the release
17 * notes of the INSTALLED version straight from the bundled readme.txt
18 * changelog — zero extra maintenance, no remote calls, always in sync with
19 * what actually shipped; earlier releases are listed below, collapsed;
20 * - after an in-place update the Filters menu item (and the submenu entry)
21 * carry the WP-native red «1» badge until THIS admin opens the page — it is
22 * visible from every admin screen but demands nothing, and it stops on its
23 * own after self::BADGE_TTL. Fresh installs never see it: there is nothing
24 * to catch up on.
25 *
26 * "Seen" is per user (user meta — each admin gets one quiet nudge), "updated"
27 * is per site (AdminNotices' update stamp). Both builds, free and PRO.
28 *
29 * Complements, not replaces, the one-off AdminNotices entries: those are for
30 * releases that change behaviour; the badge is the baseline for every release.
31 */
32 class WhatsNew
33 {
34 /** Submenu slug (?page=…). */
35 const PAGE_SLUG = 'filters-whats-new';
36
37 /** Per-user: the last version whose release notes this admin has opened. */
38 const USER_META = 'flrt_whats_new_seen';
39
40 /** The badge gives up on its own after this long since the update. */
41 const BADGE_TTL = 30 * DAY_IN_SECONDS;
42
43 /** The complete, always-current changelog lives on the site. */
44 const CHANGELOG_URL = 'https://filtereverything.pro/changelog/';
45
46 public function __construct()
47 {
48 if ( ! is_admin() ) {
49 return;
50 }
51
52 // Registered from inside Admin::adminMenu(), right after «Settings»
53 add_action( 'wpc_after_add_submenu_pages', [ $this, 'addPage' ] );
54 // Runs after every submenu (ours included) exists
55 add_action( 'admin_menu', [ $this, 'decorateMenu' ], 99 );
56 }
57
58 public static function parentSlug()
59 {
60 return 'edit.php?post_type=' . FLRT_FILTERS_SET_POST_TYPE;
61 }
62
63 public static function pageUrl()
64 {
65 return admin_url( self::parentSlug() . '&page=' . self::PAGE_SLUG );
66 }
67
68 /** Version as it appears in the changelog: 1.9.6-dev → 1.9.6 */
69 public static function baseVersion()
70 {
71 return preg_replace( '/[-+].*$/', '', FLRT_PLUGIN_VER );
72 }
73
74 public static function isWhatsNewPage()
75 {
76 return isset( $_GET['page'] ) && sanitize_key( wp_unslash( $_GET['page'] ) ) === self::PAGE_SLUG;
77 }
78
79 public function addPage()
80 {
81 add_submenu_page(
82 self::parentSlug(),
83 esc_html__( "What's new", 'filter-everything' ),
84 esc_html__( "What's new", 'filter-everything' ),
85 flrt_plugin_user_caps(),
86 self::PAGE_SLUG,
87 [ $this, 'renderPage' ]
88 );
89 }
90
91 /**
92 * Appends the badge to the Filters menu item and to the «What's new» entry
93 * while this admin has unread release notes. Opening the page marks them read.
94 */
95 public function decorateMenu()
96 {
97 if ( ! current_user_can( flrt_plugin_user_caps() ) ) {
98 return;
99 }
100
101 if ( self::isWhatsNewPage() ) {
102 $this->markSeen();
103 return;
104 }
105
106 if ( ! $this->badgeDue() ) {
107 return;
108 }
109
110 global $menu, $submenu;
111
112 $parent = self::parentSlug();
113 $badge = ' <span class="update-plugins count-1 flrt-whats-new-badge"><span class="plugin-count" aria-hidden="true">1</span>'
114 . '<span class="screen-reader-text">' . esc_html__( 'New release notes available', 'filter-everything' ) . '</span></span>';
115
116 foreach ( (array) $menu as $i => $item ) {
117 if ( isset( $item[2] ) && $item[2] === $parent ) {
118 $menu[ $i ][0] .= $badge;
119 break;
120 }
121 }
122
123 if ( isset( $submenu[ $parent ] ) ) {
124 foreach ( $submenu[ $parent ] as $i => $item ) {
125 if ( isset( $item[2] ) && $item[2] === self::PAGE_SLUG ) {
126 $submenu[ $parent ][ $i ][0] .= $badge;
127 break;
128 }
129 }
130 }
131 }
132
133 /**
134 * Unread notes exist when the site was updated in place to the running
135 * version (never on a fresh install), the update is recent enough, and this
136 * admin has not opened the page for this version yet.
137 */
138 public function badgeDue()
139 {
140 if ( get_option( AdminNotices::UPDATED_OPTION ) !== FLRT_PLUGIN_VER ) {
141 return false;
142 }
143
144 $since = (int) get_option( AdminNotices::UPDATED_AT_OPTION, 0 );
145 if ( $since && ( time() - $since ) > self::BADGE_TTL ) {
146 return false;
147 }
148
149 return get_user_meta( get_current_user_id(), self::USER_META, true ) !== FLRT_PLUGIN_VER;
150 }
151
152 public function markSeen()
153 {
154 if ( get_user_meta( get_current_user_id(), self::USER_META, true ) !== FLRT_PLUGIN_VER ) {
155 update_user_meta( get_current_user_id(), self::USER_META, FLRT_PLUGIN_VER );
156 }
157 }
158
159 public function renderPage()
160 {
161 if ( ! current_user_can( flrt_plugin_user_caps() ) ) {
162 wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'filter-everything' ) );
163 }
164
165 $releases = self::changelog();
166 $version = self::baseVersion();
167 $current = null;
168
169 if ( isset( $releases[ $version ] ) ) {
170 $current = $releases[ $version ];
171 } elseif ( ! empty( $releases ) ) {
172 // A dev/hotfix build without its own block yet: show the newest notes
173 $version = (string) array_key_first( $releases );
174 $current = $releases[ $version ];
175 }
176
177 $earlier = $releases;
178 unset( $earlier[ $version ] );
179
180 flrt_include_admin_view( 'whats-new', [
181 'version' => $version,
182 'current' => $current,
183 'earlier' => $earlier,
184 'changelog_url' => self::CHANGELOG_URL,
185 ] );
186 }
187
188 /**
189 * Parses the bundled readme.txt changelog.
190 *
191 * Understands the format the project has used for years:
192 * = 1.9.6 =
193 * *Release Date - 5 August 2026*
194 * * Fix - Fixed …
195 * * Dev - NEW: Added …
196 *
197 * @return array version => ['date' => string, 'items' => [ ['label' => 'Fix', 'new' => bool, 'text' => string], … ]]
198 * in readme order (newest first)
199 */
200 public static function changelog()
201 {
202 $file = FLRT_PLUGIN_DIR_PATH . 'readme.txt';
203 if ( ! is_readable( $file ) ) {
204 return [];
205 }
206
207 $text = (string) file_get_contents( $file );
208 $pos = stripos( $text, '== Changelog ==' );
209 if ( $pos === false ) {
210 return [];
211 }
212 $text = substr( $text, $pos + strlen( '== Changelog ==' ) );
213
214 // Stop at the next top-level section, e.g. "== Upgrade Notice =="
215 if ( preg_match( '/^==\s*[^=\r\n]+\s*==\s*$/m', $text, $m, PREG_OFFSET_CAPTURE ) ) {
216 $text = substr( $text, 0, $m[0][1] );
217 }
218
219 $releases = [];
220 $version = null;
221
222 foreach ( preg_split( '/\r\n|\r|\n/', $text ) as $line ) {
223 $line = trim( $line );
224 if ( $line === '' ) {
225 continue;
226 }
227
228 if ( preg_match( '/^=\s*([0-9][0-9A-Za-z.\-]*)\s*=$/', $line, $m ) ) {
229 $version = $m[1];
230 $releases[ $version ] = [ 'date' => '', 'items' => [] ];
231 continue;
232 }
233
234 if ( $version === null ) {
235 continue;
236 }
237
238 if ( preg_match( '/^\*\s*Release Date\s*[-–—:]\s*(.+?)\s*\*$/i', $line, $m ) ) {
239 $releases[ $version ]['date'] = $m[1];
240 continue;
241 }
242
243 if ( preg_match( '/^[\*\-]\s*(?:([A-Za-z]+)\s+-\s+)?(.+)$/', $line, $m ) ) {
244 $label = $m[1];
245 $body = trim( $m[2] );
246 $is_new = false;
247
248 if ( preg_match( '/^NEW:\s*/i', $body ) ) {
249 $is_new = true;
250 $body = preg_replace( '/^NEW:\s*/i', '', $body );
251 }
252
253 $releases[ $version ]['items'][] = [
254 'label' => $label,
255 'new' => $is_new,
256 'text' => $body,
257 ];
258 }
259 }
260
261 return $releases;
262 }
263 }
264
265 new WhatsNew();
266