PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 11.7.2
Jetpack – WP Security, Backup, Speed, & Growth v11.7.2
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / contact-form / admin.php
admin.php
1,460 lines 48.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- legacy file
2 /**
3 * Contact form elements in the admin area. Used with Classic Editor.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Assets;
9 use Automattic\Jetpack\Assets\Logo;
10 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11 use Automattic\Jetpack\Redirect;
12
13 /**
14 * Add a contact form button to the post composition screen
15 */
16 add_action( 'media_buttons', 'grunion_media_button', 999 );
17 /**
18 * Build contact form button.
19 *
20 * @return void
21 */
22 function grunion_media_button() {
23 global $post_ID, $temp_ID, $pagenow;// phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
24
25 if ( 'press-this.php' === $pagenow ) {
26 return;
27 }
28
29 $iframe_post_id = (int) ( 0 === $post_ID ? $temp_ID : $post_ID );// phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
30 $title = __( 'Add Contact Form', 'jetpack' );
31 $site_url = esc_url( admin_url( "/admin-ajax.php?post_id={$iframe_post_id}&action=grunion_form_builder&TB_iframe=true&width=768" ) );
32 ?>
33
34 <a id="insert-jetpack-contact-form" class="button thickbox" title="<?php echo esc_attr( $title ); ?>" data-editor="content" href="<?php echo esc_attr( $site_url ); ?>&id=add_form">
35 <span class="jetpack-contact-form-icon"></span> <?php echo esc_html( $title ); ?>
36 </a>
37
38 <?php
39 }
40
41 add_action( 'wp_ajax_grunion_form_builder', 'grunion_display_form_view' );
42 /**
43 * Display edit form view.
44 *
45 * @return void
46 */
47 function grunion_display_form_view() {
48 if ( current_user_can( 'edit_posts' ) ) {
49 require_once GRUNION_PLUGIN_DIR . 'grunion-form-view.php';
50 }
51 exit;
52 }
53
54 // feedback specific css items
55 add_action( 'admin_print_styles', 'grunion_admin_css' );
56 /**
57 * Enqueue styles.
58 *
59 * @return void
60 */
61 function grunion_admin_css() {
62 global $current_screen;
63 if ( $current_screen === null ) {
64 return;
65 }
66 if ( 'edit-feedback' !== $current_screen->id ) {
67 return;
68 }
69
70 wp_enqueue_script( 'wp-lists' );
71
72 wp_register_style( 'grunion-admin.css', plugin_dir_url( __FILE__ ) . 'css/grunion-admin.css', array(), JETPACK__VERSION );
73 wp_style_add_data( 'grunion-admin.css', 'rtl', 'replace' );
74
75 wp_enqueue_style( 'grunion-admin.css' );
76 }
77
78 add_action( 'admin_print_scripts', 'grunion_admin_js' );
79
80 /**
81 * Enqueue scripts.
82 *
83 * @return void
84 */
85 function grunion_admin_js() {
86 global $current_screen;
87
88 if ( 'edit-feedback' !== $current_screen->id ) {
89 return;
90 }
91
92 $script = 'var __grunionPostStatusNonce = ' . wp_json_encode( wp_create_nonce( 'grunion-post-status' ) ) . ';';
93 wp_add_inline_script( 'grunion-admin', $script, 'before' );
94 }
95
96 add_action( 'admin_head', 'grunion_add_bulk_edit_option' );
97 /**
98 * Hack a 'Bulk Spam' option for bulk edit in other than spam view
99 * Hack a 'Bulk Delete' option for bulk edit in spam view
100 *
101 * There isn't a better way to do this until
102 * https://core.trac.wordpress.org/changeset/17297 is resolved
103 */
104 function grunion_add_bulk_edit_option() {
105
106 $screen = get_current_screen();
107
108 if ( $screen === null ) {
109 return;
110 }
111
112 if ( 'edit-feedback' !== $screen->id ) {
113 return;
114 }
115
116 // When viewing spam we want to be able to be able to bulk delete
117 // When viewing anything we want to be able to bulk move to spam
118 if ( isset( $_GET['post_status'] ) && 'spam' === $_GET['post_status'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes to the site, we're only rendering the option to choose bulk delete/spam.
119 // Create Delete Permanently bulk item
120 $option_val = 'delete';
121 $option_txt = __( 'Delete Permanently', 'jetpack' );
122 $pseudo_selector = 'last-child';
123
124 } else {
125 // Create Mark Spam bulk item
126 $option_val = 'spam';
127 $option_txt = __( 'Mark as Spam', 'jetpack' );
128 $pseudo_selector = 'first-child';
129 }
130
131 ?>
132 <script type="text/javascript">
133 jQuery(document).ready(function($) {
134 $('#posts-filter .actions select').filter('[name=action], [name=action2]').find('option:<?php echo esc_attr( $pseudo_selector ); ?>').after('<option value="<?php echo esc_attr( $option_val ); ?>"><?php echo esc_attr( $option_txt ); ?></option>' );
135 })
136 </script>
137 <?php
138 }
139
140 add_action( 'admin_init', 'grunion_handle_bulk_spam' );
141 /**
142 * Handle a bulk spam report
143 */
144 function grunion_handle_bulk_spam() {
145 global $pagenow;
146
147 if ( 'edit.php' !== $pagenow
148 || ( empty( $_REQUEST['post_type'] ) || 'feedback' !== $_REQUEST['post_type'] ) ) {
149 return;
150 }
151
152 // Slip in a success message
153 if ( ! empty( $_REQUEST['message'] ) && 'marked-spam' === $_REQUEST['message'] ) {
154 add_action( 'admin_notices', 'grunion_message_bulk_spam' );
155 }
156
157 if ( ( empty( $_REQUEST['action'] ) || 'spam' !== $_REQUEST['action'] ) && ( empty( $_REQUEST['action2'] ) || 'spam' !== $_REQUEST['action2'] ) ) {
158 return;
159 }
160
161 check_admin_referer( 'bulk-posts' );
162
163 if ( empty( $_REQUEST['post'] ) ) {
164 wp_safe_redirect( wp_get_referer() );
165 exit;
166 }
167
168 $post_ids = array_map( 'intval', $_REQUEST['post'] );
169
170 foreach ( $post_ids as $post_id ) {
171 if ( ! current_user_can( 'edit_page', $post_id ) ) {
172 wp_die( esc_html__( 'You are not allowed to manage this item.', 'jetpack' ) );
173 }
174
175 $post = array(
176 'ID' => $post_id,
177 'post_status' => 'spam',
178 );
179 $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
180 wp_update_post( $post );
181
182 /**
183 * Fires after a comment has been marked by Akismet.
184 *
185 * Typically this means the comment is spam.
186 *
187 * @module contact-form
188 *
189 * @since 2.2.0
190 *
191 * @param string $comment_status Usually is 'spam', otherwise 'ham'.
192 * @param array $akismet_values From '_feedback_akismet_values' in comment meta
193 */
194 do_action( 'contact_form_akismet', 'spam', $akismet_values );
195 }
196
197 $redirect_url = add_query_arg( 'message', 'marked-spam', wp_get_referer() );
198 wp_safe_redirect( $redirect_url );
199 exit;
200 }
201 /**
202 * Display spam message.
203 *
204 * @return void
205 */
206 function grunion_message_bulk_spam() {
207 echo '<div class="updated"><p>' . esc_html__( 'Feedback(s) marked as spam', 'jetpack' ) . '</p></div>';
208 }
209
210 add_filter( 'bulk_actions-edit-feedback', 'grunion_admin_bulk_actions' );
211 /**
212 * Unset edit option when bulk editing.
213 *
214 * @param array $actions List of actions available.
215 * @return array $actions
216 */
217 function grunion_admin_bulk_actions( $actions ) {
218 global $current_screen;
219 if ( 'edit-feedback' !== $current_screen->id ) {
220 return $actions;
221 }
222
223 unset( $actions['edit'] );
224 return $actions;
225 }
226
227 add_filter( 'views_edit-feedback', 'grunion_admin_view_tabs' );
228 /**
229 * Unset publish button when editing feedback.
230 *
231 * @param array $views List of post views.
232 * @return array $views
233 */
234 function grunion_admin_view_tabs( $views ) {
235 global $current_screen;
236 if ( 'edit-feedback' !== $current_screen->id ) {
237 return $views;
238 }
239
240 unset( $views['publish'] );
241
242 preg_match( '|post_type=feedback\'( class="current")?\>(.*)\<span class=|', $views['all'], $match );
243 if ( ! empty( $match[2] ) ) {
244 $views['all'] = str_replace( $match[2], __( 'Messages', 'jetpack' ) . ' ', $views['all'] );
245 }
246
247 return $views;
248 }
249
250 add_filter( 'manage_feedback_posts_columns', 'grunion_post_type_columns_filter' );
251 /**
252 * Build Feedback admin page columns.
253 *
254 * @param array $cols List of available columns.
255 * @return array
256 */
257 function grunion_post_type_columns_filter( $cols ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
258 return array(
259 'cb' => '<input type="checkbox" />',
260 'feedback_from' => __( 'From', 'jetpack' ),
261 'feedback_source' => __( 'Source', 'jetpack' ),
262 'feedback_date' => __( 'Date', 'jetpack' ),
263 'feedback_response' => __( 'Response Data', 'jetpack' ),
264 );
265 }
266
267 /**
268 * Displays the value for the source column. (This function runs within the loop.)
269 *
270 * @return void
271 */
272 function grunion_manage_post_column_date() {
273 echo esc_html( date_i18n( 'Y/m/d', get_the_time( 'U' ) ) );
274 }
275
276 /**
277 * Displays the value for the from column.
278 *
279 * @param \WP_Post $post Current post.
280 * @return void
281 */
282 function grunion_manage_post_column_from( $post ) {
283 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post->ID );
284
285 if ( isset( $content_fields['_feedback_author'] ) ) {
286 echo esc_html( $content_fields['_feedback_author'] );
287 return;
288 }
289
290 if ( isset( $content_fields['_feedback_author_email'] ) ) {
291 printf(
292 "<a href='%1\$s' target='_blank'>%2\$s</a><br />",
293 esc_url( 'mailto:' . $content_fields['_feedback_author_email'] ),
294 esc_html( $content_fields['_feedback_author_email'] )
295 );
296 return;
297 }
298
299 if ( isset( $content_fields['_feedback_ip'] ) ) {
300 echo esc_html( $content_fields['feedback_ip'] );
301 return;
302 }
303
304 echo esc_html__( 'Unknown', 'jetpack' );
305 }
306
307 /**
308 * Displays the value for the response column.
309 *
310 * @param \WP_Post $post Current post.
311 * @return void
312 */
313 function grunion_manage_post_column_response( $post ) {
314 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post->ID );
315
316 $response_fields = array_diff_key(
317 isset( $content_fields['_feedback_all_fields'] ) ? $content_fields['_feedback_all_fields'] : array(),
318 array(
319 'email_marketing_consent' => '',
320 'entry_title' => '',
321 'entry_permalink' => '',
322 'feedback_id' => '',
323 )
324 );
325
326 echo '<hr class="feedback_response__mobile-separator" />';
327 echo '<div class="feedback_response__item">';
328 foreach ( $response_fields as $key => $value ) {
329 printf(
330 '<div class="feedback_response__item-key">%s</div><div class="feedback_response__item-value">%s</div>',
331 esc_html( preg_replace( '#^\d+_#', '', $key ) ),
332 esc_html( $value )
333 );
334 }
335 echo '</div>';
336 echo '<hr />';
337
338 echo '<div class="feedback_response__item">';
339 if ( isset( $content_fields['_feedback_ip'] ) ) {
340 echo '<div class="feedback_response__item-key">' . esc_html__( 'IP', 'jetpack' ) . '</div>';
341 echo '<div class="feedback_response__item-value">' . esc_html( $content_fields['_feedback_ip'] ) . '</div>';
342 }
343 echo '<div class="feedback_response__item-key">' . esc_html__( 'Source', 'jetpack' ) . '</div>';
344 echo '<div class="feedback_response__item-value"><a href="' . esc_url( get_permalink( $post->post_parent ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( get_permalink( $post->post_parent ) ) . '</a></div>';
345 echo '</div>';
346 }
347
348 /**
349 * Displays the value for the source column.
350 *
351 * @param \WP_Post $post Current post.
352 * @return void
353 */
354 function grunion_manage_post_column_source( $post ) {
355 if ( ! isset( $post->post_parent ) ) {
356 return;
357 }
358
359 $form_url = get_permalink( $post->post_parent );
360 $parsed_url = wp_parse_url( $form_url );
361
362 printf(
363 '<a href="%s" target="_blank" rel="noopener noreferrer">/%s</a>',
364 esc_url( $form_url ),
365 esc_html( basename( $parsed_url['path'] ) )
366 );
367 }
368
369 add_action( 'manage_posts_custom_column', 'grunion_manage_post_columns', 10, 2 );
370 /**
371 * Parse message content and display in appropriate columns.
372 *
373 * @param array $col List of columns available on admin page.
374 * @param int $post_id The current post ID.
375 * @return void
376 */
377 function grunion_manage_post_columns( $col, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
378 global $post;
379
380 /**
381 * Only call parse_fields_from_content if we're dealing with a Grunion custom column.
382 */
383 if ( ! in_array( $col, array( 'feedback_date', 'feedback_from', 'feedback_response', 'feedback_source' ), true ) ) {
384 return;
385 }
386
387 switch ( $col ) {
388 case 'feedback_date':
389 grunion_manage_post_column_date();
390 return;
391 case 'feedback_from':
392 grunion_manage_post_column_from( $post );
393 return;
394 case 'feedback_response':
395 grunion_manage_post_column_response( $post );
396 return;
397 case 'feedback_source':
398 grunion_manage_post_column_source( $post );
399 return;
400 }
401 }
402
403 add_action( 'restrict_manage_posts', 'grunion_source_filter' );
404 /**
405 * Add a post filter dropdown at the top of the admin page.
406 *
407 * @return void
408 */
409 function grunion_source_filter() {
410 $screen = get_current_screen();
411
412 if ( 'edit-feedback' !== $screen->id ) {
413 return;
414 }
415
416 $parent_id = intval( isset( $_GET['jetpack_form_parent_id'] ) ? $_GET['jetpack_form_parent_id'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
417 \Grunion_Contact_Form_Plugin::form_posts_dropdown( $parent_id );
418 }
419
420 add_action( 'pre_get_posts', 'grunion_source_filter_results' );
421 /**
422 * Filter feedback posts by parent_id if present.
423 *
424 * @param WP_Query $query Current query.
425 *
426 * @return void
427 */
428 function grunion_source_filter_results( $query ) {
429 $parent_id = intval( isset( $_GET['jetpack_form_parent_id'] ) ? $_GET['jetpack_form_parent_id'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
430
431 if ( ! $parent_id || $query->query_vars['post_type'] !== 'feedback' ) {
432 return;
433 }
434
435 // Don't apply to the filter dropdown query
436 if ( $query->query_vars['fields'] === 'id=>parent' ) {
437 return;
438 }
439
440 $query->query_vars['post_parent'] = $parent_id;
441 }
442
443 add_filter( 'post_row_actions', 'grunion_manage_post_row_actions', 10, 2 );
444 /**
445 * Add actions to feedback response rows in WP Admin.
446 *
447 * @param string[] $actions Default actions.
448 * @return string[]
449 */
450 function grunion_manage_post_row_actions( $actions ) {
451 global $post;
452
453 if ( 'feedback' !== $post->post_type ) {
454 return $actions;
455 }
456
457 $post_type_object = get_post_type_object( $post->post_type );
458 $actions = array();
459
460 if ( $post->post_status === 'trash' ) {
461 $actions['untrash'] = sprintf(
462 '<a title="%s" href="%s">%s</a>',
463 esc_attr__( 'Restore this item from the Trash', 'jetpack' ),
464 esc_url( wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&action=untrash', rawurlencode( $post->ID ) ) ) ), 'untrash-' . $post->post_type . '_' . $post->ID ),
465 esc_html__( 'Restore', 'jetpack' )
466 );
467 $actions['delete'] = sprintf(
468 '<a class="submitdelete" title="%s" href="%s">%s</a>',
469 esc_attr( __( 'Delete this item permanently', 'jetpack' ) ),
470 get_delete_post_link( $post->ID, '', true ),
471 esc_html__( 'Delete Permanently', 'jetpack' )
472 );
473 } elseif ( $post->post_status === 'publish' ) {
474 $actions['spam'] = sprintf(
475 '<a title="%s" href="%s">%s</a>',
476 esc_html__( 'Mark this message as spam', 'jetpack' ),
477 esc_url( wp_nonce_url( admin_url( 'admin-ajax.php?post_id=' . rawurlencode( $post->ID ) . '&action=spam' ) ), 'spam-feedback_' . $post->ID ),
478 esc_html__( 'Spam', 'jetpack' )
479 );
480 $actions['trash'] = sprintf(
481 '<a class="submitdelete" title="%s" href="%s">%s</a>',
482 esc_attr__( 'Trash', 'jetpack' ),
483 get_delete_post_link( $post->ID ),
484 esc_html__( 'Trash', 'jetpack' )
485 );
486 } elseif ( $post->post_status === 'spam' ) {
487 $actions['unspam unapprove'] = sprintf(
488 '<a title="%s" href="">%s</a>',
489 esc_html__( 'Mark this message as NOT spam', 'jetpack' ),
490 esc_html__( 'Not Spam', 'jetpack' )
491 );
492 $actions['delete'] = sprintf(
493 '<a class="submitdelete" title="%s" href="%s">%s</a>',
494 esc_attr( __( 'Delete this item permanently', 'jetpack' ) ),
495 get_delete_post_link( $post->ID, '', true ),
496 esc_html__( 'Delete Permanently', 'jetpack' )
497 );
498 }
499
500 return $actions;
501 }
502
503 /**
504 * Escape grunion attributes.
505 *
506 * @param string $attr - the attribute we're escaping.
507 *
508 * @return string
509 */
510 function grunion_esc_attr( $attr ) {
511 $out = esc_attr( $attr );
512 // we also have to entity-encode square brackets so they don't interfere with the shortcode parser
513 // FIXME: do this better - just stripping out square brackets for now since they mysteriously keep reappearing
514 $out = str_replace( '[', '', $out );
515 $out = str_replace( ']', '', $out );
516 return $out;
517 }
518
519 /**
520 * Sort grunion items.
521 *
522 * @param array $a - the first item we're sorting.
523 * @param array $b - the second item we're sorting.
524 *
525 * @return string
526 */
527 function grunion_sort_objects( $a, $b ) {
528 if ( isset( $a['order'] ) && isset( $b['order'] ) ) {
529 return $a['order'] - $b['order'];
530 }
531 return 0;
532 }
533
534 /**
535 * Take an array of field types from the form builder, and construct a shortcode form.
536 * returns both the shortcode form, and HTML markup representing a preview of the form
537 */
538 function grunion_ajax_shortcode() {
539 check_ajax_referer( 'grunion_shortcode' );
540
541 if ( ! current_user_can( 'edit_posts' ) ) {
542 die( '-1' );
543 }
544
545 $attributes = array();
546
547 foreach ( array( 'subject', 'to' ) as $attribute ) {
548 if ( isset( $_POST[ $attribute ] ) && is_scalar( $_POST[ $attribute ] ) && (string) $_POST[ $attribute ] !== '' ) {
549 $attributes[ $attribute ] = sanitize_text_field( wp_unslash( $_POST[ $attribute ] ) );
550 }
551 }
552
553 if ( isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ) {
554 $fields = sanitize_text_field( stripslashes_deep( $_POST['fields'] ) );
555 usort( $fields, 'grunion_sort_objects' );
556
557 $field_shortcodes = array();
558
559 foreach ( $fields as $field ) {
560 $field_attributes = array();
561
562 if ( isset( $field['required'] ) && 'true' === $field['required'] ) {
563 $field_attributes['required'] = 'true';
564 }
565
566 foreach ( array( 'options', 'label', 'type' ) as $attribute ) {
567 if ( isset( $field[ $attribute ] ) ) {
568 $field_attributes[ $attribute ] = $field[ $attribute ];
569 }
570 }
571
572 $field_shortcodes[] = new Grunion_Contact_Form_Field( $field_attributes );
573 }
574 }
575
576 $grunion = new Grunion_Contact_Form( $attributes, $field_shortcodes );
577
578 die( "\n$grunion\n" ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
579 }
580
581 /**
582 * Takes a post_id, extracts the contact-form shortcode from that post (if there is one), parses it,
583 * and constructs a json object representing its contents and attributes.
584 */
585 function grunion_ajax_shortcode_to_json() {
586 global $post;
587
588 check_ajax_referer( 'grunion_shortcode_to_json' );
589
590 if ( ! empty( $_POST['post_id'] ) && ! current_user_can( 'edit_post', (int) $_POST['post_id'] ) ) {
591 die( '-1' );
592 } elseif ( ! current_user_can( 'edit_posts' ) ) {
593 die( '-1' );
594 }
595
596 if ( ! isset( $_POST['content'] ) || ! is_numeric( $_POST['post_id'] ) ) {
597 die( '-1' );
598 }
599
600 $content = sanitize_text_field( wp_unslash( $_POST['content'] ) );
601
602 // doesn't look like a post with a [contact-form] already.
603 if ( false === has_shortcode( $content, 'contact-form' ) ) {
604 die( '' );
605 }
606
607 $post = get_post( (int) $_POST['post_id'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
608
609 do_shortcode( $content );
610
611 $grunion = Grunion_Contact_Form::$last;
612
613 $out = array(
614 'to' => '',
615 'subject' => '',
616 'fields' => array(),
617 );
618
619 foreach ( $grunion->fields as $field ) {
620 $out['fields'][ $field->get_attribute( 'id' ) ] = $field->attributes;
621 }
622
623 foreach ( array( 'to', 'subject' ) as $attribute ) {
624 $value = $grunion->get_attribute( $attribute );
625 if ( isset( $grunion->defaults[ $attribute ] ) && $value === $grunion->defaults[ $attribute ] ) {
626 $value = '';
627 }
628 $out[ $attribute ] = $value;
629 }
630
631 die( wp_json_encode( $out ) );
632 }
633
634 add_action( 'wp_ajax_grunion_shortcode', 'grunion_ajax_shortcode' );
635 add_action( 'wp_ajax_grunion_shortcode_to_json', 'grunion_ajax_shortcode_to_json' );
636
637 // process row-action spam/not spam clicks
638 add_action( 'wp_ajax_grunion_ajax_spam', 'grunion_ajax_spam' );
639
640 /**
641 * Handle marking feedback as spam.
642 */
643 function grunion_ajax_spam() {
644 global $wpdb;
645
646 if ( empty( $_POST['make_it'] ) ) {
647 return;
648 }
649
650 $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
651 check_ajax_referer( 'grunion-post-status' );
652 if ( ! current_user_can( 'edit_page', $post_id ) ) {
653 wp_die( esc_html__( 'You are not allowed to manage this item.', 'jetpack' ) );
654 }
655
656 require_once __DIR__ . '/grunion-contact-form.php';
657
658 $current_menu = '';
659 if ( isset( $_POST['sub_menu'] ) && preg_match( '|post_type=feedback|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
660 if ( preg_match( '|post_status=spam|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
661 $current_menu = 'spam';
662 } elseif ( preg_match( '|post_status=trash|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
663 $current_menu = 'trash';
664 } else {
665 $current_menu = 'messages';
666 }
667 }
668
669 $post = get_post( $post_id );
670 $post_type_object = get_post_type_object( $post->post_type );
671 $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
672 if ( $_POST['make_it'] === 'spam' ) {
673 $post->post_status = 'spam';
674 $status = wp_insert_post( $post );
675
676 /** This action is already documented in modules/contact-form/admin.php */
677 do_action( 'contact_form_akismet', 'spam', $akismet_values );
678 } elseif ( $_POST['make_it'] === 'ham' ) {
679 $post->post_status = 'publish';
680 $status = wp_insert_post( $post );
681
682 /** This action is already documented in modules/contact-form/admin.php */
683 do_action( 'contact_form_akismet', 'ham', $akismet_values );
684
685 $comment_author_email = false;
686 $reply_to_addr = false;
687 $message = false;
688 $to = false;
689 $headers = false;
690 $blog_url = wp_parse_url( site_url() );
691
692 // resend the original email
693 $email = get_post_meta( $post_id, '_feedback_email', true );
694 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post_id );
695
696 if ( ! empty( $email ) && ! empty( $content_fields ) ) {
697 if ( isset( $content_fields['_feedback_author_email'] ) ) {
698 $comment_author_email = $content_fields['_feedback_author_email'];
699 }
700
701 if ( isset( $email['to'] ) ) {
702 $to = $email['to'];
703 }
704
705 if ( isset( $email['message'] ) ) {
706 $message = $email['message'];
707 }
708
709 if ( isset( $email['headers'] ) ) {
710 $headers = $email['headers'];
711 } else {
712 $headers = 'From: "' . $content_fields['_feedback_author'] . '" <wordpress@' . $blog_url['host'] . ">\r\n";
713
714 if ( ! empty( $comment_author_email ) ) {
715 $reply_to_addr = $comment_author_email;
716 } elseif ( is_array( $to ) ) {
717 $reply_to_addr = $to[0];
718 }
719
720 if ( $reply_to_addr ) {
721 $headers .= 'Reply-To: "' . $content_fields['_feedback_author'] . '" <' . $reply_to_addr . ">\r\n";
722 }
723
724 $headers .= 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . '"';
725 }
726
727 /**
728 * Filters the subject of the email sent after a contact form submission.
729 *
730 * @module contact-form
731 *
732 * @since 3.0.0
733 *
734 * @param string $content_fields['_feedback_subject'] Feedback's subject line.
735 * @param array $content_fields['_feedback_all_fields'] Feedback's data from old fields.
736 */
737 $subject = apply_filters( 'contact_form_subject', $content_fields['_feedback_subject'], $content_fields['_feedback_all_fields'] );
738
739 Grunion_Contact_Form::wp_mail( $to, $subject, $message, $headers );
740 }
741 } elseif ( $_POST['make_it'] === 'publish' ) {
742 if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
743 wp_die( esc_html__( 'You are not allowed to move this item out of the Trash.', 'jetpack' ) );
744 }
745
746 if ( ! wp_untrash_post( $post_id ) ) {
747 wp_die( esc_html__( 'Error in restoring from Trash.', 'jetpack' ) );
748 }
749 } elseif ( $_POST['make_it'] === 'trash' ) {
750 if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
751 wp_die( esc_html__( 'You are not allowed to move this item to the Trash.', 'jetpack' ) );
752 }
753
754 if ( ! wp_trash_post( $post_id ) ) {
755 wp_die( esc_html__( 'Error in moving to Trash.', 'jetpack' ) );
756 }
757 }
758
759 $sql = "
760 SELECT post_status,
761 COUNT( * ) AS post_count
762 FROM `{$wpdb->posts}`
763 WHERE post_type = 'feedback'
764 GROUP BY post_status
765 ";
766 $status_count = (array) $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
767
768 $status = array();
769 $status_html = '';
770 foreach ( $status_count as $row ) {
771 $status[ $row['post_status'] ] = $row['post_count'];
772 }
773
774 if ( isset( $status['publish'] ) ) {
775 $status_html .= '<li><a href="edit.php?post_type=feedback"';
776 if ( $current_menu === 'messages' ) {
777 $status_html .= ' class="current"';
778 }
779
780 $status_html .= '>' . __( 'Messages', 'jetpack' ) . ' <span class="count">';
781 $status_html .= '(' . number_format( $status['publish'] ) . ')';
782 $status_html .= '</span></a> |</li>';
783 }
784
785 if ( isset( $status['trash'] ) ) {
786 $status_html .= '<li><a href="edit.php?post_status=trash&amp;post_type=feedback"';
787 if ( $current_menu === 'trash' ) {
788 $status_html .= ' class="current"';
789 }
790
791 $status_html .= '>' . __( 'Trash', 'jetpack' ) . ' <span class="count">';
792 $status_html .= '(' . number_format( $status['trash'] ) . ')';
793 $status_html .= '</span></a>';
794 if ( isset( $status['spam'] ) ) {
795 $status_html .= ' |';
796 }
797 $status_html .= '</li>';
798 }
799
800 if ( isset( $status['spam'] ) ) {
801 $status_html .= '<li><a href="edit.php?post_status=spam&amp;post_type=feedback"';
802 if ( $current_menu === 'spam' ) {
803 $status_html .= ' class="current"';
804 }
805
806 $status_html .= '>' . __( 'Spam', 'jetpack' ) . ' <span class="count">';
807 $status_html .= '(' . number_format( $status['spam'] ) . ')';
808 $status_html .= '</span></a></li>';
809 }
810
811 echo $status_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're building the html to echo.
812 exit;
813 }
814
815 /**
816 * Add the scripts that will add the "Check for Spam" button to the Feedbacks dashboard page.
817 */
818 function grunion_enable_spam_recheck() {
819 if ( ! defined( 'AKISMET_VERSION' ) ) {
820 return;
821 }
822
823 $screen = get_current_screen();
824
825 // Only add to feedback, only to non-spam view
826 if ( 'edit-feedback' !== $screen->id || ( ! empty( $_GET['post_status'] ) && 'spam' === $_GET['post_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not making site changes with this check.
827 return;
828 }
829
830 // Add the actual "Check for Spam" button.
831 add_action( 'admin_head', 'grunion_check_for_spam_button' );
832 }
833
834 add_action( 'admin_enqueue_scripts', 'grunion_enable_spam_recheck' );
835
836 /**
837 * Add the JS and CSS necessary for the Feedback admin page to function.
838 */
839 function grunion_add_admin_scripts() {
840 $screen = get_current_screen();
841
842 if ( 'edit-feedback' !== $screen->id ) {
843 return;
844 }
845
846 // Add the scripts that handle the spam check event.
847 wp_register_script(
848 'grunion-admin',
849 Assets::get_file_url_for_environment(
850 '_inc/build/contact-form/js/grunion-admin.min.js',
851 'modules/contact-form/js/grunion-admin.js'
852 ),
853 array( 'jquery' ),
854 JETPACK__VERSION,
855 true
856 );
857
858 wp_enqueue_script( 'grunion-admin' );
859
860 wp_enqueue_style( 'grunion.css' );
861
862 // Only add to feedback, only to spam view.
863 if ( empty( $_GET['post_status'] ) || 'spam' !== $_GET['post_status'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not making site changes with this check
864 return;
865 }
866
867 $feedbacks_count = wp_count_posts( 'feedback' );
868 $nonce = wp_create_nonce( 'jetpack_delete_spam_feedbacks' );
869 $success_url = remove_query_arg( array( 'jetpack_empty_feedback_spam_error', 'post_status' ) ); // Go to the "All Feedback" page.
870 $failure_url = add_query_arg( 'jetpack_empty_feedback_spam_error', '1' ); // Refresh the current page and show an error.
871 $spam_count = $feedbacks_count->spam;
872
873 $button_parameters = array(
874 /* translators: The placeholder is for showing how much of the process has completed, as a percent. e.g., "Emptying Spam (40%)" */
875 'progress_label' => __( 'Emptying Spam (%1$s%)', 'jetpack' ),
876 'success_url' => $success_url,
877 'failure_url' => $failure_url,
878 'spam_count' => $spam_count,
879 'nonce' => $nonce,
880 'label' => __( 'Empty Spam', 'jetpack' ),
881 );
882
883 wp_localize_script( 'grunion-admin', 'jetpack_empty_spam_button_parameters', $button_parameters );
884 }
885
886 add_action( 'admin_enqueue_scripts', 'grunion_add_admin_scripts' );
887
888 /**
889 * Adds the 'Export' button to the feedback dashboard page.
890 *
891 * @return void
892 */
893 function grunion_export_button() {
894 $current_screen = get_current_screen();
895 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
896 return;
897 }
898
899 if ( ! current_user_can( 'export' ) ) {
900 return;
901 }
902
903 // if there aren't any feedbacks, bail out
904 if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
905 return;
906 }
907
908 $nonce_name = 'feedback_export_nonce';
909
910 $button_html = get_submit_button(
911 __( 'Export', 'jetpack' ),
912 'primary',
913 'jetpack-export-feedback',
914 false,
915 array(
916 'data-nonce-name' => $nonce_name,
917 )
918 );
919
920 $button_html .= wp_nonce_field( 'feedback_export', $nonce_name, false, false );
921 ?>
922 <script type="text/javascript">
923 jQuery( function ( $ ) {
924 $( '#posts-filter #post-query-submit' ).after( <?php echo wp_json_encode( $button_html ); ?> );
925 } );
926 </script>
927 <?php
928 }
929
930 /**
931 * Add the "Check for Spam" button to the Feedbacks dashboard page.
932 */
933 function grunion_check_for_spam_button() {
934 // Nonce name.
935 $nonce_name = 'jetpack_check_feedback_spam_' . (string) get_current_blog_id();
936 // Get HTML for the button.
937 $button_html = get_submit_button(
938 __( 'Check for Spam', 'jetpack' ),
939 'secondary',
940 'jetpack-check-feedback-spam',
941 false,
942 array(
943 'data-failure-url' => add_query_arg( 'jetpack_check_feedback_spam_error', '1' ), // Refresh the current page and show an error.
944 'data-nonce-name' => $nonce_name,
945 )
946 );
947 $button_html .= '<span class="jetpack-check-feedback-spam-spinner"></span>';
948 $button_html .= wp_nonce_field( 'grunion_recheck_queue', $nonce_name, false, false );
949
950 // Add the button next to the filter button via js.
951 ?>
952 <script type="text/javascript">
953 jQuery( function( $ ) {
954 $( '.tablenav.bottom .bulkactions' ).append( <?php echo wp_json_encode( $button_html ); ?> );
955 } );
956 </script>
957 <?php
958 }
959
960 /**
961 * Recheck all approved feedbacks for spam.
962 */
963 function grunion_recheck_queue() {
964 $blog_id = get_current_blog_id();
965
966 if (
967 empty( $_POST[ 'jetpack_check_feedback_spam_' . (string) $blog_id ] )
968 || ! wp_verify_nonce( sanitize_key( $_POST[ 'jetpack_check_feedback_spam_' . (string) $blog_id ] ), 'grunion_recheck_queue' )
969 ) {
970 wp_send_json_error(
971 __( 'You aren’t authorized to do that.', 'jetpack' ),
972 403
973 );
974
975 return;
976 }
977
978 if ( ! current_user_can( 'delete_others_posts' ) ) {
979 wp_send_json_error(
980 __( 'You don’t have permission to do that.', 'jetpack' ),
981 403
982 );
983
984 return;
985 }
986
987 $query = 'post_type=feedback&post_status=publish';
988
989 if ( isset( $_POST['limit'], $_POST['offset'] ) ) {
990 $query .= '&posts_per_page=' . (int) $_POST['limit'] . '&offset=' . (int) $_POST['offset'];
991 }
992
993 $approved_feedbacks = get_posts( $query );
994
995 foreach ( $approved_feedbacks as $feedback ) {
996 $meta = get_post_meta( $feedback->ID, '_feedback_akismet_values', true );
997
998 if ( ! $meta ) {
999 // _feedback_akismet_values is eventually deleted when it's no longer
1000 // within a reasonable time period to check the feedback for spam, so
1001 // if it's gone, don't attempt a spam recheck.
1002 continue;
1003 }
1004
1005 $meta['recheck_reason'] = 'recheck_queue';
1006
1007 /**
1008 * Filter whether the submitted feedback is considered as spam.
1009 *
1010 * @module contact-form
1011 *
1012 * @since 3.4.0
1013 *
1014 * @param bool false Is the submitted feedback spam? Default to false.
1015 * @param array $meta Feedack values returned by the Akismet plugin.
1016 */
1017 $is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $meta );
1018
1019 if ( $is_spam ) {
1020 wp_update_post(
1021 array(
1022 'ID' => $feedback->ID,
1023 'post_status' => 'spam',
1024 )
1025 );
1026 /** This action is already documented in modules/contact-form/admin.php */
1027 do_action( 'contact_form_akismet', 'spam', $meta );
1028 }
1029 }
1030
1031 wp_send_json(
1032 array(
1033 'processed' => count( $approved_feedbacks ),
1034 )
1035 );
1036 }
1037
1038 add_action( 'wp_ajax_grunion_recheck_queue', 'grunion_recheck_queue' );
1039
1040 /**
1041 * Delete a number of spam feedbacks via an AJAX request.
1042 */
1043 function grunion_delete_spam_feedbacks() {
1044 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'jetpack_delete_spam_feedbacks' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- core doesn't sanitize nonce checks either.
1045 wp_send_json_error(
1046 __( 'You aren’t authorized to do that.', 'jetpack' ),
1047 403
1048 );
1049
1050 return;
1051 }
1052
1053 if ( ! current_user_can( 'delete_others_posts' ) ) {
1054 wp_send_json_error(
1055 __( 'You don’t have permission to do that.', 'jetpack' ),
1056 403
1057 );
1058
1059 return;
1060 }
1061
1062 $deleted_feedbacks = 0;
1063
1064 $delete_limit = 25;
1065 /**
1066 * Filter the amount of Spam feedback one can delete at once.
1067 *
1068 * @module contact-form
1069 *
1070 * @since 8.7.0
1071 *
1072 * @param int $delete_limit Number of spam to process at once. Default to 25.
1073 */
1074 $delete_limit = apply_filters( 'jetpack_delete_spam_feedbacks_limit', $delete_limit );
1075 $delete_limit = (int) $delete_limit;
1076 $delete_limit = max( 1, min( 100, $delete_limit ) ); // Allow a range of 1-100 for the delete limit.
1077
1078 $query_args = array(
1079 'post_type' => 'feedback',
1080 'post_status' => 'spam',
1081 'posts_per_page' => $delete_limit,
1082 );
1083
1084 $query = new WP_Query( $query_args );
1085 $spam_feedbacks = $query->get_posts();
1086
1087 foreach ( $spam_feedbacks as $feedback ) {
1088 wp_delete_post( $feedback->ID, true );
1089
1090 $deleted_feedbacks++;
1091 }
1092
1093 wp_send_json(
1094 array(
1095 'success' => true,
1096 'data' => array(
1097 'counts' => array(
1098 'deleted' => $deleted_feedbacks,
1099 'limit' => $delete_limit,
1100 ),
1101 ),
1102 )
1103 );
1104 }
1105 add_action( 'wp_ajax_jetpack_delete_spam_feedbacks', 'grunion_delete_spam_feedbacks' );
1106
1107 /**
1108 * Show an admin notice if the "Empty Spam" or "Check Spam" process was unable to complete, probably due to a permissions error.
1109 */
1110 function grunion_feedback_admin_notice() {
1111 if ( isset( $_GET['jetpack_empty_feedback_spam_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1112 echo '<div class="notice notice-error"><p>' . esc_html( __( 'An error occurred while trying to empty the Feedback spam folder.', 'jetpack' ) ) . '</p></div>';
1113 } elseif ( isset( $_GET['jetpack_check_feedback_spam_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1114 echo '<div class="notice notice-error"><p>' . esc_html( __( 'An error occurred while trying to check for spam among the feedback you received.', 'jetpack' ) ) . '</p></div>';
1115 }
1116 }
1117 add_action( 'admin_notices', 'grunion_feedback_admin_notice' );
1118
1119 /**
1120 * Class Grunion_Admin
1121 *
1122 * Singleton for Grunion admin area support.
1123 */
1124 class Grunion_Admin {
1125 /**
1126 * CSV export nonce field name
1127 *
1128 * @var string The nonce field name for CSV export.
1129 */
1130 private $export_nonce_field_csv = 'feedback_export_nonce_csv';
1131
1132 /**
1133 * GDrive export nonce field name
1134 *
1135 * @var string The nonce field name for GDrive export.
1136 */
1137 private $export_nonce_field_gdrive = 'feedback_export_nonce_gdrive';
1138
1139 /**
1140 * Instantiates this singleton class
1141 *
1142 * @return Grunion_Admin The Grunion Admin class instance.
1143 */
1144 public static function init() {
1145 static $instance = false;
1146
1147 if ( ! $instance ) {
1148 $instance = new Grunion_Admin();
1149 }
1150
1151 return $instance;
1152 }
1153
1154 /**
1155 * Grunion_Admin constructor
1156 */
1157 public function __construct() {
1158 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
1159 add_action( 'admin_footer-edit.php', array( $this, 'print_export_modal' ) );
1160
1161 add_action( 'wp_ajax_grunion_export_to_gdrive', array( $this, 'export_to_gdrive' ) );
1162 add_action( 'wp_ajax_grunion_gdrive_connection', array( $this, 'test_gdrive_connection' ) );
1163 }
1164
1165 /**
1166 * Hook handler for admin_enqueue_scripts hook
1167 */
1168 public function admin_enqueue_scripts() {
1169 $current_screen = get_current_screen();
1170 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
1171 return;
1172 }
1173 add_thickbox();
1174 $localized_strings = array(
1175 'exportError' => esc_js( __( 'There was an error exporting your results', 'jetpack' ) ),
1176 'waitingConnection' => esc_js( __( 'Waiting for connection...', 'jetpack' ) ),
1177 );
1178 wp_localize_script( 'grunion-admin', 'exportParameters', $localized_strings );
1179 }
1180
1181 /**
1182 * Prints the modal markup with export buttons/content.
1183 */
1184 public function print_export_modal() {
1185 if ( ! current_user_can( 'export' ) ) {
1186 return;
1187 }
1188
1189 // if there aren't any feedbacks, bail out
1190 if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
1191 return;
1192 }
1193
1194 $current_screen = get_current_screen();
1195 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
1196 return;
1197 }
1198
1199 $jetpack_logo = new Logo();
1200 ?>
1201 <div id="feedback-export-modal" style="display: none;">
1202 <div class="feedback-export-modal__wrapper">
1203 <div class="feedback-export-modal__header">
1204 <h1 class="feedback-export-modal__header-title"><?php esc_html_e( 'Export your Form Responses', 'jetpack' ); ?></h1>
1205 <p class="feedback-export-modal__header-subtitle"><?php esc_html_e( 'Choose your favorite file format or export destination:', 'jetpack' ); ?></p>
1206 </div>
1207 <div class="feedback-export-modal__content">
1208 <?php $this->get_csv_export_section(); ?>
1209 <?php $this->get_gdrive_export_section(); ?>
1210 </div>
1211 <div class="feedback-export-modal__footer">
1212 <div class="feedback-export-modal__footer-column">
1213 <a href="https://jetpack.com/support/jetpack-blocks/contact-form/" title="<?php echo esc_attr_x( 'Jetpack Forms', 'Name of Jetpack’s Contact Form feature', 'jetpack' ); ?>" rel="noopener noreferer" target="_blank" class="feedback-export-modal__footer-link">
1214 <?php echo $jetpack_logo->get_jp_emblem(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1215 </a>
1216 <a href="https://jetpack.com/support/jetpack-blocks/contact-form/" title="<?php echo esc_attr_x( 'Jetpack Forms', 'Name of Jetpack’s Contact Form feature', 'jetpack' ); ?>" rel="noopener noreferer" target="_blank" class="feedback-export-modal__footer-link">
1217 <?php echo esc_html_x( 'Jetpack Forms', 'Name of Jetpack’s Contact Form feature', 'jetpack' ); ?>
1218 </a>
1219 </div>
1220 <div class="feedback-export-modal__footer-column">
1221 <a href="https://automattic.com" title="Automattic" rel="noopener noreferer" target="_blank" class="feedback-export-modal__footer-link">
1222 <svg role="img" x="0" y="0" viewBox="0 0 935 38.2" enable-background="new 0 0 935 38.2" aria-labelledby="jp-automattic-byline-logo-title" height="7" class="jp-automattic-byline-logo">
1223 <desc id="jp-automattic-byline-logo-title"><?php esc_html_e( 'An Automattic Airline', 'jetpack' ); ?></desc>
1224 <path d="M317.1 38.2c-12.6 0-20.7-9.1-20.7-18.5v-1.2c0-9.6 8.2-18.5 20.7-18.5 12.6 0 20.8 8.9 20.8 18.5v1.2C337.9 29.1 329.7 38.2 317.1 38.2zM331.2 18.6c0-6.9-5-13-14.1-13s-14 6.1-14 13v0.9c0 6.9 5 13.1 14 13.1s14.1-6.2 14.1-13.1V18.6zM175 36.8l-4.7-8.8h-20.9l-4.5 8.8h-7L157 1.3h5.5L182 36.8H175zM159.7 8.2L152 23.1h15.7L159.7 8.2zM212.4 38.2c-12.7 0-18.7-6.9-18.7-16.2V1.3h6.6v20.9c0 6.6 4.3 10.5 12.5 10.5 8.4 0 11.9-3.9 11.9-10.5V1.3h6.7V22C231.4 30.8 225.8 38.2 212.4 38.2zM268.6 6.8v30h-6.7v-30h-15.5V1.3h37.7v5.5H268.6zM397.3 36.8V8.7l-1.8 3.1 -14.9 25h-3.3l-14.7-25 -1.8-3.1v28.1h-6.5V1.3h9.2l14 24.4 1.7 3 1.7-3 13.9-24.4h9.1v35.5H397.3zM454.4 36.8l-4.7-8.8h-20.9l-4.5 8.8h-7l19.2-35.5h5.5l19.5 35.5H454.4zM439.1 8.2l-7.7 14.9h15.7L439.1 8.2zM488.4 6.8v30h-6.7v-30h-15.5V1.3h37.7v5.5H488.4zM537.3 6.8v30h-6.7v-30h-15.5V1.3h37.7v5.5H537.3zM569.3 36.8V4.6c2.7 0 3.7-1.4 3.7-3.4h2.8v35.5L569.3 36.8 569.3 36.8zM628 11.3c-3.2-2.9-7.9-5.7-14.2-5.7 -9.5 0-14.8 6.5-14.8 13.3v0.7c0 6.7 5.4 13 15.3 13 5.9 0 10.8-2.8 13.9-5.7l4 4.2c-3.9 3.8-10.5 7.1-18.3 7.1 -13.4 0-21.6-8.7-21.6-18.3v-1.2c0-9.6 8.9-18.7 21.9-18.7 7.5 0 14.3 3.1 18 7.1L628 11.3zM321.5 12.4c1.2 0.8 1.5 2.4 0.8 3.6l-6.1 9.4c-0.8 1.2-2.4 1.6-3.6 0.8l0 0c-1.2-0.8-1.5-2.4-0.8-3.6l6.1-9.4C318.7 11.9 320.3 11.6 321.5 12.4L321.5 12.4z"></path><path d="M37.5 36.7l-4.7-8.9H11.7l-4.6 8.9H0L19.4 0.8H25l19.7 35.9H37.5zM22 7.8l-7.8 15.1h15.9L22 7.8zM82.8 36.7l-23.3-24 -2.3-2.5v26.6h-6.7v-36H57l22.6 24 2.3 2.6V0.8h6.7v35.9H82.8z"></path>
1225 <path d="M719.9 37l-4.8-8.9H694l-4.6 8.9h-7.1l19.5-36h5.6l19.8 36H719.9zM704.4 8l-7.8 15.1h15.9L704.4 8zM733 37V1h6.8v36H733zM781 37c-1.8 0-2.6-2.5-2.9-5.8l-0.2-3.7c-0.2-3.6-1.7-5.1-8.4-5.1h-12.8V37H750V1h19.6c10.8 0 15.7 4.3 15.7 9.9 0 3.9-2 7.7-9 9 7 0.5 8.5 3.7 8.6 7.9l0.1 3c0.1 2.5 0.5 4.3 2.2 6.1V37H781zM778.5 11.8c0-2.6-2.1-5.1-7.9-5.1h-13.8v10.8h14.4c5 0 7.3-2.4 7.3-5.2V11.8zM794.8 37V1h6.8v30.4h28.2V37H794.8zM836.7 37V1h6.8v36H836.7zM886.2 37l-23.4-24.1 -2.3-2.5V37h-6.8V1h6.5l22.7 24.1 2.3 2.6V1h6.8v36H886.2zM902.3 37V1H935v5.6h-26v9.2h20v5.5h-20v10.1h26V37H902.3z"></path>
1226 </svg>
1227 </a>
1228 </div>
1229 </div>
1230 </div>
1231 </div>
1232 <?php
1233 $opener_label = esc_html__( 'Export', 'jetpack' );
1234 $export_modal_opener = wp_is_mobile()
1235 ? "<a id='export-modal-opener' class='button button-primary' href='#TB_inline?&width=550&height=550&inlineId=feedback-export-modal'>{$opener_label}</a>"
1236 : "<a id='export-modal-opener' class='button button-primary' href='#TB_inline?&width=680&height=600&inlineId=feedback-export-modal'>{$opener_label}</a>";
1237 ?>
1238 <script type="text/javascript">
1239 jQuery( function( $ ) {
1240 $( '#posts-filter #post-query-submit' ).after( <?php echo wp_json_encode( $export_modal_opener ); ?> );
1241 } );
1242 </script>
1243 <?php
1244 }
1245
1246 /**
1247 * Ajax handler for wp_ajax_grunion_export_to_gdrive.
1248 * Exports data to Google Drive, based on POST data.
1249 *
1250 * @see Grunion_Contact_Form_Plugin::get_feedback_entries_from_post
1251 */
1252 public function export_to_gdrive() {
1253 $post_data = wp_unslash( $_POST );
1254 if (
1255 ! current_user_can( 'export' )
1256 || empty( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ) )
1257 || ! wp_verify_nonce( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ), 'feedback_export' )
1258 ) {
1259 wp_send_json_error(
1260 __( 'You aren’t authorized to do that.', 'jetpack' ),
1261 403
1262 );
1263
1264 return;
1265 }
1266
1267 $grunion = Grunion_Contact_Form_Plugin::init();
1268 $export_data = $grunion->get_feedback_entries_from_post();
1269
1270 $fields = array_keys( $export_data );
1271 $row_count = count( reset( $export_data ) );
1272
1273 $sheet_data = array( $fields );
1274
1275 for ( $i = 0; $i < $row_count; $i ++ ) {
1276
1277 $current_row = array();
1278
1279 /**
1280 * Put all the fields in `$current_row` array.
1281 */
1282 foreach ( $fields as $single_field_name ) {
1283 $current_row[] = $export_data[ $single_field_name ][ $i ];
1284 }
1285
1286 $sheet_data[] = $current_row;
1287 }
1288
1289 $user_id = (int) get_current_user_id();
1290 $spreadsheet_title = sprintf( '%s - %s', __( 'Responses', 'jetpack' ), gmdate( 'Y-m-d H:i' ) );
1291 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1292 $sheet = Jetpack_Google_Drive_Helper::create_sheet( $user_id, $spreadsheet_title, $sheet_data );
1293
1294 wp_send_json(
1295 array(
1296 'success' => ! is_wp_error( $sheet ),
1297 'data' => $sheet,
1298 )
1299 );
1300 }
1301
1302 /**
1303 * Return HTML markup for the CSV download button.
1304 */
1305 public function get_csv_export_section() {
1306 $button_csv_html = get_submit_button(
1307 esc_html__( 'Download', 'jetpack' ),
1308 'primary export-button export-csv',
1309 'jetpack-export-feedback-csv',
1310 false,
1311 array( 'data-nonce-name' => $this->export_nonce_field_csv )
1312 );
1313 ?>
1314 <div class="export-card">
1315 <div class="export-card__header">
1316 <svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
1317 <path fill-rule="evenodd" clip-rule="evenodd" d="M11.2309 5.04199L10.0797 2.73945C9.98086 2.54183 9.77887 2.41699 9.55792 2.41699H2.83333C2.51117 2.41699 2.25 2.67816 2.25 3.00033V16.7087C2.25 17.0308 2.51117 17.292 2.83333 17.292H19.1667C19.4888 17.292 19.75 17.0308 19.75 16.7087V5.62533C19.75 5.30316 19.4888 5.04199 19.1667 5.04199H11.2309ZM12.3125 3.29199L11.6449 1.95683C11.2497 1.16633 10.4417 0.666992 9.55792 0.666992H2.83333C1.54467 0.666992 0.5 1.71166 0.5 3.00033V16.7087C0.5 17.9973 1.54467 19.042 2.83333 19.042H19.1667C20.4553 19.042 21.5 17.9973 21.5 16.7087V5.62533C21.5 4.33666 20.4553 3.29199 19.1667 3.29199H12.3125Z" fill="#008710"/>
1318 </svg>
1319 <div class="export-card__header-title"><?php esc_html_e( 'CSV File', 'jetpack' ); ?></div>
1320 </div>
1321 <div class="export-card__body">
1322 <div class="export-card__body-description">
1323 <?php esc_html_e( 'Download your form response data via CSV file.', 'jetpack' ); ?>
1324 </div>
1325 <div class="export-card__body-cta">
1326 <?php
1327 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1328 echo $button_csv_html;
1329 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1330 echo wp_nonce_field( 'feedback_export', $this->export_nonce_field_csv, false, false );
1331 ?>
1332 </div>
1333 </div>
1334 </div>
1335 <?php
1336 }
1337
1338 /**
1339 * Render/output HTML markup for the export to gdrive section.
1340 * If the user doesn't hold a Google Drive connection a button to connect will render (See grunion-admin.js).
1341 */
1342 public function get_gdrive_export_section() {
1343 $user_connected = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || ( new Connection_Manager( 'jetpack' ) )->is_user_connected( get_current_user_id() );
1344 if ( ! $user_connected ) {
1345 return;
1346 }
1347
1348 $user_id = (int) get_current_user_id();
1349
1350 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1351 $has_valid_connection = Jetpack_Google_Drive_Helper::has_valid_connection( $user_id );
1352
1353 if ( $has_valid_connection ) {
1354 $button_html = $this->get_gdrive_export_button_markup();
1355 } else {
1356 $slug = 'jetpack-form-responses-connect';
1357 $button_html = sprintf(
1358 '<a href="%1$s" id="%4$s" data-nonce-name="%5$s" class="button button-primary export-button export-gdrive" title="%2$s" rel="noopener noreferer" target="_blank">%3$s</a>',
1359 esc_url( Redirect::get_url( $slug ) ),
1360 esc_attr__( 'connect to Google Drive', 'jetpack' ),
1361 esc_html__( 'Connect Google Drive', 'jetpack' ),
1362 $slug,
1363 $this->export_nonce_field_gdrive
1364 );
1365 }
1366
1367 ?>
1368 <div class="export-card">
1369 <div class="export-card__header">
1370 <svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1371 <path d="M11.8387 1.16016H2C1.44772 1.16016 1 1.60787 1 2.16016V21.8053V21.8376C1 22.3899 1.44772 22.8376 2 22.8376H16C16.5523 22.8376 17 22.3899 17 21.8376V5.80532M11.8387 1.16016V5.80532H17M11.8387 1.16016L17 5.80532M4.6129 13.0311V16.1279H9.25806M4.6129 13.0311V9.93435H9.25806M4.6129 13.0311H13.9032M13.9032 13.0311V9.93435H9.25806M13.9032 13.0311V16.1279H9.25806M9.25806 9.93435V16.1279" stroke="#008710" stroke-width="1.5"/>
1372 </svg>
1373 <div class="export-card__header-title"><?php esc_html_e( 'Google Sheets', 'jetpack' ); ?></div>
1374 <div class="export-card__beta-badge">BETA</div>
1375 </div>
1376 <div class="export-card__body">
1377 <div class="export-card__body-description">
1378 <div>
1379 <?php esc_html_e( 'Export your data into a Google Sheets file.', 'jetpack' ); ?>
1380 <?php
1381 printf(
1382 '<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferer">%3$s</a>',
1383 esc_url( Redirect::get_url( 'jetpack-support-contact-form-export' ) ),
1384 esc_attr__( 'connect to Google Drive', 'jetpack' ),
1385 esc_html__( 'You need to connect to Google Drive.', 'jetpack' )
1386 );
1387 ?>
1388 </div>
1389 <p class="export-card__body-description-footer"><?php esc_html_e( 'This premium feature is currently free to use in beta.', 'jetpack' ); ?></p>
1390 </div>
1391 <div class="export-card__body-cta">
1392 <?php
1393 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1394 echo $button_html;
1395 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1396 echo wp_nonce_field( 'feedback_export', $this->export_nonce_field_gdrive, false, false );
1397 ?>
1398 </div>
1399 </div>
1400 </div>
1401 <?php
1402 }
1403
1404 /**
1405 * Ajax handler. Sends a payload with connection status and html to replace
1406 * the Connect button with the Export button using get_gdrive_export_button
1407 */
1408 public function test_gdrive_connection() {
1409 $post_data = wp_unslash( $_POST );
1410 $user_id = (int) get_current_user_id();
1411
1412 if (
1413 ! $user_id ||
1414 ! current_user_can( 'export' ) ||
1415 empty( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ) ) ||
1416 ! wp_verify_nonce( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ), 'feedback_export' )
1417 ) {
1418 wp_send_json_error(
1419 __( 'You aren’t authorized to do that.', 'jetpack' ),
1420 403
1421 );
1422
1423 return;
1424 }
1425
1426 if ( ! class_exists( 'Jetpack_Google_Drive_Helper' ) ) {
1427 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1428 }
1429 $has_valid_connection = Jetpack_Google_Drive_Helper::has_valid_connection( $user_id );
1430
1431 $replacement_html = $has_valid_connection
1432 ? $this->get_gdrive_export_button_markup()
1433 : '';
1434
1435 wp_send_json(
1436 array(
1437 'connection' => $has_valid_connection,
1438 'html' => $replacement_html,
1439 )
1440 );
1441 }
1442
1443 /**
1444 * Markup helper so we DRY, returns the button markup for the export to GDrive feature.
1445 *
1446 * @return string The HTML button markup
1447 */
1448 public function get_gdrive_export_button_markup() {
1449 return get_submit_button(
1450 esc_html__( 'Export', 'jetpack' ),
1451 'primary export-button export-gdrive',
1452 'jetpack-export-feedback-gdrive',
1453 false,
1454 array( 'data-nonce-name' => $this->export_nonce_field_gdrive )
1455 );
1456 }
1457 }
1458
1459 Grunion_admin::init();
1460