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