PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 12.1.3
Jetpack – WP Security, Backup, Speed, & Growth v12.1.3
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 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / contact-form / admin.php
jetpack / modules / contact-form Last commit date
css 3 years ago images 10 years ago js 3 years ago admin.php 3 years ago class-grunion-contact-form-endpoint.php 1 year ago grunion-contact-form.php 3 years ago grunion-editor-view.php 4 years ago grunion-form-view.php 3 years ago
admin.php
1524 lines
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 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
9
10 use Automattic\Jetpack\Assets;
11 use Automattic\Jetpack\Assets\Logo;
12 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13 use Automattic\Jetpack\Redirect;
14
15 /**
16 * Add a contact form button to the post composition screen
17 */
18 add_action( 'media_buttons', 'grunion_media_button', 999 );
19 /**
20 * Build contact form button.
21 *
22 * @return void
23 */
24 function grunion_media_button() {
25 global $post_ID, $temp_ID, $pagenow;// phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
26
27 if ( 'press-this.php' === $pagenow ) {
28 return;
29 }
30
31 $iframe_post_id = (int) ( 0 === $post_ID ? $temp_ID : $post_ID );// phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
32 $title = __( 'Add Contact Form', 'jetpack' );
33 $site_url = esc_url( admin_url( "/admin-ajax.php?post_id={$iframe_post_id}&action=grunion_form_builder&TB_iframe=true&width=768" ) );
34 ?>
35
36 <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">
37 <span class="jetpack-contact-form-icon"></span> <?php echo esc_html( $title ); ?>
38 </a>
39
40 <?php
41 }
42
43 add_action( 'wp_ajax_grunion_form_builder', 'grunion_display_form_view' );
44 /**
45 * Display edit form view.
46 *
47 * @return void
48 */
49 function grunion_display_form_view() {
50 if ( current_user_can( 'edit_posts' ) ) {
51 require_once GRUNION_PLUGIN_DIR . 'grunion-form-view.php';
52 }
53 exit;
54 }
55
56 // feedback specific css items
57 add_action( 'admin_print_styles', 'grunion_admin_css' );
58 /**
59 * Enqueue styles.
60 *
61 * @return void
62 */
63 function grunion_admin_css() {
64 global $current_screen;
65 if ( $current_screen === null ) {
66 return;
67 }
68 if ( 'edit-feedback' !== $current_screen->id ) {
69 return;
70 }
71
72 wp_enqueue_script( 'wp-lists' );
73
74 wp_register_style( 'grunion-admin.css', plugin_dir_url( __FILE__ ) . 'css/grunion-admin.css', array(), JETPACK__VERSION );
75 wp_style_add_data( 'grunion-admin.css', 'rtl', 'replace' );
76
77 wp_enqueue_style( 'grunion-admin.css' );
78 }
79
80 add_action( 'admin_print_scripts', 'grunion_admin_js' );
81
82 /**
83 * Enqueue scripts.
84 *
85 * @return void
86 */
87 function grunion_admin_js() {
88 global $current_screen;
89
90 if ( 'edit-feedback' !== $current_screen->id ) {
91 return;
92 }
93
94 $script = 'var __grunionPostStatusNonce = ' . wp_json_encode( wp_create_nonce( 'grunion-post-status' ) ) . ';';
95 wp_add_inline_script( 'grunion-admin', $script, 'before' );
96 }
97
98 add_action( 'admin_head', 'grunion_add_bulk_edit_option' );
99 /**
100 * Hack a 'Bulk Spam' option for bulk edit in other than spam view
101 * Hack a 'Bulk Delete' option for bulk edit in spam view
102 *
103 * There isn't a better way to do this until
104 * https://core.trac.wordpress.org/changeset/17297 is resolved
105 */
106 function grunion_add_bulk_edit_option() {
107
108 $screen = get_current_screen();
109
110 if ( $screen === null ) {
111 return;
112 }
113
114 if ( 'edit-feedback' !== $screen->id ) {
115 return;
116 }
117
118 // When viewing spam we want to be able to be able to bulk delete
119 // When viewing anything we want to be able to bulk move to spam
120 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.
121 // Create Delete Permanently bulk item
122 $option_val = 'delete';
123 $option_txt = __( 'Delete Permanently', 'jetpack' );
124 $pseudo_selector = 'last-child';
125
126 } else {
127 // Create Mark Spam bulk item
128 $option_val = 'spam';
129 $option_txt = __( 'Mark as Spam', 'jetpack' );
130 $pseudo_selector = 'first-child';
131 }
132
133 ?>
134 <script type="text/javascript">
135 jQuery(document).ready(function($) {
136 $('#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>' );
137 })
138 </script>
139 <?php
140 }
141
142 add_action( 'admin_init', 'grunion_handle_bulk_spam' );
143 /**
144 * Handle a bulk spam report
145 */
146 function grunion_handle_bulk_spam() {
147 global $pagenow;
148
149 if ( 'edit.php' !== $pagenow
150 || ( empty( $_REQUEST['post_type'] ) || 'feedback' !== $_REQUEST['post_type'] ) ) {
151 return;
152 }
153
154 // Slip in a success message
155 if ( ! empty( $_REQUEST['message'] ) && 'marked-spam' === $_REQUEST['message'] ) {
156 add_action( 'admin_notices', 'grunion_message_bulk_spam' );
157 }
158
159 if ( ( empty( $_REQUEST['action'] ) || 'spam' !== $_REQUEST['action'] ) && ( empty( $_REQUEST['action2'] ) || 'spam' !== $_REQUEST['action2'] ) ) {
160 return;
161 }
162
163 check_admin_referer( 'bulk-posts' );
164
165 if ( empty( $_REQUEST['post'] ) ) {
166 wp_safe_redirect( wp_get_referer() );
167 exit;
168 }
169
170 $post_ids = array_map( 'intval', $_REQUEST['post'] );
171
172 foreach ( $post_ids as $post_id ) {
173 if ( ! current_user_can( 'edit_page', $post_id ) ) {
174 wp_die( esc_html__( 'You are not allowed to manage this item.', 'jetpack' ) );
175 }
176
177 $post = array(
178 'ID' => $post_id,
179 'post_status' => 'spam',
180 );
181 $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
182 wp_update_post( $post );
183
184 /**
185 * Fires after a comment has been marked by Akismet.
186 *
187 * Typically this means the comment is spam.
188 *
189 * @module contact-form
190 *
191 * @since 2.2.0
192 *
193 * @param string $comment_status Usually is 'spam', otherwise 'ham'.
194 * @param array $akismet_values From '_feedback_akismet_values' in comment meta
195 */
196 do_action( 'contact_form_akismet', 'spam', $akismet_values );
197 }
198
199 $redirect_url = add_query_arg( 'message', 'marked-spam', wp_get_referer() );
200 wp_safe_redirect( $redirect_url );
201 exit;
202 }
203 /**
204 * Display spam message.
205 *
206 * @return void
207 */
208 function grunion_message_bulk_spam() {
209 echo '<div class="updated"><p>' . esc_html__( 'Feedback(s) marked as spam', 'jetpack' ) . '</p></div>';
210 }
211
212 add_filter( 'bulk_actions-edit-feedback', 'grunion_admin_bulk_actions' );
213 /**
214 * Unset edit option when bulk editing.
215 *
216 * @param array $actions List of actions available.
217 * @return array $actions
218 */
219 function grunion_admin_bulk_actions( $actions ) {
220 global $current_screen;
221 if ( 'edit-feedback' !== $current_screen->id ) {
222 return $actions;
223 }
224
225 unset( $actions['edit'] );
226 return $actions;
227 }
228
229 add_filter( 'views_edit-feedback', 'grunion_admin_view_tabs' );
230 /**
231 * Unset publish button when editing feedback.
232 *
233 * @param array $views List of post views.
234 * @return array $views
235 */
236 function grunion_admin_view_tabs( $views ) {
237 global $current_screen;
238 if ( 'edit-feedback' !== $current_screen->id ) {
239 return $views;
240 }
241
242 unset( $views['publish'] );
243
244 preg_match( '|post_type=feedback\'( class="current")?\>(.*)\<span class=|', $views['all'], $match );
245 if ( ! empty( $match[2] ) ) {
246 $views['all'] = str_replace( $match[2], __( 'Messages', 'jetpack' ) . ' ', $views['all'] );
247 }
248
249 return $views;
250 }
251
252 add_filter( 'manage_feedback_posts_columns', 'grunion_post_type_columns_filter' );
253 /**
254 * Build Feedback admin page columns.
255 *
256 * @param array $cols List of available columns.
257 * @return array
258 */
259 function grunion_post_type_columns_filter( $cols ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
260 return array(
261 'cb' => '<input type="checkbox" />',
262 'feedback_from' => __( 'From', 'jetpack' ),
263 'feedback_source' => __( 'Source', 'jetpack' ),
264 'feedback_date' => __( 'Date', 'jetpack' ),
265 'feedback_response' => __( 'Response Data', 'jetpack' ),
266 );
267 }
268
269 /**
270 * Displays the value for the source column. (This function runs within the loop.)
271 *
272 * @return void
273 */
274 function grunion_manage_post_column_date() {
275 echo esc_html( date_i18n( 'Y/m/d', get_the_time( 'U' ) ) );
276 }
277
278 /**
279 * Displays the value for the from column.
280 *
281 * @param \WP_Post $post Current post.
282 * @return void
283 */
284 function grunion_manage_post_column_from( $post ) {
285 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post->ID );
286
287 if ( isset( $content_fields['_feedback_author'] ) ) {
288 echo esc_html( $content_fields['_feedback_author'] );
289 return;
290 }
291
292 if ( isset( $content_fields['_feedback_author_email'] ) ) {
293 printf(
294 "<a href='%1\$s' target='_blank'>%2\$s</a><br />",
295 esc_url( 'mailto:' . $content_fields['_feedback_author_email'] ),
296 esc_html( $content_fields['_feedback_author_email'] )
297 );
298 return;
299 }
300
301 if ( isset( $content_fields['_feedback_ip'] ) ) {
302 echo esc_html( $content_fields['feedback_ip'] );
303 return;
304 }
305
306 echo esc_html__( 'Unknown', 'jetpack' );
307 }
308
309 /**
310 * Displays the value for the response column.
311 *
312 * @param \WP_Post $post Current post.
313 * @return void
314 */
315 function grunion_manage_post_column_response( $post ) {
316 $non_printable_keys = array(
317 'email_marketing_consent',
318 'entry_title',
319 'entry_permalink',
320 'feedback_id',
321 );
322
323 $post_content = get_post_field( 'post_content', $post->ID );
324 $content = explode( '<!--more-->', $post_content );
325 $content = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
326 $chunks = explode( "\nJSON_DATA", $content );
327
328 $response_fields = array();
329
330 if ( is_array( $chunks ) && isset( $chunks[1] ) ) {
331 $rearray = json_decode( $chunks[1], true );
332 if ( is_array( $rearray ) && isset( $rearray['feedback_id'] ) ) {
333 $response_fields = $rearray;
334 }
335 }
336
337 if ( empty( $response_fields ) ) {
338 $chunks = explode( "\nArray", $content );
339 if ( $chunks[1] ) {
340 // re-construct the array string
341 $array = 'Array' . $chunks[1];
342 // re-construct the array
343 $rearray = Grunion_Contact_Form_Plugin::reverse_that_print( $array, true );
344 $response_fields = is_array( $rearray ) ? $rearray : array();
345 } else {
346 // couldn't reconstruct array, use the old method
347 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post->ID );
348 $response_fields = isset( $content_fields['_feedback_all_fields'] ) ? $content_fields['_feedback_all_fields'] : array();
349 }
350 }
351
352 $response_fields = array_diff_key( $response_fields, array_flip( $non_printable_keys ) );
353
354 echo '<hr class="feedback_response__mobile-separator" />';
355 echo '<div class="feedback_response__item">';
356 foreach ( $response_fields as $key => $value ) {
357 if ( is_array( $value ) ) {
358 $value = implode( ', ', $value );
359 }
360 printf(
361 '<div class="feedback_response__item-key">%s</div><div class="feedback_response__item-value">%s</div>',
362 esc_html( preg_replace( '#^\d+_#', '', $key ) ),
363 nl2br( esc_html( $value ) )
364 );
365 }
366 echo '</div>';
367 echo '<hr />';
368
369 echo '<div class="feedback_response__item">';
370 if ( isset( $content_fields['_feedback_ip'] ) ) {
371 echo '<div class="feedback_response__item-key">' . esc_html__( 'IP', 'jetpack' ) . '</div>';
372 echo '<div class="feedback_response__item-value">' . esc_html( $content_fields['_feedback_ip'] ) . '</div>';
373 }
374 echo '<div class="feedback_response__item-key">' . esc_html__( 'Source', 'jetpack' ) . '</div>';
375 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>';
376 echo '</div>';
377 }
378
379 /**
380 * Displays the value for the source column.
381 *
382 * @param \WP_Post $post Current post.
383 * @return void
384 */
385 function grunion_manage_post_column_source( $post ) {
386 if ( ! isset( $post->post_parent ) ) {
387 return;
388 }
389
390 $form_url = get_permalink( $post->post_parent );
391 $parsed_url = wp_parse_url( $form_url );
392
393 printf(
394 '<a href="%s" target="_blank" rel="noopener noreferrer">/%s</a>',
395 esc_url( $form_url ),
396 esc_html( basename( $parsed_url['path'] ) )
397 );
398 }
399
400 add_action( 'manage_posts_custom_column', 'grunion_manage_post_columns', 10, 2 );
401 /**
402 * Parse message content and display in appropriate columns.
403 *
404 * @param array $col List of columns available on admin page.
405 * @param int $post_id The current post ID.
406 * @return void
407 */
408 function grunion_manage_post_columns( $col, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
409 global $post;
410
411 /**
412 * Only call parse_fields_from_content if we're dealing with a Grunion custom column.
413 */
414 if ( ! in_array( $col, array( 'feedback_date', 'feedback_from', 'feedback_response', 'feedback_source' ), true ) ) {
415 return;
416 }
417
418 switch ( $col ) {
419 case 'feedback_date':
420 grunion_manage_post_column_date();
421 return;
422 case 'feedback_from':
423 grunion_manage_post_column_from( $post );
424 return;
425 case 'feedback_response':
426 grunion_manage_post_column_response( $post );
427 return;
428 case 'feedback_source':
429 grunion_manage_post_column_source( $post );
430 return;
431 }
432 }
433
434 add_action( 'restrict_manage_posts', 'grunion_source_filter' );
435 /**
436 * Add a post filter dropdown at the top of the admin page.
437 *
438 * @return void
439 */
440 function grunion_source_filter() {
441 $screen = get_current_screen();
442
443 if ( 'edit-feedback' !== $screen->id ) {
444 return;
445 }
446
447 $parent_id = intval( isset( $_GET['jetpack_form_parent_id'] ) ? $_GET['jetpack_form_parent_id'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
448 \Grunion_Contact_Form_Plugin::form_posts_dropdown( $parent_id );
449 }
450
451 add_action( 'pre_get_posts', 'grunion_source_filter_results' );
452 /**
453 * Filter feedback posts by parent_id if present.
454 *
455 * @param WP_Query $query Current query.
456 *
457 * @return void
458 */
459 function grunion_source_filter_results( $query ) {
460 $parent_id = intval( isset( $_GET['jetpack_form_parent_id'] ) ? $_GET['jetpack_form_parent_id'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
461
462 if ( ! $parent_id || $query->query_vars['post_type'] !== 'feedback' ) {
463 return;
464 }
465
466 // Don't apply to the filter dropdown query
467 if ( $query->query_vars['fields'] === 'id=>parent' ) {
468 return;
469 }
470
471 $query->query_vars['post_parent'] = $parent_id;
472 }
473
474 add_filter( 'post_row_actions', 'grunion_manage_post_row_actions', 10, 2 );
475 /**
476 * Add actions to feedback response rows in WP Admin.
477 *
478 * @param string[] $actions Default actions.
479 * @return string[]
480 */
481 function grunion_manage_post_row_actions( $actions ) {
482 global $post;
483
484 if ( 'feedback' !== $post->post_type ) {
485 return $actions;
486 }
487
488 $post_type_object = get_post_type_object( $post->post_type );
489 $actions = array();
490
491 if ( $post->post_status === 'trash' ) {
492 $actions['untrash'] = sprintf(
493 '<a title="%s" href="%s">%s</a>',
494 esc_attr__( 'Restore this item from the Trash', 'jetpack' ),
495 esc_url( wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&action=untrash', rawurlencode( $post->ID ) ) ) ), 'untrash-' . $post->post_type . '_' . $post->ID ),
496 esc_html__( 'Restore', 'jetpack' )
497 );
498 $actions['delete'] = sprintf(
499 '<a class="submitdelete" title="%s" href="%s">%s</a>',
500 esc_attr( __( 'Delete this item permanently', 'jetpack' ) ),
501 get_delete_post_link( $post->ID, '', true ),
502 esc_html__( 'Delete Permanently', 'jetpack' )
503 );
504 } elseif ( $post->post_status === 'publish' ) {
505 $actions['spam'] = sprintf(
506 '<a title="%s" href="%s">%s</a>',
507 esc_html__( 'Mark this message as spam', 'jetpack' ),
508 esc_url( wp_nonce_url( admin_url( 'admin-ajax.php?post_id=' . rawurlencode( $post->ID ) . '&action=spam' ) ), 'spam-feedback_' . $post->ID ),
509 esc_html__( 'Spam', 'jetpack' )
510 );
511 $actions['trash'] = sprintf(
512 '<a class="submitdelete" title="%s" href="%s">%s</a>',
513 esc_attr__( 'Trash', 'jetpack' ),
514 get_delete_post_link( $post->ID ),
515 esc_html__( 'Trash', 'jetpack' )
516 );
517 } elseif ( $post->post_status === 'spam' ) {
518 $actions['unspam unapprove'] = sprintf(
519 '<a title="%s" href="">%s</a>',
520 esc_html__( 'Mark this message as NOT spam', 'jetpack' ),
521 esc_html__( 'Not Spam', 'jetpack' )
522 );
523 $actions['delete'] = sprintf(
524 '<a class="submitdelete" title="%s" href="%s">%s</a>',
525 esc_attr( __( 'Delete this item permanently', 'jetpack' ) ),
526 get_delete_post_link( $post->ID, '', true ),
527 esc_html__( 'Delete Permanently', 'jetpack' )
528 );
529 }
530
531 return $actions;
532 }
533
534 /**
535 * Escape grunion attributes.
536 *
537 * @param string $attr - the attribute we're escaping.
538 *
539 * @return string
540 */
541 function grunion_esc_attr( $attr ) {
542 $out = esc_attr( $attr );
543 // we also have to entity-encode square brackets so they don't interfere with the shortcode parser
544 // FIXME: do this better - just stripping out square brackets for now since they mysteriously keep reappearing
545 $out = str_replace( '[', '', $out );
546 $out = str_replace( ']', '', $out );
547 return $out;
548 }
549
550 /**
551 * Sort grunion items.
552 *
553 * @param array $a - the first item we're sorting.
554 * @param array $b - the second item we're sorting.
555 *
556 * @return string
557 */
558 function grunion_sort_objects( $a, $b ) {
559 if ( isset( $a['order'] ) && isset( $b['order'] ) ) {
560 return $a['order'] - $b['order'];
561 }
562 return 0;
563 }
564
565 /**
566 * Take an array of field types from the form builder, and construct a shortcode form.
567 * returns both the shortcode form, and HTML markup representing a preview of the form
568 */
569 function grunion_ajax_shortcode() {
570 check_ajax_referer( 'grunion_shortcode' );
571
572 if ( ! current_user_can( 'edit_posts' ) ) {
573 die( '-1' );
574 }
575
576 $attributes = array();
577
578 foreach ( array( 'subject', 'to' ) as $attribute ) {
579 if ( isset( $_POST[ $attribute ] ) && is_scalar( $_POST[ $attribute ] ) && (string) $_POST[ $attribute ] !== '' ) {
580 $attributes[ $attribute ] = sanitize_text_field( wp_unslash( $_POST[ $attribute ] ) );
581 }
582 }
583
584 if ( isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ) {
585 $fields = sanitize_text_field( stripslashes_deep( $_POST['fields'] ) );
586 usort( $fields, 'grunion_sort_objects' );
587
588 $field_shortcodes = array();
589
590 foreach ( $fields as $field ) {
591 $field_attributes = array();
592
593 if ( isset( $field['required'] ) && 'true' === $field['required'] ) {
594 $field_attributes['required'] = 'true';
595 }
596
597 foreach ( array( 'options', 'label', 'type' ) as $attribute ) {
598 if ( isset( $field[ $attribute ] ) ) {
599 $field_attributes[ $attribute ] = $field[ $attribute ];
600 }
601 }
602
603 $field_shortcodes[] = new Grunion_Contact_Form_Field( $field_attributes );
604 }
605 }
606
607 $grunion = new Grunion_Contact_Form( $attributes, $field_shortcodes );
608
609 die( "\n$grunion\n" ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
610 }
611
612 /**
613 * Takes a post_id, extracts the contact-form shortcode from that post (if there is one), parses it,
614 * and constructs a json object representing its contents and attributes.
615 */
616 function grunion_ajax_shortcode_to_json() {
617 global $post;
618
619 check_ajax_referer( 'grunion_shortcode_to_json' );
620
621 if ( ! empty( $_POST['post_id'] ) && ! current_user_can( 'edit_post', (int) $_POST['post_id'] ) ) {
622 die( '-1' );
623 } elseif ( ! current_user_can( 'edit_posts' ) ) {
624 die( '-1' );
625 }
626
627 if ( ! isset( $_POST['content'] ) || ! is_numeric( $_POST['post_id'] ) ) {
628 die( '-1' );
629 }
630
631 $content = sanitize_text_field( wp_unslash( $_POST['content'] ) );
632
633 // doesn't look like a post with a [contact-form] already.
634 if ( false === has_shortcode( $content, 'contact-form' ) ) {
635 die( '' );
636 }
637
638 $post = get_post( (int) $_POST['post_id'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
639
640 do_shortcode( $content );
641
642 $grunion = Grunion_Contact_Form::$last;
643
644 $out = array(
645 'to' => '',
646 'subject' => '',
647 'fields' => array(),
648 );
649
650 foreach ( $grunion->fields as $field ) {
651 $out['fields'][ $field->get_attribute( 'id' ) ] = $field->attributes;
652 }
653
654 foreach ( array( 'to', 'subject' ) as $attribute ) {
655 $value = $grunion->get_attribute( $attribute );
656 if ( isset( $grunion->defaults[ $attribute ] ) && $value === $grunion->defaults[ $attribute ] ) {
657 $value = '';
658 }
659 $out[ $attribute ] = $value;
660 }
661
662 die( wp_json_encode( $out ) );
663 }
664
665 add_action( 'wp_ajax_grunion_shortcode', 'grunion_ajax_shortcode' );
666 add_action( 'wp_ajax_grunion_shortcode_to_json', 'grunion_ajax_shortcode_to_json' );
667
668 // process row-action spam/not spam clicks
669 add_action( 'wp_ajax_grunion_ajax_spam', 'grunion_ajax_spam' );
670
671 /**
672 * Handle marking feedback as spam.
673 */
674 function grunion_ajax_spam() {
675 global $wpdb;
676
677 if ( empty( $_POST['make_it'] ) ) {
678 return;
679 }
680
681 $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
682 check_ajax_referer( 'grunion-post-status' );
683 if ( ! current_user_can( 'edit_page', $post_id ) ) {
684 wp_die( esc_html__( 'You are not allowed to manage this item.', 'jetpack' ) );
685 }
686
687 require_once __DIR__ . '/grunion-contact-form.php';
688
689 $current_menu = '';
690 if ( isset( $_POST['sub_menu'] ) && preg_match( '|post_type=feedback|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
691 if ( preg_match( '|post_status=spam|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
692 $current_menu = 'spam';
693 } elseif ( preg_match( '|post_status=trash|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
694 $current_menu = 'trash';
695 } else {
696 $current_menu = 'messages';
697 }
698 }
699
700 $post = get_post( $post_id );
701 $post_type_object = get_post_type_object( $post->post_type );
702 $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
703 if ( $_POST['make_it'] === 'spam' ) {
704 $post->post_status = 'spam';
705 $status = wp_insert_post( $post );
706
707 /** This action is already documented in modules/contact-form/admin.php */
708 do_action( 'contact_form_akismet', 'spam', $akismet_values );
709 } elseif ( $_POST['make_it'] === 'ham' ) {
710 $post->post_status = 'publish';
711 $status = wp_insert_post( $post );
712
713 /** This action is already documented in modules/contact-form/admin.php */
714 do_action( 'contact_form_akismet', 'ham', $akismet_values );
715
716 $comment_author_email = false;
717 $reply_to_addr = false;
718 $message = false;
719 $to = false;
720 $headers = false;
721 $blog_url = wp_parse_url( site_url() );
722
723 // resend the original email
724 $email = get_post_meta( $post_id, '_feedback_email', true );
725 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post_id );
726
727 if ( ! empty( $email ) && ! empty( $content_fields ) ) {
728 if ( isset( $content_fields['_feedback_author_email'] ) ) {
729 $comment_author_email = $content_fields['_feedback_author_email'];
730 }
731
732 if ( isset( $email['to'] ) ) {
733 $to = $email['to'];
734 }
735
736 if ( isset( $email['message'] ) ) {
737 $message = $email['message'];
738 }
739
740 if ( isset( $email['headers'] ) ) {
741 $headers = $email['headers'];
742 } else {
743 $headers = 'From: "' . $content_fields['_feedback_author'] . '" <wordpress@' . $blog_url['host'] . ">\r\n";
744
745 if ( ! empty( $comment_author_email ) ) {
746 $reply_to_addr = $comment_author_email;
747 } elseif ( is_array( $to ) ) {
748 $reply_to_addr = $to[0];
749 }
750
751 if ( $reply_to_addr ) {
752 $headers .= 'Reply-To: "' . $content_fields['_feedback_author'] . '" <' . $reply_to_addr . ">\r\n";
753 }
754
755 $headers .= 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . '"';
756 }
757
758 /**
759 * Filters the subject of the email sent after a contact form submission.
760 *
761 * @module contact-form
762 *
763 * @since 3.0.0
764 *
765 * @param string $content_fields['_feedback_subject'] Feedback's subject line.
766 * @param array $content_fields['_feedback_all_fields'] Feedback's data from old fields.
767 */
768 $subject = apply_filters( 'contact_form_subject', $content_fields['_feedback_subject'], $content_fields['_feedback_all_fields'] );
769
770 Grunion_Contact_Form::wp_mail( $to, $subject, $message, $headers );
771 }
772 } elseif ( $_POST['make_it'] === 'publish' ) {
773 if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
774 wp_die( esc_html__( 'You are not allowed to move this item out of the Trash.', 'jetpack' ) );
775 }
776
777 if ( ! wp_untrash_post( $post_id ) ) {
778 wp_die( esc_html__( 'Error in restoring from Trash.', 'jetpack' ) );
779 }
780 } elseif ( $_POST['make_it'] === 'trash' ) {
781 if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
782 wp_die( esc_html__( 'You are not allowed to move this item to the Trash.', 'jetpack' ) );
783 }
784
785 if ( ! wp_trash_post( $post_id ) ) {
786 wp_die( esc_html__( 'Error in moving to Trash.', 'jetpack' ) );
787 }
788 }
789
790 $sql = "
791 SELECT post_status,
792 COUNT( * ) AS post_count
793 FROM `{$wpdb->posts}`
794 WHERE post_type = 'feedback'
795 GROUP BY post_status
796 ";
797 $status_count = (array) $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
798
799 $status = array();
800 $status_html = '';
801 foreach ( $status_count as $row ) {
802 $status[ $row['post_status'] ] = $row['post_count'];
803 }
804
805 if ( isset( $status['publish'] ) ) {
806 $status_html .= '<li><a href="edit.php?post_type=feedback"';
807 if ( $current_menu === 'messages' ) {
808 $status_html .= ' class="current"';
809 }
810
811 $status_html .= '>' . __( 'Messages', 'jetpack' ) . ' <span class="count">';
812 $status_html .= '(' . number_format( $status['publish'] ) . ')';
813 $status_html .= '</span></a> |</li>';
814 }
815
816 if ( isset( $status['trash'] ) ) {
817 $status_html .= '<li><a href="edit.php?post_status=trash&amp;post_type=feedback"';
818 if ( $current_menu === 'trash' ) {
819 $status_html .= ' class="current"';
820 }
821
822 $status_html .= '>' . __( 'Trash', 'jetpack' ) . ' <span class="count">';
823 $status_html .= '(' . number_format( $status['trash'] ) . ')';
824 $status_html .= '</span></a>';
825 if ( isset( $status['spam'] ) ) {
826 $status_html .= ' |';
827 }
828 $status_html .= '</li>';
829 }
830
831 if ( isset( $status['spam'] ) ) {
832 $status_html .= '<li><a href="edit.php?post_status=spam&amp;post_type=feedback"';
833 if ( $current_menu === 'spam' ) {
834 $status_html .= ' class="current"';
835 }
836
837 $status_html .= '>' . __( 'Spam', 'jetpack' ) . ' <span class="count">';
838 $status_html .= '(' . number_format( $status['spam'] ) . ')';
839 $status_html .= '</span></a></li>';
840 }
841
842 echo $status_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're building the html to echo.
843 exit;
844 }
845
846 /**
847 * Add the scripts that will add the "Check for Spam" button to the Feedbacks dashboard page.
848 */
849 function grunion_enable_spam_recheck() {
850 if ( ! defined( 'AKISMET_VERSION' ) ) {
851 return;
852 }
853
854 $screen = get_current_screen();
855
856 // Only add to feedback, only to non-spam view
857 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.
858 return;
859 }
860
861 // Add the actual "Check for Spam" button.
862 add_action( 'admin_head', 'grunion_check_for_spam_button' );
863 }
864
865 add_action( 'admin_enqueue_scripts', 'grunion_enable_spam_recheck' );
866
867 /**
868 * Add the JS and CSS necessary for the Feedback admin page to function.
869 */
870 function grunion_add_admin_scripts() {
871 $screen = get_current_screen();
872
873 if ( 'edit-feedback' !== $screen->id ) {
874 return;
875 }
876
877 // Add the scripts that handle the spam check event.
878 wp_register_script(
879 'grunion-admin',
880 Assets::get_file_url_for_environment(
881 '_inc/build/contact-form/js/grunion-admin.min.js',
882 'modules/contact-form/js/grunion-admin.js'
883 ),
884 array( 'jquery' ),
885 JETPACK__VERSION,
886 true
887 );
888
889 wp_enqueue_script( 'grunion-admin' );
890
891 wp_enqueue_style( 'grunion.css' );
892
893 // Only add to feedback, only to spam view.
894 if ( empty( $_GET['post_status'] ) || 'spam' !== $_GET['post_status'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not making site changes with this check
895 return;
896 }
897
898 $feedbacks_count = wp_count_posts( 'feedback' );
899 $nonce = wp_create_nonce( 'jetpack_delete_spam_feedbacks' );
900 $success_url = remove_query_arg( array( 'jetpack_empty_feedback_spam_error', 'post_status' ) ); // Go to the "All Feedback" page.
901 $failure_url = add_query_arg( 'jetpack_empty_feedback_spam_error', '1' ); // Refresh the current page and show an error.
902 $spam_count = $feedbacks_count->spam;
903
904 $button_parameters = array(
905 /* translators: The placeholder is for showing how much of the process has completed, as a percent. e.g., "Emptying Spam (40%)" */
906 'progress_label' => __( 'Emptying Spam (%1$s%)', 'jetpack' ),
907 'success_url' => $success_url,
908 'failure_url' => $failure_url,
909 'spam_count' => $spam_count,
910 'nonce' => $nonce,
911 'label' => __( 'Empty Spam', 'jetpack' ),
912 );
913
914 wp_localize_script( 'grunion-admin', 'jetpack_empty_spam_button_parameters', $button_parameters );
915 }
916
917 add_action( 'admin_enqueue_scripts', 'grunion_add_admin_scripts' );
918
919 /**
920 * Adds the 'Export' button to the feedback dashboard page.
921 *
922 * @return void
923 */
924 function grunion_export_button() {
925 $current_screen = get_current_screen();
926 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
927 return;
928 }
929
930 if ( ! current_user_can( 'export' ) ) {
931 return;
932 }
933
934 // if there aren't any feedbacks, bail out
935 if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
936 return;
937 }
938
939 $nonce_name = 'feedback_export_nonce';
940
941 $button_html = get_submit_button(
942 __( 'Export', 'jetpack' ),
943 'primary',
944 'jetpack-export-feedback',
945 false,
946 array(
947 'data-nonce-name' => $nonce_name,
948 )
949 );
950
951 $button_html .= wp_nonce_field( 'feedback_export', $nonce_name, false, false );
952 ?>
953 <script type="text/javascript">
954 jQuery( function ( $ ) {
955 $( '#posts-filter #post-query-submit' ).after( <?php echo wp_json_encode( $button_html ); ?> );
956 } );
957 </script>
958 <?php
959 }
960
961 /**
962 * Add the "Check for Spam" button to the Feedbacks dashboard page.
963 */
964 function grunion_check_for_spam_button() {
965 // Nonce name.
966 $nonce_name = 'jetpack_check_feedback_spam_' . (string) get_current_blog_id();
967 // Get HTML for the button.
968 $button_html = get_submit_button(
969 __( 'Check for Spam', 'jetpack' ),
970 'secondary',
971 'jetpack-check-feedback-spam',
972 false,
973 array(
974 'data-failure-url' => add_query_arg( 'jetpack_check_feedback_spam_error', '1' ), // Refresh the current page and show an error.
975 'data-nonce-name' => $nonce_name,
976 )
977 );
978 $button_html .= '<span class="jetpack-check-feedback-spam-spinner"></span>';
979 $button_html .= wp_nonce_field( 'grunion_recheck_queue', $nonce_name, false, false );
980
981 // Add the button next to the filter button via js.
982 ?>
983 <script type="text/javascript">
984 jQuery( function( $ ) {
985 $( '.tablenav.bottom .bulkactions' ).append( <?php echo wp_json_encode( $button_html ); ?> );
986 } );
987 </script>
988 <?php
989 }
990
991 /**
992 * Recheck all approved feedbacks for spam.
993 */
994 function grunion_recheck_queue() {
995 $blog_id = get_current_blog_id();
996
997 if (
998 empty( $_POST[ 'jetpack_check_feedback_spam_' . (string) $blog_id ] )
999 || ! wp_verify_nonce( sanitize_key( $_POST[ 'jetpack_check_feedback_spam_' . (string) $blog_id ] ), 'grunion_recheck_queue' )
1000 ) {
1001 wp_send_json_error(
1002 __( 'You aren’t authorized to do that.', 'jetpack' ),
1003 403
1004 );
1005
1006 return;
1007 }
1008
1009 if ( ! current_user_can( 'delete_others_posts' ) ) {
1010 wp_send_json_error(
1011 __( 'You don’t have permission to do that.', 'jetpack' ),
1012 403
1013 );
1014
1015 return;
1016 }
1017
1018 $query = 'post_type=feedback&post_status=publish';
1019
1020 if ( isset( $_POST['limit'], $_POST['offset'] ) ) {
1021 $query .= '&posts_per_page=' . (int) $_POST['limit'] . '&offset=' . (int) $_POST['offset'];
1022 }
1023
1024 $approved_feedbacks = get_posts( $query );
1025
1026 foreach ( $approved_feedbacks as $feedback ) {
1027 $meta = get_post_meta( $feedback->ID, '_feedback_akismet_values', true );
1028
1029 if ( ! $meta ) {
1030 // _feedback_akismet_values is eventually deleted when it's no longer
1031 // within a reasonable time period to check the feedback for spam, so
1032 // if it's gone, don't attempt a spam recheck.
1033 continue;
1034 }
1035
1036 $meta['recheck_reason'] = 'recheck_queue';
1037
1038 /**
1039 * Filter whether the submitted feedback is considered as spam.
1040 *
1041 * @module contact-form
1042 *
1043 * @since 3.4.0
1044 *
1045 * @param bool false Is the submitted feedback spam? Default to false.
1046 * @param array $meta Feedack values returned by the Akismet plugin.
1047 */
1048 $is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $meta );
1049
1050 if ( $is_spam ) {
1051 wp_update_post(
1052 array(
1053 'ID' => $feedback->ID,
1054 'post_status' => 'spam',
1055 )
1056 );
1057 /** This action is already documented in modules/contact-form/admin.php */
1058 do_action( 'contact_form_akismet', 'spam', $meta );
1059 }
1060 }
1061
1062 wp_send_json(
1063 array(
1064 'processed' => count( $approved_feedbacks ),
1065 )
1066 );
1067 }
1068
1069 add_action( 'wp_ajax_grunion_recheck_queue', 'grunion_recheck_queue' );
1070
1071 /**
1072 * Delete a number of spam feedbacks via an AJAX request.
1073 */
1074 function grunion_delete_spam_feedbacks() {
1075 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.
1076 wp_send_json_error(
1077 __( 'You aren’t authorized to do that.', 'jetpack' ),
1078 403
1079 );
1080
1081 return;
1082 }
1083
1084 if ( ! current_user_can( 'delete_others_posts' ) ) {
1085 wp_send_json_error(
1086 __( 'You don’t have permission to do that.', 'jetpack' ),
1087 403
1088 );
1089
1090 return;
1091 }
1092
1093 $deleted_feedbacks = 0;
1094
1095 $delete_limit = 25;
1096 /**
1097 * Filter the amount of Spam feedback one can delete at once.
1098 *
1099 * @module contact-form
1100 *
1101 * @since 8.7.0
1102 *
1103 * @param int $delete_limit Number of spam to process at once. Default to 25.
1104 */
1105 $delete_limit = apply_filters( 'jetpack_delete_spam_feedbacks_limit', $delete_limit );
1106 $delete_limit = (int) $delete_limit;
1107 $delete_limit = max( 1, min( 100, $delete_limit ) ); // Allow a range of 1-100 for the delete limit.
1108
1109 $query_args = array(
1110 'post_type' => 'feedback',
1111 'post_status' => 'spam',
1112 'posts_per_page' => $delete_limit,
1113 );
1114
1115 $query = new WP_Query( $query_args );
1116 $spam_feedbacks = $query->get_posts();
1117
1118 foreach ( $spam_feedbacks as $feedback ) {
1119 wp_delete_post( $feedback->ID, true );
1120
1121 ++$deleted_feedbacks;
1122 }
1123
1124 wp_send_json(
1125 array(
1126 'success' => true,
1127 'data' => array(
1128 'counts' => array(
1129 'deleted' => $deleted_feedbacks,
1130 'limit' => $delete_limit,
1131 ),
1132 ),
1133 )
1134 );
1135 }
1136 add_action( 'wp_ajax_jetpack_delete_spam_feedbacks', 'grunion_delete_spam_feedbacks' );
1137
1138 /**
1139 * Show an admin notice if the "Empty Spam" or "Check Spam" process was unable to complete, probably due to a permissions error.
1140 */
1141 function grunion_feedback_admin_notice() {
1142 if ( isset( $_GET['jetpack_empty_feedback_spam_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1143 echo '<div class="notice notice-error"><p>' . esc_html( __( 'An error occurred while trying to empty the Feedback spam folder.', 'jetpack' ) ) . '</p></div>';
1144 } elseif ( isset( $_GET['jetpack_check_feedback_spam_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1145 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>';
1146 }
1147 }
1148 add_action( 'admin_notices', 'grunion_feedback_admin_notice' );
1149
1150 /**
1151 * Class Grunion_Admin
1152 *
1153 * Singleton for Grunion admin area support.
1154 */
1155 class Grunion_Admin {
1156 /**
1157 * CSV export nonce field name
1158 *
1159 * @var string The nonce field name for CSV export.
1160 */
1161 private $export_nonce_field_csv = 'feedback_export_nonce_csv';
1162
1163 /**
1164 * GDrive export nonce field name
1165 *
1166 * @var string The nonce field name for GDrive export.
1167 */
1168 private $export_nonce_field_gdrive = 'feedback_export_nonce_gdrive';
1169
1170 /**
1171 * Instantiates this singleton class
1172 *
1173 * @return Grunion_Admin The Grunion Admin class instance.
1174 */
1175 public static function init() {
1176 static $instance = false;
1177
1178 if ( ! $instance ) {
1179 $instance = new Grunion_Admin();
1180 }
1181
1182 return $instance;
1183 }
1184
1185 /**
1186 * Grunion_Admin constructor
1187 */
1188 public function __construct() {
1189 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
1190 add_action( 'admin_footer-edit.php', array( $this, 'print_export_modal' ) );
1191
1192 add_action( 'wp_ajax_grunion_export_to_gdrive', array( $this, 'export_to_gdrive' ) );
1193 add_action( 'wp_ajax_grunion_gdrive_connection', array( $this, 'test_gdrive_connection' ) );
1194 }
1195
1196 /**
1197 * Hook handler for admin_enqueue_scripts hook
1198 */
1199 public function admin_enqueue_scripts() {
1200 $current_screen = get_current_screen();
1201 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
1202 return;
1203 }
1204 add_thickbox();
1205 $localized_strings = array(
1206 'exportError' => esc_js( __( 'There was an error exporting your results', 'jetpack' ) ),
1207 'waitingConnection' => esc_js( __( 'Waiting for connection...', 'jetpack' ) ),
1208 );
1209 wp_localize_script( 'grunion-admin', 'exportParameters', $localized_strings );
1210 }
1211
1212 /**
1213 * Prints the modal markup with export buttons/content.
1214 */
1215 public function print_export_modal() {
1216 if ( ! current_user_can( 'export' ) ) {
1217 return;
1218 }
1219
1220 // if there aren't any feedbacks, bail out
1221 if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
1222 return;
1223 }
1224
1225 $current_screen = get_current_screen();
1226 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
1227 return;
1228 }
1229
1230 $jetpack_logo = new Logo();
1231 ?>
1232 <div id="feedback-export-modal" style="display: none;">
1233 <div class="feedback-export-modal__wrapper">
1234 <div class="feedback-export-modal__header">
1235 <h1 class="feedback-export-modal__header-title"><?php esc_html_e( 'Export your Form Responses', 'jetpack' ); ?></h1>
1236 <p class="feedback-export-modal__header-subtitle"><?php esc_html_e( 'Choose your favorite file format or export destination:', 'jetpack' ); ?></p>
1237 </div>
1238 <div class="feedback-export-modal__content">
1239 <?php $this->get_csv_export_section(); ?>
1240 <?php $this->get_gdrive_export_section(); ?>
1241 </div>
1242 <div class="feedback-export-modal__footer">
1243 <div class="feedback-export-modal__footer-column">
1244 <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">
1245 <?php echo $jetpack_logo->get_jp_emblem(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1246 </a>
1247 <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">
1248 <?php echo esc_html_x( 'Jetpack Forms', 'Name of Jetpack’s Contact Form feature', 'jetpack' ); ?>
1249 </a>
1250 </div>
1251 <div class="feedback-export-modal__footer-column">
1252 <a href="https://automattic.com" title="Automattic" rel="noopener noreferer" target="_blank" class="feedback-export-modal__footer-link">
1253 <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">
1254 <desc id="jp-automattic-byline-logo-title"><?php esc_html_e( 'An Automattic Airline', 'jetpack' ); ?></desc>
1255 <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>
1256 <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>
1257 </svg>
1258 </a>
1259 </div>
1260 </div>
1261 </div>
1262 </div>
1263 <?php
1264 $opener_label = esc_html__( 'Export', 'jetpack' );
1265 $export_modal_opener = wp_is_mobile()
1266 ? "<a id='export-modal-opener' class='button button-primary' href='#TB_inline?&width=550&height=550&inlineId=feedback-export-modal'>{$opener_label}</a>"
1267 : "<a id='export-modal-opener' class='button button-primary' href='#TB_inline?&width=680&height=600&inlineId=feedback-export-modal'>{$opener_label}</a>";
1268 ?>
1269 <script type="text/javascript">
1270 jQuery( function( $ ) {
1271 $( '#posts-filter #post-query-submit' ).after( <?php echo wp_json_encode( $export_modal_opener ); ?> );
1272 } );
1273 </script>
1274 <?php
1275 }
1276
1277 /**
1278 * Ajax handler for wp_ajax_grunion_export_to_gdrive.
1279 * Exports data to Google Drive, based on POST data.
1280 *
1281 * @see Grunion_Contact_Form_Plugin::get_feedback_entries_from_post
1282 */
1283 public function export_to_gdrive() {
1284 $post_data = wp_unslash( $_POST );
1285 if (
1286 ! current_user_can( 'export' )
1287 || empty( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ) )
1288 || ! wp_verify_nonce( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ), 'feedback_export' )
1289 ) {
1290 wp_send_json_error(
1291 __( 'You aren’t authorized to do that.', 'jetpack' ),
1292 403
1293 );
1294
1295 return;
1296 }
1297
1298 $grunion = Grunion_Contact_Form_Plugin::init();
1299 $export_data = $grunion->get_feedback_entries_from_post();
1300
1301 $fields = array_keys( $export_data );
1302 $row_count = count( reset( $export_data ) );
1303
1304 $sheet_data = array( $fields );
1305
1306 for ( $i = 0; $i < $row_count; $i++ ) {
1307
1308 $current_row = array();
1309
1310 /**
1311 * Put all the fields in `$current_row` array.
1312 */
1313 foreach ( $fields as $single_field_name ) {
1314 $current_row[] = $export_data[ $single_field_name ][ $i ];
1315 }
1316
1317 $sheet_data[] = $current_row;
1318 }
1319
1320 $user_id = (int) get_current_user_id();
1321
1322 if ( ! empty( $post_data['post'] ) && $post_data['post'] !== 'all' ) {
1323 $spreadsheet_title = sprintf(
1324 '%1$s - %2$s',
1325 $this->get_export_filename( get_the_title( (int) $post_data['post'] ) ),
1326 gmdate( 'Y-m-d H:i' )
1327 );
1328 } else {
1329 $spreadsheet_title = sprintf( '%s - %s', $this->get_export_filename(), gmdate( 'Y-m-d H:i' ) );
1330 }
1331
1332 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1333 $sheet = Jetpack_Google_Drive_Helper::create_sheet( $user_id, $spreadsheet_title, $sheet_data );
1334
1335 $grunion->record_tracks_event( 'forms_export_responses', array( 'format' => 'gsheets' ) );
1336
1337 wp_send_json(
1338 array(
1339 'success' => ! is_wp_error( $sheet ),
1340 'data' => $sheet,
1341 )
1342 );
1343 }
1344
1345 /**
1346 * Return HTML markup for the CSV download button.
1347 */
1348 public function get_csv_export_section() {
1349 $button_csv_html = get_submit_button(
1350 esc_html__( 'Download', 'jetpack' ),
1351 'primary export-button export-csv',
1352 'jetpack-export-feedback-csv',
1353 false,
1354 array( 'data-nonce-name' => $this->export_nonce_field_csv )
1355 );
1356 ?>
1357 <div class="export-card">
1358 <div class="export-card__header">
1359 <svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
1360 <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"/>
1361 </svg>
1362 <div class="export-card__header-title"><?php esc_html_e( 'CSV File', 'jetpack' ); ?></div>
1363 </div>
1364 <div class="export-card__body">
1365 <div class="export-card__body-description">
1366 <?php esc_html_e( 'Download your form response data via CSV file.', 'jetpack' ); ?>
1367 </div>
1368 <div class="export-card__body-cta">
1369 <?php
1370 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1371 echo $button_csv_html;
1372 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1373 echo wp_nonce_field( 'feedback_export', $this->export_nonce_field_csv, false, false );
1374 ?>
1375 </div>
1376 </div>
1377 </div>
1378 <?php
1379 }
1380
1381 /**
1382 * Render/output HTML markup for the export to gdrive section.
1383 * If the user doesn't hold a Google Drive connection a button to connect will render (See grunion-admin.js).
1384 */
1385 public function get_gdrive_export_section() {
1386 $user_connected = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || ( new Connection_Manager( 'jetpack' ) )->is_user_connected( get_current_user_id() );
1387 if ( ! $user_connected ) {
1388 return;
1389 }
1390
1391 $user_id = (int) get_current_user_id();
1392
1393 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1394 $has_valid_connection = Jetpack_Google_Drive_Helper::has_valid_connection( $user_id );
1395
1396 if ( $has_valid_connection ) {
1397 $button_html = $this->get_gdrive_export_button_markup();
1398 } else {
1399 $slug = 'jetpack-form-responses-connect';
1400 $button_html = sprintf(
1401 '<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>',
1402 esc_url( Redirect::get_url( $slug ) ),
1403 esc_attr__( 'connect to Google Drive', 'jetpack' ),
1404 esc_html__( 'Connect Google Drive', 'jetpack' ),
1405 $slug,
1406 $this->export_nonce_field_gdrive
1407 );
1408 }
1409
1410 ?>
1411 <div class="export-card">
1412 <div class="export-card__header">
1413 <svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1414 <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"/>
1415 </svg>
1416 <div class="export-card__header-title"><?php esc_html_e( 'Google Sheets', 'jetpack' ); ?></div>
1417 <div class="export-card__beta-badge">BETA</div>
1418 </div>
1419 <div class="export-card__body">
1420 <div class="export-card__body-description">
1421 <div>
1422 <?php esc_html_e( 'Export your data into a Google Sheets file.', 'jetpack' ); ?>
1423 <?php
1424 printf(
1425 '<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferer">%3$s</a>',
1426 esc_url( Redirect::get_url( 'jetpack-support-contact-form-export' ) ),
1427 esc_attr__( 'connect to Google Drive', 'jetpack' ),
1428 esc_html__( 'You need to connect to Google Drive.', 'jetpack' )
1429 );
1430 ?>
1431 </div>
1432 <p class="export-card__body-description-footer"><?php esc_html_e( 'This premium feature is currently free to use in beta.', 'jetpack' ); ?></p>
1433 </div>
1434 <div class="export-card__body-cta">
1435 <?php
1436 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1437 echo $button_html;
1438 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1439 echo wp_nonce_field( 'feedback_export', $this->export_nonce_field_gdrive, false, false );
1440 ?>
1441 </div>
1442 </div>
1443 </div>
1444 <?php
1445 }
1446
1447 /**
1448 * Ajax handler. Sends a payload with connection status and html to replace
1449 * the Connect button with the Export button using get_gdrive_export_button
1450 */
1451 public function test_gdrive_connection() {
1452 $post_data = wp_unslash( $_POST );
1453 $user_id = (int) get_current_user_id();
1454
1455 if (
1456 ! $user_id ||
1457 ! current_user_can( 'export' ) ||
1458 empty( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ) ) ||
1459 ! wp_verify_nonce( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ), 'feedback_export' )
1460 ) {
1461 wp_send_json_error(
1462 __( 'You aren’t authorized to do that.', 'jetpack' ),
1463 403
1464 );
1465
1466 return;
1467 }
1468
1469 if ( ! class_exists( 'Jetpack_Google_Drive_Helper' ) ) {
1470 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1471 }
1472 $has_valid_connection = Jetpack_Google_Drive_Helper::has_valid_connection( $user_id );
1473
1474 $replacement_html = $has_valid_connection
1475 ? $this->get_gdrive_export_button_markup()
1476 : '';
1477
1478 wp_send_json(
1479 array(
1480 'connection' => $has_valid_connection,
1481 'html' => $replacement_html,
1482 )
1483 );
1484 }
1485
1486 /**
1487 * Markup helper so we DRY, returns the button markup for the export to GDrive feature.
1488 *
1489 * @return string The HTML button markup
1490 */
1491 public function get_gdrive_export_button_markup() {
1492 return get_submit_button(
1493 esc_html__( 'Export', 'jetpack' ),
1494 'primary export-button export-gdrive',
1495 'jetpack-export-feedback-gdrive',
1496 false,
1497 array( 'data-nonce-name' => $this->export_nonce_field_gdrive )
1498 );
1499 }
1500
1501 /**
1502 * Get a filename for export tasks
1503 *
1504 * @param string $source The filtered source for exported data.
1505 * @return string The filename without source nor date suffix.
1506 */
1507 public function get_export_filename( $source = '' ) {
1508 return $source === ''
1509 ? sprintf(
1510 /* translators: Site title, used to craft the export filename, eg "MySite - Jetpack Form Responses" */
1511 __( '%s - Jetpack Form Responses', 'jetpack' ),
1512 sanitize_file_name( get_bloginfo( 'name' ) )
1513 )
1514 : sprintf(
1515 /* translators: 1: Site title; 2: post title. Used to craft the export filename, eg "MySite - Jetpack Form Responses - Contact" */
1516 __( '%1$s - Jetpack Form Responses - %2$s', 'jetpack' ),
1517 sanitize_file_name( get_bloginfo( 'name' ) ),
1518 sanitize_file_name( $source )
1519 );
1520 }
1521 }
1522
1523 Grunion_admin::init();
1524