Index: image.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/image/image.module,v
retrieving revision 1.258.2.5
diff -u -p -r1.258.2.5 image.module
--- image.module	2 Mar 2008 14:18:11 -0000	1.258.2.5
+++ image.module	10 Apr 2008 02:03:48 -0000
@@ -90,11 +90,57 @@ function image_admin_settings() {
     '#type' => 'fieldset',
     '#title' => t('File paths')
   );
-  $form['paths']['image_default_path'] = array(
+
+  $example_tokens = array(
+    '%nodepath'     => 'safe/url/example' ,
+    '%filename'     => 'sample-filename',
+    '%filename_raw' => 'Sample filename',
+    '%label'        => 'thumb',
+    '%extension'    => 'jpg',
+    '%nid'          => 666,
+    '%uid'          => $GLOBALS['user']->uid,
+    '%date'         => date('Y-m-d'),
+    '%year'         => date('Y'),
+    '%month'        => date('m'),
+    '%day'          => date('d'),
+  );
+  $available_vocabs = taxonomy_get_vocabularies('image');
+  $vocablist = '';
+  foreach($available_vocabs as $voc) {
+    $example_tokens['%vocab-'. $voc->vid] = 'a-term';
+    $vocablist .= ' %vocab-'. $voc->vid .' = '. $voc->name .' ';
+   }
+
+  $example_path = variable_get('file_directory_path', 'files') .'/'. strtr(variable_get('image_derivatives_filename_pattern', _image_default_filename_pattern()), $example_tokens );
+
+  $form['paths']['image_derivatives_filename_pattern'] = array(
     '#type' => 'textfield',
-    '#title' => t('Default image path'),
-    '#default_value' => variable_get('image_default_path', 'images'),
-    '#description' => t('Subdirectory in the directory "%dir" where pictures will be stored. Do not include trailing slash.', array('%dir' => variable_get('file_directory_path', 'files'))),
+    '#title' => t('Filename naming pattern'),
+    '#default_value' => variable_get('image_derivatives_filename_pattern', _image_default_filename_pattern()),
+    '#description' => t('
+      Pattern to use when saving or creating derivatives (like thumbnails) for an image.
+      Available tokens are: %tokens.<br/><pre>%example_path</pre>
+      <b>%filename</b> is a sanitized version of the upload name, with special characters converted to "-" and lowercased.
+      If you want to keep the messy filename, use <b>%filename_raw</b>.<br />
+      <b>%vocab-n</b> will be replaced with the a term from the named vocabulary: %vocablist. <br />
+      Missing elements will be collapsed cleanly if possible.
+      (<b>images/%vocab-2/%filename.%label.%extension</b> will become <b>images/%filename.%extension</b> if no vocab-2 term or label is present).
+      Files are not renamed or moved after upload, even if these parameters are changed.
+      ',
+      array(
+        '%tokens' => join(', ', array_keys($example_tokens)),
+        '%example_path' => $example_path,
+        '%vocablist' => $vocablist,
+      )
+    ),
+  );
+
+  // rename/move originals
+  $form['image_move_originals'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => variable_get('image_move_originals', FALSE),
+    '#title' => t('Force renaming of original'),
+    '#description' => t('Normally previously-uploaded images remain where they were put, but if you are using the filename pattern to organise your directories, we want to move them also.Set this option, and the next time images are rebuilt, the original file will be renamed also.'),
   );
 
   $form['image_max_upload_size'] = array(
@@ -174,6 +220,15 @@ function image_admin_settings() {
 }
 
 /**
+ * Return the default filename pattern, incorporating the old 'images' directory
+ * path if appropriate.
+ */
+function _image_default_filename_pattern() {
+  // image_default_path is no longer used, but referenced here for backwards compatability
+  return variable_get('image_default_path', 'images') .'/%filename.%label.%extension';
+}
+
+/**
  * Check that the sizes provided have the required amount of information.
  */
 function image_settings_sizes_validate(&$form) {
@@ -357,7 +412,7 @@ function image_prepare(&$node, $field_na
     }
 
     // Save the file to the temp directory.
-    $file = file_save_upload($field_name, _image_filename($file->filename, IMAGE_ORIGINAL, TRUE));
+    $file = file_save_upload($field_name, _image_filename($file->filename, IMAGE_ORIGINAL, TRUE, $node));
     if (!$file) {
       return;
     }
@@ -744,6 +799,23 @@ function image_update(&$node) {
         db_query("DELETE FROM {image} WHERE fid = %d", $file->fid);
       }
 
+      // rename/move originals to match updated filename patterns ////
+      if (variable_get('image_move_originals', FALSE)) {
+        $new_filename = _image_filename($original_file->filepath, IMAGE_ORIGINAL, FALSE, $node);
+        if ($original_file->filepath != $new_filename && file_exists($original_file->filepath)) {
+          drupal_set_message("Moving original image from $original_file->filepath to $new_filename ");
+          if (file_move($original_file->filepath, $new_filename)) {
+            $node->images[IMAGE_ORIGINAL] = $new_filename;
+            // Need to force this new path to be updated. It'll get back in later'
+            db_query("DELETE FROM {files} WHERE nid = %d AND fid = %d", $node->nid, $original_file->fid);
+            db_query("DELETE FROM {file_revisions} WHERE vid = %d AND fid = %d", $node->vid, $original_file->fid);
+          }
+          else {
+            drupal_set_message("Failed to move original image from ". $original_file->filepath ." to $new_filename ", 'error');
+          }
+        }
+      }
+
       _image_build_derivatives($node, FALSE);
 
       // Display a message to the user if they're be able to modify the node
@@ -956,7 +1028,7 @@ function _image_build_derivatives(&$node
   // Resize for the necessary sizes.
   $image_info = image_get_info($original_path);
   foreach ($needed_sizes as $key => $size) {
-    $destination = _image_filename($original_path, $key, $temp);
+    $destination = _image_filename($original_path, $key, $temp, $node);
 
     $status = FALSE;
     switch ($size['operation']) {
@@ -990,30 +1062,104 @@ function _image_build_derivatives(&$node
 
 /**
  * Creates an image filename.
+ *
+ * Uses a token template string to invent new filename and folder structure for each derivative file.
  */
-function _image_filename($filename, $label = IMAGE_ORIGINAL, $temp = FALSE) {
-  $path = variable_get('image_default_path', 'images') .'/';
-  if ($temp) {
-    $path .= 'temp/';
+function _image_filename($filename, $label = IMAGE_ORIGINAL, $temp = FALSE, $node = NULL) {
+  $filename = basename($filename);
+  $pos = strrpos($filename, '.');
+
+  // Build filename according to token pattern.
+  // When previewing a new (temp) node, we don't know the nid, but it should be available by submit time
+  $tokens = array(
+    '%nodepath'  => $node->path ? $node->path : $node->nid ? 'node-'. $node->nid : '',
+    '%filename'  => image_cleanstring(substr($filename, 0, strpos($filename, '.'))),
+    '%filename_raw'  => substr($filename, 0, strpos($filename, '.')),
+    '%label'     => $label,
+    '%extension' => substr($filename, $pos+1),
+    '%nid'       => $node->nid,
+    '%uid'       => $node->uid,
+    '%date'      => date('Y-m-d'),
+    '%year'      => date('Y'),
+    '%month'     => date('m'),
+    '%day'       => date('d'),
+  );
+
+  // Initialize null vocab placeholders to avoid tokens coming through
+  $available_vocabs = taxonomy_get_vocabularies('image');
+  foreach ($available_vocabs as $voc) { $tokens['%vocab-'. $voc->vid] = ''; }
+
+  if (empty($node->taxonomy)) {
+    // When called from image_update or elsewhere, the full node_load may not have happened, so terms are unavailable
+    // Fetch them.
+   $node->taxonomy = taxonomy_node_get_terms($node->nid);
+  }
+  if (!empty($node->taxonomy)) {
+    // Sort terms into vocabulary bags and choose one
+    // TODO Weighting? Which to choose?
+    if ($tags = $node->taxonomy['tags']) {
+      // In freetagging submission, the taxonomy may still be plaintext. Parse it if I can.
+      foreach ($tags as $vid => $tagstring) {
+        $tag_array = explode(',', $tagstring);
+        $top_tag = array_shift($tag_array);
+        $tokens['%vocab-'. $vid] = image_cleanstring($top_tag);
+      }
+    }
+    else {
+      foreach ($node->taxonomy as $term) {
+        $tokens['%vocab-'. $term->vid] = image_cleanstring($term->name);
+      }
+    }
   }
 
-  $filename = basename($filename);
+  // Even original images may be renamed, but they won't include the derivative label
+  if ($label == IMAGE_ORIGINAL) {
+    $tokens['%label'] = '';
+  }
+
+  $pattern = variable_get('image_derivatives_filename_pattern', _image_default_filename_pattern());
 
-  // Insert the resized name in non-original images
-  if ($label && ($label != IMAGE_ORIGINAL)) {
-    $pos = strrpos($filename, '.');
+  // TODO Remove this block? Looks like it hasn't actually worked for years.'
     if ($pos === false) {
       // The file had no extension - which happens in really old image.module
-      // versions, so figure out the extension.
+    // versions, so figure out the extension, and construct the filename again.
       $image_info = image_get_info(file_create_path($path . $filename));
-      $filename = $filename .'.'. $label .'.'. $image_info['extension'];
+    $tokens['%filename'] = $filename;
+    $tokens['%extension'] = $image_info['extension'];
+    $filepath = strtr($pattern, $tokens);
     }
-    else {
-      $filename = substr($filename, 0, $pos) .'.'. $label . substr($filename, $pos);
+
+  $filepath = strtr($pattern, $tokens);
+
+  // Sanitize possible typos and collapse dividers around tokens that are not there. More than one slash,underscore,dash or dot turn into just one whatever
+  // TODO test edge cases
+  $filepath = preg_replace('|([/_\-\.])[/_\-\.]|', '$1', $filepath);
+
+  if ($temp) {
+    // TODO Is this right? Where should temp files fo now?
+    $filepath = 'temp/'. $filepath;
     }
+
+  $fullpath = file_directory_path() .'/'. $filepath;
+
+  // When using advanced filename patterns, we may need to ensure a directory to put this image into exists
+  $target_dir = dirname($fullpath);
+  if (! is_dir($target_dir)) {
+    image_mkdirs($target_dir);
   }
 
-  return file_create_path($path . $filename);
+  return $fullpath;
+}
+
+/**
+ * Remove dodgy characters from filenames and parts.
+ * Force to lower and seperate with "-"
+ */
+function image_cleanstring($string) {
+  // Trim any leading or trailing separators
+  $string = preg_replace('/[^a-z0-9]+/', '-', strtolower(trim($string)));
+  $string = preg_replace("/^\-+|\-+$/", "", $string);
+  return $string;
 }
 
 /**
@@ -1090,7 +1236,7 @@ function _image_is_required_size($size) 
  */
 function _image_insert(&$node, $size, $image_path) {
   $original_path = $node->images[IMAGE_ORIGINAL];
-  if (file_move($image_path, _image_filename($original_path, $size))) {
+  if (file_move($image_path, _image_filename($original_path, $size, FALSE, $node))) {
     // Update the node to reflect the actual filename, it may have been changed
     // if a file of the same name already existed.
     $node->images[$size] = $image_path;
@@ -1177,3 +1323,11 @@ function image_create_node_from($filepat
   return $node;
 }
 
+/**
+ * Recursive mkdir. Utility func
+ */
+function image_mkdirs($strPath, $mode = 0777) {
+  if(! $strPath){ trigger_error("Null call to image_mkdirs()", E_USER_WARNING); }
+  return is_dir($strPath) or ( image_mkdirs(dirname($strPath), $mode) and mkdir($strPath, $mode) );
+}
+
