PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.7.13
Post Views Counter v1.7.13
1.7.13 1.7.12 1.7.11 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.2 1.3.2.1 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9
post-views-counter / includes / class-settings-emails.php
post-views-counter / includes Last commit date
class-admin.php 2 weeks ago class-columns-modal.php 2 weeks ago class-columns.php 2 weeks ago class-counter.php 2 weeks ago class-crawler-detect.php 2 weeks ago class-cron.php 2 weeks ago class-dashboard.php 2 weeks ago class-emails-mailer.php 2 weeks ago class-emails-period.php 2 weeks ago class-emails-query.php 2 weeks ago class-emails-scheduler.php 2 weeks ago class-emails-template.php 2 weeks ago class-emails.php 2 weeks ago class-frontend.php 2 weeks ago class-functions.php 2 weeks ago class-import.php 2 weeks ago class-integration-gutenberg.php 2 weeks ago class-integrations.php 2 weeks ago class-query.php 2 weeks ago class-settings-api.php 2 weeks ago class-settings-display.php 2 weeks ago class-settings-emails.php 2 weeks ago class-settings-general.php 2 weeks ago class-settings-integrations.php 2 weeks ago class-settings-other.php 2 weeks ago class-settings-reports.php 2 weeks ago class-settings.php 2 weeks ago class-toolbar.php 2 weeks ago class-traffic-signals.php 2 weeks ago class-update.php 2 weeks ago class-widgets.php 2 weeks ago functions.php 2 weeks ago
class-settings-emails.php
942 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Post_Views_Counter_Settings_Emails class.
8 *
9 * @class Post_Views_Counter_Settings_Emails
10 */
11 class Post_Views_Counter_Settings_Emails {
12
13 /**
14 * @var Post_Views_Counter
15 */
16 private $pvc;
17
18 /**
19 * Class constructor.
20 *
21 * @return void
22 */
23 public function __construct() {
24 $this->pvc = Post_Views_Counter();
25 add_action( 'wp_ajax_pvc_send_test_email', [ $this, 'handle_test_email_ajax' ] );
26 }
27
28 /**
29 * Get sections.
30 *
31 * @return array
32 */
33 public function get_sections() {
34 return [
35 'post_views_counter_emails_summary' => [
36 'tab' => 'emails',
37 'title' => __( 'Email Summaries', 'post-views-counter' ),
38 'callback' => [ $this, 'section_summary' ]
39 ],
40 'post_views_counter_emails_recipients' => [
41 'tab' => 'emails',
42 'title' => __( 'Email Recipients', 'post-views-counter' ),
43 'callback' => [ $this, 'section_recipients' ]
44 ],
45 'post_views_counter_emails_template' => [
46 'tab' => 'emails',
47 'title' => __( 'Email Template', 'post-views-counter' ),
48 'callback' => [ $this, 'section_template' ]
49 ],
50 'post_views_counter_emails_test' => [
51 'tab' => 'emails',
52 'title' => __( 'Test Email', 'post-views-counter' ),
53 'callback' => [ $this, 'section_test' ]
54 ],
55 'post_views_counter_emails_status' => [
56 'tab' => 'emails',
57 'title' => __( 'Delivery & Status', 'post-views-counter' ),
58 'callback' => [ $this, 'section_status' ]
59 ]
60 ];
61 }
62
63 /**
64 * Get fields.
65 *
66 * @return array
67 */
68 public function get_fields() {
69 return [
70 'enabled' => [
71 'field_key' => 'enabled',
72 'tab' => 'emails',
73 'title' => __( 'Email Summary', 'post-views-counter' ),
74 'section' => 'post_views_counter_emails_summary',
75 'type' => 'boolean',
76 'label' => __( 'Send a summary of tracked content views to the selected recipient.', 'post-views-counter' )
77 ],
78 'enabled_frequencies' => [
79 'tab' => 'emails',
80 'title' => __( 'Summary Frequency', 'post-views-counter' ),
81 'section' => 'post_views_counter_emails_summary',
82 'type' => 'checkbox',
83 'class' => 'pvc-pro',
84 'disabled' => true,
85 'skip_saving' => true,
86 'pro_only' => true,
87 'value' => [ 'weekly' ],
88 'options' => [
89 'daily' => __( 'Daily', 'post-views-counter' ),
90 'weekly' => __( 'Weekly', 'post-views-counter' ),
91 'monthly' => __( 'Monthly', 'post-views-counter' )
92 ],
93 'description' => __( 'Choose how often views tracking summaries are sent.', 'post-views-counter' )
94 ],
95 'min_views_threshold' => [
96 'field_key' => 'min_views_threshold',
97 'tab' => 'emails',
98 'title' => __( 'Minimum Tracked Views', 'post-views-counter' ),
99 'section' => 'post_views_counter_emails_summary',
100 'type' => 'number',
101 'min' => 0,
102 'max' => 1000000,
103 'description' => __( 'Do not send the summary if the report period has fewer than this many tracked views.', 'post-views-counter' ),
104 'validate' => [ $this, 'validate_min_views_threshold' ]
105 ],
106 'max_top_items' => [
107 'field_key' => 'max_top_items',
108 'tab' => 'emails',
109 'title' => __( 'Top Content Items', 'post-views-counter' ),
110 'section' => 'post_views_counter_emails_summary',
111 'type' => 'number',
112 'min' => 3,
113 'max' => 10,
114 'description' => __( 'Choose how many top content items are included in each summary.', 'post-views-counter' ),
115 'validate' => [ $this, 'validate_max_top_items' ]
116 ],
117 'include_top_gainers_decliners' => [
118 'tab' => 'emails',
119 'title' => __( 'Top Gainers and Decliners', 'post-views-counter' ),
120 'section' => 'post_views_counter_emails_summary',
121 'type' => 'boolean',
122 'class' => 'pvc-pro',
123 'disabled' => true,
124 'skip_saving' => true,
125 'pro_only' => true,
126 'value' => false,
127 'label' => __( 'Show which content is gaining or losing momentum compared with previous periods.', 'post-views-counter' ),
128 'description' => __( 'When matching data is available, this appears in the Traffic signals section if the email template includes %%traffic_signals%%.', 'post-views-counter' )
129 ],
130 'include_author_summary' => [
131 'tab' => 'emails',
132 'title' => __( 'Author Summary', 'post-views-counter' ),
133 'section' => 'post_views_counter_emails_summary',
134 'type' => 'boolean',
135 'class' => 'pvc-pro',
136 'disabled' => true,
137 'skip_saving' => true,
138 'pro_only' => true,
139 'value' => false,
140 'label' => __( 'Include the authors whose content received the most tracked views during the report period.', 'post-views-counter' ),
141 'description' => __( 'When matching data is available, this appears in the Report summary section if the email template includes %%report_summary%%.', 'post-views-counter' )
142 ],
143 'include_source_summary' => [
144 'tab' => 'emails',
145 'title' => __( 'Source Summary', 'post-views-counter' ),
146 'section' => 'post_views_counter_emails_summary',
147 'type' => 'boolean',
148 'class' => 'pvc-pro',
149 'disabled' => true,
150 'skip_saving' => true,
151 'pro_only' => true,
152 'value' => false,
153 'label' => __( 'Include top referrers or source dimensions when source tracking is available.', 'post-views-counter' ),
154 'description' => __( 'When source tracking data is available, this appears in the Report summary section if the email template includes %%report_summary%%.', 'post-views-counter' )
155 ],
156 'recipient' => [
157 'field_key' => 'recipient',
158 'tab' => 'emails',
159 'title' => __( 'Recipient Email', 'post-views-counter' ),
160 'section' => 'post_views_counter_emails_recipients',
161 'type' => 'input',
162 'subclass' => 'regular-text',
163 'description' => __( 'The email address that receives summary emails. Defaults to the site administrator email.', 'post-views-counter' ),
164 'validate' => [ $this, 'validate_recipient' ]
165 ],
166 'additional_recipients' => [
167 'tab' => 'emails',
168 'title' => __( 'Additional Recipients', 'post-views-counter' ),
169 'section' => 'post_views_counter_emails_recipients',
170 'type' => 'boolean',
171 'class' => 'pvc-pro',
172 'disabled' => true,
173 'skip_saving' => true,
174 'pro_only' => true,
175 'value' => false,
176 'label' => __( 'Send summaries to multiple people with CC and BCC recipients.', 'post-views-counter' )
177 ],
178 'cc_recipients' => [
179 'tab' => 'emails',
180 'title' => __( 'CC Recipients', 'post-views-counter' ),
181 'section' => 'post_views_counter_emails_recipients',
182 'type' => 'input',
183 'subclass' => 'regular-text',
184 'class' => 'pvc-pro',
185 'disabled' => true,
186 'skip_saving' => true,
187 'pro_only' => true,
188 'value' => '',
189 'logic' => [
190 'field' => 'additional_recipients',
191 'operator' => 'is',
192 'value' => 'true'
193 ],
194 'animation' => 'slide',
195 'description' => __( 'Send visible copies to additional recipients.', 'post-views-counter' )
196 ],
197 'bcc_recipients' => [
198 'tab' => 'emails',
199 'title' => __( 'BCC Recipients', 'post-views-counter' ),
200 'section' => 'post_views_counter_emails_recipients',
201 'type' => 'input',
202 'subclass' => 'regular-text',
203 'class' => 'pvc-pro',
204 'disabled' => true,
205 'skip_saving' => true,
206 'pro_only' => true,
207 'value' => '',
208 'logic' => [
209 'field' => 'additional_recipients',
210 'operator' => 'is',
211 'value' => 'true'
212 ],
213 'animation' => 'slide',
214 'description' => __( 'Send private copies to additional recipients.', 'post-views-counter' )
215 ],
216 'email_subject_template' => [
217 'field_key' => 'email_subject_template',
218 'tab' => 'emails',
219 'title' => __( 'Email Subject', 'post-views-counter' ),
220 'section' => 'post_views_counter_emails_template',
221 'type' => 'input',
222 'subclass' => 'regular-text',
223 'description' => $this->get_subject_template_description(),
224 'validate' => [ $this, 'validate_template_text' ]
225 ],
226 'email_body_template' => [
227 'field_key' => 'email_body_template',
228 'tab' => 'emails',
229 'title' => __( 'Email Content', 'post-views-counter' ),
230 'section' => 'post_views_counter_emails_template',
231 'type' => 'editor',
232 'rows' => 10,
233 'description' => $this->get_content_template_description(),
234 'validate' => [ $this, 'validate_template_text' ]
235 ],
236 'test_recipient' => [
237 'field_key' => 'test_recipient',
238 'tab' => 'emails',
239 'title' => __( 'Send Test To', 'post-views-counter' ),
240 'section' => 'post_views_counter_emails_test',
241 'type' => 'custom',
242 'callback' => [ $this, 'setting_test_email_scaffold' ],
243 'validate' => [ $this, 'validate_test_recipient' ]
244 ],
245 'schedule_status' => [
246 'tab' => 'emails',
247 'title' => __( 'Schedule Status', 'post-views-counter' ),
248 'section' => 'post_views_counter_emails_status',
249 'type' => 'info',
250 'text' => __( 'Weekly summaries are scheduled for Monday after the previous full week has ended.', 'post-views-counter' ),
251 'description' => __( 'Summary emails are scheduled with WordPress cron. Delivery time may vary depending on site traffic and hosting configuration.', 'post-views-counter' ),
252 'skip_saving' => true
253 ],
254 'next_scheduled_email' => [
255 'tab' => 'emails',
256 'title' => __( 'Next Scheduled Email', 'post-views-counter' ),
257 'section' => 'post_views_counter_emails_status',
258 'type' => 'info',
259 'text' => $this->get_next_scheduled_text(),
260 'skip_saving' => true
261 ],
262 'last_sent_status' => [
263 'tab' => 'emails',
264 'title' => __( 'Last Sent', 'post-views-counter' ),
265 'section' => 'post_views_counter_emails_status',
266 'type' => 'info',
267 'text' => $this->get_last_sent_text(),
268 'skip_saving' => true
269 ],
270 'last_error_status' => [
271 'tab' => 'emails',
272 'title' => __( 'Last Error', 'post-views-counter' ),
273 'section' => 'post_views_counter_emails_status',
274 'type' => 'info',
275 'text' => $this->get_last_error_text(),
276 'skip_saving' => true
277 ]
278 ];
279 }
280
281 /**
282 * Section description: summary.
283 *
284 * @return void
285 */
286 public function section_summary() {
287 echo '<p class="description">' . esc_html__( 'Send email summaries with your most viewed content and view totals.', 'post-views-counter' ) . '</p>';
288 }
289
290 /**
291 * Section description: recipients.
292 *
293 * @return void
294 */
295 public function section_recipients() {
296 echo '<p class="description">' . esc_html__( 'Choose who should receive your email summaries.', 'post-views-counter' ) . '</p>';
297 }
298
299 /**
300 * Section description: status.
301 *
302 * @return void
303 */
304 public function section_status() {
305 echo '<p class="description">' . esc_html__( 'Review scheduling, delivery, and recent email status information.', 'post-views-counter' ) . '</p>';
306 }
307
308 /**
309 * Section description: template.
310 *
311 * @return void
312 */
313 public function section_template() {
314 echo '<p class="description">' . esc_html__( 'Customize the subject and message used for content views summaries.', 'post-views-counter' ) . '</p>';
315 }
316
317 /**
318 * Section description: test.
319 *
320 * @return void
321 */
322 public function section_test() {
323 echo $this->get_test_email_notice_container_html();
324 echo '<p class="description">' . esc_html__( 'Send a test using the latest available summary. This does not change the saved recipient or scheduled delivery.', 'post-views-counter' ) . '</p>';
325 }
326
327 /**
328 * Setting: test email scaffold.
329 *
330 * @return string
331 */
332 public function setting_test_email_scaffold( $field ) {
333 $default_recipient = $this->get_test_email_default_recipient();
334 $input_name = ! empty( $field['name'] ) ? $field['name'] : 'post_views_counter_settings_emails[test_recipient]';
335 $nonce = wp_create_nonce( 'pvc_send_test_email_ajax' );
336 $html = '<div class="pvc-email-test-scaffold">';
337 $html .= '<p><input id="pvc-test-email-recipient" type="email" class="regular-text" name="' . esc_attr( $input_name ) . '" value="' . esc_attr( $default_recipient ) . '" placeholder="' . esc_attr__( 'email@example.com', 'post-views-counter' ) . '" aria-label="' . esc_attr__( 'Test email recipient', 'post-views-counter' ) . '" /></p>';
338 $html .= '<p><button type="button" class="button outline pvc-send-test-email" data-pvc-email-test-nonce="' . esc_attr( $nonce ) . '" data-default-label="' . esc_attr__( 'Send Test Email', 'post-views-counter' ) . '" data-sending-label="' . esc_attr__( 'Sending...', 'post-views-counter' ) . '">' . esc_html__( 'Send Test Email', 'post-views-counter' ) . '</button></p>';
339 $html .= '</div>';
340
341 return $html;
342 }
343
344 /**
345 * Handle a one-off Ajax test email request.
346 *
347 * @return void
348 */
349 public function handle_test_email_ajax() {
350 $capability = apply_filters( 'pvc_settings_capability', 'manage_options' );
351
352 if ( ! current_user_can( $capability ) ) {
353 wp_send_json_error(
354 [
355 'code' => 'forbidden',
356 'message' => __( 'Sorry, you are not allowed to do that.', 'post-views-counter' )
357 ],
358 403
359 );
360 }
361
362 check_ajax_referer( 'pvc_send_test_email_ajax', 'nonce' );
363
364 $recipient = isset( $_POST['pvc_test_email_recipient'] ) ? sanitize_email( wp_unslash( $_POST['pvc_test_email_recipient'] ) ) : '';
365 $result = $this->send_test_email_request( $recipient );
366
367 if ( ! empty( $result['success'] ) ) {
368 wp_send_json_success(
369 [
370 'message' => $result['message']
371 ]
372 );
373 }
374
375 wp_send_json_error(
376 [
377 'code' => $result['code'],
378 'message' => $result['message']
379 ],
380 $this->get_test_email_status_code( $result['code'] )
381 );
382 }
383
384 /**
385 * Run a one-off test email request.
386 *
387 * @param string $recipient
388 * @return array
389 */
390 private function send_test_email_request( $recipient ) {
391 if ( ! is_email( $recipient ) ) {
392 return [
393 'success' => false,
394 'code' => 'invalid_recipient',
395 'message' => __( 'Enter a valid test recipient email address.', 'post-views-counter' )
396 ];
397 }
398
399 $base_settings = isset( $this->pvc->options['emails'] ) && is_array( $this->pvc->options['emails'] ) ? $this->pvc->options['emails'] : [];
400 $test_settings = array_merge(
401 $base_settings,
402 [
403 'enabled' => true,
404 'recipient' => $recipient,
405 'min_views_threshold' => 0,
406 'send_empty_reports' => true
407 ]
408 );
409 $emails = new Post_Views_Counter_Emails();
410 $rendered = $emails->render_weekly_summary( null, [ 'settings' => $test_settings ] );
411 $data = isset( $rendered['data'] ) && is_array( $rendered['data'] ) ? $rendered['data'] : [];
412
413 if ( ! $this->has_real_test_email_data( $data ) ) {
414 return [
415 'success' => false,
416 'code' => 'not_enough_data',
417 'message' => __( 'There is not enough tracked data from the latest complete week to send a real test summary yet.', 'post-views-counter' )
418 ];
419 }
420
421 $mailer = new Post_Views_Counter_Emails_Mailer( $emails );
422 $result = $mailer->send_rendered_summary(
423 $rendered,
424 [
425 'recipient' => $recipient,
426 'is_test' => true,
427 'source' => 'test',
428 'summary_type' => 'weekly',
429 'settings' => $test_settings
430 ]
431 );
432
433 if ( ! empty( $result['sent'] ) ) {
434 return [
435 'success' => true,
436 'code' => 'sent',
437 'message' => __( 'The summary test email was sent successfully.', 'post-views-counter' )
438 ];
439 }
440
441 return [
442 'success' => false,
443 'code' => 'send_failed',
444 'message' => ! empty( $result['error'] )
445 ? sprintf( __( 'The test email could not be sent. %s', 'post-views-counter' ), sanitize_text_field( $result['error'] ) )
446 : __( 'The test email could not be sent. Check your WordPress mail configuration and try again.', 'post-views-counter' )
447 ];
448 }
449
450 /**
451 * Validate recipient.
452 *
453 * @param string $value
454 * @param array $field
455 * @return string
456 */
457 public function validate_recipient( $value, $field ) {
458 $recipient = $this->prepare_single_email_recipient( $value );
459 $enabled = $this->is_emails_enabled_in_request();
460 $previous = $this->get_previous_value( $field, $this->pvc->get_default_emails_settings()['recipient'] );
461
462 if ( ! $recipient['is_valid'] ) {
463 add_settings_error( 'post_views_counter_settings_emails', 'pvc_emails_recipient_invalid', __( 'Enter a valid recipient email address for the views summary email.', 'post-views-counter' ), 'error' );
464
465 return $previous;
466 }
467
468 if ( $enabled && $recipient['is_empty'] ) {
469 add_settings_error( 'post_views_counter_settings_emails', 'pvc_emails_recipient_required', __( 'Recipient email is required while the views summary email is enabled.', 'post-views-counter' ), 'error' );
470
471 return $previous;
472 }
473
474 return $recipient['email'];
475 }
476
477 /**
478 * Validate saved test recipient.
479 *
480 * @param array $input
481 * @param array $field
482 * @return array
483 */
484 public function validate_test_recipient( $input, $field ) {
485 $input = is_array( $input ) ? $input : [];
486 $field_key = ! empty( $field['field_key'] ) ? sanitize_key( $field['field_key'] ) : 'test_recipient';
487 $previous = sanitize_email( (string) $this->get_previous_value( $field, '' ) );
488 $recipient = $this->prepare_single_email_recipient( isset( $input[$field_key] ) ? $input[$field_key] : '' );
489
490 if ( ! is_email( $previous ) )
491 $previous = '';
492
493 if ( ! $recipient['is_valid'] ) {
494 add_settings_error( 'post_views_counter_settings_emails', 'pvc_emails_test_recipient_invalid', __( 'Enter a valid test recipient email address.', 'post-views-counter' ), 'error' );
495
496 $input[$field_key] = $previous;
497
498 return $input;
499 }
500
501 $input[$field_key] = $recipient['email'];
502
503 return $input;
504 }
505
506 /**
507 * Prepare a single recipient email value.
508 *
509 * @param mixed $value
510 * @return array
511 */
512 private function prepare_single_email_recipient( $value ) {
513 if ( is_array( $value ) ) {
514 return [
515 'email' => '',
516 'is_empty' => false,
517 'is_valid' => false
518 ];
519 }
520
521 $raw_value = trim( (string) $value );
522
523 if ( $raw_value === '' ) {
524 return [
525 'email' => '',
526 'is_empty' => true,
527 'is_valid' => true
528 ];
529 }
530
531 if ( preg_match( '/[,;\r\n]|\s/', $raw_value ) ) {
532 return [
533 'email' => '',
534 'is_empty' => false,
535 'is_valid' => false
536 ];
537 }
538
539 $email = sanitize_email( $raw_value );
540
541 return [
542 'email' => $email,
543 'is_empty' => false,
544 'is_valid' => (bool) is_email( $email )
545 ];
546 }
547
548 /**
549 * Validate minimum views threshold.
550 *
551 * @param mixed $value
552 * @param array $field
553 * @return int
554 */
555 public function validate_min_views_threshold( $value, $field ) {
556 $threshold = (int) $value;
557
558 if ( $threshold < 0 ) {
559 add_settings_error( 'post_views_counter_settings_emails', 'pvc_emails_threshold_invalid', __( 'Minimum tracked views cannot be lower than 0.', 'post-views-counter' ), 'error' );
560 $threshold = 0;
561 }
562
563 if ( isset( $field['max'] ) )
564 $threshold = min( $threshold, (int) $field['max'] );
565
566 return $threshold;
567 }
568
569 /**
570 * Validate email template text.
571 *
572 * @param string $value
573 * @param array $field
574 * @return string
575 */
576 public function validate_template_text( $value, $field ) {
577 $field_key = ! empty( $field['field_key'] ) ? sanitize_key( $field['field_key'] ) : '';
578 $context = $this->get_template_context_for_field( $field_key );
579 $is_body_template = $field_key === 'email_body_template' || ( ! empty( $field['type'] ) && $field['type'] === 'editor' );
580 $sanitized = $is_body_template ? Post_Views_Counter_Emails_Template::sanitize_template_html( $value ) : sanitize_text_field( $value );
581 $cleaned_template = Post_Views_Counter_Emails_Template::strip_invalid_template_tags( $sanitized, $context );
582
583 if ( ! empty( $cleaned_template['removed_tags'] ) ) {
584 add_settings_error(
585 'post_views_counter_settings_emails',
586 'pvc_emails_template_tags_invalid_' . sanitize_key( $field['field_key'] ),
587 sprintf(
588 __( '%1$s contained tags that are not available in this field. Removed tags: %2$s. Supported tags here: %3$s', 'post-views-counter' ),
589 wp_strip_all_tags( $field['title'] ),
590 implode( ', ', array_map( 'sanitize_text_field', $cleaned_template['removed_tags'] ) ),
591 implode( ', ', array_map( 'sanitize_text_field', $this->get_template_tags_for_field( $field_key ) ) )
592 ),
593 'warning'
594 );
595 }
596
597 return isset( $cleaned_template['template'] ) ? $cleaned_template['template'] : $sanitized;
598 }
599
600 /**
601 * Validate the top content items limit.
602 *
603 * @param mixed $value
604 * @param array $field
605 * @return int
606 */
607 public function validate_max_top_items( $value, $field ) {
608 $max_top_items = (int) $value;
609
610 if ( isset( $field['min'] ) )
611 $max_top_items = max( (int) $field['min'], $max_top_items );
612
613 if ( isset( $field['max'] ) )
614 $max_top_items = min( (int) $field['max'], $max_top_items );
615
616 return $max_top_items;
617 }
618
619 /**
620 * Determine whether PVC Pro is active.
621 *
622 * @return bool
623 */
624 private function is_pro_active() {
625 return class_exists( 'Post_Views_Counter_Pro' );
626 }
627
628 /**
629 * Get supported template tags.
630 *
631 * @return array
632 */
633 public function get_free_template_tags() {
634 return Post_Views_Counter_Emails_Template::get_free_template_tags();
635 }
636
637 /**
638 * Find unsupported template tags.
639 *
640 * @param string $template
641 * @param string $context
642 * @return array
643 */
644 public function find_unknown_template_tags( $template, $context = 'content' ) {
645 return Post_Views_Counter_Emails_Template::find_unknown_template_tags( $template, [], 'weekly', $context );
646 }
647
648 /**
649 * Get the supported tag context for a field.
650 *
651 * @param string $field_key
652 * @return string
653 */
654 private function get_template_context_for_field( $field_key ) {
655 return $field_key === 'email_subject_template' ? 'subject' : 'content';
656 }
657
658 /**
659 * Get supported template tags for a specific field.
660 *
661 * @param string $field_key
662 * @return array
663 */
664 private function get_template_tags_for_field( $field_key ) {
665 if ( $this->get_template_context_for_field( $field_key ) === 'subject' )
666 return Post_Views_Counter_Emails_Template::get_subject_template_tags();
667
668 return Post_Views_Counter_Emails_Template::get_content_template_tags();
669 }
670
671 /**
672 * Get the primary content block tags shown inline for the email body.
673 *
674 * @return array
675 */
676 private function get_primary_content_template_tags() {
677 return [
678 '%%site_name%%',
679 '%%report_label%%',
680 '%%period_label%%',
681 '%%report_summary%%',
682 '%%top_content%%',
683 '%%traffic_signals%%'
684 ];
685 }
686
687 /**
688 * Get the subject tags shown in the UI.
689 *
690 * Keep this list intentionally smaller than the full allowlist used
691 * for validation and rendering.
692 *
693 * @return array
694 */
695 private function get_display_subject_template_tags() {
696 return [
697 '%%site_name%%',
698 '%%report_label%%',
699 '%%period_label%%',
700 '%%total_views%%',
701 '%%top_post_title%%'
702 ];
703 }
704
705 /**
706 * Get the Email Subject field description.
707 *
708 * @return string
709 */
710 private function get_subject_template_description() {
711 $html = esc_html__( 'Customize the subject line for summary emails.', 'post-views-counter' );
712 $html .= ' ' . sprintf(
713 esc_html__( 'Use supported subject tags such as %1$s and %2$s.', 'post-views-counter' ),
714 '<code>%%site_name%%</code>',
715 '<code>%%total_views%%</code>'
716 );
717
718 return $this->sanitize_description_html( $html );
719 }
720
721 /**
722 * Get the Email Content field description.
723 *
724 * @return string
725 */
726 private function get_content_template_description() {
727 $html = esc_html__( 'Customize the main message sent with each email summary.', 'post-views-counter' );
728 $html .= ' ' . sprintf(
729 esc_html__( 'Use supported content tags such as %s.', 'post-views-counter' ),
730 $this->render_template_tags_html( array_merge( [ '%%site_url%%' ], $this->get_primary_content_template_tags() ) )
731 );
732
733 return $this->sanitize_description_html( $html );
734 }
735
736 /**
737 * Render template tags as safe inline HTML.
738 *
739 * @param array $tags
740 * @return string
741 */
742 private function render_template_tags_html( $tags ) {
743 $tags = is_array( $tags ) ? $tags : [];
744
745 if ( empty( $tags ) )
746 return '';
747
748 $tag_html = array_map(
749 static function( $tag ) {
750 return '<code>' . esc_html( sanitize_text_field( $tag ) ) . '</code>';
751 },
752 $tags
753 );
754
755 return implode( ', ', $tag_html );
756 }
757
758 /**
759 * Sanitize safe HTML used in settings field descriptions.
760 *
761 * @param string $html
762 * @return string
763 */
764 private function sanitize_description_html( $html ) {
765 return wp_kses(
766 (string) $html,
767 [
768 'code' => [],
769 'span' => [
770 'class' => true
771 ]
772 ]
773 );
774 }
775
776 /**
777 * Get previous saved value for a field.
778 *
779 * @param array $field
780 * @param mixed $fallback
781 * @return mixed
782 */
783 public function get_previous_value( $field, $fallback = null ) {
784 $field_key = ! empty( $field['field_key'] ) ? $field['field_key'] : '';
785
786 if ( $field_key !== '' && array_key_exists( $field_key, $this->pvc->options['emails'] ) )
787 return $this->pvc->options['emails'][$field_key];
788
789 return $fallback;
790 }
791
792 /**
793 * Determine whether emails are enabled in the current request.
794 *
795 * @return bool
796 */
797 private function is_emails_enabled_in_request() {
798 $request = isset( $_POST['post_views_counter_settings_emails'] ) && is_array( $_POST['post_views_counter_settings_emails'] ) ? wp_unslash( $_POST['post_views_counter_settings_emails'] ) : [];
799
800 if ( array_key_exists( 'enabled', $request ) )
801 return $request['enabled'] === 'true' || $request['enabled'] === true || $request['enabled'] === '1' || $request['enabled'] === 1;
802
803 return ! empty( $this->pvc->options['emails']['enabled'] );
804 }
805
806 /**
807 * Get formatted last sent text.
808 *
809 * @return string
810 */
811 private function get_last_sent_text() {
812 $value = ! empty( $this->pvc->options['emails']['last_sent_at'] ) ? $this->format_saved_datetime( $this->pvc->options['emails']['last_sent_at'] ) : '';
813
814 return $value !== '' ? $value : __( 'Not sent yet.', 'post-views-counter' );
815 }
816
817 /**
818 * Get formatted next scheduled send text.
819 *
820 * @return string
821 */
822 private function get_next_scheduled_text() {
823 $timestamp = false;
824
825 if ( isset( $this->pvc->emails_scheduler ) && $this->pvc->emails_scheduler instanceof Post_Views_Counter_Emails_Scheduler )
826 $timestamp = $this->pvc->emails_scheduler->get_next_scheduled_timestamp();
827 else
828 $timestamp = wp_next_scheduled( 'pvc_weekly_content_summary_send' );
829
830 if ( ! $timestamp )
831 return __( 'Not scheduled.', 'post-views-counter' );
832
833 return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp );
834 }
835
836 /**
837 * Get formatted last error text.
838 *
839 * @return string
840 */
841 private function get_last_error_text() {
842 $value = $this->get_latest_error_message();
843
844 return $value !== '' ? $value : __( 'No email errors recorded yet.', 'post-views-counter' );
845 }
846
847 /**
848 * Get the latest stored email error message.
849 *
850 * @return string
851 */
852 private function get_latest_error_message() {
853 if ( ! empty( $this->pvc->options['emails']['latest_status']['last_error'] ) )
854 return sanitize_text_field( $this->pvc->options['emails']['latest_status']['last_error'] );
855
856 if ( ! empty( $this->pvc->options['emails']['last_error'] ) )
857 return sanitize_text_field( $this->pvc->options['emails']['last_error'] );
858
859 return '';
860 }
861
862 /**
863 * Get the default test email recipient.
864 *
865 * @return string
866 */
867 private function get_test_email_default_recipient() {
868 $test_recipient = ! empty( $this->pvc->options['emails']['test_recipient'] ) ? sanitize_email( $this->pvc->options['emails']['test_recipient'] ) : '';
869
870 if ( is_email( $test_recipient ) )
871 return $test_recipient;
872
873 $recipient = ! empty( $this->pvc->options['emails']['recipient'] ) ? sanitize_email( $this->pvc->options['emails']['recipient'] ) : '';
874
875 if ( is_email( $recipient ) )
876 return $recipient;
877
878 $admin_email = sanitize_email( get_option( 'admin_email' ) );
879
880 return is_email( $admin_email ) ? $admin_email : '';
881 }
882
883 /**
884 * Determine whether the rendered summary contains real complete-period data.
885 *
886 * @param array $data
887 * @return bool
888 */
889 private function has_real_test_email_data( $data ) {
890 if ( ! is_array( $data ) )
891 return false;
892
893 if ( ! empty( $data['status']['empty'] ) )
894 return false;
895
896 return ! empty( $data['overview']['total_views'] ) && ! empty( $data['overview']['viewed_content_count'] );
897 }
898
899 /**
900 * Get the current test email notice container markup.
901 *
902 * @return string
903 */
904 private function get_test_email_notice_container_html() {
905 return '<div id="pvc-email-test-notice" class="pvc-email-test-notice" aria-live="polite"></div>';
906 }
907
908 /**
909 * Get the HTTP status code for a test email response.
910 *
911 * @param string $code
912 * @return int
913 */
914 private function get_test_email_status_code( $code ) {
915 if ( $code === 'not_enough_data' )
916 return 422;
917
918 if ( $code === 'invalid_recipient' )
919 return 400;
920
921 return 500;
922 }
923
924 /**
925 * Format a saved datetime value.
926 *
927 * @param mixed $value
928 * @return string
929 */
930 private function format_saved_datetime( $value ) {
931 if ( is_numeric( $value ) )
932 $timestamp = (int) $value;
933 else
934 $timestamp = strtotime( (string) $value );
935
936 if ( ! $timestamp )
937 return sanitize_text_field( (string) $value );
938
939 return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp );
940 }
941 }
942