Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/*
4
 * This file is part of the symfony package.
5
 * (c) 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
 * sfBrowserBase is the base class for sfBrowser.
13
 *
14
 * It implements features that is independent from the symfony controllers.
15
 *
16
 * @package    symfony
17
 * @subpackage util
18
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
19
 * @version    SVN: $Id: sfBrowserBase.class.php 28702 2010-03-23 12:02:01Z fabien $
20
 */
21
abstract class sfBrowserBase
22
{
23
  protected
24
    $hostname           = null,
25
    $remote             = null,
26
    $dom                = null,
27
    $stack              = array(),
28
    $stackPosition      = -1,
29
    $cookieJar          = array(),
30
    $fields             = array(),
31
    $files              = array(),
32
    $vars               = array(),
33
    $defaultServerArray = array(),
34
    $headers            = array(),
35
    $currentException   = null,
36
    $domCssSelector     = null;
37
 
38
  /**
39
   * Class constructor.
40
   *
41
   * @param string $hostname  Hostname to browse
42
   * @param string $remote    Remote address to spook
43
   * @param array  $options   Options for sfBrowser
44
   *
45
   * @return void
46
   */
47
  public function __construct($hostname = null, $remote = null, $options = array())
48
  {
49
    $this->initialize($hostname, $remote, $options);
50
  }
51
 
52
  /**
53
   * Initializes sfBrowser - sets up environment
54
   *
55
   * @param string $hostname  Hostname to browse
56
   * @param string $remote    Remote address to spook
57
   * @param array  $options   Options for sfBrowser
58
   *
59
   * @return void
60
   */
61
  public function initialize($hostname = null, $remote = null, $options = array())
62
  {
63
    unset($_SERVER['argv']);
64
    unset($_SERVER['argc']);
65
 
66
    // setup our fake environment
67
    $this->hostname = null === $hostname ? 'localhost' : $hostname;
68
    $this->remote   = null === $remote ? '127.0.0.1' : $remote;
69
 
70
    // we set a session id (fake cookie / persistence)
71
    $this->newSession();
72
 
73
    // store default global $_SERVER array
74
    $this->defaultServerArray = $_SERVER;
75
 
76
    // register our shutdown function
77
    register_shutdown_function(array($this, 'shutdown'));
78
  }
79
 
80
  /**
81
   * Sets variable name
82
   *
83
   * @param string $name   The variable name
84
   * @param mixed  $value  The value
85
   *
86
   * @return sfBrowserBase
87
   */
88
  public function setVar($name, $value)
89
  {
90
    $this->vars[$name] = $value;
91
 
92
    return $this;
93
  }
94
 
95
  /**
96
   * Sets a HTTP header for the very next request.
97
   *
98
   * @param string $header  The header name
99
   * @param string $value   The header value
100
   */
101
  public function setHttpHeader($header, $value)
102
  {
103
    $this->headers[$header] = $value;
104
 
105
    return $this;
106
  }
107
 
108
  /**
109
   * Sets a cookie.
110
   *
111
   * @param  string  $name     The cookie name
112
   * @param  string  $value    Value for the cookie
113
   * @param  string  $expire   Cookie expiration period
114
   * @param  string  $path     Path
115
   * @param  string  $domain   Domain name
116
   * @param  bool    $secure   If secure
117
   * @param  bool    $httpOnly If uses only HTTP
118
   *
119
   * @return sfBrowserBase     This sfBrowserBase instance
120
   */
121
  public function setCookie($name, $value, $expire = null, $path = '/', $domain = '', $secure = false, $httpOnly = false)
122
  {
123
    $this->cookieJar[$name] = array(
124
      'name'     => $name,
125
      'value'    => $value,
126
      'expire'   => $expire,
127
      'path'     => $path,
128
      'domain'   => $domain,
129
      'secure'   => (Boolean) $secure,
130
      'httpOnly' => $httpOnly,
131
    );
132
 
133
    return $this;
134
  }
135
 
136
  /**
137
   * Removes a cookie by name.
138
   *
139
   * @param string $name   The cookie name
140
   *
141
   * @return sfBrowserBase This sfBrowserBase instance
142
   */
143
  public function removeCookie($name)
144
  {
145
    unset($this->cookieJar[$name]);
146
 
147
    return $this;
148
  }
149
 
150
  /**
151
   * Clears all cookies.
152
   *
153
   * @return sfBrowserBase This sfBrowserBase instance
154
   */
155
  public function clearCookies()
156
  {
157
    $this->cookieJar = array();
158
 
159
    return $this;
160
  }
161
 
162
  /**
163
   * Sets username and password for simulating http authentication.
164
   *
165
   * @param string $username  The username
166
   * @param string $password  The password
167
   *
168
   * @return sfBrowserBase
169
   */
170
  public function setAuth($username, $password)
171
  {
172
    $this->vars['PHP_AUTH_USER'] = $username;
173
    $this->vars['PHP_AUTH_PW']   = $password;
174
 
175
    return $this;
176
  }
177
 
178
  /**
179
   * Gets a uri.
180
   *
181
   * @param string $uri         The URI to fetch
182
   * @param array  $parameters  The Request parameters
183
   * @param bool   $changeStack  Change the browser history stack?
184
   *
185
   * @return sfBrowserBase
186
   */
187
  public function get($uri, $parameters = array(), $changeStack = true)
188
  {
189
    return $this->call($uri, 'get', $parameters, $changeStack);
190
  }
191
 
192
  /**
193
   * Posts a uri.
194
   *
195
   * @param string $uri         The URI to fetch
196
   * @param array  $parameters  The Request parameters
197
   * @param bool   $changeStack  Change the browser history stack?
198
   *
199
   * @return sfBrowserBase
200
   */
201
  public function post($uri, $parameters = array(), $changeStack = true)
202
  {
203
    return $this->call($uri, 'post', $parameters, $changeStack);
204
  }
205
 
206
  /**
207
   * Calls a request to a uri.
208
   *
209
   * @param string $uri          The URI to fetch
210
   * @param string $method       The request method
211
   * @param array  $parameters   The Request parameters
212
   * @param bool   $changeStack  Change the browser history stack?
213
   *
214
   * @return sfBrowserBase
215
   */
216
  public function call($uri, $method = 'get', $parameters = array(), $changeStack = true)
217
  {
218
    // check that the previous call() hasn't returned an uncatched exception
219
    $this->checkCurrentExceptionIsEmpty();
220
 
221
    $uri = $this->fixUri($uri);
222
 
223
    // add uri to the stack
224
    if ($changeStack)
225
    {
226
      $this->stack = array_slice($this->stack, 0, $this->stackPosition + 1);
227
      $this->stack[] = array(
228
        'uri'        => $uri,
229
        'method'     => $method,
230
        'parameters' => $parameters,
231
      );
232
      $this->stackPosition = count($this->stack) - 1;
233
    }
234
 
235
    list($path, $queryString) = false !== ($pos = strpos($uri, '?')) ? array(substr($uri, 0, $pos), substr($uri, $pos + 1)) : array($uri, '');
236
    $queryString = html_entity_decode($queryString);
237
 
238
    // remove anchor
239
    $path = preg_replace('/#.*/', '', $path);
240
 
241
    // removes all fields from previous request
242
    $this->fields = array();
243
 
244
    // prepare the request object
245
    $_SERVER = $this->defaultServerArray;
246
    $_SERVER['HTTP_HOST']       = $this->hostname;
247
    $_SERVER['SERVER_NAME']     = $_SERVER['HTTP_HOST'];
248
    $_SERVER['SERVER_PORT']     = 80;
249
    $_SERVER['HTTP_USER_AGENT'] = 'PHP5/CLI';
250
    $_SERVER['REMOTE_ADDR']     = $this->remote;
251
    $_SERVER['REQUEST_METHOD']  = strtoupper($method);
252
    $_SERVER['PATH_INFO']       = $path;
253
    $_SERVER['REQUEST_URI']     = '/index.php'.$uri;
254
    $_SERVER['SCRIPT_NAME']     = '/index.php';
255
    $_SERVER['SCRIPT_FILENAME'] = '/index.php';
256
    $_SERVER['QUERY_STRING']    = $queryString;
257
 
258
    if ($this->stackPosition >= 1)
259
    {
260
      $_SERVER['HTTP_REFERER'] = sprintf('http%s://%s%s', isset($this->defaultServerArray['HTTPS']) ? 's' : '', $this->hostname, $this->stack[$this->stackPosition - 1]['uri']);
261
    }
262
 
263
    foreach ($this->vars as $key => $value)
264
    {
265
      $_SERVER[strtoupper($key)] = $value;
266
    }
267
 
268
    foreach ($this->headers as $header => $value)
269
    {
270
      $_SERVER['HTTP_'.strtoupper(str_replace('-', '_', $header))] = $value;
271
    }
272
    $this->headers = array();
273
 
274
    // request parameters
275
    $_GET = $_POST = array();
276
    if (in_array(strtoupper($method), array('POST', 'DELETE', 'PUT')))
277
    {
278
      if (isset($parameters['_with_csrf']) && $parameters['_with_csrf'])
279
      {
280
        unset($parameters['_with_csrf']);
281
        $form = new BaseForm();
282
        $parameters[$form->getCSRFFieldName()] = $form->getCSRFToken();
283
      }
284
 
285
      $_POST = $parameters;
286
    }
287
    if (strtoupper($method) == 'GET')
288
    {
289
      $_GET = $parameters;
290
    }
291
 
292
    // handle input type="file" fields
293
    $_FILES = array();
294
    if (count($this->files))
295
    {
296
      $_FILES = $this->files;
297
    }
298
    $this->files = array();
299
 
300
    parse_str($queryString, $qs);
301
    if (is_array($qs))
302
    {
303
      $_GET = array_merge($qs, $_GET);
304
    }
305
 
306
    // expire cookies
307
    $cookies = $this->cookieJar;
308
    foreach ($cookies as $name => $cookie)
309
    {
310
      if ($cookie['expire'] && $cookie['expire'] < time())
311
      {
312
        unset($this->cookieJar[$name]);
313
      }
314
    }
315
 
316
    // restore cookies
317
    $_COOKIE = array();
318
    foreach ($this->cookieJar as $name => $cookie)
319
    {
320
      $_COOKIE[$name] = $cookie['value'];
321
    }
322
 
323
    $this->doCall();
324
 
325
    $response = $this->getResponse();
326
 
327
    // save cookies
328
    foreach ($response->getCookies() as $name => $cookie)
329
    {
330
      // FIXME: deal with path, secure, ...
331
      $this->cookieJar[$name] = $cookie;
332
    }
333
 
334
    // support for the ETag header
335
    if ($etag = $response->getHttpHeader('Etag'))
336
    {
337
      $this->vars['HTTP_IF_NONE_MATCH'] = $etag;
338
    }
339
    else
340
    {
341
      unset($this->vars['HTTP_IF_NONE_MATCH']);
342
    }
343
 
344
    // support for the last modified header
345
    if ($lastModified = $response->getHttpHeader('Last-Modified'))
346
    {
347
      $this->vars['HTTP_IF_MODIFIED_SINCE'] = $lastModified;
348
    }
349
    else
350
    {
351
      unset($this->vars['HTTP_IF_MODIFIED_SINCE']);
352
    }
353
 
354
    // for HTML/XML content, create a DOM and sfDomCssSelector objects for the response content
355
    $this->dom = null;
356
    $this->domCssSelector = null;
357
    if (preg_match('/(x|ht)ml/i', $response->getContentType(), $matches))
358
    {
359
      $this->dom = new DomDocument('1.0', $response->getCharset());
360
      $this->dom->validateOnParse = true;
361
      if ('x' == $matches[1])
362
      {
363
        @$this->dom->loadXML($response->getContent());
364
      }
365
      else
366
      {
367
        @$this->dom->loadHTML($response->getContent());
368
      }
369
      $this->domCssSelector = new sfDomCssSelector($this->dom);
370
    }
371
 
372
    return $this;
373
  }
374
 
375
  /**
376
   * Calls a request to a uri.
377
   */
378
  abstract protected function doCall();
379
 
380
  /**
381
   * Go back in the browser history stack.
382
   *
383
   * @return sfBrowserBase
384
   */
385
  public function back()
386
  {
387
    if ($this->stackPosition < 1)
388
    {
389
      throw new LogicException('You are already on the first page.');
390
    }
391
 
392
    --$this->stackPosition;
393
    return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
394
  }
395
 
396
  /**
397
   * Go forward in the browser history stack.
398
   *
399
   * @return sfBrowserBase
400
   */
401
  public function forward()
402
  {
403
    if ($this->stackPosition > count($this->stack) - 2)
404
    {
405
      throw new LogicException('You are already on the last page.');
406
    }
407
 
408
    ++$this->stackPosition;
409
    return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
410
  }
411
 
412
  /**
413
   * Reload the current browser.
414
   *
415
   * @return sfBrowserBase
416
   */
417
  public function reload()
418
  {
419
    if (-1 == $this->stackPosition)
420
    {
421
      throw new LogicException('No page to reload.');
422
    }
423
 
424
    return $this->call($this->stack[$this->stackPosition]['uri'], $this->stack[$this->stackPosition]['method'], $this->stack[$this->stackPosition]['parameters'], false);
425
  }
426
 
427
  /**
428
   * Get response DOM CSS selector.
429
   *
430
   * @return sfDomCssSelector
431
   */
432
  public function getResponseDomCssSelector()
433
  {
434
    if (null === $this->domCssSelector)
435
    {
436
      throw new LogicException('The DOM is not accessible because the browser response content type is not HTML.');
437
    }
438
 
439
    return $this->domCssSelector;
440
  }
441
 
442
  /**
443
   * Get the response DOM XPath selector.
444
   *
445
   * @return DOMXPath
446
   *
447
   * @uses getResponseDom()
448
   */
449
  public function getResponseDomXpath()
450
  {
451
    return new DOMXPath($this->getResponseDom());
452
  }
453
 
454
  /**
455
   * Get response DOM.
456
   *
457
   * @return sfDomCssSelector
458
   */
459
  public function getResponseDom()
460
  {
461
    if (null === $this->dom)
462
    {
463
      throw new LogicException('The DOM is not accessible because the browser response content type is not HTML.');
464
    }
465
 
466
    return $this->dom;
467
  }
468
 
469
  /**
470
   * Gets response.
471
   *
472
   * @return sfWebResponse
473
   */
474
  abstract public function getResponse();
475
 
476
  /**
477
   * Gets request.
478
   *
479
   * @return sfWebRequest
480
   */
481
  abstract public function getRequest();
482
 
483
  /**
484
   * Gets user.
485
   *
486
   * @return sfUser
487
   */
488
  abstract public function getUser();
489
 
490
  /**
491
   * Gets the current exception.
492
   *
493
   * @return Exception
494
   */
495
  public function getCurrentException()
496
  {
497
    return $this->currentException;
498
  }
499
 
500
  /**
501
   * Sets the current exception.
502
   *
503
   * @param Exception $exception An Exception instance
504
   */
505
  public function setCurrentException(Exception $exception)
506
  {
507
    $this->currentException = $exception;
508
  }
509
 
510
  /**
511
   * Resets the current exception.
512
   */
513
  public function resetCurrentException()
514
  {
515
    $this->currentException = null;
516
    sfException::clearLastException();
517
  }
518
 
519
  /**
520
   * Test for an uncaught exception.
521
   *
522
   * @return  boolean
523
   */
524
  public function checkCurrentExceptionIsEmpty()
525
  {
526
    return null === $this->getCurrentException() || $this->getCurrentException() instanceof sfError404Exception;
527
  }
528
 
529
  /**
530
   * Follow redirects?
531
   *
532
   * @throws sfException If request was not a redirect
533
   *
534
   * @return sfBrowserBase
535
   */
536
  public function followRedirect()
537
  {
538
    if (null === $this->getResponse()->getHttpHeader('Location'))
539
    {
540
      throw new LogicException('The request was not redirected.');
541
    }
542
 
543
    return $this->get($this->getResponse()->getHttpHeader('Location'));
544
  }
545
 
546
  /**
547
   * Sets a form field in the browser.
548
   *
549
   * @param string $name   The field name
550
   * @param string $value  The field value
551
   *
552
   * @return sfBrowserBase
553
   */
554
  public function setField($name, $value)
555
  {
556
    // as we don't know yet the form, just store name/value pairs
557
    $this->parseArgumentAsArray($name, $value, $this->fields);
558
 
559
    return $this;
560
  }
561
 
562
  /**
563
   * Simulates deselecting a checkbox or radiobutton.
564
   *
565
   * @param string  $name       The checkbox or radiobutton id, name or text
566
   *
567
   * @return sfBrowserBase
568
   *
569
   * @see    doSelect()
570
   */
571
  public function deselect($name)
572
  {
573
    $this->doSelect($name, false);
574
 
575
    return $this;
576
  }
577
 
578
  /**
579
   * Simulates selecting a checkbox or radiobutton.
580
   *
581
   * @param string  $name       The checkbox or radiobutton id, name or text
582
   *
583
   * @return sfBrowserBase
584
   *
585
   * @see    doSelect()
586
   */
587
  public function select($name)
588
  {
589
    $this->doSelect($name, true);
590
 
591
    return $this;
592
  }
593
 
594
  /**
595
   * Simulates selecting a checkbox or radiobutton.
596
   *
597
   * This method is called internally by the select() and deselect() methods.
598
   *
599
   * @param string  $name       The checkbox or radiobutton id, name or text
600
   * @param boolean $selected   If true the item will be selected
601
   *
602
   */
603
  public function doSelect($name, $selected)
604
  {
605
    $xpath = $this->getResponseDomXpath();
606
 
607
    if ($element = $xpath->query(sprintf('//input[(@type="radio" or @type="checkbox") and (.="%s" or @id="%s" or @name="%s")]', $name, $name, $name))->item(0))
608
    {
609
      if ($selected)
610
      {
611
        if ($element->getAttribute('type') == 'radio')
612
        {
613
          //we need to deselect all other radio buttons with the same name
614
          foreach ($xpath->query(sprintf('//input[@type="radio" and @name="%s"]', $element->getAttribute('name'))) as $radio)
615
          {
616
            $radio->removeAttribute('checked');
617
          }
618
        }
619
        $element->setAttribute('checked', 'checked');
620
      }
621
      else
622
      {
623
        if ($element->getAttribute('type') == 'radio')
624
        {
625
          throw new InvalidArgumentException('Radiobutton cannot be deselected - Select another radiobutton to deselect the current.');
626
        }
627
        $element->removeAttribute('checked');
628
      }
629
    }
630
    else
631
    {
632
      throw new InvalidArgumentException(sprintf('Cannot find the "%s" checkbox or radiobutton.', $name));
633
    }
634
  }
635
 
636
  /**
637
   * Simulates a click on a link or button.
638
   *
639
   * Available options:
640
   *
641
   *  * position: The position of the linked to link if several ones have the same name
642
   *              (the first one is 1, not 0)
643
   *  * method:   The method to used instead of the form ones
644
   *              (useful when you need to click on a link that is converted to a form with JavaScript code)
645
   *
646
   * @param  string|DOMElement $name      The link, button text, CSS selector or DOMElement
647
   * @param  array             $arguments The arguments to pass to the link
648
   * @param  array             $options   An array of options
649
   *
650
   * @return sfBrowserBase
651
   *
652
   * @uses   doClickElement() doClick() doClickCssSelector()
653
   */
654
  public function click($name, $arguments = array(), $options = array())
655
  {
656
    if ($name instanceof DOMElement)
657
    {
658
      list($uri, $method, $parameters) = $this->doClickElement($name, $arguments, $options);
659
    }
660
    else
661
    {
662
      try
663
      {
664
        list($uri, $method, $parameters) = $this->doClick($name, $arguments, $options);
665
      }
666
      catch (InvalidArgumentException $e)
667
      {
668
        list($uri, $method, $parameters) = $this->doClickCssSelector($name, $arguments, $options);
669
      }
670
    }
671
 
672
    return $this->call($uri, $method, $parameters);
673
  }
674
 
675
  /**
676
   * Simulates a click on a link or button.
677
   *
678
   * This method is called internally by the {@link click()} method.
679
   *
680
   * @param  string $name      The link or button text
681
   * @param  array  $arguments The arguments to pass to the link
682
   * @param  array  $options   An array of options
683
   *
684
   * @return array An array composed of the URI, the method and the arguments to pass to the {@link call()} call
685
   *
686
   * @uses   getResponseDomXpath() doClickElement()
687
   * @throws InvalidArgumentException If a matching element cannot be found
688
   *
689
   * @deprecated call {@link click()} using a CSS selector instead
690
   */
691
  public function doClick($name, $arguments = array(), $options = array())
692
  {
693
    if (false !== strpos($name, '[') || false !== strpos($name, ']'))
694
    {
695
      throw new InvalidArgumentException(sprintf('The name "%s" is not valid', $name));
696
    }
697
 
698
    $query  = sprintf('//a[.="%s"]', $name);
699
    $query .= sprintf('|//a/img[@alt="%s"]/ancestor::a', $name);
700
    $query .= sprintf('|//input[((@type="submit" or @type="button") and @value="%s") or (@type="image" and @alt="%s")]', $name, $name);
701
    $query .= sprintf('|//button[.="%s" or @id="%s" or @name="%s"]', $name, $name, $name);
702
 
703
    $list = $this->getResponseDomXpath()->query($query);
704
 
705
    $position = isset($options['position']) ? $options['position'] - 1 : 0;
706
 
707
    if (!$item = $list->item($position))
708
    {
709
      throw new InvalidArgumentException(sprintf('Cannot find the "%s" link or button (position %d).', $name, $position + 1));
710
    }
711
 
712
    return $this->doClickElement($item, $arguments, $options);
713
  }
714
 
715
  /**
716
   * Simulates a click on an element indicated by CSS selector.
717
   *
718
   * This method is called internally by the {@link click()} method.
719
   *
720
   * @param  string $selector  The CSS selector
721
   * @param  array  $arguments The arguments to pass to the link
722
   * @param  array  $options   An array of options
723
   *
724
   * @return array An array composed of the URI, the method and the arguments to pass to the {@link call()} call
725
   *
726
   * @uses   getResponseDomCssSelector() doClickElement()
727
   * @throws InvalidArgumentException If a matching element cannot be found
728
   */
729
  public function doClickCssSelector($selector, $arguments = array(), $options = array())
730
  {
731
    $elements = $this->getResponseDomCssSelector()->matchAll($selector)->getNodes();
732
    $position = isset($options['position']) ? $options['position'] - 1 : 0;
733
 
734
    if (isset($elements[$position]))
735
    {
736
      return $this->doClickElement($elements[$position], $arguments, $options);
737
    }
738
    else
739
    {
740
      throw new InvalidArgumentException(sprintf('Could not find the element "%s" (position %d) in the current DOM.', $selector, $position + 1));
741
    }
742
  }
743
 
744
  /**
745
   * Simulates a click on the supplied DOM element.
746
   *
747
   * This method is called internally by the {@link click()} method.
748
   *
749
   * @param  DOMElement $item      The element being clicked
750
   * @param  array      $arguments The arguments to pass to the link
751
   * @param  array      $options   An array of options
752
   *
753
   * @return array An array composed of the URI, the method and the arguments to pass to the call() call
754
   *
755
   * @uses getResponseDomXpath()
756
   */
757
  public function doClickElement(DOMElement $item, $arguments = array(), $options = array())
758
  {
759
    $method = strtolower(isset($options['method']) ? $options['method'] : 'get');
760
 
761
    if ('a' == $item->nodeName)
762
    {
763
      if (in_array($method, array('post', 'put', 'delete')))
764
      {
765
        if (isset($options['_with_csrf']) && $options['_with_csrf'])
766
        {
767
          $arguments['_with_csrf'] = true;
768
        }
769
 
770
        return array($item->getAttribute('href'), $method, $arguments);
771
      }
772
      else
773
      {
774
        return array($item->getAttribute('href'), 'get', $arguments);
775
      }
776
    }
777
    else if ('button' == $item->nodeName || ('input' == $item->nodeName && in_array($item->getAttribute('type'), array('submit', 'button', 'image'))))
778
    {
779
      // add the item's value to the arguments
780
      $this->parseArgumentAsArray($item->getAttribute('name'), $item->getAttribute('value'), $arguments);
781
 
782
      // use the ancestor form element
783
      do
784
      {
785
        if (null === $item = $item->parentNode)
786
        {
787
          throw new Exception('The clicked form element does not have a form ancestor.');
788
        }
789
      }
790
      while ('form' != $item->nodeName);
791
    }
792
 
793
    // form attributes
794
    $url = $item->getAttribute('action');
795
    if (!$url || '#' == $url)
796
    {
797
      $url = $this->stack[$this->stackPosition]['uri'];
798
    }
799
    $method = strtolower(isset($options['method']) ? $options['method'] : ($item->getAttribute('method') ? $item->getAttribute('method') : 'get'));
800
 
801
    // merge form default values and arguments
802
    $defaults = array();
803
    $arguments = sfToolkit::arrayDeepMerge($this->fields, $arguments);
804
 
805
    $xpath = $this->getResponseDomXpath();
806
    foreach ($xpath->query('descendant::input | descendant::textarea | descendant::select', $item) as $element)
807
    {
808
      if ($element->hasAttribute('disabled'))
809
      {
810
        continue;
811
      }
812
 
813
      $elementName = $element->getAttribute('name');
814
      $nodeName    = $element->nodeName;
815
      $value       = null;
816
 
817
      if ($nodeName == 'input' && ($element->getAttribute('type') == 'checkbox' || $element->getAttribute('type') == 'radio'))
818
      {
819
        if ($element->getAttribute('checked'))
820
        {
821
          $value = $element->hasAttribute('value') ? $element->getAttribute('value') : '1';
822
        }
823
      }
824
      else if ($nodeName == 'input' && $element->getAttribute('type') == 'file')
825
      {
826
        $filename = array_key_exists($elementName, $arguments) ? $arguments[$elementName] : sfToolkit::getArrayValueForPath($arguments, $elementName, '');
827
 
828
        if (is_readable($filename))
829
        {
830
          $fileError = UPLOAD_ERR_OK;
831
          $fileSize = filesize($filename);
832
        }
833
        else
834
        {
835
          $fileError = UPLOAD_ERR_NO_FILE;
836
          $fileSize = 0;
837
        }
838
 
839
        unset($arguments[$elementName]);
840
 
841
        $this->parseArgumentAsArray($elementName, array('name' => basename($filename), 'type' => '', 'tmp_name' => $filename, 'error' => $fileError, 'size' => $fileSize), $this->files);
842
      }
843
      else if ('input' == $nodeName && !in_array($element->getAttribute('type'), array('submit', 'button', 'image')))
844
      {
845
        $value = $element->getAttribute('value');
846
      }
847
      else if ($nodeName == 'textarea')
848
      {
849
        $value = '';
850
        foreach ($element->childNodes as $el)
851
        {
852
          $value .= $this->getResponseDom()->saveXML($el);
853
        }
854
      }
855
      else if ($nodeName == 'select')
856
      {
857
        if ($multiple = $element->hasAttribute('multiple'))
858
        {
859
          $elementName = str_replace('[]', '', $elementName);
860
          $value = array();
861
        }
862
        else
863
        {
864
          $value = null;
865
        }
866
 
867
        $found = false;
868
        foreach ($xpath->query('descendant::option', $element) as $option)
869
        {
870
          if ($option->getAttribute('selected'))
871
          {
872
            $found = true;
873
            if ($multiple)
874
            {
875
              $value[] = $option->getAttribute('value');
876
            }
877
            else
878
            {
879
              $value = $option->getAttribute('value');
880
            }
881
          }
882
        }
883
 
884
        // if no option is selected and if it is a simple select box, take the first option as the value
885
        $option = $xpath->query('descendant::option', $element)->item(0);
886
        if (!$found && !$multiple && $option instanceof DOMElement)
887
        {
888
          $value = $option->getAttribute('value');
889
        }
890
      }
891
 
892
      if (null !== $value)
893
      {
894
        $this->parseArgumentAsArray($elementName, $value, $defaults);
895
      }
896
    }
897
 
898
    // create request parameters
899
    $arguments = sfToolkit::arrayDeepMerge($defaults, $arguments);
900
    if (in_array($method, array('post', 'put', 'delete')))
901
    {
902
      return array($url, $method, $arguments);
903
    }
904
    else
905
    {
906
      $queryString = http_build_query($arguments, null, '&');
907
      $sep = false === strpos($url, '?') ? '?' : '&';
908
 
909
      return array($url.($queryString ? $sep.$queryString : ''), 'get', array());
910
    }
911
  }
912
 
913
  /**
914
   * Parses arguments as array
915
   *
916
   * @param string $name   The argument name
917
   * @param string $value  The argument value
918
   * @param array  $vars
919
   */
920
  protected function parseArgumentAsArray($name, $value, &$vars)
921
  {
922
    if (false !== $pos = strpos($name, '['))
923
    {
924
      $var = &$vars;
925
      $tmps = array_filter(preg_split('/(\[ | \[\] | \])/x', $name), create_function('$s', 'return $s !== "";'));
926
      foreach ($tmps as $tmp)
927
      {
928
        $var = &$var[$tmp];
929
      }
930
      if ($var && '[]' === substr($name, -2))
931
      {
932
        if (!is_array($var))
933
        {
934
          $var = array($var);
935
        }
936
        $var[] = $value;
937
      }
938
      else
939
      {
940
        $var = $value;
941
      }
942
    }
943
    else
944
    {
945
      $vars[$name] = $value;
946
    }
947
  }
948
 
949
  /**
950
   * Reset browser to original state
951
   *
952
   * @return sfBrowserBase
953
   */
954
  public function restart()
955
  {
956
    $this->newSession();
957
    $this->cookieJar     = array();
958
    $this->stack         = array();
959
    $this->fields        = array();
960
    $this->vars          = array();
961
    $this->dom           = null;
962
    $this->stackPosition = -1;
963
 
964
    return $this;
965
  }
966
 
967
  /**
968
   * Shutdown function to clean up and remove sessions
969
   *
970
   * @return void
971
   */
972
  public function shutdown()
973
  {
974
    $this->checkCurrentExceptionIsEmpty();
975
  }
976
 
977
  /**
978
   * Fixes uri removing # declarations and front controller.
979
   *
980
   * @param  string $uri  The URI to fix
981
   * @return string The fixed uri
982
   */
983
  public function fixUri($uri)
984
  {
985
    // remove absolute information if needed (to be able to do follow redirects, click on links, ...)
986
    if (0 === strpos($uri, 'http'))
987
    {
988
      // detect secure request
989
      if (0 === strpos($uri, 'https'))
990
      {
991
        $this->defaultServerArray['HTTPS'] = 'on';
992
      }
993
      else
994
      {
995
        unset($this->defaultServerArray['HTTPS']);
996
      }
997
 
998
      $uri = preg_replace('#^https?\://[^/]+/#', '/', $uri);
999
    }
1000
    $uri = str_replace('/index.php', '', $uri);
1001
 
1002
    // # as a uri
1003
    if ($uri && '#' == $uri[0])
1004
    {
1005
      $uri = $this->stack[$this->stackPosition]['uri'].$uri;
1006
    }
1007
 
1008
    return $uri;
1009
  }
1010
 
1011
  /**
1012
   * Creates a new session in the browser.
1013
   *
1014
   * @return void
1015
   */
1016
  protected function newSession()
1017
  {
1018
    $this->defaultServerArray['session_id'] = $_SERVER['session_id'] = md5(uniqid(rand(), true));
1019
  }
1020
}