Merge with 2fda205cc5dee96b60d73fe9d4eff7ff727e0321
authorymh <ymh.work@gmail.com>
Wed, 28 Apr 2010 17:20:48 +0200
changeset 93 8e4e2f2a2fdf
parent 92 50bc52a9d662 (current diff)
parent 91 2fda205cc5de (diff)
child 94 fd776c2b3062
Merge with 2fda205cc5dee96b60d73fe9d4eff7ff727e0321
--- a/web/thdProject/apps/frontend/config/settings.yml	Wed Apr 28 17:19:34 2010 +0200
+++ b/web/thdProject/apps/frontend/config/settings.yml	Wed Apr 28 17:20:48 2010 +0200
@@ -27,7 +27,8 @@
     # Output escaping settings
     escaping_strategy:      false            # Determines how variables are made available to templates. Accepted values: on, off.
     escaping_method:        ESC_SPECIALCHARS # Function or helper used for escaping. Accepted values: ESC_RAW, ESC_ENTITIES, ESC_JS, ESC_JS_NO_ENTITIES, and ESC_SPECIALCHARS.
-    standard_helpers:       [Partial, Cache, Form, ThdHtml]
+    standard_helpers:       [Partial, Cache, Form, ThdHtml, UcImageScaler]
+    enabled_modules:        [default, UcImageScaler]
     
     login_module:           account
     login_action:           loginUser
--- a/web/thdProject/apps/frontend/modules/homepage/templates/_randomFilm.php	Wed Apr 28 17:19:34 2010 +0200
+++ b/web/thdProject/apps/frontend/modules/homepage/templates/_randomFilm.php	Wed Apr 28 17:20:48 2010 +0200
@@ -72,7 +72,7 @@
                function playerLoaded(player) {
 
                    // Charge les tags
-    				
+
 
                    tagTool.player = $f("player");
                    tagTool.showTagInPage = false;
@@ -81,7 +81,7 @@
     </script>
 <div id="player">
 	<h3 class="head">Regardez et annotez des extraits :</h3>
-	<div id="player-ba" class="player-ba" style="background:transparent url('<?php echo film_image_path($imageFilename); ?>') no-repeat;">
+	<div id="player-ba" class="player-ba" style="background:transparent url('<?php echo uc_url_for_scimage(film_image_path($imageFilename), 720, 405); ?>') no-repeat;">
 		<div class="infos">
 	    	<a href="<?php echo $actionUri; ?>" class="title"><?php echo $film->getTitle(); ?></a><br/><span class="film-infos">De <?php echo thd_render_flat_list($film->getDirectorsArray(), 'name'); ?></span>
 	    </div>
@@ -91,18 +91,18 @@
 		    <li><span class="head">
 		      <i>Tags liés au film :</i>
 			    </span></li>
-			    <?php 
+			    <?php
 				    foreach($film->getTagsArray() as $item) {
-				    	if($item){ 
+				    	if($item){
 				    	$tag = $item;
 				    	echo '<li class="tag-score-'.$tag['score'].'"><a href="">'.$tag.'</a>
 				      </li>';
-				        
+
 				    }
 			    } ?>
-			     
+
 			 </ul>
 	    </div>
-	</div>	
+	</div>
 </div>
 <div class="tag-action"><a href="<?php echo $actionUri ?>" class="link-button">Tagger le film</a></div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/config/config.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,4 @@
+<?php
+
+$this->dispatcher->connect('routing.load_configuration', array('UcImageScalerEventListener', 'listenToRoutingLoadConfigurationEvent'));
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/lib/UcImageCache.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,7 @@
+<?php
+
+class UcImageCache extends sfFileCache {
+  public function getImageMd5($key) {
+    return md5_file($this->getFilePath($key));
+  }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/lib/UcImageCacheManager.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,83 @@
+<?php
+
+
+
+class UcImageCacheManager
+{
+  protected static $instance;
+
+  protected $cache;
+
+  protected function __construct() {
+    $this->loadBaseConfiguration();
+  }
+  /**
+   * Load the base configuration
+   */
+  public function loadBaseConfiguration() {
+    $this->cache = new UcImageCache(array('cache_dir' => sfConfig::get('sf_app_cache_dir').'/uc_image'));
+  }
+
+  public static function createImageIdFor($imageUrl) {
+    return md5($imageUrl);
+  }
+
+  public static function getInstance() {
+    if(is_null(self::$instance)) {
+      self::$instance = new UcImageCacheManager();
+    }
+
+    return self::$instance;
+  }
+
+  /**
+   * return cached image id
+   *
+   * @param string $urlImage
+   * @param int $mw
+   * @param int $mh
+   * @return string
+   */
+  public function getOrCreatedCachedImageId($imageUrl, $mw, $mh, $useTransparency=false, $keepRatio=true) {
+    $imageId = self::createImageIdFor($imageUrl);
+    $imageKey = $imageId.'-'.$mw.'x'.$mh;
+
+    //not already cached
+    if(!$this->cache->has($imageKey)) {
+      $thumbnail = new sfThumbnail($mw, $mh, $keepRatio, true, 80, 'sfImageMagickAdapter');
+
+      try {
+        $thumbnail->loadFile($imageUrl);
+      } catch (Exception $e) {
+        // Image could not be loaded. Use a default one
+        sfContext::getInstance() -> getLogger() -> warning( "Unable to create cached image of ".$imageUrl." for the following reason : ".
+          $e->getMessage() .". 404 image will be used");
+
+        if ($mw > $mh) {
+          $imagePath = sfConfig::get('app_image_cache_landscape_image_path');
+        } else {
+          $imagePath = sfConfig::get('app_image_cache_portrait_image_path');
+        }
+
+        $thumbnail->loadFile($imagePath);
+      }
+
+      $contentType = ($useTransparency) ? null : 'image/jpeg';
+      $this->cache->set($imageKey, $thumbnail->toString($contentType));
+
+      // Free all ressources
+      $thumbnail->freeSource();
+      $thumbnail->freeAll();
+    }
+
+    return $imageId;
+  }
+
+  public function getImage($imageKey) {
+    return $this->cache->get($imageKey);
+  }
+
+  public function getImageMd5($imageKey) {
+    return $this->cache->getImageMd5($imageKey);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/lib/UcImageScalerEventListener.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,16 @@
+<?php
+
+class UcImageScalerEventListener
+{
+  static public function listenToRoutingLoadConfigurationEvent(sfEvent $event)
+  {
+    $r = $event->getSubject();
+    if( !$r -> hasRouteName( 'UcImageScalerCachedImageUrl') )
+    {
+      $r->prependRoute('UcImageScalerCachedImageUrl',
+      new sfRoute('/dynimage/:mw/:mh/:fid/*',
+      array('module' => 'UcImageScaler', 'action' => 'ImageDisplay')
+      ));
+    }
+  }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/lib/UcImageUtil.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,297 @@
+<?php
+
+//// cyqui@smile.fr Commentaire useless : Cette classe n'est pas completement utilisé, je l'ai mise car je pense que vous aurez besoin d'effectuer des operations sur les images.
+//// et elle etait dans ma bibliotheque personnelle.
+class UcImageUtil
+{
+
+
+    /**
+     * Resize an image according to his original proportions.
+     *
+     *
+     * @param string $src ( source path of an image in gif / jpeg fromats )
+     * @param int $w
+     * @param int $h
+     * @param string $dst ( file to write, or null )
+     * @return mixed
+     *
+     */
+    public static function resample_picfile( $src, $w, $h, $dst = null, $extname=null )
+    {
+
+        $extname    = strtolower( substr( strrchr( $src , "."), 1 ) );
+
+        switch( $extname )
+        {
+            case 'jpeg':
+            case 'jpg':
+
+                $create_func = 'imagecreatefromjpeg';
+                $out_func    = 'imagejpeg';
+
+            break;
+            case 'png':
+
+                $create_func = 'imagecreatefrompng';
+                $out_func    = 'imagepng';
+
+            break;
+            case 'gif' :
+
+                $create_func = 'imagecreatefromgif';
+                $out_func    = 'imagegif';
+
+            break;
+
+            //autodetect
+            default:
+
+            $imageinfos = @getimagesize( $src );
+
+            switch( $imageinfos[2] )
+            {
+            case IMAGETYPE_JPEG:
+                $t = 'jpeg';
+            break;
+            case IMAGETYPE_PNG:
+                $t = 'png';
+            break;
+            case IMAGETYPE_GIF:
+                $t = 'gif';
+            break;
+            }
+
+            $create_func = 'imagecreatefrom'.$t;
+
+            break;
+        }
+
+        $src_img = $create_func($src);
+
+        if( $src_img )
+        {
+            $src_w  = imagesx($src_img);
+            $src_h  = imagesy($src_img);
+
+            $scaleX = $w / $src_w;
+            $scaleY = $h / $src_h;
+            $scale  = min($scaleX, $scaleY);
+
+            $dstW   = $w;
+            $dstH   = $h;
+            $dstX   = $dstY = 0;
+
+            $scaleR = $scaleX / $scaleY;
+
+            $dstW = (int)($scale * $src_w + 0.5);
+            $dstH = (int)($scale * $src_h + 0.5);
+
+            $dstX = 0;
+            $dstY = 0;
+
+            $dst_img = ImageCreateTrueColor( $dstW, $dstH );
+            imagecopyresampled(
+                $dst_img, $src_img, $dstX, $dstY, 0, 0,
+                $dstW, $dstH, $src_w, $src_h);
+
+
+            //write image to file
+            if( !is_null( $dst ) )
+            {
+                if ($t == 'jpeg') {
+                  $res = imagejpeg($dst_img, $dst, 100);
+                } elseif ($t == 'png') {
+                  $res = imagepng($dst_img, $dst, 9);
+                } else {
+                  $res = imagegif($dst_img, $dst);
+                }
+
+                imagedestroy( $dst_img );
+            }
+            else //display image binary
+            {
+                if ($t == 'jpeg') {
+                  return imagejpeg($dst_img, null, 100);
+                } elseif ($t == 'png') {
+                  return imagepng($dst_img, null, 9);
+                } else {
+                  return imagegif($dst_img, null);
+                }
+            }
+
+
+            imagedestroy( $src_img );
+
+            if( !is_null( $dst ) )
+            {
+                 return file_exists( $dst );
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Resize an image according to his original proportions.
+     *
+     *
+     * @param resource $src ( source path of an image in gif / jpeg fromats )
+     * @param int $w
+     * @param int $h
+     * @param string $dst ( file to write, or null )
+     * @return mixed
+     *
+     */
+    public static function resample_pic_resource( $src_img, $w, $h )
+    {
+
+        $lowend      = 0.8;
+        $highend     = 1.25;
+
+        if( $src_img )
+        {
+            $src_w  = imagesx($src_img);
+            $src_h  = imagesy($src_img);
+
+            $scaleX = (float)$w / $src_w;
+            $scaleY = (float)$h / $src_h;
+            $scale  = min($scaleX, $scaleY);
+
+            $dstW   = $w;
+            $dstH   = $h;
+            $dstX   = $dstY = 0;
+
+            $scaleR = $scaleX / $scaleY;
+
+            $dstW = (int)($scale * $src_w + 0.5);
+            $dstH = (int)($scale * $src_h + 0.5);
+
+            $dstX = 0;
+            $dstY = 0;
+
+            $dst_img = ImageCreateTrueColor( $dstW, $dstH );
+            imagecopyresampled(
+                $dst_img, $src_img, $dstX, $dstY, 0, 0,
+                $dstW, $dstH, $src_w, $src_h);
+
+
+           return $dst_img;
+        }
+
+        return false;
+    }
+
+    public static function getMimeTypeForExt( $ext )
+    {
+        switch( $ext )
+        {
+            case 'jpg':
+            case 'jpeg':
+
+                return 'image/jpeg';
+
+            break;
+
+            case 'gif':
+
+                return 'image/gif';
+
+            break;
+
+        }
+    }
+
+    /**
+     * Effectue une rotation de 90, 180, 270° sur une image et retourne l'image modifiée
+     *
+     * @param resource $imgSrc
+     * @param int $angle
+     * @return resource image
+     * @see http://fr2.php.net/imagecopy ( original source code )
+     */
+    public static function ImageRotateRightAngle( $imgSrc, $angle )
+    {
+
+        // ensuring we got really RightAngle (if not we choose the closest one)
+        $angle = min( ( (int)(($angle+45) / 90) * 90), 270 );
+
+        // no need to fight
+        if( $angle == 0 )
+           return( $imgSrc );
+
+        // dimenstion of source image
+        $srcX = imagesx( $imgSrc );
+        $srcY = imagesy( $imgSrc );
+
+        switch( $angle )
+        {
+        case 90:
+           $imgDest = imagecreatetruecolor( $srcY, $srcX );
+           for( $x=0; $x<$srcX; $x++ )
+               for( $y=0; $y<$srcY; $y++ )
+                   imagecopy($imgDest, $imgSrc, $srcY-$y-1, $x, $x, $y, 1, 1);
+           break;
+
+        case 180:
+           $imgDest = self::ImageFlip( $imgSrc, self::IMAGE_FLIP_BOTH );
+           break;
+
+        case 270:
+           $imgDest = imagecreatetruecolor( $srcY, $srcX );
+           for( $x=0; $x<$srcX; $x++ )
+               for( $y=0; $y<$srcY; $y++ )
+                   imagecopy($imgDest, $imgSrc, $y, $srcX-$x-1, $x, $y, 1, 1);
+           break;
+        }
+
+        return $imgDest;
+    }
+
+    /**
+     *
+     * @param resource $imgSrc
+     * @param int $type une valeur parmi IMAGE_FLIP_HORIZONTAL|IMAGE_FLIP_VERTICAL|IMAGE_FLIP_BOTH
+     * @return resource image
+     * @see http://fr2.php.net/imagecopy ( original source code )
+     */
+    public static function ImageFlip($imgsrc, $type)
+    {
+      $width = imagesx($imgsrc);
+      $height = imagesy($imgsrc);
+
+      $imgdest = imagecreatetruecolor($width, $height);
+       ImageAlphaBlending($imgdest, false);
+
+      switch( $type )
+         {
+         // mirror wzgl. osi
+         case self::IMAGE_FLIP_HORIZONTAL:
+             for( $y=0 ; $y<$height ; $y++ )
+                 imagecopy($imgdest, $imgsrc, 0, $height-$y-1, 0, $y, $width, 1);
+             break;
+
+         case self::IMAGE_FLIP_VERTICAL:
+             for( $x=0 ; $x<$width ; $x++ )
+                 imagecopy($imgdest, $imgsrc, $width-$x-1, 0, $x, 0, 1, $height);
+             break;
+
+         case self::IMAGE_FLIP_BOTH:
+             for( $x=0 ; $x<$width ; $x++ )
+                 imagecopy($imgdest, $imgsrc, $width-$x-1, 0, $x, 0, 1, $height);
+
+             $rowBuffer = imagecreatetruecolor($width, 1);
+             for( $y=0 ; $y<($height/2) ; $y++ )
+                 {
+                 imagecopy($rowBuffer, $imgdest  , 0, 0, 0, $height-$y-1, $width, 1);
+                 imagecopy($imgdest  , $imgdest  , 0, $height-$y-1, 0, $y, $width, 1);
+                 imagecopy($imgdest  , $rowBuffer, 0, $y, 0, 0, $width, 1);
+                 }
+
+             imagedestroy( $rowBuffer );
+             break;
+         }
+
+      return $imgdest;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/lib/helper/UcImageScalerHelper.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * The route UcImageCachedUrl is used to compose the URL.
+ *
+ # Sample declaration :
+ UcImageCachedUrl:
+ url: /dynimage/:mw/:mh/:fid/*
+ param: { module: ucimagescaler, action: ImageDisplay }
+
+ */
+function uc_url_for_scimage( $urlImage, $mw, $mh, $useTransparency=false, $filename='', $keepRatio=true)
+{
+  if( $filename == '' )
+  {
+    $filename = strrchr( $urlImage, "/" );
+    $filename = substr($filename, 1);
+
+    // Create jpeg
+    if (!$useTransparency) {
+      $filename = substr($filename, 0, strrpos($filename, '.')).'.jpg';
+    }
+  }
+
+  $imgid    = UcImageCacheManager::getInstance() -> getOrCreatedCachedImageId($urlImage, $mw, $mh , $useTransparency, $keepRatio);
+  $imageUrl =  sfConfig::get('app_image_cache_host').url_for("@UcImageScalerCachedImageUrl?mw=$mw&mh=$mh&fid=$imgid&fname=$filename");
+  return $imageUrl;
+}
+/**
+ *
+ * return an image tag
+ * Sample :
+
+ <div style="background-color:red;">
+ <?php
+ echo uc_imgtag_scimage('http://www.fond-ecran-image.com/galerie-membre,mante-religieuse,manthe-jpeg.jpg', "100", "50", "mantereligieuse.jpg" );
+ echo uc_imgtag_scimage('http://www.fond-ecran-image.com/galerie-membre,mante-religieuse,manthe-jpeg.jpg', "200", "100", "mantereligieuse.jpg" );
+ echo uc_imgtag_scimage('http://www.fond-ecran-image.com/galerie-membre,mante-religieuse,manthe-jpeg.jpg', "500", "500", "mantereligieuse.jpg" );
+ echo uc_imgtag_scimage('http://www.fond-ecran-image.com/ginexistant.jpg', "500", "500", "mantereligieuse.jpg" );
+ ?>
+ </div>
+
+ * @param string $urlImage
+ * @param int $mw
+ * @param int $mh
+ * @param string $filename
+ * @param array $htmlattributes
+ * @return string
+ */
+function uc_imgtag_scimage( $urlImage, $mw, $mh, $useTransparency=false, $filename = '', $htmlattributes = Array() )
+{
+  $url   = uc_url_for_scimage( $urlImage, $mw, $mh, $useTransparency, $filename );
+  $attrs = '';
+
+  foreach( $htmlattributes as $name=>$value )
+  {
+    $attrs = ' '.$name.'="'.$value.'"';
+  }
+  return '<img src="'.$url.'"'.$attrs.' />';
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/modules/UcImageScaler/actions/ImageDisplayAction.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * Display an image according to his source, and cache options
+ *
+ * request parameters :
+ *
+ *  -mw max width of the image
+ *  -mh max height of the image
+ *  -src [ external, relative_to_webdir ]
+ *  -fid path identifying the image
+ */
+class ImageDisplayAction extends sfAction {
+	public function execute($request) {
+  	// Parse request
+  	$fid = $request->getParameter('fid');
+    $mw = $request->getParameter('mw');
+    $mh = $request->getParameter('mh');
+    $imageKey = $fid.'-'.$mw.'x'.$mh;
+
+    // Initialize response headers
+    $response = $this->getResponse();
+    $manager = UcImageCacheManager::getInstance();
+    $etag = $manager->getImageMd5($imageKey);
+
+    // Check etag
+    if ($request->getHttpHeader('IF_NONE_MATCH') == $etag) {
+        $response->setStatusCode(304);
+        $response->setHeaderOnly(true);
+        return sfView::NONE;
+    }
+
+    // Send resource
+    $uri = $request->getUri();
+    $ext = substr($uri, strrpos($uri, ".")+1);
+    $contentType = 'image/jpeg';
+    if ($ext == 'png') $contentType = 'image/png';
+    $response->clearHttpHeaders();
+    $response->setHttpHeader("Pragma", "public");
+    $response->setHttpHeader("ETag", $etag);
+    $response->setHttpHeader("Content-Type", $contentType);
+
+    // Set response content
+    $response->setContent($manager->getImage($imageKey));
+    return sfView::NONE;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/UcImageScalerPlugin/modules/UcImageScaler/config/view.yml	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,2 @@
+all:
+  has_layout: false
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfThumbnailPlugin/LICENSE	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,7 @@
+Copyright (c) 2004-2006 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfThumbnailPlugin/README	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,203 @@
+= sfThumbnailPlugin plugin =
+
+The `sfThumbnailPlugin` creates thumbnails from images. It relies on your
+choice of the [http://php.net/gd/ GD] or [http://www.imagemagick.org
+ImageMagick] libraries.
+
+== Installation ==
+
+To install the plugin for a symfony project, the usual process is to use the
+symfony command line.
+
+With symfony 1.0, use:
+
+{{{
+$ symfony plugin-install http://plugins.symfony-project.com/sfThumbnailPlugin
+}}}
+
+With symfony 1.1/1.2, use:
+
+{{{
+$ symfony plugin:install sfThumbnailPlugin
+}}}
+
+Alternatively, if you don't have PEAR installed, you can download the latest
+package attached to this plugin's wiki page and extract it under your project's
+`plugins/` directory. 
+
+Clear the cache to enable the autoloading to find the new classes:
+{{{
+$ php symfony cc
+}}}
+
+You're done.
+
+'''Note''': If the [http://php.net/gd GD library] is not activated, you might
+have to uncomment the related line in your `php.ini` and restart your web
+server to enable PHP image handling functions.
+
+'''Note''': To use !ImageMagick, you'll need to download and install the
+binaries from http://www.imagemagick.org.
+
+== Contents ==
+
+The plugin contains three classes, `sfThumbnail`, `sfGDAdapter` and
+`sfImageMagickAdapter`. Available methods are:
+
+{{{
+// Initialize the thumbnail attributes
+__construct($maxWidth = null, $maxHeight = null, $scale = true, $inflate = true, $quality = 75, $adapterClass = null, $adapterOptions = array())
+
+// Load image file from a file system
+loadFile($imageFile) 
+
+// Load image file from a string (GD adapter only, currently)
+loadData($imageString, $mimeType)
+
+// Save the thumbnail to a file
+save($thumbFile, $targetMime = null)
+}}}
+
+Supported GD image types are 'image/jpeg', 'image/png' and 'image/gif'.
+
+!ImageMagick supports over [http://www.imagemagick.org/script/formats.php 100
+types].
+
+Note that the $quality setting only applies to JPEG images.
+
+== Usage ==
+
+=== Creating a thumbnail from an existing image file ===
+
+The process of creating a thumbnail from an existing image file is pretty
+straightforward. 
+
+First, you must initialize a new `sfThumbnail` object with two parameters: the
+maximum width and height of the desired thumbnail.
+
+{{{
+// Initialize the object for 150x150 thumbnails
+$thumbnail = new sfThumbnail(150, 150);
+}}}
+
+Then, specify a file path to the image to reduce to the `loadFile()` method.
+
+{{{
+// Load the image to reduce
+$thumbnail->loadFile('/path/to/image/file.png');
+}}}
+
+Finally, ask the thumbnail object to save the thumbnail. You must provide a
+file path. Optionally, if you don't want the thumbnail to use the same mime
+type as the source image, you can specify a mime type as the second parameter.
+
+{{{
+// Save the thumbnail
+$thumbnail->save('/path/to/thumbnail/file.jpg', 'image/jpg');
+}}}
+
+Both the source and destination file paths must be absolute paths in your
+filesystem. To store files under a symfony project directory, make sure you use
+the
+[http://www.symfony-project.com/book/trunk/19-Mastering-Symfony-s-Configuration-Files#The%20Basic%20File%20Structure
+directory constants], accessed by `sfConfig::get()`.
+
+=== Creating a thumbnail for an uploaded file ===
+
+If you upload images, you might need to create thumbnails of each uploaded file. For instance, to save a thumbnail of maximum size 150x150px at the same time as the uploaded image, the form handling action can look like:
+
+{{{
+public function executeUpload()
+{
+  // Retrieve the name of the uploaded file
+  $fileName = $this->getRequest()->getFileName('file');
+
+  // Create the thumbnail
+  $thumbnail = new sfThumbnail(150, 150);
+  $thumbnail->loadFile($this->getRequest()->getFilePath('file'));
+  $thumbnail->save(sfConfig::get('sf_upload_dir').'/thumbnail/'.$fileName, 'image/png');
+
+  // Move the uploaded file to the 'uploads' directory
+  $this->getRequest()->moveFile('file', sfConfig::get('sf_upload_dir').'/'.$fileName);
+
+  // Do whatever is next
+  $this->redirect('media/show?filename='.$fileName); 
+}
+}}}
+
+Don't forget to create the `uploads/thumbnail/` directory before calling the action. 
+
+== !ImageMagick-Specific Usage ==
+
+Usage is the same as above except you need to explicitly call the sfImageMagickAdapter class.
+
+{{{
+$thumbnail = new sfThumbnail(150, 150, true, true, 75, 'sfImageMagickAdapter');
+}}}
+
+=== Custom Options ===
+
+The last option in the constructor is an array that you can use to pass custom
+options to !ImageMagick. Below are some examples of this functionality.
+
+Extract the first page from a PDF document:
+
+{{{
+$thumbnail = new sfThumbnail(150, 150, true, true, 75, 'sfImageMagickAdapter', array('extract' => 1));
+}}}
+
+"1" stands for the first page, "2" for the second page, etc.
+
+If for some reason you use a non-standard name for your !ImageMagick binary, you can specify it like so:
+
+{{{
+$thumbnail = new sfThumbnail(150, 150, true, true, 75, 'sfImageMagickAdapter', array('convert' => 'my_imagemagick_binary'));
+}}}
+
+By default sfThumbnail resizes the image in order to get to the desired width
+and height of the thumbnail. But what if you want to force the thumbnail to be
+a certain width and height but without distorting the image of the source size
+scale is different than the thumbnail size scale.
+
+Now you can use a custom option "method" to achieve just that but "shaving" the
+source image in order to get it to be the same scale as the thumbnail and them
+resize it to the required dimentions.
+
+When "shave_bottom" is used and the image’s width is greater than the
+height then sfThumbnail shaves from both left side and right side until the
+desired scale.
+
+There is one requirement for the "method" option to work as expected and it is
+to turn off scaling (set the third parameter of sfThumbnail to FALSE).
+
+{{{
+$thumbnail = new sfThumbnail(150, 150, false, true, 75, 'sfImageMagickAdapter', array('method' => 'shave_all'));
+
+$thumbnail->loadFile('http://www.walkerbooks.co.uk/assets_walker/dynamic/1172005677146.png');
+$thumbnail->save('/tmp/shave.png', 'image/png'); 
+}}}
+
+== Changelog ==
+
+=== Trunk ===
+
+ * fabien: added a toResource() method to get the image resource for the thumbnail
+
+=== 2007-09-15 | 1.5.0 Stable ===
+
+ * kupokomapa: toString($mime) method now works for both adapters
+ * kupokomapa: loadFile() method can now accept a URI if sfWebBrowserPluging is available
+ * kupokomapa: Implemented "method" option for sfImageMagickAdapter where "method" for now can be "shave_all" or "shave_bottom"
+
+=== 2007-09-12 | 1.4.0 Stable ===
+
+ * davedash: Added toString($mime) function to save file to a string
+
+=== 2007-05-18 | 1.3.0 Stable ===
+
+ * bmeynell: Updated README
+ * bmeynell: Fixed ticket (#1710)
+ * bmeynell: Added adapter support
+ * francois: Updated README
+
+=== 2006-11-29 | 1.2.0 Stable ===
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfThumbnailPlugin/lib/sfGDAdapter.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,213 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) 2004-2007 Fabien Potencier <fabien.potencier@symfony-project.com>
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfGDAdapter provides a mechanism for creating thumbnail images.
+ * @see http://www.php.net/gd
+ *
+ * @package    sfThumbnailPlugin
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @author     Benjamin Meynell <bmeynell@colorado.edu>
+ */
+
+class sfGDAdapter
+{
+
+  protected
+    $sourceWidth,
+    $sourceHeight,
+    $sourceMime,
+    $maxWidth,
+    $maxHeight,
+    $scale,
+    $inflate,
+    $quality,
+    $source,
+    $thumb;
+
+  /**
+   * List of accepted image types based on MIME
+   * descriptions that this adapter supports
+   */
+  protected $imgTypes = array(
+    'image/jpeg',
+    'image/pjpeg',
+    'image/png',
+    'image/gif',
+  );
+
+  /**
+   * Stores function names for each image type
+   */
+  protected $imgLoaders = array(
+    'image/jpeg'  => 'imagecreatefromjpeg',
+    'image/pjpeg' => 'imagecreatefromjpeg',
+    'image/png'   => 'imagecreatefrompng',
+    'image/gif'   => 'imagecreatefromgif',
+  );
+
+  /**
+   * Stores function names for each image type
+   */
+  protected $imgCreators = array(
+    'image/jpeg'  => 'imagejpeg',
+    'image/pjpeg' => 'imagejpeg',
+    'image/png'   => 'imagepng',
+    'image/gif'   => 'imagegif',
+  );
+
+  public function __construct($maxWidth, $maxHeight, $scale, $inflate, $quality, $options)
+  {
+    if (!extension_loaded('gd'))
+    {
+      throw new Exception ('GD not enabled. Check your php.ini file.');
+    }
+    $this->maxWidth = $maxWidth;
+    $this->maxHeight = $maxHeight;
+    $this->scale = $scale;
+    $this->inflate = $inflate;
+    $this->quality = $quality;
+    $this->options = $options;
+  }
+
+  public function loadFile($thumbnail, $image)
+  {
+    $imgData = @GetImageSize($image);
+
+    if (!$imgData)
+    {
+      throw new Exception(sprintf('Could not load image %s', $image));
+    }
+
+    if (in_array($imgData['mime'], $this->imgTypes))
+    {
+      $loader = $this->imgLoaders[$imgData['mime']];
+      if(!function_exists($loader))
+      {
+        throw new Exception(sprintf('Function %s not available. Please enable the GD extension.', $loader));
+      }
+
+      $this->source = $loader($image);
+      $this->sourceWidth = $imgData[0];
+      $this->sourceHeight = $imgData[1];
+      $this->sourceMime = $imgData['mime'];
+      $thumbnail->initThumb($this->sourceWidth, $this->sourceHeight, $this->maxWidth, $this->maxHeight, $this->scale, $this->inflate);
+
+      $this->thumb = imagecreatetruecolor($thumbnail->getThumbWidth(), $thumbnail->getThumbHeight());
+      if ($imgData[0] == $this->maxWidth && $imgData[1] == $this->maxHeight)
+      {
+        $this->thumb = $this->source;
+      }
+      else
+      {
+        imagecopyresampled($this->thumb, $this->source, 0, 0, 0, 0, $thumbnail->getThumbWidth(), $thumbnail->getThumbHeight(), $imgData[0], $imgData[1]);
+      }
+
+      return true;
+    }
+    else
+    {
+      throw new Exception(sprintf('Image MIME type %s not supported', $imgData['mime']));
+    }
+  }
+
+  public function loadData($thumbnail, $image, $mime)
+  {
+    if (in_array($mime,$this->imgTypes))
+    {
+      $this->source = imagecreatefromstring($image);
+      $this->sourceWidth = imagesx($this->source);
+      $this->sourceHeight = imagesy($this->source);
+      $this->sourceMime = $mime;
+      $thumbnail->initThumb($this->sourceWidth, $this->sourceHeight, $this->maxWidth, $this->maxHeight, $this->scale, $this->inflate);
+
+      $this->thumb = imagecreatetruecolor($thumbnail->getThumbWidth(), $thumbnail->getThumbHeight());
+      if ($this->sourceWidth == $this->maxWidth && $this->sourceHeight == $this->maxHeight)
+      {
+        $this->thumb = $this->source;
+      }
+      else
+      {
+        imagecopyresampled($this->thumb, $this->source, 0, 0, 0, 0, $thumbnail->getThumbWidth(), $thumbnail->getThumbHeight(), $this->sourceWidth, $this->sourceHeight);
+      }
+
+      return true;
+    }
+    else
+    {
+      throw new Exception(sprintf('Image MIME type %s not supported', $mime));
+    }
+  }
+
+  public function save($thumbnail, $thumbDest, $targetMime = null)
+  {
+    if($targetMime !== null)
+    {
+      $creator = $this->imgCreators[$targetMime];
+    }
+    else
+    {
+      $creator = $this->imgCreators[$thumbnail->getMime()];
+    }
+
+    if ($creator == 'imagejpeg')
+    {
+      imagejpeg($this->thumb, $thumbDest, $this->quality);
+    }
+    else
+    {
+      $creator($this->thumb, $thumbDest);
+    }
+  }
+
+  public function toString($thumbnail, $targetMime = null)
+  {
+    if ($targetMime !== null)
+    {
+      $creator = $this->imgCreators[$targetMime];
+    }
+    else
+    {
+      $creator = $this->imgCreators[$thumbnail->getMime()];
+    }
+
+    ob_start();
+    $creator($this->thumb);
+
+    return ob_get_clean();
+  }
+
+  public function toResource()
+  {
+    return $this->thumb;
+  }
+
+  public function freeSource()
+  {
+    if (is_resource($this->source))
+    {
+      imagedestroy($this->source);
+    }
+  }
+
+  public function freeThumb()
+  {
+    if (is_resource($this->thumb))
+    {
+      imagedestroy($this->thumb);
+    }
+  }
+
+  public function getSourceMime()
+  {
+    return $this->sourceMime;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfThumbnailPlugin/lib/sfImageMagickAdapter.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,381 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) 2004-2007 Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfImageMagickAdapter provides a mechanism for creating thumbnail images.
+ * @see http://www.imagemagick.org
+ *
+ * @package    sfThumbnailPlugin
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @author     Benjamin Meynell <bmeynell@colorado.edu>
+ */
+
+class sfImageMagickAdapter
+{
+
+  protected
+    $sourceWidth,
+    $sourceHeight,
+    $sourceMime,
+    $maxWidth,
+    $maxHeight,
+    $scale,
+    $inflate,
+    $quality,
+    $source,
+    $magickCommands;
+
+  /**
+   * Mime types this adapter supports
+   */
+  protected $imgTypes = array(
+    'application/pdf',
+    'application/postscript',
+    'application/vnd.palm',
+    'application/x-icb',
+    'application/x-mif',
+    'image/dcx',
+    'image/g3fax',
+    'image/gif',
+    'image/jng',
+    'image/jpeg',
+    'image/pbm',
+    'image/pcd',
+    'image/pict',
+    'image/pjpeg',
+    'image/png',
+    'image/ras',
+    'image/sgi',
+    'image/svg',
+    'image/tga',
+    'image/tiff',
+    'image/vda',
+    'image/vnd.wap.wbmp',
+    'image/vst',
+    'image/x-fits',
+    'image/x-ms-bmp',
+    'image/x-otb',
+    'image/x-palm',
+    'image/x-pcx',
+    'image/x-pgm',
+    'image/x-photoshop',
+    'image/x-ppm',
+    'image/x-ptiff',
+    'image/x-viff',
+    'image/x-win-bitmap',
+    'image/x-xbitmap',
+    'image/x-xv',
+    'image/xpm',
+    'image/xwd',
+    'text/plain',
+    'video/mng',
+    'video/mpeg',
+    'video/mpeg2',
+  );
+
+  /**
+   * Imagemagick-specific Type to Mime type map
+   */
+  protected $mimeMap = array(
+    'bmp'   => 'image/bmp',
+    'bmp2'  => 'image/bmp',
+    'bmp3'  => 'image/bmp',
+    'cur'   => 'image/x-win-bitmap',
+    'dcx'   => 'image/dcx',
+    'epdf'  => 'application/pdf',
+    'epi'   => 'application/postscript',
+    'eps'   => 'application/postscript',
+    'eps2'  => 'application/postscript',
+    'eps3'  => 'application/postscript',
+    'epsf'  => 'application/postscript',
+    'epsi'  => 'application/postscript',
+    'ept'   => 'application/postscript',
+    'ept2'  => 'application/postscript',
+    'ept3'  => 'application/postscript',
+    'fax'   => 'image/g3fax',
+    'fits'  => 'image/x-fits',
+    'g3'    => 'image/g3fax',
+    'gif'   => 'image/gif',
+    'gif87' => 'image/gif',
+    'icb'   => 'application/x-icb',
+    'ico'   => 'image/x-win-bitmap',
+    'icon'  => 'image/x-win-bitmap',
+    'jng'   => 'image/jng',
+    'jpeg'  => 'image/jpeg',
+    'jpg'   => 'image/jpeg',
+    'm2v'   => 'video/mpeg2',
+    'miff'  => 'application/x-mif',
+    'mng'   => 'video/mng',
+    'mpeg'  => 'video/mpeg',
+    'mpg'   => 'video/mpeg',
+    'otb'   => 'image/x-otb',
+    'p7'    => 'image/x-xv',
+    'palm'  => 'image/x-palm',
+    'pbm'   => 'image/pbm',
+    'pcd'   => 'image/pcd',
+    'pcds'  => 'image/pcd',
+    'pcl'   => 'application/pcl',
+    'pct'   => 'image/pict',
+    'pcx'   => 'image/x-pcx',
+    'pdb'   => 'application/vnd.palm',
+    'pdf'   => 'application/pdf',
+    'pgm'   => 'image/x-pgm',
+    'picon' => 'image/xpm',
+    'pict'  => 'image/pict',
+    'pjpeg' => 'image/pjpeg',
+    'png'   => 'image/png',
+    'png24' => 'image/png',
+    'png32' => 'image/png',
+  );
+
+  public function __construct($maxWidth, $maxHeight, $scale, $inflate, $quality, $options)
+  {
+    $this->magickCommands = array();
+    $this->magickCommands['convert'] = isset($options['convert']) ? escapeshellcmd($options['convert']) : 'convert';
+    $this->magickCommands['identify'] = isset($options['identify']) ? escapeshellcmd($options['identify']) : 'identify';
+
+    exec($this->magickCommands['convert'], $stdout);
+    if (strpos($stdout[0], 'ImageMagick') === false)
+    {
+      throw new Exception(sprintf("ImageMagick convert command not found"));
+    }
+
+    exec($this->magickCommands['identify'], $stdout);
+    if (strpos($stdout[0], 'ImageMagick') === false)
+    {
+      throw new Exception(sprintf("ImageMagick identify command not found"));
+    }
+
+    $this->maxWidth = $maxWidth;
+    $this->maxHeight = $maxHeight;
+    $this->scale = $scale;
+    $this->inflate = $inflate;
+    $this->quality = $quality;
+    $this->options = $options;
+  }
+
+  public function toString($thumbnail, $targetMime = null)
+  {
+    ob_start();
+    $this->save($thumbnail, null, $targetMime);
+
+    return ob_get_clean();
+  }
+
+  public function toResource()
+  {
+    throw new Exception('The ImageMagick adapter does not support the toResource method.');
+  }
+
+  public function loadFile($thumbnail, $image)
+  {
+    // try and use getimagesize()
+    // on failure, use identify instead
+    $imgData = @getimagesize($image);
+    if (!$imgData)
+    {
+      exec($this->magickCommands['identify'].' '.escapeshellarg($image), $stdout, $retval);
+      if ($retval === 1)
+      {
+        throw new Exception('Image could not be identified.');
+      }
+      else
+      {
+        // get image data via identify
+        list($img, $type, $dimen) = explode(' ', $stdout[0]);
+        list($width, $height) = explode('x', $dimen);
+
+        $this->sourceWidth = $width;
+        $this->sourceHeight = $height;
+        $this->sourceMime = $this->mimeMap[strtolower($type)];
+      }
+    }
+    else
+    {
+      // use image data from getimagesize()
+      $this->sourceWidth = $imgData[0];
+      $this->sourceHeight = $imgData[1];
+      $this->sourceMime = $imgData['mime'];
+    }
+    $this->image = $image;
+
+    // open file resource
+    $source = fopen($image, 'r');
+
+    $this->source = $source;
+
+    $thumbnail->initThumb($this->sourceWidth, $this->sourceHeight, $this->maxWidth, $this->maxHeight, $this->scale, $this->inflate);
+
+    return true;
+  }
+
+  public function loadData($thumbnail, $image, $mime)
+  {
+    throw new Exception('This function is not yet implemented. Try a different adapter.');
+  }
+
+  public function save($thumbnail, $thumbDest, $targetMime = null)
+  {
+    $command = '';
+
+    $width  = $this->sourceWidth;
+    $height = $this->sourceHeight;
+    $x = $y = 0;
+    switch (@$this->options['method']) 
+    {
+      case "shave_all":
+        $proportion['source'] = $width / $height;
+        $proportion['thumb'] = $thumbnail->getThumbWidth() / $thumbnail->getThumbHeight();
+        
+        if ($proportion['source'] > 1 && $proportion['thumb'] < 1)
+        {
+          $x = ($width - $height * $proportion['thumb']) / 2;
+        }
+        else
+        {
+          if ($proportion['source'] > $proportion['thumb'])
+          {
+            $x = ($width - $height * $proportion['thumb']) / 2;
+          }
+          else
+          {
+            $y = ($height - $width / $proportion['thumb']) / 2;
+          }
+        }
+
+        $command = sprintf(" -shave %dx%d", $x, $y);
+        break;
+
+      case "shave_bottom":
+        if ($width > $height)
+        {
+          $x = ceil(($width - $height) / 2 );
+          $width = $height;
+        }
+        elseif ($height > $width)
+        {
+          $y = 0;
+          $height = $width;
+        }
+
+        if (is_null($thumbDest))
+        {
+          $command = sprintf(
+            " -crop %dx%d+%d+%d %s '-' | %s",
+            $width, $height,
+            $x, $y,
+            escapeshellarg($this->image),
+            $this->magickCommands['convert']
+          );
+
+          $this->image = '-';
+        }
+        else
+        {
+          $command = sprintf(
+            " -crop %dx%d+%d+%d %s %s && %s",
+            $width, $height,
+            $x, $y,
+            escapeshellarg($this->image), escapeshellarg($thumbDest),
+            $this->magickCommands['convert']
+          );
+
+          $this->image = $thumbDest;
+        }
+
+        break;
+      case 'custom':
+      	$coords = $this->options['coords'];
+      	if (empty($coords)) break;
+      	
+      	$x = $coords['x1'];
+      	$y = $coords['y1'];
+      	$width = $coords['x2'] - $coords['x1'];
+      	$height = $coords['y2'] - $coords['y1'];
+      	
+        if (is_null($thumbDest))
+        {
+          $command = sprintf(
+            " -crop %dx%d+%d+%d %s '-' | %s",
+            $width, $height,
+            $x, $y,
+            escapeshellarg($this->image),
+            $this->magickCommands['convert']
+          );
+
+          $this->image = '-';
+        }
+        else
+        {
+          $command = sprintf(
+            " -crop %dx%d+%d+%d %s %s && %s",
+            $width, $height,
+            $x, $y,
+            escapeshellarg($this->image), escapeshellarg($thumbDest),
+            $this->magickCommands['convert']
+          );
+
+          $this->image = $thumbDest;
+        }
+      	break;
+    } // end switch
+
+    $command .= ' -thumbnail ';
+    $command .= $thumbnail->getThumbWidth().'x'.$thumbnail->getThumbHeight();
+
+    // absolute sizing
+    if (!$this->scale)
+    {
+      $command .= '!';
+    }
+
+    if ($this->quality && $targetMime == 'image/jpeg')
+    {
+      $command .= ' -quality '.$this->quality.'% ';
+    }
+
+    // extract images such as pages from a pdf doc
+    $extract = '';
+    if (isset($this->options['extract']) && is_int($this->options['extract']))
+    {
+      if ($this->options['extract'] > 0)
+      {
+        $this->options['extract']--;
+      }
+      $extract = '['.escapeshellarg($this->options['extract']).'] ';
+    }
+
+    $output = (is_null($thumbDest))?'-':$thumbDest;
+    $output = (($mime = array_search($targetMime, $this->mimeMap))?$mime.':':'').$output;
+
+    $cmd = $this->magickCommands['convert'].' '.$command.' '.escapeshellarg($this->image).$extract.' '.escapeshellarg($output);
+
+    (is_null($thumbDest))?passthru($cmd):exec($cmd);
+  }
+
+  public function freeSource()
+  {
+    if (is_resource($this->source))
+    {
+      fclose($this->source);
+    }
+  }
+
+  public function freeThumb()
+  {
+    return true;
+  }
+
+  public function getSourceMime()
+  {
+    return $this->sourceMime;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfThumbnailPlugin/lib/sfThumbnail.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,264 @@
+<?php
+
+/*
+ * This file is part of the symfony package.
+ * (c) 2004-2007 Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfThumbnail provides a mechanism for creating thumbnail images.
+ *
+ * This is taken from Harry Fueck's Thumbnail class and
+ * converted for PHP5 strict compliance for use with symfony.
+ *
+ * @package    sfThumbnailPlugin
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @author     Benjamin Meynell <bmeynell@colorado.edu>
+ */
+class sfThumbnail
+{
+  /**
+   * Width of thumbnail in pixels
+   */
+  protected $thumbWidth;
+
+  /**
+   * Height of thumbnail in pixels
+   */
+  protected $thumbHeight;
+
+  /**
+   * Temporary file if the source is not local
+   */
+  protected $tempFile = null;
+
+  /**
+   * Thumbnail constructor
+   *
+   * @param int (optional) max width of thumbnail
+   * @param int (optional) max height of thumbnail
+   * @param boolean (optional) if true image scales
+   * @param boolean (optional) if true inflate small images
+   * @param string (optional) adapter class name
+   * @param array (optional) adapter options
+   */
+  public function __construct($maxWidth = null, $maxHeight = null, $scale = true, $inflate = true, $quality = 75, $adapterClass = null, $adapterOptions = array())
+  {
+    if (!$adapterClass)
+    {
+      if (extension_loaded('gd'))
+      {
+        $adapterClass = 'sfGDAdapter';
+      }
+      else
+      {
+        $adapterClass = 'sfImageMagickAdapter';
+      }
+    }
+    $this->adapter = new $adapterClass($maxWidth, $maxHeight, $scale, $inflate, $quality, $adapterOptions);
+  }
+
+  /**
+   * Loads an image from a file or URL and creates an internal thumbnail out of it
+   *
+   * @param string filename (with absolute path) of the image to load. If the filename is a http(s) URL, then an attempt to download the file will be made.
+   *
+   * @return boolean True if the image was properly loaded
+   * @throws Exception If the image cannot be loaded, or if its mime type is not supported
+   */
+  public function loadFile($image)
+  {
+    if (eregi('http(s)?://', $image))
+    {
+      if (class_exists('sfWebBrowser'))
+      {
+        if (!is_null($this->tempFile)) {
+          unlink($this->tempFile);
+        }
+        $this->tempFile = tempnam('/tmp', 'sfThumbnailPlugin');
+
+        $b = new sfWebBrowser();
+        try
+        {
+          $b->get($image);
+          if ($b->getResponseCode() != 200) {
+            throw new Exception(sprintf('%s returned error code %s', $image, $b->getResponseCode()));
+          }
+          file_put_contents($this->tempFile, $b->getResponseText());
+          if (!filesize($this->tempFile)) {
+            throw new Exception('downloaded file is empty');
+          } else {
+            $image = $this->tempFile;
+          }
+        }
+        catch (Exception $e)
+        {
+          throw new Exception("Source image is a URL but it cannot be used because ". $e->getMessage());
+        }
+      }
+      else
+      {
+        throw new Exception("Source image is a URL but sfWebBrowserPlugin is not installed");
+      }
+    }
+    else
+    {
+      if (!is_readable($image))
+      {
+        throw new Exception(sprintf('The file "%s" is not readable.', $image));
+      }
+    }
+
+
+    $this->adapter->loadFile($this, $image);
+  }
+
+  /**
+  * Loads an image from a string (e.g. database) and creates an internal thumbnail out of it
+  *
+  * @param string the image string (must be a format accepted by imagecreatefromstring())
+  * @param string mime type of the image
+  *
+  * @return boolean True if the image was properly loaded
+  * @access public
+  * @throws Exception If image mime type is not supported
+  */
+  public function loadData($image, $mime)
+  {
+    $this->adapter->loadData($this, $image, $mime);
+  }
+
+  /**
+   * Saves the thumbnail to the filesystem
+   * If no target mime type is specified, the thumbnail is created with the same mime type as the source file.
+   *
+   * @param string the image thumbnail file destination (with absolute path)
+   * @param string The mime-type of the thumbnail (possible values are 'image/jpeg', 'image/png', and 'image/gif')
+   *
+   * @access public
+   * @return void
+   */
+  public function save($thumbDest, $targetMime = null)
+  {
+    $this->adapter->save($this, $thumbDest, $targetMime);
+  }
+
+  /**
+   * Returns the thumbnail as a string
+   * If no target mime type is specified, the thumbnail is created with the same mime type as the source file.
+   *
+   *
+   * @param string The mime-type of the thumbnail (possible values are adapter dependent)
+   *
+   * @access public
+   * @return string
+   */
+  public function toString($targetMime = null)
+  {
+    return $this->adapter->toString($this, $targetMime);
+  }
+
+  public function toResource()
+  {
+    return $this->adapter->toResource($this);
+  }
+
+  public function freeSource()
+  {
+    if (!is_null($this->tempFile)) {
+      unlink($this->tempFile);
+    }
+    $this->adapter->freeSource();
+  }
+
+  public function freeThumb()
+  {
+    $this->adapter->freeThumb();
+  }
+
+  public function freeAll()
+  {
+    $this->adapter->freeSource();
+    $this->adapter->freeThumb();
+  }
+
+  /**
+   * Returns the width of the thumbnail
+   */
+  public function getThumbWidth()
+  {
+    return $this->thumbWidth;
+  }
+
+  /**
+   * Returns the height of the thumbnail
+   */
+  public function getThumbHeight()
+  {
+    return $this->thumbHeight;
+  }
+
+  /**
+   * Returns the mime type of the source image
+   */
+  public function getMime()
+  {
+    return $this->adapter->getSourceMime();
+  }
+
+  /**
+   * Computes the thumbnail width and height
+   * Used by adapter
+   */
+  public function initThumb($sourceWidth, $sourceHeight, $maxWidth, $maxHeight, $scale, $inflate)
+  {
+    if ($maxWidth > 0)
+    {
+      $ratioWidth = $maxWidth / $sourceWidth;
+    }
+    if ($maxHeight > 0)
+    {
+      $ratioHeight = $maxHeight / $sourceHeight;
+    }
+
+    if ($scale)
+    {
+      if ($maxWidth && $maxHeight)
+      {
+        $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
+      }
+      if ($maxWidth xor $maxHeight)
+      {
+        $ratio = (isset($ratioWidth)) ? $ratioWidth : $ratioHeight;
+      }
+      if ((!$maxWidth && !$maxHeight) || (!$inflate && $ratio > 1))
+      {
+        $ratio = 1;
+      }
+
+      $this->thumbWidth = floor($ratio * $sourceWidth);
+      $this->thumbHeight = ceil($ratio * $sourceHeight);
+    }
+    else
+    {
+      if (!isset($ratioWidth) || (!$inflate && $ratioWidth > 1))
+      {
+        $ratioWidth = 1;
+      }
+      if (!isset($ratioHeight) || (!$inflate && $ratioHeight > 1))
+      {
+        $ratioHeight = 1;
+      }
+      $this->thumbWidth = floor($ratioWidth * $sourceWidth);
+      $this->thumbHeight = ceil($ratioHeight * $sourceHeight);
+    }
+  }
+
+  public function __destruct()
+  {
+    $this->freeAll();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/LICENSE	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,7 @@
+Copyright (c) 2004-2008 Francois Zaninotto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/README	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,269 @@
+sfWebBrowser plugin
+===================
+
+The `sfWebBrowserPlugin` proposes an HTTP client capable of making web requests. The interface is similar to that of `sfTestBrowser`.
+
+Possible uses
+-------------
+
+ * Querying a Web service 
+ * Monitoring a Website
+ * Mashup of content from several websites
+ * Aggregation of RSS feeds
+ * Proxy to another server
+ * Cross-domain AJAX interactions
+ * API to foreign websites
+ * Fetch images and other types of content
+ * ...
+
+Contents
+--------
+
+This plugin contains four classes: `sfWebBrowser`, `sfCurlAdapter`, `sfFopenAdapter`, and `sfSocketsAdapter`. Unit tests are available in the SVN repository, to be placed in a symfony application's `test/` directory.
+
+Features
+--------
+
+The `sfWebBrowser` class makes web requests based on a URI:
+
+    [php]
+    $b = new sfWebBrowser();
+    $b->get('http://www.example.com/');
+    $res = $b->getResponseText();
+
+The usual methods of the `sfTestBrowser` also work there, with the fluid interface.
+
+    [php]
+    // Inline
+    $b->get('http://www.example.com/')->get('http://www.google.com/')->back()->reload();
+    // More readable
+    $b->get('http://www.example.com/')
+      ->get('http://www.google.com/')
+      ->back()
+      ->reload();
+
+The browser accepts absolute and relative URIs
+
+    [php]
+    $b->get('http://www.example.com/test.html');
+    $b->get('test.html');
+
+The `get()` method accepts parameters either as a query string, or as an associative array.
+
+    [php]
+    $b->get('http://www.example.com/test.php?foo=bar');
+    $b->get('http://www.example.com/test.php', array('foo' => 'bar'));
+
+POST, PUT and DELETE requests are also supported.
+
+    [php]
+    $b->post('http://www.example.com/test.php', array('foo' => 'bar'));
+    $b->put('http://www.example.com/test.php', array('foo' => 'bar'));
+    $b->delete('http://www.example.com/test.php', array('foo' => 'bar'));
+
+You can access the response in various formats, at your convenience:
+
+    [php]
+    $myString         = $b->getResponseText();
+    $myString         = $b->getResponseBody(); // drop the <head> part
+    $myDomDocument    = $b->getResponseDom();
+    $myDomCssSelector = $b->getResponseDomCssSelector();
+    $mySimpleXml      = $b->getResponseXml();
+
+You can also interact with the response with the `setFields()` and `click()` methods.
+
+    [php]
+    $b->get('http://www.example.com/login')
+      ->setField('user', 'foobar')
+      ->setField('password', 'barbaz')
+      ->click('submit');
+
+The browser supports HTTP and HTTPS requests, proxies, redirects, and timeouts.
+
+Gzip and deflate content-encoded response bodies are also supported, provided that you have the [http://php.net/zlib zlib extention] enabled.
+
+Adapters
+--------
+
+The browser can use various adapters to perform the requests, and uses the following selection order by default:
+
+ * `sfCurlAdapter`: Uses [Curl](http://php.net/curl) to fetch pages. This adapter is a lot faster than `sfFopenAdapter`, however PHP must be compiled with the `with-curl` option, and the `curl` extension must be enabled in `php.ini` (which is rarely the case by default) for it to work. 
+
+ * `sfFopenAdapter`: Uses [`fopen()`](http://php.net/fopen ) to fetch pages. `fopen()` can take an URL as a parameter provided that PHP is compiled with sockets support, and `allow_url_fopen` is defined to `true` in `php.ini`. This is the case in most PHP distributions, so the default adapter should work in almost every platform. On the other hand, the compatibility has a cost: this adapter is slow.
+
+ * `sfSocketsAdapter`: Uses [`fsockopen()`](http://php.net/fsockopen) to fetch pages.
+
+Alternatively, you can specify an adapter explicitly when you create a new browser object, as follows:
+
+    [php]
+    // use default adapter, i.e. sfCurlAdapter
+    $b = new sfWebBrowser(array());
+    // use sfFopenAdapter
+    $b = new sfWebBrowser(array(), 'sfFopenAdapter');
+
+Currenly, `sfCurlAdapter` offers slightly more functionality than the other adapters. Namely, it supports multipart file uploads and cookies, which means you can login to a site as well as upload files via forms.
+
+    [php]
+    // upload files via a form
+    $b = new sfWebBrowser();
+    $b->post($url_of_form, array(
+      'file_field' => '/path/to/my/local/file.jpg'
+    ));
+    // login to a website
+    $b = new sfWebBrowser(array(), 'sfCurlAdapter', array('cookies' => true));
+    $b->post($url_of_login_form, array(
+      'user_field' => $username,
+      'pass_field' => $password
+    ));
+
+Full examples are available in the unit tests.
+
+Error Handling
+--------------
+
+`sfWebBrowser` distinguishes to types of error: adapter errors and response errors. Thus, `sfWebBrowser` calls should be run this way :
+
+    [php]
+    $b = new sfWebBrowser();
+    try
+    {
+      if (!$b->get($url)->responseIsError())
+      {
+        // Successful response (eg. 200, 201, etc)
+      }
+      else
+      {
+        // Error response (eg. 404, 500, etc)
+      }
+    }
+    catch (Exception $e)
+    {
+      // Adapter error (eg. Host not found)
+    }
+
+Besides, you should always remember that the response contents may contain incorrect code. Consider it as 'tainted', and therefore always use the [escaping](http://www.symfony-project.com/book/trunk/07-Inside-the-View-Layer#Output%20Escaping) when outputting it to a template.
+
+    [php]
+    // In the action
+    $this->title = (string) $b->getResponseXml()->body->h1
+
+    // In the template
+    <?php echo $title // dangerous ?>
+    <?php echo $sf_data->get('title') // correct ?>
+
+Installation
+------------
+
+* Install the plugin
+
+        $ symfony plugin-install http://plugins.symfony-project.com/sfWebBrowserPlugin
+
+
+* Clear the cache to enable the autoloading to find the new class
+
+        $ symfony cc
+
+Known limitations
+-----------------
+
+Cookies, caching, and file uploads are not yet supported in any of the packages (some of this functionality is available with `sfCurlAdapter`, see above).
+
+Changelog
+---------
+
+### Trunk
+
+### 2009-05-12 | 1.1.2 Stable
+
+  * francois: Fixed sfCurlAdapter destructor
+  * francois: Fixed sf1.2 compatibility issue for custom exception
+  * francois: Fixed a few limit case bugs and made the tests pass
+  
+### 2009-04-22 | 1.1.1 Stable
+
+  * francois: Fixed README syntax for parameters array
+  * bmeynell: Fixed custom options in `sfCurlAdapter`
+  
+### 2008-09-23 | 1.1.0 Stable
+
+  * francois: Translated README to Markdown
+  * francois: Added support for custom options in `sfCurlAdapter`
+  * francois: Added suppot for Timeout with `sfCurlAdapter` (based on a patch by adoanhuu)
+  * blacksun: Allow for SSL certificate verification
+  * francois: Added a test to check exceptions thrown by `getResponseXML`
+  * bmeynell: added multipart file upload support to `sfCurlAdapter`
+  * bmeynell: fixed regex in getResponseBody() which was returning an empty body
+  * bmeynell: `sfCurlAdapter`: Added new options: 'cookies', 'cookies_dir', 'cookies_file', 'verbose', 'verbose_log'
+  * bmeynell: `sfCurlAdapter`: Increased speed by Moving some initialization from call() to the constructer
+  * tristan:  Easier management of invalid XML responses
+  * francois: Fixed a bug in `sfFopenAdapter` error handler
+  * bmeynell: Added chunked transfer encoding support to `sfSocketsAdapter`
+  * bmeynell: Added support for 301 redirects in `sfSocketsAdapter`
+
+### 2007-03-27 | 1.0.1 stable
+
+  * bmeynell: Fixed a bug with `sfCurlAdapter` causing 'Bad Request' error responses
+  * francois: Fixed a bug with `get()` when `arg_separator.output` is not set to '&' in `php.ini` (patch from river.bright)
+  * francois: Fixed a bug with `get()` when query string is already present in the url (based on a patch from Jeff Merlet)
+  * francois: Fixed auto-adapter decision in `sfWebBrowser::__construct()`
+  
+### 2007-03-08 | 1.0.0 stable
+
+  * francois: Added auto-adapter decision in `sfWebBrowser::__construct()`
+  * francois: Changed tested URLs a bit to avoid redirection issues with google
+  * bmeynell: Added `sfSocketsAdapter`
+  * bmeynell: `sfCurlAdapter`: more detailed error messages & leaner request setup
+
+### 2007-02-22 | 0.9.6 Beta
+
+ * bmeynell, tristan: Allowed for requests with any method in `sfCurlAdapter`
+ * tristan: Added `sfWebBrowser::responseIsError()`
+ * tristan: Added `sfWebBrowser::getResponseMessage()`
+ * tristan: Refactored error management in `sfFopenAdapter`
+
+### 2007-02-21 | 0.9.5 Beta
+
+ * bmeynell: Fixed bug with relative uri's attempting to use a port other than 80 (sfWebBrowser, 132 - 146)
+ * bmeynell: Fixed small bug not printing hostname on exception (sfFopenAdapter, 61-62)
+ * bmeynell: Created sfCurlAdapter and passes all unit tests
+ * bmeynell: Removed '$changeStack = true' from call() prototype in sfCurlAdapter, sfFopenAdapter, and moved changestack check to sfWebBrowser
+ * bmeynell: Added $askeet_url to sfWebBrowserTest
+ * bmeynell: Added easy toggling between adapters in sfWebBrowserTest
+ * tristan: Added put() and delete() public methods
+ * tristan: Added unit tests to validate request HTTP method
+
+### 2007-02-16 | 0.9.4 Beta
+
+ * francois: Refactored the browser to make it multi-adapter
+ * francois: '''BC break''' constructor signature changed : `new sfWebBrowser(array $headers, string $adapter_class, array $adapter_options)` 
+ * francois: Fixed notice when trying to retrieve inexistent header
+ * francois: Fixed header case normalization
+ * francois: Transformed setResponseXXX() methods to public
+ * francois: Fixed caps in `initializeRequestHeaders()`
+ * francois: Fixed unit test #40
+
+### 2007-02-16 | 0.9.3 Beta
+
+ * tristan: Added support for gzip and deflate.
+ * tristan: Possibility to pass default request headers to sfWebBrowser's constructor
+ * tristan: "Accept-Encoding" header is automatically set depending on PHP capabilities
+ * tristan: Fixed problems with request and response headers case 
+ * tristan: Renamed "browser options" to "adapter options" (http://www.symfony-project.com/forum/index.php/m/21635/)
+ * tristan: '''BC break''' constructor signature changed : `new sfWebBrowser(array $headers, array $adapter_options)`
+ * tristan: Unit tested POST requests  
+ * tristan: Changed way httpd headers are stored internally
+ * tristan: Fixed small bug in `getResponseBody()`
+ * francois: Fixed unit test for malformed headers
+ 
+### 2007-02-09 | 0.9.2 Beta
+
+ * francois: Fixed notice with `getResponseXML()`
+ 
+### 2007-02-08 | 0.9.1 Beta
+
+ * francois: Fixed notice with some headers
+ * francois: Added license and copyright
+ 
+### 2007-02-08 | 0.9.0 Beta
+
+ * francois: Initial release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/lib/sfCurlAdapter.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,237 @@
+<?php
+
+/*
+ * This file is part of the sfWebBrowserPlugin package.
+ * (c) 2004-2006 Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com> for the click-related functions
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfWebBrowser provides a basic HTTP client.
+ *
+ * @package    sfWebBrowserPlugin
+ * @author     Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * @author     Ben Meynell <bmeynell@colorado.edu>
+ * @version    0.9
+ */
+
+class sfCurlAdapter
+{
+  protected
+    $options = array(),
+    $curl    = null,
+    $headers = array();
+
+  /**
+   * Build a curl adapter instance
+   * Accepts an option of parameters passed to the PHP curl adapter:
+   *  ssl_verify  => [true/false]
+   *  verbose     => [true/false]
+   *  verbose_log => [true/false]
+   * Additional options are passed as curl options, under the form:
+   *  userpwd => CURL_USERPWD
+   *  timeout => CURL_TIMEOUT
+   *  ...
+   *
+   * @param array $options Curl-specific options
+   */
+  public function __construct($options = array())
+  {
+    if (!extension_loaded('curl'))
+    {
+      throw new Exception('Curl extension not loaded');
+    }
+
+    $this->options = $options;
+    $curl_options = $options;
+    
+    $this->curl = curl_init();
+
+    // cookies
+    if (isset($curl_options['cookies']))
+    {
+      if (isset($curl_options['cookies_file']))
+      {
+        $cookie_file = $curl_options['cookies_file'];
+        unset($curl_options['cookies_file']);
+      }
+      else
+      {
+        $cookie_file = sfConfig::get('sf_data_dir').'/sfWebBrowserPlugin/sfCurlAdapter/cookies.txt';
+      }
+      if (isset($curl_options['cookies_dir']))
+      {
+        $cookie_dir = $curl_options['cookies_dir'];
+        unset($curl_options['cookies_dir']);
+      }
+      else
+      {
+        $cookie_dir = sfConfig::get('sf_data_dir').'/sfWebBrowserPlugin/sfCurlAdapter';
+      }
+      if (!is_dir($cookie_dir))
+      {
+        if (!mkdir($cookie_dir, 0777, true))
+        {
+          throw new Exception(sprintf('Could not create directory "%s"', $cookie_dir));
+        }
+      }
+
+      curl_setopt($this->curl, CURLOPT_COOKIESESSION, false);
+      curl_setopt($this->curl, CURLOPT_COOKIEJAR, $cookie_file);
+      curl_setopt($this->curl, CURLOPT_COOKIEFILE, $cookie_file);
+      unset($curl_options['cookies']);
+    }
+
+    // default settings
+    curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, 1);
+    curl_setopt($this->curl, CURLOPT_AUTOREFERER, true);
+    curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, false);
+    curl_setopt($this->curl, CURLOPT_FRESH_CONNECT, true);
+    
+    if(isset($this->options['followlocation']))
+    {
+      curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, (bool) $this->options['followlocation']);
+    }
+    
+    // activate ssl certificate verification?
+    
+    if (isset($this->options['ssl_verify_host']))
+    {
+      curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, (bool) $this->options['ssl_verify_host']);
+    }
+    if (isset($curl_options['ssl_verify']))
+    {
+      curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, (bool) $this->options['ssl_verify']);
+      unset($curl_options['ssl_verify']);
+    }
+    // verbose execution?
+    if (isset($curl_options['verbose']))
+    {
+      curl_setopt($this->curl, CURLOPT_NOPROGRESS, false);
+      curl_setopt($this->curl, CURLOPT_VERBOSE, true);
+      unset($curl_options['cookies']);
+    }
+    if (isset($curl_options['verbose_log']))
+    {
+      $log_file = sfConfig::get('sf_log_dir').'/sfCurlAdapter_verbose.log';
+      curl_setopt($this->curl, CURLOPT_VERBOSE, true);
+      $this->fh = fopen($log_file, 'a+b');
+      curl_setopt($this->curl, CURLOPT_STDERR, $this->fh);
+      unset($curl_options['verbose_log']);
+    }
+    
+    // Additional options
+    foreach ($curl_options as $key => $value)
+    {
+      $const = constant('CURLOPT_' . strtoupper($key));
+      if(!is_null($const))
+      {
+        curl_setopt($this->curl, $const, $value);
+      }
+    }
+    
+    // response header storage - uses callback function
+    curl_setopt($this->curl, CURLOPT_HEADERFUNCTION, array($this, 'read_header'));
+  }
+
+  /**
+   * Submits a request
+   *
+   * @param string  The request uri
+   * @param string  The request method
+   * @param array   The request parameters (associative array)
+   * @param array   The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */
+  public function call($browser, $uri, $method = 'GET', $parameters = array(), $headers = array())
+  {
+    // uri
+    curl_setopt($this->curl, CURLOPT_URL, $uri);
+
+    // request headers
+    $m_headers = array_merge($browser->getDefaultRequestHeaders(), $browser->initializeRequestHeaders($headers));
+    $request_headers = explode("\r\n", $browser->prepareHeaders($m_headers));
+    curl_setopt($this->curl, CURLOPT_HTTPHEADER, $request_headers);
+   
+    // encoding support
+    if(isset($headers['Accept-Encoding']))
+    {
+      curl_setopt($this->curl, CURLOPT_ENCODING, $headers['Accept-Encoding']);
+    }
+    
+    // timeout support
+    if(isset($this->options['Timeout']))
+    {
+      curl_setopt($this->curl, CURLOPT_TIMEOUT, $this->options['Timeout']);
+    }
+    
+    if (!empty($parameters))
+    {
+      if (!is_array($parameters))
+      {
+        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $parameters);
+      }
+      else
+      {
+        // multipart posts (file upload support)
+        $has_files = false;
+        foreach ($parameters as $name => $value)
+        {
+          if (is_array($value)) {
+            continue;
+          }
+          if (is_file($value))
+          {
+            $has_files = true;
+            $parameters[$name] = '@'.realpath($value);
+          }
+        }
+        if($has_files)
+        {
+          curl_setopt($this->curl, CURLOPT_POSTFIELDS, $parameters);
+        }
+        else
+        {
+          curl_setopt($this->curl, CURLOPT_POSTFIELDS, http_build_query($parameters, '', '&'));
+        }
+      }
+    }
+
+    // handle any request method
+    curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method);
+
+    $response = curl_exec($this->curl);
+
+    if (curl_errno($this->curl))
+    {
+      throw new Exception(curl_error($this->curl));
+    }
+
+    $requestInfo = curl_getinfo($this->curl);
+
+    $browser->setResponseCode($requestInfo['http_code']);
+    $browser->setResponseHeaders($this->headers);
+    $browser->setResponseText($response);
+
+    // clear response headers
+    $this->headers = array();
+
+    return $browser;
+  }
+
+  public function __destruct()
+  {
+    curl_close($this->curl);
+  }
+
+  protected function read_header($curl, $headers)
+  {
+    $this->headers[] = $headers;
+    
+    return strlen($headers);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/lib/sfFopenAdapter.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of the sfWebBrowserPlugin package.
+ * (c) 2004-2006 Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com> for the click-related functions
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfWebBrowser provides a basic HTTP client.
+ *
+ * @package    sfWebBrowserPlugin
+ * @author     Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * @version    0.9
+ */
+class sfFopenAdapter
+{ 
+  protected
+    $options             = array(),
+    $adapterErrorMessage = null,
+    $browser             = null;
+    
+  public function __construct($options = array())
+  {
+    $this->options = $options;     
+  }
+    
+  /**
+   * Submits a request
+   *
+   * @param string  The request uri
+   * @param string  The request method
+   * @param array   The request parameters (associative array)
+   * @param array   The request headers (associative array)
+   * @param boolean To specify is the request changes the browser history
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function call($browser, $uri, $method = 'GET', $parameters = array(), $headers = array())
+  {    
+    $m_headers = array_merge(array('Content-Type' => 'application/x-www-form-urlencoded'), $browser->getDefaultRequestHeaders(), $browser->initializeRequestHeaders($headers));
+    $request_headers = $browser->prepareHeaders($m_headers);
+    
+    // Read the response from the server
+    // FIXME: use sockets to avoid depending on allow_url_fopen
+    $context = stream_context_create(array('http' => array_merge(
+      $this->options, 
+      array('method' => $method), 
+      array('content' => is_array($parameters) ? http_build_query($parameters) : $parameters),
+      array('header' => $request_headers)
+    )));
+
+    // Custom error handler
+    // -- browser instance must be accessible from handleRuntimeError()
+    $this->browser = $browser;
+    set_error_handler(array($this, 'handleRuntimeError'), E_WARNING);
+    if($handle = fopen($uri, 'r', false, $context))
+    {
+      $response_headers = stream_get_meta_data($handle);
+      $browser->setResponseCode(array_shift($response_headers['wrapper_data']));
+      $browser->setResponseHeaders($response_headers['wrapper_data']);
+      $browser->setResponseText(stream_get_contents($handle));
+      fclose($handle);
+    }
+
+    restore_error_handler();
+    
+    if ($this->adapterErrorMessage == true)
+    {
+      $msg = $this->adapterErrorMessage;
+      $this->adapterErrorMessage = null;
+      throw new Exception($msg);
+    }
+    
+    return $browser;
+  }
+  
+  /**
+   * Handles PHP runtime error.
+   * 
+   * This handler is used to catch any warnigns sent by fopen($url) and reformat them to something
+   * usable.
+   *
+   * @see  http://php.net/set_error_handler
+   */
+  function handleRuntimeError($errno, $errstr, $errfile = null, $errline = null, $errcontext = array() )
+  {
+     $error_types = array (
+                E_ERROR              => 'Error',
+                E_WARNING            => 'Warning',
+                E_PARSE              => 'Parsing Error',
+                E_NOTICE             => 'Notice',
+                E_CORE_ERROR         => 'Core Error',
+                E_CORE_WARNING       => 'Core Warning',
+                E_COMPILE_ERROR      => 'Compile Error',
+                E_COMPILE_WARNING    => 'Compile Warning',
+                E_USER_ERROR         => 'User Error',
+                E_USER_WARNING       => 'User Warning',
+                E_USER_NOTICE        => 'User Notice',
+                E_STRICT             => 'Runtime Notice',
+                E_RECOVERABLE_ERROR  => 'Catchable Fatal Error'
+                );
+    
+    $msg = sprintf('%s : "%s" occured in %s on line %d',
+                   $error_types[$errno], $errstr, $errfile, $errline);
+
+    $matches = array();
+    if (preg_match('/HTTP\/\d\.\d (\d{3}) (.*)$/', $errstr, $matches))
+    {
+      $this->browser->setResponseCode($matches[1]);
+      $this->browser->setResponseMessage($matches[2]);
+      $body = sprintf('The %s adapter cannot handle error responses body. Try using another adapter.', __CLASS__);
+      $this->browser->setResponseText($body);
+    }
+    else
+    {
+      $this->adapterErrorMessage = $msg;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/lib/sfSocketsAdapter.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of the sfWebBrowserPlugin package.
+ * (c) 2004-2006 Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com> for the click-related functions
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfWebBrowser provides a basic HTTP client.
+ *
+ * @package    sfWebBrowserPlugin
+ * @author     Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * @author     Benjamin Meynell <bmeynell@colorado.edu>
+ * @version    0.9
+ */
+class sfSocketsAdapter
+{
+  protected
+    $options             = array(),
+    $adapterErrorMessage = null,
+    $browser             = null;
+
+  public function __construct($options = array())
+  {
+    $this->options = $options;
+  }
+
+  /**
+   * Submits a request
+   *
+   * @param string  The request uri
+   * @param string  The request method
+   * @param array   The request parameters (associative array)
+   * @param array   The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */
+  public function call($browser, $uri, $method = 'GET', $parameters = array(), $headers = array())
+  {
+    $m_headers = array_merge(array('Content-Type' => 'application/x-www-form-urlencoded'), $browser->getDefaultRequestHeaders(), $browser->initializeRequestHeaders($headers));
+    $request_headers = $browser->prepareHeaders($m_headers);
+
+    $url_info = parse_url($uri);
+
+    // initialize default values
+    isset($url_info['path']) ? $path = $url_info['path'] : $path = '/';
+    isset($url_info['query']) ? $qstring = '?'.$url_info['query'] : $qstring = null;
+    isset($url_info['port']) ? null : $url_info['port'] = 80;
+
+    if (!$socket = @fsockopen($url_info['host'], $url_info['port'], $errno, $errstr, 15))
+    {
+      throw new Exception("Could not connect ($errno): $errstr");
+    }
+
+    // build request
+    $request = "$method $path$qstring HTTP/1.1\r\n";
+    $request .= 'Host: '.$url_info['host'].':'.$url_info['port']."\r\n";
+    $request .= $request_headers;
+    $request .= "Connection: Close\r\n";
+
+    if ($method == 'PUT' && is_array($parameters) && array_key_exists('file', $parameters))
+    {
+      $fp = fopen($parameters['file'], 'rb');
+      $sent = 0;
+      $blocksize = (2 << 20); // 2MB chunks
+      $filesize = filesize($parameters['file']);
+
+      $request .= 'Content-Length: '.$filesize."\r\n";
+
+      $request .= "\r\n";
+
+      fwrite($socket, $request);
+
+      while ($sent < $filesize)
+      {
+        $data = fread($fp, $blocksize);
+        fwrite($socket, $data);
+        $sent += $blocksize;
+      }
+      fclose($fp);
+    }
+    elseif ($method == 'POST' || $method == 'PUT')
+    {
+      $body = is_array($parameters) ? http_build_query($parameters, '', '&') : $parameters;
+      $request .= 'Content-Length: '.strlen($body)."\r\n";
+      $request .= "\r\n";
+      $request .= $body;
+    }
+
+    $request .= "\r\n";
+
+    fwrite($socket, $request);
+
+    $response = '';
+    $response_body = '';
+    while (!feof($socket))
+    {
+      $response .= fgets($socket, 1024);
+    }
+    fclose($socket);
+
+    // parse response components: status line, headers and body
+    $response_lines = explode("\r\n", $response);
+
+    // http status line (ie "HTTP 1.1 200 OK")
+    $status_line = array_shift($response_lines);
+
+    $start_body = false;
+    $response_headers = array();
+    for($i=0; $i<count($response_lines); $i++)
+    {
+      // grab body
+      if ($start_body == true)
+      {
+        // ignore chunked encoding size
+        if (!preg_match('@^[0-9A-Fa-f]+\s*$@', $response_lines[$i]))
+        {
+          $response_body .= $response_lines[$i];
+        }
+      }
+
+      // body starts after first blank line
+      else if ($start_body == false && $response_lines[$i] == '')
+      {
+        $start_body = true;
+      }
+
+      // grab headers
+      else
+      {
+        $response_headers[] = $response_lines[$i];
+      }
+    }
+    
+    $browser->setResponseHeaders($response_headers);
+    
+    // grab status code
+    preg_match('@(\d{3})@', $status_line, $status_code);
+    if(isset($status_code[1]))
+    {
+      $browser->setResponseCode($status_code[1]);
+    }
+    $browser->setResponseText(trim($response_body));
+
+    return $browser;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/lib/sfWebBrowser.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,851 @@
+<?php
+
+/*
+ * This file is part of the sfWebBrowserPlugin package.
+ * (c) 2004-2006 Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com> for the click-related functions
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * sfWebBrowser provides a basic HTTP client.
+ *
+ * @package    sfWebBrowserPlugin
+ * @author     Francois Zaninotto <francois.zaninotto@symfony-project.com>
+ * @author     Tristan Rivoallan <tristan@rivoallan.net>
+ * @version    0.9
+ */
+class sfWebBrowser
+{
+  protected
+    $defaultHeaders          = array(),
+    $stack                   = array(),
+    $stackPosition           = -1,
+    $responseHeaders         = array(),
+    $responseCode            = '',
+    $responseMessage         = '',
+    $responseText            = '',
+    $responseDom             = null,
+    $responseDomCssSelector  = null,
+    $responseXml             = null,
+    $fields                  = array(),
+    $urlInfo                 = array();
+
+  public function __construct($defaultHeaders = array(), $adapterClass = null, $adapterOptions = array())
+  {
+    if(!$adapterClass)
+    {
+      if (function_exists('curl_init'))
+      {
+        $adapterClass = 'sfCurlAdapter';
+      }
+      else if(ini_get('allow_url_fopen') == 1)
+      {
+        $adapterClass = 'sfFopenAdapter';
+      }
+      else
+      {
+        $adapterClass = 'sfSocketsAdapter';
+      }
+    }
+    $this->defaultHeaders = $this->fixHeaders($defaultHeaders);
+    $this->adapter = new $adapterClass($adapterOptions);
+  }
+    
+  // Browser methods
+  
+  /**
+   * Restarts the browser
+   *
+   * @param array default browser options
+   *
+   * @return sfWebBrowser The current browser object
+   */    
+  public function restart($defaultHeaders = array())
+  {
+    $this->defaultHeaders = $this->fixHeaders($defaultHeaders);
+    $this->stack          = array();
+    $this->stackPosition  = -1;
+    $this->urlInfo        = array();
+    $this->initializeResponse();
+    
+    return $this;
+  }
+
+  /**
+   * Sets the browser user agent name
+   *
+   * @param string agent name
+   *
+   * @return sfWebBrowser The current browser object
+   */    
+  public function setUserAgent($agent)
+  {
+    $this->defaultHeaders['User-Agent'] = $agent;
+    
+    return $this;
+  }
+
+  /**
+   * Gets the browser user agent name
+   *
+   * @return string agent name
+   */    
+  public function getUserAgent()
+  {
+    return isset($this->defaultHeaders['User-Agent']) ? $this->defaultHeaders['User-Agent'] : '';
+  }
+
+  /**
+   * Submits a GET request
+   *
+   * @param string The request uri
+   * @param array  The request parameters (associative array)
+   * @param array  The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */
+  public function get($uri, $parameters = array(), $headers = array())
+  {
+    if ($parameters)
+    {
+      $uri .= ((false !== strpos($uri, '?')) ? '&' : '?') . http_build_query($parameters, '', '&');
+    }
+    return $this->call($uri, 'GET', array(), $headers);
+  }
+
+  public function head($uri, $parameters = array(), $headers = array())
+  {
+    if ($parameters)
+    {
+      $uri .= ((false !== strpos($uri, '?')) ? '&' : '?') . http_build_query($parameters, '', '&');
+    }
+    return $this->call($uri, 'HEAD', array(), $headers);
+  }
+
+  /**
+   * Submits a POST request
+   *
+   * @param string The request uri
+   * @param array  The request parameters (associative array)
+   * @param array  The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function post($uri, $parameters = array(), $headers = array())
+  {
+    return $this->call($uri, 'POST', $parameters, $headers);
+  }
+  
+  /**
+   * Submits a PUT request.
+   *
+   * @param string The request uri
+   * @param array  The request parameters (associative array)
+   * @param array  The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function put($uri, $parameters = array(), $headers = array())
+  {
+    return $this->call($uri, 'PUT', $parameters, $headers);
+  }
+
+  /**
+   * Submits a DELETE request.
+   *
+   * @param string The request uri
+   * @param array  The request parameters (associative array)
+   * @param array  The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function delete($uri, $parameters = array(), $headers = array())
+  {
+    return $this->call($uri, 'DELETE', $parameters, $headers);
+  }
+
+  /**
+   * Submits a request
+   *
+   * @param string  The request uri
+   * @param string  The request method
+   * @param array   The request parameters (associative array)
+   * @param array   The request headers (associative array)
+   * @param boolean To specify is the request changes the browser history
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function call($uri, $method = 'GET', $parameters = array(), $headers = array(), $changeStack = true)
+  {
+    $urlInfo = parse_url($uri);
+
+    // Check headers
+    $headers = $this->fixHeaders($headers);
+
+    // check port
+    if (isset($urlInfo['port']))
+    {
+      $this->urlInfo['port'] = $urlInfo['port'];
+    }
+    else if (!isset($this->urlInfo['port']))
+    {
+      $this->urlInfo['port'] = 80;
+    }
+
+    if(!isset($urlInfo['host']))
+    {
+      // relative link
+      $uri = $this->urlInfo['scheme'].'://'.$this->urlInfo['host'].':'.$this->urlInfo['port'].'/'.$uri;
+    }
+    else if($urlInfo['scheme'] != 'http' && $urlInfo['scheme'] != 'https')
+    {
+      throw new Exception('sfWebBrowser handles only http and https requests'); 
+    }
+
+    $this->urlInfo = parse_url($uri);
+
+    $this->initializeResponse();
+
+    if ($changeStack)
+    {
+      $this->addToStack($uri, $method, $parameters, $headers);
+    }
+
+    $browser = $this->adapter->call($this, $uri, $method, $parameters, $headers);
+
+    // redirect support
+    if ((in_array($browser->getResponseCode(), array(301, 307)) && in_array($method, array('GET', 'HEAD'))) || in_array($browser->getResponseCode(), array(302,303)))
+    {
+      $this->call($browser->getResponseHeader('Location'), 'GET', array(), $headers);
+    }
+
+    return $browser;
+  }
+
+  /**
+   * Gives a value to a form field in the response
+   *
+   * @param string field name
+   * @param string field value
+   *
+   * @return sfWebBrowser The current browser object
+   */ 
+  public function setField($name, $value)
+  {
+    // as we don't know yet the form, just store name/value pairs
+    $this->parseArgumentAsArray($name, $value, $this->fields);
+
+    return $this;
+  }
+  
+  /**
+   * Looks for a link or a button in the response and submits the related request
+   *
+   * @param string The link/button value/href/alt
+   * @param array request parameters (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */ 
+  public function click($name, $arguments = array())
+  {
+    if (!$dom = $this->getResponseDom())
+    {
+      throw new Exception('Cannot click because there is no current page in the browser');
+    }
+
+    $xpath = new DomXpath($dom);
+
+    // text link, the name being in an attribute
+    if ($link = $xpath->query(sprintf('//a[@*="%s"]', $name))->item(0))
+    {
+      return $this->get($link->getAttribute('href'));
+    }
+
+    // text link, the name being the text value
+    if ($links = $xpath->query('//a[@href]'))
+    {
+      foreach($links as $link)
+      {
+        if(preg_replace(array('/\s{2,}/', '/\\r\\n|\\n|\\r/'), array(' ', ''), $link->nodeValue) == $name)
+        {
+          return $this->get($link->getAttribute('href'));  
+        }
+      }
+    }
+    
+    // image link, the name being the alt attribute value
+    if ($link = $xpath->query(sprintf('//a/img[@alt="%s"]/ancestor::a', $name))->item(0))
+    {
+      return $this->get($link->getAttribute('href'));
+    }
+
+    // form, the name being the button or input value
+    if (!$form = $xpath->query(sprintf('//input[((@type="submit" or @type="button") and @value="%s") or (@type="image" and @alt="%s")]/ancestor::form', $name, $name))->item(0))
+    {
+      throw new Exception(sprintf('Cannot find the "%s" link or button.', $name));
+    }
+
+    // form attributes
+    $url = $form->getAttribute('action');
+    $method = $form->getAttribute('method') ? strtolower($form->getAttribute('method')) : 'get';
+
+    // merge form default values and arguments
+    $defaults = array();
+    foreach ($xpath->query('descendant::input | descendant::textarea | descendant::select', $form) as $element)
+    {
+      $elementName = $element->getAttribute('name');
+      $nodeName    = $element->nodeName;
+      $value       = null;
+      if ($nodeName == 'input' && ($element->getAttribute('type') == 'checkbox' || $element->getAttribute('type') == 'radio'))
+      {
+        if ($element->getAttribute('checked'))
+        {
+          $value = $element->getAttribute('value');
+        }
+      }
+      else if (
+        $nodeName == 'input'
+        &&
+        (($element->getAttribute('type') != 'submit' && $element->getAttribute('type') != 'button') || $element->getAttribute('value') == $name)
+        &&
+        ($element->getAttribute('type') != 'image' || $element->getAttribute('alt') == $name)
+      )
+      {
+        $value = $element->getAttribute('value');
+      }
+      else if ($nodeName == 'textarea')
+      {
+        $value = '';
+        foreach ($element->childNodes as $el)
+        {
+          $value .= $dom->saveXML($el);
+        }
+      }
+      else if ($nodeName == 'select')
+      {
+        if ($multiple = $element->hasAttribute('multiple'))
+        {
+          $elementName = str_replace('[]', '', $elementName);
+          $value = array();
+        }
+        else
+        {
+          $value = null;
+        }
+
+        $found = false;
+        foreach ($xpath->query('descendant::option', $element) as $option)
+        {
+          if ($option->getAttribute('selected'))
+          {
+            $found = true;
+            if ($multiple)
+            {
+              $value[] = $option->getAttribute('value');
+            }
+            else
+            {
+              $value = $option->getAttribute('value');
+            }
+          }
+        }
+
+        // if no option is selected and if it is a simple select box, take the first option as the value
+        if (!$found && !$multiple)
+        {
+          $value = $xpath->query('descendant::option', $element)->item(0)->getAttribute('value');
+        }
+      }
+
+      if (null !== $value)
+      {
+        $this->parseArgumentAsArray($elementName, $value, $defaults);
+      }
+    }
+
+    // create request parameters
+    $arguments = sfToolkit::arrayDeepMerge($defaults, $this->fields, $arguments);
+    if ('post' == $method)
+    {
+      return $this->post($url, $arguments);
+    }
+    else
+    {
+      return $this->get($url, $arguments);
+    }
+  }
+
+  protected function parseArgumentAsArray($name, $value, &$vars)
+  {
+    if (false !== $pos = strpos($name, '['))
+    {
+      $var = &$vars;
+      $tmps = array_filter(preg_split('/(\[ | \[\] | \])/x', $name));
+      foreach ($tmps as $tmp)
+      {
+        $var = &$var[$tmp];
+      }
+      if ($var)
+      {
+        if (!is_array($var))
+        {
+          $var = array($var);
+        }
+        $var[] = $value;
+      }
+      else
+      {
+        $var = $value;
+      }
+    }
+    else
+    {
+      $vars[$name] = $value;
+    }
+  }
+
+  /**
+   * Adds the current request to the history stack
+   *
+   * @param string  The request uri
+   * @param string  The request method
+   * @param array   The request parameters (associative array)
+   * @param array   The request headers (associative array)
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function addToStack($uri, $method, $parameters, $headers)
+  {
+    $this->stack = array_slice($this->stack, 0, $this->stackPosition + 1);
+    $this->stack[] = array(
+      'uri'        => $uri,
+      'method'     => $method,
+      'parameters' => $parameters,
+      'headers'    => $headers
+    );
+    $this->stackPosition = count($this->stack) - 1;
+    
+    return $this;
+  }
+
+  /**
+   * Submits the previous request in history again
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function back()
+  {
+    if ($this->stackPosition < 1)
+    {
+      throw new Exception('You are already on the first page.');
+    }
+
+    --$this->stackPosition;
+    return $this->call($this->stack[$this->stackPosition]['uri'], 
+                       $this->stack[$this->stackPosition]['method'], 
+                       $this->stack[$this->stackPosition]['parameters'], 
+                       $this->stack[$this->stackPosition]['headers'],
+                       false);
+  }
+
+  /**
+   * Submits the next request in history again
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function forward()
+  {
+    if ($this->stackPosition > count($this->stack) - 2)
+    {
+      throw new Exception('You are already on the last page.');
+    }
+
+    ++$this->stackPosition;
+    return $this->call($this->stack[$this->stackPosition]['uri'], 
+                       $this->stack[$this->stackPosition]['method'], 
+                       $this->stack[$this->stackPosition]['parameters'], 
+                       $this->stack[$this->stackPosition]['headers'],
+                       false);
+  }
+
+  /**
+   * Submits the current request again
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function reload()
+  {
+    if (-1 == $this->stackPosition)
+    {
+      throw new Exception('No page to reload.');
+    }
+
+    return $this->call($this->stack[$this->stackPosition]['uri'], 
+                       $this->stack[$this->stackPosition]['method'], 
+                       $this->stack[$this->stackPosition]['parameters'], 
+                       $this->stack[$this->stackPosition]['headers'],
+                       false);
+  }
+  
+  /**
+   * Transforms an associative array of header names => header values to its HTTP equivalent.
+   * 
+   * @param    array     $headers
+   * @return   string
+   */
+  public function prepareHeaders($headers = array())
+  {
+    $prepared_headers = array();
+    foreach ($headers as $name => $value)
+    {
+      $prepared_headers[] = sprintf("%s: %s\r\n", ucfirst($name), $value);
+    }
+    
+    return implode('', $prepared_headers);
+  }
+  
+  // Response methods
+
+  /**
+   * Initializes the response and erases all content from prior requests
+   */  
+  public function initializeResponse()
+  {
+    $this->responseHeaders        = array();
+    $this->responseCode           = '';
+    $this->responseText           = '';
+    $this->responseDom            = null;
+    $this->responseDomCssSelector = null;
+    $this->responseXml            = null;
+    $this->fields                 = array();
+  }
+
+  /**
+   * Set the response headers
+   *
+   * @param array The response headers as an array of strings shaped like "key: value"
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function setResponseHeaders($headers = array())
+  {
+    $header_array = array();
+    foreach($headers as $header)
+    {
+      $arr = explode(': ', $header); 
+      if(isset($arr[1]))
+      {
+        $header_array[$this->normalizeHeaderName($arr[0])] = trim($arr[1]);
+      }
+    }
+    
+    $this->responseHeaders = $header_array;
+    
+    return $this;
+  }
+  
+  /**
+   * Set the response code
+   *
+   * @param string The first line of the response
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function setResponseCode($firstLine)
+  {
+    preg_match('/\d{3}/', $firstLine, $matches);
+    if(isset($matches[0]))
+    {
+      $this->responseCode = $matches[0];
+    }
+    else
+    {
+      $this->responseCode = '';
+    }
+    
+    return $this;
+  }  
+
+  /**
+   * Set the response contents
+   *
+   * @param string The response contents
+   *
+   * @return sfWebBrowser The current browser object
+   */  
+  public function setResponseText($res)
+  {
+    $this->responseText = $res;
+    
+    return $this;
+  }
+
+  /**
+   * Get a text version of the response
+   *
+   * @return string The response contents
+   */
+  public function getResponseText()
+  {
+    $text = $this->responseText;
+
+    // Decode any content-encoding (gzip or deflate) if needed
+    switch (strtolower($this->getResponseHeader('content-encoding'))) {
+
+        // Handle gzip encoding
+        case 'gzip':
+            $text = $this->decodeGzip($text);
+            break;
+
+        // Handle deflate encoding
+        case 'deflate':
+            $text = $this->decodeDeflate($text);
+            break;
+
+        default:
+            break;
+    }
+
+    return $text;
+  }
+
+  /**
+   * Get a text version of the body part of the response (without <body> and </body>)
+   *
+   * @return string The body part of the response contents
+   */
+  public function getResponseBody()
+  {
+    preg_match('/<body.*?>(.*)<\/body>/si', $this->getResponseText(), $matches);
+
+    return isset($matches[1]) ? $matches[1] : '';
+  }
+  
+  /**
+   * Get a DOMDocument version of the response 
+   *
+   * @return DOMDocument The reponse contents
+   */
+  public function getResponseDom()
+  {
+    if(!$this->responseDom)
+    {
+      // for HTML/XML content, create a DOM object for the response content
+      if (preg_match('/(x|ht)ml/i', $this->getResponseHeader('Content-Type')))
+      {
+        $this->responseDom = new DomDocument('1.0', 'utf8');
+        $this->responseDom->validateOnParse = true;
+        @$this->responseDom->loadHTML($this->getResponseText());
+      }
+    }
+
+    return $this->responseDom;
+  }
+
+  /**
+   * Get a sfDomCssSelector version of the response 
+   *
+   * @return sfDomCssSelector The response contents
+   */
+  public function getResponseDomCssSelector()
+  {
+    if(!$this->responseDomCssSelector)
+    {
+      // for HTML/XML content, create a DOM object for the response content
+      if (preg_match('/(x|ht)ml/i', $this->getResponseHeader('Content-Type')))
+      {
+        $this->responseDomCssSelector = new sfDomCssSelector($this->getResponseDom());
+      }
+    }
+
+    return $this->responseDomCssSelector;
+  }
+
+  /**
+   * Get a SimpleXML version of the response 
+   *
+   * @return  SimpleXMLElement                      The reponse contents
+   * @throws  sfWebBrowserInvalidResponseException  when response is not in a valid format 
+   */
+  public function getResponseXML()
+  {
+    if(!$this->responseXml)
+    {
+      // for HTML/XML content, create a DOM object for the response content
+      if (preg_match('/(x|ht)ml/i', $this->getResponseHeader('Content-Type')))
+      {
+        $this->responseXml = @simplexml_load_string($this->getResponseText());
+      }
+    }
+    
+    // Throw an exception if response is not valid XML
+    if (get_class($this->responseXml) != 'SimpleXMLElement')
+    {
+      $msg = sprintf("Response is not a valid XML string : \n%s", $this->getResponseText());
+      throw new sfWebBrowserInvalidResponseException($msg);
+    }
+    
+    return $this->responseXml;
+  }
+
+  /**
+   * Returns true if server response is an error.
+   * 
+   * @return   bool
+   */
+  public function responseIsError()
+  {
+    return in_array((int)($this->getResponseCode() / 100), array(4, 5)); 
+  }
+
+  /**
+   * Get the response headers
+   *
+   * @return array The response headers
+   */
+  public function getResponseHeaders()
+  {
+    return $this->responseHeaders;
+  }  
+
+  /**
+   * Get a response header
+   *
+   * @param string The response header name
+   *
+   * @return string The response header value
+   */
+  public function getResponseHeader($key)
+  {
+    $normalized_key = $this->normalizeHeaderName($key);
+    return (isset($this->responseHeaders[$normalized_key])) ? $this->responseHeaders[$normalized_key] : '';
+  }  
+  
+  /**
+   * Decodes gzip-encoded content ("content-encoding: gzip" response header).
+   * 
+   * @param       stream     $gzip_text
+   * @return      string     
+   */
+  protected function decodeGzip($gzip_text)
+  {
+    return gzinflate(substr($gzip_text, 10));
+  }
+
+  /**
+   * Decodes deflate-encoded content ("content-encoding: deflate" response header).
+   * 
+   * @param       stream     $deflate_text
+   * @return      string     
+   */  
+  protected function decodeDeflate($deflate_text)
+  {
+    return gzuncompress($deflate_text);
+  }
+  
+  /**
+   * Get the response code
+   *
+   * @return string The response code
+   */
+  public function getResponseCode()
+  {
+    return $this->responseCode; 
+  }
+  
+  /**
+   * Returns the response message (the 'Not Found' part in  'HTTP/1.1 404 Not Found')
+   * 
+   * @return   string 
+   */
+  public function getResponseMessage()
+  {
+    return $this->responseMessage;    
+  }
+  
+  /**
+   * Sets response message.
+   * 
+   * @param    string    $message
+   */
+  public function setResponseMessage($msg)
+  {
+    $this->responseMessage = $msg;
+  }
+  
+  public function getUrlInfo()
+  {
+    return $this->urlInfo; 
+  }
+  
+  public function getDefaultRequestHeaders()
+  {
+    return $this->defaultHeaders;
+  }
+  
+  /**
+   * Adds default headers to the supplied headers array.
+   * 
+   * @param       array    $headers
+   * @return      array
+   */
+  public function initializeRequestHeaders($headers = array())
+  {
+    // Supported encodings
+    $encodings = array();
+    if (isset($headers['Accept-Encoding']))
+    {
+      $encodings = explode(',', $headers['Accept-Encoding']);
+    }
+    if (function_exists('gzinflate') && !in_array('gzip', $encodings))
+    {
+      $encodings[] = 'gzip';
+    }
+    if (function_exists('gzuncompress') && !in_array('deflate', $encodings))
+    {
+      $encodings[] = 'deflate';
+    }
+    
+    $headers['Accept-Encoding'] = implode(',', array_unique($encodings));
+    
+    return $headers;
+  }
+  
+  /**
+   * Validates supplied headers and turns all names to lowercase.
+   * 
+   * @param     array     $headers
+   * @return    array
+   */
+  private function fixHeaders($headers)
+  {
+    $fixed_headers = array();
+    foreach ($headers as $name => $value)
+    {
+      if (!preg_match('/([a-z]*)(-[a-z]*)*/i', $name))
+      {
+        $msg = sprintf('Invalid header "%s"', $name);
+        throw new Exception($msg);
+      }
+      $fixed_headers[$this->normalizeHeaderName($name)] = trim($value);
+    }
+
+    return $fixed_headers;
+  }
+  
+  /**
+   * Retrieves a normalized Header.
+   *
+   * @param string Header name
+   *
+   * @return string Normalized header
+   */
+  protected function normalizeHeaderName($name)
+  {
+    return preg_replace('/\-(.)/e', "'-'.strtoupper('\\1')", strtr(ucfirst($name), '_', '-'));
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/thdProject/plugins/sfWebBrowserPlugin/lib/sfWebBrowserInvalidResponseException.class.php	Wed Apr 28 17:20:48 2010 +0200
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * This exception is thrown when response sent to browser is not in a valid format.
+ *
+ * @package    sfWebBrowserPlugin
+ * @author     Tristan Rivoallan <tristan@rivoallan.net>
+ */
+class sfWebBrowserInvalidResponseException extends sfException
+{
+  /**
+   * Class constructor.
+   *
+   * @param string The error message
+   * @param int    The error code
+   */
+  public function __construct($message = null, $code = 0)
+  {
+    if(method_exists($this, 'setName'))
+    {
+      $this->setName('sfWebBrowserInvalidResponseException');
+    }
+    parent::__construct($message, $code);
+  }
+}