WordPress rename attachment file with working thumbnails - php

I'm trying to write a function to rename the attachment file, both on upload, but also after upload.
I've written a nice function that can do this, but it results in missing thumbnails in the WP dashboard. I suspect it's because the GUID becomes wrong, but WP has made it impossible to change that GUID now (I think), so I'm not sure what else to do. Hoping someone here can help.
This is what I have, based on everything I could find online.
add_action("add_attachment", "rename_attachment", 10, 1);
function rename_attachment($post_id) {
custom_attachment_rename($post_id, "My name filename here");
}
function custom_attachment_rename( $post_id, $new_filename = 'new filename' ){
// Get path info of orginal file
$og_path = get_attached_file($post_id);
$path_info = pathinfo($og_path);
// Santize filename
$safe_filename = wp_unique_filename($path_info['dirname'], $new_filename);
// Build out path to new file
$new_path = $path_info['dirname']. "/" . $safe_filename . "." .$path_info['extension'];
// Rename the file and update it's location in WP
rename($og_path, $new_path);
update_attached_file( $post_id, $new_path );
// URL to the new file
$new_url = wp_get_attachment_url($post_id);
// Update attachment data
$id = wp_update_post([
'ID' => $post_id,
'post_title' => $new_filename,
'guid' => $new_url // Doesn't seem to work
]);
// Try this to reset the GUID for the attachment. Doesn't seem to actually reset it.
// global $wpdb;
// $result = $wpdb->update($wpdb->posts, ['guid' => $new_url], ['ID' => $post_id]);
// Update all links to old "sizes" files, or create it for new upload.
$metadata = get_post_meta($post_id, '_wp_attachment_metadata', true);
if( empty($metadata) ) {
// New upload.
$data = wp_generate_attachment_metadata($post_id, $new_path);
//update_post_meta($post_id, '_wp_attachment_metadata', $data); // Tried this. Doesn't work.
wp_update_attachment_metadata($post_id, $data); // Also doesn't work
} else {
// Regenerating an existing image
// TODO loop through $metadata and update the filename and resave? Maybe just delete it and regenerate instead?
}
// TODO Update use of the old filename in post_content throughout site
}
These have been the helpful posts I've gone over so far.
https://wordpress.stackexchange.com/questions/166943/how-to-rename-an-image-attachment-filename-using-php
https://wordpress.stackexchange.com/questions/30313/change-attachment-filename
https://wordpress.stackexchange.com/a/221254/4562
The weird thing is, if I die at the end of this function, then it works. So I suspect something else in WP is overwriting the _wp_attachment_metadata for this attachment. That is why I suspect the GUID is the issue. Something else is looking up the attachment via GUID and find a URL to a file that no longer exists (as I changed the filename) and running wp_generate_attachment_metadata on a bad file path. That is my hunch.
I don't have any other plugins installed.

The reason your code doesn't work is not related to the GUID.
It is because, in WP core, the function wp_update_attachment_metadata() is called with the original file name at the end of the media upload request handling. And the hook add_attachment is called inside wp_insert_attachment() function.
function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrides = array( 'test_form' => false ) ) {
... ...
// Save the data.
$attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true );
if ( ! is_wp_error( $attachment_id ) ) {
// Set a custom header with the attachment_id.
// Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
if ( ! headers_sent() ) {
header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
}
// The image sub-sizes are created during wp_generate_attachment_metadata().
// This is generally slow and may cause timeouts or out of memory errors.
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
}
return $attachment_id;
}
We can use the "wp_update_attachment_metadata" filter to change the metadata with the new file name.
Please check the below code. I tested and it is working well.
class CustomAttachmentRename {
public $new_filename = 'custom file';
private $new_path;
function __construct () {
add_action("add_attachment", array($this, "rename_attachment"), 10, 1);
}
function rename_attachment($post_id) {
// Get path info of orginal file
$og_path = get_attached_file($post_id);
$path_info = pathinfo($og_path);
// Santize filename
$safe_filename = wp_unique_filename($path_info['dirname'], $this->new_filename);
// Build out path to new file
$this->new_path = $path_info['dirname']. "/" . $safe_filename . "." .$path_info['extension'];
// Rename the file and update it's location in WP
rename($og_path, $this->new_path);
update_attached_file( $post_id, $this->new_path );
// Register filter to update metadata.
add_filter('wp_update_attachment_metadata', array($this, 'custom_update_attachment_metadata'), 10, 2);
}
function custom_update_attachment_metadata($data, $post_id) {
return wp_generate_attachment_metadata($post_id, $this->new_path);
}
}
$customAttachmentRename = new CustomAttachmentRename();
$customAttachmentRename->new_filename = "test image" . time();

I turned #chengmin answer into a single function like this:
/**
* Renames a file. Will also regenerate all the thumbnails that go with the file.
* #SEE https://stackoverflow.com/questions/64990515/wordpress-rename-attachment-file-with-working-thumbnails
*
* #param string $post_id The WordPress post_id for the attachment
* #param string $new_file_name The filename (without extension) that you want to rename to
*/
function attachment_rename($post_id, $filename) {
// Get path info of orginal file
$og_url = wp_get_attachment_url($post_id);
$og_path = get_attached_file($post_id);
$path_info = pathinfo($og_path);
$og_meta = get_post_meta($post_id, '_wp_attachment_metadata', true);
// Santize filename
$safe_filename = wp_unique_filename($path_info['dirname'], $filename);
// Build out path to new file
$new_path = $path_info['dirname']. "/" . $safe_filename . "." .$path_info['extension'];
// Rename the file in the file system
rename($og_path, $new_path);
// Delete old image sizes if we have them
if( !empty($og_meta) ) {
delete_attachment_files($post_id);
}
// Now save new path to file in WP
update_attached_file( $post_id, $new_path );
// Register filter to update metadata
$new_data = wp_generate_attachment_metadata($post_id, $new_path);
return add_filter('wp_update_attachment_metadata', function($data, $post_id) use ($new_data) {
return $new_data;
}, 10, 2);
}
/**
* Delete all old image files, if they aren't used post_content anywhere.
* The dont-delete check isn't perfect, it will give a lot of false positives (keeping more files than it should), but it's best I could come up with.
* #SEE https://github.com/WordPress/WordPress/blob/f4cda1b62ffca52115e4b04d9d75047060d69e68/wp-includes/post.php#L5983
*
* #param string $post_id The WordPress post_id for the attachment
*/
function delete_attachment_files($post_id) {
$meta = wp_get_attachment_metadata( $post_id );
$backup_sizes = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
$file = get_attached_file( $post_id );
$url = wp_get_attachment_url($post_id);
// Remove og image so it doesn't get deleted in wp_delete_attachment_files()
$meta['original_image'] = "";
// Check if image is used in a post somehwere
$url_without_extension = substr($url, 0 , (strrpos($url, ".")));
$args = [
"s" => $url_without_extension,
"posts_per_page" => 1,
"post_type" => "any"
];
$found = get_posts($args);
if( empty($found) ) {
return wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file );
}
return false;
}

Related

wp_delete_attachment, wp_delete_file_from_directory and wp_delete_file() does not delete file from server

I am trying to regenerate a PDF file whenever a post is being saved/updated. The PDF file name changes according to post_meta data, therefore I want to delete the existing PDF attachment AND the file from the server when a post is saved/updated, before the pdf gets regenerated and attached.
wp_delete_attachment() deletes the attachment alright, but the file stays on the server even when forced to delete.
I also tried wp_delete_file_from_directory( $file, $path); It returns true for having deleted the file, but the file stays on the server. Same for wp_delete_file();
The only thing that seemed to be working was unlink(), but that creates another problem, because in case the file name doesn't change, unlink() seems to put a stop to creating the file with the same name.
wp_update_post( $my_post );
if(get_post_status( $post_id ) == "publish"):
$existing_PDFs = get_attached_media('application/pdf', $post_id);
foreach($existing_PDFs as $pdf):
$file = get_attached_file($pdf->ID, true);
$path = pathinfo($file, PATHINFO_DIRNAME);
wp_delete_file_from_directory( $file, $path);
wp_delete_file( $file );
wp_delete_attachment($pdf->ID, true);
endforeach;
include('generate-single-machine-pdf.php');
endif;
What's the secret to get Wordpress to delete a file from the server along with the attachment?
The solution was to conditionally use unlink after wp_delete_attachment in case the generated PDF had the same name as the deleted PDF. In that case no deletion of attachment or unlinking was needed, only to overwrite the existing file.
For updating posts:
$savePath = trailingslashit($uploads_dir).$new_file_name.'.pdf';
$pdf->Output($savePath, 'F');
$existing_PDF = get_attached_media('application/pdf', $post_id);
foreach($existing_PDF as $pdf):
$oldPDFID= $pdf->ID;
$file = get_attached_file($oldPDFID, true);
$old_file_name = pathinfo($file,PATHINFO_BASENAME);
endforeach;
}
$old_pdf = pathinfo($file,PATHINFO_BASENAME);
#######CREATE PDF ATTACHMENT####
$args = array(
'post_title' => "$new_file_name",
'post_content' => '',
'post_mime_type' => 'application/pdf',
);
$new_file_name = $new_file_name.".pdf";
//DELETE OLD ATTACHMENT
if($new_file_name != $old_file_name):
wp_delete_attachment($oldPDFID, true);
unlink($file);
endif;
$pdfID = wp_insert_attachment($args, $savePath, $post_id);

Dynamically attaching file to Contact Form 7 E-Mail

I'm working in WordPress with Contact Form 7. I'm dynamically creating a Word Document based on the users submitted data, and I want to attach that file to the e-mail that the user is sent from Contact Form 7.
To confirm, the file is created and saved in the correct location. My issue is definitely with attaching it to the e-mail from CF7.
I have the following code at the moment:
add_action('wpcf7_before_send_mail', 'cv_word_doc');
function cv_word_doc($WPCF7_ContactForm) {
// Check we're on the CV Writer (212)
if ( $WPCF7_ContactForm->id() === 212 ) {
//Get current form
$wpcf7 = WPCF7_ContactForm::get_current();
// get current SUBMISSION instance
$submission = WPCF7_Submission::get_instance();
if ($submission) {
// get submission data
$data = $submission->get_posted_data();
// nothing's here... do nothing...
if (empty($data))
return;
// collect info to add to word doc, removed for brevity...
// setup upload directory and name the file
$upload_dir = wp_upload_dir();
$upload_dir = $upload_dir['basedir'] . '/cv/';
$fileName = 'cv-' . str_replace(' ', '-', strtolower($firstName)) . '-' . str_replace(' ', '-', strtolower($lastName)) .'-'. time() .'.docx';
// PHPWord stuff, removed for brevity...
// save the doc
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$objWriter->save($upload_dir . $fileName);
// add upload to e-mail
$submission->add_uploaded_file('docx', $upload_dir . $fileName);
// carry on with cf7
return $wpcf7;
}
}
}
Everything works up until $submission->add_uploaded_file('docx', $upload_dir . $fileName);.
There's not much documentation about, but I have read that I need to include something similar to:
add_filter( 'wpcf7_mail_components', 'mycustom_wpcf7_mail_components' );
function mycustom_wpcf7_mail_components( $components ) {
$components['attachments'][] = 'full path of your PDF file';
return $components;
}
(source: https://wordpress.stackexchange.com/questions/239579/attaching-a-pdf-to-contact-form-7-e-mail-via-functions-php)
In order to get the attachment to show. However, I don't know how I can get the specific file that I need, as the file is unique to each submission and all of the variables will be in a separate function.
Fixed this issue.
I added:
// make file variable global
global $CV_file;
$CV_file = $upload_dir . $fileName;
after $submission->add_uploaded_file('docx', $upload_dir . $fileName);
Then seperate to the add_action, I used the filter and referenced the global var:
add_filter( 'wpcf7_mail_components', 'mycustom_wpcf7_mail_components' );
function mycustom_wpcf7_mail_components( $components ) {
global $CV_file;
$components['attachments'][] = $CV_file;
return $components;
}
Here is the working solution:
add_filter('wpcf7_mail_components', 'custom_wpcf7_mail_components');
function custom_wpcf7_mail_components($components)
{
//Get current form
$wpcf7 = WPCF7_ContactForm::get_current();
$attachment_file_path = '';
//check the relevant form id
if ($wpcf7->id == '30830') {
// get current SUBMISSION instance
$submission = WPCF7_Submission::get_instance();
if ($submission) {
// get submission data
$data = $submission->get_posted_data();
// setup upload directory
$upload_dir = wp_upload_dir();
if (isset($data['file_name']) && !empty($data['file_name'])) {
/*
* Form hidden attachment file name Ex: 'ProRail_NA_Gen5.pdf'
* You can hard-code the file name or set file name to hidden form field using JavaScript
*/
$file_name = $data['file_name'];
//get upload base dir path Ex: {path}/html/app/uploads
$base_dir = $upload_dir['basedir'];
//file uploaded folder
$file_dir = 'download';
//set attachment full path
$attachment_file_path = $base_dir .'/'.$file_dir.'/'.$file_name;
//append new file to mail attachments
$components['attachments'][] = $attachment_file_path;
}
}
}
return $components;
}
I'll leave a full example using wpcf7_before_send_mail to create and store the PDF and wpcf7_mail_components to attach it to the email.
PDF created with FPDF.
<?php
/**
* Plugin Name: Create and attach PDF to CF7 email
* Author: brasofilo
* Plugin URL: https://stackoverflow.com/q/48189010/
*/
!defined('ABSPATH') && exit;
require_once('fpdf/fpdf.php');
add_action('plugins_loaded', array(SendFormAttachment::get_instance(), 'plugin_setup'));
class SendFormAttachment {
protected static $instance = NULL;
public $formID = 5067; # YOUR ID
public $theFile = false;
public function __construct() { }
public static function get_instance() {
NULL === self::$instance and self::$instance = new self;
return self::$instance;
}
public function plugin_setup() {
add_action('wpcf7_before_send_mail', function ( $contact_form, $abort, $submission ) {
if ($contact_form->id() == $this->formID) {
$posted_data = $submission->get_posted_data();
$uploads = wp_upload_dir();
$the_path = $uploads['basedir'] . '/cf7_pdf/';
$fname = $this->createPDF($posted_data, $the_path);
$this->theFile = $the_path . $fname;
}
}, 10, 3);
add_filter( 'wpcf7_mail_components', function( $components ) {
if( $this->theFile )
$components['attachments'][] = $this->theFile;
return $components;
});
}
public function createPDF($posted_data, $savepath) {
$pdf = new FPDF();
$pdf->AliasNbPages();
$pdf->AddPage();
$pdf->SetFont('Times','',12);
$pdf->SetCreator('example.com');
$pdf->SetAuthor('Author name', true);
$pdf->SetTitle('The title', true);
$pdf->SetSubject('The subject', true);
for($i=1;$i<=40;$i++)
$pdf->Cell(0,10,'Printing line number '.$i,0,1);
$filename = rand() . '_' . time() . '.pdf';
$pdf->Output('F', $savepath . $filename);
return $filename;
}
}
The original code would have worked up until V5.4 where add_uploaded_file was made private.
There is a replacement public function you can use to attach the file with the absolute file path:
$submission->add_extra_attachments( $absolute_file_path );
It may also work with paths relative to the wp_uploads folder.

Make Wordpress wp_delete_attachment delete files from a pre-defined custom folder

We have created a custom post type which allows our client to upload files to a folder outside of the standard Wordpress uploads folder (wp-content/upload-assets). These files are to be handled seperately from the standard wp-content/uploads folder and this is why we CANNOT use
define( 'UPLOADS', 'mycustomfolder' );
in the wp-config.php.
Instead we use this to temporarily change the uploads folder to wp-content/upload-assets:
add_filter('upload_dir', 'my_upload_dir');
$uploaded_file = wp_handle_upload($_FILES['xxxx_image'], $upload_overrides);
remove_filter('upload_dir', 'my_upload_dir');
We are using this to remove all attachments from a particular post:
add_filter('upload_dir', 'my_upload_dir');
$attachments = get_posts( array(
'post_type' => 'attachment',
'posts_per_page' => -1,
'post_status' => 'any',
'post_parent' => $pid
) );
foreach ( $attachments as $attachment ) {
if ( false === wp_delete_attachment( $attachment->ID, true ) ) {
echo 'Attachment could not be deleted.';
}
}
remove_filter('upload_dir', 'my_upload_dir');
wp_delete_attachment should also delete all associated files from disk but it doesn't work because our files are in our custom folder (wp-content/upload-assets).
Here's the code for our my_upload_dir function:
function my_upload_dir($upload) {
$upload['subdir'] = '';
$upload['basedir'] = WP_CONTENT_DIR;
$upload['baseurl'] = WP_CONTENT_URL;
$upload['path'] = $upload['basedir'] . '/upload-assets';
$upload['url'] = $upload['baseurl'] . '/upload-assets';
return $upload;
}
How do we make wp_delete_attachment remove the files in our custom wp-content/upload-assets folder?
Hi You can do this way if want to delete the file, but it will not remove all post meta fields, taxonomy, comments, etc. associated with the attachment.
Hope this help
foreach ( $attachments as $attachment ) {
if ( false === wp_delete_attachment( $attachment->ID, true ) ) {
$file = get_attached_file( $attachment->ID );
$file = str_replace( "uploads", "upload-assets", $file);
wp_delete_file( $file );
}
}
I have not tested this code, but i hope it should work
Staying in the domain of current question you can use "get_attached_file" filter to directly alter the file path string used to grab the path of file to be deleted. Specifically add this function to your functions.php
function sr_update_uploads_directory( $file )
{
return str_replace( "uploads", "upload-assets", $file) // Change path to upload-assets from uploads
}
add_filter( 'get_attached_file', 'sr_update_uploads_directory' );
Don't have time for testing currently. So, please excuse me for that. Hope it helps. :)

Wordpress multisite wp_upload_dir wrong

currently i'm trying to upload a Image to a Wordpress-Multisite at server side.
(The image is already on another path at the same server)
I've written a small PHP-script which should handle this:
$upload_dir = wp_upload_dir();
var_dump( $upload_dir);
$filename = basename($imagepath);
if (wp_mkdir_p($upload_dir['path'])) {
$file = $upload_dir['path'].'/'.$filename;
} else {
$file = $upload_dir['basedir'].'/'.$filename;
}
copy($imagepath, $file);
$wp_filetype = wp_check_filetype($filename, null);
$attachment = array(
'guid' => $upload_dir['url'].'/'.basename($filename),
'post_mime_type' => $wp_filetype['type'],
'post_title' => sanitize_file_name($filename),
'post_content' => '',
'post_status' => 'inherit', );
$attach_id = wp_insert_attachment($attachment, $file, $postid);
Everything works fine, but the image is stored in the wrong folder.
wp_upload_dir(); returns the common path /wp-content/uploads/2016/03, not the path for the specific subsite, wich will be /wp-content/uploads/sites/SITE_ID/2016/03
Later when WP uses the image, the url of the image is set to http://example.com/wp-content/uploads/sites/SITE_ID/2016/03/IMAGE.JPG (which is not correct...)
Uploading a file with the built-in media-upload tool from wordpress works correct (files are stored in /wp-content/uploads/sites/SITE_ID/2016/03)
You see, wp_upload_dir() is not working correct..
Thanks..
i solved it!
For everyone with the same problem, the solution is simple:
When you're on multisite, register a filter for "upload_dir"
define a function, where you change temporarily the paths and urls
That's it!
if(is_multisite()){
add_filter('upload_dir', 'fix_upload_paths');
}
function fix_upload_paths($data)
{
$data['basedir'] = $data['basedir'].'/sites/'.get_current_blog_id();
$data['path'] = $data['basedir'].$data['subdir'];
$data['baseurl'] = $data['baseurl'].'/sites/'.get_current_blog_id();
$data['url'] = $data['baseurl'].$data['subdir'];
return $data;
}
Hope someone can use it. Hours of hours for me.
A simpler method is to use the switch_to_blog() function
if( is_multisite() ) {
switch_to_blog( get_current_blog_id() );
$imgdir = wp_upload_dir();
// do more process related to current blog
// close the swicth
restore_current_blog();
}
See details at https://developer.wordpress.org/reference/functions/switch_to_blog/

Wordpress copy to uploads directory creates 0 bytes files

For some reason, this code I've been working with since the beginning seems to have stopped working on my dev environment which is set up on a free hosting service. However, it still works locally. Now, I can't recall if I've ever actually tested it on the dev environment so maybe it never worked there to begin with.
Basically, I'm using Gravity Forms to create custom posts (works just fine) and am also using multiple file uploads to add image attachments to the post. Since that's not entirely supported by GF yet I fiddled around and managed to create this little function here:
function create_post_attachment_from_gf($f, $from_url) {
//for image field, it sends content with a bunch of other parameters in a string with pipe delimiters
$split_image = explode('|', $from_url);
if( count($split_image) > 0 )
$from_url = $split_image[0];
if( !empty( $_FILES[$f]['name'] ) ) {
$file = $_FILES[$f];
$uploads = wp_upload_dir( null );
$filename = wp_unique_filename( $uploads['path'], $file['name'], null );
// Strip the query strings.
$filename = str_replace('?','-', $filename);
$filename = str_replace('&','-', $filename);
// Compute the URL
$url = $uploads['url'] . "/$filename";
$new_file = $uploads['path'] . "/$filename";
copy($from_url,$new_file);
// Move the file to the uploads dir
$stat = stat( dirname( $new_file ));
$perms = $stat['mode'] & 0000666;
# chmod( $new_file, $perms );
return array( 'file' => $new_file, 'url' => $url, 'type' => $file['type'], 'name' => $file['name'] );
}
return false;
}
And then call it this way to create the attachment:
$copied_file = create_post_attachment_from_gf('input_' . $file_uploads[$i], $entry[$file_uploads[$i]]);
$subdir = wp_upload_dir( null )["subdir"];
if( $copied_file ){
$attachment = array(
'guid' => $copied_file['url'],
'post_mime_type' => $copied_file['type'],
'post_title' => $copied_file['name'],
'post_content' => '',
'post_status' => 'inherit'
);
$attach_id = wp_insert_attachment( $attachment, $subdir . '/' .$copied_file['name'], $post_id );
$attach_data = wp_generate_attachment_metadata( $attach_id, $copied_file['file'] );
wp_update_attachment_metadata( $attach_id, $attach_data );
}
It looks like the copy is what fails here. It creates an empty file in my uploads folder with the right filename. I checked my folder permissions and even went ahead and set everything under uploads (including the gravity forms folders) to 777 and it still doesn't work for some reason.
Both my local and remote websites are running on WP 3.8.1 and all plugins are up to date on both. The databases are pretty much exactly the same aside from some test data which shouldn't interfere with this. Adding an attachment directly from the dashboard works fine. Is this something I should take up with the hosting company? Or is my code faulty? Or maybe there's a way to create an attachment without moving the file - that'd be fine with me.
Any help is appreciated.
Well, it looks like, while the copying still doesn't work remotely, I could've been uploading the image directly to the right folder from the get go.
//Have gform upload directly in the upload folder
add_filter("gform_upload_path", "change_upload_path", 10, 2);
function change_upload_path($path_info, $form_id){
$uploads = wp_upload_dir( null );
$path_info["path"] = $uploads['path'] . "/";
$path_info["url"] = $uploads['url'] . "/";
return $path_info;
}
So I removed the part that moved the file over and recreated the filename from my previous function and everything works as planned now. Can't believe I spent a few hours on this... Hopefully this helps someone else?

Categories