Development

/branches/1.1/lib/util/sfBrowser.class.php

You must first sign up to be able to contribute.

root/branches/1.1/lib/util/sfBrowser.class.php

Revision 15726, 20.4 kB (checked in by fabien, 5 years ago)

[1.1, 1.2, 1.3] fixed sf_culture being set automatically when testing, but not when using the site (closes #5852)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * sfBrowser simulates a fake browser which can surf a symfony application.
13  *
14  * @package    symfony
15  * @subpackage util
16  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
17  * @version    SVN: $Id$
18  */
19 class sfBrowser
20 {
21   protected
22     $context            = null,
23     $hostname           = null,
24     $remote             = null,
25     $dom                = null,
26     $stack              = array(),
27     $stackPosition      = -1,
28     $cookieJar          = array(),
29     $fields             = array(),
30     $files              = array(),
31     $vars               = array(),
32     $defaultServerArray = array(),
33     $headers            = array(),
34     $currentException   = null,
35     $rawConfiguration   = null;
36
37   /**
38    * Class constructor.
39    *
40    * @param string $hostname  Hostname to browse
41    * @param string $remote    Remote address to spook
42    * @param array  $options   Options for sfBrowser
43    *
44    * @return void
45    */
46   public function __construct($hostname = null, $remote = null, $options = array())
47   {
48     $this->initialize($hostname, $remote, $options);
49   }
50
51   /**
52    * Initializes sfBrowser - sets up environment
53    *
54    * @param string $hostname  Hostname to browse
55    * @param string $remote    Remote address to spook
56    * @param array  $options   Options for sfBrowser
57    *
58    * @return void
59    */
60   public function initialize($hostname = null, $remote = null, $options = array())
61   {
62     unset($_SERVER['argv']);
63     unset($_SERVER['argc']);
64
65     // setup our fake environment
66     $this->hostname = $hostname;
67     $this->remote   = $remote;
68
69     sfConfig::set('sf_path_info_array', 'SERVER');
70     sfConfig::set('sf_test', true);
71
72     // we set a session id (fake cookie / persistence)
73     $this->newSession();
74
75     // store default global $_SERVER array
76     $this->defaultServerArray = $_SERVER;
77
78     // register our shutdown function
79     register_shutdown_function(array($this, 'shutdown'));
80   }
81
82   /**
83    * Sets variable name
84    *
85    * @param string $name   The variable name
86    * @param mixed  $value  The value
87    *
88    * @return sfBrowser
89    */
90   public function setVar($name, $value)
91   {
92     $this->vars[$name] = $value;
93
94     return $this;
95   }
96
97   /**
98    * Sets a HTTP header for the very next request.
99    *
100    * @param string $header  The header name
101    * @param string $value   The header value
102    */
103   public function setHttpHeader($header, $value)
104   {
105     $this->headers[$header] = $value;
106
107     return $this;
108   }
109
110   /**
111    * Sets username and password for simulating http authentication.
112    *
113    * @param string $username  The username
114    * @param string $password  The password
115    *
116    * @return sfBrowser
117    */
118   public function setAuth($username, $password)
119   {
120     $this->vars['PHP_AUTH_USER'] = $username;
121     $this->vars['PHP_AUTH_PW']   = $password;
122
123     return $this;
124   }
125
126   /**
127    * Gets a uri.
128    *
129    * @param string $uri         The URI to fetch
130    * @param array  $parameters  The Request parameters
131    *
132    * @return sfBrowser
133    */
134   public function get($uri, $parameters = array())
135   {
136     return $this->call($uri, 'get', $parameters);
137   }
138
139   /**
140    * Posts a uri.
141    *
142    * @param string $uri         The URI to fetch
143    * @param array  $parameters  The Request parameters
144    *
145    * @return sfBrowser
146    */
147   public function post($uri, $parameters = array())
148   {
149     return $this->call($uri, 'post', $parameters);
150   }
151
152   /**
153    * Calls a request to a uri.
154    *
155    * @param string $uri          The URI to fetch
156    * @param string $method       The request method
157    * @param array  $parameters   The Request parameters
158    * @param bool   $changeStack  Change the browser history stack?
159    *
160    * @return sfBrowser
161    */
162   public function call($uri, $method = 'get', $parameters = array(), $changeStack = true)
163   {
164     // check that the previous call() hasn't returned an uncatched exception
165     $this->checkCurrentExceptionIsEmpty();
166
167     $uri = $this->fixUri($uri);
168
169     // add uri to the stack
170     if ($changeStack)
171     {
172       $this->stack = array_slice($this->stack, 0, $this->stackPosition + 1);
173       $this->stack[] = array(
174         'uri'        => $uri,
175         'method'     => $method,
176         'parameters' => $parameters,
177       );
178       $this->stackPosition = count($this->stack) - 1;
179     }
180
181     list($path, $query_string) = false !== ($pos = strpos($uri, '?')) ? array(substr($uri, 0, $pos), substr($uri, $pos + 1)) : array($uri, '');
182     $query_string = html_entity_decode($query_string);
183
184     // remove anchor
185     $path = preg_replace('/#.*/', '', $path);
186
187     // removes all fields from previous request
188     $this->fields = array();
189
190     // prepare the request object
191     $_SERVER = $this->defaultServerArray;
192     $_SERVER['HTTP_HOST']       = $this->hostname ? $this->hostname : sfConfig::get('sf_app').'-'.sfConfig::get('sf_environment');
193     $_SERVER['SERVER_NAME']     = $_SERVER['HTTP_HOST'];
194     $_SERVER['SERVER_PORT']     = 80;
195     $_SERVER['HTTP_USER_AGENT'] = 'PHP5/CLI';
196     $_SERVER['REMOTE_ADDR']     = $this->remote ? $this->remote : '127.0.0.1';
197     $_SERVER['REQUEST_METHOD']  = strtoupper($method);
198     $_SERVER['PATH_INFO']       = $path;
199     $_SERVER['REQUEST_URI']     = '/index.php'.$uri;
200     $_SERVER['SCRIPT_NAME']     = '/index.php';
201     $_SERVER['SCRIPT_FILENAME'] = '/index.php';
202     $_SERVER['QUERY_STRING']    = $query_string;
203     foreach ($this->vars as $key => $value)
204     {
205       $_SERVER[strtoupper($key)] = $value;
206     }
207
208     foreach ($this->headers as $header => $value)
209     {
210       $_SERVER['HTTP_'.strtoupper(str_replace('-', '_', $header))] = $value;
211     }
212     $this->headers = array();
213
214     // request parameters
215     $_GET = $_POST = array();
216     if (strtoupper($method) == 'POST')
217     {
218       $_POST = $parameters;
219     }
220     if (strtoupper($method) == 'GET')
221     {
222       $_GET  = $parameters;
223     }
224
225     // handle input type="file" fields
226     if (count($this->files))
227     {
228       $_FILES = $this->files;
229     }
230     $this->files = array();
231
232     parse_str($query_string, $qs);
233     if (is_array($qs))
234     {
235       $_GET = array_merge($qs, $_GET);
236     }
237
238     // restore cookies
239     $_COOKIE = array();
240     foreach ($this->cookieJar as $name => $cookie)
241     {
242       $_COOKIE[$name] = $cookie['value'];
243     }
244
245     ob_start();
246
247     // recycle our context object
248     $this->context = $this->getContext(true);
249
250     // launch request via controller
251     $controller = $this->context->getController();
252     $request    = $this->context->getRequest();
253     $response   = $this->context->getResponse();
254
255     // we register a fake rendering filter
256     sfConfig::set('sf_rendering_filter', array('sfFakeRenderingFilter', null));
257
258     $this->currentException = null;
259
260     // dispatch our request
261     $controller->dispatch();
262
263     $retval = ob_get_clean();
264
265     // append retval to the response content
266     $response->setContent($retval);
267
268     // manually shutdown user to save current session data
269     $this->context->getUser()->shutdown();
270     $this->context->getStorage()->shutdown();
271
272     // save cookies
273     $this->cookieJar = array();
274     foreach ($response->getCookies() as $name => $cookie)
275     {
276       // FIXME: deal with expire, path, secure, ...
277       $this->cookieJar[$name] = $cookie;
278     }
279
280     // support for the ETag header
281     if ($etag = $this->context->getResponse()->getHttpHeader('Etag'))
282     {
283       $this->vars['HTTP_IF_NONE_MATCH'] = $etag;
284     }
285     else
286     {
287       unset($this->vars['HTTP_IF_NONE_MATCH']);
288     }
289
290     // support for the last modified header
291     if ($lastModified = $this->context->getResponse()->getHttpHeader('Last-Modified'))
292     {
293       $this->vars['HTTP_IF_MODIFIED_SINCE'] = $lastModified;
294     }
295     else
296     {
297       unset($this->vars['HTTP_IF_MODIFIED_SINCE']);
298     }
299
300     // for HTML/XML content, create a DOM and sfDomCssSelector objects for the response content
301     if (preg_match('/(x|ht)ml/i', $response->getContentType(), $matches))
302     {
303       $this->dom = new DomDocument('1.0', sfConfig::get('sf_charset'));
304       $this->dom->validateOnParse = true;
305       if ('x' == $matches[1])
306       {
307         @$this->dom->loadXML($response->getContent());
308       }
309       else
310       {
311         @$this->dom->loadHTML($response->getContent());
312       }
313       $this->domCssSelector = new sfDomCssSelector($this->dom);
314     }
315     else
316     {
317       $this->dom = null;
318       $this->domCssSelector = null;
319     }
320
321     return $this;
322   }
323
324   /**
325    * Go back in the browser history stack.
326    *
327    * @return sfBrowser
328    */
329   public function back()
330   {
331     if ($this->stackPosition < 1)
332     {
333       throw new sfException('You are already on the first page.');
334     }
335
336     --$this->stackPosition;
337     return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
338   }
339
340   /**
341    * Go forward in the browser history stack.
342    *
343    * @return sfBrowser
344    */
345   public function forward()
346   {
347     if ($this->stackPosition > count($this->stack) - 2)
348     {
349       throw new sfException('You are already on the last page.');
350     }
351
352     ++$this->stackPosition;
353     return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
354   }
355
356   /**
357    * Reload the current browser.
358    *
359    * @return sfBrowser
360    */
361   public function reload()
362   {
363     if (-1 == $this->stackPosition)
364     {
365       throw new sfException('No page to reload.');
366     }
367
368     return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
369   }
370
371   /**
372    * Get response dom css selector.
373    *
374    * @return sfDomCssSelector
375    */
376   public function getResponseDomCssSelector()
377   {
378     if (is_null($this->dom))
379     {
380       throw new sfException('The DOM is not accessible because the browser response content type is not HTML.');
381     }
382
383     return $this->domCssSelector;
384   }
385
386   /**
387    * Get response dom.
388    *
389    * @return sfDomCssSelector
390    */
391   public function getResponseDom()
392   {
393     if (is_null($this->dom))
394     {
395       throw new sfException('The DOM is not accessible because the browser response content type is not HTML.');
396     }
397
398     return $this->dom;
399   }
400
401   /**
402    * Returns the current application context.
403    *
404    * @param  bool $forceReload  true to force context reload, false otherwise
405    *
406    * @return sfContext
407    */
408   public function getContext($forceReload = false)
409   {
410     if (is_null($this->context) || $forceReload)
411     {
412       $isContextEmpty = is_null($this->context);
413       $context = $isContextEmpty ? sfContext::getInstance() : $this->context;
414
415       $currentConfiguration = $context->getConfiguration();
416       $configuration = ProjectConfiguration::getApplicationConfiguration($currentConfiguration->getApplication(), $currentConfiguration->getEnvironment(), $currentConfiguration->isDebug());
417       $this->context = sfContext::createInstance($configuration);
418       unset($currentConfiguration);
419
420       if (!$isContextEmpty)
421       {
422         sfConfig::clear();
423         sfConfig::add($this->rawConfiguration);
424       }
425       else
426       {
427         $this->rawConfiguration = sfConfig::getAll();
428       }
429
430       $this->context->getEventDispatcher()->connect('application.throw_exception', array($this, 'ListenToException'));
431     }
432
433     return $this->context;
434   }
435
436   /**
437    * Gets response.
438    *
439    * @return sfWebResponse
440    */
441   public function getResponse()
442   {
443     return $this->context->getResponse();
444   }
445
446   /**
447    * Gets request.
448    *
449    * @return sfWebRequest
450    */
451   public function getRequest()
452   {
453     return $this->context->getRequest();
454   }
455
456   /**
457    * Gets current exception.
458    *
459    * @return sfException
460    */
461   public function getCurrentException()
462   {
463     return $this->currentException;
464   }
465
466   /**
467    * Resets the current exception.
468    */
469   public function resetCurrentException()
470   {
471     $this->currentException = null;
472   }
473
474   /**
475    * Test for an uncaught exception.
476    *
477    * @return  boolean
478    */
479   public function checkCurrentExceptionIsEmpty()
480   {
481     return is_null($this->getCurrentException()) || $this->getCurrentException() instanceof sfError404Exception;
482   }
483
484   /**
485    * Follow redirects?
486    *
487    * @throws sfException If request was not a redirect
488    *
489    * @return sfBrowser
490    */
491   public function followRedirect()
492   {
493     if (null === $this->context->getResponse()->getHttpHeader('Location'))
494     {
495       throw new sfException('The request was not redirected.');
496     }
497
498     return $this->get($this->context->getResponse()->getHttpHeader('Location'));
499   }
500
501   /**
502    * Sets a form field in the browser.
503    *
504    * @param string $name   The field name
505    * @param string $value  The field value
506    *
507    * @return sfBrowser
508    */
509   public function setField($name, $value)
510   {
511     // as we don't know yet the form, just store name/value pairs
512     $this->parseArgumentAsArray($name, $value, $this->fields);
513
514     return $this;
515   }
516
517   /**
518    * Simulates a click on a link or button.
519    *
520    * @param string $name       The link or button text
521    * @param array  $arguments
522    *
523    * @return sfBrowser
524    */
525   public function click($name, $arguments = array())
526   {
527     $dom = $this->getResponseDom();
528
529     if (!$dom)
530     {
531       throw new sfException('Cannot click because there is no current page in the browser.');
532     }
533
534     $xpath = new DomXpath($dom);
535
536     // text link
537     if ($link = $xpath->query(sprintf('//a[.="%s"]', $name))->item(0))
538     {
539       return $this->get($link->getAttribute('href'));
540     }
541
542     // image link
543     if ($link = $xpath->query(sprintf('//a/img[@alt="%s"]/ancestor::a', $name))->item(0))
544     {
545       return $this->get($link->getAttribute('href'));
546     }
547
548     // form
549     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))
550     {
551       if (!$form = $xpath->query(sprintf('//button[.="%s" or @id="%s" or @name="%s"]/ancestor::form', $name, $name, $name))->item(0))
552       {
553         throw new sfException(sprintf('Cannot find the "%s" link or button.', $name));
554       }
555     }
556
557     // form attributes
558     $url = $form->getAttribute('action');
559     $method = $form->getAttribute('method') ? strtolower($form->getAttribute('method')) : 'get';
560
561     // merge form default values and arguments
562     $defaults = array();
563     $arguments = sfToolkit::arrayDeepMerge($this->fields, $arguments);
564
565     foreach ($xpath->query('descendant::input | descendant::textarea | descendant::select', $form) as $element)
566     {
567       $elementName = $element->getAttribute('name');
568       $nodeName    = $element->nodeName;
569       $value       = null;
570
571       if ($nodeName == 'input' && ($element->getAttribute('type') == 'checkbox' || $element->getAttribute('type') == 'radio'))
572       {
573         if ($element->getAttribute('checked'))
574         {
575           $value = $element->hasAttribute('value') ? $element->getAttribute('value') : '1';
576         }
577       }
578       else if ($nodeName == 'input' && $element->getAttribute('type') == 'file')
579       {
580         $ph = new sfParameterHolder();
581         $ph->add($arguments);
582
583         $filename = $ph->get($elementName, '');
584
585         if (is_readable($filename))
586         {
587           $fileError = UPLOAD_ERR_OK;
588           $fileSize = filesize($filename);
589         }
590         else
591         {
592           $fileError = UPLOAD_ERR_NO_FILE;
593           $fileSize = 0;
594         }
595
596         $ph->remove($elementName);
597         $arguments = $ph->getAll();
598
599         $this->parseArgumentAsArray($elementName, array('name' => basename($filename), 'type' => '', 'tmp_name' => $filename, 'error' => $fileError, 'size' => $fileSize), $this->files);
600       }
601       else if (
602         $nodeName == 'input'
603         &&
604         (($element->getAttribute('type') != 'submit' && $element->getAttribute('type') != 'button') || $element->getAttribute('value') == $name)
605         &&
606         ($element->getAttribute('type') != 'image' || $element->getAttribute('alt') == $name)
607       )
608       {
609         $value = $element->getAttribute('value');
610       }
611       else if ($nodeName == 'textarea')
612       {
613         $value = '';
614         foreach ($element->childNodes as $el)
615         {
616           $value .= $dom->saveXML($el);
617         }
618       }
619       else if ($nodeName == 'select')
620       {
621         if ($multiple = $element->hasAttribute('multiple'))
622         {
623           $elementName = str_replace('[]', '', $elementName);
624           $value = array();
625         }
626         else
627         {
628           $value = null;
629         }
630
631         $found = false;
632         foreach ($xpath->query('descendant::option', $element) as $option)
633         {
634           if ($option->getAttribute('selected'))
635           {
636             $found = true;
637             if ($multiple)
638             {
639               $value[] = $option->getAttribute('value');
640             }
641             else
642             {
643               $value = $option->getAttribute('value');
644             }
645           }
646         }
647
648         // if no option is selected and if it is a simple select box, take the first option as the value
649         $option = $xpath->query('descendant::option', $element)->item(0);
650         if (!$found && !$multiple && $option instanceof DOMElement)
651         {
652           $value = $option->getAttribute('value');
653         }
654       }
655
656       if (null !== $value)
657       {
658         $this->parseArgumentAsArray($elementName, $value, $defaults);
659       }
660     }
661
662     // create request parameters
663     $arguments = sfToolkit::arrayDeepMerge($defaults, $arguments);
664     if ('post' == $method)
665     {
666       return $this->post($url, $arguments);
667     }
668     else
669     {
670       $query_string = http_build_query($arguments, null, '&');
671       $sep = false === strpos($url, '?') ? '?' : '&';
672
673       return $this->get($url.($query_string ? $sep.$query_string : ''));
674     }
675   }
676
677   /**
678    * Parses arguments as array
679    *
680    * @param string $name   The argument name
681    * @param string $value  The argument value
682    * @param array  $vars
683    */
684   protected function parseArgumentAsArray($name, $value, &$vars)
685   {
686     if (false !== $pos = strpos($name, '['))
687     {
688       $var = &$vars;
689       $tmps = array_filter(preg_split('/(\[ | \[\] | \])/x', $name), create_function('$s', 'return $s !== "";'));
690       foreach ($tmps as $tmp)
691       {
692         $var = &$var[$tmp];
693       }
694       if ($var)
695       {
696         if (!is_array($var))
697         {
698           $var = array($var);
699         }
700         $var[] = $value;
701       }
702       else
703       {
704         $var = $value;
705       }
706     }
707     else
708     {
709       $vars[$name] = $value;
710     }
711   }
712
713   /**
714    * Reset browser to original state
715    *
716    * @return sfBrowser
717    */
718   public function restart()
719   {
720     $this->newSession();
721     $this->cookieJar     = array();
722     $this->stack         = array();
723     $this->fields        = array();
724     $this->vars          = array();
725     $this->dom           = null;
726     $this->stackPosition = -1;
727
728     return $this;
729   }
730
731   /**
732    * Shutdown function to clean up and remove sessions
733    *
734    * @return void
735    */
736   public function shutdown()
737   {
738     $this->checkCurrentExceptionIsEmpty();
739
740     // we remove all session data
741     sfToolkit::clearDirectory(sfConfig::get('sf_test_cache_dir').'/sessions');
742   }
743
744   /**
745    * Fixes uri removing # declarations and front controller.
746    *
747    * @param  string $uri  The URI to fix
748    * @return string The fixed uri
749    */
750   protected function fixUri($uri)
751   {
752     // remove absolute information if needed (to be able to do follow redirects, click on links, ...)
753     if (0 === strpos($uri, 'http'))
754     {
755       // detect secure request
756       if (0 === strpos($uri, 'https'))
757       {
758         $this->defaultServerArray['HTTPS'] = 'on';
759       }
760       else
761       {
762         unset($this->defaultServerArray['HTTPS']);
763       }
764
765       $uri = substr($uri, strpos($uri, 'index.php') + strlen('index.php'));
766     }
767     $uri = str_replace('/index.php', '', $uri);
768
769     // # as a uri
770     if ($uri && '#' == $uri[0])
771     {
772       $uri = $this->stack[$this->stackPosition]['uri'].$uri;
773     }
774
775     return $uri;
776   }
777
778   /**
779    * Creates a new session in the browser.
780    *
781    * @return void
782    */
783   protected function newSession()
784   {
785     $this->defaultServerArray['session_id'] = $_SERVER['session_id'] = md5(uniqid(rand(), true));
786   }
787
788   /**
789    * Listener for exceptions
790    *
791    * @param  sfEvent $event  The event to handle
792    *
793    * @return void
794    */
795   public function listenToException(sfEvent $event)
796   {
797     $this->currentException = $event->getSubject();
798   }
799 }
800
801 class sfFakeRenderingFilter extends sfFilter
802 {
803   public function execute($filterChain)
804   {
805     $filterChain->execute();
806
807     $this->context->getResponse()->sendContent();
808   }
809 }
810
Note: See TracBrowser for help on using the browser.