PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.1.3
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.1.3
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / admin / class-avcf-upgrade-notice.php
atarim-visual-collaboration / admin Last commit date
atarim-connect-screen.php 2 days ago class-avcf-offboarding.php 2 days ago class-avcf-settings.php 2 days ago class-avcf-upgrade-notice.php 2 days ago class-user-meta.php 2 days ago
class-avcf-upgrade-notice.php
230 lines
1 <?php
2 /**
3 * Plugin-update notice rendering for the Plugins screen.
4 *
5 * Parses the readme "Upgrade Notice" body into one or more title/body blocks
6 * and renders them as a zebra-striped list under the plugin row.
7 *
8 * Authoring convention (readme "Upgrade Notice" section):
9 * - A line fully wrapped in ** ** is a block title and starts a new notice.
10 * - Following lines are that block body, until the next title line.
11 * - Inline bold in a body uses <b> or <strong> tags only.
12 * - Links use markdown [label](https://...) or bare https:// URLs.
13 *
14 * @package Atarim
15 */
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 class AVCF_Upgrade_Notice {
22
23 public function __construct() {
24 $this->init_hooks();
25 }
26
27 private function init_hooks() {
28 add_filter( 'site_transient_update_plugins', array( $this, 'hide_notice_on_updates_screen' ) );
29 add_action( 'in_plugin_update_message-' . AVCF_PLUGIN_BASE, array( $this, 'render_update_message' ), 10, 2 );
30 }
31
32 /**
33 * Hide the raw upgrade_notice on the Dashboard -> Updates screen only.
34 *
35 * @param object $transient Update plugins transient.
36 * @return object
37 */
38 public function hide_notice_on_updates_screen( $transient ) {
39
40 if ( ! is_admin() ) {
41 return $transient;
42 }
43
44 global $pagenow;
45
46 if ( ! isset( $transient->response[ AVCF_PLUGIN_BASE ] ) ) {
47 return $transient;
48 }
49
50 if ( $pagenow === 'update-core.php'
51 && isset( $transient->response[ AVCF_PLUGIN_BASE ]->upgrade_notice ) ) {
52 unset( $transient->response[ AVCF_PLUGIN_BASE ]->upgrade_notice );
53 }
54
55 return $transient;
56 }
57
58 /**
59 * Render the upgrade-notice block list on the Plugins screen.
60 *
61 * @param array $plugin_data Plugin data from the plugins list table.
62 * @param object $response Update offer object (provides ->upgrade_notice).
63 * @return void
64 */
65 public function render_update_message( $plugin_data, $response ) {
66
67 if ( empty( $response->upgrade_notice ) ) {
68 return;
69 }
70
71 // Decode entities, convert <br> to newlines, keep only inline bold tags.
72 $raw = html_entity_decode( $response->upgrade_notice, ENT_QUOTES, get_bloginfo( 'charset' ) );
73 $raw = preg_replace( '#<br\s*/?>#i', "\n", $raw );
74 $raw = strip_tags( $raw, '<b><strong>' );
75 $raw = str_replace( array( "\r\n", "\r" ), "\n", $raw );
76 $raw = trim( $raw );
77
78 if ( $raw === '' ) {
79 return;
80 }
81
82 $blocks = $this->parse_blocks( $raw );
83
84 if ( empty( $blocks ) ) {
85 return;
86 }
87
88 $wrap_style = 'display:block;margin-top:8px;border:1px solid #f0e0b8;';
89 $wrap_style .= 'border-left:4px solid #d63638;border-radius:6px;overflow:hidden;';
90 $wrap_style .= 'line-height:1.5;box-sizing:border-box;';
91
92 echo '<span class="atarim-upgrade-notices" style="' . esc_attr( $wrap_style ) . '">';
93
94 $allowed_html = array(
95 'a' => array(
96 'href' => array(),
97 'target' => array(),
98 'rel' => array(),
99 ),
100 'b' => array(),
101 'strong' => array(),
102 );
103
104 $count = count( $blocks );
105
106 foreach ( $blocks as $i => $block ) {
107
108 // Row 1 = odd (no fill), row 2 = even (light fill), alternating.
109 $bg = ( $i % 2 === 1 ) ? '#fff8e5' : '#ffffff';
110 $separator = ( $i < $count - 1 ) ? 'border-bottom:1px solid #f3ead0;' : '';
111 $row_style = 'display:block;padding:10px 14px;background:' . $bg . ';' . $separator;
112
113 echo '<span style="' . esc_attr( $row_style ) . '">';
114
115 $title = trim( preg_replace( "/\s+/", " ", $block['title'] ) );
116 if ( $title !== '' ) {
117 echo '<strong style="display:block;margin-bottom:2px;">' . esc_html( $title ) . '</strong>';
118 }
119
120 $body = trim( preg_replace( "/\s+/", " ", $block['body'] ) );
121 if ( $body !== '' ) {
122 echo wp_kses( $this->format_body( $body ), $allowed_html );
123 }
124
125 echo '</span>';
126 }
127
128 echo '</span>';
129 }
130
131 /**
132 * Split a multi-block upgrade notice into title/body pairs.
133 *
134 * A line fully wrapped in ** ** is a title and starts a new block.
135 * Every other non-empty line is appended to the current block body,
136 * with its <b>/<strong> markup preserved for later sanitisation.
137 *
138 * @param string $raw Normalised notice text.
139 * @return array List of array( 'title' => string, 'body' => string ).
140 */
141 private function parse_blocks( $raw ) {
142
143 $lines = explode( "\n", $raw );
144 $blocks = array();
145 $current = null;
146
147 foreach ( $lines as $line ) {
148
149 $trimmed = trim( $line );
150 if ( $trimmed === '' ) {
151 continue;
152 }
153
154 $is_title = ( preg_match( '/^\*\*.+\*\*$/', $trimmed ) === 1 );
155
156 if ( $is_title ) {
157 if ( $current !== null ) {
158 $blocks[] = $current;
159 }
160 $current = array(
161 'title' => trim( strip_tags( str_replace( '**', '', $trimmed ) ) ),
162 'body' => '',
163 );
164 } else {
165 if ( $current === null ) {
166 $current = array(
167 'title' => '',
168 'body' => '',
169 );
170 }
171 $current['body'] .= ( $current['body'] === '' ? '' : ' ' ) . $trimmed;
172 }
173 }
174
175 if ( $current !== null ) {
176 $blocks[] = $current;
177 }
178
179 return $blocks;
180 }
181
182 /**
183 * Build sanitised body HTML.
184 *
185 * Converts markdown [label](url) links and bare URLs into anchors and
186 * leaves inline <b>/<strong> markup in place. The returned string is
187 * passed through wp_kses by the caller, which strips disallowed tags
188 * and any attributes.
189 *
190 * @param string $text Body text (may contain <b>/<strong> and links).
191 * @return string HTML fragment for wp_kses.
192 */
193 private function format_body( $text ) {
194
195 $pattern = '~\[([^\]]+)\]\((https?://[^\s)]+)\)|(https?://[^\s<)]+)~i';
196 $out = '';
197 $offset = 0;
198
199 if ( preg_match_all( $pattern, $text, $matches, PREG_OFFSET_CAPTURE ) ) {
200 foreach ( $matches[0] as $i => $match ) {
201
202 $full = $match[0];
203 $start = $match[1];
204
205 // Plain text before this match (may contain inline bold tags).
206 $out .= substr( $text, $offset, $start - $offset );
207
208 if ( $matches[1][ $i ][1] !== -1 ) {
209 // [label](url)
210 $label = $matches[1][ $i ][0];
211 $url = $matches[2][ $i ][0];
212 } else {
213 // Bare URL - trim trailing sentence punctuation.
214 $url = rtrim( $matches[3][ $i ][0], '.,);:' );
215 $label = $url;
216 }
217
218 $out .= '<a href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">'
219 . $label . '</a>';
220
221 $offset = $start + strlen( $full );
222 }
223 }
224
225 $out .= substr( $text, $offset );
226
227 return $out;
228 }
229 }
230