Development

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

You must first sign up to be able to contribute.

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

Revision 11021, 14.0 kB (checked in by fabien, 6 years ago)

[1.0] fixed sfBrowser doesn't send checked checkedboxes without value attributes correctly (closes #4230)

  • 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     $vars               = array(),
31     $defaultServerArray = array(),
32     $currentException   = null;
33
34   public function initialize($hostname = null, $remote = null, $options = array())
35   {
36     unset($_SERVER['argv']);
37     unset($_SERVER['argc']);
38
39     // setup our fake environment
40     $this->hostname = $hostname;
41     $this->remote   = $remote;
42
43     sfConfig::set('sf_path_info_array', 'SERVER');
44     sfConfig::set('sf_test', true);
45
46     // we set a session id (fake cookie / persistence)
47     $this->newSession();
48
49     // store default global $_SERVER array
50     $this->defaultServerArray = $_SERVER;
51
52     // register our shutdown function
53     register_shutdown_function(array($this, 'shutdown'));
54   }
55
56   public function setVar($name, $value)
57   {
58     $this->vars[$name] = $value;
59
60     return $this;
61   }
62
63   public function setAuth($login, $password)
64   {
65     $this->vars['PHP_AUTH_USER'] = $login;
66     $this->vars['PHP_AUTH_PW']   = $password;
67
68     return $this;
69   }
70
71   public function get($uri, $parameters = array())
72   {
73     return $this->call($uri, 'get', $parameters);
74   }
75
76   public function post($uri, $parameters = array())
77   {
78     return $this->call($uri, 'post', $parameters);
79   }
80
81   public function call($uri, $method = 'get', $parameters = array(), $changeStack = true)
82   {
83     $uri = $this->fixUri($uri);
84
85     // add uri to the stack
86     if ($changeStack)
87     {
88       $this->stack = array_slice($this->stack, 0, $this->stackPosition + 1);
89       $this->stack[] = array(
90         'uri'        => $uri,
91         'method'     => $method,
92         'parameters' => $parameters,
93       );
94       $this->stackPosition = count($this->stack) - 1;
95     }
96
97     list($path, $query_string) = false !== ($pos = strpos($uri, '?')) ? array(substr($uri, 0, $pos), substr($uri, $pos + 1)) : array($uri, '');
98     $query_string = html_entity_decode($query_string);
99
100     // remove anchor
101     $path = preg_replace('/#.*/', '', $path);
102
103     // removes all fields from previous request
104     $this->fields = array();
105
106     // prepare the request object
107     $_SERVER = $this->defaultServerArray;
108     $_SERVER['HTTP_HOST']       = $this->hostname ? $this->hostname : sfConfig::get('sf_app').'-'.sfConfig::get('sf_environment');
109     $_SERVER['SERVER_NAME']     = $_SERVER['HTTP_HOST'];
110     $_SERVER['SERVER_PORT']     = 80;
111     $_SERVER['HTTP_USER_AGENT'] = 'PHP5/CLI';
112     $_SERVER['REMOTE_ADDR']     = $this->remote ? $this->remote : '127.0.0.1';
113     $_SERVER['REQUEST_METHOD']  = strtoupper($method);
114     $_SERVER['PATH_INFO']       = $path;
115     $_SERVER['REQUEST_URI']     = '/index.php'.$uri;
116     $_SERVER['SCRIPT_NAME']     = '/index.php';
117     $_SERVER['SCRIPT_FILENAME'] = '/index.php';
118     $_SERVER['QUERY_STRING']    = $query_string;
119     foreach ($this->vars as $key => $value)
120     {
121       $_SERVER[strtoupper($key)] = $value;
122     }
123
124     // request parameters
125     $_GET = $_POST = array();
126     if (strtoupper($method) == 'POST')
127     {
128       $_POST = $parameters;
129     }
130     if (strtoupper($method) == 'GET')
131     {
132       $_GET  = $parameters;
133     }
134     parse_str($query_string, $qs);
135     if (is_array($qs))
136     {
137       $_GET = array_merge($qs, $_GET);
138     }
139
140     // restore cookies
141     $_COOKIE = array();
142     foreach ($this->cookieJar as $name => $cookie)
143     {
144       $_COOKIE[$name] = $cookie['value'];
145     }
146
147     // recycle our context object
148     sfContext::removeInstance();
149     $this->context = sfContext::getInstance();
150
151     // launch request via controller
152     $controller = $this->context->getController();
153     $request    = $this->context->getRequest();
154     $response   = $this->context->getResponse();
155
156     // we register a fake rendering filter
157     sfConfig::set('sf_rendering_filter', array('sfFakeRenderingFilter', null));
158
159     $this->currentException = null;
160
161     // dispatch our request
162     ob_start();
163     try
164     {
165       $controller->dispatch();
166     }
167     catch (sfException $e)
168     {
169       $this->currentException = $e;
170
171       $e->printStackTrace();
172     }
173     catch (Exception $e)
174     {
175       $this->currentException = $e;
176
177       $sfException = new sfException();
178       $sfException->printStackTrace($e);
179     }
180     $retval = ob_get_clean();
181
182     if ($this->currentException instanceof sfStopException)
183     {
184       $this->currentException = null;
185     }
186
187     // append retval to the response content
188     $response->setContent($retval);
189
190     // manually shutdown user to save current session data
191     $this->context->getUser()->shutdown();
192     $this->context->getStorage()->shutdown();
193
194     // save cookies
195     $this->cookieJar = array();
196     foreach ($response->getCookies() as $name => $cookie)
197     {
198       // FIXME: deal with expire, path, secure, ...
199       $this->cookieJar[$name] = $cookie;
200     }
201
202     // for HTML/XML content, create a DOM and sfDomCssSelector objects for the response content
203     if (preg_match('/(x|ht)ml/i', $response->getContentType(), $matches))
204     {
205       $this->dom = new DomDocument('1.0', sfConfig::get('sf_charset'));
206       $this->dom->validateOnParse = true;
207       if ('x' == $matches[1])
208       {
209         @$this->dom->loadXML($response->getContent());
210       }
211       else
212       {
213         @$this->dom->loadHTML($response->getContent());
214       }
215       $this->domCssSelector = new sfDomCssSelector($this->dom);
216     }
217     else
218     {
219       $this->dom = null;
220       $this->domCssSelector = null;
221     }
222
223     return $this;
224   }
225
226   public function back()
227   {
228     if ($this->stackPosition < 1)
229     {
230       throw new sfException('You are already on the first page.');
231     }
232
233     --$this->stackPosition;
234     return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
235   }
236
237   public function forward()
238   {
239     if ($this->stackPosition > count($this->stack) - 2)
240     {
241       throw new sfException('You are already on the last page.');
242     }
243
244     ++$this->stackPosition;
245     return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
246   }
247
248   public function reload()
249   {
250     if (-1 == $this->stackPosition)
251     {
252       throw new sfException('No page to reload.');
253     }
254
255     return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
256   }
257
258   public function getResponseDomCssSelector()
259   {
260     if (is_null($this->dom))
261     {
262       throw new sfException('The DOM is not accessible because the browser response content type is not HTML.');
263     }
264
265     return $this->domCssSelector;
266   }
267
268   public function getResponseDom()
269   {
270     if (is_null($this->dom))
271     {
272       throw new sfException('The DOM is not accessible because the browser response content type is not HTML.');
273     }
274
275     return $this->dom;
276   }
277
278   public function getContext()
279   {
280     return $this->context;
281   }
282
283   public function getResponse()
284   {
285     return $this->context->getResponse();
286   }
287
288   public function getRequest()
289   {
290     return $this->context->getRequest();
291   }
292
293   public function getCurrentException()
294   {
295     return $this->currentException;
296   }
297
298   public function followRedirect()
299   {
300     if (null === $this->getContext()->getResponse()->getHttpHeader('Location'))
301     {
302       throw new sfException('The request was not redirected');
303     }
304
305     return $this->get($this->getContext()->getResponse()->getHttpHeader('Location'));
306   }
307
308   public function setField($name, $value)
309   {
310     // as we don't know yet the form, just store name/value pairs
311     $this->parseArgumentAsArray($name, $value, $this->fields);
312
313     return $this;
314   }
315
316   // link or button
317   public function click($name, $arguments = array())
318   {
319     $dom = $this->getResponseDom();
320
321     if (!$dom)
322     {
323       throw new sfException('Cannot click because there is no current page in the browser');
324     }
325
326     $xpath = new DomXpath($dom);
327
328     // text link
329     if ($link = $xpath->query(sprintf('//a[.="%s"]', $name))->item(0))
330     {
331       return $this->get($link->getAttribute('href'));
332     }
333
334     // image link
335     if ($link = $xpath->query(sprintf('//a/img[@alt="%s"]/ancestor::a', $name))->item(0))
336     {
337       return $this->get($link->getAttribute('href'));
338     }
339
340     // form
341     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))
342     {
343       if (!$form = $xpath->query(sprintf('//button[.="%s" or @id="%s" or @name="%s"]/ancestor::form', $name, $name, $name))->item(0))
344       {
345         throw new sfException(sprintf('Cannot find the "%s" link or button.', $name));
346       }
347     }
348
349     // form attributes
350     $url = $form->getAttribute('action');
351     $method = $form->getAttribute('method') ? strtolower($form->getAttribute('method')) : 'get';
352
353     // merge form default values and arguments
354     $defaults = array();
355     foreach ($xpath->query('descendant::input | descendant::textarea | descendant::select', $form) as $element)
356     {
357       $elementName = $element->getAttribute('name');
358       $nodeName    = $element->nodeName;
359       $value       = null;
360       if ($nodeName == 'input' && ($element->getAttribute('type') == 'checkbox' || $element->getAttribute('type') == 'radio'))
361       {
362         if ($element->getAttribute('checked'))
363         {
364           $value = $element->hasAttribute('value') ? $element->getAttribute('value') : '1';
365         }
366       }
367       else if (
368         $nodeName == 'input'
369         &&
370         (($element->getAttribute('type') != 'submit' && $element->getAttribute('type') != 'button') || $element->getAttribute('value') == $name)
371         &&
372         ($element->getAttribute('type') != 'image' || $element->getAttribute('alt') == $name)
373       )
374       {
375         $value = $element->getAttribute('value');
376       }
377       else if ($nodeName == 'textarea')
378       {
379         $value = '';
380         foreach ($element->childNodes as $el)
381         {
382           $value .= $dom->saveXML($el);
383         }
384       }
385       else if ($nodeName == 'select')
386       {
387         if ($multiple = $element->hasAttribute('multiple'))
388         {
389           $elementName = str_replace('[]', '', $elementName);
390           $value = array();
391         }
392         else
393         {
394           $value = null;
395         }
396
397         $found = false;
398         foreach ($xpath->query('descendant::option', $element) as $option)
399         {
400           if ($option->getAttribute('selected'))
401           {
402             $found = true;
403             if ($multiple)
404             {
405               $value[] = $option->getAttribute('value');
406             }
407             else
408             {
409               $value = $option->getAttribute('value');
410             }
411           }
412         }
413
414         // if no option is selected and if it is a simple select box, take the first option as the value
415         $option = $xpath->query('descendant::option', $element)->item(0);
416         if (!$found && !$multiple && $option instanceof DOMElement)
417         {
418           $value = $option->getAttribute('value');
419         }
420       }
421
422       if (null !== $value)
423       {
424         $this->parseArgumentAsArray($elementName, $value, $defaults);
425       }
426     }
427
428     // create request parameters
429     $arguments = sfToolkit::arrayDeepMerge($defaults, $this->fields, $arguments);
430     if ('post' == $method)
431     {
432       return $this->post($url, $arguments);
433     }
434     else
435     {
436       $query_string = http_build_query($arguments);
437       $sep = false === strpos($url, '?') ? '?' : '&';
438
439       return $this->get($url.($query_string ? $sep.$query_string : ''));
440     }
441   }
442
443   protected function parseArgumentAsArray($name, $value, &$vars)
444   {
445     if (false !== $pos = strpos($name, '['))
446     {
447       $var = &$vars;
448       $tmps = array_filter(preg_split('/(\[ | \[\] | \])/x', $name), create_function('$s', 'return $s !== "";'));
449       foreach ($tmps as $tmp)
450       {
451         $var = &$var[$tmp];
452       }
453       if ($var)
454       {
455         if (!is_array($var))
456         {
457           $var = array($var);
458         }
459         $var[] = $value;
460       }
461       else
462       {
463         $var = $value;
464       }
465     }
466     else
467     {
468       $vars[$name] = $value;
469     }
470   }
471
472   public function restart()
473   {
474     $this->newSession();
475     $this->cookieJar     = array();
476     $this->stack         = array();
477     $this->fields        = array();
478     $this->vars          = array();
479     $this->dom           = null;
480     $this->stackPosition = -1;
481
482     return $this;
483   }
484
485   public function shutdown()
486   {
487     // we remove all session data
488     sfToolkit::clearDirectory(sfConfig::get('sf_test_cache_dir').'/sessions');
489   }
490
491   protected function fixUri($uri)
492   {
493     // remove absolute information if needed (to be able to do follow redirects, click on links, ...)
494     if (0 === strpos($uri, 'http'))
495     {
496       // detect secure request
497       if (0 === strpos($uri, 'https'))
498       {
499         $this->defaultServerArray['HTTPS'] = 'on';
500       }
501       else
502       {
503         unset($this->defaultServerArray['HTTPS']);
504       }
505
506       $uri = substr($uri, strpos($uri, 'index.php') + strlen('index.php'));
507     }
508     $uri = str_replace('/index.php', '', $uri);
509
510     // # as a uri
511     if ($uri && '#' == $uri[0])
512     {
513       $uri = $this->stack[$this->stackPosition]['uri'].$uri;
514     }
515
516     return $uri;
517   }
518
519   protected function newSession()
520   {
521     $this->defaultServerArray['session_id'] = $_SERVER['session_id'] = md5(uniqid(rand(), true));
522   }
523 }
524
525 class sfFakeRenderingFilter extends sfFilter
526 {
527   public function execute($filterChain)
528   {
529     $filterChain->execute();
530
531     $this->getContext()->getResponse()->sendContent();
532   }
533 }
534
Note: See TracBrowser for help on using the browser.