Changeset 3368671
- Timestamp:
- 09/26/2025 07:49:23 PM (6 months ago)
- Location:
- safe-attachment-names/trunk
- Files:
-
- 2 edited
-
readme.txt (modified) (1 diff)
-
safe-attachment-names.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
safe-attachment-names/trunk/readme.txt
r3151081 r3368671 4 4 Tags: admin, attachment, accents, administration, latin, special characters, attachments 5 5 Requires at least: 5.0 6 Tested up to: 6. 6.26 Tested up to: 6.8.2 7 7 Stable tag: trunk 8 8 License: GPLv2 or later -
safe-attachment-names/trunk/safe-attachment-names.php
r3151081 r3368671 19 19 class safe_attachment_names { 20 20 21 private static $_instance = null; 22 23 /** 24 * Bootstrap 25 * 26 * @author Tommy Lacroix <tlacroix@nuagelab.com> 27 * @access public 28 */ 29 public static function boot() 30 { 31 if (self::$_instance === null) { 32 self::$_instance = new safe_attachment_names(); 33 self::$_instance->setup(); 34 return true; 35 } 36 return false; 37 } // boot() 38 39 40 /** 41 * Setup plugin 42 * 43 * @author Tommy Lacroix <tlacroix@nuagelab.com> 44 * @access public 45 */ 46 public function setup() 47 { 48 global $current_blog; 49 50 // Add admin menu 51 add_action('admin_menu', array(&$this, 'add_admin_menu')); 52 53 // Set wp_handle_upload_prefilter 54 add_filter('wp_handle_upload_prefilter', array($this, 'upload_prefilter')); 55 56 // Load text domain 57 load_plugin_textdomain('safe-attachment-name', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/'); 58 } // setup() 59 60 61 /** 62 * Add admin menu action; added by setup() 63 * 64 * @author Tommy Lacroix <tlacroix@nuagelab.com> 65 * @access public 66 */ 67 public function add_admin_menu() 68 { 69 // This part is not ready yet 70 //add_management_page(__("Sanitize attachment names",'safe-attachment-name'), __("Sanitize attachment names",'safe-attachment-name'), 'update_core', basename(__FILE__), array(&$this, 'admin_page')); 71 } // add_admin_menu() 72 73 74 /** 75 * Admin page action; added by add_admin_menu() 76 * 77 * @author Tommy Lacroix <tlacroix@nuagelab.com> 78 * @access public 79 */ 80 public function admin_page() 81 { 82 if (isset($_POST['action'])) { 83 if (wp_verify_nonce($_POST['nonce'],$_POST['action'])) { 84 $parts = explode('+',$_POST['action']); 85 switch ($parts[0]) { 86 case 'sanitize': 87 if (!$_POST['accept-terms']) { 88 $error_terms = true; 89 } else { 90 return $this->do_change(); 91 } 92 break; 93 } 94 } 95 } 96 97 if (!isset($error_terms)) $error_terms = false; 98 99 echo '<div class="wrap">'; 100 101 echo '<div id="icon-tools" class="icon32"><br></div>'; 102 echo '<h2>'.__('Sanitize Attachment Names','safe-attachment-name').'</h2>'; 103 echo '<form method="post">'; 104 105 $action = 'sanitize+'.uniqid(); 106 wp_nonce_field($action,'nonce'); 107 108 echo '<input type="hidden" name="action" value="'.$action.'" />'; 109 110 echo '<table class="form-table">'; 111 echo '<tbody>'; 112 113 echo '<tr valign="top">'; 114 echo '<td colspan="2"><input type="checkbox" name="accept-terms" id="accept-terms" value="1" /> <label for="accept-terms"'.($error_terms?' style="color:red;font-weight:bold;"':'').'>'.__('I have backed up my database and will assume the responsability of any data loss or corruption.','safe-attachment-name').'</label></td>'; 115 echo '</tr>'; 116 117 echo '</tbody></table>'; 118 119 echo '<p class="submit"><input type="submit" name="submit" id="submit" class="button-primary" value="'.esc_html(__('Sanitize','safe-attachment-name')).'"></p>'; 120 121 echo '</form>'; 122 123 echo '</div>'; 124 } // admin_page() 125 126 127 /** 128 * Change domain. This is where the magic happens. 129 * Called by admin_page() upon form submission. 130 * 131 * @author Tommy Lacroix <tlacroix@nuagelab.com> 132 * @access private 133 */ 134 private function do_change() 135 { 136 global $wpdb; 137 138 @set_time_limit(0); 139 140 echo '<div class="wrap">'; 141 142 echo '<div id="icon-tools" class="icon32"><br></div>'; 143 echo '<h2>Sanitizing attachment names</h2>'; 144 echo '<pre>'; 145 146 mysql_connect(DB_HOST, DB_USER, DB_PASSWORD); 147 mysql_select_db(DB_NAME); 148 mysql_query('SET NAMES '.DB_CHARSET); 149 if (function_exists('mysql_set_charset')) mysql_set_charset(DB_CHARSET); 150 151 $upload_dir = wp_upload_dir(); 152 153 $ret = mysql_query('SELECT * FROM '.$wpdb->prefix.'posts WHERE post_type="attachment";'); 154 while ($row = mysql_fetch_assoc($ret)) { 155 $ret2 = mysql_query('SELECT * FROM '.$wpdb->prefix.'postmeta WHERE post_id='.intval($row['ID']).';'); 156 $metas = array(); 157 while ($row2 = mysql_fetch_assoc($ret2)) { 158 $metas[$row2['meta_key']] = $row2; 159 } 160 161 // Check if filename is problematic 162 $fn1 = $metas['_wp_attached_file']['meta_value']; 163 $fn2 = htmlentities($fn1, null, 'UTF-8'); 164 165 $fns = array($fn1, utf8_encode($fn1), utf8_decode($fn1)); 166 167 if ($fn1 != $fn2) { 168 $error = false; 169 $fn4 = $this->sanitizeFilename($fn1); 170 171 echo $fn1 . ' -> '.$fn4; 172 173 $fnx = false; 174 foreach ($fns as $x) { 175 if (file_exists($upload_dir['basedir'].'/'.$x)) { 176 $fnx = $x; 177 break; 178 } 179 } 180 if (!$fnx) { 181 _e(': error, cannot find source file.'); 182 echo PHP_EOL; 183 $error = true; 184 } else { 185 _e( ': OK.' ); 186 echo PHP_EOL; 187 } 188 189 // Rename file 190 $renames = array(); 191 $renames[$upload_dir['basedir'].'/'.$fnx] = $upload_dir['basedir'].'/'.$fn4; 192 193 // Update _wp_attachment_metadata 194 $info = unserialize($metas['_wp_attachment_metadata']['meta_value']); 195 $info['file'] = $fn4; 196 197 if ($info['sizes']) { 198 foreach ( $info['sizes'] as &$size ) { 199 $sf1 = dirname( $fn1 ) . '/' . $size['file']; 200 $sf4 = $this->sanitizeFilename( $sf1 ); 201 if ( $sf1 != $sf4 ) { 202 echo $sf1 . ' -> ' . $sf4; 203 204 $fns = array( $sf1, utf8_encode( $sf1 ), utf8_decode( $sf1 ) ); 205 $sfx = false; 206 foreach ( $fns as $x ) { 207 if ( file_exists( $upload_dir['basedir'] . '/' . $x ) ) { 208 $sfx = $x; 209 break; 210 } 211 } 212 if ( ! $sfx ) { 213 _e( ': error, cannot find source file.' ); 214 echo PHP_EOL; 215 $error = true; 216 } else { 217 _e( ': OK.' ); 218 echo PHP_EOL; 219 } 220 221 // Rename file 222 $renames[ $upload_dir['basedir'] . '/' . $sfx ] = $upload_dir['basedir'] . '/' . $sf4; 223 224 $size['file'] = basename( $sf4 ); 225 } 226 } 227 } 228 229 if (!$error) { 230 // Rename all 231 foreach ( $renames as $f1 => $f2 ) { 232 rename( $f1, $f2 ); 233 } 234 235 // Update _wp_attachment 236 mysql_query( 'UPDATE ' . $wpdb->prefix . 'postmeta SET meta_value="' . mysql_real_escape_string( $fn4 ) . '" WHERE meta_id=' . $metas['_wp_attached_file']['meta_id'] . ';' ); 237 238 mysql_query( 'UPDATE ' . $wpdb->prefix . 'postmeta SET meta_value="' . mysql_real_escape_string( serialize( $info ) ) . '" WHERE meta_id=' . $metas['_wp_attachment_metadata']['meta_id'] . ';' ); 239 240 // Update guid 241 mysql_query( 'UPDATE ' . $wpdb->prefix . 'posts SET guid="' . mysql_real_escape_string( $upload_dir['baseurl'] . '/' . $fn4 ) . '" WHERE post_id=' . $row['ID'] . ';' ); 242 } 243 } else { 244 //echo $fn1 . ' [ok]'.PHP_EOL; 245 } 246 } 247 248 echo '</pre>'; 249 echo '<hr>'; 250 echo '<form method="post"><input type="submit" value="'.esc_html(__('Back','safe-attachment-name')).'" />'; 251 } // do_change() 252 253 254 /** 255 * Prefilter uploaded files to remove accents 256 * 257 * array $file 258 * return array 259 */ 260 public function upload_prefilter($file) 261 { 262 $file['name'] = $this->sanitizeFilename($file['name']); 263 return $file; 264 } // upload_prefilter() 265 266 267 /** 268 * Sanitize a file name 269 * 270 * @param string $fn1 File name 271 * @return string 272 */ 273 private function sanitizeFilename($fn1) { 274 $upload_dir = wp_upload_dir(); 275 276 if (!file_exists($upload_dir['basedir'].'/'.$fn1)) { 277 //die('not found: '.$upload_dir['basedir'].'/'.$fn1); 278 } 279 280 $fn3 = strtolower($this->stripAccents($fn1)); 281 $parts = explode('/', $fn3); 282 foreach ($parts as &$part) $part = preg_replace('/[^a-zA-Z0-9\-_\.]+/','_',$part); 283 284 $fn4_old = $fn4 = implode('/', $parts); 285 $i = 1; 286 while (file_exists($upload_dir['basedir'].'/'.$fn4)) { 287 $pi = pathinfo($fn4_old); 288 $fn4 = $pi['dirname'].'/'.$pi['filename'].'-'.$i.'.'.$pi['extension']; 289 $i++; 290 } 291 292 return $fn4; 293 } 294 295 /** 296 * Strip accents from string and replace by the unaccented letter 297 * 298 * @param $stripAccents 299 * @return string 300 */ 301 private function stripAccents($stripAccents){ 302 return utf8_encode(strtr(utf8_decode($stripAccents),utf8_decode('àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ'),'aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY')); 303 } // stripAccents() 21 private static $_instance = null; 22 23 /** 24 * Bootstrap 25 * 26 * @author Tommy Lacroix <tlacroix@nuagelab.com> 27 * @access public 28 */ 29 public static function boot() 30 { 31 if (self::$_instance === null) { 32 self::$_instance = new safe_attachment_names(); 33 self::$_instance->setup(); 34 return true; 35 } 36 return false; 37 } // boot() 38 39 40 /** 41 * Setup plugin 42 * 43 * @author Tommy Lacroix <tlacroix@nuagelab.com> 44 * @access public 45 */ 46 public function setup() 47 { 48 global $current_blog; 49 50 // Add admin menu 51 add_action('admin_menu', array(&$this, 'add_admin_menu')); 52 53 // Set wp_handle_upload_prefilter 54 add_filter('wp_handle_upload_prefilter', array($this, 'upload_prefilter')); 55 56 // Load text domain 57 load_plugin_textdomain('safe-attachment-name', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/'); 58 } // setup() 59 60 61 /** 62 * Add admin menu action; added by setup() 63 * 64 * @author Tommy Lacroix <tlacroix@nuagelab.com> 65 * @access public 66 */ 67 public function add_admin_menu() 68 { 69 // Check if user has proper capabilities 70 if (current_user_can('manage_options')) { 71 //add_management_page(__("Sanitize attachment names",'safe-attachment-name'), __("Sanitize attachment names",'safe-attachment-name'), 'manage_options', basename(__FILE__), array(&$this, 'admin_page')); 72 } 73 } // add_admin_menu() 74 75 76 /** 77 * Admin page action; added by add_admin_menu() 78 * 79 * @author Tommy Lacroix <tlacroix@nuagelab.com> 80 * @access public 81 */ 82 public function admin_page() 83 { 84 // Security check: verify user capabilities 85 if (!current_user_can('manage_options')) { 86 wp_die(__('You do not have sufficient permissions to access this page.')); 87 } 88 89 if (isset($_POST['action'])) { 90 // Sanitize and validate input 91 $action = sanitize_text_field($_POST['action']); 92 $nonce = sanitize_text_field($_POST['safe_attachment_nonce']); 93 94 // Enhanced CSRF protection verification 95 if (wp_verify_nonce($nonce, $action) && check_admin_referer($action, 'safe_attachment_nonce')) { 96 $parts = explode('+', $action); 97 switch ($parts[0]) { 98 case 'sanitize': 99 $accept_terms = isset($_POST['accept-terms']) ? (bool) $_POST['accept-terms'] : false; 100 if (!$accept_terms) { 101 $error_terms = true; 102 } else { 103 return $this->do_change(); 104 } 105 break; 106 } 107 } 108 } 109 110 if (!isset($error_terms)) $error_terms = false; 111 112 echo '<div class="wrap">'; 113 114 echo '<div id="icon-tools" class="icon32"><br></div>'; 115 echo '<h2>'.__('Sanitize Attachment Names','safe-attachment-name').'</h2>'; 116 echo '<form method="post">'; 117 118 // Enhanced CSRF protection with specific action name 119 $action = 'safe_attachment_sanitize_' . wp_create_nonce('safe_attachment_sanitize'); 120 wp_nonce_field($action, 'safe_attachment_nonce'); 121 122 echo '<input type="hidden" name="action" value="' . esc_attr($action) . '" />'; 123 124 echo '<table class="form-table">'; 125 echo '<tbody>'; 126 127 echo '<tr valign="top">'; 128 echo '<td colspan="2"><input type="checkbox" name="accept-terms" id="accept-terms" value="1" /> <label for="accept-terms"'.($error_terms?' style="color:red;font-weight:bold;"':'').'>'.__('I have backed up my database and will assume the responsability of any data loss or corruption.','safe-attachment-name').'</label></td>'; 129 echo '</tr>'; 130 131 echo '</tbody></table>'; 132 133 echo '<p class="submit"><input type="submit" name="submit" id="submit" class="button-primary" value="'.esc_html(__('Sanitize','safe-attachment-name')).'"></p>'; 134 135 echo '</form>'; 136 137 echo '</div>'; 138 } // admin_page() 139 140 141 /** 142 * Change domain. This is where the magic happens. 143 * Called by admin_page() upon form submission. 144 * 145 * @author Tommy Lacroix <tlacroix@nuagelab.com> 146 * @access private 147 */ 148 private function do_change() 149 { 150 global $wpdb; 151 152 @set_time_limit(0); 153 154 echo '<div class="wrap">'; 155 156 echo '<div id="icon-tools" class="icon32"><br></div>'; 157 echo '<h2>Sanitizing attachment names</h2>'; 158 echo '<pre>'; 159 160 $upload_dir = wp_upload_dir(); 161 162 // Use WordPress $wpdb instead of deprecated mysql functions 163 $attachments = $wpdb->get_results($wpdb->prepare( 164 "SELECT * FROM {$wpdb->posts} WHERE post_type = %s", 165 'attachment' 166 )); 167 168 foreach ($attachments as $row) { 169 $post_metas = $wpdb->get_results($wpdb->prepare( 170 "SELECT * FROM {$wpdb->postmeta} WHERE post_id = %d", 171 $row->ID 172 )); 173 $metas = array(); 174 foreach ($post_metas as $row2) { 175 $metas[$row2->meta_key] = $row2; 176 } 177 178 // Check if filename is problematic 179 if (!isset($metas['_wp_attached_file'])) { 180 continue; // Skip if no attached file meta 181 } 182 $fn1 = $metas['_wp_attached_file']->meta_value; 183 $fn2 = htmlentities($fn1, null, 'UTF-8'); 184 185 $fns = array($fn1, utf8_encode($fn1), utf8_decode($fn1)); 186 187 if ($fn1 != $fn2) { 188 $error = false; 189 $fn4 = $this->sanitizeFilename($fn1); 190 191 echo $fn1 . ' -> '.$fn4; 192 193 $fnx = false; 194 foreach ($fns as $x) { 195 if (file_exists($upload_dir['basedir'].'/'.$x)) { 196 $fnx = $x; 197 break; 198 } 199 } 200 if (!$fnx) { 201 _e(': error, cannot find source file.'); 202 echo PHP_EOL; 203 $error = true; 204 } else { 205 _e( ': OK.' ); 206 echo PHP_EOL; 207 } 208 209 // Rename file 210 $renames = array(); 211 $renames[$upload_dir['basedir'].'/'.$fnx] = $upload_dir['basedir'].'/'.$fn4; 212 213 // Update _wp_attachment_metadata 214 if (!isset($metas['_wp_attachment_metadata'])) { 215 continue; // Skip if no metadata 216 } 217 $info = maybe_unserialize($metas['_wp_attachment_metadata']->meta_value); 218 $info['file'] = $fn4; 219 220 if ($info['sizes']) { 221 foreach ( $info['sizes'] as &$size ) { 222 $sf1 = dirname( $fn1 ) . '/' . $size['file']; 223 $sf4 = $this->sanitizeFilename( $sf1 ); 224 if ( $sf1 != $sf4 ) { 225 echo $sf1 . ' -> ' . $sf4; 226 227 $fns = array( $sf1, utf8_encode( $sf1 ), utf8_decode( $sf1 ) ); 228 $sfx = false; 229 foreach ( $fns as $x ) { 230 if ( file_exists( $upload_dir['basedir'] . '/' . $x ) ) { 231 $sfx = $x; 232 break; 233 } 234 } 235 if ( ! $sfx ) { 236 _e( ': error, cannot find source file.' ); 237 echo PHP_EOL; 238 $error = true; 239 } else { 240 _e( ': OK.' ); 241 echo PHP_EOL; 242 } 243 244 // Rename file 245 $renames[ $upload_dir['basedir'] . '/' . $sfx ] = $upload_dir['basedir'] . '/' . $sf4; 246 247 $size['file'] = basename( $sf4 ); 248 } 249 } 250 } 251 252 if (!$error) { 253 // Initialize WordPress filesystem API 254 global $wp_filesystem; 255 if (empty($wp_filesystem)) { 256 require_once(ABSPATH . '/wp-admin/includes/file.php'); 257 WP_Filesystem(); 258 } 259 260 // Rename all files using WordPress filesystem API 261 foreach ( $renames as $f1 => $f2 ) { 262 if ($wp_filesystem->exists($f1)) { 263 $wp_filesystem->move($f1, $f2); 264 } 265 } 266 267 // Update _wp_attachment using WordPress $wpdb 268 $wpdb->update( 269 $wpdb->postmeta, 270 array('meta_value' => $fn4), 271 array('meta_id' => $metas['_wp_attached_file']->meta_id), 272 array('%s'), 273 array('%d') 274 ); 275 276 $wpdb->update( 277 $wpdb->postmeta, 278 array('meta_value' => maybe_serialize($info)), 279 array('meta_id' => $metas['_wp_attachment_metadata']->meta_id), 280 array('%s'), 281 array('%d') 282 ); 283 284 // Update guid 285 $wpdb->update( 286 $wpdb->posts, 287 array('guid' => $upload_dir['baseurl'] . '/' . $fn4), 288 array('ID' => $row->ID), 289 array('%s'), 290 array('%d') 291 ); 292 } 293 } else { 294 //echo $fn1 . ' [ok]'.PHP_EOL; 295 } 296 } 297 298 echo '</pre>'; 299 echo '<hr>'; 300 echo '<form method="post"><input type="submit" value="'.esc_html(__('Back','safe-attachment-name')).'" />'; 301 } // do_change() 302 303 304 /** 305 * Prefilter uploaded files to remove accents 306 * 307 * @param array $file The uploaded file array 308 * @return array Modified file array 309 */ 310 public function upload_prefilter($file) 311 { 312 // Validate file array structure 313 if (!is_array($file) || !isset($file['name'])) { 314 return $file; 315 } 316 317 // Sanitize and process filename 318 $file['name'] = sanitize_file_name($this->sanitizeFilename($file['name'])); 319 return $file; 320 } // upload_prefilter() 321 322 323 /** 324 * Sanitize a file name 325 * 326 * @param string $fn1 File name 327 * @return string 328 */ 329 private function sanitizeFilename($fn1) { 330 $upload_dir = wp_upload_dir(); 331 332 // Initialize WordPress filesystem API if needed 333 global $wp_filesystem; 334 if (empty($wp_filesystem)) { 335 require_once(ABSPATH . '/wp-admin/includes/file.php'); 336 WP_Filesystem(); 337 } 338 339 $fn3 = strtolower($this->stripAccents($fn1)); 340 $parts = explode('/', $fn3); 341 foreach ($parts as &$part) { 342 $part = preg_replace('/[^a-zA-Z0-9\-_\.]+/','_', $part); 343 $part = str_replace('_.', '.', $part); 344 } 345 $fn4_old = $fn4 = implode('/', $parts); 346 $i = 1; 347 348 // Use WordPress filesystem API for file existence check 349 while ($wp_filesystem->exists($upload_dir['basedir'].'/'.$fn4)) { 350 $pi = pathinfo($fn4_old); 351 $fn4 = $pi['dirname'].'/'.$pi['filename'].'-'.$i.'.'.$pi['extension']; 352 $i++; 353 } 354 355 return $fn4; 356 } 357 358 /** 359 * Strip accents from string and replace by the unaccented letter 360 * 361 * @param $stripAccents 362 * @return string 363 */ 364 private function stripAccents($stripAccents){ 365 return utf8_encode(strtr(utf8_decode($stripAccents),utf8_decode('àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ'),'aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY')); 366 } // stripAccents() 304 367 305 368 } // safe_attachment_names class
Note: See TracChangeset
for help on using the changeset viewer.