PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 12.6.4
Jetpack – WP Security, Backup, Speed, & Growth v12.6.4
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 2 years ago images 3 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 2 years ago grunion-response-email-template.php 3 years ago
admin.php
1526 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 ( ! empty( $content_fields['_feedback_author'] ) ) {
288 echo esc_html( $content_fields['_feedback_author'] );
289 return;
290 }
291
292 if ( ! empty( $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 ( ! empty( $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 $content_fields = array();
317 $non_printable_keys = array(
318 'email_marketing_consent',
319 'entry_title',
320 'entry_permalink',
321 'feedback_id',
322 );
323
324 $post_content = get_post_field( 'post_content', $post->ID );
325 $content = explode( '<!--more-->', $post_content );
326 $content = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
327 $chunks = explode( "\nJSON_DATA", $content );
328
329 $response_fields = array();
330
331 if ( is_array( $chunks ) && isset( $chunks[1] ) ) {
332 $rearray = json_decode( $chunks[1], true );
333 if ( is_array( $rearray ) && isset( $rearray['feedback_id'] ) ) {
334 $response_fields = $rearray;
335 }
336 }
337
338 if ( empty( $response_fields ) ) {
339 $chunks = explode( "\nArray", $content );
340 if ( $chunks[1] ) {
341 // re-construct the array string
342 $array = 'Array' . $chunks[1];
343 // re-construct the array
344 $rearray = Grunion_Contact_Form_Plugin::reverse_that_print( $array, true );
345 $response_fields = is_array( $rearray ) ? $rearray : array();
346 } else {
347 // couldn't reconstruct array, use the old method
348 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post->ID );
349 $response_fields = isset( $content_fields['_feedback_all_fields'] ) ? $content_fields['_feedback_all_fields'] : array();
350 }
351 }
352
353 $response_fields = array_diff_key( $response_fields, array_flip( $non_printable_keys ) );
354
355 echo '<hr class="feedback_response__mobile-separator" />';
356 echo '<div class="feedback_response__item">';
357 foreach ( $response_fields as $key => $value ) {
358 if ( is_array( $value ) ) {
359 $value = implode( ', ', $value );
360 }
361 printf(
362 '<div class="feedback_response__item-key">%s</div><div class="feedback_response__item-value">%s</div>',
363 esc_html( preg_replace( '#^\d+_#', '', $key ) ),
364 nl2br( esc_html( $value ) )
365 );
366 }
367 echo '</div>';
368 echo '<hr />';
369
370 echo '<div class="feedback_response__item">';
371 if ( isset( $content_fields['_feedback_ip'] ) ) {
372 echo '<div class="feedback_response__item-key">' . esc_html__( 'IP', 'jetpack' ) . '</div>';
373 echo '<div class="feedback_response__item-value">' . esc_html( $content_fields['_feedback_ip'] ) . '</div>';
374 }
375 echo '<div class="feedback_response__item-key">' . esc_html__( 'Source', 'jetpack' ) . '</div>';
376 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>';
377 echo '</div>';
378 }
379
380 /**
381 * Displays the value for the source column.
382 *
383 * @param \WP_Post $post Current post.
384 * @return void
385 */
386 function grunion_manage_post_column_source( $post ) {
387 if ( ! isset( $post->post_parent ) ) {
388 return;
389 }
390
391 $form_url = get_permalink( $post->post_parent );
392 $parsed_url = wp_parse_url( $form_url );
393
394 printf(
395 '<a href="%s" target="_blank" rel="noopener noreferrer">/%s</a>',
396 esc_url( $form_url ),
397 esc_html( basename( $parsed_url['path'] ) )
398 );
399 }
400
401 add_action( 'manage_posts_custom_column', 'grunion_manage_post_columns', 10, 2 );
402 /**
403 * Parse message content and display in appropriate columns.
404 *
405 * @param array $col List of columns available on admin page.
406 * @param int $post_id The current post ID.
407 * @return void
408 */
409 function grunion_manage_post_columns( $col, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
410 global $post;
411
412 /**
413 * Only call parse_fields_from_content if we're dealing with a Grunion custom column.
414 */
415 if ( ! in_array( $col, array( 'feedback_date', 'feedback_from', 'feedback_response', 'feedback_source' ), true ) ) {
416 return;
417 }
418
419 switch ( $col ) {
420 case 'feedback_date':
421 grunion_manage_post_column_date();
422 return;
423 case 'feedback_from':
424 grunion_manage_post_column_from( $post );
425 return;
426 case 'feedback_response':
427 grunion_manage_post_column_response( $post );
428 return;
429 case 'feedback_source':
430 grunion_manage_post_column_source( $post );
431 return;
432 }
433 }
434
435 add_action( 'restrict_manage_posts', 'grunion_source_filter' );
436 /**
437 * Add a post filter dropdown at the top of the admin page.
438 *
439 * @return void
440 */
441 function grunion_source_filter() {
442 $screen = get_current_screen();
443
444 if ( 'edit-feedback' !== $screen->id ) {
445 return;
446 }
447
448 $parent_id = intval( isset( $_GET['jetpack_form_parent_id'] ) ? $_GET['jetpack_form_parent_id'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
449 \Grunion_Contact_Form_Plugin::form_posts_dropdown( $parent_id );
450 }
451
452 add_action( 'pre_get_posts', 'grunion_source_filter_results' );
453 /**
454 * Filter feedback posts by parent_id if present.
455 *
456 * @param WP_Query $query Current query.
457 *
458 * @return void
459 */
460 function grunion_source_filter_results( $query ) {
461 $parent_id = intval( isset( $_GET['jetpack_form_parent_id'] ) ? $_GET['jetpack_form_parent_id'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
462
463 if ( ! $parent_id || $query->query_vars['post_type'] !== 'feedback' ) {
464 return;
465 }
466
467 // Don't apply to the filter dropdown query
468 if ( $query->query_vars['fields'] === 'id=>parent' ) {
469 return;
470 }
471
472 $query->query_vars['post_parent'] = $parent_id;
473 }
474
475 add_filter( 'post_row_actions', 'grunion_manage_post_row_actions', 10, 2 );
476 /**
477 * Add actions to feedback response rows in WP Admin.
478 *
479 * @param string[] $actions Default actions.
480 * @return string[]
481 */
482 function grunion_manage_post_row_actions( $actions ) {
483 global $post;
484
485 if ( 'feedback' !== $post->post_type ) {
486 return $actions;
487 }
488
489 $post_type_object = get_post_type_object( $post->post_type );
490 $actions = array();
491
492 if ( $post->post_status === 'trash' ) {
493 $actions['untrash'] = sprintf(
494 '<a title="%s" href="%s">%s</a>',
495 esc_attr__( 'Restore this item from the Trash', 'jetpack' ),
496 esc_url( wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&action=untrash', rawurlencode( $post->ID ) ) ) ), 'untrash-' . $post->post_type . '_' . $post->ID ),
497 esc_html__( 'Restore', 'jetpack' )
498 );
499 $actions['delete'] = sprintf(
500 '<a class="submitdelete" title="%s" href="%s">%s</a>',
501 esc_attr( __( 'Delete this item permanently', 'jetpack' ) ),
502 get_delete_post_link( $post->ID, '', true ),
503 esc_html__( 'Delete Permanently', 'jetpack' )
504 );
505 } elseif ( $post->post_status === 'publish' ) {
506 $actions['spam'] = sprintf(
507 '<a title="%s" href="%s">%s</a>',
508 esc_html__( 'Mark this message as spam', 'jetpack' ),
509 esc_url( wp_nonce_url( admin_url( 'admin-ajax.php?post_id=' . rawurlencode( $post->ID ) . '&action=spam' ) ), 'spam-feedback_' . $post->ID ),
510 esc_html__( 'Spam', 'jetpack' )
511 );
512 $actions['trash'] = sprintf(
513 '<a class="submitdelete" title="%s" href="%s">%s</a>',
514 esc_attr_x( 'Trash', 'verb', 'jetpack' ),
515 get_delete_post_link( $post->ID ),
516 esc_html_x( 'Trash', 'verb', 'jetpack' )
517 );
518 } elseif ( $post->post_status === 'spam' ) {
519 $actions['unspam unapprove'] = sprintf(
520 '<a title="%s" href="">%s</a>',
521 esc_html__( 'Mark this message as NOT spam', 'jetpack' ),
522 esc_html__( 'Not Spam', 'jetpack' )
523 );
524 $actions['delete'] = sprintf(
525 '<a class="submitdelete" title="%s" href="%s">%s</a>',
526 esc_attr( __( 'Delete this item permanently', 'jetpack' ) ),
527 get_delete_post_link( $post->ID, '', true ),
528 esc_html__( 'Delete Permanently', 'jetpack' )
529 );
530 }
531
532 return $actions;
533 }
534
535 /**
536 * Escape grunion attributes.
537 *
538 * @param string $attr - the attribute we're escaping.
539 *
540 * @return string
541 */
542 function grunion_esc_attr( $attr ) {
543 $out = esc_attr( $attr );
544 // we also have to entity-encode square brackets so they don't interfere with the shortcode parser
545 // FIXME: do this better - just stripping out square brackets for now since they mysteriously keep reappearing
546 $out = str_replace( '[', '', $out );
547 $out = str_replace( ']', '', $out );
548 return $out;
549 }
550
551 /**
552 * Sort grunion items.
553 *
554 * @param array $a - the first item we're sorting.
555 * @param array $b - the second item we're sorting.
556 *
557 * @return string
558 */
559 function grunion_sort_objects( $a, $b ) {
560 if ( isset( $a['order'] ) && isset( $b['order'] ) ) {
561 return $a['order'] - $b['order'];
562 }
563 return 0;
564 }
565
566 /**
567 * Take an array of field types from the form builder, and construct a shortcode form.
568 * returns both the shortcode form, and HTML markup representing a preview of the form
569 */
570 function grunion_ajax_shortcode() {
571 $field_shortcodes = array();
572 check_ajax_referer( 'grunion_shortcode' );
573
574 if ( ! current_user_can( 'edit_posts' ) ) {
575 die( '-1' );
576 }
577
578 $attributes = array();
579
580 foreach ( array( 'subject', 'to' ) as $attribute ) {
581 if ( isset( $_POST[ $attribute ] ) && is_scalar( $_POST[ $attribute ] ) && (string) $_POST[ $attribute ] !== '' ) {
582 $attributes[ $attribute ] = sanitize_text_field( wp_unslash( $_POST[ $attribute ] ) );
583 }
584 }
585
586 if ( isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ) {
587 $fields = sanitize_text_field( stripslashes_deep( $_POST['fields'] ) );
588 usort( $fields, 'grunion_sort_objects' );
589
590 $field_shortcodes = array();
591
592 foreach ( $fields as $field ) {
593 $field_attributes = array();
594
595 if ( isset( $field['required'] ) && 'true' === $field['required'] ) {
596 $field_attributes['required'] = 'true';
597 }
598
599 foreach ( array( 'options', 'label', 'type' ) as $attribute ) {
600 if ( isset( $field[ $attribute ] ) ) {
601 $field_attributes[ $attribute ] = $field[ $attribute ];
602 }
603 }
604
605 $field_shortcodes[] = new Grunion_Contact_Form_Field( $field_attributes );
606 }
607 }
608
609 $grunion = new Grunion_Contact_Form( $attributes, $field_shortcodes );
610
611 die( "\n$grunion\n" ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
612 }
613
614 /**
615 * Takes a post_id, extracts the contact-form shortcode from that post (if there is one), parses it,
616 * and constructs a json object representing its contents and attributes.
617 */
618 function grunion_ajax_shortcode_to_json() {
619 global $post;
620
621 check_ajax_referer( 'grunion_shortcode_to_json' );
622
623 if ( ! empty( $_POST['post_id'] ) && ! current_user_can( 'edit_post', (int) $_POST['post_id'] ) ) {
624 die( '-1' );
625 } elseif ( ! current_user_can( 'edit_posts' ) ) {
626 die( '-1' );
627 }
628
629 if ( ! isset( $_POST['content'] ) || ! is_numeric( $_POST['post_id'] ) ) {
630 die( '-1' );
631 }
632
633 $content = sanitize_text_field( wp_unslash( $_POST['content'] ) );
634
635 // doesn't look like a post with a [contact-form] already.
636 if ( false === has_shortcode( $content, 'contact-form' ) ) {
637 die( '' );
638 }
639
640 $post = get_post( (int) $_POST['post_id'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
641
642 do_shortcode( $content );
643
644 $grunion = Grunion_Contact_Form::$last;
645
646 $out = array(
647 'to' => '',
648 'subject' => '',
649 'fields' => array(),
650 );
651
652 foreach ( $grunion->fields as $field ) {
653 $out['fields'][ $field->get_attribute( 'id' ) ] = $field->attributes;
654 }
655
656 foreach ( array( 'to', 'subject' ) as $attribute ) {
657 $value = $grunion->get_attribute( $attribute );
658 if ( isset( $grunion->defaults[ $attribute ] ) && $value === $grunion->defaults[ $attribute ] ) {
659 $value = '';
660 }
661 $out[ $attribute ] = $value;
662 }
663
664 die( wp_json_encode( $out ) );
665 }
666
667 add_action( 'wp_ajax_grunion_shortcode', 'grunion_ajax_shortcode' );
668 add_action( 'wp_ajax_grunion_shortcode_to_json', 'grunion_ajax_shortcode_to_json' );
669
670 // process row-action spam/not spam clicks
671 add_action( 'wp_ajax_grunion_ajax_spam', 'grunion_ajax_spam' );
672
673 /**
674 * Handle marking feedback as spam.
675 */
676 function grunion_ajax_spam() {
677 global $wpdb;
678
679 if ( empty( $_POST['make_it'] ) ) {
680 return;
681 }
682
683 $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
684 check_ajax_referer( 'grunion-post-status' );
685 if ( ! current_user_can( 'edit_page', $post_id ) ) {
686 wp_die( esc_html__( 'You are not allowed to manage this item.', 'jetpack' ) );
687 }
688
689 require_once __DIR__ . '/grunion-contact-form.php';
690
691 $current_menu = '';
692 if ( isset( $_POST['sub_menu'] ) && preg_match( '|post_type=feedback|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
693 if ( preg_match( '|post_status=spam|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
694 $current_menu = 'spam';
695 } elseif ( preg_match( '|post_status=trash|', sanitize_text_field( wp_unslash( $_POST['sub_menu'] ) ) ) ) {
696 $current_menu = 'trash';
697 } else {
698 $current_menu = 'messages';
699 }
700 }
701
702 $post = get_post( $post_id );
703 $post_type_object = get_post_type_object( $post->post_type );
704 $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
705 if ( $_POST['make_it'] === 'spam' ) {
706 $post->post_status = 'spam';
707 $status = wp_insert_post( $post );
708
709 /** This action is already documented in modules/contact-form/admin.php */
710 do_action( 'contact_form_akismet', 'spam', $akismet_values );
711 } elseif ( $_POST['make_it'] === 'ham' ) {
712 $post->post_status = 'publish';
713 $status = wp_insert_post( $post );
714
715 /** This action is already documented in modules/contact-form/admin.php */
716 do_action( 'contact_form_akismet', 'ham', $akismet_values );
717
718 $comment_author_email = false;
719 $reply_to_addr = false;
720 $message = false;
721 $to = false;
722 $headers = false;
723 $blog_url = wp_parse_url( site_url() );
724
725 // resend the original email
726 $email = get_post_meta( $post_id, '_feedback_email', true );
727 $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post_id );
728
729 if ( ! empty( $email ) && ! empty( $content_fields ) ) {
730 if ( isset( $content_fields['_feedback_author_email'] ) ) {
731 $comment_author_email = $content_fields['_feedback_author_email'];
732 }
733
734 if ( isset( $email['to'] ) ) {
735 $to = $email['to'];
736 }
737
738 if ( isset( $email['message'] ) ) {
739 $message = $email['message'];
740 }
741
742 if ( isset( $email['headers'] ) ) {
743 $headers = $email['headers'];
744 } else {
745 $headers = 'From: "' . $content_fields['_feedback_author'] . '" <wordpress@' . $blog_url['host'] . ">\r\n";
746
747 if ( ! empty( $comment_author_email ) ) {
748 $reply_to_addr = $comment_author_email;
749 } elseif ( is_array( $to ) ) {
750 $reply_to_addr = $to[0];
751 }
752
753 if ( $reply_to_addr ) {
754 $headers .= 'Reply-To: "' . $content_fields['_feedback_author'] . '" <' . $reply_to_addr . ">\r\n";
755 }
756
757 $headers .= 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . '"';
758 }
759
760 /**
761 * Filters the subject of the email sent after a contact form submission.
762 *
763 * @module contact-form
764 *
765 * @since 3.0.0
766 *
767 * @param string $content_fields['_feedback_subject'] Feedback's subject line.
768 * @param array $content_fields['_feedback_all_fields'] Feedback's data from old fields.
769 */
770 $subject = apply_filters( 'contact_form_subject', $content_fields['_feedback_subject'], $content_fields['_feedback_all_fields'] );
771
772 Grunion_Contact_Form::wp_mail( $to, $subject, $message, $headers );
773 }
774 } elseif ( $_POST['make_it'] === 'publish' ) {
775 if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
776 wp_die( esc_html__( 'You are not allowed to move this item out of the Trash.', 'jetpack' ) );
777 }
778
779 if ( ! wp_untrash_post( $post_id ) ) {
780 wp_die( esc_html__( 'Error in restoring from Trash.', 'jetpack' ) );
781 }
782 } elseif ( $_POST['make_it'] === 'trash' ) {
783 if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
784 wp_die( esc_html__( 'You are not allowed to move this item to the Trash.', 'jetpack' ) );
785 }
786
787 if ( ! wp_trash_post( $post_id ) ) {
788 wp_die( esc_html__( 'Error in moving to Trash.', 'jetpack' ) );
789 }
790 }
791
792 $sql = "
793 SELECT post_status,
794 COUNT( * ) AS post_count
795 FROM `{$wpdb->posts}`
796 WHERE post_type = 'feedback'
797 GROUP BY post_status
798 ";
799 $status_count = (array) $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
800
801 $status = array();
802 $status_html = '';
803 foreach ( $status_count as $row ) {
804 $status[ $row['post_status'] ] = $row['post_count'];
805 }
806
807 if ( isset( $status['publish'] ) ) {
808 $status_html .= '<li><a href="edit.php?post_type=feedback"';
809 if ( $current_menu === 'messages' ) {
810 $status_html .= ' class="current"';
811 }
812
813 $status_html .= '>' . __( 'Messages', 'jetpack' ) . ' <span class="count">';
814 $status_html .= '(' . number_format( $status['publish'] ) . ')';
815 $status_html .= '</span></a> |</li>';
816 }
817
818 if ( isset( $status['trash'] ) ) {
819 $status_html .= '<li><a href="edit.php?post_status=trash&amp;post_type=feedback"';
820 if ( $current_menu === 'trash' ) {
821 $status_html .= ' class="current"';
822 }
823
824 $status_html .= '>' . _x( 'Trash', 'noun', 'jetpack' ) . ' <span class="count">';
825 $status_html .= '(' . number_format( $status['trash'] ) . ')';
826 $status_html .= '</span></a>';
827 if ( isset( $status['spam'] ) ) {
828 $status_html .= ' |';
829 }
830 $status_html .= '</li>';
831 }
832
833 if ( isset( $status['spam'] ) ) {
834 $status_html .= '<li><a href="edit.php?post_status=spam&amp;post_type=feedback"';
835 if ( $current_menu === 'spam' ) {
836 $status_html .= ' class="current"';
837 }
838
839 $status_html .= '>' . __( 'Spam', 'jetpack' ) . ' <span class="count">';
840 $status_html .= '(' . number_format( $status['spam'] ) . ')';
841 $status_html .= '</span></a></li>';
842 }
843
844 echo $status_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're building the html to echo.
845 exit;
846 }
847
848 /**
849 * Add the scripts that will add the "Check for Spam" button to the Feedbacks dashboard page.
850 */
851 function grunion_enable_spam_recheck() {
852 if ( ! defined( 'AKISMET_VERSION' ) ) {
853 return;
854 }
855
856 $screen = get_current_screen();
857
858 // Only add to feedback, only to non-spam view
859 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.
860 return;
861 }
862
863 // Add the actual "Check for Spam" button.
864 add_action( 'admin_head', 'grunion_check_for_spam_button' );
865 }
866
867 add_action( 'admin_enqueue_scripts', 'grunion_enable_spam_recheck' );
868
869 /**
870 * Add the JS and CSS necessary for the Feedback admin page to function.
871 */
872 function grunion_add_admin_scripts() {
873 $screen = get_current_screen();
874
875 if ( 'edit-feedback' !== $screen->id ) {
876 return;
877 }
878
879 // Add the scripts that handle the spam check event.
880 wp_register_script(
881 'grunion-admin',
882 Assets::get_file_url_for_environment(
883 '_inc/build/contact-form/js/grunion-admin.min.js',
884 'modules/contact-form/js/grunion-admin.js'
885 ),
886 array( 'jquery' ),
887 JETPACK__VERSION,
888 true
889 );
890
891 wp_enqueue_script( 'grunion-admin' );
892
893 wp_enqueue_style( 'grunion.css' );
894
895 // Only add to feedback, only to spam view.
896 if ( empty( $_GET['post_status'] ) || 'spam' !== $_GET['post_status'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not making site changes with this check
897 return;
898 }
899
900 $feedbacks_count = wp_count_posts( 'feedback' );
901 $nonce = wp_create_nonce( 'jetpack_delete_spam_feedbacks' );
902 $success_url = remove_query_arg( array( 'jetpack_empty_feedback_spam_error', 'post_status' ) ); // Go to the "All Feedback" page.
903 $failure_url = add_query_arg( 'jetpack_empty_feedback_spam_error', '1' ); // Refresh the current page and show an error.
904 $spam_count = $feedbacks_count->spam;
905
906 $button_parameters = array(
907 /* translators: The placeholder is for showing how much of the process has completed, as a percent. e.g., "Emptying Spam (40%)" */
908 'progress_label' => __( 'Emptying Spam (%1$s%)', 'jetpack' ),
909 'success_url' => $success_url,
910 'failure_url' => $failure_url,
911 'spam_count' => $spam_count,
912 'nonce' => $nonce,
913 'label' => __( 'Empty Spam', 'jetpack' ),
914 );
915
916 wp_localize_script( 'grunion-admin', 'jetpack_empty_spam_button_parameters', $button_parameters );
917 }
918
919 add_action( 'admin_enqueue_scripts', 'grunion_add_admin_scripts' );
920
921 /**
922 * Adds the 'Export' button to the feedback dashboard page.
923 *
924 * @return void
925 */
926 function grunion_export_button() {
927 $current_screen = get_current_screen();
928 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
929 return;
930 }
931
932 if ( ! current_user_can( 'export' ) ) {
933 return;
934 }
935
936 // if there aren't any feedbacks, bail out
937 if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
938 return;
939 }
940
941 $nonce_name = 'feedback_export_nonce';
942
943 $button_html = get_submit_button(
944 __( 'Export', 'jetpack' ),
945 'primary',
946 'jetpack-export-feedback',
947 false,
948 array(
949 'data-nonce-name' => $nonce_name,
950 )
951 );
952
953 $button_html .= wp_nonce_field( 'feedback_export', $nonce_name, false, false );
954 ?>
955 <script type="text/javascript">
956 jQuery( function ( $ ) {
957 $( '#posts-filter #post-query-submit' ).after( <?php echo wp_json_encode( $button_html ); ?> );
958 } );
959 </script>
960 <?php
961 }
962
963 /**
964 * Add the "Check for Spam" button to the Feedbacks dashboard page.
965 */
966 function grunion_check_for_spam_button() {
967 // Nonce name.
968 $nonce_name = 'jetpack_check_feedback_spam_' . (string) get_current_blog_id();
969 // Get HTML for the button.
970 $button_html = get_submit_button(
971 __( 'Check for Spam', 'jetpack' ),
972 'secondary',
973 'jetpack-check-feedback-spam',
974 false,
975 array(
976 'data-failure-url' => add_query_arg( 'jetpack_check_feedback_spam_error', '1' ), // Refresh the current page and show an error.
977 'data-nonce-name' => $nonce_name,
978 )
979 );
980 $button_html .= '<span class="jetpack-check-feedback-spam-spinner"></span>';
981 $button_html .= wp_nonce_field( 'grunion_recheck_queue', $nonce_name, false, false );
982
983 // Add the button next to the filter button via js.
984 ?>
985 <script type="text/javascript">
986 jQuery( function( $ ) {
987 $( '.tablenav.bottom .bulkactions' ).append( <?php echo wp_json_encode( $button_html ); ?> );
988 } );
989 </script>
990 <?php
991 }
992
993 /**
994 * Recheck all approved feedbacks for spam.
995 */
996 function grunion_recheck_queue() {
997 $blog_id = get_current_blog_id();
998
999 if (
1000 empty( $_POST[ 'jetpack_check_feedback_spam_' . (string) $blog_id ] )
1001 || ! wp_verify_nonce( sanitize_key( $_POST[ 'jetpack_check_feedback_spam_' . (string) $blog_id ] ), 'grunion_recheck_queue' )
1002 ) {
1003 wp_send_json_error(
1004 __( 'You aren’t authorized to do that.', 'jetpack' ),
1005 403
1006 );
1007
1008 return;
1009 }
1010
1011 if ( ! current_user_can( 'delete_others_posts' ) ) {
1012 wp_send_json_error(
1013 __( 'You don’t have permission to do that.', 'jetpack' ),
1014 403
1015 );
1016
1017 return;
1018 }
1019
1020 $query = 'post_type=feedback&post_status=publish';
1021
1022 if ( isset( $_POST['limit'], $_POST['offset'] ) ) {
1023 $query .= '&posts_per_page=' . (int) $_POST['limit'] . '&offset=' . (int) $_POST['offset'];
1024 }
1025
1026 $approved_feedbacks = get_posts( $query );
1027
1028 foreach ( $approved_feedbacks as $feedback ) {
1029 $meta = get_post_meta( $feedback->ID, '_feedback_akismet_values', true );
1030
1031 if ( ! $meta ) {
1032 // _feedback_akismet_values is eventually deleted when it's no longer
1033 // within a reasonable time period to check the feedback for spam, so
1034 // if it's gone, don't attempt a spam recheck.
1035 continue;
1036 }
1037
1038 $meta['recheck_reason'] = 'recheck_queue';
1039
1040 /**
1041 * Filter whether the submitted feedback is considered as spam.
1042 *
1043 * @module contact-form
1044 *
1045 * @since 3.4.0
1046 *
1047 * @param bool false Is the submitted feedback spam? Default to false.
1048 * @param array $meta Feedack values returned by the Akismet plugin.
1049 */
1050 $is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $meta );
1051
1052 if ( $is_spam ) {
1053 wp_update_post(
1054 array(
1055 'ID' => $feedback->ID,
1056 'post_status' => 'spam',
1057 )
1058 );
1059 /** This action is already documented in modules/contact-form/admin.php */
1060 do_action( 'contact_form_akismet', 'spam', $meta );
1061 }
1062 }
1063
1064 wp_send_json(
1065 array(
1066 'processed' => is_countable( $approved_feedbacks ) ? count( $approved_feedbacks ) : 0,
1067 )
1068 );
1069 }
1070
1071 add_action( 'wp_ajax_grunion_recheck_queue', 'grunion_recheck_queue' );
1072
1073 /**
1074 * Delete a number of spam feedbacks via an AJAX request.
1075 */
1076 function grunion_delete_spam_feedbacks() {
1077 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.
1078 wp_send_json_error(
1079 __( 'You aren’t authorized to do that.', 'jetpack' ),
1080 403
1081 );
1082
1083 return;
1084 }
1085
1086 if ( ! current_user_can( 'delete_others_posts' ) ) {
1087 wp_send_json_error(
1088 __( 'You don’t have permission to do that.', 'jetpack' ),
1089 403
1090 );
1091
1092 return;
1093 }
1094
1095 $deleted_feedbacks = 0;
1096
1097 $delete_limit = 25;
1098 /**
1099 * Filter the amount of Spam feedback one can delete at once.
1100 *
1101 * @module contact-form
1102 *
1103 * @since 8.7.0
1104 *
1105 * @param int $delete_limit Number of spam to process at once. Default to 25.
1106 */
1107 $delete_limit = apply_filters( 'jetpack_delete_spam_feedbacks_limit', $delete_limit );
1108 $delete_limit = (int) $delete_limit;
1109 $delete_limit = max( 1, min( 100, $delete_limit ) ); // Allow a range of 1-100 for the delete limit.
1110
1111 $query_args = array(
1112 'post_type' => 'feedback',
1113 'post_status' => 'spam',
1114 'posts_per_page' => $delete_limit,
1115 );
1116
1117 $query = new WP_Query( $query_args );
1118 $spam_feedbacks = $query->get_posts();
1119
1120 foreach ( $spam_feedbacks as $feedback ) {
1121 wp_delete_post( $feedback->ID, true );
1122
1123 ++$deleted_feedbacks;
1124 }
1125
1126 wp_send_json(
1127 array(
1128 'success' => true,
1129 'data' => array(
1130 'counts' => array(
1131 'deleted' => $deleted_feedbacks,
1132 'limit' => $delete_limit,
1133 ),
1134 ),
1135 )
1136 );
1137 }
1138 add_action( 'wp_ajax_jetpack_delete_spam_feedbacks', 'grunion_delete_spam_feedbacks' );
1139
1140 /**
1141 * Show an admin notice if the "Empty Spam" or "Check Spam" process was unable to complete, probably due to a permissions error.
1142 */
1143 function grunion_feedback_admin_notice() {
1144 if ( isset( $_GET['jetpack_empty_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 empty the Feedback spam folder.', 'jetpack' ) ) . '</p></div>';
1146 } elseif ( isset( $_GET['jetpack_check_feedback_spam_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1147 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>';
1148 }
1149 }
1150 add_action( 'admin_notices', 'grunion_feedback_admin_notice' );
1151
1152 /**
1153 * Class Grunion_Admin
1154 *
1155 * Singleton for Grunion admin area support.
1156 */
1157 class Grunion_Admin {
1158 /**
1159 * CSV export nonce field name
1160 *
1161 * @var string The nonce field name for CSV export.
1162 */
1163 private $export_nonce_field_csv = 'feedback_export_nonce_csv';
1164
1165 /**
1166 * GDrive export nonce field name
1167 *
1168 * @var string The nonce field name for GDrive export.
1169 */
1170 private $export_nonce_field_gdrive = 'feedback_export_nonce_gdrive';
1171
1172 /**
1173 * Instantiates this singleton class
1174 *
1175 * @return Grunion_Admin The Grunion Admin class instance.
1176 */
1177 public static function init() {
1178 static $instance = false;
1179
1180 if ( ! $instance ) {
1181 $instance = new Grunion_Admin();
1182 }
1183
1184 return $instance;
1185 }
1186
1187 /**
1188 * Grunion_Admin constructor
1189 */
1190 public function __construct() {
1191 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
1192 add_action( 'admin_footer-edit.php', array( $this, 'print_export_modal' ) );
1193
1194 add_action( 'wp_ajax_grunion_export_to_gdrive', array( $this, 'export_to_gdrive' ) );
1195 add_action( 'wp_ajax_grunion_gdrive_connection', array( $this, 'test_gdrive_connection' ) );
1196 }
1197
1198 /**
1199 * Hook handler for admin_enqueue_scripts hook
1200 */
1201 public function admin_enqueue_scripts() {
1202 $current_screen = get_current_screen();
1203 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
1204 return;
1205 }
1206 add_thickbox();
1207 $localized_strings = array(
1208 'exportError' => esc_js( __( 'There was an error exporting your results', 'jetpack' ) ),
1209 'waitingConnection' => esc_js( __( 'Waiting for connection...', 'jetpack' ) ),
1210 );
1211 wp_localize_script( 'grunion-admin', 'exportParameters', $localized_strings );
1212 }
1213
1214 /**
1215 * Prints the modal markup with export buttons/content.
1216 */
1217 public function print_export_modal() {
1218 if ( ! current_user_can( 'export' ) ) {
1219 return;
1220 }
1221
1222 // if there aren't any feedbacks, bail out
1223 if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
1224 return;
1225 }
1226
1227 $current_screen = get_current_screen();
1228 if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ), true ) ) {
1229 return;
1230 }
1231
1232 $jetpack_logo = new Logo();
1233 ?>
1234 <div id="feedback-export-modal" style="display: none;">
1235 <div class="feedback-export-modal__wrapper">
1236 <div class="feedback-export-modal__header">
1237 <h1 class="feedback-export-modal__header-title"><?php esc_html_e( 'Export your Form Responses', 'jetpack' ); ?></h1>
1238 <p class="feedback-export-modal__header-subtitle"><?php esc_html_e( 'Choose your favorite file format or export destination:', 'jetpack' ); ?></p>
1239 </div>
1240 <div class="feedback-export-modal__content">
1241 <?php $this->get_csv_export_section(); ?>
1242 <?php $this->get_gdrive_export_section(); ?>
1243 </div>
1244 <div class="feedback-export-modal__footer">
1245 <div class="feedback-export-modal__footer-column">
1246 <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">
1247 <?php echo $jetpack_logo->get_jp_emblem(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1248 </a>
1249 <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">
1250 <?php echo esc_html_x( 'Jetpack Forms', 'Name of Jetpack’s Contact Form feature', 'jetpack' ); ?>
1251 </a>
1252 </div>
1253 <div class="feedback-export-modal__footer-column">
1254 <a href="https://automattic.com" title="Automattic" rel="noopener noreferer" target="_blank" class="feedback-export-modal__footer-link">
1255 <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">
1256 <desc id="jp-automattic-byline-logo-title"><?php esc_html_e( 'An Automattic Airline', 'jetpack' ); ?></desc>
1257 <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>
1258 <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>
1259 </svg>
1260 </a>
1261 </div>
1262 </div>
1263 </div>
1264 </div>
1265 <?php
1266 $opener_label = esc_html__( 'Export', 'jetpack' );
1267 $export_modal_opener = wp_is_mobile()
1268 ? "<a id='export-modal-opener' class='button button-primary' href='#TB_inline?&width=550&height=550&inlineId=feedback-export-modal'>{$opener_label}</a>"
1269 : "<a id='export-modal-opener' class='button button-primary' href='#TB_inline?&width=680&height=600&inlineId=feedback-export-modal'>{$opener_label}</a>";
1270 ?>
1271 <script type="text/javascript">
1272 jQuery( function( $ ) {
1273 $( '#posts-filter #post-query-submit' ).after( <?php echo wp_json_encode( $export_modal_opener ); ?> );
1274 } );
1275 </script>
1276 <?php
1277 }
1278
1279 /**
1280 * Ajax handler for wp_ajax_grunion_export_to_gdrive.
1281 * Exports data to Google Drive, based on POST data.
1282 *
1283 * @see Grunion_Contact_Form_Plugin::get_feedback_entries_from_post
1284 */
1285 public function export_to_gdrive() {
1286 $post_data = wp_unslash( $_POST );
1287 if (
1288 ! current_user_can( 'export' )
1289 || empty( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ) )
1290 || ! wp_verify_nonce( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ), 'feedback_export' )
1291 ) {
1292 wp_send_json_error(
1293 __( 'You aren’t authorized to do that.', 'jetpack' ),
1294 403
1295 );
1296
1297 return;
1298 }
1299
1300 $grunion = Grunion_Contact_Form_Plugin::init();
1301 $export_data = $grunion->get_feedback_entries_from_post();
1302
1303 $fields = array_keys( $export_data );
1304 $row_count = is_countable( $export_data ) ? count( reset( $export_data ) ) : 0;
1305
1306 $sheet_data = array( $fields );
1307
1308 for ( $i = 0; $i < $row_count; $i++ ) {
1309
1310 $current_row = array();
1311
1312 /**
1313 * Put all the fields in `$current_row` array.
1314 */
1315 foreach ( $fields as $single_field_name ) {
1316 $current_row[] = $export_data[ $single_field_name ][ $i ];
1317 }
1318
1319 $sheet_data[] = $current_row;
1320 }
1321
1322 $user_id = (int) get_current_user_id();
1323
1324 if ( ! empty( $post_data['post'] ) && $post_data['post'] !== 'all' ) {
1325 $spreadsheet_title = sprintf(
1326 '%1$s - %2$s',
1327 $this->get_export_filename( get_the_title( (int) $post_data['post'] ) ),
1328 gmdate( 'Y-m-d H:i' )
1329 );
1330 } else {
1331 $spreadsheet_title = sprintf( '%s - %s', $this->get_export_filename(), gmdate( 'Y-m-d H:i' ) );
1332 }
1333
1334 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1335 $sheet = Jetpack_Google_Drive_Helper::create_sheet( $user_id, $spreadsheet_title, $sheet_data );
1336
1337 $grunion->record_tracks_event( 'forms_export_responses', array( 'format' => 'gsheets' ) );
1338
1339 wp_send_json(
1340 array(
1341 'success' => ! is_wp_error( $sheet ),
1342 'data' => $sheet,
1343 )
1344 );
1345 }
1346
1347 /**
1348 * Return HTML markup for the CSV download button.
1349 */
1350 public function get_csv_export_section() {
1351 $button_csv_html = get_submit_button(
1352 esc_html__( 'Download', 'jetpack' ),
1353 'primary export-button export-csv',
1354 'jetpack-export-feedback-csv',
1355 false,
1356 array( 'data-nonce-name' => $this->export_nonce_field_csv )
1357 );
1358 ?>
1359 <div class="export-card">
1360 <div class="export-card__header">
1361 <svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
1362 <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"/>
1363 </svg>
1364 <div class="export-card__header-title"><?php esc_html_e( 'CSV File', 'jetpack' ); ?></div>
1365 </div>
1366 <div class="export-card__body">
1367 <div class="export-card__body-description">
1368 <?php esc_html_e( 'Download your form response data via CSV file.', 'jetpack' ); ?>
1369 </div>
1370 <div class="export-card__body-cta">
1371 <?php
1372 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1373 echo $button_csv_html;
1374 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1375 echo wp_nonce_field( 'feedback_export', $this->export_nonce_field_csv, false, false );
1376 ?>
1377 </div>
1378 </div>
1379 </div>
1380 <?php
1381 }
1382
1383 /**
1384 * Render/output HTML markup for the export to gdrive section.
1385 * If the user doesn't hold a Google Drive connection a button to connect will render (See grunion-admin.js).
1386 */
1387 public function get_gdrive_export_section() {
1388 $user_connected = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || ( new Connection_Manager( 'jetpack' ) )->is_user_connected( get_current_user_id() );
1389 if ( ! $user_connected ) {
1390 return;
1391 }
1392
1393 $user_id = (int) get_current_user_id();
1394
1395 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1396 $has_valid_connection = Jetpack_Google_Drive_Helper::has_valid_connection( $user_id );
1397
1398 if ( $has_valid_connection ) {
1399 $button_html = $this->get_gdrive_export_button_markup();
1400 } else {
1401 $slug = 'jetpack-form-responses-connect';
1402 $button_html = sprintf(
1403 '<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>',
1404 esc_url( Redirect::get_url( $slug ) ),
1405 esc_attr__( 'connect to Google Drive', 'jetpack' ),
1406 esc_html__( 'Connect Google Drive', 'jetpack' ),
1407 $slug,
1408 $this->export_nonce_field_gdrive
1409 );
1410 }
1411
1412 ?>
1413 <div class="export-card">
1414 <div class="export-card__header">
1415 <svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1416 <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"/>
1417 </svg>
1418 <div class="export-card__header-title"><?php esc_html_e( 'Google Sheets', 'jetpack' ); ?></div>
1419 <div class="export-card__beta-badge">BETA</div>
1420 </div>
1421 <div class="export-card__body">
1422 <div class="export-card__body-description">
1423 <div>
1424 <?php esc_html_e( 'Export your data into a Google Sheets file.', 'jetpack' ); ?>
1425 <?php
1426 printf(
1427 '<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferer">%3$s</a>',
1428 esc_url( Redirect::get_url( 'jetpack-support-contact-form-export' ) ),
1429 esc_attr__( 'connect to Google Drive', 'jetpack' ),
1430 esc_html__( 'You need to connect to Google Drive.', 'jetpack' )
1431 );
1432 ?>
1433 </div>
1434 <p class="export-card__body-description-footer"><?php esc_html_e( 'This premium feature is currently free to use in beta.', 'jetpack' ); ?></p>
1435 </div>
1436 <div class="export-card__body-cta">
1437 <?php
1438 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1439 echo $button_html;
1440 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we're literally building all this html to output it
1441 echo wp_nonce_field( 'feedback_export', $this->export_nonce_field_gdrive, false, false );
1442 ?>
1443 </div>
1444 </div>
1445 </div>
1446 <?php
1447 }
1448
1449 /**
1450 * Ajax handler. Sends a payload with connection status and html to replace
1451 * the Connect button with the Export button using get_gdrive_export_button
1452 */
1453 public function test_gdrive_connection() {
1454 $post_data = wp_unslash( $_POST );
1455 $user_id = (int) get_current_user_id();
1456
1457 if (
1458 ! $user_id ||
1459 ! current_user_can( 'export' ) ||
1460 empty( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ) ) ||
1461 ! wp_verify_nonce( sanitize_text_field( $post_data[ $this->export_nonce_field_gdrive ] ), 'feedback_export' )
1462 ) {
1463 wp_send_json_error(
1464 __( 'You aren’t authorized to do that.', 'jetpack' ),
1465 403
1466 );
1467
1468 return;
1469 }
1470
1471 if ( ! class_exists( 'Jetpack_Google_Drive_Helper' ) ) {
1472 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-google-drive-helper.php';
1473 }
1474 $has_valid_connection = Jetpack_Google_Drive_Helper::has_valid_connection( $user_id );
1475
1476 $replacement_html = $has_valid_connection
1477 ? $this->get_gdrive_export_button_markup()
1478 : '';
1479
1480 wp_send_json(
1481 array(
1482 'connection' => $has_valid_connection,
1483 'html' => $replacement_html,
1484 )
1485 );
1486 }
1487
1488 /**
1489 * Markup helper so we DRY, returns the button markup for the export to GDrive feature.
1490 *
1491 * @return string The HTML button markup
1492 */
1493 public function get_gdrive_export_button_markup() {
1494 return get_submit_button(
1495 esc_html__( 'Export', 'jetpack' ),
1496 'primary export-button export-gdrive',
1497 'jetpack-export-feedback-gdrive',
1498 false,
1499 array( 'data-nonce-name' => $this->export_nonce_field_gdrive )
1500 );
1501 }
1502
1503 /**
1504 * Get a filename for export tasks
1505 *
1506 * @param string $source The filtered source for exported data.
1507 * @return string The filename without source nor date suffix.
1508 */
1509 public function get_export_filename( $source = '' ) {
1510 return $source === ''
1511 ? sprintf(
1512 /* translators: Site title, used to craft the export filename, eg "MySite - Jetpack Form Responses" */
1513 __( '%s - Jetpack Form Responses', 'jetpack' ),
1514 sanitize_file_name( get_bloginfo( 'name' ) )
1515 )
1516 : sprintf(
1517 /* translators: 1: Site title; 2: post title. Used to craft the export filename, eg "MySite - Jetpack Form Responses - Contact" */
1518 __( '%1$s - Jetpack Form Responses - %2$s', 'jetpack' ),
1519 sanitize_file_name( get_bloginfo( 'name' ) ),
1520 sanitize_file_name( $source )
1521 );
1522 }
1523 }
1524
1525 Grunion_admin::init();
1526