diff --git a/.gitignore b/.gitignore index 1ede1ed..7612f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ mu-plugins/* !websub-compat.php !mu-plugins/osi-events-manager-tweaks !mu-plugins/osi-editor-tweaks +!mu-plugins/osi-supporter-cleanup themes/* !themes/osi diff --git a/mu-plugins/osi-supporter-cleanup/osi-supporter-cleanup.php b/mu-plugins/osi-supporter-cleanup/osi-supporter-cleanup.php new file mode 100644 index 0000000..5ca2221 --- /dev/null +++ b/mu-plugins/osi-supporter-cleanup/osi-supporter-cleanup.php @@ -0,0 +1,195 @@ +] + * : Report what would be removed without removing it. Pass --dry-run=false to remove. + * --- + * default: 'true' + * --- + * + * [--force] + * : Delete permanently instead of moving to the trash. + * + * [--trashed] + * : Target matching supporter posts already in the trash. Use with --force to purge them. + * + * [--newest-first] + * : Work from the highest ID down, e.g. to remove a test submission with --limit=1. + * + * [--batch-size=] + * : Posts per batch. + * --- + * default: 100 + * --- + * + * [--limit=] + * : Stop after this many posts (0 = no limit). + * --- + * default: 0 + * --- + * + * [--sleep=] + * : Pause between batches. + * --- + * default: 2 + * --- + * + * [--yes] + * : Skip the confirmation prompt on a real run. + * + * ## EXAMPLES + * + * wp osi-supporter-cleanup + * wp osi-supporter-cleanup --limit=10 --dry-run=false + * wp osi-supporter-cleanup --dry-run=false --yes | tee supporter-cleanup.log + * wp osi-supporter-cleanup --trashed --force --dry-run=false + * wp osi-supporter-cleanup --newest-first --limit=1 --dry-run=false + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + * + * @return void + */ + public function __invoke( array $args, array $assoc_args ): void { + $dry_run = ! in_array( strtolower( (string) $assoc_args['dry-run'] ), array( 'false', '0', 'no', '' ), true ); + $force = ! empty( $assoc_args['force'] ); + $status = empty( $assoc_args['trashed'] ) ? 'pending' : 'trash'; + $desc = ! empty( $assoc_args['newest-first'] ); + $batch_size = max( 1, absint( $assoc_args['batch-size'] ) ); + $limit = absint( $assoc_args['limit'] ); + $sleep = absint( $assoc_args['sleep'] ); + $verb = ( $dry_run ? 'would be ' : '' ) . ( $force ? 'deleted' : 'trashed' ); + + $this->log( sprintf( '%s %s posts.', $dry_run ? 'DRY RUN on' : ( $force ? 'DELETING' : 'TRASHING' ), $status ) ); + + if ( ! $dry_run ) { + WP_CLI::confirm( sprintf( '%s supporter posts from other forms%s?', $force ? 'Permanently delete' : 'Trash', $limit ? " (limit {$limit})" : '' ), $assoc_args ); + wp_defer_term_counting( true ); + } + + $limit = $limit ? $limit : PHP_INT_MAX; + $processed = 0; + $failed = 0; + $last_id = $desc ? PHP_INT_MAX : 0; + + while ( $processed + $failed < $limit ) { + $ids = $this->get_candidates( $status, $last_id, min( $batch_size, $limit - $processed - $failed ), $desc ); + if ( empty( $ids ) ) { + break; + } + $last_id = end( $ids ); + + foreach ( $ids as $id ) { + if ( ! $dry_run && ! ( $force ? wp_delete_post( $id, true ) : wp_trash_post( $id ) ) ) { + ++$failed; + WP_CLI::warning( sprintf( 'Failed on %d.', $id ) ); + continue; + } + ++$processed; + } + + $this->log( sprintf( '%d %s so far, %s MB in use. IDs: %s', $processed, $verb, number_format( memory_get_usage() / MB_IN_BYTES, 1 ), implode( ',', $ids ) ) ); + + if ( wp_cache_supports( 'flush_runtime' ) ) { + wp_cache_flush_runtime(); + } + if ( $sleep ) { + sleep( $sleep ); + } + } + + if ( ! $dry_run ) { + wp_defer_term_counting( false ); + } + + if ( $failed ) { + $this->log( sprintf( 'Failed: %d', $failed ) ); + } + WP_CLI::success( sprintf( '%d supporter posts from other forms %s.', $processed, $verb ) ); + } + + /** + * Fetch the next batch of anonymous, never-edited supporter IDs with no endorsement type, link or logo. + * The endorsement form always sends a type, so a submission without one came from another form. + * + * @param string $status Post status to search. + * @param integer $last_id Fetch IDs past this one. + * @param integer $size Batch size. + * @param boolean $desc Highest IDs first. + * + * @return int[] + */ + private function get_candidates( string $status, int $last_id, int $size, bool $desc ): array { + global $wpdb; + + if ( $desc ) { + $ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT p.ID FROM {$wpdb->posts} p + WHERE p.post_type = 'supporter' AND p.post_status = %s AND p.post_author = 0 AND p.ID < %d + AND NOT EXISTS ( + SELECT 1 FROM {$wpdb->postmeta} m WHERE m.post_id = p.ID + AND m.meta_key IN ( 'endorsement_type', 'link', '_thumbnail_id', '_edit_last' ) AND TRIM( m.meta_value ) NOT IN ( '', '0' ) + ) + ORDER BY p.ID DESC LIMIT %d", + $status, + $last_id, + $size + ) + ); + } else { + $ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT p.ID FROM {$wpdb->posts} p + WHERE p.post_type = 'supporter' AND p.post_status = %s AND p.post_author = 0 AND p.ID > %d + AND NOT EXISTS ( + SELECT 1 FROM {$wpdb->postmeta} m WHERE m.post_id = p.ID + AND m.meta_key IN ( 'endorsement_type', 'link', '_thumbnail_id', '_edit_last' ) AND TRIM( m.meta_value ) NOT IN ( '', '0' ) + ) + ORDER BY p.ID ASC LIMIT %d", + $status, + $last_id, + $size + ) + ); + } + + if ( $wpdb->last_error ) { + WP_CLI::error( $wpdb->last_error ); + } + + return array_map( 'intval', $ids ); + } + + /** + * Log a line with a UTC timestamp, so a teed run doubles as a record. + * + * @param string $message Message. + * + * @return void + */ + private function log( string $message ): void { + WP_CLI::log( sprintf( '[%s UTC] %s', gmdate( 'Y-m-d H:i:s' ), $message ) ); + } +} + +WP_CLI::add_command( 'osi-supporter-cleanup', 'OSI_Supporter_Cleanup_Command' ); diff --git a/plugins/osi-features/acf-fields/group_672accc71e0c4.json b/plugins/osi-features/acf-fields/group_672accc71e0c4.json index 2ffed88..2b834c1 100644 --- a/plugins/osi-features/acf-fields/group_672accc71e0c4.json +++ b/plugins/osi-features/acf-fields/group_672accc71e0c4.json @@ -46,6 +46,34 @@ "prepend": "", "append": "" }, + { + "key": "field_672accd44e671", + "label": "Endorsement type", + "name": "endorsement_type", + "aria-label": "", + "type": "radio", + "instructions": "From the endorsement form. A supporter without one did not come from that form.", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "relevanssi_exclude": 0, + "choices": { + "Individual": "Individual", + "Institutional": "Institutional", + "Both": "Both" + }, + "default_value": "", + "return_format": "value", + "allow_null": 1, + "other_choice": 0, + "allow_in_bindings": 0, + "layout": "horizontal", + "save_other_choice": 0 + }, { "key": "field_672ad07b77def", "label": "Link", @@ -130,5 +158,5 @@ "active": true, "description": "", "show_in_rest": 0, - "modified": 1733133647 + "modified": 1793810000 } diff --git a/themes/osi/functions.php b/themes/osi/functions.php index d9c61d7..95e64c9 100755 --- a/themes/osi/functions.php +++ b/themes/osi/functions.php @@ -441,6 +441,12 @@ function osi_wpdc_comment_body( string $comment_body ) { } add_filter( 'wpdc_comment_body', 'osi_wpdc_comment_body', 10, 1 ); +/** + * Title of the form that collects endorsements, and of the Flamingo channel it feeds. + * Keyed on the title because form IDs differ between environments. + */ +const OSI_ENDORSEMENT_FORM_TITLE = 'OSAID Endorsement'; + /** * Process the supporter form submission. * @@ -448,7 +454,11 @@ function osi_wpdc_comment_body( string $comment_body ) { * * @return void */ -function osi_handle_support_form_submission( WPCF7_ContactForm $contact_form ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found +function osi_handle_support_form_submission( WPCF7_ContactForm $contact_form ): void { + if ( OSI_ENDORSEMENT_FORM_TITLE !== $contact_form->title() ) { + return; + } + $submission = WPCF7_Submission::get_instance(); if ( $submission ) { @@ -467,6 +477,10 @@ function osi_handle_support_form_submission( WPCF7_ContactForm $contact_form ): * @return void */ function osi_save_supporter_form_data_to_cpt( array $form_data ): void { + if ( '' === trim( (string) ( $form_data['your-name'] ?? '' ) ) ) { + return; + } + $post_id = wp_insert_post( array( 'post_title' => $form_data['your-name'], @@ -482,12 +496,24 @@ function osi_save_supporter_form_data_to_cpt( array $form_data ): void { if ( $post_id ) { update_field( 'name', $form_data['your-name'], $post_id ); - update_field( 'organization', $form_data['your-org'], $post_id ); - update_field( 'endorsement_type', $form_data['your-endorsement'], $post_id ); - update_field( 'quote', $form_data['your-message'], $post_id ); + update_field( 'organization', $form_data['your-org'] ?? '', $post_id ); + update_field( 'endorsement_type', osi_single_endorsement_type( $form_data['your-endorsement'] ?? '' ), $post_id ); + update_field( 'quote', $form_data['your-message'] ?? '', $post_id ); } } +/** + * CF7 posts a radio as a one-item array; the Endorsement type field is a radio, so keep the single value. + * + * @param mixed $value Stored or submitted endorsement type. + * + * @return mixed + */ +function osi_single_endorsement_type( mixed $value ): mixed { + return is_array( $value ) ? (string) reset( $value ) : $value; +} +add_filter( 'acf/load_value/name=endorsement_type', 'osi_single_endorsement_type' ); + /** * Handle the supporter form spam status change. * @@ -505,7 +531,7 @@ function osi_handle_supporter_form_flamingo_spam_status_change( string $new_stat if ( 'flamingo-spam' === $old_status && 'publish' === $new_status ) { $term_obj_list = get_the_terms( $post->ID, 'flamingo_inbound_channel' ); - if ( empty( $term_obj_list ) || is_wp_error( $term_obj_list ) || 'OSAID Endorsement' !== $term_obj_list[0]->name ) { + if ( empty( $term_obj_list ) || is_wp_error( $term_obj_list ) || OSI_ENDORSEMENT_FORM_TITLE !== $term_obj_list[0]->name ) { return; } @@ -516,9 +542,7 @@ function osi_handle_supporter_form_flamingo_spam_status_change( string $new_stat 'your-message' => get_post_meta( $post->ID, '_field_your-message', true ), ); - if ( ! empty( $form_data['your-name'] ) ) { - osi_save_supporter_form_data_to_cpt( $form_data ); - } + osi_save_supporter_form_data_to_cpt( $form_data ); } } add_action( 'transition_post_status', 'osi_handle_supporter_form_flamingo_spam_status_change', 10, 3 );