aboutsummaryrefslogtreecommitdiffstats
path: root/BSF/tools
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--BSF/tools/config_local.inc.php8
-rw-r--r--BSF/tools/create_listing_file.php1696
-rw-r--r--BSF/tools/create_listing_file_local.inc.php14
-rw-r--r--BSF/tools/fill_history.pl336
-rw-r--r--BSF/tools/index.php30
-rw-r--r--BSF/tools/local-layout.css10
-rw-r--r--BSF/tools/metadata.php97
-rw-r--r--BSF/tools/prototype.js3277
-rw-r--r--BSF/tools/pwg_rel_create.sh61
-rw-r--r--BSF/tools/release_creation.readme50
-rw-r--r--BSF/tools/ws.htm402
11 files changed, 5981 insertions, 0 deletions
diff --git a/BSF/tools/config_local.inc.php b/BSF/tools/config_local.inc.php
new file mode 100644
index 000000000..6e74538fb
--- /dev/null
+++ b/BSF/tools/config_local.inc.php
@@ -0,0 +1,8 @@
+<?php
+// this file is provided as an example. It does not modify the configuration
+// as long as it remains in "tools" directory. Move it to "include"
+// directory if you want to modify default configuration.
+
+$conf['prefix_thumbnail'] = 'thumb_';
+$conf['show_gt'] = true;
+?> \ No newline at end of file
diff --git a/BSF/tools/create_listing_file.php b/BSF/tools/create_listing_file.php
new file mode 100644
index 000000000..8b1ee34ee
--- /dev/null
+++ b/BSF/tools/create_listing_file.php
@@ -0,0 +1,1696 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Piwigo - a PHP based picture gallery |
+// +-----------------------------------------------------------------------+
+// | Copyright(C) 2008 Piwigo Team http://piwigo.org |
+// | Copyright(C) 2003-2008 PhpWebGallery Team http://phpwebgallery.net |
+// | Copyright(C) 2002-2003 Pierrick LE GALL http://le-gall.net/pierrick |
+// +-----------------------------------------------------------------------+
+// | 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. |
+// +-----------------------------------------------------------------------+
+
+// +-----------------------------------------------------------------------+
+// | User configuration |
+// +-----------------------------------------------------------------------+
+
+// ****** Gallery configuration ****** //
+// Script version
+$conf['version'] = 'branch 1.7';
+
+// URL of main gallery
+// Example : http://www.my.domain/my/directory
+$conf['gallery'] = 'http://demo.piwigo.net/';
+
+// prefix for thumbnails in "thumbnail" sub directories
+$conf['prefix_thumbnail'] = 'TN-';
+
+// $conf['file_ext'] lists all extensions (case insensitive) allowed
+// for your Piwigo installation
+$conf['file_ext'] = array('jpg','JPG','jpeg','JPEG',
+ 'png','PNG','gif','GIF','mpg','zip',
+ 'avi','mp3','ogg');
+
+
+// $conf['picture_ext'] must be a subset of $conf['file_ext']
+$conf['picture_ext'] = array('jpg','JPG','jpeg','JPEG',
+ 'png','PNG','gif','GIF');
+
+// ****** Time limitation functionality ****** //
+// max execution time before refresh in seconds
+$conf['max_execution_time'] = (5*ini_get('max_execution_time'))/6; // 25 seconds with default PHP configuration
+// force the use of refresh method
+// in order to have live informations
+// or
+// to fix system witch are not safe mode but not autorized set_time_limit
+$conf['force_refresh_method'] = true;
+
+// refresh delay is seconds
+$conf['refresh_delay'] = 0;
+
+// ****** EXIF support functionality ****** //
+// $conf['use_exif'] set to true if you want to use Exif information
+$conf['use_exif'] = true;
+
+// use_exif_mapping: same behaviour as use_iptc_mapping
+$conf['use_exif_mapping'] = array(
+ 'date_creation' => 'DateTimeOriginal'
+ );
+
+// ****** IPTC support functionality ****** //
+// $conf['use_iptc'] set to true if you want to use IPTC informations of the
+// element according to get_sync_iptc_data function mapping, otherwise, set
+// to false
+$conf['use_iptc'] = false;
+
+// use_iptc_mapping : in which IPTC fields will Piwigo find image
+// information ? This setting is used during metadata synchronisation. It
+// associates a piwigo_images column name to a IPTC key
+$conf['use_iptc_mapping'] = array(
+ 'keywords' => '2#025',
+ 'date_creation' => '2#055',
+ 'author' => '2#122',
+ 'name' => '2#005',
+ 'comment' => '2#120');
+
+// ****** Directory protection functionality ****** //
+// Define if directories have to be protected if they are not
+$conf['protect'] = false;
+
+// true/false : show/hide warnings
+$conf['protect_warnings'] = true;
+
+// ****** Thumbnails generation functionality ****** //
+// Define if images have to be reduced if they are not
+$conf['thumbnail'] = false;
+
+// Define method to generate thumbnails :
+// - fixed (width and height required);
+// - width (only width required);
+// - height (only height required);
+// - ratio (only ratio is required)
+// - exif (no other parameter required)
+$conf['thumbnail_method'] = 'ratio';
+
+// Height in pixels (greater than 0)
+$conf['thumbnail_height'] = 128;
+
+// Width in pixels (greater than 0)
+$conf['thumbnail_width'] = 128;
+
+// Ratio between original and thumbnail size (strictly between 0 and 1)
+$conf['thumbnail_ratio'] = 0.2;
+
+// Define thumbnail format : jpeg, png or gif (will be verified)
+$conf['thumbnail_format'] = 'jpeg';
+
+// ****** Directory mapping ****** //
+// directories names
+$conf['thumbs'] = 'thumbnail'; // thumbnails
+$conf['high'] = 'pwg_high'; // high resolution
+$conf['represent'] = 'pwg_representative'; // non pictures representative files
+
+
+// +-----------------------------------------------------------------------+
+// | Overload configurations |
+// +-----------------------------------------------------------------------+
+@include(dirname(__FILE__).'/'.basename(__FILE__, '.php').'_local.inc.php');
+
+
+// +-----------------------------------------------------------------------+
+// | Advanced script configuration |
+// +-----------------------------------------------------------------------+
+
+// url of icon directory in yoga template
+$pwg_conf['icon_dir'] = $conf['gallery'].'/template/yoga/icon/';
+
+// list of actions managed by this script
+$pwg_conf['scan_action'] = array('clean', 'test', 'generate');
+
+// url of this script
+$pwg_conf['this_url'] =
+ (empty($_SERVER['HTTPS']) ? 'http://' : 'https://')
+ .str_replace(':'.$_SERVER['SERVER_PORT'], '', $_SERVER['HTTP_HOST'])
+ .($_SERVER['SERVER_PORT'] != 80 ? ':'.$_SERVER['SERVER_PORT'] : '')
+ .$_SERVER['PHP_SELF'];
+
+// list of reserved directory names
+$pwg_conf['reserved_directory_names'] = array($conf['thumbs'], $conf['high'], $conf['represent'], ".", "..", ".svn");
+
+// content of index.php generated in protect action
+$pwg_conf['protect_content'] = '<?php header("Location: '.$conf['gallery'].'") ?>';
+
+// backup of PHP safe_mode INI parameter (used for time limitation)
+$pwg_conf['safe_mode'] = (ini_get('safe_mode') == '1') ? true : false;
+
+// This parameter will be fixed in pwg_init()
+$pwg_conf['gd_version_major'] = '';
+$pwg_conf['gd_version_full'] = '';
+$pwg_conf['gd_supported_format'] = array();
+
+// +-----------------------------------------------------------------------+
+// | Functions |
+// +-----------------------------------------------------------------------+
+
+/**
+ * write line in log file
+ *
+ * @param string line
+ * @return string
+ */
+function pwg_log($line)
+{
+ $log_file = fopen(__FILE__.'.log', 'a');
+ fwrite($log_file, $line);
+ fclose($log_file);
+}
+
+/**
+ * Check web server graphical capabilities
+ *
+ * @return string
+ */
+function pwg_check_graphics()
+{
+ //~ pwg_log('>>>>> pwg_check_graphics() >>>>>'."\n");
+
+ global $conf, $pwg_conf;
+ $log = '';
+
+ // Verify gd library for thumbnail generation
+ if ($conf['thumbnail'] and !is_callable('gd_info'))
+ {
+ $log .= ' <code class="warning">Warning -</code> Your server can not generate thumbnails. Thumbnail creation switched off.<br />'."\n";
+ // Switch off thumbnail generation
+ $conf['thumbnail'] = false;
+ return $log;
+ }
+
+ // Verify thumnail format
+ if ($conf['thumbnail'])
+ {
+ $info = gd_info();
+
+ // Backup GD major version
+ $pwg_conf['gd_version_full'] = ereg_replace('[[:alpha:][:space:]()]+', '', $info['GD Version']);
+ list($pwg_conf['gd_version_major']) = preg_split('/[.]+/', $pwg_conf['gd_version_full']);
+
+ // Backup input/output format support
+ array_push($pwg_conf['gd_supported_format'], $info['JPG Support'] ? 'jpeg' : NULL);
+ array_push($pwg_conf['gd_supported_format'], $info['PNG Support'] ? 'png' : NULL);
+ array_push($pwg_conf['gd_supported_format'], ($info['GIF Read Support'] and $info['GIF Create Support']) ? 'gif' : NULL);
+
+ // Check output format support
+ if (!in_array($conf['thumbnail_format'], $pwg_conf['gd_supported_format']))
+ {
+ $log .= ' <code class="warning">Warning -</code> Your server does not support thumbnail\'s <code>';
+ $log .= $conf['thumbnail_format'].'</code> format. Thumbnail creation switched off.<br />'."\n";
+ }
+
+ switch ($conf['thumbnail_method'])
+ {
+ case 'exif':
+ {
+ // exif_thumbnail() must be callable
+ if (!is_callable('exif_thumbnail'))
+ {
+ $log .= ' <code class="warning">Warning -</code> Your server does not support thumbnail creation through EXIF datas. Thumbnail creation switched off.<br />'."\n";
+ }
+ break;
+ }
+ case 'fixed':
+ {
+ // $conf['thumbnail_width'] > 0
+ if (!is_numeric($conf['thumbnail_width']) or $conf['thumbnail_width'] <= 0)
+ {
+ $log .= ' <code class="failure">Failure -</code> Bad value <code>thumbnail_width = ';
+ $log .= var_export($conf['thumbnail_width'], true).'</code>. Thumbnail creation switched off.<br />'."\n";
+ }
+ // $conf['thumbnail_height'] > 0
+ if (!is_numeric($conf['thumbnail_height']) or $conf['thumbnail_height'] <= 0)
+ {
+ $log .= ' <code class="failure">Failure -</code> Bad value <code>thumbnail_height = ';
+ $log .= var_export($conf['thumbnail_height'], true).'</code>. Thumbnail creation switched off.<br />'."\n";
+ }
+ break;
+ }
+ case 'ratio':
+ {
+ // 0 < $conf['thumbnail_ratio'] < 1
+ if (!is_numeric($conf['thumbnail_ratio']) or $conf['thumbnail_ratio'] <= 0 or $conf['thumbnail_ratio'] >= 1)
+ {
+ $log .= ' <code class="failure">Failure -</code> Bad value <code>thumbnail_ratio = ';
+ $log .= var_export($conf['thumbnail_ratio'], true).'</code>. Thumbnail creation switched off.<br />'."\n";
+ }
+ break;
+ }
+ case 'width':
+ {
+ // $conf['thumbnail_width'] > 0
+ if (!is_numeric($conf['thumbnail_width']) or $conf['thumbnail_width'] <= 0)
+ {
+ $log .= ' <code class="failure">Failure -</code> Bad value <code>thumbnail_width = ';
+ $log .= var_export($conf['thumbnail_width'], true).'</code>. Thumbnail creation switched off.<br />'."\n";
+ }
+ break;
+ }
+ case 'height':
+ {
+ // $conf['thumbnail_height'] > 0
+ if (!is_numeric($conf['thumbnail_height']) or $conf['thumbnail_height'] <= 0)
+ {
+ $log .= ' <code class="failure">Failure -</code> Bad value <code>thumbnail_height = ';
+ $log .= var_export($conf['thumbnail_height'], true).'</code>. Thumbnail creation switched off.<br />'."\n";
+ }
+ break;
+ }
+ default:
+ {
+ // unknown method
+ $log .= ' <code class="failure">Failure -</code> Bad value <code>thumbnail_method = ';
+ $log .= var_export($conf['thumbnail_method'], true).'</code>. Thumbnail creation switched off.<br />'."\n";
+ break;
+ }
+ }
+
+ if (strlen($log))
+ {
+ $conf['thumbnail'] = false;
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_check_graphics() returns '.var_export($log, TRUE).' <<<<<'."\n");
+ return $log;
+}
+
+/**
+ * returns xml </dirX> lines
+ *
+ * @param integer $dir_start
+ * @param integer $dir_number
+ * @return string
+ */
+function pwg_close_level($dir_start, $dir_number)
+{
+ //~ pwg_log('>>>>> pwg_close_level($dir_start = '.var_export($dir_start, TRUE).', $dir_number = '.var_export($dir_number, TRUE).') >>>>>'."\n");
+
+ $lines ='';
+ do
+ {
+ $lines .= str_repeat(' ', 2*$dir_start).'</dir'.$dir_start.">\n";
+ $dir_number--;
+ $dir_start--;
+ }
+ while(($dir_number > 0) && ($dir_start >= 0));
+
+ //~ pwg_log('<<<<< pwg_close_level returns '.var_export($lines, TRUE).' <<<<<'."\n");
+ return $lines;
+}
+
+/**
+ * return a cleaned IPTC value
+ *
+ * @param string value
+ * @return string
+ */
+function pwg_clean_iptc_value($value)
+{
+ //~ pwg_log('>>>>> pwg_clean_iptc_value ($value = '.var_export($value, TRUE).') >>>>>'."\n");
+
+ // strip leading zeros (weird Kodak Scanner software)
+ while (isset($value[0]) and $value[0] == chr(0))
+ {
+ $value = substr($value, 1);
+ }
+ // remove binary nulls
+ $value = str_replace(chr(0x00), ' ', $value);
+
+ //~ pwg_log('<<<<< pwg_clean_iptc_value() returns '.var_export($value, TRUE).' <<<<<'."\n");
+ return $value;
+}
+
+/**
+ * returns informations from IPTC metadata, mapping is done at the beginning
+ * of the function
+ *
+ * @param string $filename
+ * @param string $map
+ * @return array
+ */
+function pwg_get_iptc_data($filename, $map)
+{
+ //~ pwg_log('>>>>> pwg_get_iptc_data ($filename = '.var_export($filename, TRUE).', $map = '.var_export($map, TRUE).') >>>>>'."\n");
+
+ $result = array();
+
+ // Read IPTC data
+ $iptc = array();
+
+ $imginfo = array();
+ getimagesize($filename, $imginfo);
+
+ if (isset($imginfo['APP13']))
+ {
+ $iptc = iptcparse($imginfo['APP13']);
+ if (is_array($iptc))
+ {
+ $rmap = array_flip($map);
+ foreach (array_keys($rmap) as $iptc_key)
+ {
+ if (isset($iptc[$iptc_key][0]))
+ {
+ if ($iptc_key == '2#025')
+ {
+ $value = implode(',', array_map('pwg_clean_iptc_value', $iptc[$iptc_key]));
+ }
+ else
+ {
+ $value = pwg_clean_iptc_value($iptc[$iptc_key][0]);
+ }
+
+ foreach (array_keys($map, $iptc_key) as $pwg_key)
+ {
+ $result[$pwg_key] = $value;
+ }
+ }
+ }
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_get_iptc_data() returns '.var_export($result, TRUE).' <<<<<'."\n");
+ return $result;
+}
+
+/**
+ * returns informations from IPTC metadata
+ *
+ * @param string $file
+ * @return array iptc
+ */
+function pwg_get_sync_iptc_data($file)
+{
+ //~ pwg_log('>>>>> pwg_get_sync_iptc_data ($file = '.var_export($file, TRUE).') >>>>>'."\n");
+
+ global $conf;
+
+ $map = $conf['use_iptc_mapping'];
+ $datefields = array('date_creation', 'date_available');
+
+ $iptc = pwg_get_iptc_data($file, $map);
+
+ foreach ($iptc as $pwg_key => $value)
+ {
+ if (in_array($pwg_key, $datefields))
+ {
+ if ( preg_match('/(\d{4})(\d{2})(\d{2})/', $value, $matches))
+ {
+ $value = $matches[1].'-'.$matches[2].'-'.$matches[3];
+ }
+ }
+ if ($pwg_key == 'keywords')
+ {
+ // official keywords separator is the comma
+ $value = preg_replace('/[.;]/', ',', $value);
+ $value = preg_replace('/^,+|,+$/', '', $value);
+ }
+ $iptc[$pwg_key] = htmlentities($value);
+ }
+
+ $iptc['keywords'] = isset($iptc['keywords']) ? implode(',', array_unique(explode(',', $iptc['keywords']))) : NULL;
+
+ //~ pwg_log('<<<<< pwg_get_sync_iptc_data() returns '.var_export($iptc, TRUE).' <<<<<'."\n");
+ return $iptc;
+}
+
+/**
+ * return extension of the representative file
+ *
+ * @param string $file_dir
+ * @param string $file_short
+ * @return string
+ */
+function pwg_get_representative_ext($file_dir, $file_short)
+{
+ //~ pwg_log('>>>>> pwg_get_representative_ext($file_dir = '.var_export($file_dir, TRUE).', $file_short = '.var_export($file_short, TRUE).') >>>>>'."\n");
+
+ global $conf;
+
+ $rep_ext = '';
+ foreach ($conf['picture_ext'] as $ext)
+ {
+ if (file_exists($file_dir.'/'.$conf['represent'].'/'.$file_short.'.'.$ext))
+ {
+ $rep_ext = $ext;
+ break;
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_get_representative_ext() returns '.var_export($rep_ext, TRUE).' <<<<<'."\n");
+ return $rep_ext;
+}
+
+/**
+ * return 'true' if high resolution picture exists else ''
+ *
+ * @param string $file_dir
+ * @param string $file_base
+ * @return boolean
+ */
+function pwg_get_high($file_dir, $file_base)
+{
+ //~ pwg_log('>>>>> pwg_get_high($file = '.var_export($file_dir, TRUE).', $line = '.var_export($file_base, TRUE).') >>>>>'."\n");
+
+ global $conf;
+
+ $high = false;
+ if (file_exists($file_dir.'/'.$conf['high'].'/'.$file_base))
+ {
+ $high = true;
+ }
+
+ //~ pwg_log('<<<<< pwg_get_high() returns '.var_export($high, TRUE).' <<<<<'."\n");
+ return $high;
+}
+
+/**
+ * return filename without extension
+ *
+ * @param string $filename
+ * @return string
+ */
+function pwg_get_filename_wo_extension($filename)
+{
+ //~ pwg_log('>>>>> _get_filename_wo_extension($filename = '.var_export($filename, TRUE).') >>>>>'."\n");
+
+ $short_name = substr($filename, 0, strrpos($filename, '.'));
+
+ //~ pwg_log('<<<<< _get_filename_wo_extension() returns '.var_export($short_name, TRUE).' <<<<<'."\n");
+ return $short_name;
+}
+
+/**
+ * return extension of the thumbnail and complete error_log
+ *
+ * @param string $file_dir
+ * @param string $file_short
+ * @param string $file_ext
+ * @param string &$error_log
+ * @return string
+ */
+function pwg_get_thumbnail_ext($file_dir, $file_short, $file_ext, &$error_log)
+{
+ //~ pwg_log('>>>>> pwg_get_thumbnail_ext($file_dir = '.var_export($file_dir, TRUE).', $file_short = '.var_export($file_short, TRUE).') >>>>>'."\n");
+
+ global $conf;
+
+ $thumb_ext = '';
+ foreach ($conf['picture_ext'] as $ext)
+ {
+ if (file_exists($file_dir.'/'.$conf['thumbs'].'/'.$conf['prefix_thumbnail'].$file_short.'.'.$ext))
+ {
+ $thumb_ext = $ext;
+ break;
+ }
+ }
+
+ if ($thumb_ext == '')
+ {
+ if ($conf['thumbnail'])
+ {
+ $log = pwg_icon_file($file_dir, $file_short, $file_ext);
+ if (strpos($log, 'success'))
+ {
+ $thumb_ext = $conf['thumbnail_format'];
+ }
+ $error_log .= $log;
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_get_thumbnail_ext() returns '.var_export($thumb_ext, TRUE).' <<<<<'."\n");
+ return $thumb_ext;
+}
+
+
+/**
+ * return error logs
+ *
+ * @param string $file_dir
+ * @param string $file_short
+ * @param string $file_ext
+ * @return string
+ */
+function pwg_icon_file($file_dir, $file_short, $file_ext)
+{
+ //~ pwg_log('>>>>> pwg_icon_file($file_dir = '.var_export($file_dir, TRUE).', $file_short = '.var_export($file_short, TRUE).') >>>>>'."\n");
+
+ global $conf, $pwg_conf;
+
+ $error_log = '';
+
+ // Get original properties (width, height)
+ if ($image_size = getimagesize($file_dir.'/'.$file_short.'.'.$file_ext))
+ {
+ $src_width = $image_size[0];
+ $src_height = $image_size[1];
+ }
+ else
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Can not generate icon for <code>';
+ $error_log .= $file_dir.'/'.$file_short.'.'.$file_ext.'</code>';
+ $error_log .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png" title="width/height are unreadable" /><br />'."\n";
+ return $error_log;
+ }
+
+ // Check input format
+ $dst_format = $conf['thumbnail_format'];
+ $src_format = ($file_ext == 'jpg' or $file_ext == 'JPG') ? 'jpeg' : strtolower($file_ext);
+ if (!in_array($src_format, $pwg_conf['gd_supported_format']))
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Can not generate icon for <code>';
+ $error_log .= $file_dir.'/'.$file_short.'.'.$file_ext.'</code>';
+ $error_log .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png" title="format not supported" /><br />'."\n";
+ return $error_log;
+ }
+
+ // Calculate icon properties (width, height)
+ switch ($conf['thumbnail_method'])
+ {
+ case 'fixed':
+ {
+ $dst_width = $conf['thumbnail_width'];
+ $dst_height = $conf['thumbnail_height'];
+ break;
+ }
+ case 'width':
+ {
+ $dst_width = $conf['thumbnail_width'];
+ $dst_height = $dst_width * $src_height / $src_width;
+ break;
+ }
+ case 'height':
+ {
+ $dst_height = $conf['thumbnail_height'];
+ $dst_width = $dst_height * $src_width / $src_height;
+ break;
+ }
+ case 'ratio':
+ {
+ $dst_width = round($src_width * $conf['thumbnail_ratio']);
+ $dst_height = round($src_height * $conf['thumbnail_ratio']);
+ break;
+ }
+ case 'exif':
+ default:
+ {
+ // Nothing to do
+ }
+ }
+
+ // Creating icon
+ if ($conf['thumbnail_method'] == 'exif')
+ {
+ $src = exif_thumbnail($file_dir.'/'.$file_short.'.'.$file_ext, $width, $height, $imagetype);
+ if ($src === false)
+ {
+ $error_log .= ' <code class="failure">Failure -</code> No EXIF thumbnail in <code>';
+ $error_log .= $file_dir.'/'.$file_short.'.'.$file_ext.'</code><br />'."\n";
+ return $error_log;
+ }
+ $dst = imagecreatefromstring($src);
+ if ($src === false)
+ {
+ $error_log .= ' <code class="failure">Failure -</code> EXIF thumbnail format not supported in <code>';
+ $error_log .= $file_dir.'/'.$file_short.'.'.$file_ext.'</code><br />'."\n";
+ return $error_log;
+ }
+ }
+ else
+ {
+ if (($pwg_conf['gd_version_major'] != 2)) // or ($conf['thumbnail_format'] == 'gif'))
+ {
+ $dst = imagecreate($dst_width, $dst_height);
+ }
+ else
+ {
+ $dst = imagecreatetruecolor($dst_width, $dst_height);
+ }
+ $src = call_user_func('imagecreatefrom'.$src_format, $file_dir.'/'.$file_short.'.'.$file_ext);
+ if (!$src)
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Internal error for <code>imagecreatefrom'.$src_format.'()</code>';
+ $error_log .= 'with <code>'.$file_dir.'/'.$file_short.'.'.$file_ext.'</code><br />'."\n";
+ return $error_log;
+ }
+
+ if (($pwg_conf['gd_version_major'] != 2))
+ {
+ if (!imagecopyresized($dst, $src, 0, 0, 0, 0, $dst_width, $dst_height, $src_width, $src_height))
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Internal error for <code>imagecopyresized()</code>';
+ $error_log .= 'with <code>'.$file_dir.'/'.$file_short.'.'.$file_ext.'</code><br />'."\n";
+ return $error_log;
+ }
+ }
+ else
+ {
+ if (!imagecopyresampled($dst, $src, 0, 0, 0, 0, $dst_width, $dst_height, $src_width, $src_height))
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Internal error for <code>imagecopyresampled()</code>';
+ $error_log .= 'with <code>'.$file_dir.'/'.$file_short.'.'.$file_ext.'</code><br />'."\n";
+ return $error_log;
+ }
+ }
+ }
+
+ if (!call_user_func('image'.$dst_format, $dst, $file_dir.'/'.$conf['thumbs'].'/'.$conf['prefix_thumbnail'].$file_short.'.'.$conf['thumbnail_format']))
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Can not write <code>';
+ $error_log .= $file_dir.'/'.$conf['thumbs'].'/'.$conf['prefix_thumbnail'].$file_short.'.'.$file_ext.'</code> to generate thumbnail<br />'."\n";
+ return $error_log;
+ }
+
+ $error_log .= ' <code class="success">Success -</code> Thumbnail generated for <code>';
+ $error_log .= $file_dir.'/'.$file_short.'.'.$file_ext.'</code><br />'."\n";
+
+ //~ pwg_log('<<<<< pwg_icon_file() returns '.var_export($error_log, TRUE).' <<<<<'."\n");
+ return $error_log;
+}
+
+/**
+ * completes xml line <element .../> and returns error log
+ *
+ * @param string $file
+ * @param string &$line
+ * @return string
+ */
+function pwg_scan_file($file_full, &$line)
+{
+ //~ pwg_log('>>>>> pwg_scan_file($file = '.var_export($file_full, TRUE).', $line = '.var_export($line, TRUE).') >>>>>'."\n");
+
+ global $conf, $pwg_conf;
+
+ $error_log ='';
+
+ $file_base = basename($file_full);
+ $file_short = pwg_get_filename_wo_extension($file_base);
+ $file_ext = pwg_get_file_extension($file_base);
+ $file_dir = dirname($file_full);
+
+ $element['file'] = $file_base;
+ $element['path'] = dirname($pwg_conf['this_url']).substr($file_dir, 1).'/'.$file_base;
+
+ if (in_array($file_ext, $conf['picture_ext']))
+ {
+ // Here we scan a picture : thumbnail is mandatory, high is optionnal, representative is not scanned
+ $element['tn_ext'] = pwg_get_thumbnail_ext($file_dir, $file_short, $file_ext, $error_log);
+ if ($element['tn_ext'] != '')
+ {
+ // picture has a thumbnail, get image width, heigth, size in Mo
+ $element['filesize'] = floor(filesize($file_full) / 1024);
+ if ($image_size = getimagesize($file_full))
+ {
+ $element['width'] = $image_size[0];
+ $element['height'] = $image_size[1];
+ }
+
+ // get high resolution
+ if (pwg_get_high($file_dir, $file_base))
+ {
+ $element['has_high'] = 'true';
+
+ $high_file = $file_dir.'/'.$conf['high'].'/'.$file_base;
+ $element['high_filesize'] = floor(filesize($high_file) / 1024);
+ }
+
+ // get EXIF meta datas
+ if ($conf['use_exif'])
+ {
+ // Verify activation of exif module
+ if (extension_loaded('exif'))
+ {
+ if ($exif = read_exif_data($file_full))
+ {
+ foreach ($conf['use_exif_mapping'] as $pwg_key => $exif_key )
+ {
+ if (isset($exif[$exif_key]))
+ {
+ if ( in_array($pwg_key, array('date_creation','date_available') ) )
+ {
+ if (preg_match('/^(\d{4}):(\d{2}):(\d{2})/', $exif[$exif_key], $matches))
+ {
+ $element[$pwg_key] = $matches[1].'-'.$matches[2].'-'.$matches[3];
+ }
+ }
+ else
+ {
+ $element[$pwg_key] = $exif[$exif_key];
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // get IPTC meta datas
+ if ($conf['use_iptc'])
+ {
+ $iptc = pwg_get_sync_iptc_data($file_full);
+ if (count($iptc) > 0)
+ {
+ foreach (array_keys($iptc) as $key)
+ {
+ $element[$key] = addslashes($iptc[$key]);
+ }
+ }
+ }
+
+ }
+ else
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Thumbnail is missing for <code>'.$file_dir.'/'.$file_base.'</code>';
+ $error_log .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png" title="'.$file_dir.'/thumbnail/'.$conf['prefix_thumbnail'].$file_short;
+ $error_log .= '.xxx ('.implode(', ', $conf['picture_ext']).')" /><br />'."\n";
+ }
+ }
+ else
+ {
+ // Here we scan a non picture file : thumbnail and high are unused, representative is optionnal
+ $element['tn_ext'] = pwg_get_thumbnail_ext($file_dir, $file_short, $file_ext, $log);
+ $ext = pwg_get_representative_ext($file_dir, $file_short);
+ if ($ext != '')
+ {
+ $element['representative_ext'] = $ext;
+ }
+ }
+
+ if (strlen($error_log) == 0)
+ {
+ $line = pwg_get_indent('element').'<element ';
+ foreach($element as $key => $value)
+ {
+ $line .= $key.'="'.$value.'" ';
+ }
+ $line .= '/>'."\n";
+ }
+
+ //~ pwg_log('<<<<< pwg_scan_file() returns '.var_export($error_log, TRUE).' <<<<<'."\n");
+ return $error_log;
+}
+
+/**
+ * returns current level in tree
+ *
+ * @return integer
+ */
+function pwg_get_level($dir)
+{
+ //~ pwg_log('>>>>> pwg_get_level($dir = '.var_export($dir, TRUE).') >>>>>'."\n");
+
+ $level = substr_count($dir, '/') - 1; // -1 because of ./ at the beginning of path
+
+ //~ pwg_log('<<<<< pwg_get_level() returns '.var_export($level, TRUE).' <<<<<'."\n");
+ return $level;
+}
+
+/**
+ * returns indentation of element
+ *
+ * @param string $element_type : 'root', 'element', 'dir'
+ * @return string
+ */
+function pwg_get_indent($element_type)
+{
+ //~ pwg_log('>>>>> pwg_get_indent($element_type = '.var_export($element_type, TRUE).') >>>>>'."\n");
+
+ $level = substr_count($_SESSION['scan_list_fold'][0], '/') - 1; // because of ./ at the beginning
+ switch($element_type)
+ {
+ case 'dir' :
+ {
+ $indent = str_repeat(' ', 2*pwg_get_level($_SESSION['scan_list_fold'][0]));
+ break;
+ }
+ case 'root' :
+ {
+ $indent = str_repeat(' ', 2*pwg_get_level($_SESSION['scan_list_fold'][0])+2);
+ break;
+ }
+ case 'element' :
+ {
+ $indent = str_repeat(' ', 2*pwg_get_level($_SESSION['scan_list_fold'][0])+4);
+ break;
+ }
+ default :
+ {
+ $indent = '';
+ break;
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_get_indent() returns '.var_export(strlen($indent), TRUE).' spaces <<<<<'."\n");
+ return $indent;
+}
+
+/**
+ * create index.php in directory and reserved sub_directories, return logs
+ *
+ * @param string dir
+ * @return string
+ */
+function pwg_protect_directories($directory)
+{
+ //~ pwg_log('>>>>> pwg_protect_directories($directory = '.var_export($directory, true).') >>>>>'."\n");
+
+ global $conf;
+
+ $error_log = '';
+ $dirlist = array($directory, $directory.'/'.$conf['thumbs'], $directory.'/'.$conf['high'], $directory.'/'.$conf['represent']);
+
+ foreach ($dirlist as $dir)
+ {
+ if (file_exists($dir))
+ {
+ if (!file_exists($dir.'/index.php'))
+ {
+ $file = @fopen($dir.'/index.php', 'w');
+ if ($file != false)
+ {
+ fwrite($file, $pwg_conf['protect_content']); // the return code should be verified
+ $error_log .= ' <code class="success">Success -</code> index.php created in directory <a href="'.$dir.'">'.$dir."</a><br />\n";
+ }
+ else
+ {
+ $error_log .= ' <code class="failure">Failure -</code> Can not create index.php in directory <code>'.$dir."</code><br />\n";
+ }
+ }
+ else
+ {
+ if ($conf['protect_warnings'])
+ {
+ $error_log .= ' <code class="warning">Warning -</code> index.php already exists in directory <a href="'.$dir.'">'.$dir."</a><br />\n";
+ }
+ }
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_protect_directories() returns '.var_export($error_log, true).' <<<<<'."\n");
+ return $error_log;
+}
+
+/**
+ * returns file extension (.xxx)
+ *
+ * @param string $file
+ * @return string
+ */
+function pwg_get_file_extension($file)
+{
+ //~ pwg_log('>>>>> pwg_get_file_extension($file = '.var_export($file, true).') >>>>>'."\n");
+
+ $ext = substr(strrchr($file, '.'), 1, strlen ($file));
+
+ //~ pwg_log('<<<<< pwg_get_file_extension() returns '.var_export($ext, true).' <<<<<'."\n");
+ return $ext;
+}
+
+/**
+ * completes directory list of supported files and returns error logs
+ *
+ * @param string $directory
+ * @return string
+ */
+function pwg_get_file_list($directory)
+{
+ //~ pwg_log('>>>>> pwg_get_file_list($directory = '.var_export($directory, true).') >>>>>'."\n");
+
+ global $conf, $pwg_conf;
+
+ $errorLog = '';
+ $dir = opendir($directory);
+ while (($file = readdir($dir)) !== false)
+ {
+ switch (filetype($directory."/".$file))
+ {
+ case 'file' :
+ {
+ if (in_array(pwg_get_file_extension($file), $conf['file_ext']))
+ {
+ // The file pointed is a regular file with a supported extension
+ array_push($_SESSION['scan_list_file'], $directory.'/'.$file);
+ //~ pwg_log('--->> Push in $_SESSION[scan_list_file] value "'.$directory.'/'.$file.'"'."\n");
+ if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $file))
+ {
+ $errorLog .= ' <code class="failure">Failure -</code> Invalid file name for <code>'.$file.'</code> in <code>'.$directory.'</code>';
+ $errorLog .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png"';
+ $errorLog .= ' title="Name should be composed of letters, figures, -, _ or . ONLY" /><br />'."\n";
+ }
+ }
+ break; // End of filetype FILE
+ }
+ case 'dir' :
+ {
+ if(!in_array($file, $pwg_conf['reserved_directory_names']))
+ {
+ // The file pointed is a directory but neither system directory nor reserved by PWG
+ array_push($_SESSION['scan_list_fold'], $directory.'/'.$file);
+ //~ pwg_log('--->> Push in $_SESSION[scan_list_fold] value "'.$directory.'/'.$file.'"'."\n");
+ if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $file))
+ {
+ $errorLog .= ' <code class="failure">Failure -</code> Invalid directory name for <code>'.$directory.'/'.$file.'</code>';
+ $errorLog .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png"';
+ $errorLog .= ' title="Name should be composed of letters, figures, -, _ or . ONLY" /><br />'."\n";
+ }
+ }
+ break; // End of filetype DIR
+ }
+ case 'fifo' :
+ case 'char' :
+ case 'block' :
+ case 'link' :
+ case 'unknown':
+ default :
+ {
+ // PWG does not manage these cases
+ break;
+ }
+ }
+ }
+ closedir($dir);
+
+ //~ pwg_log('<<<<< pwg_get_file_list() returns '.var_export($errorLog, true).' <<<<<'."\n");
+
+ return $errorLog;
+}
+
+/**
+ * returns a float value coresponding to the number of seconds since the
+ * unix epoch (1st January 1970) and the microseconds are precised :
+ * e.g. 1052343429.89276600
+ *
+ * @return float
+ */
+function pwg_get_moment()
+{
+ //~ pwg_log('>>>>> pwg_get_moment() >>>>>'."\n");
+
+ $t1 = explode(' ', microtime());
+ $t2 = explode('.', $t1[0]);
+ $t2 = $t1[1].'.'.$t2[1];
+
+ //~ pwg_log('<<<<< pwg_get_moment() returns '.var_export($t2, true).' <<<<<'."\n");
+ return $t2;
+}
+
+/**
+ * return true if HTTP_REFERER and PHP_SELF are similar
+ *
+ * return boolean
+ */
+function pwg_referer_is_me()
+{
+ global $pwg_conf;
+
+ //~ pwg_log('>>>>> pwg_referer_is_me() >>>>>'."\n");
+
+ $response = false;
+ $caller = (isset($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : '';
+
+ if (strcasecmp($pwg_conf['this_url'], $caller) == 0) {
+ $response = true;
+ }
+
+ //~ pwg_log('<<<<< pwg_referer_is_me() returns '.var_export($response, true).' <<<<<'."\n");
+ return $response;
+}
+
+// +-----------------------------------------------------------------------+
+// | pwg_<ACTION>_<STEP> Functions |
+// +-----------------------------------------------------------------------+
+
+function pwg_test_start()
+{
+ //~ pwg_log('>>>>> pwg_test_start() >>>>>'."\n");
+
+ global $g_message, $conf;
+
+ if (isset($_REQUEST['version']))
+ {
+ if ($_REQUEST['version'] != $conf['version'])
+ {
+ $g_message = '0';
+ }
+ else
+ {
+ $g_message = '1';
+ }
+ }
+ else
+ {
+ $g_message = '1';
+ }
+ $_SESSION['scan_step'] = 'exit';
+
+ //~ pwg_log('<<<<< pwg_test_start() <<<<<'."\n");
+}
+
+function pwg_test_exit()
+{
+ //~ pwg_log('>>>>> pwg_test_exit() >>>>>'."\n");
+
+ global $g_header, $g_message, $g_footer, $conf, $pwg_conf;
+
+ if (pwg_referer_is_me())
+ {
+ $g_header = ' : <span class="success">Test</span>'."\n";
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>'."\n";
+
+ // Write version
+ $g_message = ' <h3>Script version</h3>'."\n";
+ $g_message .= ' This script is tagged : <code class="failure">'.$conf['version'].'</code>'."\n";
+ // write GD support
+ if (!is_callable('gd_info'))
+ {
+ $g_message .= ' <code class="failure">Failure -</code> Your server can not generate imagess<br />'."\n";
+ }
+ else
+ {
+ $info = gd_info();
+ $gd_full_version = ereg_replace('[[:alpha:][:space:]()]+', '', $info['GD Version']);
+ list($gd_version) = preg_split('/[.]+/', $gd_full_version);
+
+ $g_message .= ' <h3>Image generation</h3>'."\n";
+ $g_message .= ' <code class="success">Success -</code> Your server can generate images<br />'."\n";
+ $g_message .= ' <code class="warning">Warning -</code> Your server support GD'.$gd_version.' (v'.$gd_full_version.')<br />'."\n";
+ $format_list = array();
+ $format = ($info['GIF Create Support']) ? '<code>gif</code>' : NULL;
+ array_push($format_list, $format);
+ $format = ($info['JPG Support']) ? '<code>jpg</code>' : NULL;
+ array_push($format_list, $format);
+ $format = ($info['PNG Support']) ? '<code>png</code>' : NULL;
+ array_push($format_list, $format);
+ $g_message .= ' <code class="warning">Warning -</code> Your server support format: '.implode(', ', $format_list)."\n";
+ }
+
+ $g_message .= ' <h3>Directory parsing</h3>'."\n";
+ if ($pwg_conf['safe_mode'])
+ {
+ $g_message .= ' <code class="warning">Warning -</code> Your server does not support to resize execution time'."\n";
+ }
+ else
+ {
+ $g_message .= ' <code class="success">Success -</code> Your server supports to resize execution time'."\n";
+ }
+ }
+ else
+ {
+ // compare version in GET parameter with $conf['version']
+ if ($g_message == '1')
+ {
+ exit('<pre>PWG-INFO-2: test successful</pre>');
+ }
+ else
+ {
+ exit('<pre>PWG-ERROR-4: Piwigo versions differs</pre>');
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_test_exit() <<<<<'."\n");
+}
+
+function pwg_clean_start()
+{
+ //~ pwg_log('>>>>> pwg_clean_start() >>>>>'."\n");
+
+ global $g_message;
+
+ if(@unlink('./listing.xml'))
+ {
+ $g_message = '1';
+ }
+ else
+ {
+ $g_message = '0';
+ }
+
+ $_SESSION['scan_step'] = 'exit';
+
+ //~ pwg_log('<<<<< pwg_clean_start() <<<<<'."\n");
+}
+
+function pwg_clean_exit()
+{
+ //~ pwg_log('>>>>> pwg_clean_exit() >>>>>'."\n");
+
+ global $g_header, $g_message, $g_footer, $conf, $pwg_conf;
+
+ if(pwg_referer_is_me())
+ {
+ $g_header = ' : <span class="success">Clean</span>';
+ if ($g_message == '1')
+ {
+ $g_message = ' <code class="success">Success -</code> <code>listing.xml</code> file deleted'."\n";
+ }
+ else
+ {
+ $g_message = ' <code class="failure">Failure -</code> <code>listing.xml</code> does not exist or is read only'."\n";
+ }
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>';
+ }
+ else
+ {
+ if ($g_message == '1')
+ {
+ exit('<pre>PWG-INFO-3 : listing.xml file deleted</pre>');
+ }
+ else
+ {
+ exit('<pre>PWG-ERROR-3 : listing.xml does not exist</pre>');
+ }
+ }
+
+ //~ pwg_log('<<<<< pwg_clean_exit() <<<<<'."\n");
+}
+
+function pwg_generate_start()
+{
+ //~ pwg_log('>>>>> pwg_generate_start() >>>>>'."\n");
+ //~ pwg_log("GENARATE start >>>\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE start >>>\n");
+
+ global $g_listing, $pwg_conf, $conf;
+
+ // Flush line <informations>
+ $xml_header_url = dirname($pwg_conf['this_url']);
+ $xml_header_date = date('Y-m-d');
+ $xml_header_version = htmlentities($conf['version']);
+
+ $attrs = array();
+ if ($conf['use_iptc'])
+ {
+ $attrs = array_merge($attrs, array_keys($conf['use_iptc_mapping']) );
+ }
+ if ($conf['use_exif'])
+ {
+ $attrs = array_merge($attrs, array_keys($conf['use_exif_mapping']) );
+ }
+ $xml_header_metadata = implode(',',array_unique($attrs));
+
+ $xml_header = '<informations';
+ $xml_header .= ' generation_date="'.$xml_header_date.'"';
+ $xml_header .= ' phpwg_version="'.$xml_header_version.'"';
+ $xml_header .= ' metadata="'.$xml_header_metadata.'"';
+ $xml_header .= ' url="'.$xml_header_url.'"';
+ $xml_header .= '>'."\n";
+
+ fwrite($g_listing, $xml_header);
+
+ // Initialization of directory and file lists
+ $_SESSION['scan_list_fold'] = array();
+ $_SESSION['scan_list_file'] = array();
+ $_SESSION['scan_logs'] .= pwg_get_file_list('.');
+ sort($_SESSION['scan_list_fold']);
+
+ // Erase first file list because root directory does not contain images.
+ $_SESSION['scan_list_file'] = array();
+
+ // What are we doing at next step
+ if(count($_SESSION['scan_list_fold']) > 0)
+ {
+ $_SESSION['scan_step'] = 'list';
+ }
+ else
+ {
+ $_SESSION['scan_step'] = 'stop';
+ }
+
+ //~ pwg_log("GENARATE start <<<\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE start <<<\n");
+ //~ pwg_log('<<<<< pwg_generate_start() <<<<<'."\n");
+}
+
+function pwg_generate_list()
+{
+ //~ pwg_log('>>>>> pwg_generate_list() >>>>>'."\n");
+ //~ pwg_log("GENARATE list >>>\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE list >>>\n");
+
+ global $g_listing;
+
+ // Flush line <dirX name=""> in xml file
+ $dirname = basename($_SESSION['scan_list_fold'][0]);
+ $line = pwg_get_indent('dir').'<dir'.pwg_get_level($_SESSION['scan_list_fold'][0]).' name="'.$dirname.'">'."\n";
+ fwrite($g_listing, $line);
+
+ // Get list of files and directories
+ $_SESSION['scan_logs'] .= pwg_get_file_list($_SESSION['scan_list_fold'][0]);
+ sort($_SESSION['scan_list_fold']); // Mandatory to keep the tree order
+ sort($_SESSION['scan_list_file']); // Easier to read when sorted
+
+ // Flush line <root>
+ $line = pwg_get_indent('root').'<root>'."\n";
+ fwrite($g_listing, $line);
+
+ // What are we doing at next step
+ $_SESSION['scan_step'] = 'scan';
+
+ //~ pwg_log("GENARATE list <<<\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE list <<<\n");
+ //~ pwg_log('<<<<< pwg_generate_list() <<<<<'."\n");
+}
+
+function pwg_generate_scan()
+{
+ //~ pwg_log('>>>>> pwg_generate_scan() >>>>>'."\n");
+ //~ pwg_log("GENARATE scan >>>\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE scan >>>\n");
+
+ global $g_listing, $conf;
+
+ while (pwg_continue() and count($_SESSION['scan_list_file']) > 0)
+ {
+ $line = '';
+ $_SESSION['scan_logs'] .= pwg_scan_file($_SESSION['scan_list_file'][0], $line);
+
+ if (strlen($line) > 0)
+ {
+ fwrite($g_listing, $line);
+ }
+ //~ pwg_log('---<< Pull of $_SESSION[scan_list_file] value "'.$_SESSION['scan_list_file'][0].'"'."\n");
+ array_shift($_SESSION['scan_list_file']);
+ $_SESSION['scan_cnt_file']++;
+ }
+
+ if (count($_SESSION['scan_list_file']) <= 0)
+ {
+ $_SESSION['scan_step'] = 'prot';
+ }
+
+ //~ pwg_log("GENERATE scan <<<\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE scan <<<\n");
+ //~ pwg_log('<<<<< pwg_generate_scan() <<<<<'."\n");
+}
+
+function pwg_generate_prot()
+{
+ //~ pwg_log('>>>>> pwg_generate_prot() >>>>>'."\n");
+ //~ pwg_log("GENARATE prot >>>\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE prot >>>\n");
+
+ global $conf, $g_listing;
+
+ // Flush line </root>
+ $line = pwg_get_indent('root').'</root>'."\n";
+ fwrite($g_listing, $line);
+
+ if ($conf['protect'])
+ {
+ $_SESSION['scan_logs'] .= pwg_protect_directories($_SESSION['scan_list_fold'][0]);
+ }
+
+ // How many directories to close
+ $current_level = pwg_get_level($_SESSION['scan_list_fold'][0]);
+ if (isset($_SESSION['scan_list_fold'][1]))
+ {
+ //~ pwg_log('---<< Pull of $_SESSION[scan_list_fold] value "'.$_SESSION['scan_list_fold'][0].'"'."\n");
+ array_shift($_SESSION['scan_list_fold']);
+ $_SESSION['scan_cnt_fold']++;
+ $next_level = pwg_get_level($_SESSION['scan_list_fold'][0]);
+ $_SESSION['scan_step'] = 'list';
+ }
+ else
+ {
+ $next_level = -1;
+ $_SESSION['scan_cnt_fold']++;
+ $_SESSION['scan_step'] = 'stop';
+ }
+
+ if ($current_level == $next_level)
+ {
+ fwrite($g_listing, pwg_close_level($current_level, 1));
+ }
+ else
+ {
+ if (($current_level > $next_level))
+ {
+ fwrite($g_listing, pwg_close_level($current_level, $current_level-$next_level+1));
+ } // Nothing to do if current_level < next_level
+ }
+
+ //~ pwg_log("GENERATE prot <<<\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE prot <<<\n");
+ //~ pwg_log('<<<<< pwg_generate_prot() <<<<<'."\n");
+}
+
+function pwg_generate_stop()
+{
+ //~ pwg_log('>>>>> pwg_generate_stop() >>>>>'."\n");
+ //~ pwg_log("GENARATE stop >>>\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE stop >>>\n");
+
+ global $pwg_conf, $g_listing, $g_header, $g_message, $g_footer;
+
+ // Flush line </informations>
+ fwrite($g_listing, '</informations>'."\n");
+
+ // backup error log before cleaning session
+ $time_elapsed = number_format(pwg_get_moment() - $_SESSION['scan_time'], 3, '.', ' ');
+
+ $g_header = ' : <span class="success">Generate</span>';
+ $g_message = ' <div>'."\n".$_SESSION['scan_logs'].' </div>'."\n";
+ $g_message .= ' <div><code class="success">'.$_SESSION['scan_cnt_fold'].'</code> directories parsed<br />'."\n";
+ $g_message .= ' <code class="success">'.$_SESSION['scan_cnt_file'].'</code> files scanned</div>'."\n";
+ $g_message .= ' <div>View <a href="listing.xml">listing.xml</a></div>'."\n";
+ $g_message .= ' <div style="{text-align: right;}">Listing generated in : <code>'.$time_elapsed.' s</code></div>';
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>';
+
+ // What are we doing at next step
+ $_SESSION['scan_step'] = 'exit';
+
+ //~ pwg_log("GENARATE stop <<<\n".var_export($_SESSION['scan_list_fold'], true)."\n".var_export($_SESSION['scan_list_file'], true)."\nGENERATE stop <<<\n");
+ //~ pwg_log('<<<<< pwg_generate_stop() <<<<<'."\n");
+}
+
+// +-----------------------------------------------------------------------+
+// | ALWAYS CALLED FUNCTIONS |
+// +-----------------------------------------------------------------------+
+
+/**
+ * This function check step and time ellapsed to determine end of loop
+ *
+ * @return bool
+ */
+function pwg_continue()
+{
+ //~ pwg_log('>>>>> pwg_continue() >>>>>'."\n");
+
+ global $conf, $pwg_conf, $g_refresh, $g_header, $g_message, $g_footer, $start_time;
+
+ if (!isset($_SESSION['scan_step']) or $_SESSION['scan_step'] == 'exit')
+ {
+ // evident end of process
+ $return = false;
+ }
+ else
+ {
+ if ($pwg_conf['safe_mode'] or $conf['force_refresh_method'])
+ {
+ // can not reset the time
+ $time_elapsed = pwg_get_moment() - $start_time;
+ if ($time_elapsed < $conf['max_execution_time'])
+ {
+ $return = true;
+ }
+ else
+ {
+ $start_time = $_SESSION['scan_time'];
+ $formated_time = number_format(pwg_get_moment() - $start_time, 3, '.', ' ');
+
+ $g_refresh = '<meta http-equiv="Refresh" content="'.$conf['refresh_delay'].'">'."\n";
+ $g_header = ' : <span class="success">'.ucfirst($_SESSION['scan_action']).'</span>';
+ $g_message = '';
+ if ($_SESSION['scan_cnt_fold'] != 0)
+ {
+ $g_message .= '<code class="success">'.$_SESSION['scan_cnt_fold'].'</code> directories scanned<br />'."\n";
+ }
+ if ($_SESSION['scan_cnt_file'] != 0)
+ {
+ $g_message .= '<code class="success">'.$_SESSION['scan_cnt_file'].'</code> files scanned<br />'."\n";
+ }
+ $nb = count($_SESSION['scan_list_fold']);
+ if ($nb > 0)
+ {
+ $g_message .= '<code class="warning">'.$nb.'</code> directories to scan<br />'."\n";
+ }
+ $nb = count($_SESSION['scan_list_file']);
+ if ($nb > 0)
+ {
+ $g_message .= '<code class="warning">'.$nb.'</code> files to scan<br />'."\n";
+ }
+ $g_message .= ' <div style="{text-align: right;}">Time elapsed : <code>'.$formated_time.' s</code></div>';
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'?action='.$_SESSION['scan_action'].'" title="Continue"><img src="'.$pwg_conf['icon_dir'].'right.png" /></a>'."\n";
+
+ $return = false;
+ }
+ }
+ else
+ {
+ // reset the time
+ set_time_limit(intval(ini_get('max_execution_time')));
+ $return = true;
+ }
+ }
+ //~ pwg_log('<<<<< pwg_continue() returns '.var_export($return, true).' <<<<<'."\n");
+
+ return $return;
+}
+
+/**
+ * This function :
+ * -> Verify the script call
+ * -> Lock the script
+ * -> Open listing.xml if action is 'generate'
+ * -> Initialize output and session variables
+ *
+ * @return nothing
+ */
+function pwg_init()
+{
+ //~ pwg_log('>>>>> pwg_init() >>>>>'."\n");
+
+ global $g_message, $g_listing, $g_footer, $conf, $pwg_conf, $start_time;
+ $init_message = '';
+
+ // Lock other script sessions, this lock will be remove during 'exit' step
+ if (!isset($_SESSION['scan_step']))
+ {
+ $fp = @fopen(__FILE__.'.lock', 'x+'); // return false if __FILE__.lock exists or if cannot create
+ if ($fp == false)
+ {
+ $g_header = $_SESSION['scan_action'];
+ $g_message = ' <code class="failure">Failure -</code> Another script is running';
+ $g_message .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png" title="Delete file '.__FILE__.'.lock and retry" />';
+ $g_message .= "\n";
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>'."\n";
+ $_SESSION['scan_step'] = 'exit';
+ //~ pwg_log('<<<<< pwg_init() failure <<<<<'."\n");
+ return;
+ }
+ else
+ {
+ fwrite($fp, session_id()); // Writing session_id to trace lock
+ fclose($fp);
+ $_SESSION['scan_step'] = 'init';
+ }
+ }
+
+ // Verify and backup parameter action. This backup will be removed during step 'exit'
+ if (isset($_REQUEST['action']))
+ {
+ if (in_array($_REQUEST['action'], $pwg_conf['scan_action']))
+ {
+ if (isset($_SESSION['scan_action']))
+ {
+ if ($_SESSION['scan_action'] != $_REQUEST['action'])
+ {
+ // Fatal error
+ $g_message = ' <code class="failure">Failure -</code> Parameter <code>action</code> differs between url and session';
+ $g_message .= "\n";
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>'."\n";
+ $_SESSION['scan_step'] = 'exit';
+ //~ pwg_log('<<<<< pwg_init() failure <<<<<'."\n");
+ return;
+ }
+ }
+ else
+ {
+ $_SESSION['scan_action'] = $_REQUEST['action'];
+ }
+ }
+ else
+ {
+ // Fatal error
+ $g_message = ' <code class="failure">Failure -</code> Problem with <code>action</code> parameter';
+ $g_message .= ' <img src="'.$pwg_conf['icon_dir'].'add_tag.png" title="empty, '.implode(', ', $pwg_conf['scan_action']).'" />';
+ $g_message .= "\n";
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>'."\n";
+ $_SESSION['scan_step'] = 'exit';
+ //~ pwg_log('<<<<< pwg_init() failure <<<<<'."\n");
+ return;
+ }
+ }
+ else
+ {
+ // Here we are on welcome page
+ $g_message = ' <ul>'."\n";
+ $g_message .= ' <li><a href="'.$pwg_conf['this_url'].'?action=test" title="Display/Compare script version">Test</a></li>'."\n";
+ $g_message .= ' <li><a href="'.$pwg_conf['this_url'].'?action=clean" title="Delete listing.xml if exists">Clean</a></li>'."\n";
+ $g_message .= ' <li><a href="'.$pwg_conf['this_url'].'?action=generate" title="Scan all images from this directory and write informations in listing.xml">Listing</a></li>'."\n";
+ $g_message .= ' </ul>'."\n";
+ $g_footer = '<a href="'.$conf['gallery'].'/admin.php?page=site_manager" title="Main gallery :: site manager">';
+ $g_footer .= '<img src="'.$pwg_conf['icon_dir'].'home.png" /></a>'."\n";
+ $_SESSION['scan_step'] = 'exit';
+ $_SESSION['scan_action'] = '';
+ }
+
+ // Actions to do at the init of generate
+ if ($_SESSION['scan_action'] == 'generate')
+ {
+ // Open XML file
+ $mode = ($_SESSION['scan_step'] == 'init') ? 'w' : 'a'; // Erase old listing.xml at the beginning of generation (mode w)
+ $g_listing = @fopen('listing.xml', $mode);
+ if ($g_listing === false)
+ {
+ $g_header = $_SESSION['scan_action'];
+ $g_message = ' <code class="failure">Failure -</code> Can not write file <code>listing.xml</code>';
+ $g_message .= "\n";
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>'."\n";
+ $_SESSION['scan_step'] = 'exit';
+ //~ pwg_log('<<<<< pwg_init() failure <<<<<'."\n");
+ return;
+ }
+
+ // Check graphical capabilities
+ $init_message = pwg_check_graphics();
+ }
+
+ // Initializing session counters. This counters will be completely unset during step 'exit'
+ if ($_SESSION['scan_step'] == 'init')
+ {
+ $_SESSION['scan_list_file'] = array();
+ $_SESSION['scan_list_fold'] = array();
+ $_SESSION['scan_cnt_file'] = 0;
+ $_SESSION['scan_cnt_fold'] = 0;
+ $_SESSION['scan_time'] = $start_time;
+ $_SESSION['scan_step'] = 'start';
+ $_SESSION['scan_logs'] = $init_message;
+ }
+
+ //~ pwg_log('<<<<< pwg_init() success <<<<<'."\n");
+}
+
+/**
+ * This function :
+ * -> Close listing.xml if action is 'generate'
+ * -> Unlock the script
+ * -> Erase session variables
+ *
+ * @return nothing
+ */
+function pwg_exit()
+{
+ //~ pwg_log('>>>>> pwg_exit() >>>>>'."\n");
+
+ global $g_listing;
+
+ // Close XML file
+ if ($_SESSION['scan_action'] == 'generate' and $g_listing != false)
+ {
+ fclose($g_listing);
+ }
+
+ // Unlock script
+ unlink(__FILE__.'.lock');
+
+ // Erase session counters
+ unset($_SESSION['scan_list_file']);
+ unset($_SESSION['scan_list_fold']);
+ unset($_SESSION['scan_cnt_file']);
+ unset($_SESSION['scan_cnt_fold']);
+ unset($_SESSION['scan_time']);
+ unset($_SESSION['scan_step']);
+ $local_action = $_SESSION['scan_action'];
+ unset($_SESSION['scan_action']);
+ unset($_SESSION['scan_logs']);
+ session_destroy();
+
+ // Call specific action post process
+ if (is_callable('pwg_'.$local_action.'_exit'))
+ {
+ call_user_func('pwg_'.$local_action.'_exit');
+ }
+
+ //~ pwg_log('<<<<< pwg_exit() <<<<<'."\n");
+}
+
+// +-----------------------------------------------------------------------+
+// | Script |
+// +-----------------------------------------------------------------------+
+session_save_path('.');
+session_start();
+
+$start_time = pwg_get_moment();
+
+// Initializing message for web page
+$g_refresh = '';
+$g_header = '';
+$g_message = '';
+$g_footer = '';
+$g_listing = '';
+
+pwg_init();
+
+while(pwg_continue())
+{
+ if (is_callable('pwg_'.$_SESSION['scan_action'].'_'.$_SESSION['scan_step']))
+ {
+ call_user_func('pwg_'.$_SESSION['scan_action'].'_'.$_SESSION['scan_step']); // Run the step : start, list, scan, stop are available
+ }
+ else
+ {
+ $g_header = $_SESSION['scan_action'];
+ $g_message = ' <code class="failure">Failure -</code> INTERNAL STEP ERROR : <code>pwg_'.$_SESSION['scan_action'].'_'.$_SESSION['scan_step'].'()</code> undefined';
+ $g_message .= "\n";
+ $g_footer = '<a href="'.$pwg_conf['this_url'].'" title="Main menu"><img src="'.$pwg_conf['icon_dir'].'up.png" /></a>'."\n";
+ $_SESSION['scan_step'] = 'exit';
+ }
+}
+
+if ($_SESSION['scan_step'] == 'exit')
+{
+ pwg_exit();
+}
+
+?>
+<html>
+ <head>
+ <?php echo $g_refresh; ?>
+ <title>Manage remote gallery</title>
+ </head>
+ <style type="text/css">
+ code {font-weight: bold}
+ img {border-style: none; vertical-align: middle}
+ ul {list-style-image: url(<?php echo $pwg_conf['icon_dir']; ?>add_tag.png)}
+ .success {color: green}
+ .warning {color: orange}
+ .failure {color: red}
+ .header {text-align: center; font-variant: small-caps; font-weight: bold;}
+ .p {color: #F93;}
+ .w {color: #ccc;}
+ .g {color: #69C;}
+ .pwg {text-decoration: none; border-bottom-style: dotted; border-bottom-width: 1px;}
+ .content {width: 75%; position: absolute; top: 10%; left: 12%;}
+ .footer {text-align: right;}
+ .pwg_block {float: left;}
+ </style>
+ <body>
+ <div class="content">
+ <fieldset class="header">
+ <span class="p">Pi</span>
+ <span class="w">wi</span>
+ <span class="g">go</span>
+ &nbsp;remote site<? echo $g_header; ?>
+ </fieldset>
+ <fieldset>
+<?php echo $g_message; ?>
+ </fieldset>
+ <fieldset class="footer">
+ <div class="pwg_block">
+ Powered by <a href="http://piwigo.org" class="pwg"><span class="p">Pi</span><span class="w">wi</span><span class="g">go</span></a>
+ </div>
+ <?php echo $g_footer; ?>
+ </fieldset>
+ </div>
+ </body>
+</html>
diff --git a/BSF/tools/create_listing_file_local.inc.php b/BSF/tools/create_listing_file_local.inc.php
new file mode 100644
index 000000000..8d914cf88
--- /dev/null
+++ b/BSF/tools/create_listing_file_local.inc.php
@@ -0,0 +1,14 @@
+<?php
+// this file is provided as an example.
+// Move it to "create_listing_file.php"
+// directory if you want to modify default configuration.
+
+// URL of main gallery
+// Example : http://www.my.domain/my/directory
+$conf['gallery'] = 'http://demo.piwigo.net/';
+
+$conf['file_ext'] = array_merge($conf['file_ext'], array('flv', 'FLV'));
+
+$conf['force_refresh_method'] = true;
+
+?> \ No newline at end of file
diff --git a/BSF/tools/fill_history.pl b/BSF/tools/fill_history.pl
new file mode 100644
index 000000000..01adacfd2
--- /dev/null
+++ b/BSF/tools/fill_history.pl
@@ -0,0 +1,336 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Getopt::Long;
+use DBI;
+use File::Basename;
+use Time::Local;
+use List::Util qw/shuffle min/;
+
+my %opt;
+GetOptions(
+ \%opt,
+ qw/dbname=s dbuser=s dbpass=s prefix=s
+ total=i start_date=s end_date=s
+ help/
+ );
+
+if (defined($opt{help}))
+{
+ print <<FIN;
+
+Fill the user comments table of Piwigo.
+
+Usage: pwg_fill_comments.pl --dbname=<database_name>
+ --dbuser=<username>
+ --dbpass=<password>
+ --tagfile=<tags filename>
+ [--prefix=<tables prefix>]
+ [--help]
+
+--dbname, --dbuser and --dbpass are connexion parameters.
+
+--tagfile
+
+--prefix : determines the prefix for your table names.
+
+--help : show this help
+
+FIN
+
+ exit(0);
+}
+
+my $usage = "\n\n".basename($0)." --help for help\n\n";
+
+foreach my $option (qw/dbname dbuser dbpass start_date end_date/) {
+ if (not exists $opt{$option}) {
+ die 'Error: '.$option.' is a mandatory option', $usage;
+ }
+}
+
+$opt{prefix} = 'piwigo_' if (not defined $opt{prefix});
+my $dbh = DBI->connect(
+ 'DBI:mysql:'.$opt{dbname},
+ $opt{dbuser},
+ $opt{dbpass}
+);
+
+my $query;
+my $sth;
+
+
+# retrieve all available users
+$query = '
+SELECT id
+ FROM '.$opt{prefix}.'users
+';
+my @user_ids = keys %{ $dbh->selectall_hashref($query, 'id') };
+
+# set a list of IP addresses for each users
+my %user_IPs = ();
+foreach my $user_id (@user_ids) {
+ for (1 .. 1 + int rand 5) {
+ push(
+ @{ $user_IPs{$user_id} },
+ join(
+ '.',
+ map {1 + int rand 255} 1..4
+ )
+ );
+ }
+}
+
+# use Data::Dumper; print Dumper(\%user_IPs); exit();
+
+# start and end dates
+my ($year,$month,$day,$hour,$min,$sec)
+ = ($opt{start_date} =~ m/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
+my $start_unixtime = timelocal(0,0,0,$day,$month-1,$year);
+
+($year,$month,$day,$hour,$min,$sec)
+ = ($opt{end_date} =~ m/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
+my $end_unixtime = timelocal(0,0,0,$day,$month-1,$year);
+
+# "tags from image" and "images from tag"
+$query = '
+SELECT image_id, tag_id
+ FROM '.$opt{prefix}.'image_tag
+';
+my %image_tags = ();
+my %tag_images = ();
+my %related_tag_of = ();
+my @tags = ();
+$sth = $dbh->prepare($query);
+$sth->execute();
+while (my $row = $sth->fetchrow_hashref()) {
+ push(
+ @{$image_tags{$row->{image_id}}},
+ $row->{tag_id}
+ );
+
+ push(
+ @{$tag_images{$row->{tag_id}}},
+ $row->{image_id}
+ );
+
+ push (
+ @tags,
+ $row->{tag_id}
+ );
+}
+
+# foreach my $tag_id (keys %tag_images) {
+# printf(
+# "tag %5u: %5u images\n",
+# $tag_id,
+# scalar @{$tag_images{$tag_id}}
+# );
+# }
+# exit();
+
+# use Data::Dumper; print Dumper(\%tag_images); exit();
+
+# categories from image_id
+$query = '
+SELECT image_id, category_id
+ FROM '.$opt{prefix}.'image_category
+';
+my %image_categories = ();
+my %category_images =();
+my %categories = ();
+$sth = $dbh->prepare($query);
+$sth->execute();
+while (my $row = $sth->fetchrow_hashref()) {
+ push(
+ @{$image_categories{$row->{image_id}}},
+ $row->{category_id}
+ );
+
+ push(
+ @{$category_images{$row->{category_id}}},
+ $row->{image_id}
+ );
+
+ $categories{ $row->{category_id} }++;
+}
+
+my @images = keys %image_categories;
+my @categories = keys %categories;
+
+# use Data::Dumper;
+# print Dumper(\%image_categories);
+
+my @sections = (
+ 'categories',
+# 'tags',
+# 'search',
+# 'list',
+# 'favorites',
+# 'most_visited',
+# 'best_rated',
+# 'recent_pics',
+# 'recent_cats',
+);
+
+my @inserts = ();
+
+USER : foreach my $user_id (@user_ids) {
+ print 'user_id: ', $user_id, "\n";
+
+ my $current_unixtime = $start_unixtime;
+ my @IPs = @{ $user_IPs{$user_id} };
+
+ VISIT : foreach my $visit_num (1..100_000) {
+ print 'visit: ', $visit_num, "\n";
+ my @temp_inserts = ();
+ my $IP = (@IPs)[int rand @IPs];
+ my $current_page = 0;
+ my $visit_size = 10 + int rand 90;
+ $current_unixtime+= 86_400 + int rand(86_400 * 30);
+
+ my $section = $sections[int rand scalar @sections];
+ # print 'section: ', $section, "\n";
+
+ push(
+ @temp_inserts,
+ {
+ section => $section,
+ }
+ );
+
+ if ($section eq 'categories') {
+ CATEGORY : foreach my $category_id (
+ (shuffle @categories)[1..int rand scalar @categories]
+ ) {
+ # print 'category: ', $category_id, "\n";
+ push(
+ @temp_inserts,
+ {
+ category_id => $category_id,
+ }
+ );
+
+ my @images = @{$category_images{$category_id}};
+ IMAGE : foreach my $image_id (
+ (shuffle @images)[1..min(10, scalar @images)]
+ ) {
+ push(
+ @temp_inserts,
+ {
+ category_id => $category_id,
+ image_id => $image_id,
+ }
+ );
+ }
+ }
+ }
+
+ if ($section eq 'tags') {
+ # TODO
+ }
+
+ # transfert @temp_inserts to @inserts
+ print 'temp_insert size: ', scalar @temp_inserts, "\n";
+ foreach my $temp_insert (@temp_inserts) {
+ $current_unixtime+= 5 + int rand 25;
+ next VISIT if ++$current_page == $visit_size;
+ last VISIT if $current_unixtime >= $end_unixtime;
+
+ my $date = unixtime_to_mysqldate($current_unixtime);
+ my $time = unixtime_to_mysqltime($current_unixtime);
+
+ my ($year, $month, $day) = split '-', $date;
+ my ($hour) = split ':', $time;
+
+ $temp_insert->{date} = $date;
+ $temp_insert->{time} = $time;
+ $temp_insert->{year} = $year;
+ $temp_insert->{month} = $month;
+ $temp_insert->{day} = $day;
+ $temp_insert->{hour} = $hour;
+ $temp_insert->{IP} = $IP;
+ $temp_insert->{section} = $section;
+ $temp_insert->{user_id} = $user_id;
+
+ push(@inserts, $temp_insert);
+ }
+ }
+}
+
+@inserts = sort {
+ $a->{date} cmp $b->{date}
+ or $a->{time} cmp $b->{time}
+} @inserts;
+
+if (scalar @inserts) {
+ my @columns =
+ qw/
+ date time year month day hour
+ user_id IP
+ section category_id image_id
+ /;
+
+ my $question_marks_string = '(';
+ $question_marks_string.= join(
+ ',',
+ map {'?'} @columns
+ );
+ $question_marks_string.= ')';
+
+ my $query = '
+INSERT INTO '.$opt{prefix}.'history
+ ('.join(', ', @columns).')
+ VALUES
+';
+ $query.= join(
+ ',',
+ map {$question_marks_string} (1 .. scalar @inserts)
+ );
+ $query.= '
+';
+
+ # print $query, "\n";
+
+ my @values = ();
+
+ foreach my $insert (@inserts) {
+ push(
+ @values,
+ map {
+ $insert->{$_}
+ } @columns
+ );
+ }
+
+ $sth = $dbh->prepare($query);
+ $sth->execute(@values)
+ or die 'cannot execute insert query';
+}
+
+sub unixtime_to_mysqldate {
+ my ($unixtime) = @_;
+
+ ($sec,$min,$hour,$day,$month,$year) = localtime($unixtime);
+
+ return sprintf(
+ '%d-%02d-%02d',
+ $year+1900,
+ $month+1,
+ $day,
+ );
+}
+
+sub unixtime_to_mysqltime {
+ my ($unixtime) = @_;
+
+ ($sec,$min,$hour,$day,$month,$year) = localtime($unixtime);
+
+ return sprintf(
+ '%d:%d:%d',
+ $hour,
+ $min,
+ $sec
+ );
+}
diff --git a/BSF/tools/index.php b/BSF/tools/index.php
new file mode 100644
index 000000000..c15b15795
--- /dev/null
+++ b/BSF/tools/index.php
@@ -0,0 +1,30 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Piwigo - a PHP based picture gallery |
+// +-----------------------------------------------------------------------+
+// | Copyright(C) 2008 Piwigo Team http://piwigo.org |
+// | Copyright(C) 2003-2008 PhpWebGallery Team http://phpwebgallery.net |
+// | Copyright(C) 2002-2003 Pierrick LE GALL http://le-gall.net/pierrick |
+// +-----------------------------------------------------------------------+
+// | 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. |
+// +-----------------------------------------------------------------------+
+
+// Recursive call
+$url = '../';
+header( 'Request-URI: '.$url );
+header( 'Content-Location: '.$url );
+header( 'Location: '.$url );
+exit();
+?>
diff --git a/BSF/tools/local-layout.css b/BSF/tools/local-layout.css
new file mode 100644
index 000000000..3017c7a06
--- /dev/null
+++ b/BSF/tools/local-layout.css
@@ -0,0 +1,10 @@
+/* Add background picture */
+BODY {
+ background-image:url(background.gif);
+ background-attachment:fixed;
+}
+
+.content UL.thumbnails SPAN.wrap2 {
+ height: 200px; /* max thumbnail height + 2px */
+}
+
diff --git a/BSF/tools/metadata.php b/BSF/tools/metadata.php
new file mode 100644
index 000000000..871b32986
--- /dev/null
+++ b/BSF/tools/metadata.php
@@ -0,0 +1,97 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Piwigo - a PHP based picture gallery |
+// +-----------------------------------------------------------------------+
+// | Copyright(C) 2008 Piwigo Team http://piwigo.org |
+// | Copyright(C) 2003-2008 PhpWebGallery Team http://phpwebgallery.net |
+// | Copyright(C) 2002-2003 Pierrick LE GALL http://le-gall.net/pierrick |
+// +-----------------------------------------------------------------------+
+// | 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. |
+// +-----------------------------------------------------------------------+
+
+$filename = 'sample.jpg';
+echo 'Informations are read from '.$filename.'<br /><br /><br />';
+
+/**
+ * return a cleaned IPTC value
+ *
+ * @param string value
+ * @return string
+ */
+function clean_iptc_value($value)
+{
+ // strip leading zeros (weird Kodak Scanner software)
+ while ( isset($value[0]) and $value[0] == chr(0))
+ {
+ $value = substr($value, 1);
+ }
+ // remove binary nulls
+ $value = str_replace(chr(0x00), ' ', $value);
+
+ return $value;
+}
+
+$iptc_result = array();
+$imginfo = array();
+getimagesize($filename, $imginfo);
+if (isset($imginfo['APP13']))
+{
+ $iptc = iptcparse($imginfo['APP13']);
+ if (is_array($iptc))
+ {
+ foreach (array_keys($iptc) as $iptc_key)
+ {
+ if (isset($iptc[$iptc_key][0]))
+ {
+ if ($iptc_key == '2#025')
+ {
+ $value = implode(
+ ',',
+ array_map(
+ 'clean_iptc_value',
+ $iptc[$iptc_key]
+ )
+ );
+ }
+ else
+ {
+ $value = clean_iptc_value($iptc[$iptc_key][0]);
+ }
+
+ $iptc_result[$iptc_key] = $value;
+ }
+ }
+ }
+
+ echo 'IPTC Fields in '.$filename.'<br />';
+ $keys = array_keys($iptc_result);
+ sort($keys);
+ foreach ($keys as $key)
+ {
+ echo '<br />'.$key.' = '.$iptc_result[$key];
+ }
+}
+else
+{
+ echo 'no IPTC information';
+}
+
+echo '<br /><br /><br />';
+echo 'EXIF Fields in '.$filename.'<br />';
+$exif = read_exif_data($filename);
+echo '<pre>';
+print_r($exif);
+echo '</pre>';
+?> \ No newline at end of file
diff --git a/BSF/tools/prototype.js b/BSF/tools/prototype.js
new file mode 100644
index 000000000..c5a9ccc05
--- /dev/null
+++ b/BSF/tools/prototype.js
@@ -0,0 +1,3277 @@
+/* Prototype JavaScript framework, version 1.5.1.1
+ * (c) 2005-2007 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.5.1.1',
+
+ Browser: {
+ IE: !!(window.attachEvent && !window.opera),
+ Opera: !!window.opera,
+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
+ },
+
+ BrowserFeatures: {
+ XPath: !!document.evaluate,
+ ElementExtensions: !!window.HTMLElement,
+ SpecificElementExtensions:
+ (document.createElement('div').__proto__ !==
+ document.createElement('form').__proto__)
+ },
+
+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
+ K: function(x) { return x }
+}
+
+var Class = {
+ create: function() {
+ return function() {
+ this.initialize.apply(this, arguments);
+ }
+ }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+ for (var property in source) {
+ destination[property] = source[property];
+ }
+ return destination;
+}
+
+Object.extend(Object, {
+ inspect: function(object) {
+ try {
+ if (object === undefined) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : object.toString();
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+
+ toJSON: function(object) {
+ var type = typeof object;
+ switch(type) {
+ case 'undefined':
+ case 'function':
+ case 'unknown': return;
+ case 'boolean': return object.toString();
+ }
+ if (object === null) return 'null';
+ if (object.toJSON) return object.toJSON();
+ if (object.ownerDocument === document) return;
+ var results = [];
+ for (var property in object) {
+ var value = Object.toJSON(object[property]);
+ if (value !== undefined)
+ results.push(property.toJSON() + ': ' + value);
+ }
+ return '{' + results.join(', ') + '}';
+ },
+
+ keys: function(object) {
+ var keys = [];
+ for (var property in object)
+ keys.push(property);
+ return keys;
+ },
+
+ values: function(object) {
+ var values = [];
+ for (var property in object)
+ values.push(object[property]);
+ return values;
+ },
+
+ clone: function(object) {
+ return Object.extend({}, object);
+ }
+});
+
+Function.prototype.bind = function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [event || window.event].concat(args));
+ }
+}
+
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ return this.toPaddedString(2, 16);
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
+ },
+
+ toPaddedString: function(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
+ },
+
+ toJSON: function() {
+ return isFinite(this) ? this.toString() : 'null';
+ }
+});
+
+Date.prototype.toJSON = function() {
+ return '"' + this.getFullYear() + '-' +
+ (this.getMonth() + 1).toPaddedString(2) + '-' +
+ this.getDate().toPaddedString(2) + 'T' +
+ this.getHours().toPaddedString(2) + ':' +
+ this.getMinutes().toPaddedString(2) + ':' +
+ this.getSeconds().toPaddedString(2) + '"';
+};
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) {}
+ }
+
+ return returnValue;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.callback(this);
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+}
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
+
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = count === undefined ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return this;
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = truncation === undefined ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : this;
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var self = arguments.callee;
+ self.text.data = this;
+ return self.div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = document.createElement('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
+ $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+ div.childNodes[0].nodeValue) : '';
+ },
+
+ toQueryParams: function(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return {};
+
+ return match[1].split(separator || '&').inject({}, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ if (hash[key].constructor != Array) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ return hash;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ succ: function() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ },
+
+ times: function(count) {
+ var result = '';
+ for (var i = 0; i < count; i++) result += this;
+ return result;
+ },
+
+ camelize: function() {
+ var parts = this.split('-'), len = parts.length;
+ if (len == 1) return parts[0];
+
+ var camelized = this.charAt(0) == '-'
+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+ : parts[0];
+
+ for (var i = 1; i < len; i++)
+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+ return camelized;
+ },
+
+ capitalize: function() {
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ },
+
+ underscore: function() {
+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+ },
+
+ dasherize: function() {
+ return this.gsub(/_/,'-');
+ },
+
+ inspect: function(useDoubleQuotes) {
+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ },
+
+ toJSON: function() {
+ return this.inspect(true);
+ },
+
+ unfilterJSON: function(filter) {
+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
+ },
+
+ isJSON: function() {
+ var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ },
+
+ evalJSON: function(sanitize) {
+ var json = this.unfilterJSON();
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ },
+
+ include: function(pattern) {
+ return this.indexOf(pattern) > -1;
+ },
+
+ startsWith: function(pattern) {
+ return this.indexOf(pattern) === 0;
+ },
+
+ endsWith: function(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.lastIndexOf(pattern) === d;
+ },
+
+ empty: function() {
+ return this == '';
+ },
+
+ blank: function() {
+ return /^\s*$/.test(this);
+ }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+ escapeHTML: function() {
+ return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ unescapeHTML: function() {
+ return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (typeof replacement == 'function') return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+ div: document.createElement('div'),
+ text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ return this.template.gsub(this.pattern, function(match) {
+ var before = match[1];
+ if (before == '\\') return match[2];
+ return before + String.interpret(object[match[3]]);
+ });
+ }
+}
+
+var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+var Enumerable = {
+ each: function(iterator) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ iterator(value, index++);
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ },
+
+ eachSlice: function(number, iterator) {
+ var index = -number, slices = [], array = this.toArray();
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.map(iterator);
+ },
+
+ all: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!(iterator || Prototype.K)(value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator) {
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!(iterator || Prototype.K)(value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push((iterator || Prototype.K)(value, index));
+ });
+ return results;
+ },
+
+ detect: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator(value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(pattern, iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ var stringValue = value.toString();
+ if (stringValue.match(pattern))
+ results.push((iterator || Prototype.K)(value, index));
+ })
+ return results;
+ },
+
+ include: function(object) {
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inGroupsOf: function(number, fillWith) {
+ fillWith = fillWith === undefined ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ },
+
+ inject: function(memo, iterator) {
+ this.each(function(value, index) {
+ memo = iterator(memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator) {
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ ((iterator || Prototype.K)(value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator) {
+ return this.map(function(value, index) {
+ return {value: value, criteria: iterator(value, index)};
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.map();
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (typeof args.last() == 'function')
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ size: function() {
+ return this.toArray().length;
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+}
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0, length = iterable.length; i < length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+}
+
+if (Prototype.Browser.WebKit) {
+ $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (!(typeof iterable == 'function' && iterable == '[object NodeList]') &&
+ iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0, length = iterable.length; i < length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+ }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+ Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(value && value.constructor == Array ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ indexOf: function(object) {
+ for (var i = 0, length = this.length; i < length; i++)
+ if (this[i] == object) return i;
+ return -1;
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ reduce: function() {
+ return this.length > 1 ? this : this[0];
+ },
+
+ uniq: function(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ },
+
+ clone: function() {
+ return [].concat(this);
+ },
+
+ size: function() {
+ return this.length;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ },
+
+ toJSON: function() {
+ var results = [];
+ this.each(function(object) {
+ var value = Object.toJSON(object);
+ if (value !== undefined) results.push(value);
+ });
+ return '[' + results.join(', ') + ']';
+ }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+ Array.prototype.concat = function() {
+ var array = [];
+ for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ if (arguments[i].constructor == Array) {
+ for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+ array.push(arguments[i][j]);
+ } else {
+ array.push(arguments[i]);
+ }
+ }
+ return array;
+ }
+}
+var Hash = function(object) {
+ if (object instanceof Hash) this.merge(object);
+ else Object.extend(this, object || {});
+};
+
+Object.extend(Hash, {
+ toQueryString: function(obj) {
+ var parts = [];
+ parts.add = arguments.callee.addPair;
+
+ this.prototype._each.call(obj, function(pair) {
+ if (!pair.key) return;
+ var value = pair.value;
+
+ if (value && typeof value == 'object') {
+ if (value.constructor == Array) value.each(function(value) {
+ parts.add(pair.key, value);
+ });
+ return;
+ }
+ parts.add(pair.key, value);
+ });
+
+ return parts.join('&');
+ },
+
+ toJSON: function(object) {
+ var results = [];
+ this.prototype._each.call(object, function(pair) {
+ var value = Object.toJSON(pair.value);
+ if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value);
+ });
+ return '{' + results.join(', ') + '}';
+ }
+});
+
+Hash.toQueryString.addPair = function(key, value, prefix) {
+ key = encodeURIComponent(key);
+ if (value === undefined) this.push(key);
+ else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
+}
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+ _each: function(iterator) {
+ for (var key in this) {
+ var value = this[key];
+ if (value && value == Hash.prototype[key]) continue;
+
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ merge: function(hash) {
+ return $H(hash).inject(this, function(mergedHash, pair) {
+ mergedHash[pair.key] = pair.value;
+ return mergedHash;
+ });
+ },
+
+ remove: function() {
+ var result;
+ for(var i = 0, length = arguments.length; i < length; i++) {
+ var value = this[arguments[i]];
+ if (value !== undefined){
+ if (result === undefined) result = value;
+ else {
+ if (result.constructor != Array) result = [result];
+ result.push(value)
+ }
+ }
+ delete this[arguments[i]];
+ }
+ return result;
+ },
+
+ toQueryString: function() {
+ return Hash.toQueryString(this);
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ },
+
+ toJSON: function() {
+ return Hash.toJSON(this);
+ }
+});
+
+function $H(object) {
+ if (object instanceof Hash) return object;
+ return new Hash(object);
+};
+
+// Safari iterates over shadowed properties
+if (function() {
+ var i = 0, Test = function(value) { this.key = value };
+ Test.prototype.key = 'foo';
+ for (var property in new Test('bar')) i++;
+ return i > 1;
+}()) Hash.prototype._each = function(iterator) {
+ var cache = [];
+ for (var key in this) {
+ var value = this[key];
+ if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
+ cache.push(key);
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+};
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+}
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responder) {
+ if (!this.include(responder))
+ this.responders.push(responder);
+ },
+
+ unregister: function(responder) {
+ this.responders = this.responders.without(responder);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (typeof responder[callback] == 'function') {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) {}
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() {
+ Ajax.activeRequestCount++;
+ },
+ onComplete: function() {
+ Ajax.activeRequestCount--;
+ }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: ''
+ }
+ Object.extend(this.options, options || {});
+
+ this.options.method = this.options.method.toLowerCase();
+ if (typeof this.options.parameters == 'string')
+ this.options.parameters = this.options.parameters.toQueryParams();
+ }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+ _complete: false,
+
+ initialize: function(url, options) {
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = Object.clone(this.options.parameters);
+
+ if (!['get', 'post'].include(this.method)) {
+ // simulate other verbs over post
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ this.parameters = params;
+
+ if (params = Hash.toQueryString(params)) {
+ // when GET, append parameters to URL
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
+
+ try {
+ if (this.options.onCreate) this.options.onCreate(this.transport);
+ Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous)
+ setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ // user-defined headers
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (typeof extras.push == 'function')
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ return !this.transport.status
+ || (this.transport.status >= 200 && this.transport.status < 300);
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState];
+ var transport = this.transport, json = this.evalJSON();
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + this.transport.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ var contentType = this.getHeader('Content-type');
+ if (contentType && contentType.strip().
+ match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+ Ajax.Responders.dispatch('on' + state, this, transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ // avoid memory leak in MSIE: clean up
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name);
+ } catch (e) { return null }
+ },
+
+ evalJSON: function() {
+ try {
+ var json = this.getHeader('X-JSON');
+ return json ? json.evalJSON() : null;
+ } catch (e) { return null }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval((this.transport.responseText || '').unfilterJSON());
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+ initialize: function(container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ }
+
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+
+ var onComplete = this.options.onComplete || Prototype.emptyFunction;
+ this.options.onComplete = (function(transport, param) {
+ this.updateContent();
+ onComplete(transport, param);
+ }).bind(this);
+
+ this.request(url);
+ },
+
+ updateContent: function() {
+ var receiver = this.container[this.success() ? 'success' : 'failure'];
+ var response = this.transport.responseText;
+
+ if (!this.options.evalScripts) response = response.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (this.options.insertion)
+ new this.options.insertion(receiver, response);
+ else
+ receiver.update(response);
+ }
+
+ if (this.success()) {
+ if (this.onComplete)
+ setTimeout(this.onComplete.bind(this), 10);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(container, url, options) {
+ this.setOptions(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = {};
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.options.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(request) {
+ if (this.options.decay) {
+ this.decay = (request.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = request.responseText;
+ }
+ this.timer = setTimeout(this.onTimerEvent.bind(this),
+ this.decay * this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (typeof element == 'string')
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(query.snapshotItem(i));
+ return results;
+ };
+
+ document.getElementsByClassName = function(className, parentElement) {
+ var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+ return document._getElementsByXPath(q, parentElement);
+ }
+
+} else document.getElementsByClassName = function(className, parentElement) {
+ var children = ($(parentElement) || document.body).getElementsByTagName('*');
+ var elements = [], child, pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
+ for (var i = 0, length = children.length; i < length; i++) {
+ child = children[i];
+ var elementClassName = child.className;
+ if (elementClassName.length == 0) continue;
+ if (elementClassName == className || elementClassName.match(pattern))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) var Element = {};
+
+Element.extend = function(element) {
+ var F = Prototype.BrowserFeatures;
+ if (!element || !element.tagName || element.nodeType == 3 ||
+ element._extended || F.SpecificElementExtensions || element == window)
+ return element;
+
+ var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
+ T = Element.Methods.ByTag;
+
+ // extend methods for all tags (Safari doesn't need this)
+ if (!F.ElementExtensions) {
+ Object.extend(methods, Element.Methods),
+ Object.extend(methods, Element.Methods.Simulated);
+ }
+
+ // extend methods for specific tags
+ if (T[tagName]) Object.extend(methods, T[tagName]);
+
+ for (var property in methods) {
+ var value = methods[property];
+ if (typeof value == 'function' && !(property in element))
+ element[property] = cache.findOrStore(value);
+ }
+
+ element._extended = Prototype.emptyFunction;
+ return element;
+};
+
+Element.extend.cache = {
+ findOrStore: function(value) {
+ return this[value] = this[value] || function() {
+ return value.apply(null, [this].concat($A(arguments)));
+ }
+ }
+};
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function(element) {
+ element = $(element);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ return element;
+ },
+
+ hide: function(element) {
+ $(element).style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ $(element).style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: function(element, html) {
+ html = typeof html == 'undefined' ? '' : html.toString();
+ $(element).innerHTML = html.stripScripts();
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ },
+
+ replace: function(element, html) {
+ element = $(element);
+ html = typeof html == 'undefined' ? '' : html.toString();
+ if (element.outerHTML) {
+ element.outerHTML = html.stripScripts();
+ } else {
+ var range = element.ownerDocument.createRange();
+ range.selectNodeContents(element);
+ element.parentNode.replaceChild(
+ range.createContextualFragment(html.stripScripts()), element);
+ }
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(), attribute = pair.last();
+ var value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property) {
+ element = $(element);
+ var elements = [];
+ while (element = element[property])
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return $(element).recursivelyCollect('parentNode');
+ },
+
+ descendants: function(element) {
+ return $A($(element).getElementsByTagName('*')).each(Element.extend);
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
+ },
+
+ immediateDescendants: function(element) {
+ if (!(element = $(element).firstChild)) return [];
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ if (element) return [element].concat($(element).nextSiblings());
+ return [];
+ },
+
+ previousSiblings: function(element) {
+ return $(element).recursivelyCollect('previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return $(element).recursivelyCollect('nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return element.previousSiblings().reverse().concat(element.nextSiblings());
+ },
+
+ match: function(element, selector) {
+ if (typeof selector == 'string')
+ selector = new Selector(selector);
+ return selector.match($(element));
+ },
+
+ up: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = element.ancestors();
+ return expression ? Selector.findElement(ancestors, expression, index) :
+ ancestors[index || 0];
+ },
+
+ down: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ var descendants = element.descendants();
+ return expression ? Selector.findElement(descendants, expression, index) :
+ descendants[index || 0];
+ },
+
+ previous: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+ var previousSiblings = element.previousSiblings();
+ return expression ? Selector.findElement(previousSiblings, expression, index) :
+ previousSiblings[index || 0];
+ },
+
+ next: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+ var nextSiblings = element.nextSiblings();
+ return expression ? Selector.findElement(nextSiblings, expression, index) :
+ nextSiblings[index || 0];
+ },
+
+ getElementsBySelector: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element, args);
+ },
+
+ getElementsByClassName: function(element, className) {
+ return document.getElementsByClassName(className, element);
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (Prototype.Browser.IE) {
+ if (!element.attributes) return null;
+ var t = Element._attributeTranslations;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ var attribute = element.attributes[name];
+ return attribute ? attribute.nodeValue : null;
+ }
+ return element.getAttribute(name);
+ },
+
+ getHeight: function(element) {
+ return $(element).getDimensions().height;
+ },
+
+ getWidth: function(element) {
+ return $(element).getDimensions().width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ if (elementClassName.length == 0) return false;
+ if (elementClassName == className ||
+ elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+ return true;
+ return false;
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element).add(className);
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element).remove(className);
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+ return element;
+ },
+
+ observe: function() {
+ Event.observe.apply(Event, arguments);
+ return $A(arguments).first();
+ },
+
+ stopObserving: function() {
+ Event.stopObserving.apply(Event, arguments);
+ return $A(arguments).first();
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.blank();
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = Position.cumulativeOffset(element);
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ style = style == 'float' ? 'cssFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value) {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ }
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
+
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
+
+ setStyle: function(element, styles, camelized) {
+ element = $(element);
+ var elementStyle = element.style;
+
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property])
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
+ (camelized ? property : property.camelize())] = styles[property];
+
+ return element;
+ },
+
+ setOpacity: function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+ return element;
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ var display = $(element).getStyle('display');
+ if (display != 'none' && display != null) // Safari bug
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ var originalDisplay = els.display;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = 'block';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = originalDisplay;
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (window.opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ return element;
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ return element;
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return element;
+ element._overflow = element.style.overflow || 'auto';
+ if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+ element.style.overflow = 'hidden';
+ return element;
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (!element._overflow) return element;
+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+ element._overflow = null;
+ return element;
+ }
+};
+
+Object.extend(Element.Methods, {
+ childOf: Element.Methods.descendantOf,
+ childElements: Element.Methods.immediateDescendants
+});
+
+if (Prototype.Browser.Opera) {
+ Element.Methods._getStyle = Element.Methods.getStyle;
+ Element.Methods.getStyle = function(element, style) {
+ switch(style) {
+ case 'left':
+ case 'top':
+ case 'right':
+ case 'bottom':
+ if (Element._getStyle(element, 'position') == 'static') return null;
+ default: return Element._getStyle(element, style);
+ }
+ };
+}
+else if (Prototype.Browser.IE) {
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
+
+ if (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset'+style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ style.filter = filter.replace(/alpha\([^\)]*\)/gi,'');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ // IE is missing .innerHTML support for TABLE-related elements
+ Element.Methods.update = function(element, html) {
+ element = $(element);
+ html = typeof html == 'undefined' ? '' : html.toString();
+ var tagName = element.tagName.toUpperCase();
+ if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+ var div = document.createElement('div');
+ switch (tagName) {
+ case 'THEAD':
+ case 'TBODY':
+ div.innerHTML = '<table><tbody>' + html.stripScripts() + '</tbody></table>';
+ depth = 2;
+ break;
+ case 'TR':
+ div.innerHTML = '<table><tbody><tr>' + html.stripScripts() + '</tr></tbody></table>';
+ depth = 3;
+ break;
+ case 'TD':
+ div.innerHTML = '<table><tbody><tr><td>' + html.stripScripts() + '</td></tr></tbody></table>';
+ depth = 4;
+ }
+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
+ depth.times(function() { div = div.firstChild });
+ $A(div.childNodes).each(function(node) { element.appendChild(node) });
+ } else {
+ element.innerHTML = html.stripScripts();
+ }
+ setTimeout(function() { html.evalScripts() }, 10);
+ return element;
+ }
+}
+else if (Prototype.Browser.Gecko) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
+
+Element._attributeTranslations = {
+ names: {
+ colspan: "colSpan",
+ rowspan: "rowSpan",
+ valign: "vAlign",
+ datetime: "dateTime",
+ accesskey: "accessKey",
+ tabindex: "tabIndex",
+ enctype: "encType",
+ maxlength: "maxLength",
+ readonly: "readOnly",
+ longdesc: "longDesc"
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ var node = element.getAttributeNode('title');
+ return node.specified ? node.nodeValue : null;
+ }
+ }
+};
+
+(function() {
+ Object.extend(this, {
+ href: this._getAttr,
+ src: this._getAttr,
+ type: this._getAttr,
+ disabled: this._flag,
+ checked: this._flag,
+ readonly: this._flag,
+ multiple: this._flag
+ });
+}).call(Element._attributeTranslations.values);
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ var t = Element._attributeTranslations, node;
+ attribute = t.names[attribute] || attribute;
+ node = $(element).getAttributeNode(attribute);
+ return node && node.specified;
+ }
+};
+
+Element.Methods.ByTag = {};
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div').__proto__) {
+ window.HTMLElement = {};
+ window.HTMLElement.prototype = document.createElement('div').__proto__;
+ Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.hasAttribute = function(element, attribute) {
+ if (element.hasAttribute) return element.hasAttribute(attribute);
+ return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
+ }
+
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
+
+ if (!tagName) Object.extend(Element.Methods, methods || {});
+ else {
+ if (tagName.constructor == Array) tagName.each(extend);
+ else extend(tagName);
+ }
+
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = {};
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ var cache = Element.extend.cache;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = cache.findOrStore(value);
+ }
+ }
+
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ window[klass] = {};
+ window[klass].prototype = document.createElement(tagName).__proto__;
+ return window[klass];
+ }
+
+ if (F.ElementExtensions) {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+ }
+
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (typeof klass == "undefined") continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
+
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
+};
+
+var Toggle = { display: Element.toggle };
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+ this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+ initialize: function(element, content) {
+ this.element = $(element);
+ this.content = content.stripScripts();
+
+ if (this.adjacency && this.element.insertAdjacentHTML) {
+ try {
+ this.element.insertAdjacentHTML(this.adjacency, this.content);
+ } catch (e) {
+ var tagName = this.element.tagName.toUpperCase();
+ if (['TBODY', 'TR'].include(tagName)) {
+ this.insertContent(this.contentFromAnonymousTable());
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ this.range = this.element.ownerDocument.createRange();
+ if (this.initializeRange) this.initializeRange();
+ this.insertContent([this.range.createContextualFragment(this.content)]);
+ }
+
+ setTimeout(function() {content.evalScripts()}, 10);
+ },
+
+ contentFromAnonymousTable: function() {
+ var div = document.createElement('div');
+ div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+ return $A(div.childNodes[0].childNodes[0].childNodes);
+ }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+ initializeRange: function() {
+ this.range.setStartBefore(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment, this.element);
+ }).bind(this));
+ }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(true);
+ },
+
+ insertContent: function(fragments) {
+ fragments.reverse(false).each((function(fragment) {
+ this.element.insertBefore(fragment, this.element.firstChild);
+ }).bind(this));
+ }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.appendChild(fragment);
+ }).bind(this));
+ }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+ initializeRange: function() {
+ this.range.setStartAfter(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment,
+ this.element.nextSibling);
+ }).bind(this));
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license. Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create();
+
+Selector.prototype = {
+ initialize: function(expression) {
+ this.expression = expression.strip();
+ this.compileMatcher();
+ },
+
+ compileMatcher: function() {
+ // Selectors with namespaced attributes can't use the XPath version
+ if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression))
+ return this.compileXPathMatcher();
+
+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+ c = Selector.criteria, le, p, m;
+
+ if (Selector._cache[e]) {
+ this.matcher = Selector._cache[e]; return;
+ }
+ this.matcher = ["this.matcher = function(root) {",
+ "var r = root, h = Selector.handlers, c = false, n;"];
+
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ this.matcher.push(typeof c[i] == 'function' ? c[i](m) :
+ new Template(c[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.matcher.push("return h.unique(n);\n}");
+ eval(this.matcher.join('\n'));
+ Selector._cache[this.expression] = this.matcher;
+ },
+
+ compileXPathMatcher: function() {
+ var e = this.expression, ps = Selector.patterns,
+ x = Selector.xpath, le, m;
+
+ if (Selector._cache[e]) {
+ this.xpath = Selector._cache[e]; return;
+ }
+
+ this.matcher = ['.//*'];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ if (m = e.match(ps[i])) {
+ this.matcher.push(typeof x[i] == 'function' ? x[i](m) :
+ new Template(x[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.xpath = this.matcher.join('');
+ Selector._cache[this.expression] = this.xpath;
+ },
+
+ findElements: function(root) {
+ root = root || document;
+ if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+ return this.matcher(root);
+ },
+
+ match: function(element) {
+ return this.findElements(document).include(element);
+ },
+
+ toString: function() {
+ return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector:" + this.expression.inspect() + ">";
+ }
+};
+
+Object.extend(Selector, {
+ _cache: {},
+
+ xpath: {
+ descendant: "//*",
+ child: "/*",
+ adjacent: "/following-sibling::*[1]",
+ laterSibling: '/following-sibling::*',
+ tagName: function(m) {
+ if (m[1] == '*') return '';
+ return "[local-name()='" + m[1].toLowerCase() +
+ "' or local-name()='" + m[1].toUpperCase() + "']";
+ },
+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+ id: "[@id='#{1}']",
+ attrPresence: "[@#{1}]",
+ attr: function(m) {
+ m[3] = m[5] || m[6];
+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+ },
+ pseudo: function(m) {
+ var h = Selector.xpath.pseudos[m[1]];
+ if (!h) return '';
+ if (typeof h === 'function') return h(m);
+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+ },
+ operators: {
+ '=': "[@#{1}='#{3}']",
+ '!=': "[@#{1}!='#{3}']",
+ '^=': "[starts-with(@#{1}, '#{3}')]",
+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+ '*=': "[contains(@#{1}, '#{3}')]",
+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+ },
+ pseudos: {
+ 'first-child': '[not(preceding-sibling::*)]',
+ 'last-child': '[not(following-sibling::*)]',
+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
+ 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+ 'checked': "[@checked]",
+ 'disabled': "[@disabled]",
+ 'enabled': "[not(@disabled)]",
+ 'not': function(m) {
+ var e = m[6], p = Selector.patterns,
+ x = Selector.xpath, le, m, v;
+
+ var exclusion = [];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in p) {
+ if (m = e.match(p[i])) {
+ v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m);
+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+ return "[not(" + exclusion.join(" and ") + ")]";
+ },
+ 'nth-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+ },
+ 'nth-last-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+ },
+ 'nth-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("position() ", m);
+ },
+ 'nth-last-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+ },
+ 'first-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+ },
+ 'last-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+ },
+ 'only-of-type': function(m) {
+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+ },
+ nth: function(fragment, m) {
+ var mm, formula = m[6], predicate;
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ if (mm = formula.match(/^(\d+)$/)) // digit only
+ return '[' + fragment + "= " + mm[1] + ']';
+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (mm[1] == "-") mm[1] = -1;
+ var a = mm[1] ? Number(mm[1]) : 1;
+ var b = mm[2] ? Number(mm[2]) : 0;
+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+ "((#{fragment} - #{b}) div #{a} >= 0)]";
+ return new Template(predicate).evaluate({
+ fragment: fragment, a: a, b: b });
+ }
+ }
+ }
+ },
+
+ criteria: {
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+ attr: function(m) {
+ m[3] = (m[5] || m[6]);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+ },
+ pseudo: function(m) {
+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+ },
+ descendant: 'c = "descendant";',
+ child: 'c = "child";',
+ adjacent: 'c = "adjacent";',
+ laterSibling: 'c = "laterSibling";'
+ },
+
+ patterns: {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s|(?=:))/,
+ attrPresence: /^\[([\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/
+ },
+
+ handlers: {
+ // UTILITY FUNCTIONS
+ // joins two collections
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ a.push(node);
+ return a;
+ },
+
+ // marks an array of nodes for counting
+ mark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = true;
+ return nodes;
+ },
+
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = undefined;
+ return nodes;
+ },
+
+ // mark each child node with its position (for nth calls)
+ // "ofType" flag indicates whether we're indexing for nth-of-type
+ // rather than nth-child
+ index: function(parentNode, reverse, ofType) {
+ parentNode._counted = true;
+ if (reverse) {
+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+ node = nodes[i];
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ } else {
+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ },
+
+ // filters out duplicates and extends all nodes
+ unique: function(nodes) {
+ if (nodes.length == 0) return nodes;
+ var results = [], n;
+ for (var i = 0, l = nodes.length; i < l; i++)
+ if (!(n = nodes[i])._counted) {
+ n._counted = true;
+ results.push(Element.extend(n));
+ }
+ return Selector.handlers.unmark(results);
+ },
+
+ // COMBINATOR FUNCTIONS
+ descendant: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName('*'));
+ return results;
+ },
+
+ child: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+ }
+ return results;
+ },
+
+ adjacent: function(nodes) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ var next = this.nextElementSibling(node);
+ if (next) results.push(next);
+ }
+ return results;
+ },
+
+ laterSibling: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, Element.nextSiblings(node));
+ return results;
+ },
+
+ nextElementSibling: function(node) {
+ while (node = node.nextSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ previousElementSibling: function(node) {
+ while (node = node.previousSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ // TOKEN FUNCTIONS
+ tagName: function(nodes, root, tagName, combinator) {
+ tagName = tagName.toUpperCase();
+ var results = [], h = Selector.handlers;
+ if (nodes) {
+ if (combinator) {
+ // fastlane for ordinary descendant combinators
+ if (combinator == "descendant") {
+ for (var i = 0, node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName(tagName));
+ return results;
+ } else nodes = this[combinator](nodes);
+ if (tagName == "*") return nodes;
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName.toUpperCase() == tagName) results.push(node);
+ return results;
+ } else return root.getElementsByTagName(tagName);
+ },
+
+ id: function(nodes, root, id, combinator) {
+ var targetNode = $(id), h = Selector.handlers;
+ if (!nodes && root == document) return targetNode ? [targetNode] : [];
+ if (nodes) {
+ if (combinator) {
+ if (combinator == 'child') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (targetNode.parentNode == node) return [targetNode];
+ } else if (combinator == 'descendant') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.descendantOf(targetNode, node)) return [targetNode];
+ } else if (combinator == 'adjacent') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Selector.handlers.previousElementSibling(targetNode) == node)
+ return [targetNode];
+ } else nodes = h[combinator](nodes);
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node == targetNode) return [targetNode];
+ return [];
+ }
+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+ },
+
+ className: function(nodes, root, className, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ return Selector.handlers.byClassName(nodes, root, className);
+ },
+
+ byClassName: function(nodes, root, className) {
+ if (!nodes) nodes = Selector.handlers.descendant([root]);
+ var needle = ' ' + className + ' ';
+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+ nodeClassName = node.className;
+ if (nodeClassName.length == 0) continue;
+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+ results.push(node);
+ }
+ return results;
+ },
+
+ attrPresence: function(nodes, root, attr) {
+ var results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.hasAttribute(node, attr)) results.push(node);
+ return results;
+ },
+
+ attr: function(nodes, root, attr, value, operator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ var handler = Selector.operators[operator], results = [];
+ for (var i = 0, node; node = nodes[i]; i++) {
+ var nodeValue = Element.readAttribute(node, attr);
+ if (nodeValue === null) continue;
+ if (handler(nodeValue, value)) results.push(node);
+ }
+ return results;
+ },
+
+ pseudo: function(nodes, name, value, root, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ return Selector.pseudos[name](nodes, value, root);
+ }
+ },
+
+ pseudos: {
+ 'first-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.previousElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'last-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.nextElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'only-child': function(nodes, value, root) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+ results.push(node);
+ return results;
+ },
+ 'nth-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root);
+ },
+ 'nth-last-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true);
+ },
+ 'nth-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, false, true);
+ },
+ 'nth-last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true, true);
+ },
+ 'first-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, false, true);
+ },
+ 'last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, true, true);
+ },
+ 'only-of-type': function(nodes, formula, root) {
+ var p = Selector.pseudos;
+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+ },
+
+ // handles the an+b logic
+ getIndices: function(a, b, total) {
+ if (a == 0) return b > 0 ? [b] : [];
+ return $R(1, total).inject([], function(memo, i) {
+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+ return memo;
+ });
+ },
+
+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+ nth: function(nodes, formula, root, reverse, ofType) {
+ if (nodes.length == 0) return [];
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ var h = Selector.handlers, results = [], indexed = [], m;
+ h.mark(nodes);
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (!node.parentNode._counted) {
+ h.index(node.parentNode, reverse, ofType);
+ indexed.push(node.parentNode);
+ }
+ }
+ if (formula.match(/^\d+$/)) { // just a number
+ formula = Number(formula);
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.nodeIndex == formula) results.push(node);
+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (m[1] == "-") m[1] = -1;
+ var a = m[1] ? Number(m[1]) : 1;
+ var b = m[2] ? Number(m[2]) : 0;
+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+ for (var j = 0; j < l; j++)
+ if (node.nodeIndex == indices[j]) results.push(node);
+ }
+ }
+ h.unmark(nodes);
+ h.unmark(indexed);
+ return results;
+ },
+
+ 'empty': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ // IE treats comments as element nodes
+ if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+ results.push(node);
+ }
+ return results;
+ },
+
+ 'not': function(nodes, selector, root) {
+ var h = Selector.handlers, selectorType, m;
+ var exclusions = new Selector(selector).findElements(root);
+ h.mark(exclusions);
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node._counted) results.push(node);
+ h.unmark(exclusions);
+ return results;
+ },
+
+ 'enabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node.disabled) results.push(node);
+ return results;
+ },
+
+ 'disabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.disabled) results.push(node);
+ return results;
+ },
+
+ 'checked': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.checked) results.push(node);
+ return results;
+ }
+ },
+
+ operators: {
+ '=': function(nv, v) { return nv == v; },
+ '!=': function(nv, v) { return nv != v; },
+ '^=': function(nv, v) { return nv.startsWith(v); },
+ '$=': function(nv, v) { return nv.endsWith(v); },
+ '*=': function(nv, v) { return nv.include(v); },
+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+ '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+ },
+
+ matchElements: function(elements, expression) {
+ var matches = new Selector(expression).findElements(), h = Selector.handlers;
+ h.mark(matches);
+ for (var i = 0, results = [], element; element = elements[i]; i++)
+ if (element._counted) results.push(element);
+ h.unmark(matches);
+ return results;
+ },
+
+ findElement: function(elements, expression, index) {
+ if (typeof expression == 'number') {
+ index = expression; expression = false;
+ }
+ return Selector.matchElements(elements, expression || '*')[index || 0];
+ },
+
+ findChildElements: function(element, expressions) {
+ var exprs = expressions.join(','), expressions = [];
+ exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ var results = [], h = Selector.handlers;
+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
+ selector = new Selector(expressions[i].strip());
+ h.concat(results, selector.findElements(element));
+ }
+ return (l > 1) ? h.unique(results) : results;
+ }
+});
+
+function $$() {
+ return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+ reset: function(form) {
+ $(form).reset();
+ return form;
+ },
+
+ serializeElements: function(elements, getHash) {
+ var data = elements.inject({}, function(result, element) {
+ if (!element.disabled && element.name) {
+ var key = element.name, value = $(element).getValue();
+ if (value != null) {
+ if (key in result) {
+ if (result[key].constructor != Array) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return getHash ? data : Hash.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, getHash) {
+ return Form.serializeElements(Form.getElements(form), getHash);
+ },
+
+ getElements: function(form) {
+ return $A($(form).getElementsByTagName('*')).inject([],
+ function(elements, child) {
+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ }
+ );
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('disable');
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('enable');
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ return $(form).getElements().find(function(element) {
+ return element.type != 'hidden' && !element.disabled &&
+ ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || {});
+
+ var params = options.parameters;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (typeof params == 'string') params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(form.readAttribute('action'), options);
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+}
+
+Form.Element.Methods = {
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = {};
+ pair[element.name] = value;
+ return Hash.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type)))
+ element.select();
+ } catch (e) {}
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.blur();
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.disabled = false;
+ return element;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element);
+ default:
+ return Form.Element.Serializers.textarea(element);
+ }
+ },
+
+ inputSelector: function(element) {
+ return element.checked ? element.value : null;
+ },
+
+ textarea: function(element) {
+ return element.value;
+ },
+
+ select: function(element) {
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ // extend element because hasAttribute may not be native
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+ initialize: function(element, frequency, callback) {
+ this.frequency = frequency;
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ var value = this.getValue();
+ var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+ ? this.lastValue != value : String(this.lastValue) != String(value));
+ if (changed) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ Form.getElements(this.element).each(this.registerCallback.bind(this));
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ default:
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) {
+ var Event = new Object();
+}
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+
+ element: function(event) {
+ return $(event.target || event.srcElement);
+ },
+
+ isLeftClick: function(event) {
+ return (((event.which) && (event.which == 1)) ||
+ ((event.button) && (event.button == 1)));
+ },
+
+ pointerX: function(event) {
+ return event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ pointerY: function(event) {
+ return event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ stop: function(event) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else {
+ event.returnValue = false;
+ event.cancelBubble = true;
+ }
+ },
+
+ // find the first node with the given tagName, starting from the
+ // node the event was triggered on; traverses the DOM upwards
+ findElement: function(event, tagName) {
+ var element = Event.element(event);
+ while (element.parentNode && (!element.tagName ||
+ (element.tagName.toUpperCase() != tagName.toUpperCase())))
+ element = element.parentNode;
+ return element;
+ },
+
+ observers: false,
+
+ _observeAndCache: function(element, name, observer, useCapture) {
+ if (!this.observers) this.observers = [];
+ if (element.addEventListener) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.addEventListener(name, observer, useCapture);
+ } else if (element.attachEvent) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.attachEvent('on' + name, observer);
+ }
+ },
+
+ unloadCache: function() {
+ if (!Event.observers) return;
+ for (var i = 0, length = Event.observers.length; i < length; i++) {
+ Event.stopObserving.apply(this, Event.observers[i]);
+ Event.observers[i][0] = null;
+ }
+ Event.observers = false;
+ },
+
+ observe: function(element, name, observer, useCapture) {
+ element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (Prototype.Browser.WebKit || element.attachEvent))
+ name = 'keydown';
+
+ Event._observeAndCache(element, name, observer, useCapture);
+ },
+
+ stopObserving: function(element, name, observer, useCapture) {
+ element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (Prototype.Browser.WebKit || element.attachEvent))
+ name = 'keydown';
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, observer, useCapture);
+ } else if (element.detachEvent) {
+ try {
+ element.detachEvent('on' + name, observer);
+ } catch (e) {}
+ }
+ }
+});
+
+/* prevent memory leaks in IE */
+if (Prototype.Browser.IE)
+ Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ realOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if(element.tagName=='BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p == 'relative' || p == 'absolute') break;
+ }
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ offsetParent: function(element) {
+ if (element.offsetParent) return element.offsetParent;
+ if (element == document.body) return element;
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return element;
+
+ return document.body;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = this.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = this.realOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = this.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ page: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element,'position')=='absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!window.opera || element.tagName=='BODY') {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return [valueL, valueT];
+ },
+
+ clone: function(source, target) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || {})
+
+ // find page position of source
+ source = $(source);
+ var p = Position.page(source);
+
+ // find coordinate system to use
+ target = $(target);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(target,'position') == 'absolute') {
+ parent = Position.offsetParent(target);
+ delta = Position.page(parent);
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if(options.setWidth) target.style.width = source.offsetWidth + 'px';
+ if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.style.position == 'absolute') return;
+ Position.prepare();
+
+ var offsets = Position.positionedOffset(element);
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.width = width + 'px';
+ element.style.height = height + 'px';
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.style.position == 'relative') return;
+ Position.prepare();
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned. For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (Prototype.Browser.WebKit) {
+ Position.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return [valueL, valueT];
+ }
+}
+
+Element.addMethods(); \ No newline at end of file
diff --git a/BSF/tools/pwg_rel_create.sh b/BSF/tools/pwg_rel_create.sh
new file mode 100644
index 000000000..a2a584a61
--- /dev/null
+++ b/BSF/tools/pwg_rel_create.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# +--------------------------------------------------------------------------+
+# | pwg_rel_create.sh |
+# +--------------------------------------------------------------------------+
+# | author : Pierrick LE GALL <http://le-gall.net/pierrick> |
+# | project : Piwigo |
+# +--------------------------------------------------------------------------+
+
+if [ $# -lt 2 ]
+then
+ echo
+ echo 'usage : '$(basename $0)' <tag> <version number>'
+ echo
+ exit 1
+fi
+
+tag=$1
+version=$2
+
+name=phpwebgallery-$version
+
+cd /tmp
+if [ -e $name ]
+then
+ rm -rf $name
+fi
+
+if [ -e $version ]
+then
+ rm -rf $version
+fi
+mkdir $version
+
+# cvs export -r $tag -d $version phpwebgallery
+svn export http://svn.gna.org/svn/phpwebgallery/tags/$tag $name
+# creating mysql.inc.php empty and writeable
+touch $name/include/mysql.inc.php
+chmod a+w $name/include/mysql.inc.php
+
+# find $name -name "*.php" \
+# | xargs grep -l 'branch 1.7' \
+# | xargs perl -pi -e "s/branch 1.7/${version}/g"
+
+cd /tmp
+for ext in zip tar.gz tar.bz2
+do
+ file=$version/$name.$ext
+ if [ -f $file ]
+ then
+ rm $name
+ fi
+done
+
+
+zip -r $version/$name.zip $name
+tar -czf $version/$name.tar.gz $name
+tar -cjf $version/$name.tar.bz2 $name
+
+cd /tmp/$version
+md5sum p* >MD5SUMS
diff --git a/BSF/tools/release_creation.readme b/BSF/tools/release_creation.readme
new file mode 100644
index 000000000..a8b4f6f52
--- /dev/null
+++ b/BSF/tools/release_creation.readme
@@ -0,0 +1,50 @@
+=====================================
+ Piwigo release creation guide
+=====================================
+
+Technical creation
+==================
+
+I take release 1.7.0 as an example. In URLs "plg" is my personnal Gna!
+username, so use yours.
+
+- tag creation tags/release-1_7_0 :
+
+$ svn copy \
+ -r 1999 \
+ -m "Create release 1.7.0 from branch 1.7 r1999" \
+ svn+ssh://plg@svn.gna.org/svn/phpwebgallery/branch/branch-1_7 \
+ svn+ssh://plg@svn.gna.org/svn/phpwebgallery/tags/release-1_7_0
+
+- checkout new Subversion release 1.7.0
+
+$ svn co svn+ssh://plg@svn.gna.org/svn/phpwebgallery/tags/release-1_7_0 1.7.0
+$ cd 1.7.0
+
+- in include/config_default.inc.php, change the following parameters:
+ - $conf['check_upgrade_feed'] = false;
+ - $conf['show_version'] = false;
+ - $conf['show_gt'] = false;
+ - $conf['die_on_sql_error'] = false;
+
+- in include/constants.php, change the PHPWG_VERSION to 1.7.0
+
+- commit your changes to tags/release-1_7_0, with the following comment:
+
+> New version 1.7.0 hard coded.
+>
+> Stable release required modifications: don't show version and generation
+> time in footer, don't check the upgrade feed, don't die on sql errors.
+
+$ svn commit
+
+- create the release (pwg_rel_create.sh is in tools directory)
+
+$ mkdir /tmp/1.7.0
+$ cd /tmp/1.7.0
+$ pwg_rel_create.sh release-1_7_0 1.7.0
+$ md5sum p* > MD5SUMS
+
+- upload the release to Gna! download area
+
+$ scp -r /tmp/1.7.0 plg@download.gna.org:/upload/phpwebgallery/release/1.7/
diff --git a/BSF/tools/ws.htm b/BSF/tools/ws.htm
new file mode 100644
index 000000000..25eb21fe4
--- /dev/null
+++ b/BSF/tools/ws.htm
@@ -0,0 +1,402 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>PWG web service explorer</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<script type="text/javascript" src="prototype.js" ></script>
+
+<script type="text/javascript">
+
+function setVisibility(id, vis)
+{
+ $(id).style.visibility = vis;
+}
+
+function dumpError(err)
+{
+ var s= 'Error ';
+ if ('string' === typeof err )
+ s += err;
+ else
+ {
+ s += err.name+'<br/>';
+ s += err.message;
+ if (err.stack!=null)
+ {//mozilla only
+ s += '<br/><small><pre>'+ err.stack + '</pre></small>';
+ }
+ }
+ $("error").update(s);
+}
+
+var gServiceUrl;
+var gCachedMethods;
+
+Ajax.Responders.register({
+
+onException: function(req, err) {
+ try {
+ document.pwgError = err;
+ dumpError(err);
+ }
+ catch (e)
+ {
+ alert (err);
+ alert (err.message);
+ }
+},
+
+onComplete: function(req, transport) {
+ if (!req.responseIsSuccess())
+ {
+ var s = 'Completion failure\n' + transport.status + ' ' + transport.statusText;
+ if (transport.status>=300)
+ {
+ s += '\n';
+ s += transport.responseText.substr(0,1000);
+ }
+ dumpError(s);
+ }
+ }
+}
+);
+
+
+function pwgGetJsonResult(transport)
+{
+ var resp;
+ try {
+ eval('resp = ' + transport.responseText);
+ }
+ catch (e)
+ {
+ var s = e.message;
+ s += '\n' + transport.responseText.substr(0,1000).escapeHTML();
+ throw new Error( s );
+ }
+ if (resp==null || resp.result==null || resp.stat==null || resp.stat!='ok')
+ {
+ var s = 'JSON evaluation error';
+ if (resp)
+ {
+ if (resp.stat!=null) s+= '\n'+resp.stat;
+ if (resp.message!=null) s+= '\n'+ resp.message;
+ }
+ throw new Error(s);
+ }
+ return resp.result;
+}
+
+function pwgChangeUrl()
+{
+ $("error").update("");
+ setVisibility("methodListWrapper", "hidden");
+ $("methodList").update("");
+ setVisibility("methodWrapper", "hidden");
+ setVisibility("methodDetailWrapper", "hidden");
+
+ gServiceUrl = $F('ws_url');
+ gCachedMethods = new Hash();
+
+ try {
+ var ajaxReq = new Ajax.Request(
+ gServiceUrl,
+ {method:'get', parameters:'format=json&method=reflection.getMethodList',
+ onSuccess: function (r) { onSuccess_getMethodList(r); }
+ }
+ )
+ }catch (e)
+ {
+ dumpError(e);
+ }
+ return false;
+}
+
+function onSuccess_getMethodList(transport)
+{
+ var result = pwgGetJsonResult(transport);
+ var ml = '';
+ for (var i=0; i<result.methods.length; i++)
+ {
+ ml += '<li><a href="#" onclick="return pwgSelectMethod(this.innerHTML)">'+ result.methods[i]+'</a></li>';
+ }
+ $("methodList").update(ml);
+ setVisibility("methodListWrapper", "visible");
+}
+
+function pwgSelectMethod(methodName)
+{
+ $("error").update("");
+ $("methodName").update(methodName);
+ setVisibility("methodDetailWrapper", "hidden");
+ setVisibility("methodWrapper", "visible");
+
+ if ( gCachedMethods[methodName] )
+ fillNewMethod( gCachedMethods[methodName] );
+ else
+ {
+ try {
+ var ajaxReq = new Ajax.Request(
+ gServiceUrl,
+ {method:'get', parameters:'format=json&method=reflection.getMethodDetails&methodName='+methodName,
+ onSuccess: function (r) { onSuccess_getMethodDetails(r); }
+ }
+ )
+ }catch (e)
+ {
+ dumpError( e );
+ }
+ }
+ return false;
+}
+
+function onSuccess_getMethodDetails(transport)
+{
+ var result = pwgGetJsonResult(transport);
+ fillNewMethod( gCachedMethods[result.name] = $H(result) );
+}
+
+function fillNewMethod(method)
+{
+ var methodParamsElt = $("methodParams");
+ while (methodParamsElt.tBodies[0].rows.length)
+ methodParamsElt.tBodies[0].deleteRow(methodParamsElt.tBodies[0].rows.length-1);
+
+ if (method.params && method.params.length>0)
+ {
+ for (var i=0; i<method.params.length; i++)
+ {
+ var row = methodParamsElt.tBodies[0].insertRow(-1);
+ var isOptional = method.params[i].optional;
+ var acceptArray = method.params[i].acceptArray;
+ var defaultValue = method.params[i].defaultValue == null ? '' : method.params[i].defaultValue;
+
+ row.insertCell(0).innerHTML = method.params[i].name;
+ row.insertCell(1).innerHTML = '<span title="parameter is '+(isOptional ? 'optional':'required') +'">'+(isOptional ? '?':'*')+'</span>'
+ + (method.params[i].acceptArray ? ' <span title="parameter can be an array; use | (pipe) character to split values">[ ]</span>':'');
+ row.insertCell(2).innerHTML = '<input id="methodParameterSend_'+i+'" type="checkbox" '+(isOptional ? '':'checked="checked"')+'/>';
+ row.insertCell(3).innerHTML = '<input id="methodParameterValue_'+i+'"" value="'+defaultValue+'" style="width:99%" onchange="$(\'methodParameterSend_'+i+'\').checked=true;"/>';
+ }
+ }
+ $("methodDescription").update(method.description);
+ setVisibility("methodDetailWrapper", "visible");
+}
+
+function pwgInvokeMethod( newWindow )
+{
+ var methodName = $('methodName').innerHTML;
+ var method = gCachedMethods[methodName];
+
+ var reqUrl = gServiceUrl;
+ reqUrl += "?format="+$F('responseFormat');
+
+ if ($('requestFormat').value == 'get')
+ {
+ reqUrl += "&method="+methodName;
+ for ( var i=0; i<method.params.length; i++)
+ {
+ if (! $('methodParameterSend_'+i).checked)
+ continue;
+
+ if ( method.params[i].acceptArray && $F('methodParameterValue_'+i).split('|').length > 1 )
+ {
+ $F('methodParameterValue_'+i).split('|').each(
+ function(v) {
+ reqUrl += '&'+method.params[i].name+'[]='+v;
+ }
+ );
+ }
+ else
+ reqUrl += '&'+method.params[i].name+'='+$F('methodParameterValue_'+i);
+ }
+ if ( !newWindow )
+ $("invokeFrame").src = reqUrl;
+ else
+ window.open(reqUrl);
+ }
+ else
+ {
+ var form = $("invokeForm");
+ form.action = reqUrl;
+ var t = '<input type="hidden" name="'+'method'+'" value="'+methodName+'"/>';
+ for ( var i=0; i<method.params.length; i++)
+ {
+ if (! $('methodParameterSend_'+i).checked)
+ continue;
+
+ if ( method.params[i].acceptArray && $F('methodParameterValue_'+i).split('|').length > 1 )
+ {
+ $F('methodParameterValue_'+i).split('|').each(
+ function(v) {
+ t += '<input type="hidden" name="'+method.params[i].name+'[]" value="'+v+'"/>';
+ }
+ );
+ }
+ else
+ t += '<input type="hidden" name="'+method.params[i].name+'" value="'+$F('methodParameterValue_'+i)+'"/>';
+ }
+ form.innerHTML = t;
+ form.target = newWindow ? "_blank" : "invokeFrame";
+ form.submit();
+ }
+ return false;
+}
+</script>
+
+
+<style>
+#methodListWrapper {
+ width: 13em;
+ float: left;
+ display: inline;
+ visibility: hidden;
+}
+
+#methodList {
+ padding-left: 10px;
+ margin-left: 15px;
+}
+
+#methodWrapper {
+ margin-left: 14em;
+ visibility: hidden;
+}
+
+#methodName {
+ margin-top: 0;
+ margin-bottom: 3px;
+}
+
+
+#error {
+ height: 90px;
+ overflow: scroll;
+ color: red;
+}
+
+#methodParams {
+ border-collapse: collapse;
+ font-size: small;
+}
+
+#methodParams input {
+ font-size: 90%;
+ border: 1px solid black;
+ text-indent: 2px;
+}
+
+
+a {
+ color: #02f;
+ background-color: white;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: white;
+ background-color: #02f;
+ text-decoration: none;
+ cursor:pointer;
+}
+
+</style>
+
+</head>
+
+
+<body>
+
+<div>
+ <label>PWG Web service url
+ <input name="ws_url" id="ws_url" size="64"/>
+<script type="text/javascript">
+ var match = document.location.toString().match(/^(https?.*\/)tools\/ws\.html?$/);
+ if (match!=null) $('ws_url').value = match[1]+'ws.php';
+</script>
+ </label>
+ <a href="#" onclick="return pwgChangeUrl();">Go!</a>
+</div>
+
+<div id="error">
+</div>
+
+<div>
+
+<div id="methodListWrapper"><h2>Methods</h2>
+ <ul id="methodList">
+ <li><a href="#" onclick="return pwgSelectMethod(this.innerHTML)">getVersion</a></li>
+ </ul>
+</div>
+
+<div id="methodWrapper">
+ <h2 id="methodName"></h2>
+ <div id="methodDetailWrapper">
+
+ <table>
+ <tr style="vertical-align:top">
+
+ <td>
+ <div id="methodDescription"></div>
+ <table>
+ <tr>
+ <td>Request format:</td>
+ <td>
+ <select id="requestFormat">
+ <option value="get" selected="selected">GET</option>
+ <option value="post">POST</option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <td>Response format:</td>
+ <td>
+ <select id="responseFormat">
+ <option value="rest" selected="selected">REST (xml)</option>
+ <option value="json">JSON</option>
+ <option value="php">PHP serial</option>
+ <option value="xmlrpc">XML RPC</option>
+ </select>
+ </td>
+ </tr>
+ </table>
+ <p>
+ <a href="#" onclick="return pwgInvokeMethod(false)">Invoke</a>
+ <a href="#" onclick="return pwgInvokeMethod(true)">Invoke (new Window)</a>
+ </p>
+ </td>
+
+
+ <td>
+ <table id="methodParams" border="1" cellspacing="0" cellpadding="2px">
+ <thead>
+ <tr>
+ <td style="width:150px">Parameter</td>
+ <td>Extra</td>
+ <td>Send</td>
+ <td style="width:160px">Value</td>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+ </td>
+
+ </tr>
+ </table>
+
+ <div style="display:none;">
+ <!-- hiddenForm for POST -->
+ <form method="post" action="" target="invokeFrame" id="invokeForm">
+ <input type="submit" value="submit"/>
+ </form>
+ </div>
+
+ <iframe width="100%" height="400px" id="invokeFrame" name="invokeFrame" style="clear:both"></iframe>
+ </div> <!-- methodDetailWrapper -->
+</div> <!-- methodWrapper -->
+
+</div>
+
+</body>
+</html>