-
Notifications
You must be signed in to change notification settings - Fork 82
Add local caching for remote actor avatars #2610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
2c05ec1 to
7ab2e4d
Compare
3201822 to
e230bac
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds local caching for remote ActivityPub actor avatars to reduce external dependencies and improve performance. When remote actors are created or updated, their avatars are downloaded, optimized (resized to 1200px max and converted to WebP), and stored locally in /activitypub/actors/{actor_id}/. The implementation gracefully falls back to remote URLs if caching fails and cleans up cached files when actors are deleted.
Key changes:
- Avatar downloading and local caching with image optimization during actor creation/update
- Automatic cleanup of cached avatar files when actors are deleted
- Fallback to remote URL if local caching fails
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
| includes/collection/class-remote-actors.php | Added cache_avatar() method to download and cache avatars during actor create/update; updated get_avatar_url() to attempt local caching for legacy data; removed avatar URL storage from prepare_custom_post_type() since it's now handled in cache_avatar() |
| includes/class-attachments.php | Added cache_actor_avatar() to download, optimize, and store avatar files; added delete_actor_avatar_files() to clean up avatar directories; added delete_actor_avatar_on_delete() hook to remove avatars when actors are deleted; added $actors_dir constant for avatar storage path |
Comments suppressed due to low confidence (1)
includes/collection/class-remote-actors.php:667
- The comment says "Returns the locally cached avatar URL if available, otherwise falls back to the default avatar", but the actual implementation attempts to download and cache the remote avatar if not found in meta (line 690). This could cause unexpected delays during page rendering if the avatar hasn't been cached yet.
Consider updating the docblock to accurately reflect the behavior:
/**
* Get the avatar URL for a remote actor.
*
* Returns the locally cached avatar URL if available. If not cached,
* attempts to download and cache the remote avatar. Falls back to
* the default avatar if no icon is available.
*
* @param int $id The ID of the remote actor post.
*
* @return string The avatar URL or empty string if not found.
*/ /**
* Get the avatar URL for a remote actor.
*
* Returns the locally cached avatar URL if available, otherwise falls back
* to the default avatar.
*
* @param int $id The ID of the remote actor post.
*
* @return string The avatar URL or empty string if not found.
*/
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
includes/collection/class-remote-actors.php:667
- The documentation comment says "Returns the locally cached avatar URL if available, otherwise falls back to the default avatar", but this is misleading. The method actually has three cases:
- Returns cached URL from meta (line 670)
- Returns default avatar if no icon exists (line 682)
- Attempts to cache and returns either local URL or remote URL (line 692)
The description should be updated to accurately reflect the fallback behavior:
* Returns the locally cached avatar URL if available. If not cached, attempts to
* download and cache the avatar. Falls back to the remote URL if caching fails,
* or to the default avatar if no icon exists.
/**
* Get the avatar URL for a remote actor.
*
* Returns the locally cached avatar URL if available, otherwise falls back
* to the default avatar.
*
* @param int $id The ID of the remote actor post.
*
* @return string The avatar URL or empty string if not found.
*/
includes/class-attachments.php:607
- This
save_fileimplementation usessanitize_file_name( basename( $url_path ) )and then writes the downloaded content directly intowp-content/uploads/activitypub/...without enforcing that the file is actually an image or that its extension is safe. Becausesave_actor_avatarand other callers pass attacker-controlled remote URLs here, a malicious ActivityPub actor can point their avatar at a.phppayload and have it written into the uploads tree (e.g.,wp-content/uploads/activitypub/actors/{id}/shell.php), which on many WordPress setups will be executed as PHP when requested, yielding remote code execution. To mitigate, strictly validate MIME type and extension before moving the file (only allow image types like jpg/png/webp, strip or reject disallowed extensions) and configure theactivitypubuploads subdirectories so that PHP execution is disabled at the web server level.
$url_path = \wp_parse_url( $attachment_data['url'], PHP_URL_PATH );
$file_name = \sanitize_file_name( \basename( $url_path ) );
$file_path = $paths['basedir'] . '/' . $file_name;
// Initialize filesystem if needed.
\WP_Filesystem();
global $wp_filesystem;
// Make sure file name is unique.
$counter = 1;
while ( $wp_filesystem->exists( $file_path ) ) {
$path_info = pathinfo( $file_name );
$file_name = $path_info['filename'] . '-' . $counter;
if ( ! empty( $path_info['extension'] ) ) {
$file_name .= '.' . $path_info['extension'];
}
$file_path = $paths['basedir'] . '/' . $file_name;
++$counter;
}
// Move file to destination.
if ( ! $wp_filesystem->move( $tmp_file, $file_path, true ) ) {
\wp_delete_file( $tmp_file );
return new \WP_Error( 'file_move_failed', \__( 'Failed to move file to destination.', 'activitypub' ) );
}
// Optimize images (resize and convert to WebP).
$optimized = self::optimize_image( $file_path, $max_dimension );
if ( $optimized['changed'] ) {
$file_path = $optimized['path'];
$file_name = \basename( $file_path );
}
// Get mime type and validate file.
$file_info = \wp_check_filetype_and_ext( $file_path, $file_name );
$mime_type = $file_info['type'] ?? $attachment_data['mediaType'] ?? '';
return array(
'url' => $paths['baseurl'] . '/' . $file_name,
'mime_type' => $mime_type,
includes/class-attachments.php:556
save_filedownloads whatever URL is provided inattachment_data['url']viadownload_urlwithout any host or address-range restrictions, andsave_actor_avatarpasses remote actor avatar URLs directly into this path. A malicious ActivityPub actor can set theiriconURL to an internal or sensitive endpoint (e.g. an internal HTTP service or metadata URL), causing the server to perform SSRF requests and persist the response underwp-content/uploads/activitypub/actors/, where it can be fetched back over the internet. To fix this, validate avatar/attachment URLs before callingdownload_url(e.g., enforcehttp/https, block private/loopback/metadata IP ranges, and optionally maintain an allowlist/denylist of hosts) and fail closed when validation fails.
private static function save_file( $attachment_data, $object_id, $object_type, $max_dimension = self::MAX_IMAGE_DIMENSION ) {
if ( ! \function_exists( 'download_url' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
// Download remote URL.
$tmp_file = \download_url( $attachment_data['url'] );
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ac8f90f to
65fbd48
Compare
b53ec6a to
e02f223
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Optimize imported ActivityPub images by resizing large images and converting to WebP format for reduced file sizes: - Add MAX_IMAGE_DIMENSION constant (1200px) - Add optimize_image() method using WordPress image editor - Convert images to WebP when supported, fallback to JPEG - Skip GIFs (may be animated) and SVGs (vector graphics) - Update save_file() and save_attachment() to optimize images - Update tests to handle WebP conversion
Address Copilot feedback: - Early return when no resize needed for PNG/WebP (avoids returning true) - Simplify result handling - editor->save() always returns array - Set changed=true when any save operation occurs
Allow callers to specify the maximum dimension for image resizing.
Allow callers to specify custom image dimensions when saving files, enabling reuse for different contexts like avatars.
- Return only file path from optimize_image instead of array with changed flag - Let WordPress auto-detect mime type instead of manually setting it - Add temp file extension handling for proper mime type detection - Add get_unique_path helper to prevent file collisions during format conversion - Move WP_Filesystem initialization to function start
Cache avatars from remote actors locally to reduce external dependencies and improve load times: - Add actors directory constant and cache_actor_avatar method - Download and optimize avatars on actor create/update - Resize avatars to 100px max (MAX_AVATAR_DIMENSION constant) - Store local URL in _activitypub_avatar_url meta - Clean up avatar files when actor is deleted - Fall back to remote URL if local caching fails
- Use get_storage_paths() for actor avatar storage paths. - Rename cache_actor_avatar to save_actor_avatar. - Rename delete_actor_avatar_files to delete_actors_directory. - Remove separate on_delete hook wrapper function. - Add 'actor' type support to get_storage_paths().
- Test successful avatar save with valid URL. - Test empty and invalid URL handling. - Test download error handling. - Test avatar replacement. - Test directory deletion. - Test non-actor post type protection.
- Test successful avatar save with valid URL. - Test empty and invalid URL handling. - Test download error handling. - Test avatar replacement. - Test directory deletion. - Test non-actor post type protection.
Co-authored-by: Konstantin Obenland <[email protected]>
- Swap save_actor_avatar parameter order (actor_id first) - Increase MAX_AVATAR_DIMENSION to 512px for better quality - Add actor_id validation to prevent path traversal - Add is_array check to cache_avatar for flexibility - Return avatar_url from cache_avatar - Use short ternary for avatar_url assignment - Update tests to match new parameter order
Co-authored-by: Konstantin Obenland <[email protected]>
018a8a3 to
5ff8a55
Compare
15da076 to
2b78427
Compare
|
Hi. Have you tested this with Automattic's Gravatar Enhanced? The plug-in caches not only Gravatars in comments, but also the Fediverse avatars. |
fixes #926
Proposed changes:
Other information:
Uses existing test coverage - the avatar caching gracefully handles test environments where HTTP downloads aren't mocked.
Note: This PR depends on #2609 (image optimization) - set as base branch.
Testing instructions:
wp-content/uploads/activitypub/actors/{actor_id}/for the cached avataractivitypub_update_remote_actors)Changelog entry
Changelog Entry Details
Significance
Type
Message
Add local caching for remote actor avatars.