Index: trunk/admin.php
===================================================================
--- trunk/admin.php	(revision 1118)
+++ trunk/admin.php	(revision 1119)
@@ -93,4 +93,5 @@
     'U_RATING'=> $link_start.'rating',
     'U_CADDIE'=> $link_start.'element_set&amp;cat=caddie',
+    'U_TAGS'=> $link_start.'tags',
     'U_THUMBNAILS'=> $link_start.'thumbnail',
     'U_USERS'=> $link_start.'user_list',
Index: trunk/admin/element_set_global.php
===================================================================
--- trunk/admin/element_set_global.php	(revision 1118)
+++ trunk/admin/element_set_global.php	(revision 1119)
@@ -45,38 +45,4 @@
 
 // +-----------------------------------------------------------------------+
-// |                               functions                               |
-// +-----------------------------------------------------------------------+
-
-/**
- * returns the list of uniq keywords among given elements
- *
- * @param array element_ids
- */
-function get_elements_keywords($element_ids)
-{
-  if (0 == count($element_ids))
-  {
-    return array();
-  }
-
-  $keywords = array();
-
-  $query = '
-SELECT keywords
-  FROM '.IMAGES_TABLE.'
-  WHERE id IN ('.implode(',', $element_ids).')
-;';
-  $result = pwg_query($query);
-  while ($row = mysql_fetch_array($result))
-  {
-    if (isset($row['keywords']) and !empty($row['keywords']))
-    {
-      $keywords = array_merge($keywords, explode(',', $row['keywords']));
-    }
-  }
-  return array_unique($keywords);
-}
-
-// +-----------------------------------------------------------------------+
 // |                       global mode form submission                     |
 // +-----------------------------------------------------------------------+
@@ -112,4 +78,20 @@
   }
 
+  if (isset($_POST['add_tags']) and count($collection) > 0)
+  {
+    add_tags($_POST['add_tags'], $collection);
+  }
+
+  if (isset($_POST['del_tags']) and count($collection) > 0)
+  {
+    $query = '
+DELETE
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id IN ('.implode(',', $collection).')
+    AND tag_id IN ('.implode(',', $_POST['del_tags']).')
+;';
+    pwg_query($query);
+  }
+  
   if ($_POST['associate'] != 0 and count($collection) > 0)
   {
@@ -193,9 +175,4 @@
   $dbfields = array('primary' => array('id'), 'update' => array());
 
-  if (!empty($_POST['add_keywords']) or $_POST['remove_keyword'] != '0')
-  {
-    array_push($dbfields['update'], 'keywords');
-  }
-
   $formfields = array('author', 'name', 'date_creation');
   foreach ($formfields as $formfield)
@@ -211,5 +188,5 @@
   {
     $query = '
-SELECT id, keywords
+SELECT id
   FROM '.IMAGES_TABLE.'
   WHERE id IN ('.implode(',', $collection).')
@@ -221,42 +198,4 @@
       $data = array();
       $data['id'] = $row['id'];
-
-      if (!empty($_POST['add_keywords']))
-      {
-        $data['keywords'] =
-          implode(
-            ',',
-            array_unique(
-              array_merge(
-                get_keywords(empty($row['keywords']) ? '' : $row['keywords']),
-                get_keywords($_POST['add_keywords'])
-                )
-              )
-            );
-      }
-
-      if ($_POST['remove_keyword'] != '0')
-      {
-        if (!isset($data['keywords']))
-        {
-          $data['keywords'] = empty($row['keywords']) ? '' : $row['keywords'];
-        }
-
-        $data['keywords'] =
-          implode(
-            ',',
-            array_unique(
-              array_diff(
-                get_keywords($data['keywords']),
-                array($_POST['remove_keyword'])
-                )
-              )
-            );
-
-        if ($data['keywords'] == '')
-        {
-          unset($data['keywords']);
-        }
-      }
 
       if ('set' == $_POST['author_action'])
@@ -385,22 +324,33 @@
 }
 
-$blockname = 'remove_keyword_option';
-
-$template->assign_block_vars(
-  $blockname,
-  array('VALUE'=> 0,
-        'OPTION' => '------------'
-    ));
-
-$keywords = get_elements_keywords($page['cat_elements_id']);
-
-foreach ($keywords as $keyword)
-{
-  $template->assign_block_vars(
-  $blockname,
-  array('VALUE'=> $keyword,
-        'OPTION' => $keyword
-    ));
-}
+// add tags
+$template->assign_vars(
+  array(
+    'ADD_TAG_SELECTION' => get_html_tag_selection(get_all_tags(), 'add_tags'),
+    )
+  );
+
+// remove tags
+$query = '
+SELECT tag_id, name, url_name, count(*) counter
+  FROM '.IMAGE_TAG_TABLE.'
+    INNER JOIN '.TAGS_TABLE.' ON tag_id = id
+  WHERE image_id IN ('.implode(',', $page['cat_elements_id']).')
+  GROUP BY tag_id
+  ORDER BY name ASC
+;';
+$result = pwg_query($query);
+
+$tags = array();
+while($row = mysql_fetch_array($result))
+{
+  array_push($tags, $row);
+}
+
+$template->assign_vars(
+  array(
+    'DEL_TAG_SELECTION' => get_html_tag_selection($tags, 'del_tags'),
+    )
+  );
 
 // creation date
Index: trunk/admin/element_set_unit.php
===================================================================
--- trunk/admin/element_set_unit.php	(revision 1118)
+++ trunk/admin/element_set_unit.php	(revision 1119)
@@ -104,16 +104,12 @@
       $data{'date_creation'} = $row['date_creation'];
     }
-
-    $keywords = get_keywords($_POST['keywords-'.$row['id']]);
-    if (count($keywords) > 0)
-    {
-      $data{'keywords'} = implode(',', $keywords);
-    }
-    else
-    {
-      $data{'keywords'} = '';
-    }
-
+    
     array_push($datas, $data);
+
+    // tags management
+    if (isset($_POST[ 'tags-'.$row['id'] ]))
+    {
+      set_tags($_POST[ 'tags-'.$row['id'] ], $row['id']);
+    }
   }
   
@@ -122,5 +118,5 @@
     array(
       'primary' => array('id'),
-      'update' => array('name','author','comment','date_creation','keywords')
+      'update' => array('name','author','comment','date_creation')
       ),
     $datas
@@ -193,9 +189,11 @@
   $template->assign_vars(array('NAV_BAR' => $nav_bar));
 
+  // tags
+  $all_tags = get_all_tags();
  
   $element_ids = array();
 
   $query = '
-SELECT id,path,tn_ext,name,date_creation,comment,keywords,author,file
+SELECT id,path,tn_ext,name,date_creation,comment,author,file
   FROM '.IMAGES_TABLE.'
   WHERE id IN ('.implode(',', $page['cat_elements_id']).')
@@ -211,4 +209,11 @@
     
     $src = get_thumbnail_src($row['path'], @$row['tn_ext']);
+
+    $query = '
+SELECT tag_id
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id = '.$row['id'].'
+;';
+    $selected_tags = array_from_query($query, 'tag_id');
     
     // creation date
@@ -238,5 +243,10 @@
         'DESCRIPTION' => @$row['comment'],
         'DATE_CREATION_YEAR' => $year,
-        'KEYWORDS' => @$row['keywords']
+        
+        'TAG_SELECTION' => get_html_tag_selection(
+          $all_tags,
+          'tags-'.$row['id'],
+          $selected_tags
+          ),
         )
       );
Index: trunk/admin/include/functions.php
===================================================================
--- trunk/admin/include/functions.php	(revision 1118)
+++ trunk/admin/include/functions.php	(revision 1119)
@@ -270,4 +270,12 @@
   pwg_query($query);
 
+  // destruction of the links between images and tags
+  $query = '
+DELETE FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id IN (
+'.wordwrap(implode(', ', $ids), 80, "\n").')
+;';
+  pwg_query($query);
+
   // destruction of the favorites associated with the picture
   $query = '
@@ -575,23 +583,4 @@
     return '';
   }
-}
-
-/**
- * returns an array with relevant keywords found in the given string.
- *
- * Keywords must be separated by comma or space characters.
- *
- * @param string keywords_string
- * @return array
- */
-function get_keywords($keywords_string)
-{
-  return
-    array_unique(
-      preg_split(
-        '/[\s,]+/',
-        $keywords_string
-        )
-      );
 }
 
@@ -2095,4 +2084,171 @@
 
 /**
+ * Set tags to an image. Warning: given tags are all tags associated to the
+ * image, not additionnal tags.
+ *
+ * @param array tag ids
+ * @param int image id
+ * @return void
+ */
+function set_tags($tags, $image_id)
+{
+  $query = '
+DELETE
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id = '.$image_id.'
+;';
+  pwg_query($query);
+
+  if (count($tags) > 0)
+  {
+    $inserts = array();
+    foreach ($tags as $tag_id)
+    {
+      array_push(
+        $inserts,
+        array(
+          'tag_id' => $tag_id,
+          'image_id' => $image_id
+          )
+        );
+    }
+    mass_inserts(
+      IMAGE_TAG_TABLE,
+      array_keys($inserts[0]),
+      $inserts
+      );
+  }
+}
+
+/**
+ * Add new tags to a set of images.
+ *
+ * @param array tag ids
+ * @param array image ids
+ * @return void
+ */
+function add_tags($tags, $images)
+{
+  if (count($tags) == 0 or count($tags) == 0)
+  {
+    return;
+  }
+  
+  // we can't insert twice the same {image_id,tag_id} so we must first
+  // delete lines we'll insert later
+  $query = '
+DELETE
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id IN ('.implode(',', $images).')
+    AND tag_id IN ('.implode(',', $tags).')
+;';
+  pwg_query($query);
+
+  $inserts = array();
+  foreach ($images as $image_id)
+  {
+    foreach ($tags as $tag_id)
+    {
+      array_push(
+        $inserts,
+        array(
+          'image_id' => $image_id,
+          'tag_id' => $tag_id,
+          )
+        );
+    }
+  }
+  mass_inserts(
+    IMAGE_TAG_TABLE,
+    array_keys($inserts[0]),
+    $inserts
+    );
+}
+
+function tag_id_from_tag_name($tag_name)
+{
+  global $page;
+
+  if (isset($page['tag_id_from_tag_name_cache'][$tag_name]))
+  {
+    return $page['tag_id_from_tag_name_cache'][$tag_name];
+  }
+  
+  if (function_exists('mysql_real_escape_string'))
+  {
+    $tag_name = mysql_real_escape_string($tag_name);
+  }
+  else
+  {
+    $tag_name = mysql_escape_string($tag_name);
+  }
+
+  // does the tag already exist?
+  $query = '
+SELECT id
+  FROM '.TAGS_TABLE.'
+  WHERE name = \''.$tag_name.'\'
+;';
+  $existing_tags = array_from_query($query, 'id');
+
+  if (count($existing_tags) == 0)
+  {
+    mass_inserts(
+      TAGS_TABLE,
+      array('name', 'url_name'),
+      array(
+        array(
+          'name' => $tag_name,
+          'url_name' => str2url($tag_name),
+          )
+        )
+      );
+
+    $page['tag_id_from_tag_name_cache'][$tag_name] = mysql_insert_id();
+  }
+  else
+  {
+    $page['tag_id_from_tag_name_cache'][$tag_name] = $existing_tags[0];
+  }
+
+  return $page['tag_id_from_tag_name_cache'][$tag_name];
+}
+
+function set_tags_of($tags_of)
+{
+  if (count($tags_of) > 0)
+  {
+    $query = '
+DELETE
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id IN ('.implode(',', array_keys($tags_of)).')
+;';
+    pwg_query($query);
+
+    $inserts = array();
+    
+    foreach ($tags_of as $image_id => $tag_ids)
+    {
+      foreach ($tag_ids as $tag_id)
+      {
+        array_push(
+          $inserts,
+          array(
+            'image_id' => $image_id,
+            'tag_id' => $tag_id,
+            )
+          );
+      }
+    }
+
+    mass_inserts(
+      IMAGE_TAG_TABLE,
+      array_keys($inserts[0]),
+      $inserts
+      );
+  }
+}
+
+/**
  * Do maintenance on all PWG tables
  *
@@ -2144,5 +2300,3 @@
 
 }
-
-
 ?>
Index: trunk/admin/include/functions_metadata.php
===================================================================
--- trunk/admin/include/functions_metadata.php	(revision 1118)
+++ trunk/admin/include/functions_metadata.php	(revision 1119)
@@ -51,8 +51,5 @@
   if (isset($iptc['keywords']))
   {
-    // keywords separator is the comma, nothing else. Allowed characters in
-    // keywords : [A-Za-z0-9], "-" and "_". All other characters will be
-    // considered as separators
-    $iptc['keywords'] = preg_replace('/[^\w-]+/', ',', $iptc['keywords']);
+    // keywords separator is the comma
     $iptc['keywords'] = preg_replace('/^,+|,+$/', '', $iptc['keywords']);
   }
@@ -91,4 +88,5 @@
 
   $datas = array();
+  $tags_of = array();
 
   foreach ($files as $id => $file)
@@ -124,5 +122,23 @@
         foreach (array_keys($iptc) as $key)
         {
-          $data[$key] = addslashes($iptc[$key]);
+          if ($key == 'keywords' or $key == 'tags')
+          {
+            if (!isset($tags_of[$id]))
+            {
+              $tags_of[$id] = array();
+            }
+            
+            foreach (explode(',', $iptc[$key]) as $tag_name)
+            {
+              array_push(
+                $tags_of[$id],
+                tag_id_from_tag_name($tag_name)
+                );
+            }
+          }
+          else
+          {
+            $data[$key] = addslashes($iptc[$key]);
+          }
         }
       }
@@ -158,5 +174,8 @@
         array_merge(
           $update_fields,
-          array_keys($conf['use_iptc_mapping'])
+          array_diff(
+            array_keys($conf['use_iptc_mapping']),
+            array('tags', 'keywords')
+            )
           );
     }
@@ -167,6 +186,9 @@
         'update'  => array_unique($update_fields)
         );
+    echo '<pre>'; print_r($datas); echo '</pre>';
     mass_updates(IMAGES_TABLE, $fields, $datas);
   }
+
+  set_tags_of(tags_of);
 }
 
Index: trunk/admin/picture_modify.php
===================================================================
--- trunk/admin/picture_modify.php	(revision 1118)
+++ trunk/admin/picture_modify.php	(revision 1119)
@@ -101,14 +101,4 @@
   }
 
-  $keywords = get_keywords($_POST['keywords']);
-  if (count($keywords) > 0)
-  {
-    $data{'keywords'} = implode(',', $keywords);
-  }
-  else
-  {
-    $data{'keywords'} = '';
-  }
-
   mass_updates(
     IMAGES_TABLE,
@@ -120,4 +110,9 @@
     );
 
+  set_tags(
+    isset($_POST['tags']) ? $_POST['tags'] : array(),
+    $_GET['image_id']
+    );
+
   array_push($page['infos'], l10n('Picture informations updated'));
 }
@@ -216,4 +211,12 @@
 $image_file = $row['file'];
 
+// tags
+$query = '
+SELECT tag_id
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE image_id = '.$_GET['image_id'].'
+;';
+$selected_tags = array_from_query($query, 'tag_id');
+
 // Navigation path
 
@@ -258,7 +261,9 @@
     'CREATION_DATE' => $date,
 
-    'KEYWORDS' =>
-      isset($_POST['keywords']) ?
-        stripslashes($_POST['keywords']) : @$row['keywords'],
+    'TAG_SELECTION' => get_html_tag_selection(
+      get_all_tags(),
+      'tags',
+      $selected_tags
+      ),
 
     'DESCRIPTION' =>
Index: trunk/admin/site_update.php
===================================================================
--- trunk/admin/site_update.php	(revision 1118)
+++ trunk/admin/site_update.php	(revision 1119)
@@ -716,4 +716,5 @@
   $start = get_moment();
   $datas = array();
+  $tags_of = array();
   foreach ( $files as $id=>$file )
   {
@@ -724,4 +725,23 @@
       $data['id']=$id;
       array_push($datas, $data);
+
+      foreach (array('keywords', 'tags') as $key)
+      {
+        if (isset($data[$key]))
+        {
+          if (!isset($tags_of[$id]))
+          {
+            $tags_of[$id] = array();
+          }
+        
+          foreach (explode(',', $data[$key]) as $tag_name)
+          {
+            array_push(
+              $tags_of[$id],
+              tag_id_from_tag_name($tag_name)
+              );
+          }
+        }
+      }
     }
     else
@@ -730,15 +750,28 @@
     }
   }
-  $update_fields = $site_reader->get_update_attributes();
-  $update_fields = array_merge($update_fields, array('date_metadata_update'));
-  $fields =
-      array(
-        'primary' => array('id'),
-        'update'  => array_unique($update_fields)
-        );
-  //print_r($datas);
-  if (!$simulate and count($datas)>0 )
-  {
-    mass_updates(IMAGES_TABLE, $fields, $datas);
+
+  if (!$simulate)
+  {
+    if (count($datas) > 0)
+    {
+      mass_updates(
+        IMAGES_TABLE,
+        // fields
+        array(
+          'primary' => array('id'),
+          'update'  => array_unique(
+            array_merge(
+              array_diff(
+                $site_reader->get_update_attributes(),
+                // keywords and tags fields are managed separately
+                array('keywords', 'tags')
+                ),
+              array('date_metadata_update'))
+            )
+          ),
+        $datas
+        );
+    }
+    set_tags_of($tags_of);
   }
 
Index: trunk/admin/tags.php
===================================================================
--- trunk/admin/tags.php	(revision 1119)
+++ trunk/admin/tags.php	(revision 1119)
@@ -0,0 +1,265 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | PhpWebGallery - a PHP based picture gallery                           |
+// | Copyright (C) 2002-2003 Pierrick LE GALL - pierrick@phpwebgallery.net |
+// | Copyright (C) 2003-2005 PhpWebGallery Team - http://phpwebgallery.net |
+// +-----------------------------------------------------------------------+
+// | branch        : BSF (Best So Far)
+// | file          : $RCSfile$
+// | last update   : $Date: 2006-03-09 23:46:28 +0100 (jeu, 09 mar 2006) $
+// | last modifier : $Author: rub $
+// | revision      : $Revision: 1072 $
+// +-----------------------------------------------------------------------+
+// | This program is free software; you can redistribute it and/or modify  |
+// | it under the terms of the GNU General Public License as published by  |
+// | the Free Software Foundation                                          |
+// |                                                                       |
+// | This program is distributed in the hope that it will be useful, but   |
+// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
+// | General Public License for more details.                              |
+// |                                                                       |
+// | You should have received a copy of the GNU General Public License     |
+// | along with this program; if not, write to the Free Software           |
+// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
+// | USA.                                                                  |
+// +-----------------------------------------------------------------------+
+
+if( !defined("PHPWG_ROOT_PATH") )
+{
+  die ("Hacking attempt!");
+}
+
+include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
+check_status(ACCESS_ADMINISTRATOR);
+
+// +-----------------------------------------------------------------------+
+// |                                edit tags                              |
+// +-----------------------------------------------------------------------+
+
+if (isset($_POST['submit']))
+{
+  $query = '
+SELECT name
+  FROM '.TAGS_TABLE.'
+;';
+  $existing_names = array_from_query($query, 'name');
+  
+
+  $current_name_of = array();
+  $query = '
+SELECT id, name
+  FROM '.TAGS_TABLE.'
+  WHERE id IN ('.$_POST['edit_list'].')
+;';
+  $result = pwg_query($query);
+  while ($row = mysql_fetch_array($result))
+  {
+    $current_name_of[ $row['id'] ] = $row['name'];
+  }
+  
+  $updates = array();
+  // we must not rename tag with an already existing name
+  foreach (explode(',', $_POST['edit_list']) as $tag_id)
+  {
+    if (function_exists('mysql_real_escape_string'))
+    {
+      $tag_name = mysql_real_escape_string($_POST['tag_name-'.$tag_id]);
+    }
+    else
+    {
+      $tag_name = mysql_escape_string($_POST['tag_name-'.$tag_id]);
+    }
+
+    if ($tag_name != $current_name_of[$tag_id])
+    {
+      if (in_array($tag_name, $existing_names))
+      {
+        array_push(
+          $page['errors'],
+          sprintf(
+            l10n('Tag "%s" already exist'),
+            $tag_name
+            )
+          );
+      }
+      else if (!empty($tag_name))
+      {
+        array_push(
+          $updates,
+          array(
+            'id' => $tag_id,
+            'name' => $tag_name,
+            'url_name' => str2url($tag_name),
+            )
+          );
+      }
+    }
+  }
+  mass_updates(
+    TAGS_TABLE,
+    array(
+      'primary' => array('id'),
+      'update' => array('name', 'url_name'),
+      ),
+    $updates
+    );
+}
+
+// +-----------------------------------------------------------------------+
+// |                               delete tags                             |
+// +-----------------------------------------------------------------------+
+
+if (isset($_POST['delete']) and isset($_POST['tags']))
+{
+  $query = '
+SELECT name
+  FROM '.TAGS_TABLE.'
+  WHERE id IN ('.implode(',', $_POST['tags']).')
+;';
+  $tag_names = array_from_query($query, 'name');
+  
+  $query = '
+DELETE
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE tag_id IN ('.implode(',', $_POST['tags']).')
+;';
+  pwg_query($query);
+  
+  $query = '
+DELETE
+  FROM '.TAGS_TABLE.'
+  WHERE id IN ('.implode(',', $_POST['tags']).')
+;';
+  pwg_query($query);
+  
+  array_push(
+    $page['infos'],
+    sprintf(
+      l10n('The %d following tags were deleted : %s'),
+      count($tag_names),
+      implode(', ', $tag_names)
+      )
+    );
+}
+
+// +-----------------------------------------------------------------------+
+// |                               add a tag                               |
+// +-----------------------------------------------------------------------+
+
+if (isset($_POST['add_tag']) and !empty($_POST['add_tag']))
+{
+  if (function_exists('mysql_real_escape_string'))
+  {
+    $tag_name = mysql_real_escape_string($_POST['add_tag']);
+  }
+  else
+  {
+    $tag_name = mysql_escape_string($_POST['add_tag']);
+  }
+
+  // does the tag already exist?
+  $query = '
+SELECT id
+  FROM '.TAGS_TABLE.'
+  WHERE name = \''.$tag_name.'\'
+;';
+  $existing_tags = array_from_query($query, 'id');
+
+  if (count($existing_tags) == 0)
+  {
+    mass_inserts(
+      TAGS_TABLE,
+      array('name', 'url_name'),
+      array(
+        array(
+          'name' => $tag_name,
+          'url_name' => str2url($tag_name),
+          )
+        )
+      );
+    
+    array_push(
+      $page['infos'],
+      sprintf(
+        l10n('Tag "%s" was added'),
+        $tag_name
+        )
+      );
+  }
+  else
+  {
+    array_push(
+      $page['errors'],
+      sprintf(
+        l10n('Tag "%s" already exist'),
+        $tag_name
+        )
+      );
+  }
+}
+
+// +-----------------------------------------------------------------------+
+// |                             template init                             |
+// +-----------------------------------------------------------------------+
+
+$template->set_filenames(array('tags' => 'admin/tags.tpl'));
+
+$template->assign_vars(
+  array(
+    'F_ACTION' => PHPWG_ROOT_PATH.'admin.php?page=tags'
+    )
+  );
+
+// +-----------------------------------------------------------------------+
+// |                             form creation                             |
+// +-----------------------------------------------------------------------+
+
+$template->assign_vars(
+  array(
+    'TAG_SELECTION' => get_html_tag_selection(
+      get_all_tags(),
+      'tags'
+      ),
+    )
+  );
+
+if (isset($_POST['edit']) and isset($_POST['tags']))
+{
+  $template->assign_block_vars(
+    'edit_tags',
+    array(
+      'LIST' => implode(',', $_POST['tags']),
+      )
+    );
+
+  $query = '
+SELECT id, name
+  FROM '.TAGS_TABLE.'
+  WHERE id IN ('.implode(',', $_POST['tags']).')
+;';
+  $result = pwg_query($query);
+  while ($row = mysql_fetch_array($result))
+  {
+    $name_of[ $row['id'] ] = $row['name'];
+  }
+
+  foreach ($_POST['tags'] as $tag_id)
+  {
+    $template->assign_block_vars(
+      'edit_tags.tag',
+      array(
+        'ID' => $tag_id,
+        'NAME' => $name_of[$tag_id],
+        )
+      );
+  }
+}
+
+// +-----------------------------------------------------------------------+
+// |                           sending html code                           |
+// +-----------------------------------------------------------------------+
+
+$template->assign_var_from_handle('ADMIN_CONTENT', 'tags');
+
+?>
Index: trunk/include/category_default.inc.php
===================================================================
--- trunk/include/category_default.inc.php	(revision 1118)
+++ trunk/include/category_default.inc.php	(revision 1119)
@@ -80,5 +80,5 @@
     $thumbnail_title .= ' : '.$row['filesize'].' KB';
   }
-
+  
   // link on picture.php page
   $url = duplicate_picture_url(
Index: trunk/include/common.inc.php
===================================================================
--- trunk/include/common.inc.php	(revision 1118)
+++ trunk/include/common.inc.php	(revision 1119)
@@ -149,4 +149,5 @@
   {
     ob_start();// buffer output so that cookies work
+
     echo
       '<p>'
Index: trunk/include/config_default.inc.php
===================================================================
--- trunk/include/config_default.inc.php	(revision 1118)
+++ trunk/include/config_default.inc.php	(revision 1119)
@@ -440,4 +440,16 @@
 
 // +-----------------------------------------------------------------------+
+// |                                 tags                                  |
+// +-----------------------------------------------------------------------+
+
+// full_tag_cloud_items_number: number of tags to show in the full tag
+// cloud. Only the most represented tags will be shown
+$conf['full_tag_cloud_items_number'] = 200;
+
+// tags_levels: number of levels to use for display. Each level is bind to a
+// CSS class tagLevelX.
+$conf['tags_levels'] = 10;
+
+// +-----------------------------------------------------------------------+
 // | Notification by mail                                                  |
 // +-----------------------------------------------------------------------+
@@ -456,4 +468,3 @@
 // Max mails sended on one pass
 $conf['nbm_max_mails_send'] = 35;
-
 ?>
Index: trunk/include/constants.php
===================================================================
--- trunk/include/constants.php	(revision 1118)
+++ trunk/include/constants.php	(revision 1119)
@@ -71,3 +71,5 @@
 define('USER_MAIL_NOTIFICATION_TABLE', $prefixeTable.'user_mail_notification');
 define('CATEGORIES_LINK_TABLE', $prefixeTable.'categories_link');
+define('TAGS_TABLE', $prefixeTable.'tags');
+define('IMAGE_TAG_TABLE', $prefixeTable.'image_tag');
 ?>
Index: trunk/include/functions.inc.php
===================================================================
--- trunk/include/functions.inc.php	(revision 1118)
+++ trunk/include/functions.inc.php	(revision 1119)
@@ -32,4 +32,5 @@
 include_once( PHPWG_ROOT_PATH .'include/functions_group.inc.php' );
 include_once( PHPWG_ROOT_PATH .'include/functions_html.inc.php' );
+include_once( PHPWG_ROOT_PATH .'include/functions_tag.inc.php' );
 include_once( PHPWG_ROOT_PATH .'include/functions_url.inc.php' );
 
@@ -268,4 +269,33 @@
   return $picture_size;
 }
+
+/**
+ * simplify a string to insert it into an URL
+ *
+ * based on str2url function from Dotclear
+ *
+ * @param string
+ * @return string
+ */
+function str2url($str)
+{
+  $str = strtr(
+    $str,
+    'ÀÁÂÃÄÅàáâãäåÇçÒÓÔÕÖØòóôõöøÈÉÊËèéêëÌÍÎÏìíîïÙÚÛÜùúûü¾ÝÿýÑñ',
+    'AAAAAAaaaaaaCcOOOOOOooooooEEEEeeeeIIIIiiiiUUUUuuuuYYyyNn'
+    );
+
+  $str = str_replace('Æ', 'AE', $str);
+  $str = str_replace('æ', 'ae', $str);
+  $str = str_replace('¼', 'OE', $str);
+  $str = str_replace('½', 'oe', $str);
+
+  $str = preg_replace('/[^a-z0-9_\s\'\:\/\[\]-]/','',strtolower($str));
+  $str = preg_replace('/[\s\'\:\/\[\]-]+/',' ',trim($str));
+  $res = str_replace(' ','_',$str);
+  
+  return $res;
+}
+
 //-------------------------------------------- PhpWebGallery specific functions
 
@@ -830,4 +860,3 @@
   return $available_upgrade_ids;
 }
-
 ?>
Index: trunk/include/functions_html.inc.php
===================================================================
--- trunk/include/functions_html.inc.php	(revision 1118)
+++ trunk/include/functions_html.inc.php	(revision 1119)
@@ -6,5 +6,4 @@
 // +-----------------------------------------------------------------------+
 // | branch        : BSF (Best So Far)
-// | file          : $Id$
 // | last update   : $Date$
 // | last modifier : $Author$
@@ -496,4 +495,54 @@
 
 /**
+ * Returns an HTML list of tags. It can be a multi select field or a list of
+ * checkboxes.
+ *
+ * @param string HTML field name
+ * @param array selected tag ids
+ * @return array
+ */
+function get_html_tag_selection(
+  $tags,
+  $fieldname,
+  $selecteds = array(),
+  $forbidden_categories = null
+  )
+{
+  global $conf;
+  
+  $output = '<ul class="tagSelection">';
+  foreach ($tags as $tag)
+  {
+    $output.=
+      '<li>'
+      .'<label>'
+      .'<input type="checkbox" name="'.$fieldname.'[]"'
+      .' value="'.$tag['tag_id'].'"'
+      ;
+
+    if (in_array($tag['tag_id'], $selecteds))
+    {
+      $output.= ' checked="checked"';
+    }
+    
+    $output.=
+      ' />'
+      .' '.$tag['name']
+      .'</label>'
+      .'</li>'
+      ."\n"
+      ;
+  }
+  $output.= '</ul>';
+
+  return $output;
+}
+
+function name_compare($a, $b)
+{
+  return strcmp($a['name'], $b['name']);
+}
+
+/**
  * exits the current script (either exit or redirect)
  */
Index: trunk/include/functions_search.inc.php
===================================================================
--- trunk/include/functions_search.inc.php	(revision 1118)
+++ trunk/include/functions_search.inc.php	(revision 1119)
@@ -85,5 +85,5 @@
   $clauses = array();
 
-  foreach (array('file','name','comment','keywords','author') as $textfield)
+  foreach (array('file','name','comment','author') as $textfield)
   {
     if (isset($search['fields'][$textfield]))
@@ -110,5 +110,5 @@
   if (isset($search['fields']['allwords']))
   {
-    $fields = array('file', 'name', 'comment', 'keywords', 'author');
+    $fields = array('file', 'name', 'comment', 'author');
     // in the OR mode, request bust be :
     // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
@@ -209,11 +209,67 @@
   $search_clause = $where_separator;
 
-  if (isset($forbidden))
-  {
-    $search_clause.= "\n    AND ".$forbidden;
-  }
-
   return $search_clause;
 }
 
+/**
+ * returns the list of items corresponding to the search id
+ *
+ * @param int search id
+ * @return array
+ */
+function get_search_items($search_id)
+{
+  $items = array();
+  
+  $search_clause = get_sql_search_clause($search_id);
+  
+  if (!empty($search_clause))
+  {
+    $query = '
+SELECT DISTINCT(id)
+  FROM '.IMAGES_TABLE.'
+    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
+  WHERE '.$search_clause.'
+;';
+    $items = array_from_query($query, 'id');
+  }
+
+  $search = get_search_array($search_id);
+
+  if (isset($search['fields']['tags']))
+  {
+    $tag_items = get_image_ids_for_tags(
+      $search['fields']['tags']['words'],
+      $search['fields']['tags']['mode']
+      );
+
+    switch ($search['mode'])
+    {
+      case 'AND':
+      {
+        if (empty($search_clause))
+        {
+          $items = $tag_items;
+        }
+        else
+        {
+          $items = array_intersect($items, $tag_items);
+        }
+        break;
+      }
+      case 'OR':
+      {
+        $items = array_unique(
+          array_merge(
+            $items,
+            $tag_items
+            )
+          );
+        break;
+      }
+    }
+  }
+  
+  return $items;
+}
 ?>
Index: trunk/include/functions_tag.inc.php
===================================================================
--- trunk/include/functions_tag.inc.php	(revision 1119)
+++ trunk/include/functions_tag.inc.php	(revision 1119)
@@ -0,0 +1,218 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | PhpWebGallery - a PHP based picture gallery                           |
+// | Copyright (C) 2002-2003 Pierrick LE GALL - pierrick@phpwebgallery.net |
+// | Copyright (C) 2003-2006 PhpWebGallery Team - http://phpwebgallery.net |
+// +-----------------------------------------------------------------------+
+// | branch        : BSF (Best So Far)
+// | file          : $RCSfile$
+// | last update   : $Date: 2006-03-16 23:58:16 +0100 (jeu, 16 mar 2006) $
+// | last modifier : $Author: rub $
+// | revision      : $Revision: 1085 $
+// | revision      : $Revision: 1085 $
+// +-----------------------------------------------------------------------+
+// | This program is free software; you can redistribute it and/or modify  |
+// | it under the terms of the GNU General Public License as published by  |
+// | the Free Software Foundation                                          |
+// |                                                                       |
+// | This program is distributed in the hope that it will be useful, but   |
+// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
+// | General Public License for more details.                              |
+// |                                                                       |
+// | You should have received a copy of the GNU General Public License     |
+// | along with this program; if not, write to the Free Software           |
+// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
+// | USA.                                                                  |
+// +-----------------------------------------------------------------------+
+
+
+/**
+ * Tags available. Each return tag is represented as an array with its id,
+ * its name, its weight (count), its url name. Tags are not sorted.
+ *
+ * The returned list can be a subset of all existing tags due to
+ * permissions, only if a list of forbidden categories is provided
+ *
+ * @param array forbidden categories
+ * @return array
+ */
+function get_available_tags($forbidden_categories = null)
+{
+  // we can find top fatter tags among reachable images
+  $tags_query = '
+SELECT tag_id, name, url_name, count(*) counter
+  FROM '.IMAGE_TAG_TABLE.'
+    INNER JOIN '.TAGS_TABLE.' ON tag_id = id';
+
+  if (!is_null($forbidden_categories))
+  {
+    // first we need all reachable image ids
+    $images_query = '
+SELECT DISTINCT image_id
+  FROM '.IMAGE_CATEGORY_TABLE.'
+  WHERE category_id NOT IN ('.implode(',', $forbidden_categories).')
+;';
+    $image_ids = array_from_query($images_query, 'image_id');
+
+    $tags_query.= '
+  WHERE image_id IN ('.
+      wordwrap(
+        implode(', ', $image_ids),
+        80,
+        "\n"
+        ).')';
+  }
+  
+  $tags_query.= '
+  GROUP BY tag_id
+;';
+
+  $result = pwg_query($tags_query);
+  
+  $tags = array();
+
+  while ($row = mysql_fetch_array($result))
+  {
+    array_push($tags, $row);
+  }
+
+  return $tags;
+}
+
+/**
+ * All tags, even tags associated to no image.
+ *
+ * @return array
+ */
+function get_all_tags()
+{
+  $query = '
+SELECT id AS tag_id, name, url_name
+  FROM '.TAGS_TABLE.'
+  ORDER BY name
+;';
+  $result = pwg_query($query);
+  
+  $tags = array();
+
+  while ($row = mysql_fetch_array($result))
+  {
+    array_push($tags, $row);
+  }
+
+  return $tags;
+}
+
+/**
+ * Giving a set of tags with a counter for each one, calculate the display
+ * level of each tag.
+ *
+ * The level of each tag depends on the average count of tags. This
+ * calcylation method avoid having very different levels for tags having
+ * nearly the same count when set are small.
+ *
+ * @param array tags
+ * @return array
+ */
+function add_level_to_tags($tags)
+{
+  global $conf;
+
+  if (count($tags) == 0)
+  {
+    return $tags;
+  }
+  
+  $total_count = 0;
+
+  foreach ($tags as $tag)
+  {
+    $total_count+= $tag['counter'];
+  }
+
+  // average count of available tags will determine the level of each tag
+  $tag_average_count = $total_count / count($tags);
+
+  // tag levels threshold calculation: a tag with an average rate must have
+  // the middle level.
+  for ($i = 1; $i < $conf['tags_levels']; $i++)
+  {
+    $threshold_of_level[$i] =
+      2 * $i * $tag_average_count / $conf['tags_levels'];
+  }
+
+  // display sorted tags
+  foreach (array_keys($tags) as $k)
+  {
+    $tags[$k]['level'] = 1;
+    
+    // based on threshold, determine current tag level
+    for ($i = $conf['tags_levels'] - 1; $i >= 1; $i--)
+    {
+      if ($tags[$k]['counter'] > $threshold_of_level[$i])
+      {
+        $tags[$k]['level'] = $i + 1;
+        break;
+      }
+    }
+  }
+
+  return $tags;
+}
+
+/**
+ * return the list of image ids corresponding to given tags. AND & OR mode
+ * supported.
+ *
+ * @param array tag ids
+ * @param string mode
+ * @return array
+ */
+function get_image_ids_for_tags($tag_ids, $mode = 'AND')
+{
+  switch ($mode)
+  {
+    case 'AND':
+    {
+      // strategy is to list images associated to each tag
+      $tag_images = array();
+      
+      foreach ($tag_ids as $tag_id)
+      {
+        $query = '
+SELECT image_id
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE tag_id = '.$tag_id.'
+;';
+        $tag_images[$tag_id] = array_from_query($query, 'image_id');
+      }
+
+      // then we calculate the intersection, the images that are associated to
+      // every tags
+      $items = array_shift($tag_images);
+      foreach ($tag_images as $images)
+      {
+        $items = array_intersect($items, $images);
+      }
+
+      return array_unique($items);
+      break;
+    }
+    case 'OR':
+    {
+      $query = '
+SELECT DISTINCT image_id
+  FROM '.IMAGE_TAG_TABLE.'
+  WHERE tag_id IN ('.implode(',', $tag_ids).')
+;';
+      return array_from_query($query, 'image_id');
+      break;
+    }
+    default:
+    {
+      die('get_image_ids_for_tags: unknown mode, only AND & OR are supported');
+    }
+  }
+}
+?>
Index: trunk/include/functions_url.inc.php
===================================================================
--- trunk/include/functions_url.inc.php	(revision 1118)
+++ trunk/include/functions_url.inc.php	(revision 1119)
@@ -259,26 +259,21 @@
   $section_string = '';
 
+  $section_of = array(
+    'category' => 'categories',
+    'tags'     => 'tags',
+    'list'     => 'list',
+    'search'   => 'search',
+    );
+
+  foreach ($section_of as $param => $section)
+  {
+    if (isset($params[$param]))
+    {
+      $params['section'] = $section;
+    }
+  }
+
   if (!isset($params['section']))
   {
-    if (isset($params['category']))
-    {
-      $params['section'] = 'categories';
-    }
-    else if (isset($params['tags']))
-    {
-      $params['section'] = 'tags';
-    }
-    else if (isset($params['list']))
-    {
-      $params['section'] = 'list';
-    }
-    else if (isset($params['search']))
-    {
-      $params['section'] = 'search';
-    }
-  }
-
-  if (!isset($params['section']))
-  {
     $params['section'] = 'categories';
   }
@@ -290,5 +285,5 @@
       if (!isset($params['category']))
       {
-        //$section_string.= '/categories';
+        $section_string.= '/categories';
       }
       else
@@ -310,5 +305,10 @@
       foreach ($params['tags'] as $tag)
       {
-        $section_string.= '/'.$tag;
+        $section_string.= '/'.$tag['id'];
+        
+        if (isset($tag['url_name']))
+        {
+          $section_string.= '-'.$tag['url_name'];
+        }
       }
 
Index: trunk/include/functions_user.inc.php
===================================================================
--- trunk/include/functions_user.inc.php	(revision 1118)
+++ trunk/include/functions_user.inc.php	(revision 1119)
@@ -634,4 +634,3 @@
   return ($user['adviser'] == 'true');
 }
-
 ?>
Index: trunk/include/section_init.inc.php
===================================================================
--- trunk/include/section_init.inc.php	(revision 1118)
+++ trunk/include/section_init.inc.php	(revision 1119)
@@ -146,13 +146,9 @@
 
   $next_token++;
-
-  for ($i = $next_token; ; $i++)
-  {
-    if (!isset($tokens[$i]))
-    {
-      break;
-    }
-
-    preg_match('/^(\d+)/', $tokens[$i], $matches);
+  $i = $next_token;
+
+  while (isset($tokens[$i]))
+  {
+    preg_match('/^(\d+)(?:-(.*))?/', $tokens[$i], $matches);
     if (!isset($matches[1]))
     {
@@ -166,5 +162,14 @@
       }
     }
-    array_push($page['tags'], $matches[1]);
+    
+    array_push(
+      $page['tags'],
+      array(
+        'id'       => $matches[1],
+        'url_name' => isset($matches[2]) ? $matches[2] : '',
+        )
+      );
+
+    $i++;
   }
 
@@ -226,11 +231,8 @@
 }
 
-for ($i = $next_token; ; $i++)
-{
-  if (!isset($tokens[$i]))
-  {
-    break;
-  }
-
+$i = $next_token;
+
+while (isset($tokens[$i]))
+{
   if (preg_match('/^start-(\d+)/', $tokens[$i], $matches))
   {
@@ -241,7 +243,10 @@
   {
     $chronology_tokens = explode('-', $tokens[$i] );
+
     $page['chronology_field'] = $chronology_tokens[0];
+
     array_shift($chronology_tokens);
     $page['chronology_style'] = $chronology_tokens[0];
+
     array_shift($chronology_tokens);
     if ( count($chronology_tokens)>0 )
@@ -256,4 +261,6 @@
     }
   }
+
+  $i++;
 }
 
@@ -339,4 +346,81 @@
   }
 // +-----------------------------------------------------------------------+
+// |                            tags section                               |
+// +-----------------------------------------------------------------------+
+  if ($page['section'] == 'tags')
+  {
+    $page['tag_ids'] = array();
+    foreach ($page['tags'] as $tag)
+    {
+      array_push($page['tag_ids'], $tag['id']);
+    }
+
+    $items = get_image_ids_for_tags($page['tag_ids']);
+
+    // permissions depends on category, so to only keep images that are
+    // reachable to the connected user, we need to check category
+    // associations
+    if (!empty($user['forbidden_categories']))
+    {
+      $query = '
+SELECT image_id
+  FROM '.IMAGE_CATEGORY_TABLE.'
+  WHERE image_id IN ('.implode(',', $items).')
+    AND '.$forbidden.'
+;';
+      $items = array_unique(
+        array_from_query($query, 'image_id')
+        );
+    }
+
+    // tag names
+    $query = '
+SELECT name, url_name, id
+  FROM '.TAGS_TABLE.'
+  WHERE id IN ('.implode(',', $page['tag_ids']).')
+;';
+    $result = pwg_query($query);
+    $tag_infos = array();
+    
+    while ($row = mysql_fetch_array($result))
+    {
+      $tag_infos[ $row['id'] ]['name'] = $row['name'];
+      $tag_infos[ $row['id'] ]['url_name'] = $row['url_name'];
+    }
+
+    $title = count($page['tags']) > 1 ? l10n('Tags') : l10n('Tag');
+    $title.= ' ';
+
+    $tag_num = 1;
+    foreach ($page['tag_ids'] as $tag_id)
+    {
+      $title.=
+        ($tag_num++ > 1 ? ' + ' : '')
+        .'<a href="'
+        .make_index_url(
+          array(
+            'tags' => array(
+              array(
+                'id' => $tag_id,
+                'url_name' => $tag_infos[$tag_id]['url_name'],
+                ),
+              )
+            )
+          )
+        .'">'
+        .$tag_infos[$tag_id]['name']
+        .'</a>';
+    }
+
+    $page = array_merge(
+      $page,
+      array(
+        'title' => $title,
+        'items' => array_values($items),
+        'thumbnails_include' => 'include/category_default.inc.php',
+        )
+      );
+  }
+// +-----------------------------------------------------------------------+
 // |                           search section                              |
 // +-----------------------------------------------------------------------+
@@ -344,12 +428,13 @@
   {
     include_once( PHPWG_ROOT_PATH .'include/functions_search.inc.php' );
+
     $query = '
 SELECT DISTINCT(id)
   FROM '.IMAGES_TABLE.'
     INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
-  WHERE '.get_sql_search_clause($page['search']).'
+  WHERE id IN ('.implode(',', get_search_items($page['search'])).')
     AND '.$forbidden.'
   '.$conf['order_by'].'
-;';
+;'; 
 
     $page = array_merge(
Index: trunk/index.php
===================================================================
--- trunk/index.php	(revision 1118)
+++ trunk/index.php	(revision 1119)
@@ -222,4 +222,74 @@
   }
 }
+//------------------------------------------------------------------------ tags
+if ('tags' == $page['section'])
+{
+  $template->assign_block_vars('tags', array());
+  
+  // display tags associated to currently tagged items, less current tags
+  $query = '
+SELECT tag_id, name, url_name, count(*) counter
+  FROM '.IMAGE_TAG_TABLE.'
+    INNER JOIN '.TAGS_TABLE.' ON tag_id = id
+  WHERE image_id IN ('.implode(',', $items).')
+    AND tag_id NOT IN ('.implode(',', $page['tag_ids']).')
+  GROUP BY tag_id
+  ORDER BY name ASC
+;';
+  $result = pwg_query($query);
+
+  $tags = array();
+  
+  while($row = mysql_fetch_array($result))
+  {
+    array_push($tags, $row);
+  }
+
+  $tags = add_level_to_tags($tags);
+
+  foreach ($tags as $tag)
+  {
+    $template->assign_block_vars(
+      'tags.tag',
+      array(
+        'URL_ADD' => make_index_URL(
+          array(
+            'tags' => array_merge(
+              $page['tags'],
+              array(
+                array(
+                  'id' => $tag['tag_id'],
+                  'url_name' => $tag['url_name'],
+                  ),
+                )
+              )
+            )
+          ),
+
+        'URL' => make_index_URL(
+          array(
+            'tags' => array(
+              array(
+                'id' => $tag['tag_id'],
+                'url_name' => $tag['url_name'],
+                ),
+              )
+            )
+          ),
+        
+        'NAME' => $tag['name'],
+        
+        'TITLE' => l10n('See pictures linked to this tag only'),
+
+        'TITLE_ADD' => sprintf(
+          l10n('%d pictures are also linked to current tags'),
+          $tag['counter']
+          ),
+        
+        'CLASS' => 'tagLevel'.$tag['level']
+        )
+      );
+  }
+}
 //---------------------------------------------------------- special categories
 // favorites categories
@@ -333,4 +403,14 @@
 }
 
+// tags link
+$template->assign_block_vars(
+  'summary',
+  array(
+    'TITLE' => l10n('See available tags'),
+    'NAME' => l10n('Tags'),
+    'U_SUMMARY'=> get_root_url().'tags.php',
+    )
+  );
+
 // search link
 $template->assign_block_vars(
Index: trunk/install/db/19-database.php
===================================================================
--- trunk/install/db/19-database.php	(revision 1119)
+++ trunk/install/db/19-database.php	(revision 1119)
@@ -0,0 +1,151 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | PhpWebGallery - a PHP based picture gallery                           |
+// | Copyright (C) 2002-2003 Pierrick LE GALL - pierrick@phpwebgallery.net |
+// | Copyright (C) 2003-2005 PhpWebGallery Team - http://phpwebgallery.net |
+// +-----------------------------------------------------------------------+
+// | branch        : BSF (Best So Far)
+// | file          : $RCSfile$
+// | last update   : $Date: 2005-09-21 00:04:57 +0200 (mer, 21 sep 2005) $
+// | last modifier : $Author: plg $
+// | revision      : $Revision: 870 $
+// +-----------------------------------------------------------------------+
+// | This program is free software; you can redistribute it and/or modify  |
+// | it under the terms of the GNU General Public License as published by  |
+// | the Free Software Foundation                                          |
+// |                                                                       |
+// | This program is distributed in the hope that it will be useful, but   |
+// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
+// | General Public License for more details.                              |
+// |                                                                       |
+// | You should have received a copy of the GNU General Public License     |
+// | along with this program; if not, write to the Free Software           |
+// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
+// | USA.                                                                  |
+// +-----------------------------------------------------------------------+
+
+if (!defined('PHPWG_ROOT_PATH'))
+{
+  die('Hacking attempt!');
+}
+
+$upgrade_description = '#images.keywords moved to new table #tags';
+
+// +-----------------------------------------------------------------------+
+// |                              New tables                               |
+// +-----------------------------------------------------------------------+
+
+$query = '
+CREATE TABLE '.PREFIX_TABLE.'tags (
+  id smallint(5) UNSIGNED NOT NULL auto_increment,
+  name varchar(255) BINARY NOT NULL,
+  url_name varchar(255) BINARY NOT NULL,
+  PRIMARY KEY (id)
+) TYPE=MyISAM
+;';
+pwg_query($query);
+
+$query = '
+CREATE TABLE '.PREFIX_TABLE.'image_tag (
+  image_id mediumint(8) UNSIGNED NOT NULL,
+  tag_id smallint(5) UNSIGNED NOT NULL,
+  PRIMARY KEY (image_id,tag_id)
+) TYPE=MyISAM
+;';
+pwg_query($query);
+
+// +-----------------------------------------------------------------------+
+// |                        Move keywords to tags                          |
+// +-----------------------------------------------------------------------+
+
+// each tag label is associated to a numeric identifier
+$tag_id = array();
+// to each tag id (key) a list of image ids (value) is associated
+$tag_images = array();
+
+$current_id = 1;
+
+$query = '
+SELECT id, keywords
+  FROM '.PREFIX_TABLE.'images
+  WHERE keywords IS NOT NULL
+;';
+$result = pwg_query($query);
+while ($row = mysql_fetch_array($result))
+{
+  foreach(preg_split('/[,]+/', $row['keywords']) as $keyword)
+  {
+    if (!isset($tag_id[$keyword]))
+    {
+      $tag_id[$keyword] = $current_id++;
+    }
+    
+    if (!isset($tag_images[ $tag_id[$keyword] ]))
+    {
+      $tag_images[ $tag_id[$keyword] ] = array();
+    }
+
+    array_push($tag_images[ $tag_id[$keyword] ], $row['id']);
+  }
+}
+
+$datas = array();
+foreach ($tag_id as $tag_name => $tag_id)
+{
+  array_push(
+    $datas,
+    array(
+      'id'       => $tag_id,
+      'name'     => $tag_name,
+      'url_name' => str2url($tag_name),
+      )
+    );
+}
+mass_inserts(
+  PREFIX_TABLE.'tags',
+  array_keys($datas[0]),
+  $datas
+  );
+
+$datas = array();
+foreach ($tag_images as $tag_id => $images)
+{
+  foreach (array_unique($images) as $image_id)
+  {
+    array_push(
+      $datas,
+      array(
+        'tag_id'   => $tag_id,
+        'image_id' => $image_id,
+        )
+      );
+  }
+}
+
+mass_inserts(
+  PREFIX_TABLE.'image_tag',
+  array_keys($datas[0]),
+  $datas
+  );
+
+// +-----------------------------------------------------------------------+
+// |                         Delete images.keywords                        |
+// +-----------------------------------------------------------------------+
+
+$query = '
+ALTER TABLE '.PREFIX_TABLE.'images DROP COLUMN keywords
+;';
+pwg_query($query);
+
+// +-----------------------------------------------------------------------+
+// |                           End notification                            |
+// +-----------------------------------------------------------------------+
+
+echo
+"\n"
+.'Table '.PREFIX_TABLE.'tags created and filled'."\n"
+.'Table '.PREFIX_TABLE.'image_tag created and filled'."\n"
+.'Column '.PREFIX_TABLE.'images.keywords dropped'."\n"
+;
+?>
Index: trunk/install/phpwebgallery_structure.sql
===================================================================
--- trunk/install/phpwebgallery_structure.sql	(revision 1118)
+++ trunk/install/phpwebgallery_structure.sql	(revision 1119)
@@ -1,3 +1,2 @@
--- MySQL dump 9.11
 -- MySQL dump 9.11
 --
@@ -147,4 +146,15 @@
 
 --
+-- Table structure for table `phpwebgallery_image_tag`
+--
+
+DROP TABLE IF EXISTS `phpwebgallery_image_tag`;
+CREATE TABLE `phpwebgallery_image_tag` (
+  `image_id` mediumint(8) unsigned NOT NULL default '0',
+  `tag_id` smallint(5) unsigned NOT NULL default '0',
+  PRIMARY KEY  (`image_id`,`tag_id`)
+) TYPE=MyISAM;
+
+--
 -- Table structure for table `phpwebgallery_images`
 --
@@ -164,6 +174,4 @@
   `width` smallint(9) unsigned default NULL,
   `height` smallint(9) unsigned default NULL,
-  `keywords` varchar(255) default NULL,
-  `storage_category_id` smallint(5) unsigned default NULL,
   `representative_ext` varchar(4) default NULL,
   `date_metadata_update` date default NULL,
@@ -173,5 +181,4 @@
   PRIMARY KEY  (`id`),
   KEY `images_i2` (`date_available`),
-  KEY `images_i1` (`storage_category_id`),
   KEY `images_i3` (`average_rate`),
   KEY `images_i4` (`hit`),
@@ -230,4 +237,16 @@
 
 --
+-- Table structure for table `phpwebgallery_tags`
+--
+
+DROP TABLE IF EXISTS `phpwebgallery_tags`;
+CREATE TABLE `phpwebgallery_tags` (
+  `id` smallint(5) unsigned NOT NULL auto_increment,
+  `name` varchar(255) binary NOT NULL default '',
+  `url_name` varchar(255) binary NOT NULL default '',
+  PRIMARY KEY  (`id`)
+) TYPE=MyISAM;
+
+--
 -- Table structure for table `phpwebgallery_upgrade`
 --
@@ -261,5 +280,5 @@
   `need_update` enum('true','false') NOT NULL default 'true',
   `forbidden_categories` text,
-  `nb_total_images` mediumint(8) unsigned,
+  `nb_total_images` mediumint(8) unsigned default NULL,
   PRIMARY KEY  (`user_id`)
 ) TYPE=MyISAM;
@@ -297,5 +316,5 @@
   `nb_image_line` tinyint(1) unsigned NOT NULL default '5',
   `nb_line_page` tinyint(3) unsigned NOT NULL default '3',
-  `status` enum('webmaster', 'admin', 'normal', 'generic', 'guest') NOT NULL default 'guest',
+  `status` enum('webmaster','admin','normal','generic','guest') NOT NULL default 'guest',
   `adviser` enum('true','false') NOT NULL default 'false',
   `language` varchar(50) NOT NULL default 'english',
Index: trunk/language/en_UK.iso-8859-1/admin.lang.php
===================================================================
--- trunk/language/en_UK.iso-8859-1/admin.lang.php	(revision 1118)
+++ trunk/language/en_UK.iso-8859-1/admin.lang.php	(revision 1119)
@@ -41,4 +41,5 @@
 $lang['A new version of PhpWebGallery is available.'] = 'A new version of PhpWebGallery is available.';
 $lang['Actions'] = 'Actions';
+$lang['Add a tag'] = 'Add a tag';
 $lang['Add a user'] = 'Add a user';
 $lang['Add group'] = 'Add group';
@@ -59,6 +60,8 @@
 $lang['Controversy'] = 'Controversy';
 $lang['Creation date'] = 'Creation date';
+$lang['Current name'] = 'Current name';
 $lang['Database'] = 'Database';
 $lang['Delete Representant'] = 'Delete Representant';
+$lang['Delete selected tags'] = 'Delete selected tags';
 $lang['Delete selected users'] = 'Delete selected users';
 $lang['Deletions'] = 'Deletions';
@@ -70,4 +73,6 @@
 $lang['Does not represent'] = 'Does not represent';
 $lang['Edit all picture informations'] = 'Edit all picture informations';
+$lang['Edit selected tags'] = 'Edit selected tags';
+$lang['Edit tags'] = 'Edit tags';
 $lang['Elements'] = 'Elements';
 $lang['Empty caddie'] = 'Empty caddie';
@@ -92,4 +97,5 @@
 $lang['Manage permissions for group "%s"'] = 'Manage permissions for group "%s"';
 $lang['Manage permissions for user "%s"'] = 'Manage permissions for user "%s"';
+$lang['Manage tags'] = 'Manage tags';
 $lang['Maximum height of the pictures'] = 'Maximum height of the pictures';
 $lang['Maximum width of the pictures'] = 'Maximum width of the pictures';
@@ -99,5 +105,7 @@
 $lang['Move'] = 'Move';
 $lang['Name'] = 'Name';
+$lang['New name'] = 'New name';
 $lang['New parent category'] = 'New parent category';
+$lang['New tag'] = 'New tag';
 $lang['No'] = 'No';
 $lang['Number of comments per page'] = 'Number of comments per page';
@@ -112,4 +120,5 @@
 $lang['Order by'] = 'Order by';
 $lang['Other private categories'] = 'Other private categories';
+$lang['Page banner'] = 'Page banner';
 $lang['Parent category'] = 'Parent category';
 $lang['Path'] = 'Path';
@@ -119,5 +128,4 @@
 $lang['PhpWebGallery Administration'] = 'PhpWebGallery Administration';
 $lang['PhpWebGallery version'] = 'PhpWebGallery version';
-$lang['Page banner'] = 'Page banner';
 $lang['Picture informations updated'] = 'Picture informations updated';
 $lang['Position'] = 'Position';
@@ -147,4 +155,5 @@
 $lang['Submit'] = 'Submit';
 $lang['Sum of rates'] = 'Sum of rates';
+$lang['Tag selection'] = 'Tag selection';
 $lang['Take selected elements out of caddie'] = 'Take selected elements out of caddie';
 $lang['Unable to check for upgrade since allow_url_fopen is disabled.'] = 'Unable to check for upgrade since allow_url_fopen is disabled.';
@@ -163,10 +172,7 @@
 $lang['You need to confirm deletion'] = 'You need to confirm deletion';
 $lang['actions'] = 'Actions';
-$lang['properties'] = 'Properties';
-$lang['is_high_enabled'] = 'High definition';
-$lang['is_high_disabled'] = '';
-$lang['enabled_high'] = 'High definition enabled';
+$lang['add keywords'] = 'add keywords';
+$lang['add tags'] = 'add tags';
 $lang['adviser'] = 'Adviser';
-$lang['add keywords'] = 'add keywords';
 $lang['associate to category'] = 'associate to category';
 $lang['associate to group'] = 'associate to group';
@@ -209,4 +215,5 @@
 $lang['elements per page'] = 'elements per page';
 $lang['elements'] = 'elements';
+$lang['enabled_high'] = 'High definition enabled';
 $lang['file'] = 'file';
 $lang['filesize'] = 'filesize';
@@ -225,4 +232,6 @@
 $lang['history'] = 'History';
 $lang['instructions'] = 'Instructions';
+$lang['is_high_disabled'] = '';
+$lang['is_high_enabled'] = 'High definition';
 $lang['jump to category'] = 'jump to category';
 $lang['jump to image'] = 'jump to image';
@@ -240,60 +249,60 @@
 $lang['name'] = 'name';
 $lang['nbm_break_list_user'] = 'List of users to send mail is limited to %d. Others users are not listed.';
-$lang['nbm_nbm_break_send_mail'] = 'Sent mail is limited to %d send by pass. Others mails are skipped.';
-$lang['nbm_msg_no_mail_to_send'] = '%d mails were not sent.';
-$lang['nbm_msg_n_mails_sent'] = '%d mails were sent.';
+$lang['nbm_col_check_user_send_mail'] = 'To send ?';
+$lang['nbm_col_last_send'] = 'Last send';
+$lang['nbm_col_mail'] = 'email';
+$lang['nbm_col_user'] = 'User';
+$lang['nbm_complementary_mail_content'] = 'Complementary mail content';
+$lang['nbm_content_byebye'] = 'See you soon';
+$lang['nbm_content_goto'] = 'Go to %s %s.';
+$lang['nbm_content_hello'] = 'Hello %s';
+$lang['nbm_content_new_elements'] = 'New elements were added on %s';
+$lang['nbm_content_new_elements_between'] = 'New elements were added between %s and %s';
+$lang['nbm_content_subscribe_by_admin'] = 'You are subcribed by webmaster for the notification by mail';
+$lang['nbm_content_subscribe_by_himself'] = 'You are subcribed for the notification by mail';
+$lang['nbm_content_subscribe_link'] = 'To subscribe, click on %s .';
+$lang['nbm_content_subscribe_unsubscribe_contact'] = 'On problems or questions, send a message to %s.';
+$lang['nbm_content_unsubscribe_by_admin'] = 'You are unsubcribed by webmaster for the notification by mail';
+$lang['nbm_content_unsubscribe_by_himself'] = 'You are unsubcribed for the notification by mail';
+$lang['nbm_content_unsubscribe_link'] = 'To unsubscribe, click on %s .';
+$lang['nbm_info_send_mail_as'] = 'With blank value, gallery title will be used';
+$lang['nbm_item_notification'] = 'Notification';
 $lang['nbm_msg_error_sending_email_to'] = 'Error when sending email to %s [%s].';
 $lang['nbm_msg_mail_sent_to'] = 'Mail sent to %s [%s].';
+$lang['nbm_msg_n_mails_sent'] = '%d mails were sent.';
+$lang['nbm_msg_no_mail_to_send'] = '%d mails were not sent.';
+$lang['nbm_nbm_break_send_mail'] = 'Sent mail is limited to %d send by pass. Others mails are skipped.';
+$lang['nbm_no_mail_to_send'] = 'No mail to send.';
+$lang['nbm_no_user_available_to_send_L1'] = 'No user are available in order to send mail.';
+$lang['nbm_no_user_available_to_send_L2'] = 'A user is available, if the are news elements to notify';
+$lang['nbm_no_user_to send_notifications_by_mail'] = 'No user to send notifications by mail.';
 $lang['nbm_object_news'] = 'New elements added';
 $lang['nbm_object_subcribe'] = 'Subcribe of notification by mail';
 $lang['nbm_object_unsubcribe'] = 'Unsubcribe of notification by mail';
-$lang['nbm_content_hello'] = 'Hello %s';
-$lang['nbm_content_new_elements_between'] = 'New elements were added between %s and %s';
-$lang['nbm_content_new_elements'] = 'New elements were added on %s';
-$lang['nbm_content_goto'] = 'Go to %s %s.';
-$lang['nbm_content_subscribe_by_admin'] = 'You are subcribed by webmaster for the notification by mail';
-$lang['nbm_content_unsubscribe_by_admin'] = 'You are unsubcribed by webmaster for the notification by mail';
-$lang['nbm_content_subscribe_by_himself'] = 'You are subcribed for the notification by mail';
-$lang['nbm_content_unsubscribe_by_himself'] = 'You are unsubcribed for the notification by mail';
-$lang['nbm_content_byebye'] = 'See you soon';
-$lang['nbm_content_unsubscribe_link'] = 'To unsubscribe, click on %s .';
-$lang['nbm_content_subscribe_link'] = 'To subscribe, click on %s .';
-$lang['nbm_content_subscribe_unsubscribe_contact'] = 'On problems or questions, send a message to %s.';
-$lang['nbm_no_mail_to_send'] = 'No mail to send.';
-$lang['nbm_no_user_to send_notifications_by_mail'] = 'No user to send notifications by mail.';
+$lang['nbm_param_mode'] = 'Parameter';
+$lang['nbm_send_check_all'] = 'Check All';
+$lang['nbm_send_complementary_mail_content'] = 'Complementary mail content';
+$lang['nbm_send_detailed_content'] = 'Send detailed content';
+$lang['nbm_send_mail_as'] = 'Send mail as';
 $lang['nbm_send_mail_to_users'] = 'Send mail to users';
+$lang['nbm_send_mode'] = 'Send';
+$lang['nbm_send_options'] = 'Options';
+$lang['nbm_send_submit'] = 'Send';
+$lang['nbm_send_uncheck_all'] = 'Uncheck All';
+$lang['nbm_subscribe_col'] = 'Subscribed';
+$lang['nbm_subscribe_mode'] = 'Subscribe';
+$lang['nbm_title_param'] = 'Parameters';
+$lang['nbm_title_send'] = 'Select sendings';
+$lang['nbm_title_subscribe'] = 'Subscribe/unscribe users';
+$lang['nbm_unsubscribe_col'] = 'Unsubcribed';
+$lang['nbm_updated_param_count'] = '%d parameters are updated.';
+$lang['nbm_user_change_enabled_error_on_updated_data_count'] = '%d user(s) are not updated.';
+$lang['nbm_user_change_enabled_false'] = 'User %s [%s] removed of subscribe list.';
+$lang['nbm_user_change_enabled_true'] = 'User %s [%s] added to subscribe list.';
+$lang['nbm_user_change_enabled_updated_data_count'] = '%d user(s) are updated.';
+$lang['nbm_user_not_change_enabled_false'] = 'User %s [%s] not removed of subscribe list.';
+$lang['nbm_user_not_change_enabled_true'] = 'User %s [%s] not added to subscribe list.';
 $lang['nbm_user_x_added'] = 'User %s [%s] added.';
-$lang['nbm_item_notification'] = 'Notification';
-$lang['nbm_param_mode'] = 'Parameter';
-$lang['nbm_subscribe_mode'] = 'Subscribe';
-$lang['nbm_send_mode'] = 'Send';
-$lang['nbm_title_param'] = 'Parameters';
-$lang['nbm_updated_param_count'] = '%d parameters are updated.';
-$lang['nbm_send_mail_as'] = 'Send mail as';
-$lang['nbm_info_send_mail_as'] = 'With blank value, gallery title will be used';
-$lang['nbm_send_detailed_content'] = 'Send detailed content';
-$lang['nbm_complementary_mail_content'] = 'Complementary mail content';
-$lang['nbm_title_subscribe'] = 'Subscribe/unscribe users';
 $lang['nbm_warning_subscribe_unsubcribe'] = 'Warning, subscribe or unscribe send mails to users';
-$lang['nbm_subscribe_col'] = 'Subscribed';
-$lang['nbm_unsubscribe_col'] = 'Unsubcribed';
-$lang['nbm_no_user_available_to_send_L1'] = 'No user are available in order to send mail.';
-$lang['nbm_no_user_available_to_send_L2'] = 'A user is available, if the are news elements to notify';
-$lang['nbm_title_send'] = 'Select sendings';
-$lang['nbm_col_user'] = 'User';
-$lang['nbm_col_mail'] = 'email';
-$lang['nbm_col_last_send'] = 'Last send';
-$lang['nbm_col_check_user_send_mail'] = 'To send ?';
-$lang['nbm_send_options'] = 'Options';
-$lang['nbm_send_complementary_mail_content'] = 'Complementary mail content';
-$lang['nbm_send_submit'] = 'Send';
-$lang['nbm_send_check_all'] = 'Check All';
-$lang['nbm_send_uncheck_all'] = 'Uncheck All';
-$lang['nbm_user_change_enabled_true'] = 'User %s [%s] added to subscribe list.';
-$lang['nbm_user_change_enabled_false'] = 'User %s [%s] removed of subscribe list.';
-$lang['nbm_user_not_change_enabled_true'] = 'User %s [%s] not added to subscribe list.';
-$lang['nbm_user_not_change_enabled_false'] = 'User %s [%s] not removed of subscribe list.';
-$lang['nbm_user_change_enabled_updated_data_count'] = '%d user(s) are updated.';
-$lang['nbm_user_change_enabled_error_on_updated_data_count'] = '%d user(s) are not updated.';
 $lang['no_write_access'] = 'no write access';
 $lang['order_by'] = 'order by';
@@ -304,8 +313,8 @@
 $lang['pictures'] = 'pictures';
 $lang['private'] = 'private';
+$lang['properties'] = 'Properties';
 $lang['public'] = 'public';
 $lang['purge history'] = 'purge history';
 $lang['purge never used notification feeds'] = 'purge never used notification feeds';
-$lang['repair and optimize database'] = 'repair and optimize database';
 $lang['purge sessions'] = 'purge sessions';
 $lang['randomly represented'] = 'randomly represented';
@@ -323,4 +332,6 @@
 $lang['remote_site_uncorrect_url'] = 'Remote site url must start by http or https and must only contain characters among "/", "a-zA-Z0-9", "-" or "_"';
 $lang['remove keyword'] = 'remove keyword';
+$lang['remove tags'] = 'remove tags';
+$lang['repair and optimize database'] = 'repair and optimize database';
 $lang['selection'] = 'selection';
 $lang['set to'] = 'set to';
@@ -443,9 +454,9 @@
 $lang['user_id URL parameter is missing'] = 'user_id URL parameter is missing';
 $lang['user_status'] = 'User status';
-$lang['user_status_webmaster'] = 'Webmaster';
 $lang['user_status_admin'] = 'Administrator';
-$lang['user_status_normal'] = 'User';
 $lang['user_status_generic'] = 'Generic';
 $lang['user_status_guest'] = 'Guest';
+$lang['user_status_normal'] = 'User';
+$lang['user_status_webmaster'] = 'Webmaster';
 $lang['username'] = 'username';
 $lang['users'] = 'Users';
Index: trunk/language/en_UK.iso-8859-1/common.lang.php
===================================================================
--- trunk/language/en_UK.iso-8859-1/common.lang.php	(revision 1118)
+++ trunk/language/en_UK.iso-8859-1/common.lang.php	(revision 1119)
@@ -40,7 +40,12 @@
 $lang['%d new elements'] = '%d new elements';
 $lang['%d new users'] = '%d new users';
+$lang['%d pictures are also linked to current tags'] = '%d pictures are also linked to current tags';
 $lang['%d waiting elements'] = '%d waiting elements';
 $lang['About'] = 'About';
+$lang['All tags must match'] = 'All tags must match';
+$lang['All tags'] = 'All tags';
+$lang['Any tag'] = 'Any tag';
 $lang['At least one listed rule must be satisfied.'] = 'At least one listed rule must be satisfied.';
+$lang['At least one tag must match'] = 'At least one tag must match';
 $lang['Author'] = 'Author';
 $lang['Average rate'] = 'Average rate';
@@ -63,6 +68,6 @@
 $lang['Enter your personnal informations'] = 'Enter your personnal informations';
 $lang['Error sending email'] = 'Error sending email';
+$lang['File name'] = 'File name';
 $lang['File'] = 'File';
-$lang['File name'] = 'File name';
 $lang['Filesize'] = 'Filesize';
 $lang['Filter and display'] = 'Filter and display';
@@ -92,19 +97,24 @@
 $lang['Profile'] = 'Profile';
 $lang['Quick connect'] = 'Quick connect';
+$lang['RSS feed'] = 'RSS feed';
 $lang['Rate'] = 'Rate';
-$lang['RSS feed'] = 'RSS feed';
 $lang['Register'] = 'Register';
 $lang['Registration'] = 'Registration';
+$lang['Related tags'] = 'Related tags';
 $lang['Reset'] = 'Reset';
 $lang['Retrieve password'] = 'Retrieve password';
 $lang['Search rules'] = 'Search rules';
+$lang['Search tags'] = 'Search tags';
 $lang['Search'] = 'Search';
+$lang['See available tags'] = 'See available tags';
+$lang['See pictures linked to this tag only'] = 'See pictures linked to this tag only';
 $lang['Send new password'] = 'Send new password';
 $lang['Since'] = 'Since';
 $lang['Sort by'] = 'Sort by';
 $lang['Sort order'] = 'Sort order';
+$lang['Tag'] = 'Tag';
+$lang['Tags'] = 'Tags';
 $lang['The RSS notification feed provides notification on news from this website : new pictures, updated categories, new comments. Use a RSS feed reader.'] = 'The RSS notification feed provides notification on news from this website : new pictures, updated categories, new comments. Use a RSS feed reader.';
 $lang['Unknown feed identifier'] = 'Unknown feed identifier';
-$lang['nbm_unknown_identifier'] = 'Unknown identifier';
 $lang['User comments'] = 'User comments';
 $lang['Username'] = 'Username';
@@ -118,4 +128,5 @@
 $lang['add_favorites_hint'] = 'Add this picture to your favorites';
 $lang['admin'] = 'Administration';
+$lang['adviser_mode_enabled'] = 'Adviser mode enabled';
 $lang['all'] = 'all';
 $lang['all_categories'] = 'all categories';
@@ -136,6 +147,6 @@
 $lang['calendar_picture_hint'] = 'displays pictures added on ';
 $lang['calendar_view'] = 'View';
+$lang['chronology_monthly_calendar'] = 'Monthly calendar';
 $lang['chronology_monthly_list'] = 'Monthly list';
-$lang['chronology_monthly_calendar'] = 'Monthly calendar';
 $lang['chronology_weekly_list'] = 'Weekly list';
 $lang['click_to_redirect'] = 'Click here if your browser does not automatically forward you';
@@ -212,6 +223,6 @@
 $lang['maxwidth'] = 'Maximum width of the pictures';
 $lang['maxwidth_error'] = 'Maximum width must be a number superior to 50';
+$lang['mode_created_hint'] = 'displays a calendar by creation date';
 $lang['mode_normal_hint'] = 'return to normal view mode';
-$lang['mode_created_hint'] = 'displays a calendar by creation date';
 $lang['mode_posted_hint'] = 'displays a calendar by date posted';
 $lang['month'][10] = 'October';
@@ -229,8 +240,9 @@
 $lang['most_visited_cat'] = 'Most visited';
 $lang['most_visited_cat_hint'] = 'displays most visited pictures';
+$lang['nb_image_line_error'] = 'The number of images per row must be a not null scalar';
 $lang['nb_image_per_row'] = 'Number of images per row';
-$lang['nb_image_line_error'] = 'The number of images per row must be a not null scalar';
+$lang['nb_line_page_error'] = 'The number of rows per page must be a not null scalar';
 $lang['nb_row_per_page'] = 'Number of rows per page';
-$lang['nb_line_page_error'] = 'The number of rows per page must be a not null scalar';
+$lang['nbm_unknown_identifier'] = 'Unknown identifier';
 $lang['never_rated'] = 'You\'ve never rated this item';
 $lang['new_password'] = 'New password';
@@ -333,4 +345,3 @@
 $lang['w_month'] = 'Month';
 $lang['yes'] = 'Yes';
-$lang['adviser_mode_enabled'] = 'Adviser mode enabled';
 ?>
Index: trunk/language/fr_FR.iso-8859-1/admin.lang.php
===================================================================
--- trunk/language/fr_FR.iso-8859-1/admin.lang.php	(revision 1118)
+++ trunk/language/fr_FR.iso-8859-1/admin.lang.php	(revision 1119)
@@ -26,4 +26,5 @@
 // +-----------------------------------------------------------------------+
 
+
 $lang['%d categories including %d physical and %d virtual'] = '%d catégories dont %d physiques et %d virtuelles';
 $lang['%d categories moved'] = '%d catégories déplacées';
@@ -41,4 +42,5 @@
 $lang['A new version of PhpWebGallery is available.'] = 'Une nouvelle version de PhpWebGallery est disponible.';
 $lang['Actions'] = 'Actions';
+$lang['Add a tag'] = 'Ajouter un tag';
 $lang['Add a user'] = 'Ajouter un utilisateur';
 $lang['Add group'] = 'Ajouter un groupe';
@@ -59,6 +61,8 @@
 $lang['Controversy'] = 'Controverse';
 $lang['Creation date'] = 'Date de création';
+$lang['Current name'] = 'Nom courant';
 $lang['Database'] = 'Base de données';
 $lang['Delete Representant'] = 'Supprimer le représentant';
+$lang['Delete selected tags'] = 'Supprimer les tags sélectionnés';
 $lang['Delete selected users'] = 'Supprimer les utilisateurs sélectionnés';
 $lang['Deletions'] = 'Suppressions';
@@ -70,4 +74,6 @@
 $lang['Does not represent'] = 'Ne représente pas';
 $lang['Edit all picture informations'] = 'Modifier toutes les informations liées à cette image';
+$lang['Edit selected tags'] = 'Editer les tags sélectionnés';
+$lang['Edit tags'] = 'Editer les tags';
 $lang['Elements'] = 'Éléments';
 $lang['Empty caddie'] = 'Vider le panier';
@@ -92,4 +98,5 @@
 $lang['Manage permissions for group "%s"'] = 'Gérer les permissions pour le groupe "%s"';
 $lang['Manage permissions for user "%s"'] = 'Gérer les permissions pour l\'utilisateur "%s"';
+$lang['Manage tags'] = 'Gérer les tags';
 $lang['Maximum height of the pictures'] = 'Hauteur maximum des images';
 $lang['Maximum width of the pictures'] = 'Largeur maximum des images';
@@ -99,5 +106,7 @@
 $lang['Move'] = 'Déplacer';
 $lang['Name'] = 'Nom';
+$lang['New name'] = 'Nouveau nom';
 $lang['New parent category'] = 'Nouvelle catégorie parente';
+$lang['New tag'] = 'Nouveau tag';
 $lang['No'] = 'Non';
 $lang['Number of comments per page'] = 'Nombre de commentaires utilisateur par page';
@@ -147,4 +156,5 @@
 $lang['Submit'] = 'Valider';
 $lang['Sum of rates'] = 'Somme des notes';
+$lang['Tag selection'] = 'Sélection de tags';
 $lang['Take selected elements out of caddie'] = 'Sortir les éléments sélectionnés du panier';
 $lang['Unable to check for upgrade since allow_url_fopen is disabled.'] = 'Impossible de connaître la dernière version cat la fonction allow_url_fopen est désactivée.';
@@ -163,10 +173,7 @@
 $lang['You need to confirm deletion'] = 'Vous devez confirmer la suppression';
 $lang['actions'] = 'Actions';
-$lang['properties'] = 'Propriétés';
-$lang['is_high_enabled'] = 'Haute définition';
-$lang['is_high_disabled'] = '';
-$lang['enabled_high'] = 'Haute définition actif';
+$lang['add keywords'] = 'ajouter des mots-clef';
+$lang['add tags'] = 'ajouter les tags';
 $lang['adviser'] = 'Conseiller';
-$lang['add keywords'] = 'ajouter des mots-clef';
 $lang['associate to category'] = 'associer à la catégorie';
 $lang['associate to group'] = 'associer au groupe';
@@ -209,4 +216,5 @@
 $lang['elements per page'] = 'éléments par page';
 $lang['elements'] = 'éléments';
+$lang['enabled_high'] = 'Haute définition actif';
 $lang['file'] = 'fichier';
 $lang['filesize'] = 'poids';
@@ -225,4 +233,6 @@
 $lang['history'] = 'Historique';
 $lang['instructions'] = 'Instructions';
+$lang['is_high_disabled'] = '';
+$lang['is_high_enabled'] = 'Haute définition';
 $lang['jump to category'] = 'se rendre dans la catégorie';
 $lang['jump to image'] = 'se rendre à l\'image';
@@ -240,60 +250,60 @@
 $lang['name'] = 'nom';
 $lang['nbm_break_list_user'] = 'La liste des utilisateurs pour l\'envoi est limitéé à %d. Les autres utilisateurs ne sont pas listés.';
-$lang['nbm_nbm_break_send_mail'] = 'Les mails envoyés sont limités à %d envois d\'une seule passe. Les autres envois de mail ont été ignorés.';
-$lang['nbm_msg_no_mail_to_send'] = '%s mails n\'ont pas été envoyés.';
-$lang['nbm_msg_n_mails_sent'] = '%s mails ont été envoyés.';
+$lang['nbm_col_check_user_send_mail'] = 'A envoyer ?';
+$lang['nbm_col_last_send'] = 'Dernier envoi';
+$lang['nbm_col_mail'] = 'email';
+$lang['nbm_col_user'] = 'Utilisateur';
+$lang['nbm_complementary_mail_content'] = 'Contenu complémentaire au mail';
+$lang['nbm_content_byebye'] = 'A bientôt';
+$lang['nbm_content_goto'] = 'Rendez-vous sur %s %s.';
+$lang['nbm_content_hello'] = 'Bonjour %s';
+$lang['nbm_content_new_elements'] = 'Des nouveaux éléments ont été ajoutés le %s';
+$lang['nbm_content_new_elements_between'] = 'Des nouveaux éléments ont été ajoutés entre le %s et le %s';
+$lang['nbm_content_subscribe_by_admin'] = 'Vous venez d\'être inscrit par le webmestre du site pour revevoir la notification par mail.';
+$lang['nbm_content_subscribe_by_himself'] = 'Vous venez de vous inscrire pour revevoir la notification par mail.';
+$lang['nbm_content_subscribe_link'] = 'Pour vous inscrire, cliquez sur %s .';
+$lang['nbm_content_subscribe_unsubscribe_contact'] = 'En cas de problèmes ou de questions, envoyer un mail à %s.';
+$lang['nbm_content_unsubscribe_by_admin'] = 'Vous venez d\'être désinscrit par le webmestre du site pour revevoir la notification par mail.';
+$lang['nbm_content_unsubscribe_by_himself'] = 'Vous venez de vous désinscrire pour revevoir la notification par mail.';
+$lang['nbm_content_unsubscribe_link'] = 'Pour vous désinscrire, cliquez sur %s .';
+$lang['nbm_info_send_mail_as'] = 'Sans valeur, le titre de la galerie sera utilisé';
+$lang['nbm_item_notification'] = 'Notification';
 $lang['nbm_msg_error_sending_email_to'] = 'Erreur lors de l\'envoi du mail à %s [%s].';
 $lang['nbm_msg_mail_sent_to'] = 'Mail envoyé à %s [%s].';
+$lang['nbm_msg_n_mails_sent'] = '%s mails ont été envoyés.';
+$lang['nbm_msg_no_mail_to_send'] = '%s mails n\'ont pas été envoyés.';
+$lang['nbm_nbm_break_send_mail'] = 'Les mails envoyés sont limités à %d envois d\'une seule passe. Les autres envois de mail ont été ignorés.';
+$lang['nbm_no_mail_to_send'] = 'Pas de mail à envoyer.';
+$lang['nbm_no_user_available_to_send_L1'] = 'Il n\'y a pas d\'utilisateur à notifier par mail.';
+$lang['nbm_no_user_available_to_send_L2'] = 'Un utilisateur est à notifier si de nouveaux éléments sont disponibles pour cet utilisateur.';
+$lang['nbm_no_user_to send_notifications_by_mail'] = 'Pas d\'utilisateur pour envoyer des notifications par mails.';
 $lang['nbm_object_news'] = 'Nouveaux éléments ajoutés';
 $lang['nbm_object_subcribe'] = 'Inscription à la notification par mail';
 $lang['nbm_object_unsubcribe'] = 'Désinscription à la notification par mail';
-$lang['nbm_content_hello'] = 'Bonjour %s';
-$lang['nbm_content_new_elements_between'] = 'Des nouveaux éléments ont été ajoutés entre le %s et le %s';
-$lang['nbm_content_new_elements'] = 'Des nouveaux éléments ont été ajoutés le %s';
-$lang['nbm_content_goto'] = 'Rendez-vous sur %s %s.';
-$lang['nbm_content_subscribe_by_admin'] = 'Vous venez d\'être inscrit par le webmestre du site pour revevoir la notification par mail.';
-$lang['nbm_content_unsubscribe_by_admin'] = 'Vous venez d\'être désinscrit par le webmestre du site pour revevoir la notification par mail.';
-$lang['nbm_content_subscribe_by_himself'] = 'Vous venez de vous inscrire pour revevoir la notification par mail.';
-$lang['nbm_content_unsubscribe_by_himself'] = 'Vous venez de vous désinscrire pour revevoir la notification par mail.';
-$lang['nbm_content_byebye'] = 'A bientôt';
-$lang['nbm_content_unsubscribe_link'] = 'Pour vous désinscrire, cliquez sur %s .';
-$lang['nbm_content_subscribe_link'] = 'Pour vous inscrire, cliquez sur %s .';
-$lang['nbm_content_subscribe_unsubscribe_contact'] = 'En cas de problèmes ou de questions, envoyer un mail à %s.';
-$lang['nbm_no_mail_to_send'] = 'Pas de mail à envoyer.';
-$lang['nbm_no_user_to send_notifications_by_mail'] = 'Pas d\'utilisateur pour envoyer des notifications par mails.';
+$lang['nbm_param_mode'] = 'Paramètrage';
+$lang['nbm_send_check_all'] = 'Tout cocher';
+$lang['nbm_send_complementary_mail_content'] = 'Contenu complémentaire du mail';
+$lang['nbm_send_detailed_content'] = 'Envoi d\'un contenu détaillé';
+$lang['nbm_send_mail_as'] = 'Envoyer le mail en tant que';
 $lang['nbm_send_mail_to_users'] = 'Envoi de mail aux utilisateurs';
+$lang['nbm_send_mode'] = 'Envoi';
+$lang['nbm_send_options'] = 'Options';
+$lang['nbm_send_submit'] = 'Envoyer';
+$lang['nbm_send_uncheck_all'] = 'Tout décocher';
+$lang['nbm_subscribe_col'] = 'Inscrits';
+$lang['nbm_subscribe_mode'] = 'Inscription';
+$lang['nbm_title_param'] = 'Paramètres';
+$lang['nbm_title_send'] = 'Sélection des envois';
+$lang['nbm_title_subscribe'] = 'Inscrire/desinscrire les utilisateurs';
+$lang['nbm_unsubscribe_col'] = 'Non Inscrits';
+$lang['nbm_updated_param_count'] = '%d paramètres ont été mis à jour.';
+$lang['nbm_user_change_enabled_error_on_updated_data_count'] = '%d utilisateurs n\'ont pas été mis à jour.';
+$lang['nbm_user_change_enabled_false'] = 'L\'utilisateur %s [%s] a été supprimé de la liste des inscrits.';
+$lang['nbm_user_change_enabled_true'] = 'L\'utilisateur %s [%s] a été ajouté à la liste des inscrits.';
+$lang['nbm_user_change_enabled_updated_data_count'] = '%d utilisateurs ont été mis à jour.';
+$lang['nbm_user_not_change_enabled_false'] = 'L\'utilisateur %s [%s] n\'a pas été supprimé de la liste des inscrits.';
+$lang['nbm_user_not_change_enabled_true'] = 'L\'utilisateur %s [%s] n\'a pas été ajouté à la liste des inscrits.';
 $lang['nbm_user_x_added'] = 'Utilisateur %s [%s] ajouté.';
-$lang['nbm_item_notification'] = 'Notification';
-$lang['nbm_param_mode'] = 'Paramètrage';
-$lang['nbm_subscribe_mode'] = 'Inscription';
-$lang['nbm_send_mode'] = 'Envoi';
-$lang['nbm_title_param'] = 'Paramètres';
-$lang['nbm_updated_param_count'] = '%d paramètres ont été mis à jour.';
-$lang['nbm_send_mail_as'] = 'Envoyer le mail en tant que';
-$lang['nbm_info_send_mail_as'] = 'Sans valeur, le titre de la galerie sera utilisé';
-$lang['nbm_send_detailed_content'] = 'Envoi d\'un contenu détaillé';
-$lang['nbm_complementary_mail_content'] = 'Contenu complémentaire au mail';
-$lang['nbm_title_subscribe'] = 'Inscrire/desinscrire les utilisateurs';
 $lang['nbm_warning_subscribe_unsubcribe'] = 'Attention, l\'inscription ou la desincription entraine l\'envoi de mails aux utilisateurs concernés';
-$lang['nbm_subscribe_col'] = 'Inscrits';
-$lang['nbm_unsubscribe_col'] = 'Non Inscrits';
-$lang['nbm_no_user_available_to_send_L1'] = 'Il n\'y a pas d\'utilisateur à notifier par mail.';
-$lang['nbm_no_user_available_to_send_L2'] = 'Un utilisateur est à notifier si de nouveaux éléments sont disponibles pour cet utilisateur.';
-$lang['nbm_title_send'] = 'Sélection des envois';
-$lang['nbm_col_user'] = 'Utilisateur';
-$lang['nbm_col_mail'] = 'email';
-$lang['nbm_col_last_send'] = 'Dernier envoi';
-$lang['nbm_col_check_user_send_mail'] = 'A envoyer ?';
-$lang['nbm_send_options'] = 'Options';
-$lang['nbm_send_complementary_mail_content'] = 'Contenu complémentaire du mail';
-$lang['nbm_send_submit'] = 'Envoyer';
-$lang['nbm_send_check_all'] = 'Tout cocher';
-$lang['nbm_send_uncheck_all'] = 'Tout décocher';
-$lang['nbm_user_change_enabled_true'] = 'L\'utilisateur %s [%s] a été ajouté à la liste des inscrits.';
-$lang['nbm_user_change_enabled_false'] = 'L\'utilisateur %s [%s] a été supprimé de la liste des inscrits.';
-$lang['nbm_user_not_change_enabled_true'] = 'L\'utilisateur %s [%s] n\'a pas été ajouté à la liste des inscrits.';
-$lang['nbm_user_not_change_enabled_false'] = 'L\'utilisateur %s [%s] n\'a pas été supprimé de la liste des inscrits.';
-$lang['nbm_user_change_enabled_updated_data_count'] = '%d utilisateurs ont été mis à jour.';
-$lang['nbm_user_change_enabled_error_on_updated_data_count'] = '%d utilisateurs n\'ont pas été mis à jour.';
 $lang['no_write_access'] = 'pas d\'accès en écriture';
 $lang['order_by'] = 'trier selon';
@@ -304,8 +314,8 @@
 $lang['pictures'] = 'images';
 $lang['private'] = 'privée';
+$lang['properties'] = 'Propriétés';
 $lang['public'] = 'publique';
 $lang['purge history'] = 'purger l\'historique';
 $lang['purge never used notification feeds'] = 'purger les flux de notification jamais utilisés';
-$lang['repair and optimize database'] = 'réparer et optimiser la base de données';
 $lang['purge sessions'] = 'purger les sessions';
 $lang['randomly represented'] = 'représentant au hasard';
@@ -323,4 +333,6 @@
 $lang['remote_site_uncorrect_url'] = 'L\'URL d\'un site distant doit commencer par "http" ou "https" et ne doit contenir des caractères que parmi "/", "a-zA-Z0-9", "-" ou "_"';
 $lang['remove keyword'] = 'supprimer mot-clef';
+$lang['remove tags'] = 'supprimer les tags';
+$lang['repair and optimize database'] = 'réparer et optimiser la base de données';
 $lang['selection'] = 'sélection';
 $lang['set to'] = 'changer en';
@@ -443,9 +455,9 @@
 $lang['user_id URL parameter is missing'] = 'le paramètre d\'URL "user_id" manque';
 $lang['user_status'] = 'Statut de l\'utilisateur';
-$lang['user_status_webmaster'] = 'Webmestre';
 $lang['user_status_admin'] = 'Administrateur';
-$lang['user_status_normal'] = 'Visiteur';
 $lang['user_status_generic'] = 'Générique';
 $lang['user_status_guest'] = 'Invité';
+$lang['user_status_normal'] = 'Visiteur';
+$lang['user_status_webmaster'] = 'Webmestre';
 $lang['username'] = 'nom utilisateur';
 $lang['users'] = 'Utilisateurs';
Index: trunk/language/fr_FR.iso-8859-1/common.lang.php
===================================================================
--- trunk/language/fr_FR.iso-8859-1/common.lang.php	(revision 1118)
+++ trunk/language/fr_FR.iso-8859-1/common.lang.php	(revision 1119)
@@ -40,6 +40,11 @@
 $lang['%d new elements'] = '%d nouveaux éléments';
 $lang['%d new users'] = '%d nouveaux utilisateurs';
+$lang['%d pictures are also linked to current tags'] = '%d images sont également liées aux tags courants';
 $lang['About'] = 'À propos';
+$lang['All tags must match'] = 'Tous les tags doivent correspondre';
+$lang['All tags'] = 'Tous les tags';
+$lang['Any tag'] = 'N\'importe quel tag';
 $lang['At least one listed rule must be satisfied.'] = 'Au moins un des critères doit être satisfait.';
+$lang['At least one tag must match'] = 'Au moins un tag doit correspondre';
 $lang['Author'] = 'Auteur';
 $lang['Average rate'] = 'Note moyenne';
@@ -62,6 +67,6 @@
 $lang['Enter your personnal informations'] = 'Entrer vos informations personnelles';
 $lang['Error sending email'] = 'Erreur à l\'envoi du mail';
+$lang['File name'] = 'Nom du fichier';
 $lang['File'] = 'Fichier';
-$lang['File name'] = 'Nom du fichier';
 $lang['Filesize'] = 'Poids';
 $lang['Filter and display'] = 'Filtrer et afficher';
@@ -91,19 +96,24 @@
 $lang['Profile'] = 'Profil';
 $lang['Quick connect'] = 'Connexion rapide';
+$lang['RSS feed'] = 'flux RSS';
 $lang['Rate'] = 'Note';
-$lang['RSS feed'] = 'flux RSS';
 $lang['Register'] = 'S\'enregistrer';
 $lang['Registration'] = 'Enregistrement';
+$lang['Related tags'] = 'Tags liés';
 $lang['Reset'] = 'Annuler';
 $lang['Retrieve password'] = 'Récupérer un mot de passe';
 $lang['Search rules'] = 'Critères de recherche';
+$lang['Search tags'] = 'Rechercher les tags';
 $lang['Search'] = 'Rechercher';
+$lang['See available tags'] = 'Voir les tags disponibles';
+$lang['See pictures linked to this tag only'] = 'Voir les images liées uniquement à ce tag';
 $lang['Send new password'] = 'Envoyer le nouveau mot de passe';
 $lang['Since'] = 'Depuis';
 $lang['Sort by'] = 'Trier selon';
 $lang['Sort order'] = 'Ordre de tri';
+$lang['Tag'] = 'Tag';
+$lang['Tags'] = 'Tags';
 $lang['The RSS notification feed provides notification on news from this website : new pictures, updated categories, new comments. Use a RSS feed reader.'] = 'Le flux RSS notifie les événements de la galerie : nouvelles images, catégories mises à jour, nouveaux commentaires utilisateur. À utiliser avec un lecteur de flux RSS.';
 $lang['Unknown feed identifier'] = 'Identifiant de flux inconnu';
-$lang['nbm_unknown_identifier'] = 'Identifiants inconnus';
 $lang['User comments'] = 'Commentaires utilisateur';
 $lang['Username'] = 'Nom d\'utilisateur';
@@ -117,4 +127,5 @@
 $lang['add_favorites_hint'] = 'Ajouter cette image à vos favoris';
 $lang['admin'] = 'Administration';
+$lang['adviser_mode_enabled'] = 'Mode conseiller actif';
 $lang['all'] = 'tout';
 $lang['all_categories'] = 'toutes les catégories';
@@ -135,6 +146,6 @@
 $lang['calendar_picture_hint'] = 'afficher les images du ';
 $lang['calendar_view'] = 'Vue';
+$lang['chronology_monthly_calendar'] = 'Calendrier mensuel';
 $lang['chronology_monthly_list'] = 'Liste mensuelle';
-$lang['chronology_monthly_calendar'] = 'Calendrier mensuel';
 $lang['chronology_weekly_list'] = 'Liste hebdomadaire';
 $lang['click_to_redirect'] = 'Cliquez ici si votre navigateur ne vous redirige pas.';
@@ -211,6 +222,6 @@
 $lang['maxwidth'] = 'Largeur maximum des images';
 $lang['maxwidth_error'] = 'La largeur des images doit être supérieure à 50';
+$lang['mode_created_hint'] = 'afficher un calendrier par date de création';
 $lang['mode_normal_hint'] = 'retourne à la vue normale';
-$lang['mode_created_hint'] = 'afficher un calendrier par date de création';
 $lang['mode_posted_hint'] = 'afficher un calendrier par date d\'ajout';
 $lang['month'][10] = 'Octobre';
@@ -228,8 +239,9 @@
 $lang['most_visited_cat'] = 'Plus vues';
 $lang['most_visited_cat_hint'] = 'afficher les images les plus vues';
+$lang['nb_image_line_error'] = 'Le nombre d\'images par ligne doit être un entier non nul';
 $lang['nb_image_per_row'] = 'Nombre de miniatures par ligne';
-$lang['nb_image_line_error'] = 'Le nombre d\'images par ligne doit être un entier non nul';
+$lang['nb_line_page_error'] = 'Le nombre de lignes par page doit être un entier non nul';
 $lang['nb_row_per_page'] = 'Nombre de lignes par page';
-$lang['nb_line_page_error'] = 'Le nombre de lignes par page doit être un entier non nul';
+$lang['nbm_unknown_identifier'] = 'Identifiants inconnus';
 $lang['never_rated'] = 'Vous n\'avez jamais voté pour cette image';
 $lang['new_password'] = 'Nouveau mot de passe';
@@ -333,4 +345,3 @@
 $lang['w_month'] = 'Mois';
 $lang['yes'] = 'Oui';
-$lang['adviser_mode_enabled'] = 'Mode conseiller actif';
 ?>
Index: trunk/picture.php
===================================================================
--- trunk/picture.php	(revision 1118)
+++ trunk/picture.php	(revision 1119)
@@ -699,23 +699,41 @@
 $infos['INFO_FILE'] = $picture['current']['file'];
 
-// keywords
-if (!empty($picture['current']['keywords']))
-{
-  $infos['INFO_KEYWORDS'] =
-    // FIXME because of search engine partial rewrite, giving the author
-    // name threw GET is not supported anymore. This feature should come
-    // back later, with a better design (tag classification).
-//     preg_replace(
-//       '/([^,]+)/',
-//       '<a href="'.
-//         PHPWG_ROOT_PATH.'category.php?cat=search&amp;search=keywords:$1'
-//         .'">$1</a>',
-//       $picture['current']['keywords']
-//       );
-    $picture['current']['keywords'];
+// tags
+$query = '
+SELECT id, name, url_name
+  FROM '.IMAGE_TAG_TABLE.'
+    INNER JOIN '.TAGS_TABLE.' ON tag_id = id
+  WHERE image_id = '.$page['image_id'].'
+;';
+$result = pwg_query($query);
+
+if (mysql_num_rows($result) > 0)
+{
+  $tags = array();
+  
+  while ($row = mysql_fetch_array($result))
+  {
+    array_push(
+      $tags,
+      '<a href="'
+      .make_index_URL(
+        array(
+          'tags' => array(
+            array(
+              'id' => $row['id'],
+              'url_name' => $row['url_name'],
+              ),
+            )
+          )
+        )
+      .'">'.$row['name'].'</a>'
+      );
+  }
+
+  $infos['INFO_TAGS'] = implode(', ', $tags);
 }
 else
 {
-  $infos['INFO_KEYWORDS'] = l10n('N/A');
+  $infos['INFO_TAGS'] = l10n('N/A');
 }
 
Index: trunk/search.php
===================================================================
--- trunk/search.php	(revision 1118)
+++ trunk/search.php	(revision 1119)
@@ -66,4 +66,12 @@
   }
 
+  if (isset($_POST['tags']))
+  {
+    $search['fields']['tags'] = array(
+      'words' => $_POST['tags'],
+      'mode'  => $_POST['tag_mode'],
+      );
+  }
+  
   if ($_POST['search_author'])
   {
@@ -201,4 +209,14 @@
   'U_HELP' => PHPWG_ROOT_PATH.'/popuphelp.php?page=search',
   'U_HOME' => make_index_url(),
+
+  'TAG_SELECTION' => get_html_tag_selection(
+    get_available_tags(
+      isset($user['forbidden_categories'])
+      ? explode(',', $user['forbidden_categories'])
+      : null
+      ),
+    'tags',
+    isset($_POST['tags']) ? $_POST['tags'] : array()
+    ),
   )
 );
Index: trunk/search_rules.php
===================================================================
--- trunk/search_rules.php	(revision 1118)
+++ trunk/search_rules.php	(revision 1119)
@@ -84,4 +84,32 @@
 }
 
+if (isset($search['fields']['tags']))
+{
+  $template->assign_block_vars(
+    'tags',
+    array(
+      'LIST_INTRO' => ($search['fields']['tags']['mode'] == 'AND')
+        ? l10n('All tags must match')
+        : l10n('At least one tag must match')
+      )
+    );
+
+  $query = '
+SELECT name
+  FROM '.TAGS_TABLE.'
+  WHERE id IN ('.implode(',', $search['fields']['tags']['words']).')
+;';
+  $result = pwg_query($query);
+  while ($row = mysql_fetch_array($result))
+  {
+    $template->assign_block_vars(
+      'tags.tag',
+      array(
+        'NAME' => $row['name'],
+        )
+      );
+  }
+}
+
 if (isset($search['fields']['author']))
 {
Index: trunk/tags.php
===================================================================
--- trunk/tags.php	(revision 1119)
+++ trunk/tags.php	(revision 1119)
@@ -0,0 +1,120 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | PhpWebGallery - a PHP based picture gallery                           |
+// | Copyright (C) 2002-2003 Pierrick LE GALL - pierrick@phpwebgallery.net |
+// | Copyright (C) 2003-2005 PhpWebGallery Team - http://phpwebgallery.net |
+// +-----------------------------------------------------------------------+
+// | branch        : BSF (Best So Far)
+// | file          : $RCSfile$
+// | last update   : $Date: 2006-03-22 02:01:47 +0100 (mer, 22 mar 2006) $
+// | last modifier : $Author: rvelices $
+// | revision      : $Revision: 1092 $
+// +-----------------------------------------------------------------------+
+// | This program is free software; you can redistribute it and/or modify  |
+// | it under the terms of the GNU General Public License as published by  |
+// | the Free Software Foundation                                          |
+// |                                                                       |
+// | This program is distributed in the hope that it will be useful, but   |
+// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
+// | General Public License for more details.                              |
+// |                                                                       |
+// | You should have received a copy of the GNU General Public License     |
+// | along with this program; if not, write to the Free Software           |
+// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
+// | USA.                                                                  |
+// +-----------------------------------------------------------------------+
+
+// +-----------------------------------------------------------------------+
+// |                             functions                                 |
+// +-----------------------------------------------------------------------+
+
+function counter_compare($a, $b)
+{
+  if ($a['counter'] == $b['counter'])
+  {
+    return tag_id_compare($a, $b);
+  }
+
+  return ($a['counter'] < $b['counter']) ? -1 : 1;
+}
+
+function tag_id_compare($a, $b)
+{
+  return ($a['tag_id'] < $b['tag_id']) ? -1 : 1;
+}
+
+// +-----------------------------------------------------------------------+
+// |                           initialization                              |
+// +-----------------------------------------------------------------------+
+
+define('PHPWG_ROOT_PATH','./');
+include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
+
+check_status(ACCESS_GUEST);
+
+// +-----------------------------------------------------------------------+
+// |                       page header and options                         |
+// +-----------------------------------------------------------------------+
+
+$title= l10n('Tags');
+$page['body_id'] = 'theTagsPage';
+include(PHPWG_ROOT_PATH.'include/page_header.php');
+
+$template->set_filenames(array('tags'=>'tags.tpl'));
+$template->assign_vars(
+  array(
+    'U_HOME' => make_index_url(),
+    )
+  );
+
+// +-----------------------------------------------------------------------+
+// |                        tag cloud construction                         |
+// +-----------------------------------------------------------------------+
+
+// find all tags available for the current user
+$tags = get_available_tags(explode(',', $user['forbidden_categories']));
+
+// we want only the first most represented tags, so we sort them by counter
+// and take the first tags
+usort($tags, 'counter_compare');
+$tags = array_slice($tags, 0, $conf['full_tag_cloud_items_number']);
+
+// depending on its counter and the other tags counter, each tag has a level
+$tags = add_level_to_tags($tags);
+
+// we want tags diplayed in alphabetic order
+usort($tags, 'name_compare');
+
+// display sorted tags
+foreach ($tags as $tag)
+{
+  $template->assign_block_vars(
+    'tag',
+    array(
+      'URL' => make_index_url(
+        array(
+          'tags' => array(
+            array(
+              'id' => $tag['tag_id'],
+              'url_name' => $tag['url_name'],
+              ),
+            ),
+          )
+        ),
+      
+      'NAME' => $tag['name'],
+      'TITLE' => $tag['counter'],
+      'CLASS' => 'tagLevel'.$tag['level'],
+      )
+    );
+}
+
+// +-----------------------------------------------------------------------+
+// |                           html code display                           |
+// +-----------------------------------------------------------------------+
+
+$template->assign_block_vars('title',array());
+$template->parse('tags');
+include(PHPWG_ROOT_PATH.'include/page_tail.php');
+?>
Index: trunk/template/yoga/admin.tpl
===================================================================
--- trunk/template/yoga/admin.tpl	(revision 1118)
+++ trunk/template/yoga/admin.tpl	(revision 1119)
@@ -55,4 +55,5 @@
 	<li><a href="{U_RATING}">{lang:Rating}</a></li>
 	<li><a href="{U_CADDIE}">{lang:Caddie}</a></li>
+	<li><a href="{U_TAGS}">{lang:Tags}</a></li>
       </ul>
     </dd>
Index: trunk/template/yoga/admin/element_set_global.tpl
===================================================================
--- trunk/template/yoga/admin/element_set_global.tpl	(revision 1118)
+++ trunk/template/yoga/admin/element_set_global.tpl	(revision 1119)
@@ -84,19 +84,13 @@
 
     <tr>
-      <td>{lang:add keywords}</td>
-      <td><input type="text" name="add_keywords" value="" /></td>
+      <td>{lang:add tags}</td>
+      <td>{ADD_TAG_SELECTION}</td>
     </tr>
 
     <tr>
-      <td>{lang:remove keyword}</td>
-      <td>
-        <select name="remove_keyword">
-          <!-- BEGIN remove_keyword_option -->
-          <option value="{remove_keyword_option.VALUE}">{remove_keyword_option.OPTION}</option>
-          <!-- END remove_keyword_option -->
-        </select>
-      </td>
+      <td>{lang:remove tags}</td>
+      <td>{DEL_TAG_SELECTION}</td>
     </tr>
-
+    
     <tr>
       <td>{lang:author}</td>
Index: trunk/template/yoga/admin/element_set_unit.tpl
===================================================================
--- trunk/template/yoga/admin/element_set_unit.tpl	(revision 1118)
+++ trunk/template/yoga/admin/element_set_unit.tpl	(revision 1119)
@@ -70,6 +70,6 @@
 
     <tr>
-      <td><strong>{lang:Keywords}</strong></td>
-      <td><input type="text" name="keywords-{element.ID}" value="{element.KEYWORDS}" size="50" /></td>
+      <td><strong>{lang:Tags}</strong></td>
+      <td>{element.TAG_SELECTION}</td>
     </tr>
 
Index: trunk/template/yoga/admin/picture_modify.tpl
===================================================================
--- trunk/template/yoga/admin/picture_modify.tpl	(revision 1118)
+++ trunk/template/yoga/admin/picture_modify.tpl	(revision 1119)
@@ -100,8 +100,9 @@
 
       <tr>
-        <td><strong>{lang:Keywords}</strong></td>
-        <td><input type="text" name="keywords" value="{KEYWORDS}" size="50" /></td>
+        <td><strong>{lang:Tags}</strong></td>
+        <td>{TAG_SELECTION}</td>
       </tr>
 
+      
       <tr>
         <td><strong>{lang:Description}</strong></td>
Index: trunk/template/yoga/admin/tags.tpl
===================================================================
--- trunk/template/yoga/admin/tags.tpl	(revision 1119)
+++ trunk/template/yoga/admin/tags.tpl	(revision 1119)
@@ -0,0 +1,55 @@
+<div class="titrePage">
+  <h2>{lang:Manage tags}</h2>
+</div>
+
+<form action="{F_ACTION}" method="post">
+
+  <!-- BEGIN edit_tags -->
+  <input type="hidden" name="edit_list" value="{edit_tags.LIST}" />
+
+  <fieldset>
+    <legend>{lang:Edit tags}</legend>
+
+    <table class="table2">
+      <tr class="throw">
+        <th>{lang:Current name}</th>
+        <th>{lang:New name}</th>
+      </tr>
+      <!-- BEGIN tag -->
+      <tr>
+        <td>{edit_tags.tag.NAME}</td>
+        <td><input type="text" name="tag_name-{edit_tags.tag.ID}" value="{edit_tags.tag.NAME}" /></td>
+      </tr>
+      <!-- END tag -->
+    </table>
+
+    <p>
+      <input type="submit" name="submit" value="{lang:Submit}" {TAG_INPUT_ENABLED} />
+      <input type="reset" value="{lang:Reset}" />
+    </p>
+  </fieldset>
+  <!-- END edit_tags -->
+
+  <fieldset>
+    <legend>{lang:Add a tag}</legend>
+
+    <label>
+      {lang:New tag}
+      <input type="text" name="add_tag" />
+    </label>
+    
+    <p><input type="submit" name="add" value="{lang:Submit}" {TAG_INPUT_ENABLED}/></p>
+  </fieldset>
+
+  <fieldset>
+    <legend>{lang:Tag selection}</legend>
+    
+    {TAG_SELECTION}
+
+    <p>
+      <input type="submit" name="edit" value="{lang:Edit selected tags}" {TAG_INPUT_ENABLED}/>
+      <input type="submit" name="delete" value="{lang:Delete selected tags}" {TAG_INPUT_ENABLED}/>
+    </p>
+  </fieldset>
+
+</form>
Index: trunk/template/yoga/content.css
===================================================================
--- trunk/template/yoga/content.css	(revision 1118)
+++ trunk/template/yoga/content.css	(revision 1119)
@@ -14,4 +14,5 @@
 BODY#thePasswordPage #content,
 BODY#theNotificationPage #content,
+BODY#theTagsPage #content,
 BODY#theNBMPage #content
 {
Index: trunk/template/yoga/default-layout.css
===================================================================
--- trunk/template/yoga/default-layout.css	(revision 1118)
+++ trunk/template/yoga/default-layout.css	(revision 1119)
@@ -262,2 +262,11 @@
 }
 
+UL.tagSelection {
+  width: 500px;
+  padding: 0;
+}
+
+UL.tagSelection LI {
+  display: inline;
+  white-space: nowrap;
+}
Index: trunk/template/yoga/index.tpl
===================================================================
--- trunk/template/yoga/index.tpl	(revision 1118)
+++ trunk/template/yoga/index.tpl	(revision 1119)
@@ -20,4 +20,21 @@
   </dd>
 </dl>
+
+<!-- BEGIN tags -->
+<dl>
+  <dt>{lang:Related tags}</dt>
+  <dd>
+    <ul id="menuTagCloud">
+      <!-- BEGIN tag -->
+      <li>
+        <a href="{tags.tag.URL_ADD}" title="{tags.tag.TITLE_ADD}"><img src="{pwg_root}{themeconf:icon_dir}/add_tag.png" alt="+"></a>
+        <a href="{tags.tag.URL}" class="{tags.tag.CLASS}" title="{tags.tag.TITLE}">{tags.tag.NAME}</a>
+      </li>
+      <!-- END tag -->
+    </ul>
+  </dd>
+</dl>
+<!-- END tags -->
+
 <dl>
   <dt>{lang:special_categories}</dt>
Index: trunk/template/yoga/menubar.css
===================================================================
--- trunk/template/yoga/menubar.css	(revision 1118)
+++ trunk/template/yoga/menubar.css	(revision 1119)
@@ -96,2 +96,11 @@
 }
 
+#menubar #menuTagCloud {
+  text-align: center;
+  margin: 5px 0;
+}
+
+#menubar #menuTagCloud LI
+{
+  display: inline;
+}
Index: trunk/template/yoga/picture.tpl
===================================================================
--- trunk/template/yoga/picture.tpl	(revision 1118)
+++ trunk/template/yoga/picture.tpl	(revision 1119)
@@ -108,6 +108,6 @@
   </tr>
   <tr>
-    <td class="label">{lang:Keywords}</td>
-    <td class="value">{INFO_KEYWORDS}</td>
+    <td class="label">{lang:Tags}</td>
+    <td class="value">{INFO_TAGS}</td>
   </tr>
   <tr>
Index: trunk/template/yoga/search.tpl
===================================================================
--- trunk/template/yoga/search.tpl	(revision 1118)
+++ trunk/template/yoga/search.tpl	(revision 1119)
@@ -36,4 +36,14 @@
 	</td>
   </tr>
+  
+  <tr>
+    <td colspan="2"><b>{lang:Search tags} :</b></td>
+    <td colspan="2" valign="middle">
+      {TAG_SELECTION}
+      <br /><label><input type="radio" name="tag_mode" value="AND" checked="checked" /> {lang:All tags}</label>
+      <br /><label><input type="radio" name="tag_mode" value="OR" /> {lang:Any tag}</label>
+    </td>
+  </tr>
+  
   <tr>
     <td colspan="2"><b>{L_SEARCH_DATE} :</b>
Index: trunk/template/yoga/search_rules.tpl
===================================================================
--- trunk/template/yoga/search_rules.tpl	(revision 1118)
+++ trunk/template/yoga/search_rules.tpl	(revision 1119)
@@ -10,4 +10,16 @@
   <!-- END words -->
 
+  <!-- BEGIN tags -->
+  <li>
+    <p>{tags.LIST_INTRO}</p>
+
+    <ul>
+      <!-- BEGIN tag -->
+      <li>{tags.tag.NAME}</li>
+      <!-- END tag -->
+    </ul>
+  </li>
+  <!-- END tags -->
+  
   <!-- BEGIN author -->
   <li>{author.CONTENT}</li>
Index: trunk/template/yoga/tags.tpl
===================================================================
--- trunk/template/yoga/tags.tpl	(revision 1119)
+++ trunk/template/yoga/tags.tpl	(revision 1119)
@@ -0,0 +1,17 @@
+<!-- $Id: comments.tpl 960 2005-12-03 17:33:38Z chrisaga $ -->
+<div id="content">
+
+  <div class="titrePage">
+    <ul class="categoryActions">
+      <li><a href="{U_HOME}" title="{lang:return to homepage}"><img src="{themeconf:icon_dir}/home.png" class="button" alt="{lang:home}"/></a></li>
+    </ul>
+    <h2>{lang:Tags}</h2>
+  </div>
+
+  <ul id="fullTagCloud">
+    <!-- BEGIN tag -->
+    <li><a href="{tag.URL}" class="{tag.CLASS}" title="{tag.TITLE}">{tag.NAME}</a></li>
+    <!-- END tag -->
+  </ul>
+
+</div> <!-- content -->
