Skip to content

Commit b22ffba

Browse files
kodinkatcorsacca
andauthored
Remove Associated Storage S3 Objects During Record Deletion (#2896)
* Add purge_post_storage_objects method to clean up S3 storage on post deletion - Implemented a new private method to remove S3-backed storage objects associated with a post, including image fields in post meta, comment attachments, and user meta. - Integrated this method into the delete_post function to ensure storage cleanup occurs during post deletion. - Added checks for the availability of DT_Storage_API to prevent errors if the API is not enabled. * Enhance error handling in S3 object deletion during post purging - Updated the purge_post_storage_objects method to log failures when deleting S3 objects associated with posts, ensuring better tracking of issues during storage cleanup. - Added checks for the result of DT_Storage_API::delete_file to log a message if the deletion fails, improving the robustness of the storage management process. * add filter * phpcs * Refactor S3 object deletion handling in comment and post purging - Enhanced the error handling for S3 object deletions in both the and comment processing methods, ensuring that failures are logged correctly. - Introduced a filter for comment storage meta keys to streamline the deletion process for associated audio and image URLs. - Improved code readability and maintainability by consolidating repeated logic for checking and deleting S3 objects. * Refactor comment storage meta key retrieval and improve S3 object deletion logic - Introduced a new method, get_comment_storage_meta_keys, to centralize the retrieval of comment meta keys backed by storage, enhancing code maintainability. - Updated the purge_post_storage_objects method to utilize the new retrieval method, streamlining the process of deleting S3 objects associated with comments. - Improved SQL query preparation for fetching comment storage keys, ensuring better performance and security. * Refactor SQL query preparation for comment storage key retrieval - Improved the SQL query construction for fetching comment storage keys by consolidating the query into a single prepared statement, enhancing performance and security. - Added a PHPCS ignore comment to clarify the safety of the constructed placeholders. * Update PHPCS comments for SQL query preparation in comment storage key retrieval - Disabled PHPCS warning for interpolated SQL queries to clarify the safety of constructed placeholders. - Re-enabled the warning after the SQL query preparation to maintain code quality standards. --------- Co-authored-by: corsac <corsacca@gmail.com>
1 parent cb42d59 commit b22ffba

2 files changed

Lines changed: 131 additions & 6 deletions

File tree

dt-posts/dt-posts.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,11 +1409,19 @@ public static function delete_post_comment( int $comment_id, bool $check_permiss
14091409
// Provide space for any additional processing of metadata values.
14101410
if ( DT_Storage_API::is_enabled() ) {
14111411
$comment_meta = get_comment_meta( $comment_id );
1412-
if ( isset( $comment_meta, $comment_meta['audio_url'] ) && is_array( $comment_meta['audio_url'] ) && ( count( $comment_meta['audio_url'] ) > 0 ) ) {
1413-
DT_Storage_API::delete_file( $comment_meta['audio_url'][0] );
1414-
}
1415-
if ( isset( $comment_meta, $comment_meta['image_url'] ) && is_array( $comment_meta['image_url'] ) && ( count( $comment_meta['image_url'] ) > 0 ) ) {
1416-
DT_Storage_API::delete_file( $comment_meta['image_url'][0] );
1412+
$comment_storage_meta_keys = self::get_comment_storage_meta_keys();
1413+
if ( !empty( $comment_storage_meta_keys ) && is_array( $comment_meta ) ) {
1414+
foreach ( $comment_storage_meta_keys as $meta_key ) {
1415+
if ( isset( $comment_meta[ $meta_key ] ) && is_array( $comment_meta[ $meta_key ] ) && count( $comment_meta[ $meta_key ] ) > 0 ) {
1416+
$storage_key = $comment_meta[ $meta_key ][0];
1417+
if ( !empty( $storage_key ) ) {
1418+
$result = DT_Storage_API::delete_file( $storage_key );
1419+
if ( !is_array( $result ) || empty( $result['file_deleted'] ) ) {
1420+
dt_write_log( "delete_post_comment: failed to delete S3 object '{$storage_key}' for comment {$comment_id}" );
1421+
}
1422+
}
1423+
}
1424+
}
14171425
}
14181426
}
14191427

@@ -1536,6 +1544,8 @@ private static function get_post_comments_meta( int $post_id ) {
15361544

15371545
$comments_reactions_dict = [];
15381546
$comments_meta_dict = [];
1547+
$comment_storage_meta_keys = self::get_comment_storage_meta_keys();
1548+
15391549
foreach ( $comments_meta as $meta ) {
15401550

15411551
// if meta_key starts with "reaction"...
@@ -1563,7 +1573,7 @@ private static function get_post_comments_meta( int $post_id ) {
15631573

15641574
// Provide space for the reshaping of meta-values.
15651575
$meta_value = $meta->meta_value;
1566-
if ( in_array( $meta->meta_key, [ 'audio_url', 'image_url' ] ) && !empty( $meta_value ) && DT_Storage_API::is_enabled() ) {
1576+
if ( in_array( $meta->meta_key, $comment_storage_meta_keys, true ) && !empty( $meta_value ) && DT_Storage_API::is_enabled() ) {
15671577
$storage_obj_url = DT_Storage_API::get_file_url( $meta_value );
15681578
$meta_value = !empty( $storage_obj_url ) ? $storage_obj_url : $meta_value;
15691579
}

dt-posts/posts.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,119 @@ public static function get_multi_select_options( $post_type, $field, $search = '
14721472
return $options;
14731473
}
14741474

1475+
/**
1476+
* Returns the list of comment meta keys that are backed by storage
1477+
* (e.g. S3) and should be treated as file keys.
1478+
*
1479+
* @return array
1480+
*/
1481+
protected static function get_comment_storage_meta_keys(): array {
1482+
$keys = apply_filters( 'dt_post_comment_storage_meta_keys', [ 'audio_url', 'image_url' ] );
1483+
return is_array( $keys ) ? $keys : [ 'audio_url', 'image_url' ];
1484+
}
1485+
1486+
/**
1487+
* Removes any S3-backed storage objects associated with a post (post meta image fields,
1488+
* comment/activity audio and image attachments, and dt_post_user_meta image fields).
1489+
*
1490+
* This is a best-effort cleanup: S3 deletion failures are logged but do not block
1491+
* the overall record deletion. The purge runs before database deletes so that
1492+
* storage keys can be read while meta and related rows still exist.
1493+
* No-op if DT_Storage_API is not available or not enabled.
1494+
*
1495+
* @param string $post_type Post type.
1496+
* @param int $post_id Post ID.
1497+
*/
1498+
private static function purge_post_storage_objects( string $post_type, int $post_id ) {
1499+
if ( !class_exists( 'DT_Storage_API' ) || !method_exists( 'DT_Storage_API', 'delete_file' ) || !DT_Storage_API::is_enabled() ) {
1500+
return;
1501+
}
1502+
1503+
global $wpdb;
1504+
$field_settings = DT_Posts::get_post_field_settings( $post_type );
1505+
$all_post_meta = get_post_meta( $post_id );
1506+
1507+
// Delete S3 objects for image-type post fields stored in post meta.
1508+
if ( is_array( $field_settings ) && !empty( $all_post_meta ) ) {
1509+
foreach ( $field_settings as $field_key => $settings ) {
1510+
if ( isset( $settings['type'] ) && $settings['type'] === 'image' && isset( $all_post_meta[ $field_key ][0] ) ) {
1511+
$storage_key = $all_post_meta[ $field_key ][0];
1512+
if ( !empty( $storage_key ) ) {
1513+
$result = DT_Storage_API::delete_file( $storage_key );
1514+
if ( !is_array( $result ) || empty( $result['file_deleted'] ) ) {
1515+
dt_write_log( "purge_post_storage_objects: failed to delete S3 object '{$storage_key}' for post {$post_id}" );
1516+
}
1517+
}
1518+
}
1519+
}
1520+
}
1521+
1522+
// Delete S3 objects referenced by comment meta attached to this post (e.g., audio_url, image_url).
1523+
$comment_storage_meta_keys = self::get_comment_storage_meta_keys();
1524+
if ( !empty( $comment_storage_meta_keys ) ) {
1525+
$placeholders = implode( ', ', array_fill( 0, count( $comment_storage_meta_keys ), '%s' ) );
1526+
$params = array_merge( [ $post_id ], $comment_storage_meta_keys );
1527+
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1528+
$comment_storage_keys = $wpdb->get_col(
1529+
$wpdb->prepare(
1530+
"
1531+
SELECT cm.meta_value
1532+
FROM {$wpdb->commentmeta} cm
1533+
INNER JOIN {$wpdb->comments} c ON c.comment_ID = cm.comment_id
1534+
WHERE c.comment_post_ID = %d
1535+
AND cm.meta_key IN ( $placeholders )
1536+
",
1537+
$params
1538+
)
1539+
);
1540+
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1541+
1542+
if ( !empty( $comment_storage_keys ) ) {
1543+
foreach ( array_unique( array_filter( $comment_storage_keys ) ) as $storage_key ) {
1544+
$result = DT_Storage_API::delete_file( $storage_key );
1545+
if ( !is_array( $result ) || empty( $result['file_deleted'] ) ) {
1546+
dt_write_log( "purge_post_storage_objects: failed to delete S3 object '{$storage_key}' for post {$post_id}" );
1547+
}
1548+
}
1549+
}
1550+
}
1551+
1552+
// Delete any S3 objects that might be stored in dt_post_user_meta for image-type fields.
1553+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $wpdb->dt_post_user_meta is a $wpdb table name property.
1554+
$post_user_meta_rows = $wpdb->get_results(
1555+
$wpdb->prepare(
1556+
"
1557+
SELECT meta_key, meta_value
1558+
FROM $wpdb->dt_post_user_meta
1559+
WHERE post_id = %d
1560+
",
1561+
$post_id
1562+
),
1563+
ARRAY_A
1564+
);
1565+
1566+
if ( !empty( $post_user_meta_rows ) && is_array( $field_settings ) ) {
1567+
foreach ( $post_user_meta_rows as $row ) {
1568+
$meta_key = $row['meta_key'] ?? '';
1569+
$meta_value = $row['meta_value'] ?? '';
1570+
1571+
if ( empty( $meta_key ) || empty( $meta_value ) ) {
1572+
continue;
1573+
}
1574+
1575+
$field_key = self::get_field_key_from_meta( $meta_key, $field_settings );
1576+
if ( $field_key && isset( $field_settings[ $field_key ]['type'] ) && $field_settings[ $field_key ]['type'] === 'image' ) {
1577+
$result = DT_Storage_API::delete_file( $meta_value );
1578+
if ( !is_array( $result ) || empty( $result['file_deleted'] ) ) {
1579+
dt_write_log( "purge_post_storage_objects: failed to delete S3 object '{$meta_value}' for post {$post_id}" );
1580+
}
1581+
}
1582+
}
1583+
}
1584+
1585+
do_action( 'dt_purge_post_storage_objects', $post_type, $post_id );
1586+
}
1587+
14751588
public static function delete_post( string $post_type, int $post_id, bool $check_permissions = true ){
14761589
if ( $check_permissions && !self::can_delete( $post_type, $post_id ) ) {
14771590
return new WP_Error( __FUNCTION__, 'You do not have permission for this', [ 'status' => 403 ] );
@@ -1481,6 +1594,8 @@ public static function delete_post( string $post_type, int $post_id, bool $check
14811594

14821595
do_action( 'dt_before_post_deleted', $post_type, $post_id );
14831596

1597+
self::purge_post_storage_objects( $post_type, $post_id );
1598+
14841599
$post_title = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE ID = %d AND post_type = %s", $post_id, $post_type ) );
14851600

14861601
$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->dt_notifications WHERE post_id = %s", $post_id ) );

0 commit comments

Comments
 (0)