Repository navigation
Report failed DAM imports instead of claiming success - #1358
Conversation
download_asset() returns a WP_Error when the download fails, but import_asset() only caught exceptions, so the error was dropped and the REST endpoint answered with success. The editor got a broken attachment with no file, and handle_failed_download() never ran. - import_asset() now returns the download error, so the attachment is deleted and the error is sent to the media modal. - handle_failed_download() logs the attachment ID, public_id and error to the plugin debug log and the PHP error log. - download_asset() deletes the file it primed for the download when the request fails or the response isn't a 200, instead of leaving an empty file in the uploads folder.
The endpoint only required the upload_files capability and used the posted attachment ID as is. Now that a failed download deletes the attachment, any user able to upload files could delete any attachment by posting its ID with a URL that can't be downloaded. - The endpoint now requires the attachment to exist and the user to be able to edit it. - A failed download only deletes an attachment that has no file yet and that the user can delete. - The logged public_id is read from the stored meta, instead of one generated when it's missing. - The error, which can come from a remote response header, is sanitized before it's logged or returned. - The primed file is also removed when an exception is thrown during the download.
Deleting the attachment of a failed import also ran the delete sync, which sends a destroy request for the asset on Cloudinary: a new import already counts as synced before its file is downloaded. The request currently fails because the attachment has no resource type yet, but the source asset the user picked must never be at risk. The cleanup now deletes the attachment in WordPress only. The exception cleanup in download_asset() also uses a flag to know whether the primed file became the attachment's file, instead of comparing paths that a filter can rewrite.
The DAM page called the asset download endpoint after every insert, including assets already in the Media Library, which have no attachment to download into. The endpoint now rejects that request, and with no error handler the import progress never cleared. A failed download was also shown as a success. - Only call the download endpoint for new assets, as the media modal does. - Show the error when the download fails or the request is rejected. - Always clear the import progress.
| $error | ||
| ); | ||
| Utils::log( $message, 'download-sync-failed' ); | ||
| error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
There was a problem hiding this comment.
Nit (optional): error_log() runs on every failed import, even when WP_DEBUG is off. Utils::log() above already writes the message to the System Report. Consider calling error_log() only when WP_DEBUG_LOG is on, so production PHP logs stay clean.
There was a problem hiding this comment.
Thanks! I'd rather keep it unconditional: one of the issues this fixes is that failures were logged nowhere.
Utils::log() only stores the message while the System Report is enabled, which is off by default, so a production site would otherwise have no trace of failed imports.
It's one line per failed import, which should be rare, and with WP_DEBUG_LOG off it goes to the server's PHP error log, where we'd want to look.
|
|
||
| // Send error. | ||
| // Send error. The media modal expects the admin-ajax response format. | ||
| wp_send_json_error( $error ); |
There was a problem hiding this comment.
Nit (optional, follow-up): This REST callback still answers with wp_send_json_error() (status 200) and not with a REST error. This was already the case, and the comment explains why (media-modal.js expects the admin-ajax format), so it's fine for this PR. It would be good to move to WP_Error responses with a proper status code later, and update both JS callers.
There was a problem hiding this comment.
Agreed, we'll do it in a follow-up: return a WP_Error with a proper status from the endpoint and update both JS callers. Core already flags wp_send_json() during REST requests as incorrect usage since WP 5.5 (it only adds an X-WP-DoingItWrong header for now), so it's worth doing. Leaving it out of this PR, but the follow-up ticket we already have planned can potentially handle this.
| * @param {string} message The error message. | ||
| */ | ||
| showError( message ) { | ||
| window.alert( |
There was a problem hiding this comment.
Nit (optional): When a user inserts several assets and they all fail, this shows one window.alert() for each asset. One combined message would be easier for the user.
There was a problem hiding this comment.
Good point. The media modal also shows one alert per asset, so I'd rather change both together in the same follow-up as the REST error responses, to keep them consistent.
Fixes failed imports from the Cloudinary DAM being reported as successful.
The issue
When the server fails to download an asset inserted from the DAM, for example a network error or a
401/404/5xxresponse, the import is reported as a success:Download_Sync::download_asset()returns aWP_Erroron failure, butimport_asset()only caught exceptions. The error was dropped and the REST endpoint responded withsuccess: true.handle_failed_download()never ran, and it didn't log anything anyway.One way to hit this is an account using
authenticateddelivery: inserting an asset with a crop or an effect applied makes the plugin download the unsigned derived URL, which Cloudinary answers with a401. That underlying download issue is out of scope here and will be handled separately. This PR makes sure any such failure is reported and cleaned up.The solution
import_asset()now returns the download error. The REST endpoint then callshandle_failed_download(), which deletes the attachment and sends the error to the media modal, which shows it in an alert.handle_failed_download()logs the attachment ID, the stored public_id and the error, both to the plugin debug log (System Report) and to the PHP error log. The error can come from Cloudinary'sx-cld-errorheader, so it's sanitized first.download_asset()deletes the primed file when the request fails, the response isn't a200, or an exception is thrown. This also covers the other callers (Storage,Unsync).upload_filesand took the postedattachment_idas is. Since a failed download now deletes the attachment, that would have let any Author delete any attachment. The endpoint now requires an existing attachment the user can edit (edit_post), and a failed download only deletes an attachment that has no file yet and that the user can delete (delete_post). The normal flow is unaffected: the attachment is created by the user doing the import. This also stops an Author from replacing another user's attachment file through this endpoint, which was already possible before.destroyrequest for the asset on Cloudinary, and a new import already counts as synced before its file is downloaded. That request currently fails because the pending attachment has no resource type yet, but the asset the user picked in the DAM must never be at risk.media-library.js) called the same endpoint after every insert, including assets already in the Media Library, which have no attachment to download into. It now only calls it for new assets, as the media modal does, shows the error when the download fails or the request is rejected, and always clears the import progress. Without this, the stricter permission check would leave the progress spinner stuck when re-inserting an imported asset. Includes the rebuiltjs/cloudinary.js.tests/phpunit/tests/test-download-sync.php(16 tests), including the permission checks through the REST route and a check that nodestroyrequest is made.Known limitation: after a failed import, a tile without a thumbnail can stay in the media modal grid until the page is reloaded. It comes from the existing
.fail()handler inmedia-modal.js, which was rarely reached before this fix. The attachment itself is deleted.QA notes
Prerequisites
WP_DEBUGandWP_DEBUG_LOGenabled (both on by default in the repo'swp-env).Simulate a failed download. Add this mu-plugin, e.g.
.wp-env/mu-plugins/qa-block-cloudinary-download.phponwp-env. It makes the server's download of the asset from Cloudinary fail, while the DAM keeps working in the browser:To follow the debug log while testing on
wp-env(the site'swp-contentfolder isn't the one in the repo):Steps
develop: no error is shown. Depending onWP_DEBUG_DISPLAY, the item either stays in the modal with a grey placeholder or silently disappears (a PHP warning breaks the JSON response). After a reload, Media > Library has a new attachment with no image, there's a leftover file in the uploads folder (a few bytes, or empty on a4xx/5xxresponse), and nothing is logged.QA: blocked download from Cloudinary. No new attachment is in Media > Library after a reload, no new file is in the uploads folder, andwp-content/debug.loghas a lineCloudinary: failed to import asset into attachment <id> (public_id: <public_id>). Deleting the attachment. Error: QA: blocked download from Cloudinary.develop: no error is shown, and the import progress clears as if it worked.