PluginProbe
Email Post Changes / 0.7
Email Post Changes v0.7
trunk 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.7.1 1.7.2
← All changes | class.email-post-changes.php +152 -624 trunk0.7 View file →
@@ -7,16 +7,13 @@
7 7 var $right_post;
8 8
9 9 var $text_diff;
10 10
11 - var $do_options_capability_checks = false;
12 -
13 11 const ADMIN_PAGE = 'email_post_changes';
14 12 const OPTION_GROUP = 'email_post_changes';
15 13 const OPTION = 'email_post_changes';
16 - const META_KEY = '_epc_user_override';
17 14
18 - static function init() {
15 + function &init() {
19 16 static $instance = null;
20 17
21 18 if ( $instance )
22 19 return $instance;
@@ -26,42 +23,23 @@
26 23 return $instance;
27 24 }
28 25
29 26 function __construct() {
30 - $this->defaults = apply_filters( 'email_post_changes_default_options', array(
31 - 'enable' => 1,
32 - 'users' => array(),
27 + $this->defaults = array(
33 28 'emails' => array( get_option( 'admin_email' ) ),
34 29 'post_types' => array( 'post', 'page' ),
35 30 'drafts' => 0,
36 - ) );
31 + );
37 32
38 - add_action( 'post_updated', array( $this, 'post_updated' ), 10, 3 );
39 - add_action( 'epc_new_bbpress_item', array( $this, 'post_updated' ), 10, 3 ); // Support for bbPress 2
40 -
41 - if ( current_user_can( 'read' ) ) {
42 - add_action( 'admin_menu', array( $this, 'admin_menu' ), 115 );
43 - }
44 -
45 - register_post_meta( '', self::META_KEY, [
46 - 'type' => 'string',
47 - 'description' => 'A per-user, per-post setting determining if the user should receive emails for changes to the post. Values look like `{(int) $user_id}:{(int) $should_receive_changes}.',
48 - 'single' => false,
49 - 'show_in_rest' => false,
50 - 'revisions_enabled' => false,
51 - ] );
33 + add_action( 'wp_insert_post', array( &$this, 'wp_insert_post' ), 10, 2 );
34 + add_action( 'admin_menu', array( &$this, 'admin_menu' ) );
52 35 }
53 36
54 37 function get_post_types() {
55 - $post_types = get_post_types( array( 'public' => true ) );
56 - $_post_types = array();
57 -
58 - foreach ( $post_types as $post_type ) {
59 - if ( post_type_supports( $post_type, 'revisions' ) )
60 - $_post_types[] = $post_type;
61 - }
62 -
63 - return $_post_types;
38 + $post_types = get_post_types();
39 + if ( false !== $pos = array_search( 'revision', $post_types ) )
40 + unset( $post_types[$pos] );
41 + return $post_types;
64 42 }
65 43
66 44 function get_options( $just_defaults = false ) {
67 45 if ( $just_defaults )
@@ -71,133 +49,36 @@
71 49
72 50 return wp_parse_args( $options, $this->defaults );
73 51 }
74 52
75 - /**
76 - * @param int $post_id
77 - * @return array<int, bool> Keys are User IDs, values are whether that user wants to receive changes for the given post.
78 - */
79 - function get_post_overrides( $post_id ) {
80 - $overrides = get_post_meta( $post_id, self::META_KEY, false );
81 -
82 - $return = [];
83 - foreach ( $overrides as $override ) {
84 - [ $user_id, $value ] = explode( ':', $override );
85 - $return[$user_id] = (bool) $value;
86 - }
87 -
88 - return $return;
89 - }
90 -
91 - /**
92 - * @param int $user_id
93 - * @return array<int, bool> Keys are Post IDs, values are whether the given user wants to receive changes for that post.
94 - */
95 - function get_user_overrides( $user_id ) {
96 - $query = new WP_Query;
97 -
98 - $posts = $query->query( [
99 - 'meta_key' => self::META_KEY,
100 - 'meta_value' => "{$user_id}:.*",
101 - 'meta_compare' => 'REGEXP',
102 - 'post_type' => 'any',
103 - 'post_status' => 'any',
104 - 'posts_per_page' => -1,
105 - ] );
106 -
107 - $return = [];
108 - foreach ( $posts as $post ) {
109 - $return[$post->ID] = $this->get_post_user_override( $post->ID, $user_id );
110 - }
111 -
112 - return $return;
113 - }
114 -
115 - function get_post_user_override( $post_id, $user_id ) {
116 - $overrides = $this->get_post_overrides( $post_id );
117 - foreach ( $overrides as $override_user_id => $override ) {
118 - if ( $override_user_id === $user_id ) {
119 - return $override;
120 - }
121 - }
122 -
123 - return null;
124 - }
125 -
126 - /**
127 - * @param int $post_id
128 - * @param int $user_id
129 - * @param bool|null $override Null to delete, bool to set.
130 - */
131 - function set_post_user_override( $post_id, $user_id, $override ) {
132 - $current_override = $this->get_post_user_override( $post_id, $user_id );
133 -
134 - if ( $current_override === $override ) {
135 - return;
136 - }
137 -
138 - if ( null === $override ) {
139 - delete_post_meta( $post_id, self::META_KEY, "{$user_id}:{ (int) $current_override }" );
140 - return;
141 - }
142 -
143 - if ( null === $current_override ) {
144 - add_post_meta( $post_id, self::META_KEY, "{$user_id}:{ $override ? 1 : 0 }", false );
145 - } else {
146 - update_post_meta( $post_id, self::META_KEY, "{$user_id}:{ $override ? 1 : 0 }", "{$user_id}:{ (int) $current_override }" );
147 - }
148 - }
149 -
150 53 // The meat of the plugin
151 - function post_updated( $post_id, $post_after, $post_before ) {
152 - if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
153 - return;
154 - }
155 -
54 + function wp_insert_post( $post_id, $post ) {
156 55 $options = $this->get_options();
157 - if ( ! $options['enable'] ) {
158 - return;
159 - }
160 56
161 - // Transitioning from an Auto Draft to Published shouldn't result in a notification.
162 - if ( $post_before->post_status === 'auto-draft' && $post_after->post_status === 'publish' ) {
57 + if ( !$options['drafts'] && 'draft' == $post->post_status )
163 58 return;
164 - }
165 59
166 - // If we're purely saving a draft, and don't have the draft option enabled, skip. If we're transitioning one way or the other, send a notification.
167 - if ( 0 == $options['drafts'] && in_array( $post_before->post_status, array( 'draft', 'auto-draft', 'pending' ) ) && in_array( $post_after->post_status, array( 'draft', 'pending' ) ) ) {
168 - return;
60 + if ( 'revision' == $post->post_type ) { // Revision is saved first
61 + if ( wp_is_post_autosave( $post ) )
62 + return;
63 + $this->left_post = $post;
64 + } elseif ( !empty( $this->left_post ) && $this->left_post->post_parent == $post->ID ) { // Then new post
65 + if ( !in_array( $post->post_type, $options['post_types'] ) )
66 + return;
67 + $this->right_post = $post;
169 68 }
170 69
171 - if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
70 + if ( !$this->left_post || !$this->right_post )
172 71 return;
173 - }
174 72
175 - if ( !in_array( $post_before->post_type, $options['post_types'] ) ) {
176 - return;
177 - }
178 -
179 - $this->left_post = $post_before;
180 - $this->right_post = $post_after;
181 -
182 - // If this is a new post, set an empty title for $this->left_post so that it appears in the diff.
183 - $child_posts = wp_get_post_revisions( $post_id, array( 'numberposts' => 1 ) );
184 - if ( ( is_countable( $child_posts ) ? count( $child_posts ) : 0 ) == 0 ) {
185 - $this->left_post->post_title = '';
186 - }
187 -
188 - if ( !$this->left_post || !$this->right_post ) {
189 - return;
190 - }
191 -
192 73 $html_diffs = array();
193 74 $text_diffs = array();
194 75 $identical = true;
195 76 foreach ( _wp_post_revision_fields() as $field => $field_title ) {
196 - $left = apply_filters( "_wp_post_revision_field_$field", $this->left_post->$field, $field, $this->left_post, 'from' );
197 - $right = apply_filters( "_wp_post_revision_field_$field", $this->right_post->$field, $field, $this->right_post, 'to' );
77 + $left = apply_filters( "_wp_post_revision_field_$field", $this->left_post->$field, $field );
78 + $right = apply_filters( "_wp_post_revision_field_$field", $this->right_post->$field, $field );
198 79
199 - if ( !$diff = $this->wp_text_diff( $left, $right ) )
80 + if ( !$diff = wp_text_diff( $left, $right ) )
200 81 continue;
201 82 $html_diffs[$field_title] = $diff;
202 83
203 84 $left = normalize_whitespace( $left );
@@ -202,10 +83,10 @@
202 83
203 84 $left = normalize_whitespace( $left );
204 85 $right = normalize_whitespace( $right );
205 86
206 - $left_lines = explode( "\n", $left );
207 - $right_lines = explode( "\n", $right );
87 + $left_lines = split( "\n", $left );
88 + $right_lines = split( "\n", $right );
208 89
209 90 require_once( dirname( __FILE__ ) . '/unified.php' );
210 91
211 92 $text_diff = new Text_Diff( $left_lines, $right_lines );
@@ -214,155 +95,113 @@
214 95
215 96 $identical = false;
216 97 }
217 98
218 - if ( $identical ) {
219 - $this->left_post = null;
220 - $this->right_post = null;
99 + if ( $identical )
221 100 return;
222 - }
223 101
224 102 // Grab the meta data
225 - $the_author = get_the_author_meta( 'display_name', get_current_user_id() ); // The revision
103 + $the_author = get_the_author_meta( 'display_name', $this->left_post->post_author ); // The revision
226 104 $the_title = get_the_title( $this->right_post->ID ); // New title (may be same as old title)
227 - $the_date = gmdate( _x( 'j F, Y \a\t G:i \U\T\C', 'time format for when a post was modified' ), strtotime( $this->right_post->post_modified_gmt . '+0000' ) ); // Modified time
228 - $the_permalink = esc_url( get_permalink( $this->right_post->ID ) );
229 - $the_edit_link = esc_url( get_edit_post_link( $this->right_post->ID ) );
105 + $the_date = gmdate( 'j F, Y \a\t G:i \U\T\C', strtotime( $this->right_post->post_modified_gmt . '+0000' ) ); // Modified time
106 + $the_permalink = clean_url( get_permalink( $this->right_post->ID ) );
107 + $the_edit_link = clean_url( get_edit_post_link( $this->right_post->ID ) );
230 108
231 - // Send email
232 - $charset = apply_filters( 'wp_mail_charset', get_option( 'blog_charset' ) );
233 - $blogname = html_entity_decode( get_option( 'blogname' ), ENT_QUOTES, $charset );
234 - $title = html_entity_decode( $the_title, ENT_QUOTES, $charset );
109 + $left_title = __( 'Revision' );
110 + $right_title = sprintf( __( 'Current %s' ), $post_type = ucfirst( $this->right_post->post_type ) );
235 111
236 - add_action( 'phpmailer_init', array( $this, 'phpmailer_init' ) );
112 + $head_sprintf = __( '%s made the following changes to the %s %s on %s' );
237 113
238 - $overrides = $this->get_post_overrides( $this->right_post->ID );
239 114
240 - $opt_ins = array_keys( array_filter( $overrides ) );
241 - $opt_outs = array_diff( array_keys( $overrides ), $opt_ins );
115 + // HTML
116 + $html_diff_head = '<h2>' . sprintf( __( '%s changed' ), $post_type ) . "</h2>\n";
117 + $html_diff_head .= '<p>' . sprintf( $head_sprintf,
118 + esc_html( $the_author ),
119 + sprintf( _x( '&#8220;%s&#8221; [%s]', '1 = link, 2 = "edit"' ),
120 + "<a href='$the_permalink'>" . esc_html( $the_title ) . '</a>',
121 + "<a href='$the_edit_link'>" . __( 'edit' ) . '</a>'
122 + ),
123 + $this->right_post->post_type,
124 + $the_date
125 + ) . "</p>\n\n";
242 126
243 - $user_ids = array_diff( $options['users'], $opt_outs );
244 - $user_ids = array_merge( $user_ids, $opt_ins );
127 + $html_diff_head .= "<table style='width: 100%; border-collapse: collapse; border: none;'><tr>\n";
128 + $html_diff_head .= "<td style='width: 50%; padding: 0; margin: 0;'>" . esc_html( $left_title ) . ' @ ' . esc_html( $this->left_post->post_date_gmt ) . "</td>\n";
129 + $html_diff_head .= "<td style='width: 50%; padding: 0; margin: 0;'>" . esc_html( $right_title ) . ' @ ' . esc_html( $this->right_post->post_modified_gmt ) . "</td>\n";
130 + $html_diff_head .= "</tr></table>\n\n";
245 131
246 - $user_emails = array();
247 - foreach( $user_ids as $user_id ) {
248 - if ( function_exists( 'is_multisite' ) && is_multisite() ) {
249 - if ( is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
250 - $user_emails[] = get_user_option( 'user_email', $user_id );
251 - }
252 - } else {
253 - if ( $user_email = get_user_option( 'user_email', $user_id ) ) {
254 - $user_emails[] = $user_email;
255 - }
256 - }
132 + $html_diff = '';
133 + foreach ( $html_diffs as $field_title => $diff ) {
134 + $html_diff .= '<h3>' . esc_html( $field_title ) . "</h3>\n";
135 + $html_diff .= "$diff\n\n";
257 136 }
258 137
259 - $emails = array_unique( array_merge( $options['emails'], $user_emails ) );
260 - if ( $emails === [] && apply_filters( 'email_post_changes_admin_email_fallback', true ) ) {
261 - $emails[] = get_option( 'admin_email' );
262 - }
263 - $emails = apply_filters( 'email_post_changes_emails', $emails, $this->left_post->ID, $this->right_post->ID );
138 + $html_diff = rtrim( $html_diff );
264 139
265 - foreach ( $emails as $email ) {
266 - if ( empty( $email ) || ! is_email( $email ) ) {
267 - continue;
268 - }
269 - do_action( 'email_post_changes_before_email_generation', $email );
140 + // Replace classes with inline style
141 + $html_diff = str_replace( "class='diff'", 'style="width: 100%; border-collapse: collapse; border: none; white-space: pre-wrap; word-wrap: break-word; font-family: Consolas,Monaco,Courier,monospace;"', $html_diff );
142 + $html_diff = preg_replace( '#<col[^>]+/?>#i', '', $html_diff );
143 + $html_diff = str_replace( "class='diff-deletedline'", 'style="padding: 5px; width: 50%; background-color: #fdd;"', $html_diff );
144 + $html_diff = str_replace( "class='diff-addedline'", 'style="padding: 5px; width: 50%; background-color: #dfd;"', $html_diff );
145 + $html_diff = str_replace( "class='diff-context'", 'style="padding: 5px; width: 50%;"', $html_diff );
146 + $html_diff = str_replace( '<td>', '<td style="padding: 5px;">', $html_diff );
147 + $html_diff = str_replace( '<del>', '<del style="text-decoration: none; background-color: #f99;">', $html_diff );
148 + $html_diff = str_replace( '<ins>', '<ins style="text-decoration: none; background-color: #9f9;">', $html_diff );
149 + $html_diff = str_replace( array( '</td>', '</tr>', '</tbody>' ), array( "</td>\n", "</tr>\n", "</tbody>\n" ), $html_diff );
270 150
271 - $left_title = __( 'Revision' );
272 - $post_type = get_post_type_object( $this->right_post->post_type );
273 - $post_type_label = $post_type->labels->singular_name;
151 + $html_diff = $html_diff_head . $html_diff;
274 152
275 - /* i18n for a post type can be tricky, let's have presets for the default post types */
276 - if ( $this->right_post->post_type == 'post' ) {
277 - $right_title = __( 'Current post' );
278 - } else if ( $this->right_post->post_type == 'page' ) {
279 - $right_title = __( 'Current page' );
280 - } else {
281 - $right_title = sprintf( _x( 'Current %s', 'post type' ), $post_type_label );
282 - }
283 153
284 - /* translators: 1=username, 2=post title, 3=edit link, 4=post type, 5=date */
285 - $head_sprintf = __( '%1$s made the following changes to the &#8220;%2$s&#8221; %3$s %4$s on %5$s' );
154 + // Refactor some of the meta data for TEXT
155 + $length = max( strlen( $left_title ), strlen( $right_title ) );
156 + $left_title = str_pad( $left_title, $length + 2 );
157 + $right_title = str_pad( $right_title, $length + 2 );
286 158
287 - // HTML
288 - $html_diff_head = '<h2>' . sprintf( esc_html__( '%s changed' ), $post_type_label ) . "</h2>\n";
289 - $html_diff_head .= '<p>' . sprintf( esc_html( $head_sprintf ),
290 - esc_html( $the_author ),
291 - "<a href='$the_permalink'>" . esc_html( $the_title ) . '</a>',
292 - "<a href='$the_edit_link'>" . esc_html__( '[edit]' ) . '</a>',
293 - esc_html( $post_type_label ),
294 - esc_html( $the_date )
295 - ) . "</p>\n\n";
159 + // TEXT
160 + $text_diff = sprintf( $head_sprintf, $the_author, '"' . $the_title . '"', $this->right_post->post_type, $the_date ) . "\n";
161 + $text_diff .= "URL: $the_permalink\n";
162 + $text_diff .= "Edit: $the_edit_link\n\n";
296 163
297 - $html_diff_head .= "<table style='width: 100%; border-collapse: collapse; border: none;'><tr>\n";
298 - $html_diff_head .= "<td style='width: 50%; padding: 0; margin: 0;'>" . esc_html( sprintf( _x( '%1$s @ %2$s', 'post title @ date and time'), $left_title, $this->left_post->post_modified_gmt ) ) . "</td>\n";
299 - $html_diff_head .= "<td style='width: 50%; padding: 0; margin: 0;'>" . esc_html( sprintf( _x( '%1$s @ %2$s', 'post title @ date and time'), $right_title, $this->right_post->post_modified_gmt ) ) . "</td>\n";
300 - $html_diff_head .= "</tr></table>\n\n";
164 + foreach ( $text_diffs as $field_title => $diff ) {
165 + $text_diff .= "$field_title\n";
166 + $text_diff .= "===================================================================\n";
167 + $text_diff .= "--- $left_title ({$this->left_post->post_date_gmt})\n";
168 + $text_diff .= "+++ $right_title ({$this->right_post->post_modified_gmt})\n";
169 + $text_diff .= "$diff\n\n";
170 + }
301 171
302 - $html_diff = '';
303 - foreach ( $html_diffs as $field_title => $diff ) {
304 - $html_diff .= '<h3>' . esc_html( $field_title ) . "</h3>\n";
305 - $html_diff .= "$diff\n\n";
306 - }
172 + $this->text_diff = $text_diff = rtrim( $text_diff );
307 173
308 - $html_diff = rtrim( $html_diff );
309 174
310 - // Replace classes with inline style
311 - $html_diff = str_replace( '<del>', '<del style="text-decoration: none; background-color: #f99;">', $html_diff );
312 - $html_diff = str_replace( '<ins>', '<ins style="text-decoration: none; background-color: #9f9;">', $html_diff );
313 - $html_diff = str_replace( array( '</td>', '</tr>', '</tbody>' ), array( "</td>\n", "</tr>\n", "</tbody>\n" ), $html_diff );
175 + // Send email
176 + $charset = apply_filters( 'wp_mail_charset', get_option( 'blog_charset' ) );
177 + $blogname = html_entity_decode( get_option( 'blogname' ), ENT_QUOTES, $charset );
178 + $title = html_entity_decode( $the_title, ENT_QUOTES, $charset );
314 179
315 - $html_diff = $html_diff_head . $html_diff;
180 + add_action( 'phpmailer_init', array( &$this, 'phpmailer_init_once' ) );
316 181
317 -
318 - // Refactor some of the meta data for TEXT
319 - $length = max( strlen( $left_title ), strlen( $right_title ) );
320 - $left_title = str_pad( $left_title, $length + 2 );
321 - $right_title = str_pad( $right_title, $length + 2 );
322 -
323 - // TEXT
324 - $text_diff = sprintf( $head_sprintf, $the_author, $the_title, '', $post_type_label, $the_date ) . "\n";
325 - $text_diff .= sprintf( __( "URL: %s" ), $the_permalink) . "\n";
326 - $text_diff .= sprintf( __( "Edit: %s" ), $the_edit_link ) . "\n";
327 -
328 - foreach ( $text_diffs as $field_title => $diff ) {
329 - $text_diff .= "$field_title\n";
330 - $text_diff .= "===================================================================\n";
331 - $text_diff .= "--- $left_title ({$this->left_post->post_modified_gmt})\n";
332 - $text_diff .= "+++ $right_title ({$this->right_post->post_modified_gmt})\n";
333 - $text_diff .= "$diff\n\n";
334 - }
335 -
336 - $this->text_diff = $text_diff = rtrim( $text_diff );
337 -
338 - wp_mail(
339 - $email,
340 - /* translators: 1=blogname, 2=post type, 3=post title */
341 - sprintf( __( '[%1$s] %2$s changed: %3$s' ), $blogname, $post_type_label, $title ),
342 - $html_diff
343 - );
344 -
345 - do_action( 'email_post_changes_after_email_sent', $email );
346 - }
347 -
348 - remove_action( 'phpmailer_init', array( $this, 'phpmailer_init' ) );
349 -
350 - do_action( 'email_post_changes_email_sent' );
182 + wp_mail(
183 + null, // see hack in ::phpmailer_init_once()
184 + sprintf( __( '[%s] %s changed: %s' ), $blogname, $post_type, $title ),
185 + $html_diff
186 + );
351 187 }
352 188
353 - function phpmailer_init( &$phpmailer ) {
189 + /* Email hook */
190 + function phpmailer_init_once( &$phpmailer ) {
191 + remove_action( 'phpmailer_init', array( &$this, 'phpmailer_init_once' ) );
354 192 $phpmailer->AltBody = $this->text_diff;
355 193
356 - $author_email = get_the_author_meta( 'email', $this->right_post->post_author );
357 - $author_name = get_the_author_meta( 'display_name', $this->right_post->post_author );
194 + $phpmailer->to = array(); // Hack
358 195
359 - if ( ! empty( $author_email ) && ! empty( $author_name ) ) {
360 - $phpmailer->AddReplyTo(
361 - $author_email,
362 - $author_name
363 - );
364 - }
196 + $options = $this->get_options();
197 + foreach ( $options['emails'] as $email )
198 + $phpmailer->AddAddress( $email );
199 +
200 + $phpmailer->AddReplyTo(
201 + get_the_author_meta( 'email', $this->right_post->post_author ),
202 + get_the_author_meta( 'display_name', $this->right_post->post_author )
203 + );
365 204 }
366 205
367 206 function get_post_type_label( $post_type ) {
368 207 // 2.9
@@ -372,65 +211,24 @@
372 211 // 3.0
373 212 $post_type_object = get_post_type_object( $post_type );
374 213 if ( empty( $post_type_object->label ) )
375 214 return ucwords( str_replace( '_', ' ', $post_type ) );
215 +
376 216 return $post_type_object->label;
377 217 }
378 218
379 - function restrict_options() {
380 - $this->do_options_capability_checks = true;
381 - }
382 -
383 219 /* Admin */
384 - // phpcs:ignore Squiz.Scope.MethodScope.Missing
385 220 function admin_menu() {
386 - $options = $this->get_options();
221 + register_setting( self::OPTION_GROUP, self::OPTION, array( &$this, 'validate_options' ) );
387 222
388 - $current_user_can_manage_options = current_user_can( 'manage_options' );
223 + add_settings_section( self::ADMIN_PAGE, __( 'Email Post Changes' ), array( &$this, 'settings_section' ), self::ADMIN_PAGE );
224 + add_settings_field( self::ADMIN_PAGE . '_emails', __( 'Email Addresses' ), array( &$this, 'emails_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
225 + add_settings_field( self::ADMIN_PAGE . '_post_types', __( 'Post Types' ), array( &$this, 'post_types_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
226 + add_settings_field( self::ADMIN_PAGE . '_drafts', __( 'Drafts' ), array( &$this, 'drafts_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
389 227
390 - if ( ! $current_user_can_manage_options && ! $options['enable'] ) {
391 - return;
392 - }
393 -
394 - add_action( 'load-options.php', [ $this, 'restrict_options' ] );
395 -
396 - register_setting( self::OPTION_GROUP, self::OPTION, [ $this, 'validate_options' ] );
397 -
398 - add_settings_section( self::ADMIN_PAGE, __( 'Email Post Changes' ), array( $this, 'settings_section' ), self::ADMIN_PAGE );
399 - if ( $current_user_can_manage_options ) {
400 - add_settings_field( self::ADMIN_PAGE . '_enable', __( 'Enable' ), array( $this, 'enable_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
401 - }
402 -
403 - if ( current_user_can( 'list_users' ) ) {
404 - add_settings_field( self::ADMIN_PAGE . '_users', __( 'Users to Email' ), array( $this, 'users_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
405 - } else {
406 - add_settings_field( self::ADMIN_PAGE . '_users', __( 'Send me Emails' ), array( $this, 'send_me_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
407 - }
408 -
409 - $overrides_title = __( 'Exceptions' );
410 - if ( $current_user_can_manage_options ) {
411 - $current_user = wp_get_current_user();
412 -
413 - add_settings_field( self::ADMIN_PAGE . '_emails', __( 'Additional Email Addresses' ), array( $this, 'emails_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
414 - add_settings_field( self::ADMIN_PAGE . '_post_types', __( 'Post Types' ), array( $this, 'post_types_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
415 - add_settings_field( self::ADMIN_PAGE . '_drafts', __( 'Drafts' ), array( $this, 'drafts_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
416 - $overrides_title = sprintf( __( 'Exceptions for %s' ), $current_user->display_name );
417 - }
418 -
419 - add_settings_field( self::ADMIN_PAGE . '_exceptions', $overrides_title, array( $this, 'overrides_setting' ), self::ADMIN_PAGE, self::ADMIN_PAGE );
420 -
421 - $hook = add_options_page( __( 'Email Post Changes' ), __( 'Email Post Changes' ), 'read', self::ADMIN_PAGE, array( $this, 'admin_page' ) );
422 - if ( ! $current_user_can_manage_options ) {
423 - add_filter( 'option_page_capability_' . self::ADMIN_PAGE, function() { return 'read'; } );
424 - }
425 - add_action( "admin_head-$hook", array( $this, 'admin_page_head' ) );
228 + add_options_page( __( 'Email Post Changes' ), __( 'Email Post Changes' ), 'manage_options', self::ADMIN_PAGE, array( &$this, 'admin_page' ) );
426 229 }
427 230
428 - // Used in validate_options to array_walk the list of email addresses
429 - function trim_email( &$email, $key ) {
430 - $email = trim( $email );
431 - }
432 -
433 231 function validate_options( $options ) {
434 232 if ( !$options || !is_array( $options ) )
435 233 return $this->defaults;
436 234
@@ -435,230 +233,67 @@
435 233 return $this->defaults;
436 234
437 235 $return = array();
438 236
439 - $previous_options = $this->get_options();
440 -
441 - if ( $this->do_options_capability_checks && ! current_user_can( 'list_users' ) ) {
442 - $return['users'] = $previous_options['users'];
237 + if ( empty( $options['emails'] ) ) {
238 + $return['emails'] = $this->defaults['emails'];
443 239 } else {
444 - // Users setting.
445 - if ( empty( $options['users'] ) || ! is_array( $options['users'] ) ) {
446 - $return['users'] = $this->defaults['users'];
447 - } else {
448 - $return['users'] = $options['users'];
449 - }
240 + if ( is_string( $options['emails'] ) )
241 + $_emails = preg_split( '(\n|\r)', $options['emails'], -1, PREG_SPLIT_NO_EMPTY );
242 + $_emails = array_unique( (array) $_emails );
243 + $emails = array_filter( $_emails, 'is_email' );
244 + if ( $diff = array_diff( $_emails, $emails ) )
245 + $return['invalid_emails'] = $diff;
246 + $return['emails'] = $emails ? $emails : $this->defaults['emails'];
450 247 }
451 248
452 - // Me setting. Only look at this in a form submission to options.php not in some other generic option update.
453 - if ( isset( $options['send_me'] ) && '1' === $options['send_me'] ) {
454 - // Send Me setting.
455 - // The logic is different here: we should only ever see the `me` setting
456 - // when a form is being submitted from the admin page, so
457 - // `do_options_capability_checks` should always be true.
458 - if ( $this->do_options_capability_checks && current_user_can( 'read' ) ) {
459 - $current_user_id = get_current_user_id();
460 -
461 - if ( isset( $options['me'] ) && '1' === $options['me'] ) {
462 - $return['users'][] = $current_user_id;
463 - $return['users'] = array_unique( $return['users'] );
464 - } else {
465 - $return['users'] = array_diff( $return['users'], [ $current_user_id ] );
466 - }
467 - }
468 - }
469 -
470 - if ( $this->do_options_capability_checks && ! current_user_can( 'manage_options' ) ) {
471 - $return['enable'] = $previous_options['enable'];
472 - $return['emails'] = $previous_options['emails'];
473 - $return['post_types'] = $previous_options['post_types'];
474 - $return['drafts'] = $previous_options['drafts'];
249 + if ( empty( $options['post_types'] ) || !is_array( $options ) ) {
250 + $return['post_types'] = $this->defaults['post_types'];
475 251 } else {
476 - // Enable setting.
477 - $return['enable'] = ( empty( $options['enable'] ) ) ? 0 : 1;
478 -
479 - // Email setting.
480 - if ( empty( $options['emails'] ) ) {
481 - if ( is_countable( $return['users'] ) ? count( $return['users'] ) : 0 ) {
482 - $return['emails'] = array();
483 - } else {
484 - $return['emails'] = $this->defaults['emails'];
485 - }
486 - } else {
487 - $_emails = is_string( $options['emails'] ) ? preg_split( '(\n|\r)', $options['emails'], -1, PREG_SPLIT_NO_EMPTY ) : array();
488 - $_emails = array_unique( $_emails );
489 - array_walk( $_emails, array( 'Email_Post_Changes', 'trim_email' ) );
490 - $emails = array_filter( $_emails, 'is_email' );
491 -
492 - $invalid_emails = array_diff( $_emails, $emails );
493 - if ( $invalid_emails )
494 - $return['invalid_emails'] = $invalid_emails;
495 -
496 - if ( $emails ) {
497 - $return['emails'] = $emails;
498 - } elseif ( is_countable( $return['users'] ) ? count( $return['users'] ) : 0 ) {
499 - $return['emails'] = array();
500 - } else {
501 - $return['emails'] = $this->defaults['emails'];
502 - }
503 -
504 - // Don't store a huge list of invalid emails addresses in the option
505 - if ( isset( $return['invalid_emails'] ) && count( $return['invalid_emails'] ) > 200 ) {
506 - $return['invalid_emails'] = array_slice( $return['invalid_emails'], 0, 200 );
507 - $return['invalid_emails'][] = __( 'and many more not listed here' );
508 - }
509 -
510 - // Cap to at max 200 email addresses
511 - if ( ( is_countable( $return['emails'] ) ? count( $return['emails'] ) : 0 ) > 200 ) {
512 - $return['emails'] = array_slice( $return['emails'], 0, 200 );
513 - }
514 - }
515 -
516 - // Post Types setting.
517 - if ( empty( $options['post_types'] ) || ! is_array( $options['post_types'] ) ) {
518 - $return['post_types'] = $this->defaults['post_types'];
519 - } else {
520 - $post_types = array_intersect( $options['post_types'], $this->get_post_types() );
521 - $return['post_types'] = $post_types ? $post_types : $this->defaults['post_types'];
522 - }
523 -
524 - // Drafts setting.
525 - $return['drafts'] = ( empty( $options['drafts'] ) ) ? 0 : 1;
252 + $post_types = array_intersect( $options['post_types'], $this->get_post_types() );
253 + $return['post_types'] = $post_types ? $post_types : $this->defaults['post_types'];
526 254 }
527 255
528 - do_action( 'email_post_changes_validate_options', $this->get_options(), $return );
256 + $return['drafts'] = ( empty( $options['drafts'] ) ) ? 0 : 1;
529 257
530 258 return $return;
531 259 }
532 260
533 - function admin_page_head() {
534 - $options = $this->get_options();
535 - if ( ! empty( $options['invalid_emails'] ) && ! empty( $_REQUEST['settings-updated'] ) ) {
536 - add_settings_error(
537 - 'email_post_changes',
538 - 'email_post_changes',
539 - sprintf(
540 - /* Translators: Comma-separated list of invalid email addresses. */
541 - esc_html( _n( 'Invalid Email: %s', 'Invalid Emails: %s', is_countable( $options['invalid_emails'] ) ? count( $options['invalid_emails'] ) : 0 ) ),
542 - '<kbd>' . implode( '</kbd>, <kbd>', array_map( 'esc_html', $options['invalid_emails'] ) )
543 - )
544 - );
545 - }
546 -
547 -?>
548 -<style>
549 -.epc-registered-user-selection {
550 - overflow: auto;
551 - max-height: 300px;
552 - max-width: 40em;
553 - border: 1px solid #ccc;
554 - background-color: #fafafa;
555 - padding: 12px;
556 - box-sizing: border-box;
557 -}
558 -.epc-registered-user-selection ul {
559 - margin: 0;
560 - padding: 0;
561 -}
562 -.epc-additional-emails {
563 - width: 40em;
564 -}
565 -</style>
566 -<?php
567 - }
568 -
569 - // phpcs:ignore Squiz.Scope.MethodScope.Missing
570 261 function admin_page() {
571 262 $options = $this->get_options();
572 - ?>
573 -
574 - <div class="wrap">
575 - <h1><?php esc_html_e( 'Email Post Changes' ); ?></h1>
576 -
577 - <form action="options.php" method="post">
578 - <?php settings_fields( self::OPTION_GROUP ); ?>
579 - <?php do_settings_sections( self::ADMIN_PAGE ); ?>
580 - <p class="submit">
581 - <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Save Changes' ); ?>" />
582 - </p>
583 - </form>
584 - </div>
585 -
586 - <?php
587 - }
588 -
589 - function settings_section() {
590 - if ( current_user_can( 'manage_options' ) ) {
591 - return;
592 - }
593 -
594 - $options = $this->get_options();
595 -
596 - $labels = array_map( [ $this, 'get_post_type_label' ], $options['post_types'] );
597 - ?>
598 -
599 - <p><?php echo esc_html( wp_sprintf( __( 'This site can send you an email whenever changes to %l that you have access to are made.' ), $labels ) ); ?></p>
600 - <?php
601 - }
602 -
603 - function enable_setting() {
604 - $options = $this->get_options();
605 - ?>
606 -
607 - <p><label><input type="checkbox" name="email_post_changes[enable]" value="1"<?php checked( $options['enable'], 1 ); ?> /> <?php esc_html_e( 'Send an email when a post or page changes.' ); ?></label></p>
608 -
609 - <?php
610 - }
611 -
612 - function users_setting() {
613 - $options = $this->get_options();
614 263 ?>
615 - <div class="epc-registered-user-selection">
616 - <ul>
617 - <?php
618 - $users = get_users(
619 - array(
620 - 'fields' => array(
621 - 'ID',
622 - 'display_name',
623 - 'user_login',
624 - 'user_email',
625 - ),
626 - )
627 - );
628 264
629 - usort( $users, array( $this, 'sort_users_by_display_name' ) );
265 +<div class="wrap">
266 + <h2><?php _e( 'Email Post Changes' ); ?></h2>
267 +<?php if ( !empty( $options['invalid_emails'] ) ) : ?>
630 268
631 - foreach ( $users as $user ) : ?>
632 - <li><label><input type="checkbox" name="email_post_changes[users][]" value="<?php echo (int) $user->ID; ?>"<?php checked( in_array( $user->ID, $options['users'] ) ); ?> /> <?php echo esc_html( $user->display_name ); ?> ( <?php echo esc_html( $user->user_login ); ?> - <?php echo esc_html( $user->user_email ); ?> )</label></li>
269 + <div class="error">
270 + <p><?php printf( _n( 'Invalid Email: %s', 'Invalid Emails: %s', count( $options['invalid_emails'] ) ), '<kbd>' . join( '</kbd>, <kbd>', array_map( 'esc_html', $options['invalid_emails'] ) ) ); ?></p>
271 + </div>
272 +<?php endif; ?>
633 273
634 -<?php endforeach; ?>
635 - </ul>
636 - </div>
637 -<?php
638 - }
639 -
640 - function send_me_setting() {
641 - $options = $this->get_options();
642 -
643 - $is_active = in_array( get_current_user_id(), $options['users'], false );
644 -?>
645 - <p>
646 - <label><input type="checkbox" name="email_post_changes[me]" value="1"<?php checked( $is_active ); ?> /> <?php esc_html_e( 'Email me changes.' ); ?></label>
647 - <input type="hidden" name="email_post_changes[send_me]" value="1" />
274 + <form action="options.php" method="post">
275 + <?php settings_fields( self::OPTION_GROUP ); ?>
276 + <?php do_settings_sections( self::ADMIN_PAGE ); ?>
277 + <p class="submit">
278 + <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Save Changes' ); ?>" />
648 279 </p>
280 + </form>
281 +</div>
649 282 <?php
650 283 }
651 284
652 - function sort_users_by_display_name( $a, $b ) {
653 - return strcmp( strtolower( $a->display_name ), strtolower( $b->display_name ) );
654 - }
285 + function settings_section() {} // stub
655 286
656 287 function emails_setting() {
657 288 $options = $this->get_options();
658 289 ?>
659 - <textarea class="epc-additional-emails" rows="4" cols="40" name="email_post_changes[emails]"><?php echo esc_html( implode( "\n", $options['emails'] ) ); ?></textarea>
660 - <p class="description"><?php esc_html_e( 'One email address per line.' ); ?></p>
290 + <textarea rows="4" cols="40" style="width: 40em;" name="email_post_changes[emails]"><?php echo esc_html( join( "\n", $options['emails'] ) ); ?></textarea>
291 + <p class="description"><?php _e( 'Send emails to these addresses. One per line.' ); ?></p>
292 + <p class="description"><?php printf(
293 + __( "If blank, send emails to this site&#8217;s <a href='%s'>admin email address</a>." ),
294 + clean_url( get_bloginfo( 'wpurl' ) . '/wp-admin/options-general.php#admin_email' )
295 + ); ?></p>
661 296 <?php
662 297 }
663 298
664 299 function post_types_setting() {
@@ -663,15 +298,20 @@
663 298
664 299 function post_types_setting() {
665 300 $options = $this->get_options();
666 301 ?>
302 +
667 303 <ul>
668 -<?php foreach ( $this->get_post_types() as $post_type ) :
304 +<?php
305 + foreach ( $this->get_post_types() as $post_type ) :
669 306 $label = $this->get_post_type_label( $post_type );
670 307 ?>
671 308 <li><label><input type="checkbox" name="email_post_changes[post_types][]" value="<?php echo esc_attr( $post_type ); ?>"<?php checked( in_array( $post_type, $options['post_types'] ) ); ?> /> <?php echo esc_html( $label ); ?></label></li>
672 309 <?php endforeach; ?>
310 +
673 311 </ul>
312 + <p class="description"><?php _e( 'Send emails when a post of these post types changes.' ); ?></p>
313 + <p class="description"><?php _e( 'If none are selected, &#8220;post&#8221; and &#8220;page&#8221; will be checked by default.' ); ?></p>
674 314 <?php
675 315 }
676 316
677 317 function drafts_setting() {
@@ -676,121 +316,9 @@
676 316
677 317 function drafts_setting() {
678 318 $options = $this->get_options();
679 319 ?>
680 - <p><label><input type="checkbox" name="email_post_changes[drafts]" value="1"<?php checked( $options['drafts'], 1 ); ?> /> <?php esc_html_e( 'Email changes to drafts, not just published items.' ); ?></label></p>
320 +
321 + <p><label><input type="checkbox" name="email_post_changes[drafts]" value="1"<?php checked( $options['drafts'], 1 ); ?> /> <?php _e( 'Email changes to drafts, not just published items.' ); ?></label></p>
681 322 <?php
682 323 }
683 -
684 - function overrides_setting() {
685 - $options = $this->get_options();
686 -
687 - $is_active = in_array( get_current_user_id(), $options['users'], false );
688 -
689 - $labels = array_map( [ $this, 'get_post_type_label' ], $options['post_types'] );
690 -
691 - ?>
692 - <p class="description"><?php echo esc_html(
693 - wp_sprintf(
694 - __( 'If you opt in your acount above to receive changes, you may exclude specific %1$l below. If you opt out above, you may include specific %1$l below.' ),
695 - $labels
696 - )
697 - ); ?></p>
698 - <ul>
699 -
700 - <?php // TODO - real UI
701 - foreach ( $this->get_user_overrides( get_current_user_id() ) as $post_id => $override ) {
702 - if ( $override === $is_active ) {
703 - continue;
704 - }
705 - ?>
706 - <li><?php echo get_the_title( $post_id ); ?></li>
707 - <?php
708 - }
709 - }
710 -
711 - function wp_text_diff( $left_string, $right_string, $args = null ) {
712 - $defaults = array( 'title' => '', 'title_left' => '', 'title_right' => '' );
713 - $args = wp_parse_args( $args, $defaults );
714 -
715 - $left_string = normalize_whitespace( $left_string );
716 - $right_string = normalize_whitespace( $right_string );
717 - $left_lines = explode( "\n", $left_string );
718 - $right_lines = explode( "\n", $right_string );
719 -
720 - $text_diff = new Text_Diff( $left_lines, $right_lines );
721 - $renderer = new Email_Post_Changes_Diff();
722 - $diff = $renderer->render( $text_diff );
723 -
724 - if ( !$diff )
725 - return '';
726 -
727 - $r = "<table style='width: 100%; border-collapse: collapse; border: none; white-space: pre-wrap; word-wrap: break-word; font-family: Consolas,Monaco,Courier,monospace;'>\n";
728 -
729 - if ( $args['title'] || $args['title_left'] || $args['title_right'] )
730 - $r .= "<thead>\n";
731 - if ( $args['title'] )
732 - $r .= "<tr class='diff-title'><th colspan='4'>" . esc_html( $args['title'] ) . "</th></tr>\n";
733 - if ( $args['title_left'] || $args['title_right'] ) {
734 - $r .= "<tr class='diff-sub-title'>\n";
735 - $r .= "\t<td></td><th>" . esc_html( $args['title_left'] ) . "</th>\n";
736 - $r .= "\t<td></td><th>" . esc_html( $args['title_right'] ) . "</th>\n";
737 - $r .= "</tr>\n";
738 - }
739 - if ( $args['title'] || $args['title_left'] || $args['title_right'] )
740 - $r .= "</thead>\n";
741 - $r .= "<tbody>\n$diff\n</tbody>\n";
742 - $r .= "</table>";
743 - return $r;
744 - }
745 324 }
746 -
747 -if ( !class_exists( 'WP_Text_Diff_Renderer_Table' ) )
748 - require( ABSPATH . WPINC . '/wp-diff.php' );
749 -
750 -class Email_Post_Changes_Diff extends WP_Text_Diff_Renderer_Table {
751 - var $_leading_context_lines = 2;
752 - var $_trailing_context_lines = 2;
753 -
754 - /**
755 - * @ignore
756 - *
757 - * @param string $line HTML-escape the value.
758 - * @return string
759 - */
760 - public function addedLine( $line ) {
761 - return "<td style='padding: 5px; width: 1em; text-align: end;' aria-label='" . __( 'Added:' ) . "'><span aria-hidden='true'>+</span></td>"
762 - . "<td style='padding: 5px; width: 50%; width: calc( 50% - 1em - 10px ); background-color: #dfd;'>{$line}</td>";
763 - }
764 -
765 - /**
766 - * @ignore
767 - *
768 - * @param string $line HTML-escape the value.
769 - * @return string
770 - */
771 - public function deletedLine( $line ) {
772 - return "<td style='padding: 5px; width: 1em; text-align: end;' aria-label='" . __( 'Deleted:' ) . "'><span aria-hidden='true'>-</span></td>"
773 - . "<td style='padding: 5px; width: 50%; width: calc( 50% - 1em - 10px ); background-color: #fdd;'>{$line}</td>";
774 - }
775 -
776 - /**
777 - * @ignore
778 - *
779 - * @param string $line HTML-escape the value.
780 - * @return string
781 - */
782 - public function contextLine( $line ) {
783 - return "<td style='padding: 5px; width: 1em;' aria-label='" . __( 'Unchanged:' ) . "'><span aria-hidden='true'>&nbsp;</span></td>"
784 - . "<td style='padding: 5px; width: 50%; width: calc( 50% - 1em - 10px );'>{$line}</td>";
785 - }
786 -
787 - /**
788 - * @ignore
789 - *
790 - * @return string
791 - */
792 - public function emptyLine() {
793 - return '<td colspan="2" style="padding: 5px;">&nbsp;</td>';
794 - }
795 -}
796 -