* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ Phar::mapPhar('mink.phar'); require_once 'phar://mink.phar/vendor/autoload.php'; __HALT_COMPILER(); ?> `¶ç mink.phar1src/Behat/Mink/ClassLoader/MapFileClassLoader.php†DÚ9Q†'ª(Ú¶)src/Behat/Mink/Driver/DriverInterface.phpH DÚ9QH Ãör ¶*src/Behat/Mink/Element/DocumentElement.php?DÚ9Q?(æØ¶"src/Behat/Mink/Element/Element.phpû DÚ9Qû ´ðï«¶+src/Behat/Mink/Element/ElementInterface.phpwDÚ9Qwp@_ˆ¶&src/Behat/Mink/Element/NodeElement.phpÓDÚ9QÓ´S…¶-src/Behat/Mink/Element/TraversableElement.phpì DÚ9Qì ëÓ=¶,src/Behat/Mink/Exception/DriverException.phpØDÚ9QØ,X”K¶-src/Behat/Mink/Exception/ElementException.phpDÚ9QªŠÈ¶1src/Behat/Mink/Exception/ElementHtmlException.php?DÚ9Q?𓝀¶5src/Behat/Mink/Exception/ElementNotFoundException.php“DÚ9Q“Ÿ.ô¶1src/Behat/Mink/Exception/ElementTextException.phpüDÚ9Qü&å¶&src/Behat/Mink/Exception/Exception.php¤ DÚ9Q¤ ÏG¶1src/Behat/Mink/Exception/ExpectationException.phpuDÚ9Qu˜Á}a¶2src/Behat/Mink/Exception/ResponseTextException.phpÕDÚ9QÕn[ªñ¶=src/Behat/Mink/Exception/UnsupportedDriverActionException.php¢DÚ9Q¢NÅGu¶src/Behat/Mink/Mink.php­DÚ9Q­ÏakÒ¶'src/Behat/Mink/Selector/CssSelector.phpÖDÚ9QÖǧ‰Ë¶)src/Behat/Mink/Selector/NamedSelector.phpÙDÚ9QÙ“ª 9¶-src/Behat/Mink/Selector/SelectorInterface.phpYDÚ9QY«úì¶,src/Behat/Mink/Selector/SelectorsHandler.phpœ DÚ9Qœ FRµ¶src/Behat/Mink/Session.php¼DÚ9Q¼ `ƒÈ¶src/Behat/Mink/WebAssert.phpJDÚ9QJ¹»¶Bvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Client.phpž3DÚ9Qž3'³v¶Bvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Cookie.php!DÚ9Q!×v¬¶Evendor/symfony/browser-kit/Symfony/Component/BrowserKit/CookieJar.php]DÚ9Q]ºì‹2¶Cvendor/symfony/browser-kit/Symfony/Component/BrowserKit/History.php‰ DÚ9Q‰ ÆZÚ¶?vendor/symfony/browser-kit/Symfony/Component/BrowserKit/LICENSE)DÚ9Q)NmòQ¶Cvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Request.phpó DÚ9Qó ùÝÕ¶Dvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Response.php- DÚ9Q- lé¶Lvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Tests/ClientTest.phpßIDÚ9QßI®bíݶOvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Tests/CookieJarTest.phpç DÚ9Qç p/V¶Lvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Tests/CookieTest.phpòDÚ9Qò—ÌÕ¶Mvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Tests/HistoryTest.phpïDÚ9QïK “¶Mvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Tests/RequestTest.phpÅDÚ9QÅ|â±Æ¶Nvendor/symfony/browser-kit/Symfony/Component/BrowserKit/Tests/ResponseTest.php DÚ9Q {2†w¶Ivendor/symfony/css-selector/Symfony/Component/CssSelector/CssSelector.php®(DÚ9Q®(=fZ¶Vvendor/symfony/css-selector/Symfony/Component/CssSelector/Exception/ParseException.phpNDÚ9QNuZ¡¶Avendor/symfony/css-selector/Symfony/Component/CssSelector/LICENSE)DÚ9Q)NmòQ¶Mvendor/symfony/css-selector/Symfony/Component/CssSelector/Node/AttribNode.phpwDÚ9QwH–™¶Evendor/symfony/process/Symfony/Component/Process/ExecutableFinder.php DÚ9Q Š!,$¶8vendor/symfony/process/Symfony/Component/Process/LICENSE)DÚ9Q)NmòQ¶Hvendor/symfony/process/Symfony/Component/Process/PhpExecutableFinder.phpÐDÚ9QÐmpí¼¶?vendor/symfony/process/Symfony/Component/Process/PhpProcess.phpDÚ9Q@s†¶<vendor/symfony/process/Symfony/Component/Process/Process.php~DÚ9Q~ ï‚ŶCvendor/symfony/process/Symfony/Component/Process/ProcessBuilder.phpË DÚ9QË ðòÍü¶Nvendor/symfony/process/Symfony/Component/Process/Tests/AbstractProcessTest.php.DÚ9Q.ê•Π¶Rvendor/symfony/process/Symfony/Component/Process/Tests/PhpExecutableFinderTest.phpMDÚ9QMÀ D¶Ivendor/symfony/process/Symfony/Component/Process/Tests/PhpProcessTest.php»DÚ9Q»1åWû¶Mvendor/symfony/process/Symfony/Component/Process/Tests/ProcessBuilderTest.phpm DÚ9Qm †òê¶Uvendor/symfony/process/Symfony/Component/Process/Tests/ProcessFailedExceptionTest.php DÚ9Q ¸ûòâ¶Wvendor/symfony/process/Symfony/Component/Process/Tests/ProcessInSigchildEnvironment.phpÅDÚ9QÅmÃÏï¶Lvendor/symfony/process/Symfony/Component/Process/Tests/ProcessTestHelper.phpDÚ9QK‚„î¶Vvendor/symfony/process/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php DÚ9Q •ù¶Uvendor/symfony/process/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.phpÚDÚ9QÚf÷0¶Lvendor/symfony/process/Symfony/Component/Process/Tests/SimpleProcessTest.php DÚ9Q Ù4™¶%vendor/composer/autoload_classmap.phpˆDÚ9Qˆ^ÙV¶'vendor/composer/autoload_namespaces.phpwDÚ9QwïÑg×¶!vendor/composer/autoload_real.php¾DÚ9Q¾r¶œ¶vendor/composer/ClassLoader.php«DÚ9Q«;yi¶<vendor/alexandresalome/php-selenium/bin/generate_browser.phpÇDÚ9QÇ£6Tò¶+vendor/alexandresalome/php-selenium/LICENSE%DÚ9Q%,Nö¶@vendor/alexandresalome/php-selenium/src/Selenium/BaseBrowser.php÷DÚ9Q÷7¶<vendor/alexandresalome/php-selenium/src/Selenium/Browser.phpþUDÚ9QþUCôÈ]¶;vendor/alexandresalome/php-selenium/src/Selenium/Client.phpŠDÚ9QŠ"‡1¶;vendor/alexandresalome/php-selenium/src/Selenium/Driver.php DÚ9Q 5o-¶>vendor/alexandresalome/php-selenium/src/Selenium/Exception.php¾DÚ9Q¾C w&¶<vendor/alexandresalome/php-selenium/src/Selenium/Locator.php DÚ9Q q¦c¶<vendor/alexandresalome/php-selenium/src/Selenium/Pattern.php}DÚ9Q}õ?жWvendor/alexandresalome/php-selenium/src/Selenium/Specification/Dumper/BrowserDumper.phpIDÚ9QI|ŠïĶWvendor/alexandresalome/php-selenium/src/Selenium/Specification/Dumper/MethodBuilder.phpº DÚ9Qº §¬Íc¶Svendor/alexandresalome/php-selenium/src/Selenium/Specification/Loader/XmlLoader.php¤DÚ9Q¤¯ž¿·¶Ivendor/alexandresalome/php-selenium/src/Selenium/Specification/Method.php•DÚ9Q•ý£[¶Lvendor/alexandresalome/php-selenium/src/Selenium/Specification/Parameter.php…DÚ9Q…RU ¶Pvendor/alexandresalome/php-selenium/src/Selenium/Specification/Specification.phpÑDÚ9QÑ5aÒ¶+vendor/behat/mink-browserkit-driver/LICENSECDÚ9QCù—þ<¶Nvendor/behat/mink-browserkit-driver/src/Behat/Mink/Driver/BrowserKitDriver.phpaDÚ9QaÍ Þã¶'vendor/behat/mink-goutte-driver/LICENSECDÚ9QCù—þ<¶Gvendor/behat/mink-goutte-driver/src/Behat/Mink/Driver/Goutte/Client.phpÍDÚ9QÍDÒ€²¶Fvendor/behat/mink-goutte-driver/src/Behat/Mink/Driver/GoutteDriver.phpêDÚ9Qê–>¶%vendor/behat/mink-sahi-driver/LICENSECDÚ9QCù—þ<¶Bvendor/behat/mink-sahi-driver/src/Behat/Mink/Driver/SahiDriver.phpMDÚ9QMb/¤¶Jvendor/behat/mink-selenium-driver/src/Behat/Mink/Driver/SeleniumDriver.phpÆNDÚ9QÆN8kD¶Ivendor/behat/mink-selenium2-driver/src/Behat/Mink/Driver/Selenium2/syn.js°tDÚ9Q°tv»§¶Lvendor/behat/mink-selenium2-driver/src/Behat/Mink/Driver/Selenium2Driver.php‚bDÚ9Q‚b­5SG¶Kvendor/behat/mink-zombie-driver/src/Behat/Mink/Driver/NodeJS/Connection.phpÇDÚ9QÇXκ۶Tvendor/behat/mink-zombie-driver/src/Behat/Mink/Driver/NodeJS/Server/ZombieServer.phpóDÚ9Qóƒ,|¶Gvendor/behat/mink-zombie-driver/src/Behat/Mink/Driver/NodeJS/Server.phpó+DÚ9Qó+mé¦v¶Fvendor/behat/mink-zombie-driver/src/Behat/Mink/Driver/ZombieDriver.php¿YDÚ9Q¿YPd÷ ¶ vendor/behat/sahi-client/LICENSECDÚ9QCÿ²,ï¶Pvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/CheckboxAccessor.php<DÚ9Q<Y:޶Lvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/FileAccessor.phpiDÚ9QiHi±.¶Nvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/HiddenAccessor.phpoDÚ9Qo Úó÷¶Yvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/ImageSubmitButtonAccessor.phpDÚ9QYPY2¶Nvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/OptionAccessor.phpoDÚ9Qo¹î‡¶Pvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/PasswordAccessor.phpuDÚ9Qu“~¶Mvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/RadioAccessor.phplDÚ9QlYœ ¶Mvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/ResetAccessor.phplDÚ9QlÿÁF)¶Nvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/SelectAccessor.phpoDÚ9Qo¬e·¶Nvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/SubmitAccessor.phpoDÚ9Qo½.’F¶Pvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/TextareaAccessor.phpuDÚ9Qu?šÐ?¶Ovendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Form/TextboxAccessor.phprDÚ9Qr—¥n¶Jvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/HeadingAccessor.php×DÚ9Q×í!›®¶Hvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/ImageAccessor.php4DÚ9Q4_àï¶Hvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/LabelAccessor.php4DÚ9Q4LÀý¶Gvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/LinkAccessor.php1DÚ9Q1˜Š¥¶Kvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/ListItemAccessor.php=DÚ9Q=ŽÌ|u¶Gvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/SpanAccessor.php1DÚ9Q1å ¶Mvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Table/CellAccessor.php0 DÚ9Q0 örÀ¼¶Lvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Table/RowAccessor.phpˆDÚ9Qˆ=Ì4—¶Nvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Table/TableAccessor.phpŽDÚ9QŽˆpBh¶Tvendor/behat/sahi-client/src/Behat/SahiClient/Accessor/Table/TableHeaderAccessor.php DÚ9Q  #þÙ¶8vendor/behat/sahi-client/src/Behat/SahiClient/Client.phpr<DÚ9Qr<r®µ³¶<vendor/behat/sahi-client/src/Behat/SahiClient/Connection.phpQDÚ9QQü4îɶMvendor/behat/sahi-client/src/Behat/SahiClient/Exception/AbstractException.phpªDÚ9Qª!º>¶Mvendor/behat/sahi-client/src/Behat/SahiClient/Exception/AccessorException.php«DÚ9Q«‹QÔV¶Ovendor/behat/sahi-client/src/Behat/SahiClient/Exception/ConnectionException.php´DÚ9Q´9ÉN¶Fvendor/behat/sahi-client/src/Behat/SahiClient/UniversalClassLoader.phpDÚ9Q“ƒ ¶&vendor/fabpot/goutte/Goutte/Client.phpÅDÚ9QÅ«}^¶(vendor/fabpot/goutte/Goutte/Compiler.phpøDÚ9Qø„8’é¶0vendor/fabpot/goutte/Goutte/Tests/ClientTest.phpÏDÚ9QÏ6·ÔG¶vendor/fabpot/goutte/LICENSE)DÚ9Q)ØìT¶.vendor/kriswallsmith/buzz/lib/Buzz/Browser.php—DÚ9Q—˜ñ¶¶<vendor/kriswallsmith/buzz/lib/Buzz/Client/AbstractClient.phpªDÚ9QªR±;¬¶:vendor/kriswallsmith/buzz/lib/Buzz/Client/AbstractCurl.phpyDÚ9QyÒHa|¶<vendor/kriswallsmith/buzz/lib/Buzz/Client/AbstractStream.php‚DÚ9Q‚[NN¶Bvendor/kriswallsmith/buzz/lib/Buzz/Client/BatchClientInterface.php¦DÚ9Q¦¸81¶=vendor/kriswallsmith/buzz/lib/Buzz/Client/ClientInterface.php­DÚ9Q­lЊ^¶2vendor/kriswallsmith/buzz/lib/Buzz/Client/Curl.phpDÚ9QƒG¶=vendor/kriswallsmith/buzz/lib/Buzz/Client/FileGetContents.phplDÚ9Ql [µ¶7vendor/kriswallsmith/buzz/lib/Buzz/Client/MultiCurl.phpbDÚ9Qb ³D*¶Avendor/kriswallsmith/buzz/lib/Buzz/Listener/BasicAuthListener.phpjDÚ9Qj7B~+¶@vendor/kriswallsmith/buzz/lib/Buzz/Listener/CallbackListener.php×DÚ9Q×/ ÎÀ¶=vendor/kriswallsmith/buzz/lib/Buzz/Listener/History/Entry.phpiDÚ9QiY˜Æ$¶?vendor/kriswallsmith/buzz/lib/Buzz/Listener/History/Journal.phpYDÚ9QYœi R¶?vendor/kriswallsmith/buzz/lib/Buzz/Listener/HistoryListener.phpÉDÚ9QÉ‹-Òé¶=vendor/kriswallsmith/buzz/lib/Buzz/Listener/ListenerChain.phpeDÚ9Qeý<'Õ¶Avendor/kriswallsmith/buzz/lib/Buzz/Listener/ListenerInterface.phpDÚ9Qå…H8¶>vendor/kriswallsmith/buzz/lib/Buzz/Listener/LoggerListener.php¬DÚ9Q¬”2AV¶>vendor/kriswallsmith/buzz/lib/Buzz/Message/AbstractMessage.php•DÚ9Q•–!‰·¶>vendor/kriswallsmith/buzz/lib/Buzz/Message/Factory/Factory.php…DÚ9Q…o¨…˜¶Gvendor/kriswallsmith/buzz/lib/Buzz/Message/Factory/FactoryInterface.phpUDÚ9QU†ÿV¶?vendor/kriswallsmith/buzz/lib/Buzz/Message/Form/FormRequest.phpCDÚ9QCä}#¶Hvendor/kriswallsmith/buzz/lib/Buzz/Message/Form/FormRequestInterface.php<DÚ9Q<Va?Œ¶>vendor/kriswallsmith/buzz/lib/Buzz/Message/Form/FormUpload.phpÌ DÚ9QÌ Òj~¶Gvendor/kriswallsmith/buzz/lib/Buzz/Message/Form/FormUploadInterface.phpôDÚ9Qô@êõ¶?vendor/kriswallsmith/buzz/lib/Buzz/Message/MessageInterface.phpžDÚ9QžéY# ¶6vendor/kriswallsmith/buzz/lib/Buzz/Message/Request.phpBDÚ9QBŠD)¶?vendor/kriswallsmith/buzz/lib/Buzz/Message/RequestInterface.php\DÚ9Q\6¶7vendor/kriswallsmith/buzz/lib/Buzz/Message/Response.phpsDÚ9Qs& 2d¶2vendor/kriswallsmith/buzz/lib/Buzz/Util/Cookie.phpDÚ9QÁ8Û†¶5vendor/kriswallsmith/buzz/lib/Buzz/Util/CookieJar.phpDÚ9Q{¥Œ¶/vendor/kriswallsmith/buzz/lib/Buzz/Util/Url.phpeDÚ9Qe½¤ïÕ¶!vendor/kriswallsmith/buzz/LICENSE'DÚ9Q'ˆƒ[ó¶vendor/guzzle/guzzle/build.xmldDÚ9Qdž'õ¶vendor/guzzle/guzzle/LICENSEWDÚ9QW–[Ë ¶"vendor/guzzle/guzzle/phar-stub.phpQDÚ9QQ^ib7¶3vendor/guzzle/guzzle/phing/imports/dependencies.xmlÝ DÚ9QÝ ß¢ýg¶-vendor/guzzle/guzzle/phing/imports/deploy.xmlÈDÚ9QÈ–øÉz¶.vendor/guzzle/guzzle/phing/imports/metrics.xmläDÚ9Qäª[G¶+vendor/guzzle/guzzle/phing/imports/test.xml»DÚ9Q»(“Ä8¶5vendor/guzzle/guzzle/phing/tasks/ComposerLintTask.php`DÚ9Q`WaÇζ>vendor/guzzle/guzzle/phing/tasks/GuzzlePearPharPackageTask.php3/DÚ9Q3/…OqÖ¶7vendor/guzzle/guzzle/phing/tasks/GuzzleSubSplitTask.php—*DÚ9Q—*šŶ3vendor/guzzle/guzzle/phing/tasks/NodeServerTask.php= DÚ9Q= …ûF‹¶@vendor/guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php„DÚ9Q„RbѶ/vendor/guzzle/guzzle/src/Guzzle/Batch/Batch.phpr DÚ9Qr uȾ|¶6vendor/guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php@DÚ9Q@3Õf¶=vendor/guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php±DÚ9Q±êù¶¶>vendor/guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.phpMDÚ9QM`¬À¶>vendor/guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php/ DÚ9Q/ –¶QV¶?vendor/guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.phpþDÚ9QþÁ¢~ü¶8vendor/guzzle/guzzle/src/Guzzle/Batch/BatchInterface.phpMDÚ9QMÆ¿s|¶>vendor/guzzle/guzzle/src/Guzzle/Batch/BatchRequestTransfer.php~DÚ9Q~šU“Q¶:vendor/guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php¨DÚ9Q¨°ß¼„¶@vendor/guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.phpDÚ9QÉÄ#T¶Jvendor/guzzle/guzzle/src/Guzzle/Batch/Exception/BatchTransferException.php» DÚ9Q» ".|¶Avendor/guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.phpDÚ9Qwyý¦¶7vendor/guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.phpüDÚ9Qü¾"·+¶6vendor/guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php'DÚ9Q' ƒÐ¶8vendor/guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.phpîDÚ9QîÝ1ßž¶>vendor/guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.phpGDÚ9QG!Ŷ=vendor/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php® DÚ9Q® <ÙB¶?vendor/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.phpëDÚ9Q뎚ó¶=vendor/guzzle/guzzle/src/Guzzle/Cache/ClosureCacheAdapter.phpòDÚ9QòbÅÞô¶>vendor/guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.phpDÚ9QM¤€¶:vendor/guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php)DÚ9Q)¦–1ö9vendor/guzzle/guzzle/src/Guzzle/Cache/Zf1CacheAdapter.phpðDÚ9QðÁ5™B¶9vendor/guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php+DÚ9Q+jåÀ4¶@vendor/guzzle/guzzle/src/Guzzle/Common/AbstractHasDispatcher.phpiDÚ9QiBîÞ€¶5vendor/guzzle/guzzle/src/Guzzle/Common/Collection.phpR.DÚ9QR. ¹Z¶0vendor/guzzle/guzzle/src/Guzzle/Common/Event.phpBDÚ9QB=ƒt¶Kvendor/guzzle/guzzle/src/Guzzle/Common/Exception/BadMethodCallException.php†DÚ9Q†‚qS”¶Hvendor/guzzle/guzzle/src/Guzzle/Common/Exception/ExceptionCollection.phpÎDÚ9QÎYú(w¶Dvendor/guzzle/guzzle/src/Guzzle/Common/Exception/GuzzleException.phpdDÚ9Qdà’ضMvendor/guzzle/guzzle/src/Guzzle/Common/Exception/InvalidArgumentException.phpŠDÚ9QŠùVǶEvendor/guzzle/guzzle/src/Guzzle/Common/Exception/RuntimeException.phpzDÚ9Qzh^Üw¶Mvendor/guzzle/guzzle/src/Guzzle/Common/Exception/UnexpectedValueException.phpŠDÚ9QŠ¢eˆ£¶>vendor/guzzle/guzzle/src/Guzzle/Common/FromConfigInterface.phpçDÚ9Qç½Ù1r¶Avendor/guzzle/guzzle/src/Guzzle/Common/HasDispatcherInterface.php4DÚ9Q4¸ ¨ª¶;vendor/guzzle/guzzle/src/Guzzle/Common/ToArrayInterface.phpõDÚ9Qõü—N¶2vendor/guzzle/guzzle/src/Guzzle/Common/Version.phpvDÚ9Qv9a¥¶Dvendor/guzzle/guzzle/src/Guzzle/Http/AbstractEntityBodyDecorator.phpëDÚ9QëÉœð¶/vendor/guzzle/guzzle/src/Guzzle/Http/Client.phpm3DÚ9Qm3262Ô¶8vendor/guzzle/guzzle/src/Guzzle/Http/ClientInterface.php''DÚ9Q''%S‹h¶8vendor/guzzle/guzzle/src/Guzzle/Http/Curl/CurlHandle.phpÞ=DÚ9QÞ=b¹­´¶7vendor/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMulti.phpÛQDÚ9QÛQæáР¶@vendor/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiInterface.php`DÚ9Q`‰zØü¶9vendor/guzzle/guzzle/src/Guzzle/Http/Curl/CurlVersion.phpDÚ9Qɸڤ¶=vendor/guzzle/guzzle/src/Guzzle/Http/Curl/RequestMediator.php$ DÚ9Q$ ÒIN¶3vendor/guzzle/guzzle/src/Guzzle/Http/EntityBody.phpDÚ9Q¼™t¶<vendor/guzzle/guzzle/src/Guzzle/Http/EntityBodyInterface.php‹ DÚ9Q‹ ò‘ëy¶Gvendor/guzzle/guzzle/src/Guzzle/Http/Exception/BadResponseException.php]DÚ9Q]Øñ-w¶Ovendor/guzzle/guzzle/src/Guzzle/Http/Exception/ClientErrorResponseException.php°DÚ9Q°5"ùÚ¶Pvendor/guzzle/guzzle/src/Guzzle/Http/Exception/CouldNotRewindStreamException.php±DÚ9Q±„n—ð¶@vendor/guzzle/guzzle/src/Guzzle/Http/Exception/CurlException.phpCDÚ9QC&Y˜à¶@vendor/guzzle/guzzle/src/Guzzle/Http/Exception/HttpException.php®DÚ9Q®ˆ ¶Ivendor/guzzle/guzzle/src/Guzzle/Http/Exception/MultiTransferException.php6DÚ9Q6Œð‚¶Cvendor/guzzle/guzzle/src/Guzzle/Http/Exception/RequestException.phpDÚ9Qfö¶Ovendor/guzzle/guzzle/src/Guzzle/Http/Exception/ServerErrorResponseException.php°DÚ9Q°Wpñ¶Lvendor/guzzle/guzzle/src/Guzzle/Http/Exception/TooManyRedirectsException.phpiDÚ9QiǸ–¶=vendor/guzzle/guzzle/src/Guzzle/Http/IoEmittingEntityBody.php®DÚ9Q®ì™Ïu¶@vendor/guzzle/guzzle/src/Guzzle/Http/Message/AbstractMessage.php¯DÚ9Q¯j]#å¶Gvendor/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequest.php#DÚ9Q#ãþ)¥¶Pvendor/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequestInterface.phpÖDÚ9QÖ·\y¶7vendor/guzzle/guzzle/src/Guzzle/Http/Message/Header.phpìDÚ9QìU”»¶Avendor/guzzle/guzzle/src/Guzzle/Http/Message/HeaderComparison.phpÚDÚ9QÚÚ?Ù°¶Avendor/guzzle/guzzle/src/Guzzle/Http/Message/MessageInterface.php¬DÚ9Q¬§kGœ¶9vendor/guzzle/guzzle/src/Guzzle/Http/Message/PostFile.php DÚ9Q ¢« K¶Bvendor/guzzle/guzzle/src/Guzzle/Http/Message/PostFileInterface.php¿DÚ9Q¿Í%*)¶8vendor/guzzle/guzzle/src/Guzzle/Http/Message/Request.php”PDÚ9Q”P|G%¦¶?vendor/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactory.php-DÚ9Q-? |¶Hvendor/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactoryInterface.phpODÚ9QOu¶3E¶Avendor/guzzle/guzzle/src/Guzzle/Http/Message/RequestInterface.phpó"DÚ9Qó"n˜o×¶9vendor/guzzle/guzzle/src/Guzzle/Http/Message/Response.phpycDÚ9Qycc4\¶2vendor/guzzle/guzzle/src/Guzzle/Http/Mimetypes.phps¤DÚ9Qs¤éø¸¶Hvendor/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/CommaAggregator.php-DÚ9Q-:}ð¶¶Lvendor/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/DuplicateAggregator.phpaDÚ9Qaý‰ü¶Fvendor/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/PhpAggregator.php™DÚ9Q™ oí¶Qvendor/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.phpžDÚ9Qž)­¶4vendor/guzzle/guzzle/src/Guzzle/Http/QueryString.php«DÚ9Q«öž¾Î¶<vendor/guzzle/guzzle/src/Guzzle/Http/ReadLimitEntityBody.phpþ DÚ9Qþ ?¨¶7vendor/guzzle/guzzle/src/Guzzle/Http/RedirectPlugin.php|$DÚ9Q|$O¢V¶,vendor/guzzle/guzzle/src/Guzzle/Http/Url.phpœ1DÚ9Qœ1™‘Á§¶8vendor/guzzle/guzzle/src/Guzzle/Inflection/Inflector.php©DÚ9Q©—ݶAvendor/guzzle/guzzle/src/Guzzle/Inflection/InflectorInterface.php>DÚ9Q>Ö¸ìk¶Avendor/guzzle/guzzle/src/Guzzle/Inflection/MemoizingInflector.phpöDÚ9QöÐ\¢’¶Cvendor/guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.phpYDÚ9QY«ÂO"¶;vendor/guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php¼DÚ9Q¼»ý±¶<vendor/guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.phpãDÚ9Qã ÷̸¶;vendor/guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.phpDÚ9Qî†c¶8vendor/guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php…DÚ9Q…n÷!¶@vendor/guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.phpbDÚ9Qb]ì¶:vendor/guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.phpEDÚ9QEî>ã¶7vendor/guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php‰DÚ9Q‰5‘K¶9vendor/guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.phpŽDÚ9QŽÛ3¸§¶;vendor/guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.phpèDÚ9Qè$q©¶8vendor/guzzle/guzzle/src/Guzzle/Log/MessageFormatter.phpöDÚ9Qöšuqñ¶9vendor/guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.phpADÚ9QAœ‘·º¶5vendor/guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php½DÚ9Q½·ÀŽQ¶5vendor/guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.phpÛDÚ9QÛY™¥°¶>vendor/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParser.php DÚ9Q 0i$¶Gvendor/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParserInterface.phpÌDÚ9QÌŽ=¶Hvendor/guzzle/guzzle/src/Guzzle/Parser/Message/AbstractMessageParser.phpŠDÚ9QŠà‡Ê„¶@vendor/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParser.phpN DÚ9QN aÒÚ¶Ivendor/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParserInterface.php·DÚ9Q·(5Ù;¶Hvendor/guzzle/guzzle/src/Guzzle/Parser/Message/PeclHttpMessageParser.phpøDÚ9QøZ¼:¶9vendor/guzzle/guzzle/src/Guzzle/Parser/ParserRegistry.php~ DÚ9Q~ ?Ô³¶Fvendor/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/PeclUriTemplate.phpBDÚ9QB,$‹y¶Bvendor/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplate.php¨DÚ9Q¨E YB¶Kvendor/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplateInterface.phpþDÚ9Qþ[Íÿ¶8vendor/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParser.phpƒDÚ9QƒAZ<¶Avendor/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParserInterface.phpDÚ9QÑ"˜¶<vendor/guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.phpQ DÚ9QQ ¨q¶Jvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php† DÚ9Q† (Ó¶Svendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.phpRDÚ9QRV½dí¶@vendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php} DÚ9Q} ¦Às‚¶@vendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.phpiDÚ9QiKs†¶Kvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.phpãDÚ9QãXà±|¶Jvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CallbackBackoffStrategy.phpVDÚ9QVÕ‡Âý¶Jvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.phpéDÚ9Qé·áʶFvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.phpüDÚ9Qü$oo¶Mvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.phpØDÚ9QØXPÞ¶Fvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/HttpBackoffStrategy.php·DÚ9Q·2§¡¶Hvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.phpãDÚ9Qã0¥¶Nvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.phpøDÚ9Qøy†Â¶Kvendor/guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php%DÚ9Q%ž‘u¶Jvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php¤DÚ9Q¤/8¶ë¶<vendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/CachePlugin.php¤;DÚ9Q¤;á†ìn¶Fvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.phpâDÚ9Qâš-B3¶Ivendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/CallbackCacheKeyProvider.phpfDÚ9QfœÜj¶Ivendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/CallbackCanCacheStrategy.phpÞDÚ9QÞtª†–¶Jvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php‰DÚ9Q‰¢óÁ[¶Hvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheKeyProvider.phpäDÚ9Qä Ž,ß¶Dvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.phpöDÚ9Qö¾¶Hvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.phpMDÚ9QMéÍ’¶Dvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php%DÚ9Q%MºOž¶Avendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.phpšDÚ9QšXe™†¶Fvendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/RevalidationInterface.phpDÚ9QzNK¶Avendor/guzzle/guzzle/src/Guzzle/Plugin/Cache/SkipRevalidation.phpšDÚ9Qš/»¦þ¶8vendor/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php€.DÚ9Q€.űî*¶Jvendor/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.phpTDÚ9QT['Ã[¶Nvendor/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.phpô DÚ9Qô Wß{ ¶Ivendor/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/FileCookieJar.php’DÚ9Q’xMĶ>vendor/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php§DÚ9Q§9Æ$¶Rvendor/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.phpªDÚ9Qª!m|€¶Bvendor/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.phpˆDÚ9QˆyÄÓ„¶Xvendor/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.phpVDÚ9QV|¬F¡¶Lvendor/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponsePlugin.phpö DÚ9Qö Úg¶Yvendor/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php¡DÚ9Q¡«ÔÒ™¶@vendor/guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.phpq DÚ9Qq ]Khå¶8vendor/guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php›DÚ9Q›g2•E¶Fvendor/guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.phpºDÚ9QºúM¶Avendor/guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.phpî DÚ9Qî fä›S¶:vendor/guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.phpIDÚ9QI6Vï4¶<vendor/guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php‹DÚ9Q‹Ô.1E¶@vendor/guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.phpÁDÚ9QÁͬþжBvendor/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php“DÚ9Q“¶ãõ1¶Kvendor/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.phpÝDÚ9QÝA뼊¶Hvendor/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderLoader.phpŽ DÚ9QŽ ÃçJ¶?vendor/guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.phpþDÚ9Qþêãù¶2vendor/guzzle/guzzle/src/Guzzle/Service/Client.php$DÚ9Q$|DW¶;vendor/guzzle/guzzle/src/Guzzle/Service/ClientInterface.phpÂDÚ9QÂ¥¦Óµ¶Cvendor/guzzle/guzzle/src/Guzzle/Service/Command/AbstractCommand.php¿+DÚ9Q¿+°î‚é¶Bvendor/guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php'DÚ9Q'|Äh¶Dvendor/guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php DÚ9Q ŽXC¶Lvendor/guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php¥DÚ9Q¥zºì¶Ivendor/guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.phpgDÚ9Qg¸¬Ìø¶Hvendor/guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.phpDÚ9Q·0ª@¶Lvendor/guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.phpwDÚ9Qw¹eý¶Pvendor/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php‚DÚ9Q‚Îû+ȶLvendor/guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.phpªDÚ9Qª„8V3¶Fvendor/guzzle/guzzle/src/Guzzle/Service/Command/Factory/MapFactory.php¼DÚ9Q¼çŶUvendor/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.phpÓDÚ9QÓ~°¡õ¶bvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.phpA DÚ9QA £Š›†¶Wvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.phpÔ DÚ9QÔ å,OR¶Yvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.phpDÚ9Q™$‰¥¶Wvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php³DÚ9Q³ÉðãO¶\vendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php2DÚ9Q2J  ¾¶[vendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.phpãDÚ9Q㔔̶Xvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php=DÚ9Q=éönn¶cvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php7DÚ9Q7{¸Hƶ_vendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/ResponseBodyVisitor.phpDÚ9QIGw¶Vvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.phpÝDÚ9Q݇cº¶dvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.phpáDÚ9Qá¯NE”¶Xvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/BodyVisitor.php.DÚ9Q.×fª¶Zvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.phpöDÚ9QöP¯‡¶Xvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php` DÚ9Q` ÍÔj‚¶`vendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.phpCDÚ9QC´d~9¶evendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.phpDÚ9Qr£¾"¶^vendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/StatusCodeVisitor.php=DÚ9Q=ÈݶWvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php/DÚ9Q/ÜrB|¶Tvendor/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.phpüDÚ9QüLrt¶Dvendor/guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.phpv DÚ9Qv ¾mõ[¶Kvendor/guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.phpôDÚ9Qôð’¶Nvendor/guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.phpæDÚ9QæÖEk¶Jvendor/guzzle/guzzle/src/Guzzle/Service/Command/ResponseClassInterface.php»DÚ9Q»sÝo¶Kvendor/guzzle/guzzle/src/Guzzle/Service/Command/ResponseParserInterface.phpëDÚ9Qëw޲{¶Avendor/guzzle/guzzle/src/Guzzle/Service/ConfigLoaderInterface.php»DÚ9Q»VQøh¶Avendor/guzzle/guzzle/src/Guzzle/Service/Description/Operation.phpc<DÚ9Qc<ðÓÌ“¶Jvendor/guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php‰DÚ9Q‰eé ç¶Avendor/guzzle/guzzle/src/Guzzle/Service/Description/Parameter.php2bDÚ9Q2b~MN(¶Gvendor/guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php,DÚ9Q,¼…RǶGvendor/guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php'.DÚ9Q'.@Ö¶Jvendor/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.phpùDÚ9Qù´ÁÎ̶Svendor/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php±DÚ9Q±/Sëã¶Pvendor/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionLoader.php DÚ9Q nRGõ¶Jvendor/guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.phpÑDÚ9QÑóµb»¶Fvendor/guzzle/guzzle/src/Guzzle/Service/Exception/CommandException.phpŽDÚ9QŽ£Õ¯¶Nvendor/guzzle/guzzle/src/Guzzle/Service/Exception/CommandTransferException.php8DÚ9Q8 §‚E¶Qvendor/guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php™DÚ9Q™Æ£Ñï¶Yvendor/guzzle/guzzle/src/Guzzle/Service/Exception/InconsistentClientTransferException.php–DÚ9Q–y £“¶Lvendor/guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php•DÚ9Q•#ìíN¶Mvendor/guzzle/guzzle/src/Guzzle/Service/Exception/ServiceBuilderException.php•DÚ9Q•ïèNζNvendor/guzzle/guzzle/src/Guzzle/Service/Exception/ServiceNotFoundException.phpnDÚ9Qn¼S¬h¶Ivendor/guzzle/guzzle/src/Guzzle/Service/Exception/ValidationException.phpDÚ9Qb‘dè¶Tvendor/guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php_DÚ9Q_ á‘¶Uvendor/guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.phpQDÚ9QQ:6ý¶Ovendor/guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.phpÅDÚ9QÅ?ô´¶:vendor/guzzle/guzzle/src/Guzzle/Service/Resource/Model.php4DÚ9Q4ü¢\¶Evendor/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.phpL!DÚ9QL!‘P±¶Qvendor/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.phpæ DÚ9Qæ ÷@ó4¶Qvendor/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.phpDÚ9Qßw–¶Uvendor/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.phpDÚ9Q眪`¶Nvendor/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorInterface.phpDÚ9Qã2ÏP¶1vendor/guzzle/guzzle/src/Guzzle/Stream/Stream.php[DÚ9Q[tšŸ¶:vendor/guzzle/guzzle/src/Guzzle/Stream/StreamInterface.phpDÚ9Q ~:¶/vendor/instaclick/php-webdriver/bin/webunit.phpšDÚ9Qšt©~¶Cvendor/instaclick/php-webdriver/lib/WebDriver/AbstractWebDriver.phprDÚ9Qr&^Þm¶@vendor/instaclick/php-webdriver/lib/WebDriver/AppCacheStatus.php—DÚ9Q—×+ ¶Bvendor/instaclick/php-webdriver/lib/WebDriver/ApplicationCache.php/DÚ9Q/ÌÏ5¶9vendor/instaclick/php-webdriver/lib/WebDriver/Browser.phphDÚ9Qh³ìL¶<vendor/instaclick/php-webdriver/lib/WebDriver/Capability.php: DÚ9Q: itŸÆ¶=vendor/instaclick/php-webdriver/lib/WebDriver/ClassLoader.phpDÚ9Q(E¶;vendor/instaclick/php-webdriver/lib/WebDriver/Container.php¼DÚ9Q¼$?2ò¶9vendor/instaclick/php-webdriver/lib/WebDriver/Element.php¨DÚ9Q¨éé×¶;vendor/instaclick/php-webdriver/lib/WebDriver/Exception.phpU DÚ9QU ¾ò V¶5vendor/instaclick/php-webdriver/lib/WebDriver/Ime.phpDÚ9QÛ»›]¶5vendor/instaclick/php-webdriver/lib/WebDriver/Key.phpDÚ9Q’’s_¶Avendor/instaclick/php-webdriver/lib/WebDriver/LocatorStrategy.phpÎDÚ9QΖ‰Æ¶Fvendor/instaclick/php-webdriver/lib/WebDriver/SauceLabs/Capability.phpê DÚ9Qê ¯Ó•¶Evendor/instaclick/php-webdriver/lib/WebDriver/SauceLabs/SauceRest.php×DÚ9Q×è * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A class loader that uses a mapping file to look up paths. * * @author Fabien Potencier */ class MapFileClassLoader { private $map = array(); /** * Constructor. * * @param string $file Path to class mapping file */ public function __construct($file) { $this->map = require $file; } /** * Registers this instance as an autoloader. * * @param Boolean $prepend Whether to prepend the autoloader or not */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); } /** * Loads the given class or interface. * * @param string $class The name of the class */ public function loadClass($class) { if ('\\' === $class[0]) { $class = substr($class, 1); } if (isset($this->map[$class])) { require $this->map[$class]; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|null The path, if found */ public function findFile($class) { if ('\\' === $class[0]) { $class = substr($class, 1); } if (isset($this->map[$class])) { return $this->map[$class]; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Driver interface. * * @author Konstantin Kudryashov */ interface DriverInterface { /** * Sets driver's current session. * * @param Session $session */ public function setSession(Session $session); /** * Starts driver. */ public function start(); /** * Checks whether driver is started. * * @return Boolean */ public function isStarted(); /** * Stops driver. */ public function stop(); /** * Resets driver. */ public function reset(); /** * Visit specified URL. * * @param string $url url of the page */ public function visit($url); /** * Returns current URL address. * * @return string */ public function getCurrentUrl(); /** * Reloads current page. */ public function reload(); /** * Moves browser forward 1 page. */ public function forward(); /** * Moves browser backward 1 page. */ public function back(); /** * Sets HTTP Basic authentication parameters * * @param string|Boolean $user user name or false to disable authentication * @param string $password password */ public function setBasicAuth($user, $password); /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) */ public function switchToWindow($name = null); /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) */ public function switchToIFrame($name = null); /** * Sets specific request header on client. * * @param string $name * @param string $value */ public function setRequestHeader($name, $value); /** * Returns last response headers. * * @return array */ public function getResponseHeaders(); /** * Sets cookie. * * @param string $name * @param string $value */ public function setCookie($name, $value = null); /** * Returns cookie by name. * * @param string $name * * @return string|null */ public function getCookie($name); /** * Returns last response status code. * * @return integer */ public function getStatusCode(); /** * Returns last response content. * * @return string */ public function getContent(); /** * Capture a screenshot of the current window. * * @return string screenshot of MIME type image/* depending * on driver (e.g., image/png, image/jpeg) */ public function getScreenshot(); /** * Finds elements with specified XPath query. * * @param string $xpath * * @return array array of NodeElements */ public function find($xpath); /** * Returns element's tag name by it's XPath query. * * @param string $xpath * * @return string */ public function getTagName($xpath); /** * Returns element's text by it's XPath query. * * @param string $xpath * * @return string */ public function getText($xpath); /** * Returns element's html by it's XPath query. * * @param string $xpath * * @return string */ public function getHtml($xpath); /** * Returns element's attribute by it's XPath query. * * @param string $xpath * @param string $name * * @return mixed */ public function getAttribute($xpath, $name); /** * Returns element's value by it's XPath query. * * @param string $xpath * * @return mixed */ public function getValue($xpath); /** * Sets element's value by it's XPath query. * * @param string $xpath * @param string $value */ public function setValue($xpath, $value); /** * Checks checkbox by it's XPath query. * * @param string $xpath */ public function check($xpath); /** * Unchecks checkbox by it's XPath query. * * @param string $xpath */ public function uncheck($xpath); /** * Checks whether checkbox checked located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isChecked($xpath); /** * Selects option from select field located by it's XPath query. * * @param string $xpath * @param string $value * @param Boolean $multiple */ public function selectOption($xpath, $value, $multiple = false); /** * Clicks button or link located by it's XPath query. * * @param string $xpath */ public function click($xpath); /** * Double-clicks button or link located by it's XPath query. * * @param string $xpath */ public function doubleClick($xpath); /** * Right-clicks button or link located by it's XPath query. * * @param string $xpath */ public function rightClick($xpath); /** * Attaches file path to file field located by it's XPath query. * * @param string $xpath * @param string $path */ public function attachFile($xpath, $path); /** * Checks whether element visible located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isVisible($xpath); /** * Simulates a mouse over on the element. * * @param string $xpath */ public function mouseOver($xpath); /** * Brings focus to element. * * @param string $xpath */ public function focus($xpath); /** * Removes focus from element. * * @param string $xpath */ public function blur($xpath); /** * Presses specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyPress($xpath, $char, $modifier = null); /** * Pressed down specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyDown($xpath, $char, $modifier = null); /** * Pressed up specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyUp($xpath, $char, $modifier = null); /** * Drag one element onto another. * * @param string $sourceXpath * @param string $destinationXpath */ public function dragTo($sourceXpath, $destinationXpath); /** * Executes JS script. * * @param string $script */ public function executeScript($script); /** * Evaluates JS script. * * @param string $script * * @return mixed */ public function evaluateScript($script); /** * Waits some time or until JS condition turns true. * * @param integer $time time in milliseconds * @param string $condition JS condition */ public function wait($time, $condition); /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) */ public function resizeWindow($width, $height, $name = null); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Document element. * * @author Konstantin Kudryashov */ class DocumentElement extends TraversableElement { /** * Returns XPath for handled element. * * @return string */ public function getXpath() { return '//html'; } /** * Returns document content. * * @return string */ public function getContent() { return trim($this->getSession()->getDriver()->getContent()); } /** * Check whether document has specified content. * * @param string $content * * @return Boolean */ public function hasContent($content) { return $this->has('named', array( 'content', $this->getSession()->getSelectorsHandler()->xpathLiteral($content) )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base element. * * @author Konstantin Kudryashov */ abstract class Element implements ElementInterface { private $session; /** * Initialize element. * * @param Session $session */ public function __construct(Session $session) { $this->session = $session; } /** * Returns element session. * * @return Session */ public function getSession() { return $this->session; } /** * Checks whether element with specified selector exists. * * @param string $selector selector engine name * @param string $locator selector locator * * @return Boolean */ public function has($selector, $locator) { return null !== $this->find($selector, $locator); } /** * Finds first element with specified selector. * * @param string $selector selector engine name * @param string $locator selector locator * * @return NodeElement|null */ public function find($selector, $locator) { $items = $this->findAll($selector, $locator); return count($items) ? current($items) : null; } /** * Finds all elements with specified selector. * * @param string $selector selector engine name * @param string $locator selector locator * * @return array */ public function findAll($selector, $locator) { $xpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($selector, $locator); // add parent xpath before element selector if (0 === strpos($xpath, '/')) { $xpath = $this->getXpath().$xpath; } else { $xpath = $this->getXpath().'/'.$xpath; } return $this->getSession()->getDriver()->find($xpath); } /** * Returns element text (inside tag). * * @return string|null */ public function getText() { return $this->getSession()->getDriver()->getText($this->getXpath()); } /** * Returns element html. * * @return string|null */ public function getHtml() { return $this->getSession()->getDriver()->getHtml($this->getXpath()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Element interface. * * @author Konstantin Kudryashov */ interface ElementInterface { /** * Returns XPath for handled element. * * @return string */ public function getXpath(); /** * Returns element's session. * * @return Session */ public function getSession(); /** * Checks whether element with specified selector exists. * * @param string $selector selector engine name * @param string $locator selector locator * * @return Boolean */ public function has($selector, $locator); /** * Finds first element with specified selector. * * @param string $selector selector engine name * @param string $locator selector locator * * @return NodeElement|null */ public function find($selector, $locator); /** * Finds all elements with specified selector. * * @param string $selector selector engine name * @param string $locator selector locator * * @return array */ public function findAll($selector, $locator); /** * Returns element text (inside tag). * * @return string|null */ public function getText(); /** * Returns element html. * * @return string|null */ public function getHtml(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Page element node. * * @author Konstantin Kudryashov */ class NodeElement extends TraversableElement { private $xpath; /** * Initializes node element. * * @param string $xpath element xpath * @param Session $session session instance */ public function __construct($xpath, Session $session) { $this->xpath = $xpath; parent::__construct($session); } /** * Returns XPath for handled element. * * @return string */ public function getXpath() { return $this->xpath; } /** * Returns parent element to the current one. * * @return NodeElement */ public function getParent() { return $this->find('xpath', '..'); } /** * Returns current node tag name. * * @return string */ public function getTagName() { return $this->getSession()->getDriver()->getTagName($this->getXpath()); } /** * Returns element value. * * @return mixed */ public function getValue() { return $this->getSession()->getDriver()->getValue($this->getXpath()); } /** * Sets node value. * * @param string $value */ public function setValue($value) { try { $this->getSession()->getDriver()->setValue($this->getXpath(), $value); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Checks whether element has attribute with specified name. * * @param string $name * * @return Boolean */ public function hasAttribute($name) { return null !== $this->getSession()->getDriver()->getAttribute($this->getXpath(), $name); } /** * Returns specified attribute value. * * @param string $name * * @return mixed|null */ public function getAttribute($name) { return $this->getSession()->getDriver()->getAttribute($this->getXpath(), $name); } /** * Clicks current node. */ public function click() { try { $this->getSession()->getDriver()->click($this->getXpath()); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Presses current button. */ public function press() { $this->click(); } /** * Double-clicks current node. */ public function doubleClick() { try { $this->getSession()->getDriver()->doubleClick($this->getXpath()); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Right-clicks current node. */ public function rightClick() { try { $this->getSession()->getDriver()->rightClick($this->getXpath()); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Checks current node if it's a checkbox field. */ public function check() { try { $this->getSession()->getDriver()->check($this->getXpath()); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Unchecks current node if it's a checkbox field. */ public function uncheck() { try { $this->getSession()->getDriver()->uncheck($this->getXpath()); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Checks whether current node is checked if it's a checkbox field. * * @return Boolean */ public function isChecked() { return (Boolean) $this->getSession()->getDriver()->isChecked($this->getXpath()); } /** * Selects current node specified option if it's a select field. * * @param string $option * @param Boolean $multiple * * @throws ElementNotFoundException */ public function selectOption($option, $multiple = false) { if ('select' !== $this->getTagName()) { $this->getSession()->getDriver()->selectOption($this->getXpath(), $option, $multiple); return; } $opt = $this->find('named', array( 'option', $this->getSession()->getSelectorsHandler()->xpathLiteral($option) )); if (null === $opt) { throw new ElementNotFoundException( $this->getSession(), 'select option', 'value|text', $option ); } $this->getSession()->getDriver()->selectOption( $this->getXpath(), $opt->getValue(), $multiple ); } /** * Attach file to current node if it's a file input. * * @param string $path path to file (local) */ public function attachFile($path) { try { $this->getSession()->getDriver()->attachFile($this->getXpath(), $path); } catch (\Exception $exception) { throw new ElementException($this, $exception); } } /** * Checks whether current node is visible on page. * * @return Boolean */ public function isVisible() { return (Boolean) $this->getSession()->getDriver()->isVisible($this->getXpath()); } /** * Simulates a mouse over on the element. */ public function mouseOver() { $this->getSession()->getDriver()->mouseOver($this->getXpath()); } /** * Drags current node onto other node. * * @param ElementInterface $destination other node */ public function dragTo(ElementInterface $destination) { $this->getSession()->getDriver()->dragTo($this->getXpath(), $destination->getXpath()); } /** * Brings focus to element. */ public function focus() { $this->getSession()->getDriver()->focus($this->getXpath()); } /** * Removes focus from element. */ public function blur() { $this->getSession()->getDriver()->blur($this->getXpath()); } /** * Presses specific keyboard key. * * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyPress($char, $modifier = null) { $this->getSession()->getDriver()->keyPress($this->getXpath(), $char, $modifier); } /** * Pressed down specific keyboard key. * * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyDown($char, $modifier = null) { $this->getSession()->getDriver()->keyDown($this->getXpath(), $char, $modifier); } /** * Pressed up specific keyboard key. * * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyUp($char, $modifier = null) { $this->getSession()->getDriver()->keyUp($this->getXpath(), $char, $modifier); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Traversable element. * * @author Konstantin Kudryashov */ abstract class TraversableElement extends Element { /** * Finds element by it's id. * * @param string $id element id * * @return NodeElement|null */ public function findById($id) { $id = $this->getSession()->getSelectorsHandler()->xpathLiteral($id); return $this->find('xpath', "//*[@id=$id]"); } /** * Checks whether document has a link with specified locator. * * @param string $locator link id, title, text or image alt * * @return Boolean */ public function hasLink($locator) { return null !== $this->findLink($locator); } /** * Finds link with specified locator. * * @param string $locator link id, title, text or image alt * * @return NodeElement|null */ public function findLink($locator) { return $this->find('named', array( 'link', $this->getSession()->getSelectorsHandler()->xpathLiteral($locator) )); } /** * Clicks link with specified locator. * * @param string $locator link id, title, text or image alt * * @throws ElementNotFoundException */ public function clickLink($locator) { $link = $this->findLink($locator); if (null === $link) { throw new ElementNotFoundException( $this->getSession(), 'link', 'id|title|alt|text', $locator ); } $link->click(); } /** * Checks whether document has a button (input[type=submit|image|button], button) with specified locator. * * @param string $locator button id, value or alt * * @return Boolean */ public function hasButton($locator) { return null !== $this->findButton($locator); } /** * Checks whether an element has a named CSS class * * @param string $className Name of the class * * @return boolean */ public function hasClass($className) { if ($this->hasAttribute('class')) { return in_array($className, explode(' ', $this->getAttribute('class'))); } return false; } /** * Finds button (input[type=submit|image|button], button) with specified locator. * * @param string $locator button id, value or alt * * @return NodeElement|null */ public function findButton($locator) { return $this->find('named', array( 'button', $this->getSession()->getSelectorsHandler()->xpathLiteral($locator) )); } /** * Presses button (input[type=submit|image|button], button) with specified locator. * * @param string $locator button id, value or alt * * @throws ElementNotFoundException */ public function pressButton($locator) { $button = $this->findButton($locator); if (null === $button) { throw new ElementNotFoundException( $this->getSession(), 'button', 'id|name|title|alt|value', $locator ); } $button->press(); } /** * Checks whether document has a field (input, textarea, select) with specified locator. * * @param string $locator input id, name or label * * @return Boolean */ public function hasField($locator) { return null !== $this->findField($locator); } /** * Finds field (input, textarea, select) with specified locator. * * @param string $locator input id, name or label * * @return NodeElement|null */ public function findField($locator) { return $this->find('named', array( 'field', $this->getSession()->getSelectorsHandler()->xpathLiteral($locator) )); } /** * Fills in field (input, textarea, select) with specified locator. * * @param string $locator input id, name or label * @param string $value value * * @throws ElementNotFoundException */ public function fillField($locator, $value) { $field = $this->findField($locator); if (null === $field) { throw new ElementNotFoundException( $this->getSession(), 'form field', 'id|name|label|value', $locator ); } $field->setValue($value); } /** * Checks whether document has a checkbox with specified locator, which is checked. * * @param string $locator input id, name or label * * @return Boolean */ public function hasCheckedField($locator) { $field = $this->findField($locator); return null !== $field && $field->isChecked(); } /** * Checks whether document has a checkbox with specified locator, which is unchecked. * * @param string $locator input id, name or label * * @return Boolean */ public function hasUncheckedField($locator) { $field = $this->findField($locator); return null !== $field && !$field->isChecked(); } /** * Checks checkbox with specified locator. * * @param string $locator input id, name or label * * @throws ElementNotFoundException */ public function checkField($locator) { $field = $this->findField($locator); if (null === $field) { throw new ElementNotFoundException( $this->getSession(), 'form field', 'id|name|label|value', $locator ); } $field->check(); } /** * Unchecks checkbox with specified locator. * * @param string $locator input id, name or label * * @throws ElementNotFoundException */ public function uncheckField($locator) { $field = $this->findField($locator); if (null === $field) { throw new ElementNotFoundException( $this->getSession(), 'form field', 'id|name|label|value', $locator ); } $field->uncheck(); } /** * Checks whether document has a select field with specified locator. * * @param string $locator select id, name or label * * @return Boolean */ public function hasSelect($locator) { return $this->has('named', array( 'select', $this->getSession()->getSelectorsHandler()->xpathLiteral($locator) )); } /** * Selects option from select field with specified locator. * * @param string $locator input id, name or label * @param string $value option value * @param Boolean $multiple select multiple options * * @throws ElementNotFoundException */ public function selectFieldOption($locator, $value, $multiple = false) { $field = $this->findField($locator); if (null === $field) { throw new ElementNotFoundException( $this->getSession(), 'form field', 'id|name|label|value', $locator ); } $field->selectOption($value, $multiple); } /** * Checks whether document has a table with specified locator. * * @param string $locator table id or caption * * @return Boolean */ public function hasTable($locator) { return $this->has('named', array( 'table', $this->getSession()->getSelectorsHandler()->xpathLiteral($locator) )); } /** * Attach file to file field with specified locator. * * @param string $locator input id, name or label * @param string $path path to file * * @throws ElementNotFoundException */ public function attachFileToField($locator, $path) { $field = $this->findField($locator); if (null === $field) { throw new ElementNotFoundException( $this->getSession(), 'form field', 'id|name|label|value', $locator ); } $field->attachFile($path); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink driver exception. * * @author Konstantin Kudryashov */ class DriverException extends Exception { /** * Initializes exception. * * @param string $message * @param int $code * @param \Exception|null $previous */ public function __construct($message, $code = 0, \Exception $previous = null) { parent::__construct($message, null, $code, $previous); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A standard way for elements to re-throw exceptions * * @author Chris Worfolk */ class ElementException extends Exception { private $element; /** * Initialises exception. * * @param Element $element optional message * @param \Exception $exception exception */ public function __construct(Element $element, \Exception $exception) { $this->element = $element; parent::__construct(sprintf("Exception thrown by %s\n%s", $element->getXpath(), $exception->getMessage() )); } /** * Override default toString so we don't send a full backtrace in verbose mode. * * @return string */ public function __toString() { return $this->getMessage(); } /** * Get the element that caused the exception * * @return Element */ public function getElement() { return $this->element; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink's element HTML exception. * * @author Konstantin Kudryashov */ class ElementHtmlException extends ExpectationException { /** * Element instance. * * @var Element */ protected $element; /** * Initializes exception. * * @param string $message optional message * @param Session $session session instance * @param Element $element element * @param \Exception $exception expectation exception */ public function __construct($message = null, Session $session, Element $element, \Exception $exception = null) { $this->element = $element; parent::__construct($message, $session, $exception); } /** * Returns exception message with additional context info. * * @return string */ public function __toString() { try { $pageText = $this->trimString($this->element->getHtml()); $string = sprintf("%s\n\n%s%s", $this->getMessage(), $this->getResponseInfo(), $this->pipeString($pageText."\n") ); } catch (\Exception $e) { return $this->getMessage(); } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink "element not found" exception. * * @author Konstantin Kudryashov */ class ElementNotFoundException extends Exception { /** * Initializes exception. * * @param Session $session session instance * @param string $type element type * @param string $selector element selector type * @param string $locator element locator */ public function __construct(Session $session, $type = null, $selector = null, $locator = null) { $message = ''; if (null !== $type) { $message .= ucfirst($type); } else { $message .= 'Tag'; } if (null !== $locator) { if (null === $selector || in_array($selector, array('css', 'xpath'))) { $selector = 'matching '.($selector ?: 'locator'); } else { $selector = 'with '.$selector; } $message .= ' '.$selector.' "' . $locator . '" '; } $message .= 'not found.'; parent::__construct($message, $session); } /** * Returns exception message with additional context info. * * @return string */ public function __toString() { try { $pageText = $this->trimBody($this->getSession()->getPage()->getContent()); $string = sprintf("%s\n\n%s%s", $this->getMessage(), $this->getResponseInfo(), $this->pipeString($pageText."\n") ); } catch (\Exception $e) { return $this->getMessage(); } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink's element text exception. * * @author Konstantin Kudryashov */ class ElementTextException extends ElementHtmlException { /** * Returns exception message with additional context info. * * @return string */ public function __toString() { try { $pageText = $this->trimString($this->element->getText()); $string = sprintf("%s\n\n%s%s", $this->getMessage(), $this->getResponseInfo(), $this->pipeString($pageText."\n") ); } catch (\Exception $e) { return $this->getMessage(); } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink base exception class. * * @author Konstantin Kudryashov */ abstract class Exception extends \Exception { private $session; /** * Initializes Mink exception. * * @param string $message * @param Session $session * @param integer $code * @param \Exception $previous */ public function __construct($message, Session $session = null, $code = 0, \Exception $previous = null) { $this->session = $session; parent::__construct($message, $code, $previous); } /** * Returns exception session. * * @return Session */ protected function getSession() { return $this->session; } /** * Prepends every line in a string with pipe (|). * * @param string $string * * @return string */ protected function pipeString($string) { return '| ' . strtr($string, array("\n" => "\n| ")); } /** * Removes response header/footer, letting only content and trim it. * * @param string $string response content * @param integer $count trim count * * @return string */ protected function trimBody($string, $count = 1000) { $string = preg_replace(array('/^.*/s', '/<\/body>.*$/s'), array('', ''), $string); $string = $this->trimString($string, $count); return $string; } /** * Trims string to specified number of chars. * * @param string $string response content * @param integer $count trim count * * @return string */ protected function trimString($string, $count = 1000) { $string = trim($string); if ($count < mb_strlen($string)) { return mb_substr($string, 0, $count - 3) . '...'; } return $string; } /** * Returns response information string. * * @return string */ protected function getResponseInfo() { $driver = basename(str_replace('\\', '/', get_class($this->session->getDriver()))); $info = '+--[ '; if (!in_array($driver, array('SahiDriver', 'SeleniumDriver', 'Selenium2Driver'))) { $info .= 'HTTP/1.1 '.$this->session->getStatusCode().' | '; } $info .= $this->session->getCurrentUrl().' | '.$driver." ]\n|\n"; return $info; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink's expectation exception. * * @author Konstantin Kudryashov */ class ExpectationException extends Exception { /** * Initializes exception. * * @param string $message optional message * @param Session $session session instance * @param \Exception $exception expectation exception */ public function __construct($message = null, Session $session, \Exception $exception = null) { parent::__construct($message ?: $exception->getMessage(), $session); } /** * Returns exception message with additional context info. * * @return string */ public function __toString() { try { $pageText = $this->trimBody($this->getSession()->getPage()->getContent()); $string = sprintf("%s\n\n%s%s", $this->getMessage(), $this->getResponseInfo(), $this->pipeString($pageText."\n") ); } catch (\Exception $e) { return $this->getMessage(); } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink response's text exception. * * @author Konstantin Kudryashov */ class ResponseTextException extends ExpectationException { /** * Returns exception message with additional context info. * * @return string */ public function __toString() { try { $pageText = $this->trimString($this->getSession()->getPage()->getText()); $string = sprintf("%s\n\n%s%s", $this->getMessage(), $this->getResponseInfo(), $this->pipeString($pageText."\n") ); } catch (\Exception $e) { return $this->getMessage(); } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink "element not found" exception. * * @author Konstantin Kudryashov */ class UnsupportedDriverActionException extends DriverException { /** * Initializes exception. * * @param string $template what is unsupported? * @param DriverInterface $driver driver instance * @param \Exception $previous previous exception */ public function __construct($template, DriverInterface $driver, \Exception $previous = null) { $message = sprintf($template, get_class($driver)); parent::__construct($message, 0, $previous); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink sessions manager. * * @author Konstantin Kudryashov */ class Mink { private $defaultSessionName; private $sessions = array(); /** * Initializes manager. * * @param array $sessions */ public function __construct(array $sessions = array()) { foreach ($sessions as $name => $session) { $this->registerSession($name, $session); } } /** * Stops all started sessions. */ public function __destruct() { $this->stopSessions(); } /** * Registers new session. * * @param string $name * @param Session $session */ public function registerSession($name, Session $session) { $name = strtolower($name); $this->sessions[$name] = $session; } /** * Checks whether session with specified name is registered. * * @param string $name * * @return Boolean */ public function hasSession($name) { return isset($this->sessions[strtolower($name)]); } /** * Sets default session name to use. * * @param string $name name of the registered session * * @throws \InvalidArgumentException */ public function setDefaultSessionName($name) { $name = strtolower($name); if (!isset($this->sessions[$name])) { throw new \InvalidArgumentException(sprintf('Session "%s" is not registered.', $name)); } $this->defaultSessionName = $name; } /** * Returns default session name or null if none. * * @return null|string */ public function getDefaultSessionName() { return $this->defaultSessionName; } /** * Returns registered session by it's name or active one. * * @param string $name session name * * @return Session * * @throws \InvalidArgumentException */ public function getSession($name = null) { $name = strtolower($name) ?: $this->defaultSessionName; if (null === $name) { throw new \InvalidArgumentException('Specify session name to get'); } if (!isset($this->sessions[$name])) { throw new \InvalidArgumentException(sprintf('Session "%s" is not registered.', $name)); } $session = $this->sessions[$name]; // start session if needed if (!$session->isStarted()) { $session->start(); } return $session; } /** * Returns session asserter. * * @param Session|string $session session object or name * * @return WebAssert */ public function assertSession($session = null) { if (!($session instanceof Session)) { $session = $this->getSession($session); } return new WebAssert($session); } /** * Resets all started sessions. */ public function resetSessions() { foreach ($this->sessions as $name => $session) { if ($session->isStarted()) { $session->reset(); } } } /** * Restarts all started sessions. */ public function restartSessions() { foreach ($this->sessions as $name => $session) { if ($session->isStarted()) { $session->restart(); } } } /** * Stops all started sessions. */ public function stopSessions() { foreach ($this->sessions as $session) { if ($session->isStarted()) { $session->stop(); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * CSS selector engine. Transforms CSS to XPath. * * @author Konstantin Kudryashov */ class CssSelector implements SelectorInterface { /** * Translates CSS into XPath. * * @param string $locator current selector locator * * @return string */ public function translateToXPath($locator) { return CSS::toXPath($locator); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Named selectors engine. Uses registered XPath selectors to create new expressions. * * @author Konstantin Kudryashov */ class NamedSelector implements SelectorInterface { private $selectors = array( 'fieldset' => << << << << << << << << << << << << <<selectors[$name] = $xpath; } /** * Translates provided locator into XPath. * * @param string|array $locator selector name or array of (selector_name, locator) * * @return string * * @throws \InvalidArgumentException */ public function translateToXPath($locator) { if (2 < count($locator)) { throw new \InvalidArgumentException('NamedSelector expects array(name, locator) as argument'); } if (2 == count($locator)) { $selector = $locator[0]; $locator = $locator[1]; } else { $selector = (string) $locator; $locator = null; } if (!isset($this->selectors[$selector])) { throw new \InvalidArgumentException(sprintf( 'Unknown named selector provided: "%s". Expected one of (%s)', $selector, implode(', ', array_keys($this->selectors)) )); } $xpath = $this->selectors[$selector]; if (null !== $locator) { $xpath = strtr($xpath, array('%locator%' => $locator)); } return $xpath; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink selector engine interface. * * @author Konstantin Kudryashov */ interface SelectorInterface { /** * Translates provided locator into XPath. * * @param string $locator current selector locator * * @return string */ public function translateToXPath($locator); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Selectors handler. * * @author Konstantin Kudryashov */ class SelectorsHandler { private $selectors; /** * Initializes selectors handler. * * @param array $selectors default selectors to register */ public function __construct(array $selectors = array()) { $this->registerSelector('named', new NamedSelector()); $this->registerSelector('css', new CssSelector()); foreach ($selectors as $name => $selector) { $this->registerSelector($name, $selector); } } /** * Registers new selector engine with specified name. * * @param string $name selector engine name * @param SelectorInterface $selector selector engine instance */ public function registerSelector($name, SelectorInterface $selector) { $this->selectors[$name] = $selector; } /** * Checks whether selector with specified name is registered on handler. * * @param string $name selector engine name * * @return Boolean */ public function isSelectorRegistered($name) { return isset($this->selectors[$name]); } /** * Returns selector engine with specified name. * * @param string $name selector engine name * * @return SelectorInterface * * @throws \InvalidArgumentException */ public function getSelector($name) { if (!$this->isSelectorRegistered($name)) { throw new \InvalidArgumentException("Selector \"$name\" is not registered."); } return $this->selectors[$name]; } /** * Translates selector with specified name to XPath. * * @param string $selector selector engine name (registered) * @param string $locator selector locator * * @return string */ public function selectorToXpath($selector, $locator) { if ('xpath' === $selector) { return $locator; } return $this->getSelector($selector)->translateToXPath($locator); } /** * Translates string to XPath literal. * * @param string $s * * @return string */ public function xpathLiteral($s) { if (false === strpos($s, "'")) { return sprintf("'%s'", $s); } if (false === strpos($s, '"')) { return sprintf('"%s"', $s); } $string = $s; $parts = array(); while (true) { if (false !== $pos = strpos($string, "'")) { $parts[] = sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { $parts[] = "'$string'"; break; } } return sprintf("concat(%s)", implode($parts, ',')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink session. * * @author Konstantin Kudryashov */ class Session { private $driver; private $page; private $selectorsHandler; /** * Initializes session. * * @param DriverInterface $driver * @param SelectorsHandler $selectorsHandler */ public function __construct(DriverInterface $driver, SelectorsHandler $selectorsHandler = null) { $driver->setSession($this); if (null === $selectorsHandler) { $selectorsHandler = new SelectorsHandler(); } $this->driver = $driver; $this->page = new DocumentElement($this); $this->selectorsHandler = $selectorsHandler; } /** * Checks whether session (driver) was started. * * @return Boolean */ public function isStarted() { return $this->driver->isStarted(); } /** * Starts session driver. */ public function start() { $this->driver->start(); } /** * Stops session driver. */ public function stop() { $this->driver->stop(); } /** * Restart session driver. */ public function restart() { $this->driver->stop(); $this->driver->start(); } /** * Reset session driver. */ public function reset() { $this->driver->reset(); } /** * Returns session driver. * * @return DriverInterface */ public function getDriver() { return $this->driver; } /** * Returns page element. * * @return DocumentElement */ public function getPage() { return $this->page; } /** * Returns selectors handler. * * @return SelectorsHandler */ public function getSelectorsHandler() { return $this->selectorsHandler; } /** * Visit specified URL. * * @param string $url url of the page */ public function visit($url) { $this->driver->visit($url); } /** * Sets HTTP Basic authentication parameters * * @param string|Boolean $user user name or false to disable authentication * @param string $password password */ public function setBasicAuth($user, $password = '') { $this->driver->setBasicAuth($user, $password); } /** * Sets specific request header. * * @param string $name * @param string $value */ public function setRequestHeader($name, $value) { $this->driver->setRequestHeader($name, $value); } /** * Returns all response headers. * * @return array */ public function getResponseHeaders() { return $this->driver->getResponseHeaders(); } /** * Sets cookie. * * @param string $name * @param string $value */ public function setCookie($name, $value = null) { $this->driver->setCookie($name, $value); } /** * Returns cookie by name. * * @param string $name * * @return string|null */ public function getCookie($name) { return $this->driver->getCookie($name); } /** * Returns response status code. * * @return integer */ public function getStatusCode() { return $this->driver->getStatusCode(); } /** * Returns current URL address. * * @return string */ public function getCurrentUrl() { return $this->driver->getCurrentUrl(); } /** * Capture a screenshot of the current window. * * @return string screenshot of MIME type image/* depending * on driver (e.g., image/png, image/jpeg) */ public function getScreenshot() { return $this->driver->getScreenshot(); } /** * Reloads current session page. */ public function reload() { $this->driver->reload(); } /** * Moves backward 1 page in history. */ public function back() { $this->driver->back(); } /** * Moves forward 1 page in history. */ public function forward() { $this->driver->forward(); } /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) */ public function switchToWindow($name = null) { $this->driver->switchToWindow($name); } /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) */ public function switchToIFrame($name = null) { $this->driver->switchToIFrame($name); } /** * Execute JS in browser. * * @param string $script javascript */ public function executeScript($script) { $this->driver->executeScript($script); } /** * Execute JS in browser and return it's response. * * @param string $script javascript * * @return string */ public function evaluateScript($script) { return $this->driver->evaluateScript($script); } /** * Waits some time or until JS condition turns true. * * @param integer $time time in milliseconds * @param string $condition JS condition */ public function wait($time, $condition = 'false') { $this->driver->wait($time, $condition); } /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) */ public function resizeWindow($width, $height, $name = null) { return $this->driver->resizeWindow($width, $height, $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mink web assertions tool. * * @author Konstantin Kudryashov */ class WebAssert { protected $session; /** * Initializes assertion engine. * * @param Session $session */ public function __construct(Session $session) { $this->session = $session; } /** * Checks that current session address is equals to provided one. * * @param string $page * * @throws ExpectationException */ public function addressEquals($page) { $expected = $this->cleanScriptnameFromPath(parse_url($page, PHP_URL_PATH)); $actual = $this->getCurrentUrlPath(); if ($actual !== $expected) { $message = sprintf('Current page is "%s", but "%s" expected.', $actual, $expected); throw new ExpectationException($message, $this->session); } } /** * Checks that current session address is not equals to provided one. * * @param string $page * * @throws ExpectationException */ public function addressNotEquals($page) { $expected = $this->cleanScriptnameFromPath(parse_url($page, PHP_URL_PATH)); $actual = $this->getCurrentUrlPath(); if ($actual === $expected) { $message = sprintf('Current page is "%s", but should not be.', $actual); throw new ExpectationException($message, $this->session); } } /** * Checks that current session address matches regex. * * @param string $regex * * @throws ExpectationException */ public function addressMatches($regex) { $actual = $this->getCurrentUrlPath(); if (!preg_match($regex, $actual)) { $message = sprintf('Current page "%s" does not match the regex "%s".', $actual, $regex); throw new ExpectationException($message, $this->session); } } /** * Checks that specified cookie exists and its value equals to a given one * * @param string $name cookie name * @param string $value cookie value * * @throws Behat\Mink\Exception\ExpectationException */ public function cookieEquals($name, $value) { $this->cookieExists($name); $actualValue = $this->session->getCookie($name); if ($actualValue != $value) { $message = sprintf('Cookie "%s" value is "%s", but should be "%s".', $name, $actualValue, $value); throw new ExpectationException($message, $this->session); } } /** * Checks that specified cookie exists * * @param string $name cookie name * * @throws Behat\Mink\Exception\ExpectationException */ public function cookieExists($name) { if ($this->session->getCookie($name) === null) { $message = sprintf('Cookie "%s" is not set, but should be.', $name); throw new ExpectationException($message, $this->session); } } /** * Checks that current response code equals to provided one. * * @param integer $code * * @throws ExpectationException */ public function statusCodeEquals($code) { $actual = $this->session->getStatusCode(); if (intval($code) !== intval($actual)) { $message = sprintf('Current response status code is %d, but %d expected.', $actual, $code); throw new ExpectationException($message, $this->session); } } /** * Checks that current response code not equals to provided one. * * @param integer $code * * @throws ExpectationException */ public function statusCodeNotEquals($code) { $actual = $this->session->getStatusCode(); if (intval($code) === intval($actual)) { $message = sprintf('Current response status code is %d, but should not be.', $actual); throw new ExpectationException($message, $this->session); } } /** * Checks that current page contains text. * * @param string $text * * @throws ResponseTextException */ public function pageTextContains($text) { $actual = $this->session->getPage()->getText(); $regex = '/'.preg_quote($text, '/').'/ui'; if (!preg_match($regex, $actual)) { $message = sprintf('The text "%s" was not found anywhere in the text of the current page.', $text); throw new ResponseTextException($message, $this->session); } } /** * Checks that current page does not contains text. * * @param string $text * * @throws ResponseTextException */ public function pageTextNotContains($text) { $actual = $this->session->getPage()->getText(); $regex = '/'.preg_quote($text, '/').'/ui'; if (preg_match($regex, $actual)) { $message = sprintf('The text "%s" appears in the text of this page, but it should not.', $text); throw new ResponseTextException($message, $this->session); } } /** * Checks that current page text matches regex. * * @param string $regex * * @throws ResponseTextException */ public function pageTextMatches($regex) { $actual = $this->session->getPage()->getText(); if (!preg_match($regex, $actual)) { $message = sprintf('The pattern %s was not found anywhere in the text of the current page.', $regex); throw new ResponseTextException($message, $this->session); } } /** * Checks that current page text does not matches regex. * * @param string $regex * * @throws ResponseTextException */ public function pageTextNotMatches($regex) { $actual = $this->session->getPage()->getText(); if (preg_match($regex, $actual)) { $message = sprintf('The pattern %s was found in the text of the current page, but it should not.', $regex); throw new ResponseTextException($message, $this->session); } } /** * Checks that page HTML (response content) contains text. * * @param string $text * * @throws ExpectationException */ public function responseContains($text) { $actual = $this->session->getPage()->getContent(); $regex = '/'.preg_quote($text, '/').'/ui'; if (!preg_match($regex, $actual)) { $message = sprintf('The string "%s" was not found anywhere in the HTML response of the current page.', $text); throw new ExpectationException($message, $this->session); } } /** * Checks that page HTML (response content) does not contains text. * * @param string $text * * @throws ExpectationException */ public function responseNotContains($text) { $actual = $this->session->getPage()->getContent(); $regex = '/'.preg_quote($text, '/').'/ui'; if (preg_match($regex, $actual)) { $message = sprintf('The string "%s" appears in the HTML response of this page, but it should not.', $text); throw new ExpectationException($message, $this->session); } } /** * Checks that page HTML (response content) matches regex. * * @param string $regex * * @throws ExpectationException */ public function responseMatches($regex) { $actual = $this->session->getPage()->getContent(); if (!preg_match($regex, $actual)) { $message = sprintf('The pattern %s was not found anywhere in the HTML response of the page.', $regex); throw new ExpectationException($message, $this->session); } } /** * Checks that page HTML (response content) does not matches regex. * * @param $regex * * @throws ExpectationException */ public function responseNotMatches($regex) { $actual = $this->session->getPage()->getContent(); if (preg_match($regex, $actual)) { $message = sprintf('The pattern %s was found in the HTML response of the page, but it should not.', $regex); throw new ExpectationException($message, $this->session); } } /** * Checks that there is specified number of specific elements on the page. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param integer $count expected count * @param Element $container document to check against * * @throws ExpectationException */ public function elementsCount($selectorType, $selector, $count, Element $container = null) { $container = $container ?: $this->session->getPage(); $nodes = $container->findAll($selectorType, $selector); if (intval($count) !== count($nodes)) { $message = sprintf('%d elements matching %s "%s" found on the page, but should be %d.', count($nodes), $selectorType, $selector, $count); throw new ExpectationException($message, $this->session); } } /** * Checks that specific element exists on the current page. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param Element $container document to check against * * @return NodeElement * * @throws ElementNotFoundException */ public function elementExists($selectorType, $selector, Element $container = null) { $container = $container ?: $this->session->getPage(); $node = $container->find($selectorType, $selector); if (null === $node) { throw new ElementNotFoundException($this->session, 'element', $selectorType, $selector); } return $node; } /** * Checks that specific element does not exists on the current page. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param Element $container document to check against * * @throws ExpectationException */ public function elementNotExists($selectorType, $selector, Element $container = null) { $container = $container ?: $this->session->getPage(); $node = $container->find($selectorType, $selector); if (null !== $node) { $message = sprintf('An element matching %s "%s" appears on this page, but it should not.', $selectorType, $selector); throw new ExpectationException($message, $this->session); } } /** * Checks that specific element contains text. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param string $text expected text * * @throws ElementTextException */ public function elementTextContains($selectorType, $selector, $text) { $element = $this->elementExists($selectorType, $selector); $actual = $element->getText(); $regex = '/'.preg_quote($text, '/').'/ui'; if (!preg_match($regex, $actual)) { $message = sprintf('The text "%s" was not found in the text of the element matching %s "%s".', $text, $selectorType, $selector); throw new ElementTextException($message, $this->session, $element); } } /** * Checks that specific element does not contains text. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param string $text expected text * * @throws ElementTextException */ public function elementTextNotContains($selectorType, $selector, $text) { $element = $this->elementExists($selectorType, $selector); $actual = $element->getText(); $regex = '/'.preg_quote($text, '/').'/ui'; if (preg_match($regex, $actual)) { $message = sprintf('The text "%s" appears in the text of the element matching %s "%s", but it should not.', $text, $selectorType, $selector); throw new ElementTextException($message, $this->session, $element); } } /** * Checks that specific element contains HTML. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param string $html expected text * * @throws ElementHtmlException */ public function elementContains($selectorType, $selector, $html) { $element = $this->elementExists($selectorType, $selector); $actual = $element->getHtml(); $regex = '/'.preg_quote($html, '/').'/ui'; if (!preg_match($regex, $actual)) { $message = sprintf('The string "%s" was not found in the HTML of the element matching %s "%s".', $html, $selectorType, $selector); throw new ElementHtmlException($message, $this->session, $element); } } /** * Checks that specific element does not contains HTML. * * @param string $selectorType element selector type (css, xpath) * @param string $selector element selector * @param string $html expected text * * @throws ElementHtmlException */ public function elementNotContains($selectorType, $selector, $html) { $element = $this->elementExists($selectorType, $selector); $actual = $element->getHtml(); $regex = '/'.preg_quote($html, '/').'/ui'; if (preg_match($regex, $actual)) { $message = sprintf('The string "%s" appears in the HTML of the element matching %s "%s", but it should not.', $html, $selectorType, $selector); throw new ElementHtmlException($message, $this->session, $element); } } /** * Checks that specific field exists on the current page. * * @param string $field field id|name|label|value * @param Element $container document to check against * * @return NodeElement * * @throws ElementNotFoundException */ public function fieldExists($field, Element $container = null) { $container = $container ?: $this->session->getPage(); $node = $container->findField($field); if (null === $node) { throw new ElementNotFoundException($this->session, 'form field', 'id|name|label|value', $field); } return $node; } /** * Checks that specific field does not exists on the current page. * * @param string $field field id|name|label|value * @param Element $container document to check against * * @throws ExpectationException */ public function fieldNotExists($field, Element $container = null) { $container = $container ?: $this->session->getPage(); $node = $container->findField($field); if (null !== $node) { $message = sprintf('A field "%s" appears on this page, but it should not.', $field); throw new ExpectationException($message, $this->session); } } /** * Checks that specific field have provided value. * * @param string $field field id|name|label|value * @param string $value field value * @param Element $container document to check against * * @throws ExpectationException */ public function fieldValueEquals($field, $value, Element $container = null) { $node = $this->fieldExists($field, $container); $actual = $node->getValue(); $regex = '/^'.preg_quote($value, '/').'/ui'; if (!preg_match($regex, $actual)) { $message = sprintf('The field "%s" value is "%s", but "%s" expected.', $field, $actual, $value); throw new ExpectationException($message, $this->session); } } /** * Checks that specific field have provided value. * * @param string $field field id|name|label|value * @param string $value field value * @param Element $container document to check against * * @throws ExpectationException */ public function fieldValueNotEquals($field, $value, Element $container = null) { $node = $this->fieldExists($field, $container); $actual = $node->getValue(); $regex = '/^'.preg_quote($value, '/').'/ui'; if (preg_match($regex, $actual)) { $message = sprintf('The field "%s" value is "%s", but it should not be.', $field, $actual); throw new ExpectationException($message, $this->session); } } /** * Checks that specific checkbox is checked. * * @param string $field field id|name|label|value * @param Element $container document to check against * * @throws ExpectationException */ public function checkboxChecked($field, Element $container = null) { $node = $this->fieldExists($field, $container); if (!$node->isChecked()) { $message = sprintf('Checkbox "%s" is not checked, but it should be.', $field); throw new ExpectationException($message, $this->session); } } /** * Checks that specific checkbox is unchecked. * * @param string $field field id|name|label|value * @param Element $container document to check against * * @throws ExpectationException */ public function checkboxNotChecked($field, Element $container = null) { $node = $this->fieldExists($field, $container); if ($node->isChecked()) { $message = sprintf('Checkbox "%s" is checked, but it should not be.', $field); throw new ExpectationException($message, $this->session); } } /** * Gets current url of the page. * * @return string */ protected function getCurrentUrlPath() { return $this->cleanScriptnameFromPath( parse_url($this->session->getCurrentUrl(), PHP_URL_PATH) ); } /** * Trims scriptname from the URL. * * @param string $path * * @return string */ protected function cleanScriptnameFromPath($path) { return preg_replace('/^\/[^\.\/]+\.php/', '', $path); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Link; use Symfony\Component\DomCrawler\Form; use Symfony\Component\Process\PhpProcess; /** * Client simulates a browser. * * To make the actual request, you need to implement the doRequest() method. * * If you want to be able to run requests in their own process (insulated flag), * you need to also implement the getScript() method. * * @author Fabien Potencier * * @api */ abstract class Client { protected $history; protected $cookieJar; protected $server; protected $request; protected $response; protected $crawler; protected $insulated; protected $redirect; protected $followRedirects; /** * Constructor. * * @param array $server The server parameters (equivalent of $_SERVER) * @param History $history A History instance to store the browser history * @param CookieJar $cookieJar A CookieJar instance to store the cookies * * @api */ public function __construct(array $server = array(), History $history = null, CookieJar $cookieJar = null) { $this->setServerParameters($server); $this->history = null === $history ? new History() : $history; $this->cookieJar = null === $cookieJar ? new CookieJar() : $cookieJar; $this->insulated = false; $this->followRedirects = true; } /** * Sets whether to automatically follow redirects or not. * * @param Boolean $followRedirect Whether to follow redirects * * @api */ public function followRedirects($followRedirect = true) { $this->followRedirects = (Boolean) $followRedirect; } /** * Sets the insulated flag. * * @param Boolean $insulated Whether to insulate the requests or not * * @throws \RuntimeException When Symfony Process Component is not installed * * @api */ public function insulate($insulated = true) { if ($insulated && !class_exists('Symfony\\Component\\Process\\Process')) { // @codeCoverageIgnoreStart throw new \RuntimeException('Unable to isolate requests as the Symfony Process Component is not installed.'); // @codeCoverageIgnoreEnd } $this->insulated = (Boolean) $insulated; } /** * Sets server parameters. * * @param array $server An array of server parameters * * @api */ public function setServerParameters(array $server) { $this->server = array_merge(array( 'HTTP_HOST' => 'localhost', 'HTTP_USER_AGENT' => 'Symfony2 BrowserKit', ), $server); } /** * Sets single server parameter. * * @param string $key A key of the parameter * @param string $value A value of the parameter */ public function setServerParameter($key, $value) { $this->server[$key] = $value; } /** * Gets single server parameter for specified key. * * @param string $key A key of the parameter to get * @param string $default A default value when key is undefined * * @return string A value of the parameter */ public function getServerParameter($key, $default = '') { return (isset($this->server[$key])) ? $this->server[$key] : $default; } /** * Returns the History instance. * * @return History A History instance * * @api */ public function getHistory() { return $this->history; } /** * Returns the CookieJar instance. * * @return CookieJar A CookieJar instance * * @api */ public function getCookieJar() { return $this->cookieJar; } /** * Returns the current Crawler instance. * * @return Crawler A Crawler instance * * @api */ public function getCrawler() { return $this->crawler; } /** * Returns the current Response instance. * * @return Response A Response instance * * @api */ public function getResponse() { return $this->response; } /** * Returns the current Request instance. * * @return Request A Request instance * * @api */ public function getRequest() { return $this->request; } /** * Clicks on a given link. * * @param Link $link A Link instance * * @return Crawler * * @api */ public function click(Link $link) { if ($link instanceof Form) { return $this->submit($link); } return $this->request($link->getMethod(), $link->getUri()); } /** * Submits a form. * * @param Form $form A Form instance * @param array $values An array of form field values * * @return Crawler * * @api */ public function submit(Form $form, array $values = array()) { $form->setValues($values); return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles()); } /** * Calls a URI. * * @param string $method The request method * @param string $uri The URI to fetch * @param array $parameters The Request parameters * @param array $files The files * @param array $server The server parameters (HTTP headers are referenced with a HTTP_ prefix as PHP does) * @param string $content The raw body data * @param Boolean $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload()) * * @return Crawler * * @api */ public function request($method, $uri, array $parameters = array(), array $files = array(), array $server = array(), $content = null, $changeHistory = true) { $uri = $this->getAbsoluteUri($uri); $server = array_merge($this->server, $server); if (!$this->history->isEmpty()) { $server['HTTP_REFERER'] = $this->history->current()->getUri(); } $server['HTTP_HOST'] = parse_url($uri, PHP_URL_HOST); $server['HTTPS'] = 'https' == parse_url($uri, PHP_URL_SCHEME); $request = new Request($uri, $method, $parameters, $files, $this->cookieJar->allValues($uri), $server, $content); $this->request = $this->filterRequest($request); if (true === $changeHistory) { $this->history->add($request); } if ($this->insulated) { $this->response = $this->doRequestInProcess($this->request); } else { $this->response = $this->doRequest($this->request); } $response = $this->filterResponse($this->response); $this->cookieJar->updateFromResponse($response); $this->redirect = $response->getHeader('Location'); if ($this->followRedirects && $this->redirect) { return $this->crawler = $this->followRedirect(); } return $this->crawler = $this->createCrawlerFromContent($request->getUri(), $response->getContent(), $response->getHeader('Content-Type')); } /** * Makes a request in another process. * * @param Request $request A Request instance * * @return Response A Response instance * * @throws \RuntimeException When processing returns exit code */ protected function doRequestInProcess($request) { // We set the TMPDIR (for Macs) and TEMP (for Windows), because on these platforms the temp directory changes based on the user. $process = new PhpProcess($this->getScript($request), null, array('TMPDIR' => sys_get_temp_dir(), 'TEMP' => sys_get_temp_dir())); $process->run(); if (!$process->isSuccessful() || !preg_match('/^O\:\d+\:/', $process->getOutput())) { throw new \RuntimeException('OUTPUT: '.$process->getOutput().' ERROR OUTPUT: '.$process->getErrorOutput()); } return unserialize($process->getOutput()); } /** * Makes a request. * * @param Request $request A Request instance * * @return Response A Response instance */ abstract protected function doRequest($request); /** * Returns the script to execute when the request must be insulated. * * @param Request $request A Request instance * * @throws \LogicException When this abstract class is not implemented */ protected function getScript($request) { // @codeCoverageIgnoreStart throw new \LogicException('To insulate requests, you need to override the getScript() method.'); // @codeCoverageIgnoreEnd } /** * Filters the request. * * @param Request $request The request to filter * * @return Request */ protected function filterRequest(Request $request) { return $request; } /** * Filters the Response. * * @param Response $response The Response to filter * * @return Response */ protected function filterResponse($response) { return $response; } /** * Creates a crawler. * * This method returns null if the DomCrawler component is not available. * * @param string $uri A uri * @param string $content Content for the crawler to use * @param string $type Content type * * @return Crawler|null */ protected function createCrawlerFromContent($uri, $content, $type) { if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { return null; } $crawler = new Crawler(null, $uri); $crawler->addContent($content, $type); return $crawler; } /** * Goes back in the browser history. * * @return Crawler * * @api */ public function back() { return $this->requestFromRequest($this->history->back(), false); } /** * Goes forward in the browser history. * * @return Crawler * * @api */ public function forward() { return $this->requestFromRequest($this->history->forward(), false); } /** * Reloads the current browser. * * @return Crawler * * @api */ public function reload() { return $this->requestFromRequest($this->history->current(), false); } /** * Follow redirects? * * @return Crawler * * @throws \LogicException If request was not a redirect * * @api */ public function followRedirect() { if (empty($this->redirect)) { throw new \LogicException('The request was not redirected.'); } return $this->request('get', $this->redirect); } /** * Restarts the client. * * It flushes history and all cookies. * * @api */ public function restart() { $this->cookieJar->clear(); $this->history->clear(); } /** * Takes a URI and converts it to absolute if it is not already absolute. * * @param string $uri A uri * * @return string An absolute uri */ protected function getAbsoluteUri($uri) { // already absolute? if (0 === strpos($uri, 'http')) { return $uri; } if (!$this->history->isEmpty()) { $currentUri = $this->history->current()->getUri(); } else { $currentUri = sprintf('http%s://%s/', isset($this->server['HTTPS']) ? 's' : '', isset($this->server['HTTP_HOST']) ? $this->server['HTTP_HOST'] : 'localhost' ); } // anchor? if (!$uri || '#' == $uri[0]) { return preg_replace('/#.*?$/', '', $currentUri).$uri; } if ('/' !== $uri[0]) { $path = parse_url($currentUri, PHP_URL_PATH); if ('/' !== substr($path, -1)) { $path = substr($path, 0, strrpos($path, '/') + 1); } $uri = $path.$uri; } return preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $currentUri).$uri; } /** * Makes a request from a Request object directly. * * @param Request $request A Request instance * @param Boolean $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload()) * * @return Crawler */ protected function requestFromRequest(Request $request, $changeHistory = true) { return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent(), $changeHistory); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit; /** * Cookie represents an HTTP cookie. * * @author Fabien Potencier * * @api */ class Cookie { /** * Handles dates as defined by RFC 2616 section 3.3.1, and also some other * non-standard, but common formats. * * @var array */ private static $dateFormats = array( 'D, d M Y H:i:s T', 'D, d-M-y H:i:s T', 'D, d-M-Y H:i:s T', 'D, d-m-y H:i:s T', 'D, d-m-Y H:i:s T', 'D M j G:i:s Y', 'D M d H:i:s Y T', ); protected $name; protected $value; protected $expires; protected $path; protected $domain; protected $secure; protected $httponly; protected $rawValue; /** * Sets a cookie. * * @param string $name The cookie name * @param string $value The value of the cookie * @param string $expires The time the cookie expires * @param string $path The path on the server in which the cookie will be available on * @param string $domain The domain that the cookie is available * @param Boolean $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client * @param Boolean $httponly The cookie httponly flag * @param Boolean $encodedValue Whether the value is encoded or not * * @api */ public function __construct($name, $value, $expires = null, $path = null, $domain = '', $secure = false, $httponly = true, $encodedValue = false) { if ($encodedValue) { $this->value = urldecode($value); $this->rawValue = $value; } else { $this->value = $value; $this->rawValue = urlencode($value); } $this->name = $name; $this->expires = null === $expires ? null : (integer) $expires; $this->path = empty($path) ? '/' : $path; $this->domain = $domain; $this->secure = (Boolean) $secure; $this->httponly = (Boolean) $httponly; } /** * Returns the HTTP representation of the Cookie. * * @return string The HTTP representation of the Cookie * * @api */ public function __toString() { $cookie = sprintf('%s=%s', $this->name, $this->rawValue); if (null !== $this->expires) { $cookie .= '; expires='.substr(\DateTime::createFromFormat('U', $this->expires, new \DateTimeZone('GMT'))->format(self::$dateFormats[0]), 0, -5); } if ('' !== $this->domain) { $cookie .= '; domain='.$this->domain; } if ('/' !== $this->path) { $cookie .= '; path='.$this->path; } if ($this->secure) { $cookie .= '; secure'; } if ($this->httponly) { $cookie .= '; httponly'; } return $cookie; } /** * Creates a Cookie instance from a Set-Cookie header value. * * @param string $cookie A Set-Cookie header value * @param string $url The base URL * * @return Cookie A Cookie instance * * @throws \InvalidArgumentException * * @api */ public static function fromString($cookie, $url = null) { $parts = explode(';', $cookie); if (false === strpos($parts[0], '=')) { throw new \InvalidArgumentException('The cookie string "%s" is not valid.'); } list($name, $value) = explode('=', array_shift($parts), 2); $values = array( 'name' => trim($name), 'value' => trim($value), 'expires' => null, 'path' => '/', 'domain' => '', 'secure' => false, 'httponly' => false, 'passedRawValue' => true, ); if (null !== $url) { if ((false === $urlParts = parse_url($url)) || !isset($urlParts['host']) || !isset($urlParts['path'])) { throw new \InvalidArgumentException(sprintf('The URL "%s" is not valid.', $url)); } $parts = array_merge($urlParts, $parts); $values['domain'] = $parts['host']; $values['path'] = substr($parts['path'], 0, strrpos($parts['path'], '/')); } foreach ($parts as $part) { $part = trim($part); if ('secure' === strtolower($part)) { // Ignore the secure flag if the original URI is not given or is not HTTPS if (!$url || !isset($urlParts['scheme']) || 'https' != $urlParts['scheme']) { continue; } $values['secure'] = true; continue; } if ('httponly' === strtolower($part)) { $values['httponly'] = true; continue; } if (2 === count($elements = explode('=', $part, 2))) { if ('expires' === strtolower($elements[0])) { $elements[1] = self::parseDate($elements[1]); } $values[strtolower($elements[0])] = $elements[1]; } } return new static( $values['name'], $values['value'], $values['expires'], $values['path'], $values['domain'], $values['secure'], $values['httponly'], $values['passedRawValue'] ); } private static function parseDate($dateValue) { // trim single quotes around date if present if (($length = strlen($dateValue)) > 1 && "'" === $dateValue[0] && "'" === $dateValue[$length-1]) { $dateValue = substr($dateValue, 1, -1); } foreach (self::$dateFormats as $dateFormat) { if (false !== $date = \DateTime::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) { return $date->getTimestamp(); } } // attempt a fallback for unusual formatting if (false !== $date = date_create($dateValue, new \DateTimeZone('GMT'))) { return $date->getTimestamp(); } throw new \InvalidArgumentException(sprintf('Could not parse date "%s".', $dateValue)); } /** * Gets the name of the cookie. * * @return string The cookie name * * @api */ public function getName() { return $this->name; } /** * Gets the value of the cookie. * * @return string The cookie value * * @api */ public function getValue() { return $this->value; } /** * Gets the raw value of the cookie. * * @return string The cookie value * * @api */ public function getRawValue() { return $this->rawValue; } /** * Gets the expires time of the cookie. * * @return string The cookie expires time * * @api */ public function getExpiresTime() { return $this->expires; } /** * Gets the path of the cookie. * * @return string The cookie path * * @api */ public function getPath() { return $this->path; } /** * Gets the domain of the cookie. * * @return string The cookie domain * * @api */ public function getDomain() { return $this->domain; } /** * Returns the secure flag of the cookie. * * @return Boolean The cookie secure flag * * @api */ public function isSecure() { return $this->secure; } /** * Returns the httponly flag of the cookie. * * @return Boolean The cookie httponly flag * * @api */ public function isHttpOnly() { return $this->httponly; } /** * Returns true if the cookie has expired. * * @return Boolean true if the cookie has expired, false otherwise * * @api */ public function isExpired() { return null !== $this->expires && 0 !== $this->expires && $this->expires < time(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit; /** * CookieJar. * * @author Fabien Potencier * * @api */ class CookieJar { protected $cookieJar = array(); /** * Sets a cookie. * * @param Cookie $cookie A Cookie instance * * @api */ public function set(Cookie $cookie) { $this->cookieJar[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; } /** * Gets a cookie by name. * * @param string $name The cookie name * @param string $path The cookie path * @param string $domain The cookie domain * * @return Cookie|null A Cookie instance or null if the cookie does not exist * * @api */ public function get($name, $path = '/', $domain = null) { $this->flushExpiredCookies(); return isset($this->cookieJar[$domain][$path][$name]) ? $this->cookieJar[$domain][$path][$name] : null; } /** * Removes a cookie by name. * * @param string $name The cookie name * @param string $path The cookie path * @param string $domain The cookie domain * * @api */ public function expire($name, $path = '/', $domain = null) { if (null === $path) { $path = '/'; } unset($this->cookieJar[$domain][$path][$name]); if (empty($this->cookieJar[$domain][$path])) { unset($this->cookieJar[$domain][$path]); if (empty($this->cookieJar[$domain])) { unset($this->cookieJar[$domain]); } } } /** * Removes all the cookies from the jar. * * @api */ public function clear() { $this->cookieJar = array(); } /** * Updates the cookie jar from a response Set-Cookie headers. * * @param array $setCookies Set-Cookie headers from an HTTP response * @param string $uri The base URL */ public function updateFromSetCookie(array $setCookies, $uri = null) { $cookies = array(); foreach ($setCookies as $cookie) { foreach (explode(',', $cookie) as $i => $part) { if (0 === $i || preg_match('/^(?P\s*[0-9A-Za-z!#\$%\&\'\*\+\-\.^_`\|~]+)=/', $part)) { $cookies[] = ltrim($part); } else { $cookies[count($cookies) - 1] .= ','.$part; } } } foreach ($cookies as $cookie) { $this->set(Cookie::fromString($cookie, $uri)); } } /** * Updates the cookie jar from a Response object. * * @param Response $response A Response object * @param string $uri The base URL */ public function updateFromResponse(Response $response, $uri = null) { $this->updateFromSetCookie($response->getHeader('Set-Cookie', false), $uri); } /** * Returns not yet expired cookies. * * @return Cookie[] An array of cookies */ public function all() { $this->flushExpiredCookies(); $flattenedCookies = array(); foreach ($this->cookieJar as $path) { foreach ($path as $cookies) { foreach ($cookies as $cookie) { $flattenedCookies[] = $cookie; } } } return $flattenedCookies; } /** * Returns not yet expired cookie values for the given URI. * * @param string $uri A URI * @param Boolean $returnsRawValue Returns raw value or urldecoded value * * @return array An array of cookie values */ public function allValues($uri, $returnsRawValue = false) { $this->flushExpiredCookies(); $parts = array_replace(array('path' => '/'), parse_url($uri)); $cookies = array(); foreach ($this->cookieJar as $domain => $pathCookies) { if ($domain) { $domain = ltrim($domain, '.'); if ($domain != substr($parts['host'], -strlen($domain))) { continue; } } foreach ($pathCookies as $path => $namedCookies) { if ($path != substr($parts['path'], 0, strlen($path))) { continue; } foreach ($namedCookies as $cookie) { if ($cookie->isSecure() && 'https' != $parts['scheme']) { continue; } $cookies[$cookie->getName()] = $returnsRawValue ? $cookie->getRawValue() : $cookie->getValue(); } } } return $cookies; } /** * Returns not yet expired raw cookie values for the given URI. * * @param string $uri A URI * * @return array An array of cookie values */ public function allRawValues($uri) { return $this->allValues($uri, true); } /** * Removes all expired cookies. */ public function flushExpiredCookies() { foreach ($this->cookieJar as $domain => $pathCookies) { foreach ($pathCookies as $path => $namedCookies) { foreach ($namedCookies as $name => $cookie) { if ($cookie->isExpired()) { unset($this->cookieJar[$domain][$path][$name]); } } } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit; /** * History. * * @author Fabien Potencier */ class History { protected $stack = array(); protected $position = -1; /** * Constructor. */ public function __construct() { $this->clear(); } /** * Clears the history. */ public function clear() { $this->stack = array(); $this->position = -1; } /** * Adds a Request to the history. * * @param Request $request A Request instance */ public function add(Request $request) { $this->stack = array_slice($this->stack, 0, $this->position + 1); $this->stack[] = clone $request; $this->position = count($this->stack) - 1; } /** * Returns true if the history is empty. * * @return Boolean true if the history is empty, false otherwise */ public function isEmpty() { return count($this->stack) == 0; } /** * Goes back in the history. * * @return Request A Request instance * * @throws \LogicException if the stack is already on the first page */ public function back() { if ($this->position < 1) { throw new \LogicException('You are already on the first page.'); } return clone $this->stack[--$this->position]; } /** * Goes forward in the history. * * @return Request A Request instance * * @throws \LogicException if the stack is already on the last page */ public function forward() { if ($this->position > count($this->stack) - 2) { throw new \LogicException('You are already on the last page.'); } return clone $this->stack[++$this->position]; } /** * Returns the current element in the history. * * @return Request A Request instance * * @throws \LogicException if the stack is empty */ public function current() { if (-1 == $this->position) { throw new \LogicException('The page history is empty.'); } return clone $this->stack[$this->position]; } } Copyright (c) 2004-2013 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit; /** * Request object. * * @author Fabien Potencier * * @api */ class Request { protected $uri; protected $method; protected $parameters; protected $files; protected $cookies; protected $server; protected $content; /** * Constructor. * * @param string $uri The request URI * @param string $method The HTTP method request * @param array $parameters The request parameters * @param array $files An array of uploaded files * @param array $cookies An array of cookies * @param array $server An array of server parameters * @param string $content The raw body data * * @api */ public function __construct($uri, $method, array $parameters = array(), array $files = array(), array $cookies = array(), array $server = array(), $content = null) { $this->uri = $uri; $this->method = $method; $this->parameters = $parameters; $this->files = $files; $this->cookies = $cookies; $this->server = $server; $this->content = $content; } /** * Gets the request URI. * * @return string The request URI * * @api */ public function getUri() { return $this->uri; } /** * Gets the request HTTP method. * * @return string The request HTTP method * * @api */ public function getMethod() { return $this->method; } /** * Gets the request parameters. * * @return array The request parameters * * @api */ public function getParameters() { return $this->parameters; } /** * Gets the request server files. * * @return array The request files * * @api */ public function getFiles() { return $this->files; } /** * Gets the request cookies. * * @return array The request cookies * * @api */ public function getCookies() { return $this->cookies; } /** * Gets the request server parameters. * * @return array The request server parameters * * @api */ public function getServer() { return $this->server; } /** * Gets the request raw body data. * * @return string The request raw body data. * * @api */ public function getContent() { return $this->content; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit; /** * Response object. * * @author Fabien Potencier * * @api */ class Response { protected $content; protected $status; protected $headers; /** * Constructor. * * The headers array is a set of key/value pairs. If a header is present multiple times * then the value is an array of all the values. * * @param string $content The content of the response * @param integer $status The response status code * @param array $headers An array of headers * * @api */ public function __construct($content = '', $status = 200, array $headers = array()) { $this->content = $content; $this->status = $status; $this->headers = $headers; } /** * Converts the response object to string containing all headers and the response content. * * @return string The response with headers and content */ public function __toString() { $headers = ''; foreach ($this->headers as $name => $value) { if (is_string($value)) { $headers .= $this->buildHeader($name, $value); } else { foreach ($value as $headerValue) { $headers .= $this->buildHeader($name, $headerValue); } } } return $headers."\n".$this->content; } /** * Returns the build header line. * * @param string $name The header name * @param string $value The header value * * @return string The built header line */ protected function buildHeader($name, $value) { return sprintf("%s: %s\n", $name, $value); } /** * Gets the response content. * * @return string The response content * * @api */ public function getContent() { return $this->content; } /** * Gets the response status code. * * @return integer The response status code * * @api */ public function getStatus() { return $this->status; } /** * Gets the response headers. * * @return array The response headers * * @api */ public function getHeaders() { return $this->headers; } /** * Gets a response header. * * @param string $header The header name * @param Boolean $first Whether to return the first value or all header values * * @return string|array The first header value if $first is true, an array of values otherwise */ public function getHeader($header, $first = true) { foreach ($this->headers as $key => $value) { if (str_replace('-', '_', strtolower($key)) == str_replace('-', '_', strtolower($header))) { if ($first) { return is_array($value) ? (count($value) ? $value[0] : '') : $value; } return is_array($value) ? $value : array($value); } } return $first ? null : array(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit\Tests; use Symfony\Component\BrowserKit\Client; use Symfony\Component\BrowserKit\History; use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\Request; use Symfony\Component\BrowserKit\Response; class TestClient extends Client { protected $nextResponse = null; protected $nextScript = null; public function setNextResponse(Response $response) { $this->nextResponse = $response; } public function setNextScript($script) { $this->nextScript = $script; } protected function doRequest($request) { if (null === $this->nextResponse) { return new Response(); } $response = $this->nextResponse; $this->nextResponse = null; return $response; } protected function getScript($request) { $r = new \ReflectionClass('Symfony\Component\BrowserKit\Response'); $path = $r->getFileName(); return <<nextScript); EOF; } } class ClientTest extends \PHPUnit_Framework_TestCase { /** * @covers Symfony\Component\BrowserKit\Client::getHistory */ public function testGetHistory() { $client = new TestClient(array(), $history = new History()); $this->assertSame($history, $client->getHistory(), '->getHistory() returns the History'); } /** * @covers Symfony\Component\BrowserKit\Client::getCookieJar */ public function testGetCookieJar() { $client = new TestClient(array(), null, $cookieJar = new CookieJar()); $this->assertSame($cookieJar, $client->getCookieJar(), '->getCookieJar() returns the CookieJar'); } /** * @covers Symfony\Component\BrowserKit\Client::getRequest */ public function testGetRequest() { $client = new TestClient(); $client->request('GET', 'http://example.com/'); $this->assertEquals('http://example.com/', $client->getRequest()->getUri(), '->getCrawler() returns the Request of the last request'); } /** * @covers Symfony\Component\BrowserKit\Client::getResponse */ public function testGetResponse() { $client = new TestClient(); $client->setNextResponse(new Response('foo')); $client->request('GET', 'http://example.com/'); $this->assertEquals('foo', $client->getResponse()->getContent(), '->getCrawler() returns the Response of the last request'); } public function testGetContent() { $json = '{"jsonrpc":"2.0","method":"echo","id":7,"params":["Hello World"]}'; $client = new TestClient(); $client->request('POST', 'http://example.com/jsonrpc', array(), array(), array(), $json); $this->assertEquals($json, $client->getRequest()->getContent()); } /** * @covers Symfony\Component\BrowserKit\Client::getCrawler */ public function testGetCrawler() { $client = new TestClient(); $client->setNextResponse(new Response('foo')); $crawler = $client->request('GET', 'http://example.com/'); $this->assertSame($crawler, $client->getCrawler(), '->getCrawler() returns the Crawler of the last request'); } public function testRequestHttpHeaders() { $client = new TestClient(); $client->request('GET', '/'); $headers = $client->getRequest()->getServer(); $this->assertEquals('localhost', $headers['HTTP_HOST'], '->request() sets the HTTP_HOST header'); $client = new TestClient(); $client->request('GET', 'http://www.example.com'); $headers = $client->getRequest()->getServer(); $this->assertEquals('www.example.com', $headers['HTTP_HOST'], '->request() sets the HTTP_HOST header'); $client->request('GET', 'https://www.example.com'); $headers = $client->getRequest()->getServer(); $this->assertTrue($headers['HTTPS'], '->request() sets the HTTPS header'); } public function testRequestURIConversion() { $client = new TestClient(); $client->request('GET', '/foo'); $this->assertEquals('http://localhost/foo', $client->getRequest()->getUri(), '->request() converts the URI to an absolute one'); $client = new TestClient(); $client->request('GET', 'http://www.example.com'); $this->assertEquals('http://www.example.com', $client->getRequest()->getUri(), '->request() does not change absolute URIs'); $client = new TestClient(); $client->request('GET', 'http://www.example.com/'); $client->request('GET', '/foo'); $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->request() uses the previous request for relative URLs'); $client = new TestClient(); $client->request('GET', 'http://www.example.com/foo'); $client->request('GET', '#'); $this->assertEquals('http://www.example.com/foo#', $client->getRequest()->getUri(), '->request() uses the previous request for #'); $client->request('GET', '#'); $this->assertEquals('http://www.example.com/foo#', $client->getRequest()->getUri(), '->request() uses the previous request for #'); $client->request('GET', '#foo'); $this->assertEquals('http://www.example.com/foo#foo', $client->getRequest()->getUri(), '->request() uses the previous request for #'); $client = new TestClient(); $client->request('GET', 'http://www.example.com/foo/'); $client->request('GET', 'bar'); $this->assertEquals('http://www.example.com/foo/bar', $client->getRequest()->getUri(), '->request() uses the previous request for relative URLs'); $client = new TestClient(); $client->request('GET', 'http://www.example.com/foo/foobar'); $client->request('GET', 'bar'); $this->assertEquals('http://www.example.com/foo/bar', $client->getRequest()->getUri(), '->request() uses the previous request for relative URLs'); } public function testRequestReferer() { $client = new TestClient(); $client->request('GET', 'http://www.example.com/foo/foobar'); $client->request('GET', 'bar'); $server = $client->getRequest()->getServer(); $this->assertEquals('http://www.example.com/foo/foobar', $server['HTTP_REFERER'], '->request() sets the referer'); } public function testRequestHistory() { $client = new TestClient(); $client->request('GET', 'http://www.example.com/foo/foobar'); $client->request('GET', 'bar'); $this->assertEquals('http://www.example.com/foo/bar', $client->getHistory()->current()->getUri(), '->request() updates the History'); $this->assertEquals('http://www.example.com/foo/foobar', $client->getHistory()->back()->getUri(), '->request() updates the History'); } public function testRequestCookies() { $client = new TestClient(); $client->setNextResponse(new Response('foo', 200, array('Set-Cookie' => 'foo=bar'))); $client->request('GET', 'http://www.example.com/foo/foobar'); $this->assertEquals(array('foo' => 'bar'), $client->getCookieJar()->allValues('http://www.example.com/foo/foobar'), '->request() updates the CookieJar'); $client->request('GET', 'bar'); $this->assertEquals(array('foo' => 'bar'), $client->getCookieJar()->allValues('http://www.example.com/foo/foobar'), '->request() updates the CookieJar'); } public function testClick() { if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { $this->markTestSkipped('The "DomCrawler" component is not available'); } if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { $this->markTestSkipped('The "CssSelector" component is not available'); } $client = new TestClient(); $client->setNextResponse(new Response('foo')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); $client->click($crawler->filter('a')->link()); $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->click() clicks on links'); } public function testClickForm() { if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { $this->markTestSkipped('The "DomCrawler" component is not available'); } if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { $this->markTestSkipped('The "CssSelector" component is not available'); } $client = new TestClient(); $client->setNextResponse(new Response('
')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); $client->click($crawler->filter('input')->form()); $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->click() Form submit forms'); } public function testSubmit() { if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { $this->markTestSkipped('The "DomCrawler" component is not available'); } if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { $this->markTestSkipped('The "CssSelector" component is not available'); } $client = new TestClient(); $client->setNextResponse(new Response('
')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); $client->submit($crawler->filter('input')->form()); $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->submit() submit forms'); } public function testSubmitPreserveAuth() { if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { $this->markTestSkipped('The "DomCrawler" component is not available'); } if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { $this->markTestSkipped('The "CssSelector" component is not available'); } $client = new TestClient(array('PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'bar')); $client->setNextResponse(new Response('
')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); $server = $client->getRequest()->getServer(); $this->assertArrayHasKey('PHP_AUTH_USER', $server); $this->assertEquals('foo', $server['PHP_AUTH_USER']); $this->assertArrayHasKey('PHP_AUTH_PW', $server); $this->assertEquals('bar', $server['PHP_AUTH_PW']); $client->submit($crawler->filter('input')->form()); $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->submit() submit forms'); $server = $client->getRequest()->getServer(); $this->assertArrayHasKey('PHP_AUTH_USER', $server); $this->assertEquals('foo', $server['PHP_AUTH_USER']); $this->assertArrayHasKey('PHP_AUTH_PW', $server); $this->assertEquals('bar', $server['PHP_AUTH_PW']); } public function testFollowRedirect() { $client = new TestClient(); $client->followRedirects(false); $client->request('GET', 'http://www.example.com/foo/foobar'); try { $client->followRedirect(); $this->fail('->followRedirect() throws a \LogicException if the request was not redirected'); } catch (\Exception $e) { $this->assertInstanceof('LogicException', $e, '->followRedirect() throws a \LogicException if the request was not redirected'); } $client->setNextResponse(new Response('', 302, array('Location' => 'http://www.example.com/redirected'))); $client->request('GET', 'http://www.example.com/foo/foobar'); $client->followRedirect(); $this->assertEquals('http://www.example.com/redirected', $client->getRequest()->getUri(), '->followRedirect() follows a redirect if any'); $client = new TestClient(); $client->setNextResponse(new Response('', 302, array('Location' => 'http://www.example.com/redirected'))); $client->request('GET', 'http://www.example.com/foo/foobar'); $this->assertEquals('http://www.example.com/redirected', $client->getRequest()->getUri(), '->followRedirect() automatically follows redirects if followRedirects is true'); } public function testFollowRedirectWithCookies() { $client = new TestClient(); $client->followRedirects(false); $client->setNextResponse(new Response('', 302, array( 'Location' => 'http://www.example.com/redirected', 'Set-Cookie' => 'foo=bar', ))); $client->request('GET', 'http://www.example.com/'); $this->assertEquals(array(), $client->getRequest()->getCookies()); $client->followRedirect(); $this->assertEquals(array('foo' => 'bar'), $client->getRequest()->getCookies()); } public function testBack() { $client = new TestClient(); $parameters = array('foo' => 'bar'); $files = array('myfile.foo' => 'baz'); $server = array('X_TEST_FOO' => 'bazbar'); $content = 'foobarbaz'; $client->request('GET', 'http://www.example.com/foo/foobar', $parameters, $files, $server, $content); $client->request('GET', 'http://www.example.com/foo'); $client->back(); $this->assertEquals('http://www.example.com/foo/foobar', $client->getRequest()->getUri(), '->back() goes back in the history'); $this->assertArrayHasKey('foo', $client->getRequest()->getParameters(), '->back() keeps parameters'); $this->assertArrayHasKey('myfile.foo', $client->getRequest()->getFiles(), '->back() keeps files'); $this->assertArrayHasKey('X_TEST_FOO', $client->getRequest()->getServer(), '->back() keeps $_SERVER'); $this->assertEquals($content, $client->getRequest()->getContent(), '->back() keeps content'); } public function testForward() { $client = new TestClient(); $parameters = array('foo' => 'bar'); $files = array('myfile.foo' => 'baz'); $server = array('X_TEST_FOO' => 'bazbar'); $content = 'foobarbaz'; $client->request('GET', 'http://www.example.com/foo/foobar'); $client->request('GET', 'http://www.example.com/foo', $parameters, $files, $server, $content); $client->back(); $client->forward(); $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->forward() goes forward in the history'); $this->assertArrayHasKey('foo', $client->getRequest()->getParameters(), '->forward() keeps parameters'); $this->assertArrayHasKey('myfile.foo', $client->getRequest()->getFiles(), '->forward() keeps files'); $this->assertArrayHasKey('X_TEST_FOO', $client->getRequest()->getServer(), '->forward() keeps $_SERVER'); $this->assertEquals($content, $client->getRequest()->getContent(), '->forward() keeps content'); } public function testReload() { $client = new TestClient(); $parameters = array('foo' => 'bar'); $files = array('myfile.foo' => 'baz'); $server = array('X_TEST_FOO' => 'bazbar'); $content = 'foobarbaz'; $client->request('GET', 'http://www.example.com/foo/foobar', $parameters, $files, $server, $content); $client->reload(); $this->assertEquals('http://www.example.com/foo/foobar', $client->getRequest()->getUri(), '->reload() reloads the current page'); $this->assertArrayHasKey('foo', $client->getRequest()->getParameters(), '->reload() keeps parameters'); $this->assertArrayHasKey('myfile.foo', $client->getRequest()->getFiles(), '->reload() keeps files'); $this->assertArrayHasKey('X_TEST_FOO', $client->getRequest()->getServer(), '->reload() keeps $_SERVER'); $this->assertEquals($content, $client->getRequest()->getContent(), '->reload() keeps content'); } public function testRestart() { $client = new TestClient(); $client->request('GET', 'http://www.example.com/foo/foobar'); $client->restart(); $this->assertTrue($client->getHistory()->isEmpty(), '->restart() clears the history'); $this->assertEquals(array(), $client->getCookieJar()->all(), '->restart() clears the cookies'); } public function testInsulatedRequests() { if (!class_exists('Symfony\Component\Process\Process')) { $this->markTestSkipped('The "Process" component is not available'); } $client = new TestClient(); $client->insulate(); $client->setNextScript("new Symfony\Component\BrowserKit\Response('foobar')"); $client->request('GET', 'http://www.example.com/foo/foobar'); $this->assertEquals('foobar', $client->getResponse()->getContent(), '->insulate() process the request in a forked process'); $client->setNextScript("new Symfony\Component\BrowserKit\Response('foobar)"); try { $client->request('GET', 'http://www.example.com/foo/foobar'); $this->fail('->request() throws a \RuntimeException if the script has an error'); } catch (\Exception $e) { $this->assertInstanceof('RuntimeException', $e, '->request() throws a \RuntimeException if the script has an error'); } } public function testGetServerParameter() { $client = new TestClient(); $this->assertEquals('localhost', $client->getServerParameter('HTTP_HOST')); $this->assertEquals('Symfony2 BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); $this->assertEquals('testvalue', $client->getServerParameter('testkey', 'testvalue')); } public function testSetServerParameter() { $client = new TestClient(); $this->assertEquals('localhost', $client->getServerParameter('HTTP_HOST')); $this->assertEquals('Symfony2 BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); $client->setServerParameter('HTTP_HOST', 'testhost'); $this->assertEquals('testhost', $client->getServerParameter('HTTP_HOST')); $client->setServerParameter('HTTP_USER_AGENT', 'testua'); $this->assertEquals('testua', $client->getServerParameter('HTTP_USER_AGENT')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit\Tests; use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\Response; class CookieJarTest extends \PHPUnit_Framework_TestCase { public function testSetGet() { $cookieJar = new CookieJar(); $cookieJar->set($cookie = new Cookie('foo', 'bar')); $this->assertEquals($cookie, $cookieJar->get('foo'), '->set() sets a cookie'); $this->assertNull($cookieJar->get('foobar'), '->get() returns null if the cookie does not exist'); $cookieJar->set($cookie = new Cookie('foo', 'bar', time() - 86400)); $this->assertNull($cookieJar->get('foo'), '->get() returns null if the cookie is expired'); } public function testExpire() { $cookieJar = new CookieJar(); $cookieJar->set($cookie = new Cookie('foo', 'bar')); $cookieJar->expire('foo'); $this->assertNull($cookieJar->get('foo'), '->get() returns null if the cookie is expired'); } public function testAll() { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo', 'bar')); $cookieJar->set($cookie2 = new Cookie('bar', 'foo')); $this->assertEquals(array($cookie1, $cookie2), $cookieJar->all(), '->all() returns all cookies in the jar'); } public function testClear() { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo', 'bar')); $cookieJar->set($cookie2 = new Cookie('bar', 'foo')); $cookieJar->clear(); $this->assertEquals(array(), $cookieJar->all(), '->clear() expires all cookies'); } public function testUpdateFromResponse() { $response = new Response('', 200, array('Set-Cookie' => 'foo=foo')); $cookieJar = new CookieJar(); $cookieJar->updateFromResponse($response); $this->assertEquals('foo', $cookieJar->get('foo')->getValue(), '->updateFromResponse() updates cookies from a Response objects'); } public function testUpdateFromSetCookie() { $setCookies = array('foo=foo'); $cookieJar = new CookieJar(); $cookieJar->set(new Cookie('bar', 'bar')); $cookieJar->updateFromSetCookie($setCookies); $this->assertInstanceOf('Symfony\Component\BrowserKit\Cookie', $cookieJar->get('foo')); $this->assertInstanceOf('Symfony\Component\BrowserKit\Cookie', $cookieJar->get('bar')); $this->assertEquals('foo', $cookieJar->get('foo')->getValue(), '->updateFromSetCookie() updates cookies from a Set-Cookie header'); $this->assertEquals('bar', $cookieJar->get('bar')->getValue(), '->updateFromSetCookie() keeps existing cookies'); } public function testUpdateFromSetCookieWithMultipleCookies() { $timestamp = time() + 3600; $date = gmdate('D, d M Y H:i:s \G\M\T', $timestamp); $setCookies = array(sprintf('foo=foo; expires=%s; domain=.symfony.com; path=/, bar=bar; domain=.blog.symfony.com, PHPSESSID=id; expires=%s', $date, $date)); $cookieJar = new CookieJar(); $cookieJar->updateFromSetCookie($setCookies); $fooCookie = $cookieJar->get('foo', '/', '.symfony.com'); $barCookie = $cookieJar->get('bar', '/', '.blog.symfony.com'); $phpCookie = $cookieJar->get('PHPSESSID'); $this->assertInstanceOf('Symfony\Component\BrowserKit\Cookie', $fooCookie); $this->assertInstanceOf('Symfony\Component\BrowserKit\Cookie', $barCookie); $this->assertInstanceOf('Symfony\Component\BrowserKit\Cookie', $phpCookie); $this->assertEquals('foo', $fooCookie->getValue()); $this->assertEquals('bar', $barCookie->getValue()); $this->assertEquals('id', $phpCookie->getValue()); $this->assertEquals($timestamp, $fooCookie->getExpiresTime()); $this->assertNull($barCookie->getExpiresTime()); $this->assertEquals($timestamp, $phpCookie->getExpiresTime()); } /** * @dataProvider provideAllValuesValues */ public function testAllValues($uri, $values) { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo_nothing', 'foo')); $cookieJar->set($cookie2 = new Cookie('foo_expired', 'foo', time() - 86400)); $cookieJar->set($cookie3 = new Cookie('foo_path', 'foo', null, '/foo')); $cookieJar->set($cookie4 = new Cookie('foo_domain', 'foo', null, '/', '.example.com')); $cookieJar->set($cookie4 = new Cookie('foo_strict_domain', 'foo', null, '/', '.www4.example.com')); $cookieJar->set($cookie5 = new Cookie('foo_secure', 'foo', null, '/', '', true)); $this->assertEquals($values, array_keys($cookieJar->allValues($uri)), '->allValues() returns the cookie for a given URI'); } public function provideAllValuesValues() { return array( array('http://www.example.com', array('foo_nothing', 'foo_domain')), array('http://www.example.com/', array('foo_nothing', 'foo_domain')), array('http://foo.example.com/', array('foo_nothing', 'foo_domain')), array('http://foo.example1.com/', array('foo_nothing')), array('https://foo.example.com/', array('foo_nothing', 'foo_secure', 'foo_domain')), array('http://www.example.com/foo/bar', array('foo_nothing', 'foo_path', 'foo_domain')), array('http://www4.example.com/', array('foo_nothing', 'foo_domain', 'foo_strict_domain')), ); } public function testEncodedValues() { $cookieJar = new CookieJar(); $cookieJar->set($cookie = new Cookie('foo', 'bar%3Dbaz', null, '/', '', false, true, true)); $this->assertEquals(array('foo' => 'bar=baz'), $cookieJar->allValues('/')); $this->assertEquals(array('foo' => 'bar%3Dbaz'), $cookieJar->allRawValues('/')); } public function testCookieExpireWithSameNameButDifferentPaths() { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo', 'bar1', null, '/foo')); $cookieJar->set($cookie2 = new Cookie('foo', 'bar2', null, '/bar')); $cookieJar->expire('foo', '/foo'); $this->assertNull($cookieJar->get('foo'), '->get() returns null if the cookie is expired'); $this->assertEquals(array(), array_keys($cookieJar->allValues('http://example.com/'))); $this->assertEquals(array(), $cookieJar->allValues('http://example.com/foo')); $this->assertEquals(array('foo' => 'bar2'), $cookieJar->allValues('http://example.com/bar')); } public function testCookieExpireWithNullPaths() { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo', 'bar1', null, '/')); $cookieJar->expire('foo', null); $this->assertNull($cookieJar->get('foo'), '->get() returns null if the cookie is expired'); $this->assertEquals(array(), array_keys($cookieJar->allValues('http://example.com/'))); } public function testCookieWithSameNameButDifferentPaths() { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo', 'bar1', null, '/foo')); $cookieJar->set($cookie2 = new Cookie('foo', 'bar2', null, '/bar')); $this->assertEquals(array(), array_keys($cookieJar->allValues('http://example.com/'))); $this->assertEquals(array('foo' => 'bar1'), $cookieJar->allValues('http://example.com/foo')); $this->assertEquals(array('foo' => 'bar2'), $cookieJar->allValues('http://example.com/bar')); } public function testCookieWithSameNameButDifferentDomains() { $cookieJar = new CookieJar(); $cookieJar->set($cookie1 = new Cookie('foo', 'bar1', null, '/', 'foo.example.com')); $cookieJar->set($cookie2 = new Cookie('foo', 'bar2', null, '/', 'bar.example.com')); $this->assertEquals(array(), array_keys($cookieJar->allValues('http://example.com/'))); $this->assertEquals(array('foo' => 'bar1'), $cookieJar->allValues('http://foo.example.com/')); $this->assertEquals(array('foo' => 'bar2'), $cookieJar->allValues('http://bar.example.com/')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit\Tests; use Symfony\Component\BrowserKit\Cookie; class CookieTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider getTestsForToFromString */ public function testToFromString($cookie, $url = null) { $this->assertEquals($cookie, (string) Cookie::fromString($cookie, $url)); } public function getTestsForToFromString() { return array( array('foo=bar'), array('foo=bar; path=/foo'), array('foo=bar; domain=google.com'), array('foo=bar; domain=example.com; secure', 'https://example.com/'), array('foo=bar; httponly'), array('foo=bar; domain=google.com; path=/foo; secure; httponly', 'https://google.com/'), array('foo=bar=baz'), array('foo=bar%3Dbaz'), ); } public function testFromStringIgnoreSecureFlag() { $this->assertFalse(Cookie::fromString('foo=bar; secure')->isSecure()); $this->assertFalse(Cookie::fromString('foo=bar; secure', 'http://example.com/')->isSecure()); } /** * @dataProvider getExpireCookieStrings */ public function testFromStringAcceptsSeveralExpiresDateFormats($cookie) { $this->assertEquals(1596185377, Cookie::fromString($cookie)->getExpiresTime()); } public function getExpireCookieStrings() { return array( array('foo=bar; expires=Fri, 31-Jul-2020 08:49:37 GMT'), array('foo=bar; expires=Fri, 31 Jul 2020 08:49:37 GMT'), array('foo=bar; expires=Fri, 31-07-2020 08:49:37 GMT'), array('foo=bar; expires=Fri, 31-07-20 08:49:37 GMT'), array('foo=bar; expires=Friday, 31-Jul-20 08:49:37 GMT'), array('foo=bar; expires=Fri Jul 31 08:49:37 2020'), array('foo=bar; expires=\'Fri Jul 31 08:49:37 2020\''), array('foo=bar; expires=Friday July 31st 2020, 08:49:37 GMT'), ); } public function testFromStringWithCapitalization() { $this->assertEquals('Foo=Bar', (string) Cookie::fromString('Foo=Bar')); $this->assertEquals('foo=bar; expires=Fri, 31 Dec 2010 23:59:59 GMT', (string) Cookie::fromString('foo=bar; Expires=Fri, 31 Dec 2010 23:59:59 GMT')); $this->assertEquals('foo=bar; domain=www.example.org; httponly', (string) Cookie::fromString('foo=bar; DOMAIN=www.example.org; HttpOnly')); } public function testFromStringWithUrl() { $this->assertEquals('foo=bar; domain=www.example.com', (string) Cookie::FromString('foo=bar', 'http://www.example.com/')); $this->assertEquals('foo=bar; domain=www.example.com; path=/foo', (string) Cookie::FromString('foo=bar', 'http://www.example.com/foo/bar')); $this->assertEquals('foo=bar; domain=www.example.com', (string) Cookie::FromString('foo=bar; path=/', 'http://www.example.com/foo/bar')); $this->assertEquals('foo=bar; domain=www.myotherexample.com', (string) Cookie::FromString('foo=bar; domain=www.myotherexample.com', 'http://www.example.com/')); } public function testFromStringThrowsAnExceptionIfCookieIsNotValid() { $this->setExpectedException('InvalidArgumentException'); Cookie::FromString('foo'); } public function testFromStringThrowsAnExceptionIfCookieDateIsNotValid() { $this->setExpectedException('InvalidArgumentException'); Cookie::FromString('foo=bar; expires=Flursday July 31st 2020, 08:49:37 GMT'); } public function testFromStringThrowsAnExceptionIfUrlIsNotValid() { $this->setExpectedException('InvalidArgumentException'); Cookie::FromString('foo=bar', 'foobar'); } public function testGetName() { $cookie = new Cookie('foo', 'bar'); $this->assertEquals('foo', $cookie->getName(), '->getName() returns the cookie name'); } public function testGetValue() { $cookie = new Cookie('foo', 'bar'); $this->assertEquals('bar', $cookie->getValue(), '->getValue() returns the cookie value'); $cookie = new Cookie('foo', 'bar%3Dbaz', null, '/', '', false, true, true); // raw value $this->assertEquals('bar=baz', $cookie->getValue(), '->getValue() returns the urldecoded cookie value'); } public function testGetRawValue() { $cookie = new Cookie('foo', 'bar=baz'); // decoded value $this->assertEquals('bar%3Dbaz', $cookie->getRawValue(), '->getRawValue() returns the urlencoded cookie value'); $cookie = new Cookie('foo', 'bar%3Dbaz', null, '/', '', false, true, true); // raw value $this->assertEquals('bar%3Dbaz', $cookie->getRawValue(), '->getRawValue() returns the non-urldecoded cookie value'); } public function testGetPath() { $cookie = new Cookie('foo', 'bar', 0); $this->assertEquals('/', $cookie->getPath(), '->getPath() returns / is no path is defined'); $cookie = new Cookie('foo', 'bar', 0, '/foo'); $this->assertEquals('/foo', $cookie->getPath(), '->getPath() returns the cookie path'); } public function testGetDomain() { $cookie = new Cookie('foo', 'bar', 0, '/', 'foo.com'); $this->assertEquals('foo.com', $cookie->getDomain(), '->getDomain() returns the cookie domain'); } public function testIsSecure() { $cookie = new Cookie('foo', 'bar'); $this->assertFalse($cookie->isSecure(), '->isSecure() returns false if not defined'); $cookie = new Cookie('foo', 'bar', 0, '/', 'foo.com', true); $this->assertTrue($cookie->isSecure(), '->isSecure() returns the cookie secure flag'); } public function testIsHttponly() { $cookie = new Cookie('foo', 'bar'); $this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns false if not defined'); $cookie = new Cookie('foo', 'bar', 0, '/', 'foo.com', false, true); $this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns the cookie httponly flag'); } public function testGetExpiresTime() { $cookie = new Cookie('foo', 'bar'); $this->assertNull($cookie->getExpiresTime(), '->getExpiresTime() returns the expires time'); $cookie = new Cookie('foo', 'bar', $time = time() - 86400); $this->assertEquals($time, $cookie->getExpiresTime(), '->getExpiresTime() returns the expires time'); } public function testIsExpired() { $cookie = new Cookie('foo', 'bar'); $this->assertFalse($cookie->isExpired(), '->isExpired() returns false when the cookie never expires (null as expires time)'); $cookie = new Cookie('foo', 'bar', time() - 86400); $this->assertTrue($cookie->isExpired(), '->isExpired() returns true when the cookie is expired'); $cookie = new Cookie('foo', 'bar', 0); $this->assertFalse($cookie->isExpired()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit\Tests; use Symfony\Component\BrowserKit\History; use Symfony\Component\BrowserKit\Request; class HistoryTest extends \PHPUnit_Framework_TestCase { public function testAdd() { $history = new History(); $history->add(new Request('http://www.example1.com/', 'get')); $this->assertSame('http://www.example1.com/', $history->current()->getUri(), '->add() adds a request to the history'); $history->add(new Request('http://www.example2.com/', 'get')); $this->assertSame('http://www.example2.com/', $history->current()->getUri(), '->add() adds a request to the history'); $history->add(new Request('http://www.example3.com/', 'get')); $history->back(); $history->add(new Request('http://www.example4.com/', 'get')); $this->assertSame('http://www.example4.com/', $history->current()->getUri(), '->add() adds a request to the history'); $history->back(); $this->assertSame('http://www.example2.com/', $history->current()->getUri(), '->add() adds a request to the history'); } public function testClearIsEmpty() { $history = new History(); $history->add(new Request('http://www.example.com/', 'get')); $this->assertFalse($history->isEmpty(), '->isEmpty() returns false if the history is not empty'); $history->clear(); $this->assertTrue($history->isEmpty(), '->isEmpty() true if the history is empty'); } public function testCurrent() { $history = new History(); try { $history->current(); $this->fail('->current() throws a \LogicException if the history is empty'); } catch (\Exception $e) { $this->assertInstanceof('LogicException', $e, '->current() throws a \LogicException if the history is empty'); } $history->add(new Request('http://www.example.com/', 'get')); $this->assertSame('http://www.example.com/', $history->current()->getUri(), '->current() returns the current request in the history'); } public function testBack() { $history = new History(); $history->add(new Request('http://www.example.com/', 'get')); try { $history->back(); $this->fail('->back() throws a \LogicException if the history is already on the first page'); } catch (\Exception $e) { $this->assertInstanceof('LogicException', $e, '->current() throws a \LogicException if the history is already on the first page'); } $history->add(new Request('http://www.example1.com/', 'get')); $history->back(); $this->assertSame('http://www.example.com/', $history->current()->getUri(), '->back() returns the previous request in the history'); } public function testForward() { $history = new History(); $history->add(new Request('http://www.example.com/', 'get')); $history->add(new Request('http://www.example1.com/', 'get')); try { $history->forward(); $this->fail('->forward() throws a \LogicException if the history is already on the last page'); } catch (\Exception $e) { $this->assertInstanceof('LogicException', $e, '->forward() throws a \LogicException if the history is already on the last page'); } $history->back(); $history->forward(); $this->assertSame('http://www.example1.com/', $history->current()->getUri(), '->forward() returns the next request in the history'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit\Tests; use Symfony\Component\BrowserKit\Request; class RequestTest extends \PHPUnit_Framework_TestCase { public function testGetUri() { $request = new Request('http://www.example.com/', 'get'); $this->assertEquals('http://www.example.com/', $request->getUri(), '->getUri() returns the URI of the request'); } public function testGetMethod() { $request = new Request('http://www.example.com/', 'get'); $this->assertEquals('get', $request->getMethod(), '->getMethod() returns the method of the request'); } public function testGetParameters() { $request = new Request('http://www.example.com/', 'get', array('foo' => 'bar')); $this->assertEquals(array('foo' => 'bar'), $request->getParameters(), '->getParameters() returns the parameters of the request'); } public function testGetFiles() { $request = new Request('http://www.example.com/', 'get', array(), array('foo' => 'bar')); $this->assertEquals(array('foo' => 'bar'), $request->getFiles(), '->getFiles() returns the uploaded files of the request'); } public function testGetCookies() { $request = new Request('http://www.example.com/', 'get', array(), array(), array('foo' => 'bar')); $this->assertEquals(array('foo' => 'bar'), $request->getCookies(), '->getCookies() returns the cookies of the request'); } public function testGetServer() { $request = new Request('http://www.example.com/', 'get', array(), array(), array(), array('foo' => 'bar')); $this->assertEquals(array('foo' => 'bar'), $request->getServer(), '->getServer() returns the server parameters of the request'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\BrowserKit\Tests; use Symfony\Component\BrowserKit\Response; class ResponseTest extends \PHPUnit_Framework_TestCase { public function testGetUri() { $response = new Response('foo'); $this->assertEquals('foo', $response->getContent(), '->getContent() returns the content of the response'); } public function testGetStatus() { $response = new Response('foo', 304); $this->assertEquals('304', $response->getStatus(), '->getStatus() returns the status of the response'); } public function testGetHeaders() { $response = new Response('foo', 200, array('foo' => 'bar')); $this->assertEquals(array('foo' => 'bar'), $response->getHeaders(), '->getHeaders() returns the headers of the response'); } public function testGetHeader() { $response = new Response('foo', 200, array( 'Content-Type' => 'text/html', 'Set-Cookie' => array('foo=bar', 'bar=foo'), )); $this->assertEquals('text/html', $response->getHeader('Content-Type'), '->getHeader() returns a header of the response'); $this->assertEquals('text/html', $response->getHeader('content-type'), '->getHeader() returns a header of the response'); $this->assertEquals('text/html', $response->getHeader('content_type'), '->getHeader() returns a header of the response'); $this->assertEquals('foo=bar', $response->getHeader('Set-Cookie'), '->getHeader() returns the first header value'); $this->assertEquals(array('foo=bar', 'bar=foo'), $response->getHeader('Set-Cookie', false), '->getHeader() returns all header values if first is false'); $this->assertNull($response->getHeader('foo'), '->getHeader() returns null if the header is not defined'); $this->assertEquals(array(), $response->getHeader('foo', false), '->getHeader() returns an empty array if the header is not defined and first is set to false'); } public function testMagicToString() { $response = new Response('foo', 304, array('foo' => 'bar')); $this->assertEquals("foo: bar\n\nfoo", $response->__toString(), '->__toString() returns the headers and the content as a string'); } public function testMagicToStringWithMultipleSetCookieHeader() { $headers = array( 'content-type' => 'text/html; charset=utf-8', 'set-cookie' => array('foo=bar', 'bar=foo') ); $expected = 'content-type: text/html; charset=utf-8'."\n"; $expected.= 'set-cookie: foo=bar'."\n"; $expected.= 'set-cookie: bar=foo'."\n\n"; $expected.= 'foo'; $response = new Response('foo', 304, $headers); $this->assertEquals($expected, $response->__toString(), '->__toString() returns the headers and the content as a string'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; use Symfony\Component\CssSelector\Exception\ParseException; /** * CssSelector is the main entry point of the component and can convert CSS * selectors to XPath expressions. * * $xpath = CssSelector::toXpath('h1.foo'); * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier * * @api */ class CssSelector { /** * Translates a CSS expression to its XPath equivalent. * Optionally, a prefix can be added to the resulting XPath * expression with the $prefix parameter. * * @param mixed $cssExpr The CSS expression. * @param string $prefix An optional prefix for the XPath expression. * * @return string * * @throws ParseException When got None for xpath expression * * @api */ public static function toXPath($cssExpr, $prefix = 'descendant-or-self::') { if (is_string($cssExpr)) { if (!$cssExpr) { return $prefix.'*'; } if (preg_match('#^\w+\s*$#u', $cssExpr, $match)) { return $prefix.trim($match[0]); } if (preg_match('~^(\w*)#(\w+)\s*$~u', $cssExpr, $match)) { return sprintf("%s%s[@id = '%s']", $prefix, $match[1] ? $match[1] : '*', $match[2]); } if (preg_match('#^(\w*)\.(\w+)\s*$#u', $cssExpr, $match)) { return sprintf("%s%s[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]", $prefix, $match[1] ? $match[1] : '*', $match[2]); } $parser = new self(); $cssExpr = $parser->parse($cssExpr); } $expr = $cssExpr->toXpath(); // @codeCoverageIgnoreStart if (!$expr) { throw new ParseException(sprintf('Got None for xpath expression from %s.', $cssExpr)); } // @codeCoverageIgnoreEnd if ($prefix) { $expr->addPrefix($prefix); } return (string) $expr; } /** * Parses an expression and returns the Node object that represents * the parsed expression. * * @param string $string The expression to parse * * @return Node\NodeInterface * * @throws \Exception When tokenizer throws it while parsing */ public function parse($string) { $tokenizer = new Tokenizer(); $stream = new TokenStream($tokenizer->tokenize($string), $string); try { return $this->parseSelectorGroup($stream); } catch (\Exception $e) { $class = get_class($e); throw new $class(sprintf('%s at %s -> %s', $e->getMessage(), implode($stream->getUsed(), ''), $stream->peek()), 0, $e); } } /** * Parses a selector group contained in $stream and returns * the Node object that represents the expression. * * @param TokenStream $stream The stream to parse. * * @return Node\NodeInterface */ private function parseSelectorGroup($stream) { $result = array(); while (true) { $result[] = $this->parseSelector($stream); if ($stream->peek() == ',') { $stream->next(); } else { break; } } if (count($result) == 1) { return $result[0]; } return new Node\OrNode($result); } /** * Parses a selector contained in $stream and returns the Node * object that represents it. * * @param TokenStream $stream The stream containing the selector. * * @return Node\NodeInterface * * @throws ParseException When expected selector but got something else */ private function parseSelector($stream) { $result = $this->parseSimpleSelector($stream); while (true) { $peek = $stream->peek(); if (',' == $peek || null === $peek) { return $result; } elseif (in_array($peek, array('+', '>', '~'))) { // A combinator $combinator = (string) $stream->next(); // Ignore optional whitespace after a combinator while (' ' == $stream->peek()) { $stream->next(); } } else { $combinator = ' '; } $consumed = count($stream->getUsed()); $nextSelector = $this->parseSimpleSelector($stream); if ($consumed == count($stream->getUsed())) { throw new ParseException(sprintf("Expected selector, got '%s'", $stream->peek())); } $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector); } return $result; } /** * Parses a simple selector (the current token) from $stream and returns * the resulting Node object. * * @param TokenStream $stream The stream containing the selector. * * @return Node\NodeInterface * * @throws ParseException When expected symbol but got something else */ private function parseSimpleSelector($stream) { $peek = $stream->peek(); if ('*' != $peek && !$peek->isType('Symbol')) { $element = $namespace = '*'; } else { $next = $stream->next(); if ('*' != $next && !$next->isType('Symbol')) { throw new ParseException(sprintf("Expected symbol, got '%s'", $next)); } if ($stream->peek() == '|') { $namespace = $next; $stream->next(); $element = $stream->next(); if ('*' != $element && !$next->isType('Symbol')) { throw new ParseException(sprintf("Expected symbol, got '%s'", $next)); } } else { $namespace = '*'; $element = $next; } } $result = new Node\ElementNode($namespace, $element); $hasHash = false; while (true) { $peek = $stream->peek(); if ('#' == $peek) { if ($hasHash) { /* You can't have two hashes (FIXME: is there some more general rule I'm missing?) */ // @codeCoverageIgnoreStart break; // @codeCoverageIgnoreEnd } $stream->next(); $result = new Node\HashNode($result, $stream->next()); $hasHash = true; continue; } elseif ('.' == $peek) { $stream->next(); $result = new Node\ClassNode($result, $stream->next()); continue; } elseif ('[' == $peek) { $stream->next(); $result = $this->parseAttrib($result, $stream); $next = $stream->next(); if (']' != $next) { throw new ParseException(sprintf("] expected, got '%s'", $next)); } continue; } elseif (':' == $peek || '::' == $peek) { $type = $stream->next(); $ident = $stream->next(); if (!$ident || !$ident->isType('Symbol')) { throw new ParseException(sprintf("Expected symbol, got '%s'", $ident)); } if ($stream->peek() == '(') { $stream->next(); $peek = $stream->peek(); if ($peek->isType('String')) { $selector = $stream->next(); } elseif ($peek->isType('Symbol') && is_int($peek)) { $selector = intval($stream->next()); } else { // FIXME: parseSimpleSelector, or selector, or...? $selector = $this->parseSimpleSelector($stream); } $next = $stream->next(); if (')' != $next) { throw new ParseException(sprintf("Expected ')', got '%s' and '%s'", $next, $selector)); } $result = new Node\FunctionNode($result, $type, $ident, $selector); } else { $result = new Node\PseudoNode($result, $type, $ident); } continue; } else { if (' ' == $peek) { $stream->next(); } break; } // FIXME: not sure what "negation" is } return $result; } /** * Parses an attribute from a selector contained in $stream and returns * the resulting AttribNode object. * * @param Node\NodeInterface $selector The selector object whose attribute * is to be parsed. * @param TokenStream $stream The container token stream. * * @return Node\AttribNode * * @throws ParseException When encountered unexpected selector */ private function parseAttrib($selector, $stream) { $attrib = $stream->next(); if ($stream->peek() == '|') { $namespace = $attrib; $stream->next(); $attrib = $stream->next(); } else { $namespace = '*'; } if ($stream->peek() == ']') { return new Node\AttribNode($selector, $namespace, $attrib, 'exists', null); } $op = $stream->next(); if (!in_array($op, array('^=', '$=', '*=', '=', '~=', '|=', '!='))) { throw new ParseException(sprintf("Operator expected, got '%s'", $op)); } $value = $stream->next(); if (!$value->isType('Symbol') && !$value->isType('String')) { throw new ParseException(sprintf("Expected string or symbol, got '%s'", $value)); } return new Node\AttribNode($selector, $namespace, $attrib, $op, $value); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Exception; /** * ParseException is thrown when a CSS selector syntax is not valid. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class ParseException extends \Exception { } Copyright (c) 2004-2013 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPathExpr; use Symfony\Component\CssSelector\Exception\ParseException; /** * AttribNode represents a "selector[namespace|attrib operator value]" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class AttribNode implements NodeInterface { protected $selector; protected $namespace; protected $attrib; protected $operator; protected $value; /** * Constructor. * * @param NodeInterface $selector The XPath selector * @param string $namespace The namespace * @param string $attrib The attribute * @param string $operator The operator * @param string $value The value */ public function __construct($selector, $namespace, $attrib, $operator, $value) { $this->selector = $selector; $this->namespace = $namespace; $this->attrib = $attrib; $this->operator = $operator; $this->value = $value; } /** * {@inheritDoc} */ public function __toString() { if ($this->operator == 'exists') { return sprintf('%s[%s[%s]]', __CLASS__, $this->selector, $this->formatAttrib()); } return sprintf('%s[%s[%s %s %s]]', __CLASS__, $this->selector, $this->formatAttrib(), $this->operator, $this->value); } /** * {@inheritDoc} */ public function toXpath() { $path = $this->selector->toXpath(); $attrib = $this->xpathAttrib(); $value = $this->value; if ($this->operator == 'exists') { $path->addCondition($attrib); } elseif ($this->operator == '=') { $path->addCondition(sprintf('%s = %s', $attrib, XPathExpr::xpathLiteral($value))); } elseif ($this->operator == '!=') { // FIXME: this seems like a weird hack... if ($value) { $path->addCondition(sprintf('not(%s) or %s != %s', $attrib, $attrib, XPathExpr::xpathLiteral($value))); } else { $path->addCondition(sprintf('%s != %s', $attrib, XPathExpr::xpathLiteral($value))); } // path.addCondition('%s != %s' % (attrib, xpathLiteral(value))) } elseif ($this->operator == '~=') { $path->addCondition(sprintf("contains(concat(' ', normalize-space(%s), ' '), %s)", $attrib, XPathExpr::xpathLiteral(' '.$value.' '))); } elseif ($this->operator == '|=') { // Weird, but true... $path->addCondition(sprintf('%s = %s or starts-with(%s, %s)', $attrib, XPathExpr::xpathLiteral($value), $attrib, XPathExpr::xpathLiteral($value.'-'))); } elseif ($this->operator == '^=') { $path->addCondition(sprintf('starts-with(%s, %s)', $attrib, XPathExpr::xpathLiteral($value))); } elseif ($this->operator == '$=') { // Oddly there is a starts-with in XPath 1.0, but not ends-with $path->addCondition(sprintf('substring(%s, string-length(%s)-%s) = %s', $attrib, $attrib, strlen($value) - 1, XPathExpr::xpathLiteral($value))); } elseif ($this->operator == '*=') { // FIXME: case sensitive? $path->addCondition(sprintf('contains(%s, %s)', $attrib, XPathExpr::xpathLiteral($value))); } else { throw new ParseException(sprintf('Unknown operator: %s', $this->operator)); } return $path; } /** * Returns the XPath Attribute * * @return string The XPath attribute */ protected function xpathAttrib() { // FIXME: if attrib is *? if ($this->namespace == '*') { return '@'.$this->attrib; } return sprintf('@%s:%s', $this->namespace, $this->attrib); } /** * Returns a formatted attribute * * @return string The formatted attribute */ protected function formatAttrib() { if ($this->namespace == '*') { return $this->attrib; } return sprintf('%s|%s', $this->namespace, $this->attrib); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPathExpr; /** * ClassNode represents a "selector.className" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class ClassNode implements NodeInterface { protected $selector; protected $className; /** * The constructor. * * @param NodeInterface $selector The XPath Selector * @param string $className The class name */ public function __construct($selector, $className) { $this->selector = $selector; $this->className = $className; } /** * {@inheritDoc} */ public function __toString() { return sprintf('%s[%s.%s]', __CLASS__, $this->selector, $this->className); } /** * {@inheritDoc} */ public function toXpath() { $selXpath = $this->selector->toXpath(); $selXpath->addCondition(sprintf("contains(concat(' ', normalize-space(@class), ' '), %s)", XPathExpr::xpathLiteral(' '.$this->className.' '))); return $selXpath; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\Exception\ParseException; /** * CombinedSelectorNode represents a combinator node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class CombinedSelectorNode implements NodeInterface { protected static $methodMapping = array( ' ' => 'descendant', '>' => 'child', '+' => 'direct_adjacent', '~' => 'indirect_adjacent', ); protected $selector; protected $combinator; protected $subselector; /** * The constructor. * * @param NodeInterface $selector The XPath selector * @param string $combinator The combinator * @param NodeInterface $subselector The sub XPath selector */ public function __construct($selector, $combinator, $subselector) { $this->selector = $selector; $this->combinator = $combinator; $this->subselector = $subselector; } /** * {@inheritDoc} */ public function __toString() { $comb = $this->combinator == ' ' ? '' : $this->combinator; return sprintf('%s[%s %s %s]', __CLASS__, $this->selector, $comb, $this->subselector); } /** * {@inheritDoc} * @throws ParseException When unknown combinator is found */ public function toXpath() { if (!isset(self::$methodMapping[$this->combinator])) { throw new ParseException(sprintf('Unknown combinator: %s', $this->combinator)); } $method = '_xpath_'.self::$methodMapping[$this->combinator]; $path = $this->selector->toXpath(); return $this->$method($path, $this->subselector); } /** * Joins a NodeInterface into the XPath of this object. * * @param XPathExpr $xpath The XPath expression for this object * @param NodeInterface $sub The NodeInterface object to add * * @return XPathExpr An XPath instance */ protected function _xpath_descendant($xpath, $sub) { // when sub is a descendant in any way of xpath $xpath->join('/descendant::', $sub->toXpath()); return $xpath; } /** * Joins a NodeInterface as a child of this object. * * @param XPathExpr $xpath The parent XPath expression * @param NodeInterface $sub The NodeInterface object to add * * @return XPathExpr An XPath instance */ protected function _xpath_child($xpath, $sub) { // when sub is an immediate child of xpath $xpath->join('/', $sub->toXpath()); return $xpath; } /** * Joins an XPath expression as an adjacent of another. * * @param XPathExpr $xpath The parent XPath expression * @param NodeInterface $sub The adjacent XPath expression * * @return XPathExpr An XPath instance */ protected function _xpath_direct_adjacent($xpath, $sub) { // when sub immediately follows xpath $xpath->join('/following-sibling::', $sub->toXpath()); $xpath->addNameTest(); $xpath->addCondition('position() = 1'); return $xpath; } /** * Joins an XPath expression as an indirect adjacent of another. * * @param XPathExpr $xpath The parent XPath expression * @param NodeInterface $sub The indirect adjacent NodeInterface object * * @return XPathExpr An XPath instance */ protected function _xpath_indirect_adjacent($xpath, $sub) { // when sub comes somewhere after xpath as a sibling $xpath->join('/following-sibling::', $sub->toXpath()); return $xpath; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPathExpr; /** * ElementNode represents a "namespace|element" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class ElementNode implements NodeInterface { protected $namespace; protected $element; /** * Constructor. * * @param string $namespace Namespace * @param string $element Element */ public function __construct($namespace, $element) { $this->namespace = $namespace; $this->element = $element; } /** * {@inheritDoc} */ public function __toString() { return sprintf('%s[%s]', __CLASS__, $this->formatElement()); } /** * Formats the element into a string. * * @return string Element as an XPath string */ public function formatElement() { if ($this->namespace == '*') { return $this->element; } return sprintf('%s|%s', $this->namespace, $this->element); } /** * {@inheritDoc} */ public function toXpath() { if ($this->namespace == '*') { $el = strtolower($this->element); } else { // FIXME: Should we lowercase here? $el = sprintf('%s:%s', $this->namespace, $this->element); } return new XPathExpr(null, null, $el); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\Exception\ParseException; use Symfony\Component\CssSelector\XPathExpr; /** * FunctionNode represents a "selector:name(expr)" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class FunctionNode implements NodeInterface { protected static $unsupported = array('target', 'lang', 'enabled', 'disabled'); protected $selector; protected $type; protected $name; protected $expr; /** * Constructor. * * @param NodeInterface $selector The XPath expression * @param string $type * @param string $name * @param XPathExpr $expr */ public function __construct($selector, $type, $name, $expr) { $this->selector = $selector; $this->type = $type; $this->name = $name; $this->expr = $expr; } /** * {@inheritDoc} */ public function __toString() { return sprintf('%s[%s%s%s(%s)]', __CLASS__, $this->selector, $this->type, $this->name, $this->expr); } /** * {@inheritDoc} * @throws ParseException When unsupported or unknown pseudo-class is found */ public function toXpath() { $selPath = $this->selector->toXpath(); if (in_array($this->name, self::$unsupported)) { throw new ParseException(sprintf('The pseudo-class %s is not supported', $this->name)); } $method = '_xpath_'.str_replace('-', '_', $this->name); if (!method_exists($this, $method)) { throw new ParseException(sprintf('The pseudo-class %s is unknown', $this->name)); } return $this->$method($selPath, $this->expr); } /** * undocumented function * * @param XPathExpr $xpath * @param mixed $expr * @param Boolean $last * @param Boolean $addNameTest * * @return XPathExpr */ protected function _xpath_nth_child($xpath, $expr, $last = false, $addNameTest = true) { list($a, $b) = $this->parseSeries($expr); if (!$a && !$b && !$last) { // a=0 means nothing is returned... $xpath->addCondition('false() and position() = 0'); return $xpath; } if ($addNameTest) { $xpath->addNameTest(); } $xpath->addStarPrefix(); if ($a == 0) { if ($last) { $b = sprintf('last() - %s', $b); } $xpath->addCondition(sprintf('position() = %s', $b)); return $xpath; } if ($last) { // FIXME: I'm not sure if this is right $a = -$a; $b = -$b; } if ($b > 0) { $bNeg = -$b; } else { $bNeg = sprintf('+%s', -$b); } if ($a != 1) { $expr = array(sprintf('(position() %s) mod %s = 0', $bNeg, $a)); } else { $expr = array(); } if ($b >= 0) { $expr[] = sprintf('position() >= %s', $b); } elseif ($b < 0 && $last) { $expr[] = sprintf('position() < (last() %s)', $b); } $expr = implode($expr, ' and '); if ($expr) { $xpath->addCondition($expr); } return $xpath; /* FIXME: handle an+b, odd, even an+b means every-a, plus b, e.g., 2n+1 means odd 0n+b means b n+0 means a=1, i.e., all elements an means every a elements, i.e., 2n means even -n means -1n -1n+6 means elements 6 and previous */ } /** * undocumented function * * @param XPathExpr $xpath * @param XPathExpr $expr * * @return XPathExpr */ protected function _xpath_nth_last_child($xpath, $expr) { return $this->_xpath_nth_child($xpath, $expr, true); } /** * undocumented function * * @param XPathExpr $xpath * @param XPathExpr $expr * * @return XPathExpr * * @throws ParseException */ protected function _xpath_nth_of_type($xpath, $expr) { if ($xpath->getElement() == '*') { throw new ParseException('*:nth-of-type() is not implemented'); } return $this->_xpath_nth_child($xpath, $expr, false, false); } /** * undocumented function * * @param XPathExpr $xpath * @param XPathExpr $expr * * @return XPathExpr */ protected function _xpath_nth_last_of_type($xpath, $expr) { return $this->_xpath_nth_child($xpath, $expr, true, false); } /** * undocumented function * * @param XPathExpr $xpath * @param XPathExpr $expr * * @return XPathExpr */ protected function _xpath_contains($xpath, $expr) { // text content, minus tags, must contain expr if ($expr instanceof ElementNode) { $expr = $expr->formatElement(); } // FIXME: lower-case is only available with XPath 2 //$xpath->addCondition(sprintf('contains(lower-case(string(.)), %s)', XPathExpr::xpathLiteral(strtolower($expr)))); $xpath->addCondition(sprintf('contains(string(.), %s)', XPathExpr::xpathLiteral($expr))); // FIXME: Currently case insensitive matching doesn't seem to be happening return $xpath; } /** * undocumented function * * @param XPathExpr $xpath * @param XPathExpr $expr * * @return XPathExpr */ protected function _xpath_not($xpath, $expr) { // everything for which not expr applies $expr = $expr->toXpath(); $cond = $expr->getCondition(); // FIXME: should I do something about element_path? $xpath->addCondition(sprintf('not(%s)', $cond)); return $xpath; } /** * Parses things like '1n+2', or 'an+b' generally, returning (a, b) * * @param mixed $s * * @return array */ protected function parseSeries($s) { if ($s instanceof ElementNode) { $s = $s->formatElement(); } if (!$s || '*' == $s) { // Happens when there's nothing, which the CSS parser thinks of as * return array(0, 0); } if ('odd' == $s) { return array(2, 1); } if ('even' == $s) { return array(2, 0); } if ('n' == $s) { return array(1, 0); } if (false === strpos($s, 'n')) { // Just a b return array(0, intval((string) $s)); } list($a, $b) = explode('n', $s); if (!$a) { $a = 1; } elseif ('-' == $a || '+' == $a) { $a = intval($a.'1'); } else { $a = intval($a); } if (!$b) { $b = 0; } elseif ('-' == $b || '+' == $b) { $b = intval($b.'1'); } else { $b = intval($b); } return array($a, $b); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPathExpr; /** * HashNode represents a "selector#id" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class HashNode implements NodeInterface { protected $selector; protected $id; /** * Constructor. * * @param NodeInterface $selector The NodeInterface object * @param string $id The ID */ public function __construct($selector, $id) { $this->selector = $selector; $this->id = $id; } /** * {@inheritDoc} */ public function __toString() { return sprintf('%s[%s#%s]', __CLASS__, $this->selector, $this->id); } /** * {@inheritDoc} */ public function toXpath() { $path = $this->selector->toXpath(); $path->addCondition(sprintf('@id = %s', XPathExpr::xpathLiteral($this->id))); return $path; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; /** * ClassNode represents a "selector.className" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ interface NodeInterface { /** * Returns a string representation of the object. * * @return string The string representation */ public function __toString(); /** * @return XPathExpr The XPath expression * * @throws ParseException When unknown operator is found */ public function toXpath(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPathExprOr; /** * OrNode represents a "Or" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class OrNode implements NodeInterface { /** * @var NodeInterface[] */ protected $items; /** * Constructor. * * @param NodeInterface[] $items An array of NodeInterface objects */ public function __construct($items) { $this->items = $items; } /** * {@inheritDoc} */ public function __toString() { return sprintf('%s(%s)', __CLASS__, $this->items); } /** * {@inheritDoc} */ public function toXpath() { $paths = array(); foreach ($this->items as $item) { $paths[] = $item->toXpath(); } return new XPathExprOr($paths); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\Exception\ParseException; use Symfony\Component\CssSelector\XPathExpr; /** * PseudoNode represents a "selector:ident" node. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class PseudoNode implements NodeInterface { protected static $unsupported = array( 'indeterminate', 'first-line', 'first-letter', 'selection', 'before', 'after', 'link', 'visited', 'active', 'focus', 'hover', ); protected $element; protected $type; protected $ident; /** * Constructor. * * @param NodeInterface $element The NodeInterface element * @param string $type Node type * @param string $ident The ident * * @throws ParseException When incorrect PseudoNode type is given */ public function __construct($element, $type, $ident) { $this->element = $element; if (!in_array($type, array(':', '::'))) { throw new ParseException(sprintf('The PseudoNode type can only be : or :: (%s given).', $type)); } $this->type = $type; $this->ident = $ident; } /** * {@inheritDoc} */ public function __toString() { return sprintf('%s[%s%s%s]', __CLASS__, $this->element, $this->type, $this->ident); } /** * {@inheritDoc} * @throws ParseException When unsupported or unknown pseudo-class is found */ public function toXpath() { $elXpath = $this->element->toXpath(); if (in_array($this->ident, self::$unsupported)) { throw new ParseException(sprintf('The pseudo-class %s is unsupported', $this->ident)); } $method = 'xpath_'.str_replace('-', '_', $this->ident); if (!method_exists($this, $method)) { throw new ParseException(sprintf('The pseudo-class %s is unknown', $this->ident)); } return $this->$method($elXpath); } /** * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified XPath expression */ protected function xpath_checked($xpath) { // FIXME: is this really all the elements? $xpath->addCondition("(@selected or @checked) and (name(.) = 'input' or name(.) = 'option')"); return $xpath; } /** * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified XPath expression * * @throws ParseException If this element is the root element */ protected function xpath_root($xpath) { // if this element is the root element throw new ParseException(); } /** * Marks this XPath expression as the first child. * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression */ protected function xpath_first_child($xpath) { $xpath->addStarPrefix(); $xpath->addNameTest(); $xpath->addCondition('position() = 1'); return $xpath; } /** * Sets the XPath to be the last child. * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression */ protected function xpath_last_child($xpath) { $xpath->addStarPrefix(); $xpath->addNameTest(); $xpath->addCondition('position() = last()'); return $xpath; } /** * Sets the XPath expression to be the first of type. * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression * * @throws ParseException */ protected function xpath_first_of_type($xpath) { if ($xpath->getElement() == '*') { throw new ParseException('*:first-of-type is not implemented'); } $xpath->addStarPrefix(); $xpath->addCondition('position() = 1'); return $xpath; } /** * Sets the XPath expression to be the last of type. * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression * * @throws ParseException Because *:last-of-type is not implemented */ protected function xpath_last_of_type($xpath) { if ($xpath->getElement() == '*') { throw new ParseException('*:last-of-type is not implemented'); } $xpath->addStarPrefix(); $xpath->addCondition('position() = last()'); return $xpath; } /** * Sets the XPath expression to be the only child. * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression */ protected function xpath_only_child($xpath) { $xpath->addNameTest(); $xpath->addStarPrefix(); $xpath->addCondition('last() = 1'); return $xpath; } /** * Sets the XPath expression to be only of type. * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression * * @throws ParseException Because *:only-of-type is not implemented */ protected function xpath_only_of_type($xpath) { if ($xpath->getElement() == '*') { throw new ParseException('*:only-of-type is not implemented'); } $xpath->addCondition('last() = 1'); return $xpath; } /** * undocumented function * * @param XPathExpr $xpath The XPath expression * * @return XPathExpr The modified expression */ protected function xpath_empty($xpath) { $xpath->addCondition('not(*) and not(normalize-space())'); return $xpath; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests; use Symfony\Component\CssSelector\CssSelector; class CssSelectorTest extends \PHPUnit_Framework_TestCase { public function testCsstoXPath() { $this->assertEquals('descendant-or-self::*', CssSelector::toXPath('')); $this->assertEquals('descendant-or-self::h1', CssSelector::toXPath('h1')); $this->assertEquals("descendant-or-self::h1[@id = 'foo']", CssSelector::toXPath('h1#foo')); $this->assertEquals("descendant-or-self::h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", CssSelector::toXPath('h1.foo')); $this->assertEquals('descendant-or-self::foo:h1', CssSelector::toXPath('foo|h1')); } /** * @dataProvider getCssSelectors */ public function testParse($css, $xpath) { $parser = new CssSelector(); $this->assertEquals($xpath, (string) $parser->parse($css)->toXPath(), '->parse() parses an input string and returns a node'); } public function testParseExceptions() { $parser = new CssSelector(); try { $parser->parse('h1:'); $this->fail('->parse() throws an Exception if the css selector is not valid'); } catch (\Exception $e) { $this->assertInstanceOf('\Symfony\Component\CssSelector\Exception\ParseException', $e, '->parse() throws an Exception if the css selector is not valid'); $this->assertEquals("Expected symbol, got '' at h1: -> ", $e->getMessage(), '->parse() throws an Exception if the css selector is not valid'); } } public function getCssSelectors() { return array( array('h1', "h1"), array('foo|h1', "foo:h1"), array('h1, h2, h3', "h1 | h2 | h3"), array('h1:nth-child(3n+1)', "*/*[name() = 'h1' and ((position() -1) mod 3 = 0 and position() >= 1)]"), array('h1 > p', "h1/p"), array('h1#foo', "h1[@id = 'foo']"), array('h1.foo', "h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), array('h1[class*="foo bar"]', "h1[contains(@class, 'foo bar')]"), array('h1[foo|class*="foo bar"]', "h1[contains(@foo:class, 'foo bar')]"), array('h1[class]', "h1[@class]"), array('h1 .foo', "h1/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), array('h1 #foo', "h1/descendant::*[@id = 'foo']"), array('h1 [class*=foo]', "h1/descendant::*[contains(@class, 'foo')]"), array('div>.foo', "div/*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), array('div > .foo', "div/*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\AttribNode; use Symfony\Component\CssSelector\Node\ElementNode; class AttribNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { $element = new ElementNode('*', 'h1'); $operators = array( '^=' => "h1[starts-with(@class, 'foo')]", '$=' => "h1[substring(@class, string-length(@class)-2) = 'foo']", '*=' => "h1[contains(@class, 'foo')]", '=' => "h1[@class = 'foo']", '~=' => "h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", '|=' => "h1[@class = 'foo' or starts-with(@class, 'foo-')]", '!=' => "h1[not(@class) or @class != 'foo']", ); // h1[class??foo] foreach ($operators as $op => $xpath) { $attrib = new AttribNode($element, '*', 'class', $op, 'foo'); $this->assertEquals($xpath, (string) $attrib->toXpath(), '->toXpath() returns the xpath representation of the node'); } // h1[class] $attrib = new AttribNode($element, '*', 'class', 'exists', 'foo'); $this->assertEquals('h1[@class]', (string) $attrib->toXpath(), '->toXpath() returns the xpath representation of the node'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ClassNode; use Symfony\Component\CssSelector\Node\ElementNode; class ClassNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { // h1.foo $element = new ElementNode('*', 'h1'); $class = new ClassNode($element, 'foo'); $this->assertEquals("h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", (string) $class->toXpath(), '->toXpath() returns the xpath representation of the node'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\CombinedSelectorNode; use Symfony\Component\CssSelector\Node\ElementNode; class CombinedSelectorNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { $combinators = array( ' ' => "h1/descendant::p", '>' => "h1/p", '+' => "h1/following-sibling::*[name() = 'p' and (position() = 1)]", '~' => "h1/following-sibling::p", ); // h1 ?? p $element1 = new ElementNode('*', 'h1'); $element2 = new ElementNode('*', 'p'); foreach ($combinators as $combinator => $xpath) { $combinator = new CombinedSelectorNode($element1, $combinator, $element2); $this->assertEquals($xpath, (string) $combinator->toXpath(), '->toXpath() returns the xpath representation of the node'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\ElementNode; class ElementNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { // h1 $element = new ElementNode('*', 'h1'); $this->assertEquals('h1', (string) $element->toXpath(), '->toXpath() returns the xpath representation of the node'); // foo|h1 $element = new ElementNode('foo', 'h1'); $this->assertEquals('foo:h1', (string) $element->toXpath(), '->toXpath() returns the xpath representation of the node'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\FunctionNode; use Symfony\Component\CssSelector\Node\ElementNode; use Symfony\Component\CssSelector\Token; class FunctionNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { $element = new ElementNode('*', 'h1'); // h1:contains("foo") $function = new FunctionNode($element, ':', 'contains', 'foo'); $this->assertEquals("h1[contains(string(.), 'foo')]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(1) $function = new FunctionNode($element, ':', 'nth-child', 1); $this->assertEquals("*/*[name() = 'h1' and (position() = 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child() $function = new FunctionNode($element, ':', 'nth-child', ''); $this->assertEquals("h1[false() and position() = 0]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(odd) $element2 = new ElementNode('*', new Token('Symbol', 'odd', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and ((position() -1) mod 2 = 0 and position() >= 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(even) $element2 = new ElementNode('*', new Token('Symbol', 'even', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and ((position() +0) mod 2 = 0 and position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(n) $element2 = new ElementNode('*', new Token('Symbol', 'n', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and (position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(3n+1) $element2 = new ElementNode('*', new Token('Symbol', '3n+1', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and ((position() -1) mod 3 = 0 and position() >= 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(n+1) $element2 = new ElementNode('*', new Token('Symbol', 'n+1', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and (position() >= 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(1) $element2 = new ElementNode('*', new Token('Symbol', '2', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and (position() = 2)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(2n) $element2 = new ElementNode('*', new Token('Symbol', '2n', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and ((position() +0) mod 2 = 0 and position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-child(-n) $element2 = new ElementNode('*', new Token('Symbol', '-n', -1)); $function = new FunctionNode($element, ':', 'nth-child', $element2); $this->assertEquals("*/*[name() = 'h1' and ((position() +0) mod -1 = 0 and position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-last-child(2) $function = new FunctionNode($element, ':', 'nth-last-child', 2); $this->assertEquals("*/*[name() = 'h1' and (position() = last() - 2)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-of-type(2) $function = new FunctionNode($element, ':', 'nth-of-type', 2); $this->assertEquals("*/h1[position() = 2]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:nth-last-of-type(2) $function = new FunctionNode($element, ':', 'nth-last-of-type', 2); $this->assertEquals("*/h1[position() = last() - 2]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); /* // h1:not(p) $element2 = new ElementNode('*', 'p'); $function = new FunctionNode($element, ':', 'not', $element2); $this->assertEquals("h1[not()]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node'); */ } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\HashNode; use Symfony\Component\CssSelector\Node\ElementNode; class HashNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { // h1#foo $element = new ElementNode('*', 'h1'); $hash = new HashNode($element, 'foo'); $this->assertEquals("h1[@id = 'foo']", (string) $hash->toXpath(), '->toXpath() returns the xpath representation of the node'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\OrNode; use Symfony\Component\CssSelector\Node\ElementNode; class OrNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { // h1, h2, h3 $element1 = new ElementNode('*', 'h1'); $element2 = new ElementNode('*', 'h2'); $element3 = new ElementNode('*', 'h3'); $or = new OrNode(array($element1, $element2, $element3)); $this->assertEquals("h1 | h2 | h3", (string) $or->toXpath(), '->toXpath() returns the xpath representation of the node'); } public function testIssueMissingPrefix() { // h1, h2, h3 $element1 = new ElementNode('*', 'h1'); $element2 = new ElementNode('*', 'h2'); $element3 = new ElementNode('*', 'h3'); $or = new OrNode(array($element1, $element2, $element3)); $xPath = $or->toXPath(); $xPath->addPrefix('descendant-or-self::'); $this->assertEquals("descendant-or-self::h1 | descendant-or-self::h2 | descendant-or-self::h3", (string) $xPath); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests\Node; use Symfony\Component\CssSelector\Node\PseudoNode; use Symfony\Component\CssSelector\Node\ElementNode; class PseudoNodeTest extends \PHPUnit_Framework_TestCase { public function testToXpath() { $element = new ElementNode('*', 'h1'); // h1:checked $pseudo = new PseudoNode($element, ':', 'checked'); $this->assertEquals("h1[(@selected or @checked) and (name(.) = 'input' or name(.) = 'option')]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:first-child $pseudo = new PseudoNode($element, ':', 'first-child'); $this->assertEquals("*/*[name() = 'h1' and (position() = 1)]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:last-child $pseudo = new PseudoNode($element, ':', 'last-child'); $this->assertEquals("*/*[name() = 'h1' and (position() = last())]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:first-of-type $pseudo = new PseudoNode($element, ':', 'first-of-type'); $this->assertEquals("*/h1[position() = 1]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:last-of-type $pseudo = new PseudoNode($element, ':', 'last-of-type'); $this->assertEquals("*/h1[position() = last()]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:only-child $pseudo = new PseudoNode($element, ':', 'only-child'); $this->assertEquals("*/*[name() = 'h1' and (last() = 1)]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:only-of-type $pseudo = new PseudoNode($element, ':', 'only-of-type'); $this->assertEquals("h1[last() = 1]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); // h1:empty $pseudo = new PseudoNode($element, ':', 'empty'); $this->assertEquals("h1[not(*) and not(normalize-space())]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests; use Symfony\Component\CssSelector\Tokenizer; class TokenizerTest extends \PHPUnit_Framework_TestCase { protected $tokenizer; protected function setUp() { $this->tokenizer = new Tokenizer(); } /** * @dataProvider getCssSelectors */ public function testTokenize($css) { $this->assertEquals($css, $this->tokensToString($this->tokenizer->tokenize($css)), '->tokenize() lexes an input string and returns an array of tokens'); } public function testTokenizeWithQuotedStrings() { $this->assertEquals('foo[class=foo bar ]', $this->tokensToString($this->tokenizer->tokenize('foo[class="foo bar"]')), '->tokenize() lexes an input string and returns an array of tokens'); $this->assertEquals("foo[class=foo Abar ]", $this->tokensToString($this->tokenizer->tokenize('foo[class="foo \\65 bar"]')), '->tokenize() lexes an input string and returns an array of tokens'); $this->assertEquals("img[alt= ]", $this->tokensToString($this->tokenizer->tokenize('img[alt=""]')), '->tokenize() lexes an input string and returns an array of tokens'); } /** * @expectedException \Symfony\Component\CssSelector\Exception\ParseException */ public function testTokenizeInvalidString() { $this->tokensToString($this->tokenizer->tokenize('/invalid')); } public function getCssSelectors() { return array( array('h1'), array('h1:nth-child(3n+1)'), array('h1 > p'), array('h1#foo'), array('h1.foo'), array('h1[class*=foo]'), array('h1 .foo'), array('h1 #foo'), array('h1 [class*=foo]'), ); } protected function tokensToString($tokens) { $str = ''; foreach ($tokens as $token) { $str .= str_repeat(' ', $token->getPosition() - strlen($str)).$token; } return $str; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector\Tests; use Symfony\Component\CssSelector\XPathExpr; class XPathExprTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider getXPathLiteralValues */ public function testXpathLiteral($value, $literal) { $this->assertEquals($literal, XPathExpr::xpathLiteral($value)); } public function getXPathLiteralValues() { return array( array('foo', "'foo'"), array("foo's bar", '"foo\'s bar"'), array("foo's \"middle\" bar", 'concat(\'foo\', "\'", \'s "middle" bar\')'), array("foo's 'middle' \"bar\"", 'concat(\'foo\', "\'", \'s \', "\'", \'middle\', "\'", \' "bar"\')'), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; /** * Token represents a CSS Selector token. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class Token { private $type; private $value; private $position; /** * Constructor. * * @param string $type The type of this token. * @param mixed $value The value of this token. * @param integer $position The order of this token. */ public function __construct($type, $value, $position) { $this->type = $type; $this->value = $value; $this->position = $position; } /** * Gets a string representation of this token. * * @return string */ public function __toString() { return (string) $this->value; } /** * Answers whether this token's type equals to $type. * * @param string $type The type to test against this token's one. * * @return Boolean */ public function isType($type) { return $this->type == $type; } /** * Gets the position of this token. * * @return integer */ public function getPosition() { return $this->position; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; use Symfony\Component\CssSelector\Exception\ParseException; /** * Tokenizer lexes a CSS Selector to tokens. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class Tokenizer { /** * Takes a CSS selector and returns an array holding the Tokens * it contains. * * @param string $s The selector to lex. * * @return array Token[] */ public function tokenize($s) { if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { $mbEncoding = mb_internal_encoding(); mb_internal_encoding('ASCII'); } $tokens = array(); $pos = 0; $s = preg_replace('#/\*.*?\*/#s', '', $s); while (true) { if (preg_match('#\s+#A', $s, $match, 0, $pos)) { $precedingWhitespacePos = $pos; $pos += strlen($match[0]); } else { $precedingWhitespacePos = 0; } if ($pos >= strlen($s)) { if (isset($mbEncoding)) { mb_internal_encoding($mbEncoding); } return $tokens; } if (preg_match('#[+-]?\d*n(?:[+-]\d+)?#A', $s, $match, 0, $pos) && 'n' !== $match[0]) { $sym = substr($s, $pos, strlen($match[0])); $tokens[] = new Token('Symbol', $sym, $pos); $pos += strlen($match[0]); continue; } $c = $s[$pos]; $c2 = substr($s, $pos, 2); if (in_array($c2, array('~=', '|=', '^=', '$=', '*=', '::', '!='))) { $tokens[] = new Token('Token', $c2, $pos); $pos += 2; continue; } if (in_array($c, array('>', '+', '~', ',', '.', '*', '=', '[', ']', '(', ')', '|', ':', '#'))) { if (in_array($c, array('.', '#', '[')) && $precedingWhitespacePos > 0) { $tokens[] = new Token('Token', ' ', $precedingWhitespacePos); } $tokens[] = new Token('Token', $c, $pos); ++$pos; continue; } if ('"' === $c || "'" === $c) { // Quoted string $oldPos = $pos; list($sym, $pos) = $this->tokenizeEscapedString($s, $pos); $tokens[] = new Token('String', $sym, $oldPos); continue; } $oldPos = $pos; list($sym, $pos) = $this->tokenizeSymbol($s, $pos); $tokens[] = new Token('Symbol', $sym, $oldPos); continue; } } /** * Tokenizes a quoted string (i.e. 'A string quoted with \' characters'), * and returns an array holding the unquoted string contained by $s and * the new position from which tokenizing should take over. * * @param string $s The selector string containing the quoted string. * @param integer $pos The starting position for the quoted string. * * @return array * * @throws ParseException When expected closing is not found */ private function tokenizeEscapedString($s, $pos) { $quote = $s[$pos]; $pos = $pos + 1; $start = $pos; while (true) { $next = strpos($s, $quote, $pos); if (false === $next) { throw new ParseException(sprintf('Expected closing %s for string in: %s', $quote, substr($s, $start))); } $result = substr($s, $start, $next - $start); if (strlen($result) > 0 && '\\' === $result[strlen($result) - 1]) { // next quote character is escaped $pos = $next + 1; continue; } if (false !== strpos($result, '\\')) { $result = $this->unescapeStringLiteral($result); } return array($result, $next + 1); } } /** * Unescapes a string literal and returns the unescaped string. * * @param string $literal The string literal to unescape. * * @return string * * @throws ParseException When invalid escape sequence is found */ private function unescapeStringLiteral($literal) { return preg_replace_callback('#(\\\\(?:[A-Fa-f0-9]{1,6}(?:\r\n|\s)?|[^A-Fa-f0-9]))#', function ($matches) use ($literal) { if ($matches[0][0] == '\\' && strlen($matches[0]) > 1) { $matches[0] = substr($matches[0], 1); if (in_array($matches[0][0], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'))) { return chr(trim($matches[0])); } } else { throw new ParseException(sprintf('Invalid escape sequence %s in string %s', $matches[0], $literal)); } }, $literal); } /** * Lexes selector $s and returns an array holding the name of the symbol * contained in it and the new position from which tokenizing should take * over. * * @param string $s The selector string. * @param integer $pos The position in $s at which the symbol starts. * * @return array * * @throws ParseException When Unexpected symbol is found */ private function tokenizeSymbol($s, $pos) { $start = $pos; if (!preg_match('#[^\w\-]#', $s, $match, PREG_OFFSET_CAPTURE, $pos)) { // Goes to end of s return array(substr($s, $start), strlen($s)); } $matchStart = $match[0][1]; if ($matchStart == $pos) { throw new ParseException(sprintf('Unexpected symbol: %s at %s', $s[$pos], $pos)); } $result = substr($s, $start, $matchStart - $start); $pos = $matchStart; return array($result, $pos); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; /** * TokenStream represents a stream of CSS Selector tokens. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class TokenStream { private $used; private $tokens; private $source; private $peeked; private $peeking; /** * Constructor. * * @param array $tokens The tokens that make the stream. * @param mixed $source The source of the stream. */ public function __construct($tokens, $source = null) { $this->used = array(); $this->tokens = $tokens; $this->source = $source; $this->peeked = null; $this->peeking = false; } /** * Gets the tokens that have already been visited in this stream. * * @return array */ public function getUsed() { return $this->used; } /** * Gets the next token in the stream or null if there is none. * Note that if this stream was set to be peeking its behavior * will be restored to not peeking after this operation. * * @return mixed */ public function next() { if ($this->peeking) { $this->peeking = false; $this->used[] = $this->peeked; return $this->peeked; } if (!count($this->tokens)) { return null; } $next = array_shift($this->tokens); $this->used[] = $next; return $next; } /** * Peeks for the next token in this stream. This means that the next token * will be returned but it won't be considered as used (visited) until the * next() method is invoked. * If there are no remaining tokens null will be returned. * * @see next() * * @return mixed */ public function peek() { if (!$this->peeking) { if (!count($this->tokens)) { return null; } $this->peeked = array_shift($this->tokens); $this->peeking = true; } return $this->peeked; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; /** * XPathExpr represents an XPath expression. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class XPathExpr { private $prefix; private $path; private $element; private $condition; private $starPrefix; /** * Constructor. * * @param string $prefix Prefix for the XPath expression. * @param string $path Actual path of the expression. * @param string $element The element in the expression. * @param string $condition A condition for the expression. * @param Boolean $starPrefix Indicates whether to use a star prefix. */ public function __construct($prefix = null, $path = null, $element = '*', $condition = null, $starPrefix = false) { $this->prefix = $prefix; $this->path = $path; $this->element = $element; $this->condition = $condition; $this->starPrefix = $starPrefix; } /** * Gets the prefix of this XPath expression. * * @return string */ public function getPrefix() { return $this->prefix; } /** * Gets the path of this XPath expression. * * @return string */ public function getPath() { return $this->path; } /** * Answers whether this XPath expression has a star prefix. * * @return Boolean */ public function hasStarPrefix() { return $this->starPrefix; } /** * Gets the element of this XPath expression. * * @return string */ public function getElement() { return $this->element; } /** * Gets the condition of this XPath expression. * * @return string */ public function getCondition() { return $this->condition; } /** * Gets a string representation for this XPath expression. * * @return string */ public function __toString() { $path = ''; if (null !== $this->prefix) { $path .= $this->prefix; } if (null !== $this->path) { $path .= $this->path; } $path .= $this->element; if ($this->condition) { $path .= sprintf('[%s]', $this->condition); } return $path; } /** * Adds a condition to this XPath expression. * Any pre-existent condition will be ANDed to it. * * @param string $condition The condition to add. */ public function addCondition($condition) { if ($this->condition) { $this->condition = sprintf('%s and (%s)', $this->condition, $condition); } else { $this->condition = $condition; } } /** * Adds a prefix to this XPath expression. * It will be prepended to any pre-existent prefixes. * * @param string $prefix The prefix to add. */ public function addPrefix($prefix) { if ($this->prefix) { $this->prefix = $prefix.$this->prefix; } else { $this->prefix = $prefix; } } /** * Adds a condition to this XPath expression using the name of the element * as the desired value. * This method resets the element to '*'. */ public function addNameTest() { if ($this->element == '*') { // We weren't doing a test anyway return; } $this->addCondition(sprintf('name() = %s', XPathExpr::xpathLiteral($this->element))); $this->element = '*'; } /** * Adds a star prefix to this XPath expression. * This method will prepend a '*' to the path and set the star prefix flag * to true. */ public function addStarPrefix() { /* Adds a /* prefix if there is no prefix. This is when you need to keep context's constrained to a single parent. */ if ($this->path) { $this->path .= '*/'; } else { $this->path = '*/'; } $this->starPrefix = true; } /** * Joins this XPath expression with $other (another XPath expression) using * $combiner to join them. * * @param string $combiner The combiner string. * @param XPathExpr $other The other XPath expression to combine with * this one. */ public function join($combiner, $other) { $prefix = (string) $this; $prefix .= $combiner; $path = $other->getPrefix().$other->getPath(); /* We don't need a star prefix if we are joining to this other prefix; so we'll get rid of it */ if ($other->hasStarPrefix() && '*/' == $path) { $path = ''; } $this->prefix = $prefix; $this->path = $path; $this->element = $other->getElement(); $this->condition = $other->GetCondition(); } /** * Gets an XPath literal for $s. * * @param mixed $s Can either be a Node\ElementNode or a string. * * @return string */ public static function xpathLiteral($s) { if ($s instanceof Node\ElementNode) { // This is probably a symbol that looks like an expression... $s = $s->formatElement(); } else { $s = (string) $s; } if (false === strpos($s, "'")) { return sprintf("'%s'", $s); } if (false === strpos($s, '"')) { return sprintf('"%s"', $s); } $string = $s; $parts = array(); while (true) { if (false !== $pos = strpos($string, "'")) { $parts[] = sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { $parts[] = "'$string'"; break; } } return sprintf('concat(%s)', implode($parts, ', ')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\CssSelector; /** * XPathExprOr represents XPath |'d expressions. * * Note that unfortunately it isn't the union, it's the sum, so duplicate elements will appear. * * This component is a port of the Python lxml library, * which is copyright Infrae and distributed under the BSD license. * * @author Fabien Potencier */ class XPathExprOr extends XPathExpr { /** * Constructor. * * @param array $items The items in the expression. * @param string $prefix Optional prefix for the expression. */ public function __construct($items, $prefix = null) { $this->items = $items; $this->prefix = $prefix; } /** * Gets a string representation of this |'d expression. * * @return string */ public function __toString() { $prefix = $this->getPrefix(); $tmp = array(); foreach ($this->items as $i) { $tmp[] = sprintf('%s%s', $prefix, $i); } return implode($tmp, ' | '); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; use Symfony\Component\CssSelector\CssSelector; /** * Crawler eases navigation of a list of \DOMNode objects. * * @author Fabien Potencier * * @api */ class Crawler extends \SplObjectStorage { /** * @var string The current URI or the base href value */ private $uri; /** * Constructor. * * @param mixed $node A Node to use as the base for the crawling * @param string $uri The current URI or the base href value * * @api */ public function __construct($node = null, $uri = null) { $this->uri = $uri; $this->add($node); } /** * Removes all the nodes. * * @api */ public function clear() { $this->removeAll($this); } /** * Adds a node to the current list of nodes. * * This method uses the appropriate specialized add*() method based * on the type of the argument. * * @param null|\DOMNodeList|array|\DOMNode $node A node * * @api */ public function add($node) { if ($node instanceof \DOMNodeList) { $this->addNodeList($node); } elseif (is_array($node)) { $this->addNodes($node); } elseif (is_string($node)) { $this->addContent($node); } elseif (is_object($node)) { $this->addNode($node); } } /** * Adds HTML/XML content. * * @param string $content A string to parse as HTML/XML * @param null|string $type The content type of the string * * @return null|void */ public function addContent($content, $type = null) { if (empty($type)) { $type = 'text/html'; } // DOM only for HTML/XML content if (!preg_match('/(x|ht)ml/i', $type, $matches)) { return null; } $charset = 'ISO-8859-1'; if (false !== $pos = strpos($type, 'charset=')) { $charset = substr($type, $pos + 8); if (false !== $pos = strpos($charset, ';')) { $charset = substr($charset, 0, $pos); } } if ('x' === $matches[1]) { $this->addXmlContent($content, $charset); } else { $this->addHtmlContent($content, $charset); } } /** * Adds an HTML content to the list of nodes. * * The libxml errors are disabled when the content is parsed. * * If you want to get parsing errors, be sure to enable * internal errors via libxml_use_internal_errors(true) * and then, get the errors via libxml_get_errors(). Be * sure to clear errors with libxml_clear_errors() afterward. * * @param string $content The HTML content * @param string $charset The charset * * @api */ public function addHtmlContent($content, $charset = 'UTF-8') { $current = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); $dom = new \DOMDocument('1.0', $charset); $dom->validateOnParse = true; if (function_exists('mb_convert_encoding') && in_array(strtolower($charset), array_map('strtolower', mb_list_encodings()))) { $content = mb_convert_encoding($content, 'HTML-ENTITIES', $charset); } @$dom->loadHTML($content); libxml_use_internal_errors($current); libxml_disable_entity_loader($disableEntities); $this->addDocument($dom); $base = $this->filterXPath('descendant-or-self::base')->extract(array('href')); $baseHref = current($base); if (count($base) && !empty($baseHref)) { $this->uri = $baseHref; } } /** * Adds an XML content to the list of nodes. * * The libxml errors are disabled when the content is parsed. * * If you want to get parsing errors, be sure to enable * internal errors via libxml_use_internal_errors(true) * and then, get the errors via libxml_get_errors(). Be * sure to clear errors with libxml_clear_errors() afterward. * * @param string $content The XML content * @param string $charset The charset * * @api */ public function addXmlContent($content, $charset = 'UTF-8') { $current = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); $dom = new \DOMDocument('1.0', $charset); $dom->validateOnParse = true; // remove the default namespace to make XPath expressions simpler @$dom->loadXML(str_replace('xmlns', 'ns', $content), LIBXML_NONET); libxml_use_internal_errors($current); libxml_disable_entity_loader($disableEntities); $this->addDocument($dom); } /** * Adds a \DOMDocument to the list of nodes. * * @param \DOMDocument $dom A \DOMDocument instance * * @api */ public function addDocument(\DOMDocument $dom) { if ($dom->documentElement) { $this->addNode($dom->documentElement); } } /** * Adds a \DOMNodeList to the list of nodes. * * @param \DOMNodeList $nodes A \DOMNodeList instance * * @api */ public function addNodeList(\DOMNodeList $nodes) { foreach ($nodes as $node) { $this->addNode($node); } } /** * Adds an array of \DOMNode instances to the list of nodes. * * @param \DOMNode[] $nodes An array of \DOMNode instances * * @api */ public function addNodes(array $nodes) { foreach ($nodes as $node) { $this->add($node); } } /** * Adds a \DOMNode instance to the list of nodes. * * @param \DOMNode $node A \DOMNode instance * * @api */ public function addNode(\DOMNode $node) { if ($node instanceof \DOMDocument) { $this->attach($node->documentElement); } else { $this->attach($node); } } /** * Returns a node given its position in the node list. * * @param integer $position The position * * @return Crawler A new instance of the Crawler with the selected node, or an empty Crawler if it does not exist. * * @api */ public function eq($position) { foreach ($this as $i => $node) { if ($i == $position) { return new static($node, $this->uri); } } return new static(null, $this->uri); } /** * Calls an anonymous function on each node of the list. * * The anonymous function receives the position and the node as arguments. * * Example: * * $crawler->filter('h1')->each(function ($node, $i) * { * return $node->nodeValue; * }); * * @param \Closure $closure An anonymous function * * @return array An array of values returned by the anonymous function * * @api */ public function each(\Closure $closure) { $data = array(); foreach ($this as $i => $node) { $data[] = $closure($node, $i); } return $data; } /** * Reduces the list of nodes by calling an anonymous function. * * To remove a node from the list, the anonymous function must return false. * * @param \Closure $closure An anonymous function * * @return Crawler A Crawler instance with the selected nodes. * * @api */ public function reduce(\Closure $closure) { $nodes = array(); foreach ($this as $i => $node) { if (false !== $closure($node, $i)) { $nodes[] = $node; } } return new static($nodes, $this->uri); } /** * Returns the first node of the current selection * * @return Crawler A Crawler instance with the first selected node * * @api */ public function first() { return $this->eq(0); } /** * Returns the last node of the current selection * * @return Crawler A Crawler instance with the last selected node * * @api */ public function last() { return $this->eq(count($this) - 1); } /** * Returns the siblings nodes of the current selection * * @return Crawler A Crawler instance with the sibling nodes * * @throws \InvalidArgumentException When current node is empty * * @api */ public function siblings() { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } return new static($this->sibling($this->getNode(0)->parentNode->firstChild), $this->uri); } /** * Returns the next siblings nodes of the current selection * * @return Crawler A Crawler instance with the next sibling nodes * * @throws \InvalidArgumentException When current node is empty * * @api */ public function nextAll() { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } return new static($this->sibling($this->getNode(0)), $this->uri); } /** * Returns the previous sibling nodes of the current selection * * @return Crawler A Crawler instance with the previous sibling nodes * * @throws \InvalidArgumentException * * @api */ public function previousAll() { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } return new static($this->sibling($this->getNode(0), 'previousSibling'), $this->uri); } /** * Returns the parents nodes of the current selection * * @return Crawler A Crawler instance with the parents nodes of the current selection * * @throws \InvalidArgumentException When current node is empty * * @api */ public function parents() { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); $nodes = array(); while ($node = $node->parentNode) { if (1 === $node->nodeType && '_root' !== $node->nodeName) { $nodes[] = $node; } } return new static($nodes, $this->uri); } /** * Returns the children nodes of the current selection * * @return Crawler A Crawler instance with the children nodes * * @throws \InvalidArgumentException When current node is empty * * @api */ public function children() { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } return new static($this->sibling($this->getNode(0)->firstChild), $this->uri); } /** * Returns the attribute value of the first node of the list. * * @param string $attribute The attribute name * * @return string The attribute value * * @throws \InvalidArgumentException When current node is empty * * @api */ public function attr($attribute) { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } return $this->getNode(0)->getAttribute($attribute); } /** * Returns the node value of the first node of the list. * * @return string The node value * * @throws \InvalidArgumentException When current node is empty * * @api */ public function text() { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } return $this->getNode(0)->nodeValue; } /** * Extracts information from the list of nodes. * * You can extract attributes or/and the node value (_text). * * Example: * * $crawler->filter('h1 a')->extract(array('_text', 'href')); * * @param array $attributes An array of attributes * * @return array An array of extracted values * * @api */ public function extract($attributes) { $attributes = (array) $attributes; $data = array(); foreach ($this as $node) { $elements = array(); foreach ($attributes as $attribute) { if ('_text' === $attribute) { $elements[] = $node->nodeValue; } else { $elements[] = $node->getAttribute($attribute); } } $data[] = count($attributes) > 1 ? $elements : $elements[0]; } return $data; } /** * Filters the list of nodes with an XPath expression. * * @param string $xpath An XPath expression * * @return Crawler A new instance of Crawler with the filtered list of nodes * * @api */ public function filterXPath($xpath) { $document = new \DOMDocument('1.0', 'UTF-8'); $root = $document->appendChild($document->createElement('_root')); foreach ($this as $node) { $root->appendChild($document->importNode($node, true)); } $domxpath = new \DOMXPath($document); return new static($domxpath->query($xpath), $this->uri); } /** * Filters the list of nodes with a CSS selector. * * This method only works if you have installed the CssSelector Symfony Component. * * @param string $selector A CSS selector * * @return Crawler A new instance of Crawler with the filtered list of nodes * * @throws \RuntimeException if the CssSelector Component is not available * * @api */ public function filter($selector) { if (!class_exists('Symfony\\Component\\CssSelector\\CssSelector')) { // @codeCoverageIgnoreStart throw new \RuntimeException('Unable to filter with a CSS selector as the Symfony CssSelector is not installed (you can use filterXPath instead).'); // @codeCoverageIgnoreEnd } return $this->filterXPath(CssSelector::toXPath($selector)); } /** * Selects links by name or alt value for clickable images. * * @param string $value The link text * * @return Crawler A new instance of Crawler with the filtered list of nodes * * @api */ public function selectLink($value) { $xpath = sprintf('//a[contains(concat(\' \', normalize-space(string(.)), \' \'), %s)] ', static::xpathLiteral(' '.$value.' ')). sprintf('| //a/img[contains(concat(\' \', normalize-space(string(@alt)), \' \'), %s)]/ancestor::a', static::xpathLiteral(' '.$value.' ')); return $this->filterXPath($xpath); } /** * Selects a button by name or alt value for images. * * @param string $value The button text * * @return Crawler A new instance of Crawler with the filtered list of nodes * * @api */ public function selectButton($value) { $xpath = sprintf('//input[((@type="submit" or @type="button") and contains(concat(\' \', normalize-space(string(@value)), \' \'), %s)) ', static::xpathLiteral(' '.$value.' ')). sprintf('or (@type="image" and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %s)) or @id="%s" or @name="%s"] ', static::xpathLiteral(' '.$value.' '), $value, $value). sprintf('| //button[contains(concat(\' \', normalize-space(string(.)), \' \'), %s) or @id="%s" or @name="%s"]', static::xpathLiteral(' '.$value.' '), $value, $value); return $this->filterXPath($xpath); } /** * Returns a Link object for the first node in the list. * * @param string $method The method for the link (get by default) * * @return Link A Link instance * * @throws \InvalidArgumentException If the current node list is empty * * @api */ public function link($method = 'get') { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } $node = $this->getNode(0); return new Link($node, $this->uri, $method); } /** * Returns an array of Link objects for the nodes in the list. * * @return Link[] An array of Link instances * * @api */ public function links() { $links = array(); foreach ($this as $node) { $links[] = new Link($node, $this->uri, 'get'); } return $links; } /** * Returns a Form object for the first node in the list. * * @param array $values An array of values for the form fields * @param string $method The method for the form * * @return Form A Form instance * * @throws \InvalidArgumentException If the current node list is empty * * @api */ public function form(array $values = null, $method = null) { if (!count($this)) { throw new \InvalidArgumentException('The current node list is empty.'); } $form = new Form($this->getNode(0), $this->uri, $method); if (null !== $values) { $form->setValues($values); } return $form; } /** * Converts string for XPath expressions. * * Escaped characters are: quotes (") and apostrophe ('). * * Examples: * * echo Crawler::xpathLiteral('foo " bar'); * //prints 'foo " bar' * * echo Crawler::xpathLiteral("foo ' bar"); * //prints "foo ' bar" * * echo Crawler::xpathLiteral('a\'b"c'); * //prints concat('a', "'", 'b"c') * * * @param string $s String to be escaped * * @return string Converted string * */ public static function xpathLiteral($s) { if (false === strpos($s, "'")) { return sprintf("'%s'", $s); } if (false === strpos($s, '"')) { return sprintf('"%s"', $s); } $string = $s; $parts = array(); while (true) { if (false !== $pos = strpos($string, "'")) { $parts[] = sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { $parts[] = "'$string'"; break; } } return sprintf("concat(%s)", implode($parts, ', ')); } private function getNode($position) { foreach ($this as $i => $node) { if ($i == $position) { return $node; } // @codeCoverageIgnoreStart } return null; // @codeCoverageIgnoreEnd } private function sibling($node, $siblingDir = 'nextSibling') { $nodes = array(); do { if ($node !== $this->getNode(0) && $node->nodeType === 1) { $nodes[] = $node; } } while ($node = $node->$siblingDir); return $nodes; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * ChoiceFormField represents a choice form field. * * It is constructed from a HTML select tag, or a HTML checkbox, or radio inputs. * * @author Fabien Potencier * * @api */ class ChoiceFormField extends FormField { /** * @var string */ private $type; /** * @var Boolean */ private $multiple; /** * @var array */ private $options; /** * Returns true if the field should be included in the submitted values. * * @return Boolean true if the field should be included in the submitted values, false otherwise */ public function hasValue() { // don't send a value for unchecked checkboxes if (in_array($this->type, array('checkbox', 'radio')) && null === $this->value) { return false; } return true; } /** * Check if the current selected option is disabled * * @return Boolean */ public function isDisabled() { foreach ($this->options as $option) { if ($option['value'] == $this->value && $option['disabled']) { return true; } } return false; } /** * Sets the value of the field. * * @param string $value The value of the field * * @api */ public function select($value) { $this->setValue($value); } /** * Ticks a checkbox. * * @throws \LogicException When the type provided is not correct * * @api */ public function tick() { if ('checkbox' !== $this->type) { throw new \LogicException(sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->name, $this->type)); } $this->setValue(true); } /** * Ticks a checkbox. * * @throws \LogicException When the type provided is not correct * * @api */ public function untick() { if ('checkbox' !== $this->type) { throw new \LogicException(sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->name, $this->type)); } $this->setValue(false); } /** * Sets the value of the field. * * @param string $value The value of the field * * @throws \InvalidArgumentException When value type provided is not correct */ public function setValue($value) { if ('checkbox' == $this->type && false === $value) { // uncheck $this->value = null; } elseif ('checkbox' == $this->type && true === $value) { // check $this->value = $this->options[0]['value']; } else { if (is_array($value)) { if (!$this->multiple) { throw new \InvalidArgumentException(sprintf('The value for "%s" cannot be an array.', $this->name)); } foreach ($value as $v) { if (!$this->containsOption($v, $this->options)) { throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: %s).', $this->name, $v, implode(', ', $this->availableOptionValues()))); } } } elseif (!$this->containsOption($value, $this->options)) { throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: %s).', $this->name, $value, implode(', ', $this->availableOptionValues()))); } if ($this->multiple) { $value = (array) $value; } if (is_array($value)) { $this->value = $value; } else { parent::setValue($value); } } } /** * Adds a choice to the current ones. * * This method should only be used internally. * * @param \DOMNode $node A \DOMNode * * @throws \LogicException When choice provided is not multiple nor radio */ public function addChoice(\DOMNode $node) { if (!$this->multiple && 'radio' != $this->type) { throw new \LogicException(sprintf('Unable to add a choice for "%s" as it is not multiple or is not a radio button.', $this->name)); } $option = $this->buildOptionValue($node); $this->options[] = $option; if ($node->getAttribute('checked')) { $this->value = $option['value']; } } /** * Returns the type of the choice field (radio, select, or checkbox). * * @return string The type */ public function getType() { return $this->type; } /** * Returns true if the field accepts multiple values. * * @return Boolean true if the field accepts multiple values, false otherwise */ public function isMultiple() { return $this->multiple; } /** * Initializes the form field. * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('input' != $this->node->nodeName && 'select' != $this->node->nodeName) { throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input or select tag (%s given).', $this->node->nodeName)); } if ('input' == $this->node->nodeName && 'checkbox' != $this->node->getAttribute('type') && 'radio' != $this->node->getAttribute('type')) { throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input tag with a type of checkbox or radio (given type is %s).', $this->node->getAttribute('type'))); } $this->value = null; $this->options = array(); $this->multiple = false; if ('input' == $this->node->nodeName) { $this->type = $this->node->getAttribute('type'); $optionValue = $this->buildOptionValue($this->node); $this->options[] = $optionValue; if ($this->node->getAttribute('checked')) { $this->value = $optionValue['value']; } } else { $this->type = 'select'; if ($this->node->hasAttribute('multiple')) { $this->multiple = true; $this->value = array(); $this->name = str_replace('[]', '', $this->name); } $found = false; foreach ($this->xpath->query('descendant::option', $this->node) as $option) { $this->options[] = $this->buildOptionValue($option); if ($option->getAttribute('selected')) { $found = true; if ($this->multiple) { $this->value[] = $option->getAttribute('value'); } else { $this->value = $option->getAttribute('value'); } } } // if no option is selected and if it is a simple select box, take the first option as the value $option = $this->xpath->query('descendant::option', $this->node)->item(0); if (!$found && !$this->multiple && $option instanceof \DOMElement) { $this->value = $option->getAttribute('value'); } } } /** * Returns option value with associated disabled flag * * @param \DOMNode $node * * @return array */ private function buildOptionValue($node) { $option = array(); $defaultValue = (isset($node->nodeValue) && !empty($node->nodeValue)) ? $node->nodeValue : '1'; $option['value'] = $node->hasAttribute('value') ? $node->getAttribute('value') : $defaultValue; $option['disabled'] = ($node->hasAttribute('disabled') && $node->getAttribute('disabled') == 'disabled'); return $option; } /** * Checks whether given vale is in the existing options * * @param string $optionValue * @param array $options * * @return bool */ public function containsOption($optionValue, $options) { foreach ($options as $option) { if ($option['value'] == $optionValue) { return true; } } return false; } /** * Returns list of available field options * * @return array */ public function availableOptionValues() { $values = array(); foreach ($this->options as $option) { $values[] = $option['value']; } return $values; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * FileFormField represents a file form field (an HTML file input tag). * * @author Fabien Potencier * * @api */ class FileFormField extends FormField { /** * Sets the PHP error code associated with the field. * * @param integer $error The error code (one of UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, or UPLOAD_ERR_EXTENSION) * * @throws \InvalidArgumentException When error code doesn't exist */ public function setErrorCode($error) { $codes = array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION); if (!in_array($error, $codes)) { throw new \InvalidArgumentException(sprintf('The error code %s is not valid.', $error)); } $this->value = array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => $error, 'size' => 0); } /** * Sets the value of the field. * * @param string $value The value of the field * * @api */ public function upload($value) { $this->setValue($value); } /** * Sets the value of the field. * * @param string $value The value of the field */ public function setValue($value) { if (null !== $value && is_readable($value)) { $error = UPLOAD_ERR_OK; $size = filesize($value); $name = basename($value); // copy to a tmp location $tmp = tempnam(sys_get_temp_dir(), 'upload'); unlink($tmp); copy($value, $tmp); $value = $tmp; } else { $error = UPLOAD_ERR_NO_FILE; $size = 0; $name = ''; $value = ''; } $this->value = array('name' => $name, 'type' => '', 'tmp_name' => $value, 'error' => $error, 'size' => $size); } /** * Sets path to the file as string for simulating HTTP request * * @param string $path The path to the file */ public function setFilePath($path) { parent::setValue($path); } /** * Initializes the form field. * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('input' != $this->node->nodeName) { throw new \LogicException(sprintf('A FileFormField can only be created from an input tag (%s given).', $this->node->nodeName)); } if ('file' != $this->node->getAttribute('type')) { throw new \LogicException(sprintf('A FileFormField can only be created from an input tag with a type of file (given type is %s).', $this->node->getAttribute('type'))); } $this->setValue(null); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * FormField is the abstract class for all form fields. * * @author Fabien Potencier */ abstract class FormField { /** * @var \DOMNode */ protected $node; /** * @var string */ protected $name; /** * @var string */ protected $value; /** * @var \DOMDocument */ protected $document; /** * @var \DOMXPath */ protected $xpath; /** * @var Boolean */ protected $disabled; /** * Constructor. * * @param \DOMNode $node The node associated with this field */ public function __construct(\DOMNode $node) { $this->node = $node; $this->name = $node->getAttribute('name'); $this->document = new \DOMDocument('1.0', 'UTF-8'); $this->node = $this->document->importNode($this->node, true); $root = $this->document->appendChild($this->document->createElement('_root')); $root->appendChild($this->node); $this->xpath = new \DOMXPath($this->document); $this->initialize(); } /** * Returns the name of the field. * * @return string The name of the field */ public function getName() { return $this->name; } /** * Gets the value of the field. * * @return string|array The value of the field */ public function getValue() { return $this->value; } /** * Sets the value of the field. * * @param string $value The value of the field * * @api */ public function setValue($value) { $this->value = (string) $value; } /** * Returns true if the field should be included in the submitted values. * * @return Boolean true if the field should be included in the submitted values, false otherwise */ public function hasValue() { return true; } /** * Check if the current field is disabled * * @return Boolean */ public function isDisabled() { return $this->node->hasAttribute('disabled'); } /** * Initializes the form field. */ abstract protected function initialize(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * InputFormField represents an input form field (an HTML input tag). * * For inputs with type of file, checkbox, or radio, there are other more * specialized classes (cf. FileFormField and ChoiceFormField). * * @author Fabien Potencier * * @api */ class InputFormField extends FormField { /** * Initializes the form field. * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('input' != $this->node->nodeName && 'button' != $this->node->nodeName) { throw new \LogicException(sprintf('An InputFormField can only be created from an input or button tag (%s given).', $this->node->nodeName)); } if ('checkbox' == $this->node->getAttribute('type')) { throw new \LogicException('Checkboxes should be instances of ChoiceFormField.'); } if ('file' == $this->node->getAttribute('type')) { throw new \LogicException('File inputs should be instances of FileFormField.'); } $this->value = $this->node->getAttribute('value'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Field; /** * TextareaFormField represents a textarea form field (an HTML textarea tag). * * @author Fabien Potencier * * @api */ class TextareaFormField extends FormField { /** * Initializes the form field. * * @throws \LogicException When node type is incorrect */ protected function initialize() { if ('textarea' != $this->node->nodeName) { throw new \LogicException(sprintf('A TextareaFormField can only be created from a textarea tag (%s given).', $this->node->nodeName)); } $this->value = null; foreach ($this->node->childNodes as $node) { $this->value .= $this->document->saveXML($node); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; use Symfony\Component\DomCrawler\Field\FormField; /** * Form represents an HTML form. * * @author Fabien Potencier * * @api */ class Form extends Link implements \ArrayAccess { /** * @var \DOMNode */ private $button; /** * @var Field\FormField[] */ private $fields; /** * Constructor. * * @param \DOMNode $node A \DOMNode instance * @param string $currentUri The URI of the page where the form is embedded * @param string $method The method to use for the link (if null, it defaults to the method defined by the form) * * @throws \LogicException if the node is not a button inside a form tag * * @api */ public function __construct(\DOMNode $node, $currentUri, $method = null) { parent::__construct($node, $currentUri, $method); $this->initialize(); } /** * Gets the form node associated with this form. * * @return \DOMNode A \DOMNode instance */ public function getFormNode() { return $this->node; } /** * Sets the value of the fields. * * @param array $values An array of field values * * @return Form * * @api */ public function setValues(array $values) { foreach ($values as $name => $value) { $this->fields->set($name, $value); } return $this; } /** * Gets the field values. * * The returned array does not include file fields (@see getFiles). * * @return array An array of field values. * * @api */ public function getValues() { $values = array(); foreach ($this->fields->all() as $name => $field) { if ($field->isDisabled()) { continue; } if (!$field instanceof Field\FileFormField && $field->hasValue()) { $values[$name] = $field->getValue(); } } return $values; } /** * Gets the file field values. * * @return array An array of file field values. * * @api */ public function getFiles() { if (!in_array($this->getMethod(), array('POST', 'PUT', 'DELETE', 'PATCH'))) { return array(); } $files = array(); foreach ($this->fields->all() as $name => $field) { if ($field->isDisabled()) { continue; } if ($field instanceof Field\FileFormField) { $files[$name] = $field->getValue(); } } return $files; } /** * Gets the field values as PHP. * * This method converts fields with the array notation * (like foo[bar] to arrays) like PHP does. * * @return array An array of field values. * * @api */ public function getPhpValues() { $qs = http_build_query($this->getValues(), '', '&'); parse_str($qs, $values); return $values; } /** * Gets the file field values as PHP. * * This method converts fields with the array notation * (like foo[bar] to arrays) like PHP does. * * @return array An array of field values. * * @api */ public function getPhpFiles() { $qs = http_build_query($this->getFiles(), '', '&'); parse_str($qs, $values); return $values; } /** * Gets the URI of the form. * * The returned URI is not the same as the form "action" attribute. * This method merges the value if the method is GET to mimics * browser behavior. * * @return string The URI * * @api */ public function getUri() { $uri = parent::getUri(); if (!in_array($this->getMethod(), array('POST', 'PUT', 'DELETE', 'PATCH')) && $queryString = http_build_query($this->getValues(), null, '&')) { $sep = false === strpos($uri, '?') ? '?' : '&'; $uri .= $sep.$queryString; } return $uri; } protected function getRawUri() { return $this->node->getAttribute('action'); } /** * Gets the form method. * * If no method is defined in the form, GET is returned. * * @return string The method * * @api */ public function getMethod() { if (null !== $this->method) { return $this->method; } return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET'; } /** * Returns true if the named field exists. * * @param string $name The field name * * @return Boolean true if the field exists, false otherwise * * @api */ public function has($name) { return $this->fields->has($name); } /** * Removes a field from the form. * * @param string $name The field name * * @throws \InvalidArgumentException when the name is malformed * * @api */ public function remove($name) { $this->fields->remove($name); } /** * Gets a named field. * * @param string $name The field name * * @return FormField The field instance * * @throws \InvalidArgumentException When field is not present in this form * * @api */ public function get($name) { return $this->fields->get($name); } /** * Sets a named field. * * @param FormField $field The field * * @api */ public function set(FormField $field) { $this->fields->add($field); } /** * Gets all fields. * * @return FormField[] An array of fields * * @api */ public function all() { return $this->fields->all(); } /** * Returns true if the named field exists. * * @param string $name The field name * * @return Boolean true if the field exists, false otherwise */ public function offsetExists($name) { return $this->has($name); } /** * Gets the value of a field. * * @param string $name The field name * * @return FormField The associated Field instance * * @throws \InvalidArgumentException if the field does not exist */ public function offsetGet($name) { return $this->fields->get($name); } /** * Sets the value of a field. * * @param string $name The field name * @param string|array $value The value of the field * * @throws \InvalidArgumentException if the field does not exist */ public function offsetSet($name, $value) { $this->fields->set($name, $value); } /** * Removes a field from the form. * * @param string $name The field name */ public function offsetUnset($name) { $this->fields->remove($name); } /** * Sets current \DOMNode instance. * * @param \DOMNode $node A \DOMNode instance * * @throws \LogicException If given node is not a button or input or does not have a form ancestor */ protected function setNode(\DOMNode $node) { $this->button = $node; if ('button' == $node->nodeName || ('input' == $node->nodeName && in_array($node->getAttribute('type'), array('submit', 'button', 'image')))) { do { // use the ancestor form element if (null === $node = $node->parentNode) { throw new \LogicException('The selected node does not have a form ancestor.'); } } while ('form' != $node->nodeName); } elseif ('form' != $node->nodeName) { throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName)); } $this->node = $node; } private function initialize() { $this->fields = new FormFieldRegistry(); $document = new \DOMDocument('1.0', 'UTF-8'); $node = $document->importNode($this->node, true); $button = $document->importNode($this->button, true); $root = $document->appendChild($document->createElement('_root')); $root->appendChild($node); $root->appendChild($button); $xpath = new \DOMXPath($document); foreach ($xpath->query('descendant::input | descendant::button | descendant::textarea | descendant::select', $root) as $node) { if (!$node->hasAttribute('name') || !$node->getAttribute('name')) { continue; } $nodeName = $node->nodeName; if ($node === $button) { $this->set(new Field\InputFormField($node)); } elseif ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == $node->getAttribute('type')) { $this->set(new Field\ChoiceFormField($node)); } elseif ('input' == $nodeName && 'radio' == $node->getAttribute('type')) { if ($this->has($node->getAttribute('name'))) { $this->get($node->getAttribute('name'))->addChoice($node); } else { $this->set(new Field\ChoiceFormField($node)); } } elseif ('input' == $nodeName && 'file' == $node->getAttribute('type')) { $this->set(new Field\FileFormField($node)); } elseif ('input' == $nodeName && !in_array($node->getAttribute('type'), array('submit', 'button', 'image'))) { $this->set(new Field\InputFormField($node)); } elseif ('textarea' == $nodeName) { $this->set(new Field\TextareaFormField($node)); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; use Symfony\Component\DomCrawler\Field\FormField; /** * This is an internal class that must not be used directly. */ class FormFieldRegistry { private $fields = array(); private $base; /** * Adds a field to the registry. * * @param FormField $field The field * * @throws \InvalidArgumentException when the name is malformed */ public function add(FormField $field) { $segments = $this->getSegments($field->getName()); $target =& $this->fields; while ($segments) { if (!is_array($target)) { $target = array(); } $path = array_shift($segments); if ('' === $path) { $target =& $target[]; } else { $target =& $target[$path]; } } $target = $field; } /** * Removes a field and its children from the registry. * * @param string $name The fully qualified name of the base field * * @throws \InvalidArgumentException when the name is malformed */ public function remove($name) { $segments = $this->getSegments($name); $target =& $this->fields; while (count($segments) > 1) { $path = array_shift($segments); if (!array_key_exists($path, $target)) { return; } $target =& $target[$path]; } unset($target[array_shift($segments)]); } /** * Returns the value of the field and its children. * * @param string $name The fully qualified name of the field * * @return mixed The value of the field * * @throws \InvalidArgumentException when the name is malformed * @throws \InvalidArgumentException if the field does not exist */ public function &get($name) { $segments = $this->getSegments($name); $target =& $this->fields; while ($segments) { $path = array_shift($segments); if (!array_key_exists($path, $target)) { throw new \InvalidArgumentException(sprintf('Unreachable field "%s"', $path)); } $target =& $target[$path]; } return $target; } /** * Tests whether the form has the given field. * * @param string $name The fully qualified name of the field * * @return Boolean Whether the form has the given field */ public function has($name) { try { $this->get($name); return true; } catch (\InvalidArgumentException $e) { return false; } } /** * Set the value of a field and its children. * * @param string $name The fully qualified name of the field * @param mixed $value The value * * @throws \InvalidArgumentException when the name is malformed * @throws \InvalidArgumentException if the field does not exist */ public function set($name, $value) { $target =& $this->get($name); if (!is_array($value) || $target instanceof Field\ChoiceFormField) { $target->setValue($value); } else { $fields = self::create($name, $value); foreach ($fields->all() as $k => $v) { $this->set($k, $v); } } } /** * Returns the list of field with their value. * * @return array The list of fields as array((string) Fully qualified name => (mixed) value) */ public function all() { return $this->walk($this->fields, $this->base); } /** * Creates an instance of the class. * * This function is made private because it allows overriding the $base and * the $values properties without any type checking. * * @param string $base The fully qualified name of the base field * @param array $values The values of the fields * * @return FormFieldRegistry */ private static function create($base, array $values) { $registry = new static(); $registry->base = $base; $registry->fields = $values; return $registry; } /** * Transforms a PHP array in a list of fully qualified name / value. * * @param array $array The PHP array * @param string $base The name of the base field * @param array $output The initial values * * @return array The list of fields as array((string) Fully qualified name => (mixed) value) */ private function walk(array $array, $base = '', array &$output = array()) { foreach ($array as $k => $v) { $path = empty($base) ? $k : sprintf("%s[%s]", $base, $k); if (is_array($v)) { $this->walk($v, $path, $output); } else { $output[$path] = $v; } } return $output; } /** * Splits a field name into segments as a web browser would do. * * * getSegments('base[foo][3][]') = array('base', 'foo, '3', ''); * * * @param string $name The name of the field * * @return array The list of segments * * @throws \InvalidArgumentException when the name is malformed */ private function getSegments($name) { if (preg_match('/^(?P[^[]+)(?P(\[.*)|$)/', $name, $m)) { $segments = array($m['base']); while (preg_match('/^\[(?P.*?)\](?P.*)$/', $m['extra'], $m)) { $segments[] = $m['segment']; } return $segments; } throw new \InvalidArgumentException(sprintf('Malformed field path "%s"', $name)); } } Copyright (c) 2004-2013 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler; /** * Link represents an HTML link (an HTML a tag). * * @author Fabien Potencier * * @api */ class Link { /** * @var \DOMNode A \DOMNode instance */ protected $node; /** * @var string The method to use for the link */ protected $method; /** * @var string The URI of the page where the link is embedded (or the base href) */ protected $currentUri; /** * Constructor. * * @param \DOMNode $node A \DOMNode instance * @param string $currentUri The URI of the page where the link is embedded (or the base href) * @param string $method The method to use for the link (get by default) * * @throws \InvalidArgumentException if the node is not a link * * @api */ public function __construct(\DOMNode $node, $currentUri, $method = 'GET') { if (!in_array(strtolower(substr($currentUri, 0, 4)), array('http', 'file'))) { throw new \InvalidArgumentException(sprintf('Current URI must be an absolute URL ("%s").', $currentUri)); } $this->setNode($node); $this->method = $method ? strtoupper($method) : null; $this->currentUri = $currentUri; } /** * Gets the node associated with this link. * * @return \DOMNode A \DOMNode instance */ public function getNode() { return $this->node; } /** * Gets the method associated with this link. * * @return string The method * * @api */ public function getMethod() { return $this->method; } /** * Gets the URI associated with this link. * * @return string The URI * * @api */ public function getUri() { $uri = trim($this->getRawUri()); // absolute URL? if (null !== parse_url($uri, PHP_URL_SCHEME)) { return $uri; } // empty URI if (!$uri) { return $this->currentUri; } // only an anchor if ('#' === $uri[0]) { $baseUri = $this->currentUri; if (false !== $pos = strpos($baseUri, '#')) { $baseUri = substr($baseUri, 0, $pos); } return $baseUri.$uri; } // only a query string if ('?' === $uri[0]) { $baseUri = $this->currentUri; // remove the query string from the current uri if (false !== $pos = strpos($baseUri, '?')) { $baseUri = substr($baseUri, 0, $pos); } return $baseUri.$uri; } // absolute path if ('/' === $uri[0]) { return preg_replace('#^(.*?//[^/]+)(?:\/.*)?$#', '$1', $this->currentUri).$uri; } // relative path return substr($this->currentUri, 0, strrpos($this->currentUri, '/') + 1).$uri; } /** * Returns raw uri data. * * @return string */ protected function getRawUri() { return $this->node->getAttribute('href'); } /** * Sets current \DOMNode instance. * * @param \DOMNode $node A \DOMNode instance * * @throws \LogicException If given node is not an anchor */ protected function setNode(\DOMNode $node) { if ('a' != $node->nodeName) { throw new \LogicException(sprintf('Unable to click on a "%s" tag.', $node->nodeName)); } $this->node = $node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Tests; use Symfony\Component\DomCrawler\Crawler; class CrawlerTest extends \PHPUnit_Framework_TestCase { public function testConstructor() { $crawler = new Crawler(); $this->assertCount(0, $crawler, '__construct() returns an empty crawler'); $crawler = new Crawler(new \DOMNode()); $this->assertCount(1, $crawler, '__construct() takes a node as a first argument'); } /** * @covers Symfony\Component\DomCrawler\Crawler::add */ public function testAdd() { $crawler = new Crawler(); $crawler->add($this->createDomDocument()); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->add() adds nodes from a \DOMDocument'); $crawler = new Crawler(); $crawler->add($this->createNodeList()); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->add() adds nodes from a \DOMNodeList'); foreach ($this->createNodeList() as $node) { $list[] = $node; } $crawler = new Crawler(); $crawler->add($list); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->add() adds nodes from an array of nodes'); $crawler = new Crawler(); $crawler->add($this->createNodeList()->item(0)); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->add() adds nodes from an \DOMNode'); $crawler = new Crawler(); $crawler->add('Foo'); $this->assertEquals('Foo', $crawler->filterXPath('//body')->text(), '->add() adds nodes from a string'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addHtmlContent */ public function testAddHtmlContent() { $crawler = new Crawler(); $crawler->addHtmlContent('
', 'UTF-8'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addHtmlContent() adds nodes from an HTML string'); $crawler->addHtmlContent('', 'UTF-8'); $this->assertEquals('http://symfony.com', $crawler->filterXPath('//base')->attr('href'), '->addHtmlContent() adds nodes from an HTML string'); $this->assertEquals('http://symfony.com/contact', $crawler->filterXPath('//a')->link()->getUri(), '->addHtmlContent() adds nodes from an HTML string'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addHtmlContent */ public function testAddHtmlContentCharset() { $crawler = new Crawler(); $crawler->addHtmlContent('
Tiếng Việt', 'UTF-8'); $this->assertEquals('Tiếng Việt', $crawler->filterXPath('//div')->text()); } /** * @covers Symfony\Component\DomCrawler\Crawler::addHtmlContent */ public function testAddHtmlContentInvalidBaseTag() { $crawler = new Crawler(null, 'http://symfony.com'); $crawler->addHtmlContent('', 'UTF-8'); $this->assertEquals('http://symfony.com/contact', current($crawler->filterXPath('//a')->links())->getUri(), '->addHtmlContent() correctly handles a non-existent base tag href attribute'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addHtmlContent */ public function testAddHtmlContentUnsupportedCharset() { $crawler = new Crawler(); $crawler->addHtmlContent(file_get_contents(__DIR__.'/Fixtures/windows-1250.html'), 'Windows-1250'); $this->assertEquals('ŽťÄýů', $crawler->filterXPath('//p')->text()); } /** * @covers Symfony\Component\DomCrawler\Crawler::addHtmlContent */ public function testAddHtmlContentWithErrors() { libxml_use_internal_errors(true); $crawler = new Crawler(); $crawler->addHtmlContent(<< EOF , 'UTF-8'); $errors = libxml_get_errors(); $this->assertCount(1, $errors); $this->assertEquals("Tag nav invalid\n", $errors[0]->message); libxml_clear_errors(); libxml_use_internal_errors(false); } /** * @covers Symfony\Component\DomCrawler\Crawler::addXmlContent */ public function testAddXmlContent() { $crawler = new Crawler(); $crawler->addXmlContent('
', 'UTF-8'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addXmlContent() adds nodes from an XML string'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addXmlContent */ public function testAddXmlContentCharset() { $crawler = new Crawler(); $crawler->addXmlContent('
Tiếng Việt
', 'UTF-8'); $this->assertEquals('Tiếng Việt', $crawler->filterXPath('//div')->text()); } /** * @covers Symfony\Component\DomCrawler\Crawler::addXmlContent */ public function testAddXmlContentWithErrors() { libxml_use_internal_errors(true); $crawler = new Crawler(); $crawler->addXmlContent(<<
EOF , 'UTF-8'); $this->assertTrue(count(libxml_get_errors()) > 1); libxml_clear_errors(); libxml_use_internal_errors(false); } /** * @covers Symfony\Component\DomCrawler\Crawler::addContent */ public function testAddContent() { $crawler = new Crawler(); $crawler->addContent('
', 'text/html; charset=UTF-8'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addContent() adds nodes from an HTML string'); $crawler = new Crawler(); $crawler->addContent('
', 'text/html; charset=UTF-8; dir=RTL'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addContent() adds nodes from an HTML string with extended content type'); $crawler = new Crawler(); $crawler->addContent('
'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addContent() uses text/html as the default type'); $crawler = new Crawler(); $crawler->addContent('
', 'text/xml; charset=UTF-8'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addContent() adds nodes from an XML string'); $crawler = new Crawler(); $crawler->addContent('
', 'text/xml'); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addContent() adds nodes from an XML string'); $crawler = new Crawler(); $crawler->addContent('foo bar', 'text/plain'); $this->assertCount(0, $crawler, '->addContent() does nothing if the type is not (x|ht)ml'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addDocument */ public function testAddDocument() { $crawler = new Crawler(); $crawler->addDocument($this->createDomDocument()); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addDocument() adds nodes from a \DOMDocument'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addNodeList */ public function testAddNodeList() { $crawler = new Crawler(); $crawler->addNodeList($this->createNodeList()); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addNodeList() adds nodes from a \DOMNodeList'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addNodes */ public function testAddNodes() { foreach ($this->createNodeList() as $node) { $list[] = $node; } $crawler = new Crawler(); $crawler->addNodes($list); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addNodes() adds nodes from an array of nodes'); } /** * @covers Symfony\Component\DomCrawler\Crawler::addNode */ public function testAddNode() { $crawler = new Crawler(); $crawler->addNode($this->createNodeList()->item(0)); $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addNode() adds nodes from an \DOMNode'); } public function testClear() { $crawler = new Crawler(new \DOMNode()); $crawler->clear(); $this->assertCount(0, $crawler, '->clear() removes all the nodes from the crawler'); } public function testEq() { $crawler = $this->createTestCrawler()->filterXPath('//li'); $this->assertNotSame($crawler, $crawler->eq(0), '->eq() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->eq() returns a new instance of a crawler'); $this->assertEquals('Two', $crawler->eq(1)->text(), '->eq() returns the nth node of the list'); $this->assertCount(0, $crawler->eq(100), '->eq() returns an empty crawler if the nth node does not exist'); } public function testEach() { $data = $this->createTestCrawler()->filterXPath('//ul[1]/li')->each(function ($node, $i) { return $i.'-'.$node->nodeValue; }); $this->assertEquals(array('0-One', '1-Two', '2-Three'), $data, '->each() executes an anonymous function on each node of the list'); } public function testReduce() { $crawler = $this->createTestCrawler()->filterXPath('//ul[1]/li'); $nodes = $crawler->reduce(function ($node, $i) { return $i == 1 ? false : true; }); $this->assertNotSame($nodes, $crawler, '->reduce() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $nodes, '->reduce() returns a new instance of a crawler'); $this->assertCount(2, $nodes, '->reduce() filters the nodes in the list'); } public function testAttr() { $this->assertEquals('first', $this->createTestCrawler()->filterXPath('//li')->attr('class'), '->attr() returns the attribute of the first element of the node list'); try { $this->createTestCrawler()->filterXPath('//ol')->attr('class'); $this->fail('->attr() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->attr() throws an \InvalidArgumentException if the node list is empty'); } } public function testText() { $this->assertEquals('One', $this->createTestCrawler()->filterXPath('//li')->text(), '->text() returns the node value of the first element of the node list'); try { $this->createTestCrawler()->filterXPath('//ol')->text(); $this->fail('->text() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->text() throws an \InvalidArgumentException if the node list is empty'); } } public function testExtract() { $crawler = $this->createTestCrawler()->filterXPath('//ul[1]/li'); $this->assertEquals(array('One', 'Two', 'Three'), $crawler->extract('_text'), '->extract() returns an array of extracted data from the node list'); $this->assertEquals(array(array('One', 'first'), array('Two', ''), array('Three', '')), $crawler->extract(array('_text', 'class')), '->extract() returns an array of extracted data from the node list'); $this->assertEquals(array(), $this->createTestCrawler()->filterXPath('//ol')->extract('_text'), '->extract() returns an empty array if the node list is empty'); } /** * @covers Symfony\Component\DomCrawler\Crawler::filterXPath */ public function testFilterXPath() { $crawler = $this->createTestCrawler(); $this->assertNotSame($crawler, $crawler->filterXPath('//li'), '->filterXPath() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->filterXPath() returns a new instance of a crawler'); $crawler = $this->createTestCrawler()->filterXPath('//ul'); $this->assertCount(6, $crawler->filterXPath('//li'), '->filterXPath() filters the node list with the XPath expression'); } /** * @covers Symfony\Component\DomCrawler\Crawler::filter */ public function testFilter() { if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { $this->markTestSkipped('The "CssSelector" component is not available'); } $crawler = $this->createTestCrawler(); $this->assertNotSame($crawler, $crawler->filter('li'), '->filter() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->filter() returns a new instance of a crawler'); $crawler = $this->createTestCrawler()->filter('ul'); $this->assertCount(6, $crawler->filter('li'), '->filter() filters the node list with the CSS selector'); } public function testSelectLink() { $crawler = $this->createTestCrawler(); $this->assertNotSame($crawler, $crawler->selectLink('Foo'), '->selectLink() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->selectLink() returns a new instance of a crawler'); $this->assertCount(1, $crawler->selectLink('Fabien\'s Foo'), '->selectLink() selects links by the node values'); $this->assertCount(1, $crawler->selectLink('Fabien\'s Bar'), '->selectLink() selects links by the alt attribute of a clickable image'); $this->assertCount(2, $crawler->selectLink('Fabien"s Foo'), '->selectLink() selects links by the node values'); $this->assertCount(2, $crawler->selectLink('Fabien"s Bar'), '->selectLink() selects links by the alt attribute of a clickable image'); $this->assertCount(1, $crawler->selectLink('\' Fabien"s Foo'), '->selectLink() selects links by the node values'); $this->assertCount(1, $crawler->selectLink('\' Fabien"s Bar'), '->selectLink() selects links by the alt attribute of a clickable image'); $this->assertCount(4, $crawler->selectLink('Foo'), '->selectLink() selects links by the node values'); $this->assertCount(4, $crawler->selectLink('Bar'), '->selectLink() selects links by the node values'); } public function testSelectButton() { $crawler = $this->createTestCrawler(); $this->assertNotSame($crawler, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->selectButton() returns a new instance of a crawler'); $this->assertEquals(1, $crawler->selectButton('FooValue')->count(), '->selectButton() selects buttons'); $this->assertEquals(1, $crawler->selectButton('FooName')->count(), '->selectButton() selects buttons'); $this->assertEquals(1, $crawler->selectButton('FooId')->count(), '->selectButton() selects buttons'); $this->assertEquals(1, $crawler->selectButton('BarValue')->count(), '->selectButton() selects buttons'); $this->assertEquals(1, $crawler->selectButton('BarName')->count(), '->selectButton() selects buttons'); $this->assertEquals(1, $crawler->selectButton('BarId')->count(), '->selectButton() selects buttons'); } public function testLink() { $crawler = $this->createTestCrawler('http://example.com/bar/')->selectLink('Foo'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Link', $crawler->link(), '->link() returns a Link instance'); $this->assertEquals('POST', $crawler->link('post')->getMethod(), '->link() takes a method as its argument'); $crawler = $this->createTestCrawler('http://example.com/bar')->selectLink('GetLink'); $this->assertEquals('http://example.com/bar?get=param', $crawler->link()->getUri(), '->link() returns a Link instance'); try { $this->createTestCrawler()->filterXPath('//ol')->link(); $this->fail('->link() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->link() throws an \InvalidArgumentException if the node list is empty'); } } public function testLinks() { $crawler = $this->createTestCrawler('http://example.com/bar/')->selectLink('Foo'); $this->assertInternalType('array', $crawler->links(), '->links() returns an array'); $this->assertCount(4, $crawler->links(), '->links() returns an array'); $links = $crawler->links(); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Link', $links[0], '->links() returns an array of Link instances'); $this->assertEquals(array(), $this->createTestCrawler()->filterXPath('//ol')->links(), '->links() returns an empty array if the node selection is empty'); } public function testForm() { $crawler = $this->createTestCrawler('http://example.com/bar/')->selectButton('FooValue'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Form', $crawler->form(), '->form() returns a Form instance'); $this->assertEquals(array('FooName' => 'FooBar'), $crawler->form(array('FooName' => 'FooBar'))->getValues(), '->form() takes an array of values to submit as its first argument'); try { $this->createTestCrawler()->filterXPath('//ol')->form(); $this->fail('->form() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->form() throws an \InvalidArgumentException if the node list is empty'); } } public function testLast() { $crawler = $this->createTestCrawler()->filterXPath('//ul[1]/li'); $this->assertNotSame($crawler, $crawler->last(), '->last() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->last() returns a new instance of a crawler'); $this->assertEquals('Three', $crawler->last()->text()); } public function testFirst() { $crawler = $this->createTestCrawler()->filterXPath('//li'); $this->assertNotSame($crawler, $crawler->first(), '->first() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->first() returns a new instance of a crawler'); $this->assertEquals('One', $crawler->first()->text()); } public function testSiblings() { $crawler = $this->createTestCrawler()->filterXPath('//li')->eq(1); $this->assertNotSame($crawler, $crawler->siblings(), '->siblings() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->siblings() returns a new instance of a crawler'); $nodes = $crawler->siblings(); $this->assertEquals(2, $nodes->count()); $this->assertEquals('One', $nodes->eq(0)->text()); $this->assertEquals('Three', $nodes->eq(1)->text()); $nodes = $this->createTestCrawler()->filterXPath('//li')->eq(0)->siblings(); $this->assertEquals(2, $nodes->count()); $this->assertEquals('Two', $nodes->eq(0)->text()); $this->assertEquals('Three', $nodes->eq(1)->text()); try { $this->createTestCrawler()->filterXPath('//ol')->siblings(); $this->fail('->siblings() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->siblings() throws an \InvalidArgumentException if the node list is empty'); } } public function testNextAll() { $crawler = $this->createTestCrawler()->filterXPath('//li')->eq(1); $this->assertNotSame($crawler, $crawler->nextAll(), '->nextAll() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->nextAll() returns a new instance of a crawler'); $nodes = $crawler->nextAll(); $this->assertEquals(1, $nodes->count()); $this->assertEquals('Three', $nodes->eq(0)->text()); try { $this->createTestCrawler()->filterXPath('//ol')->nextAll(); $this->fail('->nextAll() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->nextAll() throws an \InvalidArgumentException if the node list is empty'); } } public function testPreviousAll() { $crawler = $this->createTestCrawler()->filterXPath('//li')->eq(2); $this->assertNotSame($crawler, $crawler->previousAll(), '->previousAll() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->previousAll() returns a new instance of a crawler'); $nodes = $crawler->previousAll(); $this->assertEquals(2, $nodes->count()); $this->assertEquals('Two', $nodes->eq(0)->text()); try { $this->createTestCrawler()->filterXPath('//ol')->previousAll(); $this->fail('->previousAll() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->previousAll() throws an \InvalidArgumentException if the node list is empty'); } } public function testChildren() { $crawler = $this->createTestCrawler()->filterXPath('//ul'); $this->assertNotSame($crawler, $crawler->children(), '->children() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->children() returns a new instance of a crawler'); $nodes = $crawler->children(); $this->assertEquals(3, $nodes->count()); $this->assertEquals('One', $nodes->eq(0)->text()); $this->assertEquals('Two', $nodes->eq(1)->text()); $this->assertEquals('Three', $nodes->eq(2)->text()); try { $this->createTestCrawler()->filterXPath('//ol')->children(); $this->fail('->children() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->children() throws an \InvalidArgumentException if the node list is empty'); } } public function testParents() { $crawler = $this->createTestCrawler()->filterXPath('//li[1]'); $this->assertNotSame($crawler, $crawler->parents(), '->parents() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->parents() returns a new instance of a crawler'); $nodes = $crawler->parents(); $this->assertEquals(3, $nodes->count()); $nodes = $this->createTestCrawler()->filterXPath('//html')->parents(); $this->assertEquals(0, $nodes->count()); try { $this->createTestCrawler()->filterXPath('//ol')->parents(); $this->fail('->parents() throws an \InvalidArgumentException if the node list is empty'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->parents() throws an \InvalidArgumentException if the node list is empty'); } } public function createTestCrawler($uri = null) { $dom = new \DOMDocument(); $dom->loadHTML('
Foo Fabien\'s Foo Fabien"s Foo \' Fabien"s Foo Bar    Fabien\'s Bar   Fabien"s Bar \' Fabien"s Bar GetLink
', array('bar' => array('InputFormField', 'bar')), ), array( 'appends the submitted button value but not other submit buttons', ' ', array('foobar' => array('InputFormField', 'foobar')), ), array( 'returns textareas', ' ', array('foo' => array('TextareaFormField', 'foo')), ), array( 'returns inputs', ' ', array('foo' => array('InputFormField', 'foo')), ), array( 'returns checkboxes', ' ', array('foo' => array('ChoiceFormField', 'foo')), ), array( 'returns not-checked checkboxes', ' ', array('foo' => array('ChoiceFormField', false)), ), array( 'returns radio buttons', ' ', array('foo' => array('ChoiceFormField', 'bar')), ), array( 'returns file inputs', ' ', array('foo' => array('FileFormField', array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0))), ), ); } public function testGetFormNode() { $dom = new \DOMDocument(); $dom->loadHTML('
'); $form = new Form($dom->getElementsByTagName('input')->item(0), 'http://example.com'); $this->assertSame($dom->getElementsByTagName('form')->item(0), $form->getFormNode(), '->getFormNode() returns the form node associated with this form'); } public function testGetMethod() { $form = $this->createForm('
'); $this->assertEquals('GET', $form->getMethod(), '->getMethod() returns get if no method is defined'); $form = $this->createForm('
'); $this->assertEquals('POST', $form->getMethod(), '->getMethod() returns the method attribute value of the form'); $form = $this->createForm('
', 'put'); $this->assertEquals('PUT', $form->getMethod(), '->getMethod() returns the method defined in the constructor if provided'); $form = $this->createForm('
', 'delete'); $this->assertEquals('DELETE', $form->getMethod(), '->getMethod() returns the method defined in the constructor if provided'); $form = $this->createForm('
', 'patch'); $this->assertEquals('PATCH', $form->getMethod(), '->getMethod() returns the method defined in the constructor if provided'); } public function testGetSetValue() { $form = $this->createForm('
'); $this->assertEquals('foo', $form['foo']->getValue(), '->offsetGet() returns the value of a form field'); $form['foo'] = 'bar'; $this->assertEquals('bar', $form['foo']->getValue(), '->offsetSet() changes the value of a form field'); try { $form['foobar'] = 'bar'; $this->fail('->offsetSet() throws an \InvalidArgumentException exception if the field does not exist'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->offsetSet() throws an \InvalidArgumentException exception if the field does not exist'); } try { $form['foobar']; $this->fail('->offsetSet() throws an \InvalidArgumentException exception if the field does not exist'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->offsetSet() throws an \InvalidArgumentException exception if the field does not exist'); } } public function testOffsetUnset() { $form = $this->createForm('
'); unset($form['foo']); $this->assertFalse(isset($form['foo']), '->offsetUnset() removes a field'); } public function testOffsetExists() { $form = $this->createForm('
'); $this->assertTrue(isset($form['foo']), '->offsetExists() return true if the field exists'); $this->assertFalse(isset($form['bar']), '->offsetExists() return false if the field does not exist'); } public function testGetValues() { $form = $this->createForm('
'); $this->assertEquals(array('foo[bar]' => 'foo', 'bar' => 'bar'), $form->getValues(), '->getValues() returns all form field values'); $form = $this->createForm('
'); $this->assertEquals(array('bar' => 'bar'), $form->getValues(), '->getValues() does not include not-checked checkboxes'); $form = $this->createForm('
'); $this->assertEquals(array('bar' => 'bar'), $form->getValues(), '->getValues() does not include file input fields'); $form = $this->createForm('
'); $this->assertEquals(array('bar' => 'bar'), $form->getValues(), '->getValues() does not include disabled fields'); } public function testSetValues() { $form = $this->createForm('
'); $form->setValues(array('foo' => false, 'bar' => 'foo')); $this->assertEquals(array('bar' => 'foo'), $form->getValues(), '->setValues() sets the values of fields'); } public function testMultiselectSetValues() { $form = $this->createForm('
'); $form->setValues(array('multi' => array("foo", "bar"))); $this->assertEquals(array('multi' => array('foo', 'bar')), $form->getValues(), '->setValue() sets the values of select'); } public function testGetPhpValues() { $form = $this->createForm('
'); $this->assertEquals(array('foo' => array('bar' => 'foo'), 'bar' => 'bar'), $form->getPhpValues(), '->getPhpValues() converts keys with [] to arrays'); } public function testGetFiles() { $form = $this->createForm('
'); $this->assertEquals(array(), $form->getFiles(), '->getFiles() returns an empty array if method is get'); $form = $this->createForm('
'); $this->assertEquals(array('foo[bar]' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0)), $form->getFiles(), '->getFiles() only returns file fields for POST'); $form = $this->createForm('
', 'put'); $this->assertEquals(array('foo[bar]' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0)), $form->getFiles(), '->getFiles() only returns file fields for PUT'); $form = $this->createForm('
', 'delete'); $this->assertEquals(array('foo[bar]' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0)), $form->getFiles(), '->getFiles() only returns file fields for DELETE'); $form = $this->createForm('
', 'patch'); $this->assertEquals(array('foo[bar]' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0)), $form->getFiles(), '->getFiles() only returns file fields for PATCH'); $form = $this->createForm('
'); $this->assertEquals(array(), $form->getFiles(), '->getFiles() does not include disabled file fields'); } public function testGetPhpFiles() { $form = $this->createForm('
'); $this->assertEquals(array('foo' => array('bar' => array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0))), $form->getPhpFiles(), '->getPhpFiles() converts keys with [] to arrays'); } /** * @dataProvider provideGetUriValues */ public function testGetUri($message, $form, $values, $uri, $method = null) { $form = $this->createForm($form, $method); $form->setValues($values); $this->assertEquals('http://example.com'.$uri, $form->getUri(), '->getUri() '.$message); } public function testGetBaseUri() { $dom = new \DOMDocument(); $dom->loadHTML('
'); $nodes = $dom->getElementsByTagName('input'); $form = new Form($nodes->item($nodes->length - 1), 'http://www.foo.com/'); $this->assertEquals('http://www.foo.com/foo.php', $form->getUri()); } public function testGetUriWithAnchor() { $form = $this->createForm('
', null, 'http://example.com/id/123'); $this->assertEquals('http://example.com/id/123#foo', $form->getUri()); } public function testGetUriActionAbsolute() { $formHtml='
'; $form = $this->createForm($formHtml); $this->assertEquals('https://login.foo.com/login.php?login_attempt=1', $form->getUri(), '->getUri() returns absolute URIs set in the action form'); $form = $this->createForm($formHtml, null, 'https://login.foo.com'); $this->assertEquals('https://login.foo.com/login.php?login_attempt=1', $form->getUri(), '->getUri() returns absolute URIs set in the action form'); $form = $this->createForm($formHtml, null, 'https://login.foo.com/bar/'); $this->assertEquals('https://login.foo.com/login.php?login_attempt=1', $form->getUri(), '->getUri() returns absolute URIs set in the action form'); // The action URI haven't the same domain Host have an another domain as Host $form = $this->createForm($formHtml, null, 'https://www.foo.com'); $this->assertEquals('https://login.foo.com/login.php?login_attempt=1', $form->getUri(), '->getUri() returns absolute URIs set in the action form'); $form = $this->createForm($formHtml, null, 'https://www.foo.com/bar/'); $this->assertEquals('https://login.foo.com/login.php?login_attempt=1', $form->getUri(), '->getUri() returns absolute URIs set in the action form'); } public function testGetUriAbsolute() { $form = $this->createForm('
', null, 'http://localhost/foo/'); $this->assertEquals('http://localhost/foo/foo', $form->getUri(), '->getUri() returns absolute URIs'); $form = $this->createForm('
', null, 'http://localhost/foo/'); $this->assertEquals('http://localhost/foo', $form->getUri(), '->getUri() returns absolute URIs'); } public function testGetUriWithOnlyQueryString() { $form = $this->createForm('
', null, 'http://localhost/foo/bar'); $this->assertEquals('http://localhost/foo/bar?get=param', $form->getUri(), '->getUri() returns absolute URIs only if the host has been defined in the constructor'); } public function testGetUriWithoutAction() { $form = $this->createForm('
', null, 'http://localhost/foo/bar'); $this->assertEquals('http://localhost/foo/bar', $form->getUri(), '->getUri() returns path if no action defined'); } public function provideGetUriValues() { return array( array( 'returns the URI of the form', '
', array(), '/foo' ), array( 'appends the form values if the method is get', '
', array(), '/foo?foo=foo' ), array( 'appends the form values and merges the submitted values', '
', array('foo' => 'bar'), '/foo?foo=bar' ), array( 'does not append values if the method is post', '
', array(), '/foo' ), array( 'does not append values if the method is patch', '
', array(), '/foo', 'PUT' ), array( 'does not append values if the method is delete', '
', array(), '/foo', 'DELETE' ), array( 'does not append values if the method is put', '
', array(), '/foo', 'PATCH' ), array( 'appends the form values to an existing query string', '
', array(), '/foo?bar=bar&foo=foo' ), array( 'returns an empty URI if the action is empty', '
', array(), '/', ), array( 'appends the form values even if the action is empty', '
', array(), '/?foo=foo', ), array( 'chooses the path if the action attribute value is a sharp (#)', '
', array(), '/#', ), ); } public function testHas() { $form = $this->createForm('
'); $this->assertFalse($form->has('foo'), '->has() returns false if a field is not in the form'); $this->assertTrue($form->has('bar'), '->has() returns true if a field is in the form'); } public function testRemove() { $form = $this->createForm('
'); $form->remove('bar'); $this->assertFalse($form->has('bar'), '->remove() removes a field'); } public function testGet() { $form = $this->createForm('
'); $this->assertEquals('Symfony\\Component\\DomCrawler\\Field\\InputFormField', get_class($form->get('bar')), '->get() returns the field object associated with the given name'); try { $form->get('foo'); $this->fail('->get() throws an \InvalidArgumentException if the field does not exist'); } catch (\InvalidArgumentException $e) { $this->assertTrue(true, '->get() throws an \InvalidArgumentException if the field does not exist'); } } public function testAll() { $form = $this->createForm('
'); $fields = $form->all(); $this->assertEquals(1, count($fields), '->all() return an array of form field objects'); $this->assertEquals('Symfony\\Component\\DomCrawler\\Field\\InputFormField', get_class($fields['bar']), '->all() return an array of form field objects'); } public function testSubmitWithoutAFormButton() { $dom = new \DOMDocument(); $dom->loadHTML('
'); $nodes = $dom->getElementsByTagName('form'); $form = new Form($nodes->item(0), 'http://example.com'); $this->assertSame($nodes->item(0), $form->getFormNode(), '->getFormNode() returns the form node associated with this form'); } /** * @expectedException \InvalidArgumentException */ public function testFormFieldRegistryAddThrowAnExceptionWhenTheNameIsMalformed() { $registry = new FormFieldRegistry(); $registry->add($this->getFormFieldMock('[foo]')); } /** * @expectedException \InvalidArgumentException */ public function testFormFieldRegistryRemoveThrowAnExceptionWhenTheNameIsMalformed() { $registry = new FormFieldRegistry(); $registry->remove('[foo]'); } /** * @expectedException \InvalidArgumentException */ public function testFormFieldRegistryGetThrowAnExceptionWhenTheNameIsMalformed() { $registry = new FormFieldRegistry(); $registry->get('[foo]'); } /** * @expectedException \InvalidArgumentException */ public function testFormFieldRegistryGetThrowAnExceptionWhenTheFieldDoesNotExist() { $registry = new FormFieldRegistry(); $registry->get('foo'); } /** * @expectedException \InvalidArgumentException */ public function testFormFieldRegistrySetThrowAnExceptionWhenTheNameIsMalformed() { $registry = new FormFieldRegistry(); $registry->set('[foo]', null); } /** * @expectedException \InvalidArgumentException */ public function testFormFieldRegistrySetThrowAnExceptionWhenTheFieldDoesNotExist() { $registry = new FormFieldRegistry(); $registry->set('foo', null); } public function testFormFieldRegistryHasReturnsTrueWhenTheFQNExists() { $registry = new FormFieldRegistry(); $registry->add($this->getFormFieldMock('foo[bar]')); $this->assertTrue($registry->has('foo')); $this->assertTrue($registry->has('foo[bar]')); $this->assertFalse($registry->has('bar')); $this->assertFalse($registry->has('foo[foo]')); } public function testFormRegistryFieldsCanBeRemoved() { $registry = new FormFieldRegistry(); $registry->add($this->getFormFieldMock('foo')); $registry->remove('foo'); $this->assertFalse($registry->has('foo')); } public function testFormRegistrySupportsMultivaluedFields() { $registry = new FormFieldRegistry(); $registry->add($this->getFormFieldMock('foo[]')); $registry->add($this->getFormFieldMock('foo[]')); $registry->add($this->getFormFieldMock('bar[5]')); $registry->add($this->getFormFieldMock('bar[]')); $registry->add($this->getFormFieldMock('bar[baz]')); $this->assertEquals( array('foo[0]', 'foo[1]', 'bar[5]', 'bar[6]', 'bar[baz]'), array_keys($registry->all()) ); } public function testFormRegistrySetValues() { $registry = new FormFieldRegistry(); $registry->add($f2 = $this->getFormFieldMock('foo[2]')); $registry->add($f3 = $this->getFormFieldMock('foo[3]')); $registry->add($fbb = $this->getFormFieldMock('foo[bar][baz]')); $f2 ->expects($this->exactly(2)) ->method('setValue') ->with(2) ; $f3 ->expects($this->exactly(2)) ->method('setValue') ->with(3) ; $fbb ->expects($this->exactly(2)) ->method('setValue') ->with('fbb') ; $registry->set('foo[2]', 2); $registry->set('foo[3]', 3); $registry->set('foo[bar][baz]', 'fbb'); $registry->set('foo', array( 2 => 2, 3 => 3, 'bar' => array( 'baz' => 'fbb' ) )); } protected function getFormFieldMock($name, $value = null) { $field = $this ->getMockBuilder('Symfony\\Component\\DomCrawler\\Field\\FormField') ->setMethods(array('getName', 'getValue', 'setValue', 'initialize')) ->disableOriginalConstructor() ->getMock() ; $field ->expects($this->any()) ->method('getName') ->will($this->returnValue($name)) ; $field ->expects($this->any()) ->method('getValue') ->will($this->returnValue($value)) ; return $field; } protected function createForm($form, $method = null, $currentUri = null) { $dom = new \DOMDocument(); $dom->loadHTML(''.$form.''); $nodes = $dom->getElementsByTagName('input'); $xPath = new \DOMXPath($dom); $nodes = $xPath->query('//input | //button'); if (null === $currentUri) { $currentUri = 'http://example.com/'; } return new Form($nodes->item($nodes->length - 1), $currentUri, $method); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\DomCrawler\Tests; use Symfony\Component\DomCrawler\Link; class LinkTest extends \PHPUnit_Framework_TestCase { /** * @expectedException \LogicException */ public function testConstructorWithANonATag() { $dom = new \DOMDocument(); $dom->loadHTML('
'); new Link($dom->getElementsByTagName('div')->item(0), 'http://www.example.com/'); } /** * @expectedException \InvalidArgumentException */ public function testConstructorWithAnInvalidCurrentUri() { $dom = new \DOMDocument(); $dom->loadHTML('foo'); new Link($dom->getElementsByTagName('a')->item(0), 'example.com'); } public function testGetNode() { $dom = new \DOMDocument(); $dom->loadHTML('foo'); $node = $dom->getElementsByTagName('a')->item(0); $link = new Link($node, 'http://example.com/'); $this->assertEquals($node, $link->getNode(), '->getNode() returns the node associated with the link'); } public function testGetMethod() { $dom = new \DOMDocument(); $dom->loadHTML('foo'); $node = $dom->getElementsByTagName('a')->item(0); $link = new Link($node, 'http://example.com/'); $this->assertEquals('GET', $link->getMethod(), '->getMethod() returns the method of the link'); $link = new Link($node, 'http://example.com/', 'post'); $this->assertEquals('POST', $link->getMethod(), '->getMethod() returns the method of the link'); } /** * @dataProvider getGetUriTests */ public function testGetUri($url, $currentUri, $expected) { $dom = new \DOMDocument(); $dom->loadHTML(sprintf('foo', $url)); $link = new Link($dom->getElementsByTagName('a')->item(0), $currentUri); $this->assertEquals($expected, $link->getUri()); } public function getGetUriTests() { return array( array('/foo', 'http://localhost/bar/foo/', 'http://localhost/foo'), array('/foo', 'http://localhost/bar/foo', 'http://localhost/foo'), array(' /foo', 'http://localhost/bar/foo/', 'http://localhost/foo'), array('/foo ', 'http://localhost/bar/foo', 'http://localhost/foo'), array('foo', 'http://localhost/bar/foo/', 'http://localhost/bar/foo/foo'), array('foo', 'http://localhost/bar/foo', 'http://localhost/bar/foo'), array('', 'http://localhost/bar/', 'http://localhost/bar/'), array('#', 'http://localhost/bar/', 'http://localhost/bar/#'), array('#bar', 'http://localhost/bar/#foo', 'http://localhost/bar/#bar'), array('?a=b', 'http://localhost/bar/', 'http://localhost/bar/?a=b'), array('http://login.foo.com/foo', 'http://localhost/bar/', 'http://login.foo.com/foo'), array('https://login.foo.com/foo', 'https://localhost/bar/', 'https://login.foo.com/foo'), array('mailto:foo@bar.com', 'http://localhost/foo', 'mailto:foo@bar.com'), array('?foo=2', 'http://localhost?foo=1', 'http://localhost?foo=2'), array('?foo=2', 'http://localhost/?foo=1', 'http://localhost/?foo=2'), array('?foo=2', 'http://localhost/bar?foo=1', 'http://localhost/bar?foo=2'), array('?foo=2', 'http://localhost/bar/?foo=1', 'http://localhost/bar/?foo=2'), array('?bar=2', 'http://localhost?foo=1', 'http://localhost?bar=2'), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Lazily loads listeners and subscribers from the dependency injection * container * * @author Fabien Potencier * @author Bernhard Schussek * @author Jordan Alliot */ class ContainerAwareEventDispatcher extends EventDispatcher { /** * The container from where services are loaded * @var ContainerInterface */ private $container; /** * The service IDs of the event listeners and subscribers * @var array */ private $listenerIds = array(); /** * The services registered as listeners * @var array */ private $listeners = array(); /** * Constructor. * * @param ContainerInterface $container A ContainerInterface instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Adds a service as event listener * * @param string $eventName Event for which the listener is added * @param array $callback The service ID of the listener service & the method * name that has to be called * @param integer $priority The higher this value, the earlier an event listener * will be triggered in the chain. * Defaults to 0. * * @throws \InvalidArgumentException */ public function addListenerService($eventName, $callback, $priority = 0) { if (!is_array($callback) || 2 !== count($callback)) { throw new \InvalidArgumentException('Expected an array("service", "method") argument'); } $this->listenerIds[$eventName][] = array($callback[0], $callback[1], $priority); } public function removeListener($eventName, $listener) { $this->lazyLoad($eventName); if (isset($this->listeners[$eventName])) { foreach ($this->listeners[$eventName] as $key => $l) { foreach ($this->listenerIds[$eventName] as $i => $args) { list($serviceId, $method, $priority) = $args; if ($key === $serviceId.'.'.$method) { if ($listener === array($l, $method)) { unset($this->listeners[$eventName][$key]); if (empty($this->listeners[$eventName])) { unset($this->listeners[$eventName]); } unset($this->listenerIds[$eventName][$i]); if (empty($this->listenerIds[$eventName])) { unset($this->listenerIds[$eventName]); } } } } } } parent::removeListener($eventName, $listener); } /** * @see EventDispatcherInterface::hasListeners */ public function hasListeners($eventName = null) { if (null === $eventName) { return (Boolean) count($this->listenerIds) || (Boolean) count($this->listeners); } if (isset($this->listenerIds[$eventName])) { return true; } return parent::hasListeners($eventName); } /** * @see EventDispatcherInterface::getListeners */ public function getListeners($eventName = null) { if (null === $eventName) { foreach (array_keys($this->listenerIds) as $serviceEventName) { $this->lazyLoad($serviceEventName); } } else { $this->lazyLoad($eventName); } return parent::getListeners($eventName); } /** * Adds a service as event subscriber * * @param string $serviceId The service ID of the subscriber service * @param string $class The service's class name (which must implement EventSubscriberInterface) */ public function addSubscriberService($serviceId, $class) { foreach ($class::getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $this->listenerIds[$eventName][] = array($serviceId, $params, 0); } elseif (is_string($params[0])) { $this->listenerIds[$eventName][] = array($serviceId, $params[0], isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { $this->listenerIds[$eventName][] = array($serviceId, $listener[0], isset($listener[1]) ? $listener[1] : 0); } } } } /** * {@inheritDoc} * * Lazily loads listeners for this event from the dependency injection * container. * * @throws \InvalidArgumentException if the service is not defined */ public function dispatch($eventName, Event $event = null) { $this->lazyLoad($eventName); return parent::dispatch($eventName, $event); } public function getContainer() { return $this->container; } /** * Lazily loads listeners for this event from the dependency injection * container. * * @param string $eventName The name of the event to dispatch. The name of * the event is the name of the method that is * invoked on listeners. */ protected function lazyLoad($eventName) { if (isset($this->listenerIds[$eventName])) { foreach ($this->listenerIds[$eventName] as $args) { list($serviceId, $method, $priority) = $args; $listener = $this->container->get($serviceId); $key = $serviceId.'.'.$method; if (!isset($this->listeners[$eventName][$key])) { $this->addListener($eventName, array($listener, $method), $priority); } elseif ($listener !== $this->listeners[$eventName][$key]) { parent::removeListener($eventName, array($this->listeners[$eventName][$key], $method)); $this->addListener($eventName, array($listener, $method), $priority); } $this->listeners[$eventName][$key] = $listener; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Debug; /** * @author Fabien Potencier */ interface TraceableEventDispatcherInterface { /** * Gets the called listeners. * * @return array An array of called listeners */ public function getCalledListeners(); /** * Gets the not called listeners. * * @return array An array of not called listeners */ public function getNotCalledListeners(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * Event is the base class for classes containing event data. * * This class contains no event data. It is used by events that do not pass * state information to an event handler when an event is raised. * * You can call the method stopPropagation() to abort the execution of * further listeners in your event listener. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek * * @api */ class Event { /** * @var Boolean Whether no further event listeners should be triggered */ private $propagationStopped = false; /** * @var EventDispatcher Dispatcher that dispatched this event */ private $dispatcher; /** * @var string This event's name */ private $name; /** * Returns whether further event listeners should be triggered. * * @see Event::stopPropagation * @return Boolean Whether propagation was already stopped for this event. * * @api */ public function isPropagationStopped() { return $this->propagationStopped; } /** * Stops the propagation of the event to further event listeners. * * If multiple event listeners are connected to the same event, no * further event listener will be triggered once any trigger calls * stopPropagation(). * * @api */ public function stopPropagation() { $this->propagationStopped = true; } /** * Stores the EventDispatcher that dispatches this Event * * @param EventDispatcherInterface $dispatcher * * @api */ public function setDispatcher(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } /** * Returns the EventDispatcher that dispatches this Event * * @return EventDispatcherInterface * * @api */ public function getDispatcher() { return $this->dispatcher; } /** * Gets the event's name. * * @return string * * @api */ public function getName() { return $this->name; } /** * Sets the event's name property. * * @param string $name The event name. * * @api */ public function setName($name) { $this->name = $name; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. * * Listeners are registered on the manager and events are dispatched through the * manager. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek * @author Fabien Potencier * @author Jordi Boggiano * @author Jordan Alliot * * @api */ class EventDispatcher implements EventDispatcherInterface { private $listeners = array(); private $sorted = array(); /** * @see EventDispatcherInterface::dispatch * * @api */ public function dispatch($eventName, Event $event = null) { if (null === $event) { $event = new Event(); } $event->setDispatcher($this); $event->setName($eventName); if (!isset($this->listeners[$eventName])) { return $event; } $this->doDispatch($this->getListeners($eventName), $eventName, $event); return $event; } /** * @see EventDispatcherInterface::getListeners */ public function getListeners($eventName = null) { if (null !== $eventName) { if (!isset($this->sorted[$eventName])) { $this->sortListeners($eventName); } return $this->sorted[$eventName]; } foreach (array_keys($this->listeners) as $eventName) { if (!isset($this->sorted[$eventName])) { $this->sortListeners($eventName); } } return $this->sorted; } /** * @see EventDispatcherInterface::hasListeners */ public function hasListeners($eventName = null) { return (Boolean) count($this->getListeners($eventName)); } /** * @see EventDispatcherInterface::addListener * * @api */ public function addListener($eventName, $listener, $priority = 0) { $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName]); } /** * @see EventDispatcherInterface::removeListener */ public function removeListener($eventName, $listener) { if (!isset($this->listeners[$eventName])) { return; } foreach ($this->listeners[$eventName] as $priority => $listeners) { if (false !== ($key = array_search($listener, $listeners, true))) { unset($this->listeners[$eventName][$priority][$key], $this->sorted[$eventName]); } } } /** * @see EventDispatcherInterface::addSubscriber * * @api */ public function addSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $this->addListener($eventName, array($subscriber, $params)); } elseif (is_string($params[0])) { $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); } } } } /** * @see EventDispatcherInterface::removeSubscriber */ public function removeSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_array($params) && is_array($params[0])) { foreach ($params as $listener) { $this->removeListener($eventName, array($subscriber, $listener[0])); } } else { $this->removeListener($eventName, array($subscriber, is_string($params) ? $params : $params[0])); } } } /** * Triggers the listeners of an event. * * This method can be overridden to add functionality that is executed * for each listener. * * @param array[callback] $listeners The event listeners. * @param string $eventName The name of the event to dispatch. * @param Event $event The event object to pass to the event handlers/listeners. */ protected function doDispatch($listeners, $eventName, Event $event) { foreach ($listeners as $listener) { call_user_func($listener, $event); if ($event->isPropagationStopped()) { break; } } } /** * Sorts the internal list of listeners for the given event by priority. * * @param string $eventName The name of the event. */ private function sortListeners($eventName) { $this->sorted[$eventName] = array(); if (isset($this->listeners[$eventName])) { krsort($this->listeners[$eventName]); $this->sorted[$eventName] = call_user_func_array('array_merge', $this->listeners[$eventName]); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. * Listeners are registered on the manager and events are dispatched through the * manager. * * @author Bernhard Schussek * * @api */ interface EventDispatcherInterface { /** * Dispatches an event to all registered listeners. * * @param string $eventName The name of the event to dispatch. The name of * the event is the name of the method that is * invoked on listeners. * @param Event $event The event to pass to the event handlers/listeners. * If not supplied, an empty Event instance is created. * * @return Event * * @api */ public function dispatch($eventName, Event $event = null); /** * Adds an event listener that listens on the specified events. * * @param string $eventName The event to listen on * @param callable $listener The listener * @param integer $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 0) * * @api */ public function addListener($eventName, $listener, $priority = 0); /** * Adds an event subscriber. * * The subscriber is asked for all the events he is * interested in and added as a listener for these events. * * @param EventSubscriberInterface $subscriber The subscriber. * * @api */ public function addSubscriber(EventSubscriberInterface $subscriber); /** * Removes an event listener from the specified events. * * @param string|array $eventName The event(s) to remove a listener from * @param callable $listener The listener to remove */ public function removeListener($eventName, $listener); /** * Removes an event subscriber. * * @param EventSubscriberInterface $subscriber The subscriber */ public function removeSubscriber(EventSubscriberInterface $subscriber); /** * Gets the listeners of a specific event or all listeners. * * @param string $eventName The name of the event * * @return array The event listeners for the specified event, or all event listeners by event name */ public function getListeners($eventName = null); /** * Checks whether an event has any registered listeners. * * @param string $eventName The name of the event * * @return Boolean true if the specified event has any listeners, false otherwise */ public function hasListeners($eventName = null); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * An EventSubscriber knows himself what events he is interested in. * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes * {@link getSubscribedEvents} and registers the subscriber as a listener for all * returned events. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek * * @api */ interface EventSubscriberInterface { /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to * * @api */ public static function getSubscribedEvents(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * Event encapsulation class. * * Encapsulates events thus decoupling the observer from the subject they encapsulate. * * @author Drak */ class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate { /** * Observer pattern subject. * * @var mixed usually object or callable */ protected $subject; /** * Array of arguments. * * @var array */ protected $arguments; /** * Encapsulate an event with $subject and $args. * * @param mixed $subject The subject of the event, usually an object. * @param array $arguments Arguments to store in the event. */ public function __construct($subject = null, array $arguments = array()) { $this->subject = $subject; $this->arguments = $arguments; } /** * Getter for subject property. * * @return mixed $subject The observer subject. */ public function getSubject() { return $this->subject; } /** * Get argument by key. * * @param string $key Key. * * @throws \InvalidArgumentException If key is not found. * * @return mixed Contents of array key. */ public function getArgument($key) { if ($this->hasArgument($key)) { return $this->arguments[$key]; } throw new \InvalidArgumentException(sprintf('%s not found in %s', $key, $this->getName())); } /** * Add argument to event. * * @param string $key Argument name. * @param mixed $value Value. * * @return GenericEvent */ public function setArgument($key, $value) { $this->arguments[$key] = $value; return $this; } /** * Getter for all arguments. * * @return array */ public function getArguments() { return $this->arguments; } /** * Set args property. * * @param array $args Arguments. * * @return GenericEvent */ public function setArguments(array $args = array()) { $this->arguments = $args; return $this; } /** * Has argument. * * @param string $key Key of arguments array. * * @return boolean */ public function hasArgument($key) { return array_key_exists($key, $this->arguments); } /** * ArrayAccess for argument getter. * * @param string $key Array key. * * @throws \InvalidArgumentException If key does not exist in $this->args. * * @return mixed */ public function offsetGet($key) { return $this->getArgument($key); } /** * ArrayAccess for argument setter. * * @param string $key Array key to set. * @param mixed $value Value. */ public function offsetSet($key, $value) { $this->setArgument($key, $value); } /** * ArrayAccess for unset argument. * * @param string $key Array key. */ public function offsetUnset($key) { if ($this->hasArgument($key)) { unset($this->arguments[$key]); } } /** * ArrayAccess has argument. * * @param string $key Array key. * * @return boolean */ public function offsetExists($key) { return $this->hasArgument($key); } /** * IteratorAggregate for iterating over the object like an array * * @return \ArrayIterator */ public function getIterator() { return new \ArrayIterator($this->arguments); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * A read-only proxy for an event dispatcher. * * @author Bernhard Schussek */ class ImmutableEventDispatcher implements EventDispatcherInterface { /** * The proxied dispatcher. * @var EventDispatcherInterface */ private $dispatcher; /** * Creates an unmodifiable proxy for an event dispatcher. * * @param EventDispatcherInterface $dispatcher The proxied event dispatcher. */ public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } /** * {@inheritdoc} */ public function dispatch($eventName, Event $event = null) { return $this->dispatcher->dispatch($eventName, $event); } /** * {@inheritdoc} */ public function addListener($eventName, $listener, $priority = 0) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function addSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function removeListener($eventName, $listener) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function removeSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function getListeners($eventName = null) { return $this->dispatcher->getListeners($eventName); } /** * {@inheritdoc} */ public function hasListeners($eventName = null) { return $this->dispatcher->hasListeners($eventName); } } Copyright (c) 2004-2013 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ContainerAwareEventDispatcherTest extends \PHPUnit_Framework_TestCase { protected function setUp() { if (!class_exists('Symfony\Component\DependencyInjection\Container')) { $this->markTestSkipped('The "DependencyInjection" component is not available'); } } public function testAddAListenerService() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $service ->expects($this->once()) ->method('onEvent') ->with($event) ; $container = new Container(); $container->set('service.listener', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $dispatcher->dispatch('onEvent', $event); } public function testAddASubscriberService() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\SubscriberService'); $service ->expects($this->once()) ->method('onEvent') ->with($event) ; $container = new Container(); $container->set('service.subscriber', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addSubscriberService('service.subscriber', 'Symfony\Component\EventDispatcher\Tests\SubscriberService'); $dispatcher->dispatch('onEvent', $event); } public function testPreventDuplicateListenerService() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $service ->expects($this->once()) ->method('onEvent') ->with($event) ; $container = new Container(); $container->set('service.listener', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'), 5); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'), 10); $dispatcher->dispatch('onEvent', $event); } /** * @expectedException \InvalidArgumentException */ public function testTriggerAListenerServiceOutOfScope() { $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $scope = new Scope('scope'); $container = new Container(); $container->addScope($scope); $container->enterScope('scope'); $container->set('service.listener', $service, 'scope'); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $container->leaveScope('scope'); $dispatcher->dispatch('onEvent'); } public function testReEnteringAScope() { $event = new Event(); $service1 = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $service1 ->expects($this->exactly(2)) ->method('onEvent') ->with($event) ; $scope = new Scope('scope'); $container = new Container(); $container->addScope($scope); $container->enterScope('scope'); $container->set('service.listener', $service1, 'scope'); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $dispatcher->dispatch('onEvent', $event); $service2 = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $service2 ->expects($this->once()) ->method('onEvent') ->with($event) ; $container->enterScope('scope'); $container->set('service.listener', $service2, 'scope'); $dispatcher->dispatch('onEvent', $event); $container->leaveScope('scope'); $dispatcher->dispatch('onEvent'); } public function testHasListenersOnLazyLoad() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $container = new Container(); $container->set('service.listener', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $event->setDispatcher($dispatcher); $event->setName('onEvent'); $service ->expects($this->once()) ->method('onEvent') ->with($event) ; $this->assertTrue($dispatcher->hasListeners()); if ($dispatcher->hasListeners('onEvent')) { $dispatcher->dispatch('onEvent'); } } public function testGetListenersOnLazyLoad() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $container = new Container(); $container->set('service.listener', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $listeners = $dispatcher->getListeners(); $this->assertTrue(isset($listeners['onEvent'])); $this->assertCount(1, $dispatcher->getListeners('onEvent')); } public function testRemoveAfterDispatch() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $container = new Container(); $container->set('service.listener', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $dispatcher->dispatch('onEvent', new Event()); $dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent')); $this->assertFalse($dispatcher->hasListeners('onEvent')); } public function testRemoveBeforeDispatch() { $event = new Event(); $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); $container = new Container(); $container->set('service.listener', $service); $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); $dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent')); $this->assertFalse($dispatcher->hasListeners('onEvent')); } } class Service { public function onEvent(Event $e) { } } class SubscriberService implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( 'onEvent' => 'onEvent', 'onEvent' => array('onEvent', 10), 'onEvent' => array('onEvent'), ); } public function onEvent(Event $e) { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class EventDispatcherTest extends \PHPUnit_Framework_TestCase { /* Some pseudo events */ const preFoo = 'pre.foo'; const postFoo = 'post.foo'; const preBar = 'pre.bar'; const postBar = 'post.bar'; private $dispatcher; private $listener; protected function setUp() { $this->dispatcher = new EventDispatcher(); $this->listener = new TestEventListener(); } protected function tearDown() { $this->dispatcher = null; $this->listener = null; } public function testInitialState() { $this->assertEquals(array(), $this->dispatcher->getListeners()); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); $this->assertFalse($this->dispatcher->hasListeners(self::postFoo)); } public function testAddListener() { $this->dispatcher->addListener('pre.foo', array($this->listener, 'preFoo')); $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo')); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertTrue($this->dispatcher->hasListeners(self::postFoo)); $this->assertCount(1, $this->dispatcher->getListeners(self::preFoo)); $this->assertCount(1, $this->dispatcher->getListeners(self::postFoo)); $this->assertCount(2, $this->dispatcher->getListeners()); } public function testGetListenersSortsByPriority() { $listener1 = new TestEventListener(); $listener2 = new TestEventListener(); $listener3 = new TestEventListener(); $listener1->name = '1'; $listener2->name = '2'; $listener3->name = '3'; $this->dispatcher->addListener('pre.foo', array($listener1, 'preFoo'), -10); $this->dispatcher->addListener('pre.foo', array($listener2, 'preFoo'), 10); $this->dispatcher->addListener('pre.foo', array($listener3, 'preFoo')); $expected = array( array($listener2, 'preFoo'), array($listener3, 'preFoo'), array($listener1, 'preFoo'), ); $this->assertSame($expected, $this->dispatcher->getListeners('pre.foo')); } public function testGetAllListenersSortsByPriority() { $listener1 = new TestEventListener(); $listener2 = new TestEventListener(); $listener3 = new TestEventListener(); $listener4 = new TestEventListener(); $listener5 = new TestEventListener(); $listener6 = new TestEventListener(); $this->dispatcher->addListener('pre.foo', $listener1, -10); $this->dispatcher->addListener('pre.foo', $listener2); $this->dispatcher->addListener('pre.foo', $listener3, 10); $this->dispatcher->addListener('post.foo', $listener4, -10); $this->dispatcher->addListener('post.foo', $listener5); $this->dispatcher->addListener('post.foo', $listener6, 10); $expected = array( 'pre.foo' => array($listener3, $listener2, $listener1), 'post.foo' => array($listener6, $listener5, $listener4), ); $this->assertSame($expected, $this->dispatcher->getListeners()); } public function testDispatch() { $this->dispatcher->addListener('pre.foo', array($this->listener, 'preFoo')); $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo')); $this->dispatcher->dispatch(self::preFoo); $this->assertTrue($this->listener->preFooInvoked); $this->assertFalse($this->listener->postFooInvoked); $this->assertInstanceOf('Symfony\Component\EventDispatcher\Event', $this->dispatcher->dispatch('noevent')); $this->assertInstanceOf('Symfony\Component\EventDispatcher\Event', $this->dispatcher->dispatch(self::preFoo)); $event = new Event(); $return = $this->dispatcher->dispatch(self::preFoo, $event); $this->assertEquals('pre.foo', $event->getName()); $this->assertSame($event, $return); } public function testDispatchForClosure() { $invoked = 0; $listener = function () use (&$invoked) { $invoked++; }; $this->dispatcher->addListener('pre.foo', $listener); $this->dispatcher->addListener('post.foo', $listener); $this->dispatcher->dispatch(self::preFoo); $this->assertEquals(1, $invoked); } public function testStopEventPropagation() { $otherListener = new TestEventListener(); // postFoo() stops the propagation, so only one listener should // be executed // Manually set priority to enforce $this->listener to be called first $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'), 10); $this->dispatcher->addListener('post.foo', array($otherListener, 'preFoo')); $this->dispatcher->dispatch(self::postFoo); $this->assertTrue($this->listener->postFooInvoked); $this->assertFalse($otherListener->postFooInvoked); } public function testDispatchByPriority() { $invoked = array(); $listener1 = function () use (&$invoked) { $invoked[] = '1'; }; $listener2 = function () use (&$invoked) { $invoked[] = '2'; }; $listener3 = function () use (&$invoked) { $invoked[] = '3'; }; $this->dispatcher->addListener('pre.foo', $listener1, -10); $this->dispatcher->addListener('pre.foo', $listener2); $this->dispatcher->addListener('pre.foo', $listener3, 10); $this->dispatcher->dispatch(self::preFoo); $this->assertEquals(array('3', '2', '1'), $invoked); } public function testRemoveListener() { $this->dispatcher->addListener('pre.bar', $this->listener); $this->assertTrue($this->dispatcher->hasListeners(self::preBar)); $this->dispatcher->removeListener('pre.bar', $this->listener); $this->assertFalse($this->dispatcher->hasListeners(self::preBar)); $this->dispatcher->removeListener('notExists', $this->listener); } public function testAddSubscriber() { $eventSubscriber = new TestEventSubscriber(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertTrue($this->dispatcher->hasListeners(self::postFoo)); } public function testAddSubscriberWithPriorities() { $eventSubscriber = new TestEventSubscriber(); $this->dispatcher->addSubscriber($eventSubscriber); $eventSubscriber = new TestEventSubscriberWithPriorities(); $this->dispatcher->addSubscriber($eventSubscriber); $listeners = $this->dispatcher->getListeners('pre.foo'); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertCount(2, $listeners); $this->assertInstanceOf('Symfony\Component\EventDispatcher\Tests\TestEventSubscriberWithPriorities', $listeners[0][0]); } public function testAddSubscriberWithMultipleListeners() { $eventSubscriber = new TestEventSubscriberWithMultipleListeners(); $this->dispatcher->addSubscriber($eventSubscriber); $listeners = $this->dispatcher->getListeners('pre.foo'); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertCount(2, $listeners); $this->assertEquals('preFoo2', $listeners[0][1]); } public function testRemoveSubscriber() { $eventSubscriber = new TestEventSubscriber(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertTrue($this->dispatcher->hasListeners(self::postFoo)); $this->dispatcher->removeSubscriber($eventSubscriber); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); $this->assertFalse($this->dispatcher->hasListeners(self::postFoo)); } public function testRemoveSubscriberWithPriorities() { $eventSubscriber = new TestEventSubscriberWithPriorities(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->dispatcher->removeSubscriber($eventSubscriber); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); } public function testRemoveSubscriberWithMultipleListeners() { $eventSubscriber = new TestEventSubscriberWithMultipleListeners(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertCount(2, $this->dispatcher->getListeners(self::preFoo)); $this->dispatcher->removeSubscriber($eventSubscriber); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); } public function testEventReceivesTheDispatcherInstance() { $test = $this; $this->dispatcher->addListener('test', function ($event) use (&$dispatcher) { $dispatcher = $event->getDispatcher(); }); $this->dispatcher->dispatch('test'); $this->assertSame($this->dispatcher, $dispatcher); } /** * @see https://bugs.php.net/bug.php?id=62976 * * This bug affects: * - The PHP 5.3 branch for versions < 5.3.18 * - The PHP 5.4 branch for versions < 5.4.8 * - The PHP 5.5 branch is not affected */ public function testWorkaroundForPhpBug62976() { $dispatcher = new EventDispatcher(); $dispatcher->addListener('bug.62976', new CallableClass()); $dispatcher->removeListener('bug.62976', function() {}); $this->assertTrue($dispatcher->hasListeners('bug.62976')); } } class CallableClass { public function __invoke() { } } class TestEventListener { public $preFooInvoked = false; public $postFooInvoked = false; /* Listener methods */ public function preFoo(Event $e) { $this->preFooInvoked = true; } public function postFoo(Event $e) { $this->postFooInvoked = true; $e->stopPropagation(); } } class TestEventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array('pre.foo' => 'preFoo', 'post.foo' => 'postFoo'); } } class TestEventSubscriberWithPriorities implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( 'pre.foo' => array('preFoo', 10), 'post.foo' => array('postFoo'), ); } } class TestEventSubscriberWithMultipleListeners implements EventSubscriberInterface { public static function getSubscribedEvents() { return array('pre.foo' => array( array('preFoo1'), array('preFoo2', 10) )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; /** * Test class for Event. */ class EventTest extends \PHPUnit_Framework_TestCase { /** * @var \Symfony\Component\EventDispatcher\Event */ protected $event; /** * @var \Symfony\Component\EventDispatcher\EventDispatcher */ protected $dispatcher; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->event = new Event; $this->dispatcher = new EventDispatcher(); } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { $this->event = null; $this->eventDispatcher = null; } public function testIsPropagationStopped() { $this->assertFalse($this->event->isPropagationStopped()); } public function testStopPropagationAndIsPropagationStopped() { $this->event->stopPropagation(); $this->assertTrue($this->event->isPropagationStopped()); } public function testSetDispatcher() { $this->event->setDispatcher($this->dispatcher); $this->assertSame($this->dispatcher, $this->event->getDispatcher()); } public function testGetDispatcher() { $this->assertNull($this->event->getDispatcher()); } public function testGetName() { $this->assertNull($this->event->getName()); } public function testSetName() { $this->event->setName('foo'); $this->assertEquals('foo', $this->event->getName()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\EventDispatcher\GenericEvent; /** * Test class for Event. */ class GenericEventTest extends \PHPUnit_Framework_TestCase { /** * @var GenericEvent */ private $event; private $subject; /** * Prepares the environment before running a test. */ protected function setUp() { parent::setUp(); $this->subject = new \StdClass(); $this->event = new GenericEvent($this->subject, array('name' => 'Event'), 'foo'); } /** * Cleans up the environment after running a test. */ protected function tearDown() { $this->subject = null; $this->event = null; parent::tearDown(); } public function testConstruct() { $this->assertEquals($this->event, new GenericEvent($this->subject, array('name' => 'Event'))); } /** * Tests Event->getArgs() */ public function testGetArguments() { // test getting all $this->assertSame(array('name' => 'Event'), $this->event->getArguments()); } public function testSetArguments() { $result = $this->event->setArguments(array('foo' => 'bar')); $this->assertAttributeSame(array('foo' => 'bar'), 'arguments', $this->event); $this->assertSame($this->event, $result); } public function testSetArgument() { $result = $this->event->setArgument('foo2', 'bar2'); $this->assertAttributeSame(array('name' => 'Event', 'foo2' => 'bar2'), 'arguments', $this->event); $this->assertEquals($this->event, $result); } public function testGetArgument() { // test getting key $this->assertEquals('Event', $this->event->getArgument('name')); } /** * @expectedException \InvalidArgumentException */ public function testGetArgException() { $this->event->getArgument('nameNotExist'); } public function testOffsetGet() { // test getting key $this->assertEquals('Event', $this->event['name']); // test getting invalid arg $this->setExpectedException('InvalidArgumentException'); $this->assertFalse($this->event['nameNotExist']); } public function testOffsetSet() { $this->event['foo2'] = 'bar2'; $this->assertAttributeSame(array('name' => 'Event', 'foo2' => 'bar2'), 'arguments', $this->event); } public function testOffsetUnset() { unset($this->event['name']); $this->assertAttributeSame(array(), 'arguments', $this->event); } public function testOffsetIsset() { $this->assertTrue(isset($this->event['name'])); $this->assertFalse(isset($this->event['nameNotExist'])); } public function testHasArgument() { $this->assertTrue($this->event->hasArgument('name')); $this->assertFalse($this->event->hasArgument('nameNotExist')); } public function testGetSubject() { $this->assertSame($this->subject, $this->event->getSubject()); } public function testHasIterator() { $data = array(); foreach ($this->event as $key => $value) { $data[$key] = $value; } $this->assertEquals(array('name' => 'Event'), $data); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\ImmutableEventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * @author Bernhard Schussek */ class ImmutableEventDispatcherTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPUnit_Framework_MockObject_MockObject */ private $innerDispatcher; /** * @var ImmutableEventDispatcher */ private $dispatcher; protected function setUp() { $this->innerDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->dispatcher = new ImmutableEventDispatcher($this->innerDispatcher); } public function testDispatchDelegates() { $event = new Event(); $this->innerDispatcher->expects($this->once()) ->method('dispatch') ->with('event', $event) ->will($this->returnValue('result')); $this->assertSame('result', $this->dispatcher->dispatch('event', $event)); } public function testGetListenersDelegates() { $this->innerDispatcher->expects($this->once()) ->method('getListeners') ->with('event') ->will($this->returnValue('result')); $this->assertSame('result', $this->dispatcher->getListeners('event')); } public function testHasListenersDelegates() { $this->innerDispatcher->expects($this->once()) ->method('hasListeners') ->with('event') ->will($this->returnValue('result')); $this->assertSame('result', $this->dispatcher->hasListeners('event')); } /** * @expectedException \BadMethodCallException */ public function testAddListenerDisallowed() { $this->dispatcher->addListener('event', function () { return 'foo'; }); } /** * @expectedException \BadMethodCallException */ public function testAddSubscriberDisallowed() { $subscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); $this->dispatcher->addSubscriber($subscriber); } /** * @expectedException \BadMethodCallException */ public function testRemoveListenerDisallowed() { $this->dispatcher->removeListener('event', function () { return 'foo'; }); } /** * @expectedException \BadMethodCallException */ public function testRemoveSubscriberDisallowed() { $subscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); $this->dispatcher->removeSubscriber($subscriber); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * Marker Interface for the Process Component. * * @author Johannes M. Schmitt */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * InvalidArgumentException for the Process Component. * * @author Romain Neutron */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * LogicException for the Process Component. * * @author Romain Neutron */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception for failed processes. * * @author Johannes M. Schmitt */ class ProcessFailedException extends RuntimeException { private $process; public function __construct(Process $process) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } parent::__construct( sprintf( 'The command "%s" failed.'."\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getCommandLine(), $process->getOutput(), $process->getErrorOutput() ) ); $this->process = $process; } public function getProcess() { return $this->process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * RuntimeException for the Process Component. * * @author Johannes M. Schmitt */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * Generic executable finder. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class ExecutableFinder { private $suffixes = array('.exe', '.bat', '.cmd', '.com'); /** * Replaces default suffixes of executable. * * @param array $suffixes */ public function setSuffixes(array $suffixes) { $this->suffixes = $suffixes; } /** * Adds new possible suffix to check for executable. * * @param string $suffix */ public function addSuffix($suffix) { $this->suffixes[] = $suffix; } /** * Finds an executable by name. * * @param string $name The executable name (without the extension) * @param string $default The default to return if no executable is found * @param array $extraDirs Additional dirs to check into * * @return string The executable path or default value */ public function find($name, $default = null, array $extraDirs = array()) { if (ini_get('open_basedir')) { $searchPath = explode(PATH_SEPARATOR, getenv('open_basedir')); $dirs = array(); foreach ($searchPath as $path) { if (is_dir($path)) { $dirs[] = $path; } else { $file = str_replace(dirname($path), '', $path); if ($file == $name && is_executable($path)) { return $path; } } } } else { $dirs = array_merge( explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs ); } $suffixes = array(''); if (defined('PHP_WINDOWS_VERSION_BUILD')) { $pathExt = getenv('PATHEXT'); $suffixes = $pathExt ? explode(PATH_SEPARATOR, $pathExt) : $this->suffixes; } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if (is_file($file = $dir.DIRECTORY_SEPARATOR.$name.$suffix) && (defined('PHP_WINDOWS_VERSION_BUILD') || is_executable($file))) { return $file; } } } return $default; } } Copyright (c) 2004-2013 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * An executable finder specifically designed for the PHP executable. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class PhpExecutableFinder { private $executableFinder; public function __construct() { $this->executableFinder = new ExecutableFinder(); } /** * Finds The PHP executable. * * @return string|false The PHP executable path or false if it cannot be found */ public function find() { // PHP_BINARY return the current sapi executable if (defined('PHP_BINARY') && PHP_BINARY && ('cli' === PHP_SAPI)) { return PHP_BINARY; } if ($php = getenv('PHP_PATH')) { if (!is_executable($php)) { return false; } return $php; } if ($php = getenv('PHP_PEAR_PHP_BIN')) { if (is_executable($php)) { return $php; } } $dirs = array(PHP_BINDIR); if (defined('PHP_WINDOWS_VERSION_BUILD')) { $dirs[] = 'C:\xampp\php\\'; } return $this->executableFinder->find('php', false, $dirs); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\RuntimeException; /** * PhpProcess runs a PHP script in an independent process. * * $p = new PhpProcess(''); * $p->run(); * print $p->getOutput()."\n"; * * @author Fabien Potencier * * @api */ class PhpProcess extends Process { private $executableFinder; /** * Constructor. * * @param string $script The PHP script to run (as a string) * @param string $cwd The working directory * @param array $env The environment variables * @param integer $timeout The timeout in seconds * @param array $options An array of options for proc_open * * @api */ public function __construct($script, $cwd = null, array $env = array(), $timeout = 60, array $options = array()) { parent::__construct(null, $cwd, $env, $script, $timeout, $options); $this->executableFinder = new PhpExecutableFinder(); } /** * Sets the path to the PHP binary to use. * * @api */ public function setPhpBinary($php) { $this->setCommandLine($php); } /** * {@inheritdoc} */ public function start($callback = null) { if (null === $this->getCommandLine()) { if (false === $php = $this->executableFinder->find()) { throw new RuntimeException('Unable to find the PHP executable.'); } $this->setCommandLine($php); } parent::start($callback); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\RuntimeException; /** * Process is a thin wrapper around proc_* functions to ease * start independent PHP processes. * * @author Fabien Potencier * * @api */ class Process { const ERR = 'err'; const OUT = 'out'; const STATUS_READY = 'ready'; const STATUS_STARTED = 'started'; const STATUS_TERMINATED = 'terminated'; const STDIN = 0; const STDOUT = 1; const STDERR = 2; private $commandline; private $cwd; private $env; private $stdin; private $timeout; private $options; private $exitcode; private $fallbackExitcode; private $processInformation; private $stdout; private $stderr; private $enhanceWindowsCompatibility; private $enhanceSigchildCompatibility; private $pipes; private $process; private $status = self::STATUS_READY; private $incrementalOutputOffset; private $incrementalErrorOutputOffset; private $fileHandles; private $readBytes; private static $sigchild; /** * Exit codes translation table. * * User-defined errors must use exit codes in the 64-113 range. * * @var array */ public static $exitCodes = array( 0 => 'OK', 1 => 'General error', 2 => 'Misuse of shell builtins', 126 => 'Invoked command cannot execute', 127 => 'Command not found', 128 => 'Invalid exit argument', // signals 129 => 'Hangup', 130 => 'Interrupt', 131 => 'Quit and dump core', 132 => 'Illegal instruction', 133 => 'Trace/breakpoint trap', 134 => 'Process aborted', 135 => 'Bus error: "access to undefined portion of memory object"', 136 => 'Floating point exception: "erroneous arithmetic operation"', 137 => 'Kill (terminate immediately)', 138 => 'User-defined 1', 139 => 'Segmentation violation', 140 => 'User-defined 2', 141 => 'Write to pipe with no one reading', 142 => 'Signal raised by alarm', 143 => 'Termination (request to terminate)', // 144 - not defined 145 => 'Child process terminated, stopped (or continued*)', 146 => 'Continue if stopped', 147 => 'Stop executing temporarily', 148 => 'Terminal stop signal', 149 => 'Background process attempting to read from tty ("in")', 150 => 'Background process attempting to write to tty ("out")', 151 => 'Urgent data available on socket', 152 => 'CPU time limit exceeded', 153 => 'File size limit exceeded', 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', 155 => 'Profiling timer expired', // 156 - not defined 157 => 'Pollable event', // 158 - not defined 159 => 'Bad syscall', ); /** * Constructor. * * @param string $commandline The command line to run * @param string $cwd The working directory * @param array $env The environment variables or null to inherit * @param string $stdin The STDIN content * @param integer $timeout The timeout in seconds * @param array $options An array of options for proc_open * * @throws RuntimeException When proc_open is not installed * * @api */ public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) { if (!function_exists('proc_open')) { throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); } $this->commandline = $commandline; $this->cwd = $cwd; // on windows, if the cwd changed via chdir(), proc_open defaults to the dir where php was started if (null === $this->cwd && defined('PHP_WINDOWS_VERSION_BUILD')) { $this->cwd = getcwd(); } if (null !== $env) { $this->env = array(); foreach ($env as $key => $value) { $this->env[(binary) $key] = (binary) $value; } } else { $this->env = null; } $this->stdin = $stdin; $this->setTimeout($timeout); $this->enhanceWindowsCompatibility = true; $this->enhanceSigchildCompatibility = !defined('PHP_WINDOWS_VERSION_BUILD') && $this->isSigchildEnabled(); $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); } public function __destruct() { // stop() will check if we have a process running. $this->stop(); } public function __clone() { $this->exitcode = null; $this->fallbackExitcode = null; $this->processInformation = null; $this->stdout = null; $this->stderr = null; $this->pipes = null; $this->process = null; $this->status = self::STATUS_READY; $this->fileHandles = null; $this->readBytes = null; } /** * Runs the process. * * The callback receives the type of output (out or err) and * some bytes from the output in real-time. It allows to have feedback * from the independent process during execution. * * The STDOUT and STDERR are also available after the process is finished * via the getOutput() and getErrorOutput() methods. * * @param callback|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return integer The exit status code * * @throws RuntimeException When process can't be launch or is stopped * * @api */ public function run($callback = null) { $this->start($callback); return $this->wait($callback); } /** * Starts the process and returns after sending the STDIN. * * This method blocks until all STDIN data is sent to the process then it * returns while the process runs in the background. * * The termination of the process can be awaited with wait(). * * The callback receives the type of output (out or err) and some bytes from * the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * If there is no callback passed, the wait() method can be called * with true as a second parameter then the callback will get all data occurred * in (and since) the start call. * * @param callback|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @throws RuntimeException When process can't be launch or is stopped * @throws RuntimeException When process is already running */ public function start($callback = null) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } $this->stdout = ''; $this->stderr = ''; $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; $callback = $this->buildCallback($callback); //Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. //Workaround for this problem is to use temporary files instead of pipes on Windows platform. //@see https://bugs.php.net/bug.php?id=51800 if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->fileHandles = array( self::STDOUT => tmpfile(), ); if (false === $this->fileHandles[self::STDOUT]) { throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); } $this->readBytes = array( self::STDOUT => 0, ); $descriptors = array(array('pipe', 'r'), $this->fileHandles[self::STDOUT], array('pipe', 'w')); } else { $descriptors = array( array('pipe', 'r'), // stdin array('pipe', 'w'), // stdout array('pipe', 'w'), // stderr ); if ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors = array_merge($descriptors, array(array('pipe', 'w'))); $this->commandline = '('.$this->commandline.') 3>/dev/null; code=$?; echo $code >&3; exit $code'; } } $commandline = $this->commandline; if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) { $commandline = 'cmd /V:ON /E:ON /C "'.$commandline.'"'; if (!isset($this->options['bypass_shell'])) { $this->options['bypass_shell'] = true; } } $this->process = proc_open($commandline, $descriptors, $this->pipes, $this->cwd, $this->env, $this->options); if (!is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; foreach ($this->pipes as $pipe) { stream_set_blocking($pipe, false); } if (null === $this->stdin) { fclose($this->pipes[0]); unset($this->pipes[0]); return; } $writePipes = array($this->pipes[0]); unset($this->pipes[0]); $stdinLen = strlen($this->stdin); $stdinOffset = 0; while ($writePipes) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->processFileHandles($callback); } $r = $this->pipes; $w = $writePipes; $e = null; $n = @stream_select($r, $w, $e, $this->timeout); if (false === $n) { break; } if ($n === 0) { proc_terminate($this->process); throw new RuntimeException('The process timed out.'); } if ($w) { $written = fwrite($writePipes[0], (binary) substr($this->stdin, $stdinOffset), 8192); if (false !== $written) { $stdinOffset += $written; } if ($stdinOffset >= $stdinLen) { fclose($writePipes[0]); $writePipes = null; } } foreach ($r as $pipe) { $type = array_search($pipe, $this->pipes); $data = fread($pipe, 8192); if (strlen($data) > 0) { call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data); } if (false === $data || feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } } $this->updateStatus(); } /** * Restarts the process. * * Be warned that the process is cloned before being started. * * @param callable $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return Process The new process * * @throws \RuntimeException When process can't be launch or is stopped * @throws \RuntimeException When process is already running * * @see start() */ public function restart($callback = null) { if ($this->isRunning()) { throw new \RuntimeException('Process is already running'); } $process = clone $this; $process->start($callback); return $process; } /** * Waits for the process to terminate. * * The callback receives the type of output (out or err) and some bytes * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callback|null $callback A valid PHP callback * * @return integer The exitcode of the process * * @throws \RuntimeException When process timed out * @throws \RuntimeException When process stopped after receiving signal */ public function wait($callback = null) { $this->updateStatus(); $callback = $this->buildCallback($callback); while ($this->pipes || (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles)) { if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) { $this->processFileHandles($callback, !$this->pipes); } if ($this->pipes) { $r = $this->pipes; $w = null; $e = null; if (false === $n = @stream_select($r, $w, $e, $this->timeout)) { $lastError = error_get_last(); // stream_select returns false when the `select` system call is interrupted by an incoming signal if (isset($lastError['message']) && false === stripos($lastError['message'], 'interrupted system call')) { $this->pipes = array(); } continue; } if (0 === $n) { proc_terminate($this->process); throw new RuntimeException('The process timed out.'); } foreach ($r as $pipe) { $type = array_search($pipe, $this->pipes); $data = fread($pipe, 8192); if (strlen($data) > 0) { // last exit code is output and caught to work around --enable-sigchild if (3 == $type) { $this->fallbackExitcode = (int) $data; } else { call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data); } } if (false === $data || feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } } } $this->updateStatus(); if ($this->processInformation['signaled']) { throw new RuntimeException(sprintf('The process stopped because of a "%s" signal.', $this->processInformation['stopsig'])); } $time = 0; while ($this->isRunning() && $time < 1000000) { $time += 1000; usleep(1000); } $exitcode = proc_close($this->process); if ($this->processInformation['signaled']) { throw new RuntimeException(sprintf('The process stopped because of a "%s" signal.', $this->processInformation['stopsig'])); } $this->exitcode = $this->processInformation['running'] ? $exitcode : $this->processInformation['exitcode']; if (-1 == $this->exitcode && null !== $this->fallbackExitcode) { $this->exitcode = $this->fallbackExitcode; } return $this->exitcode; } /** * Returns the current output of the process (STDOUT). * * @return string The process output * * @api */ public function getOutput() { $this->updateOutput(); return $this->stdout; } /** * Returns the output incrementally. * * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * * @return string The process output since the last call */ public function getIncrementalOutput() { $data = $this->getOutput(); $latest = substr($data, $this->incrementalOutputOffset); $this->incrementalOutputOffset = strlen($data); return $latest; } /** * Returns the current error output of the process (STDERR). * * @return string The process error output * * @api */ public function getErrorOutput() { $this->updateErrorOutput(); return $this->stderr; } /** * Returns the errorOutput incrementally. * * In comparison with the getErrorOutput method which always return the * whole error output, this one returns the new error output since the last * call. * * @return string The process error output since the last call */ public function getIncrementalErrorOutput() { $data = $this->getErrorOutput(); $latest = substr($data, $this->incrementalErrorOutputOffset); $this->incrementalErrorOutputOffset = strlen($data); return $latest; } /** * Returns the exit code returned by the process. * * @return integer The exit status code * * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled * * @api */ public function getExitCode() { if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method'); } $this->updateStatus(); return $this->exitcode; } /** * Returns a string representation for the exit code returned by the process. * * This method relies on the Unix exit code status standardization * and might not be relevant for other operating systems. * * @return string A string representation for the exit status code * * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ public function getExitCodeText() { $exitcode = $this->getExitCode(); return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; } /** * Checks if the process ended successfully. * * @return Boolean true if the process ended successfully, false otherwise * * @api */ public function isSuccessful() { return 0 == $this->getExitCode(); } /** * Returns true if the child process has been terminated by an uncaught signal. * * It always returns false on Windows. * * @return Boolean * * @throws RuntimeException In case --enable-sigchild is activated * * @api */ public function hasBeenSignaled() { if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); } $this->updateStatus(); return $this->processInformation['signaled']; } /** * Returns the number of the signal that caused the child process to terminate its execution. * * It is only meaningful if hasBeenSignaled() returns true. * * @return integer * * @throws RuntimeException In case --enable-sigchild is activated * * @api */ public function getTermSignal() { if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); } $this->updateStatus(); return $this->processInformation['termsig']; } /** * Returns true if the child process has been stopped by a signal. * * It always returns false on Windows. * * @return Boolean * * @api */ public function hasBeenStopped() { $this->updateStatus(); return $this->processInformation['stopped']; } /** * Returns the number of the signal that caused the child process to stop its execution. * * It is only meaningful if hasBeenStopped() returns true. * * @return integer * * @api */ public function getStopSignal() { $this->updateStatus(); return $this->processInformation['stopsig']; } /** * Checks if the process is currently running. * * @return Boolean true if the process is currently running, false otherwise */ public function isRunning() { if (self::STATUS_STARTED !== $this->status) { return false; } $this->updateStatus(); return $this->processInformation['running']; } /** * Checks if the process has been started with no regard to the current state. * * @return Boolean true if status is ready, false otherwise */ public function isStarted() { return $this->status != self::STATUS_READY; } /** * Checks if the process is terminated. * * @return Boolean true if process is terminated, false otherwise */ public function isTerminated() { $this->updateStatus(); return $this->status == self::STATUS_TERMINATED; } /** * Gets the process status. * * The status is one of: ready, started, terminated. * * @return string The current process status */ public function getStatus() { $this->updateStatus(); return $this->status; } /** * Stops the process. * * @param integer|float $timeout The timeout in seconds * * @return integer The exit-code of the process * * @throws RuntimeException if the process got signaled */ public function stop($timeout = 10) { $timeoutMicro = (int) $timeout*10E6; if ($this->isRunning()) { proc_terminate($this->process); $time = 0; while (1 == $this->isRunning() && $time < $timeoutMicro) { $time += 1000; usleep(1000); } foreach ($this->pipes as $pipe) { fclose($pipe); } $this->pipes = array(); $exitcode = proc_close($this->process); $this->exitcode = -1 === $this->processInformation['exitcode'] ? $exitcode : $this->processInformation['exitcode']; if (defined('PHP_WINDOWS_VERSION_BUILD')) { foreach ($this->fileHandles as $fileHandle) { fclose($fileHandle); } $this->fileHandles = array(); } } $this->status = self::STATUS_TERMINATED; return $this->exitcode; } /** * Adds a line to the STDOUT stream. * * @param string $line The line to append */ public function addOutput($line) { $this->stdout .= $line; } /** * Adds a line to the STDERR stream. * * @param string $line The line to append */ public function addErrorOutput($line) { $this->stderr .= $line; } /** * Gets the command line to be executed. * * @return string The command to execute */ public function getCommandLine() { return $this->commandline; } /** * Sets the command line to be executed. * * @param string $commandline The command to execute * * @return self The current Process instance */ public function setCommandLine($commandline) { $this->commandline = $commandline; return $this; } /** * Gets the process timeout. * * @return integer|null The timeout in seconds or null if it's disabled */ public function getTimeout() { return $this->timeout; } /** * Sets the process timeout. * * To disable the timeout, set this value to null. * * @param integer|null $timeout The timeout in seconds * * @return self The current Process instance * * @throws InvalidArgumentException if the timeout is negative */ public function setTimeout($timeout) { if (null === $timeout) { $this->timeout = null; return $this; } $timeout = (integer) $timeout; if ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer.'); } $this->timeout = $timeout; return $this; } /** * Gets the working directory. * * @return string The current working directory */ public function getWorkingDirectory() { // This is for BC only if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have // the readable or search mode set, even if the current directory does return getcwd() ?: null; } return $this->cwd; } /** * Sets the current working directory. * * @param string $cwd The new working directory * * @return self The current Process instance */ public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } /** * Gets the environment variables. * * @return array The current environment variables */ public function getEnv() { return $this->env; } /** * Sets the environment variables. * * @param array $env The new environment variables * * @return self The current Process instance */ public function setEnv(array $env) { $this->env = $env; return $this; } /** * Gets the contents of STDIN. * * @return string The current contents */ public function getStdin() { return $this->stdin; } /** * Sets the contents of STDIN. * * @param string $stdin The new contents * * @return self The current Process instance */ public function setStdin($stdin) { $this->stdin = $stdin; return $this; } /** * Gets the options for proc_open. * * @return array The current options */ public function getOptions() { return $this->options; } /** * Sets the options for proc_open. * * @param array $options The new options * * @return self The current Process instance */ public function setOptions(array $options) { $this->options = $options; return $this; } /** * Gets whether or not Windows compatibility is enabled * * This is true by default. * * @return Boolean */ public function getEnhanceWindowsCompatibility() { return $this->enhanceWindowsCompatibility; } /** * Sets whether or not Windows compatibility is enabled * * @param Boolean $enhance * * @return self The current Process instance */ public function setEnhanceWindowsCompatibility($enhance) { $this->enhanceWindowsCompatibility = (Boolean) $enhance; return $this; } /** * Return whether sigchild compatibility mode is activated or not * * @return Boolean */ public function getEnhanceSigchildCompatibility() { return $this->enhanceSigchildCompatibility; } /** * Activate sigchild compatibility mode * * Sigchild compatibility mode is required to get the exit code and * determine the success of a process when PHP has been compiled with * the --enable-sigchild option * * @param Boolean $enhance * * @return self The current Process instance */ public function setEnhanceSigchildCompatibility($enhance) { $this->enhanceSigchildCompatibility = (Boolean) $enhance; return $this; } /** * Builds up the callback used by wait(). * * The callbacks adds all occurred output to the specific buffer and calls * the user callback (if present) with the received output. * * @param callback|null $callback The user defined PHP callback * * @return callback A PHP callable */ protected function buildCallback($callback) { $that = $this; $out = self::OUT; $err = self::ERR; $callback = function ($type, $data) use ($that, $callback, $out, $err) { if ($out == $type) { $that->addOutput($data); } else { $that->addErrorOutput($data); } if (null !== $callback) { call_user_func($callback, $type, $data); } }; return $callback; } /** * Updates the status of the process. */ protected function updateStatus() { if (self::STATUS_STARTED !== $this->status) { return; } $this->processInformation = proc_get_status($this->process); if (!$this->processInformation['running']) { $this->status = self::STATUS_TERMINATED; if (-1 !== $this->processInformation['exitcode']) { $this->exitcode = $this->processInformation['exitcode']; } } } /** * Updates the current error output of the process (STDERR). */ protected function updateErrorOutput() { if (isset($this->pipes[self::STDERR]) && is_resource($this->pipes[self::STDERR])) { $this->addErrorOutput(stream_get_contents($this->pipes[self::STDERR])); } } /** * Updates the current output of the process (STDOUT). */ protected function updateOutput() { if (defined('PHP_WINDOWS_VERSION_BUILD') && isset($this->fileHandles[self::STDOUT]) && is_resource($this->fileHandles[self::STDOUT])) { fseek($this->fileHandles[self::STDOUT], $this->readBytes[self::STDOUT]); $this->addOutput(stream_get_contents($this->fileHandles[self::STDOUT])); } elseif (isset($this->pipes[self::STDOUT]) && is_resource($this->pipes[self::STDOUT])) { $this->addOutput(stream_get_contents($this->pipes[self::STDOUT])); } } /** * Return whether PHP has been compiled with the '--enable-sigchild' option or not * * @return Boolean */ protected function isSigchildEnabled() { if (null !== self::$sigchild) { return self::$sigchild; } ob_start(); phpinfo(INFO_GENERAL); return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } /** * Handles the windows file handles fallbacks * * @param callable $callback A valid PHP callback * @param Boolean $closeEmptyHandles if true, handles that are empty will be assumed closed */ private function processFileHandles($callback, $closeEmptyHandles = false) { $fh = $this->fileHandles; foreach ($fh as $type => $fileHandle) { fseek($fileHandle, $this->readBytes[$type]); $data = fread($fileHandle, 8192); if (strlen($data) > 0) { $this->readBytes[$type] += strlen($data); call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data); } if (false === $data || ($closeEmptyHandles && '' === $data && feof($fileHandle))) { fclose($fileHandle); unset($this->fileHandles[$type]); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; /** * Process builder. * * @author Kris Wallsmith */ class ProcessBuilder { private $arguments; private $cwd; private $env; private $stdin; private $timeout; private $options; private $inheritEnv; public function __construct(array $arguments = array()) { $this->arguments = $arguments; $this->timeout = 60; $this->options = array(); $this->env = array(); $this->inheritEnv = true; } public static function create(array $arguments = array()) { return new static($arguments); } /** * Adds an unescaped argument to the command string. * * @param string $argument A command argument * * @return ProcessBuilder */ public function add($argument) { $this->arguments[] = $argument; return $this; } /** * @param array $arguments * * @return ProcessBuilder */ public function setArguments(array $arguments) { $this->arguments = $arguments; return $this; } public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } public function inheritEnvironmentVariables($inheritEnv = true) { $this->inheritEnv = $inheritEnv; return $this; } public function setEnv($name, $value) { $this->env[$name] = $value; return $this; } public function setInput($stdin) { $this->stdin = $stdin; return $this; } /** * Sets the process timeout. * * To disable the timeout, set this value to null. * * @param integer|null * * @return ProcessBuilder * * @throws InvalidArgumentException */ public function setTimeout($timeout) { if (null === $timeout) { $this->timeout = null; return $this; } $timeout = (integer) $timeout; if ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer.'); } $this->timeout = $timeout; return $this; } public function setOption($name, $value) { $this->options[$name] = $value; return $this; } public function getProcess() { if (!count($this->arguments)) { throw new LogicException('You must add() command arguments before calling getProcess().'); } $options = $this->options; $script = implode(' ', array_map('escapeshellarg', $this->arguments)); if ($this->inheritEnv) { $env = $this->env ? $this->env + $_ENV : null; } else { $env = $this->env; } return new Process($script, $this->cwd, $env, $this->stdin, $this->timeout, $options); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Process; /** * @author Robert Schönthal */ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase { /** * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException */ public function testNegativeTimeoutFromConstructor() { $this->getProcess('', null, null, null, -1); } /** * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException */ public function testNegativeTimeoutFromSetter() { $p = $this->getProcess(''); $p->setTimeout(-1); } public function testNullTimeout() { $p = $this->getProcess(''); $p->setTimeout(10); $p->setTimeout(null); $this->assertNull($p->getTimeout()); } /** * tests results from sub processes * * @dataProvider responsesCodeProvider */ public function testProcessResponses($expected, $getter, $code) { $p = $this->getProcess(sprintf('php -r %s', escapeshellarg($code))); $p->run(); $this->assertSame($expected, $p->$getter()); } /** * tests results from sub processes * * @dataProvider pipesCodeProvider */ public function testProcessPipes($expected, $code) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Test hangs on Windows & PHP due to https://bugs.php.net/bug.php?id=60120 and https://bugs.php.net/bug.php?id=51800'); } $p = $this->getProcess(sprintf('php -r %s', escapeshellarg($code))); $p->setStdin($expected); $p->run(); $this->assertSame($expected, $p->getOutput()); $this->assertSame($expected, $p->getErrorOutput()); } public function chainedCommandsOutputProvider() { return array( array("1\n1\n", ';', '1'), array("2\n2\n", '&&', '2'), ); } /** * * @dataProvider chainedCommandsOutputProvider */ public function testChainedCommandsOutput($expected, $operator, $input) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Does it work on windows ?'); } $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); $process->run(); $this->assertEquals($expected, $process->getOutput()); } public function testCallbackIsExecutedForOutput() { $p = $this->getProcess(sprintf('php -r %s', escapeshellarg('echo \'foo\';'))); $called = false; $p->run(function ($type, $buffer) use (&$called) { $called = $buffer === 'foo'; }); $this->assertTrue($called, 'The callback should be executed with the output'); } public function testGetErrorOutput() { $p = new Process(sprintf('php -r %s', escapeshellarg('ini_set(\'display_errors\',\'on\'); $n = 0; while ($n < 3) { echo $a; $n++; }'))); $p->run(); $this->assertEquals(3, preg_match_all('/PHP Notice/', $p->getErrorOutput(), $matches)); } public function testGetIncrementalErrorOutput() { $p = new Process(sprintf('php -r %s', escapeshellarg('ini_set(\'display_errors\',\'on\'); usleep(50000); $n = 0; while ($n < 3) { echo $a; $n++; }'))); $p->start(); while ($p->isRunning()) { $this->assertLessThanOrEqual(1, preg_match_all('/PHP Notice/', $p->getIncrementalOutput(), $matches)); usleep(20000); } } public function testGetOutput() { $p = new Process(sprintf('php -r %s', escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}'))); $p->run(); $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); } public function testGetIncrementalOutput() { $p = new Process(sprintf('php -r %s', escapeshellarg('$n=0;while ($n<3) { echo \' foo \'; usleep(50000); $n++; }'))); $p->start(); while ($p->isRunning()) { $this->assertLessThanOrEqual(1, preg_match_all('/foo/', $p->getIncrementalOutput(), $matches)); usleep(20000); } } public function testExitCodeCommandFailed() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not support POSIX exit code'); } // such command run in bash return an exitcode 127 $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis'); $process->run(); $this->assertGreaterThan(0, $process->getExitCode()); } public function testExitCodeText() { $process = $this->getProcess(''); $r = new \ReflectionObject($process); $p = $r->getProperty('exitcode'); $p->setAccessible(true); $p->setValue($process, 2); $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText()); } public function testStartIsNonBlocking() { $process = $this->getProcess('php -r "sleep(4);"'); $start = microtime(true); $process->start(); $end = microtime(true); $this->assertLessThan(1 , $end-$start); } public function testUpdateStatus() { $process = $this->getProcess('php -h'); $process->run(); $this->assertTrue(strlen($process->getOutput()) > 0); } public function testGetExitCode() { $process = $this->getProcess('php -m'); $process->run(); $this->assertEquals(0, $process->getExitCode()); } public function testStatus() { $process = $this->getProcess('php -r "sleep(1);"'); $this->assertFalse($process->isRunning()); $this->assertFalse($process->isStarted()); $this->assertFalse($process->isTerminated()); $this->assertSame(Process::STATUS_READY, $process->getStatus()); $process->start(); $this->assertTrue($process->isRunning()); $this->assertTrue($process->isStarted()); $this->assertFalse($process->isTerminated()); $this->assertSame(Process::STATUS_STARTED, $process->getStatus()); $process->wait(); $this->assertFalse($process->isRunning()); $this->assertTrue($process->isStarted()); $this->assertTrue($process->isTerminated()); $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); } public function testStop() { $process = $this->getProcess('php -r "while (true) {}"'); $process->start(); $this->assertTrue($process->isRunning()); $process->stop(); $this->assertFalse($process->isRunning()); } public function testIsSuccessful() { $process = $this->getProcess('php -m'); $process->run(); $this->assertTrue($process->isSuccessful()); } public function testIsNotSuccessful() { $process = $this->getProcess('php -r "while (true) {}"'); $process->start(); $this->assertTrue($process->isRunning()); $process->stop(); $this->assertFalse($process->isSuccessful()); } public function testProcessIsNotSignaled() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not support POSIX signals'); } $process = $this->getProcess('php -m'); $process->run(); $this->assertFalse($process->hasBeenSignaled()); } public function testProcessWithoutTermSignal() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not support POSIX signals'); } $process = $this->getProcess('php -m'); $process->run(); $this->assertEquals(0, $process->getTermSignal()); } public function testProcessIsSignaledIfStopped() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not support POSIX signals'); } $process = $this->getProcess('php -r "while (true) {}"'); $process->start(); $process->stop(); $this->assertTrue($process->hasBeenSignaled()); } public function testProcessWithTermSignal() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not support POSIX signals'); } // SIGTERM is only defined if pcntl extension is present $termSignal = defined('SIGTERM') ? SIGTERM : 15; $process = $this->getProcess('php -r "while (true) {}"'); $process->start(); $process->stop(); $this->assertEquals($termSignal, $process->getTermSignal()); } public function testRestart() { $process1 = $this->getProcess('php -r "echo getmypid();"'); $process1->run(); $process2 = $process1->restart(); usleep(300000); // wait for output // Ensure that both processed finished and the output is numeric $this->assertFalse($process1->isRunning()); $this->assertFalse($process2->isRunning()); $this->assertTrue(is_numeric($process1->getOutput())); $this->assertTrue(is_numeric($process2->getOutput())); // Ensure that restart returned a new process by check that the output is different $this->assertNotEquals($process1->getOutput(), $process2->getOutput()); } public function testPhpDeadlock() { $this->markTestSkipped('Can course php to hang'); // Sleep doesn't work as it will allow the process to handle signals and close // file handles from the other end. $process = $this->getProcess('php -r "while (true) {}"'); $process->start(); // PHP will deadlock when it tries to cleanup $process } public function responsesCodeProvider() { return array( //expected output / getter / code to execute //array(1,'getExitCode','exit(1);'), //array(true,'isSuccessful','exit();'), array('output', 'getOutput', 'echo \'output\';'), ); } public function pipesCodeProvider() { $variations = array( 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', 'include \'' . __DIR__ . '/ProcessTestHelper.php\';', ); $baseData = str_repeat('*', 1024); $codes = array(); foreach (array(1, 16, 64, 1024, 4096) as $size) { $data = str_repeat($baseData, $size) . '!'; foreach ($variations as $code) { $codes[] = array($data, $code); } } return $codes; } /** * provides default method names for simple getter/setter */ public function methodProvider() { $defaults = array( array('CommandLine'), array('Timeout'), array('WorkingDirectory'), array('Env'), array('Stdin'), array('Options') ); return $defaults; } /** * @param string $commandline * @param null $cwd * @param array $env * @param null $stdin * @param integer $timeout * @param array $options * * @return Process */ abstract protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\PhpExecutableFinder; /** * @author Robert Schönthal */ class PhpExecutableFinderTest extends \PHPUnit_Framework_TestCase { /** * tests find() with the env var PHP_PATH */ public function testFindWithPhpPath() { if (defined('PHP_BINARY')) { $this->markTestSkipped('The PHP binary is easily available as of PHP 5.4'); } $f = new PhpExecutableFinder(); $current = $f->find(); //not executable PHP_PATH putenv('PHP_PATH=/not/executable/php'); $this->assertFalse($f->find(), '::find() returns false for not executable php'); //executable PHP_PATH putenv('PHP_PATH='.$current); $this->assertEquals($f->find(), $current, '::find() returns the executable php'); } /** * tests find() with default executable */ public function testFindWithSuffix() { if (defined('PHP_BINARY')) { $this->markTestSkipped('The PHP binary is easily available as of PHP 5.4'); } putenv('PHP_PATH='); putenv('PHP_PEAR_PHP_BIN='); $f = new PhpExecutableFinder(); $current = $f->find(); //TODO maybe php executable is custom or even windows if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->assertTrue(is_executable($current)); $this->assertTrue((bool) preg_match('/'.addSlashes(DIRECTORY_SEPARATOR).'php\.(exe|bat|cmd|com)$/i', $current), '::find() returns the executable php with suffixes'); } } } start(); $process->wait(); $this->assertEquals($expected, $process->getOutput()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\ProcessBuilder; class ProcessBuilderTest extends \PHPUnit_Framework_TestCase { /** * @test */ public function shouldInheritEnvironmentVars() { $snapshot = $_ENV; $_ENV = $expected = array('foo' => 'bar'); $pb = new ProcessBuilder(); $pb->add('foo')->inheritEnvironmentVariables(); $proc = $pb->getProcess(); $this->assertNull($proc->getEnv(), '->inheritEnvironmentVariables() copies $_ENV'); $_ENV = $snapshot; } /** * @test */ public function shouldInheritAndOverrideEnvironmentVars() { $snapshot = $_ENV; $_ENV = array('foo' => 'bar', 'bar' => 'baz'); $expected = array('foo' => 'foo', 'bar' => 'baz'); $pb = new ProcessBuilder(); $pb->add('foo')->inheritEnvironmentVariables() ->setEnv('foo', 'foo'); $proc = $pb->getProcess(); $this->assertEquals($expected, $proc->getEnv(), '->inheritEnvironmentVariables() copies $_ENV'); $_ENV = $snapshot; } /** * @test */ public function shouldInheritEnvironmentVarsByDefault() { $pb = new ProcessBuilder(); $proc = $pb->add('foo')->getProcess(); $this->assertNull($proc->getEnv()); } /** * @test */ public function shouldNotReplaceExplicitlySetVars() { $snapshot = $_ENV; $_ENV = array('foo' => 'bar'); $expected = array('foo' => 'baz'); $pb = new ProcessBuilder(); $pb ->setEnv('foo', 'baz') ->inheritEnvironmentVariables() ->add('foo') ; $proc = $pb->getProcess(); $this->assertEquals($expected, $proc->getEnv(), '->inheritEnvironmentVariables() copies $_ENV'); $_ENV = $snapshot; } /** * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException */ public function testNegativeTimeoutFromSetter() { $pb = new ProcessBuilder(); $pb->setTimeout(-1); } public function testNullTimeout() { $pb = new ProcessBuilder(); $pb->setTimeout(10); $pb->setTimeout(null); $r = new \ReflectionObject($pb); $p = $r->getProperty('timeout'); $p->setAccessible(true); $this->assertNull($p->getValue($pb)); } public function testShouldSetArguments() { $pb = new ProcessBuilder(array('initial')); $pb->setArguments(array('second')); $proc = $pb->getProcess(); $this->assertContains("second", $proc->getCommandLine()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Exception\ProcessFailedException; /** * @author Sebastian Marek */ class ProcessFailedExceptionTest extends \PHPUnit_Framework_TestCase { /** * tests ProcessFailedException throws exception if the process was successful */ public function testProcessFailedExceptionThrowsException() { $process = $this->getMock( 'Symfony\Component\Process\Process', array('isSuccessful'), array('php') ); $process->expects($this->once()) ->method('isSuccessful') ->will($this->returnValue(true)); $this->setExpectedException( '\InvalidArgumentException', 'Expected a failed process, but the given process was successful.' ); $exception = new ProcessFailedException($process); } /** * tests ProcessFailedException uses information from process output * to generate exception message */ public function testProcessFailedExceptionPopulatesInformationFromProcessOutput() { $cmd = 'php'; $output = "Command output"; $errorOutput = "FATAL: Unexpected error"; $process = $this->getMock( 'Symfony\Component\Process\Process', array('isSuccessful', 'getOutput', 'getErrorOutput'), array($cmd) ); $process->expects($this->once()) ->method('isSuccessful') ->will($this->returnValue(false)); $process->expects($this->once()) ->method('getOutput') ->will($this->returnValue($output)); $process->expects($this->once()) ->method('getErrorOutput') ->will($this->returnValue($errorOutput)); $exception = new ProcessFailedException($process); $this->assertEquals( "The command \"$cmd\" failed.\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", $exception->getMessage() ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Process; class ProcessInSigchildEnvironment extends Process { protected function isSigchildEnabled() { return true; } } 0) { $written = fwrite(STDOUT, (binary) $out, 1024); if (false === $written) { die(ERR_WRITE_FAILED); } $out = (binary) substr($out, $written); } if (null === $read && strlen($out) < 1) { $write = array_diff($write, array(STDOUT)); } if (in_array(STDERR, $w) && strlen($err) > 0) { $written = fwrite(STDERR, (binary) $err, 1024); if (false === $written) { die(ERR_WRITE_FAILED); } $err = (binary) substr($err, $written); } if (null === $read && strlen($err) < 1) { $write = array_diff($write, array(STDERR)); } if ($r) { $str = fread(STDIN, 1024); if (false !== $str) { $out .= $str; $err .= $str; } if (false === $str || feof(STDIN)) { $read = null; if (!feof(STDIN)) { die(ERR_READ_FAILED); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; class SigchildDisabledProcessTest extends AbstractProcessTest { /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testGetExitCode() { parent::testGetExitCode(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testExitCodeCommandFailed() { parent::testExitCodeCommandFailed(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessIsSignaledIfStopped() { parent::testProcessIsSignaledIfStopped(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessWithTermSignal() { parent::testProcessWithTermSignal(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessIsNotSignaled() { parent::testProcessIsNotSignaled(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessWithoutTermSignal() { parent::testProcessWithoutTermSignal(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testExitCodeText() { $process = $this->getProcess('qdfsmfkqsdfmqmsd'); $process->run(); $process->getExitCodeText(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testIsSuccessful() { parent::testIsSuccessful(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testIsNotSuccessful() { parent::testIsNotSuccessful(); } /** * {@inheritdoc} */ protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) { $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $stdin, $timeout, $options); $process->setEnhanceSigchildCompatibility(false); return $process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; class SigchildEnabledProcessTest extends AbstractProcessTest { /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessIsSignaledIfStopped() { parent::testProcessIsSignaledIfStopped(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessWithTermSignal() { parent::testProcessWithTermSignal(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessIsNotSignaled() { parent::testProcessIsNotSignaled(); } /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ public function testProcessWithoutTermSignal() { parent::testProcessWithoutTermSignal(); } public function testExitCodeText() { $process = $this->getProcess('qdfsmfkqsdfmqmsd'); $process->run(); $this->assertInternalType('string', $process->getExitCodeText()); } /** * {@inheritdoc} */ protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) { $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $stdin, $timeout, $options); $process->setEnhanceSigchildCompatibility(true); return $process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Process; class SimpleProcessTest extends AbstractProcessTest { private $enabledSigchild = false; public function setUp() { ob_start(); phpinfo(INFO_GENERAL); $this->enabledSigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } public function testGetExitCode() { $this->skipIfPHPSigchild(); parent::testGetExitCode(); } public function testExitCodeCommandFailed() { $this->skipIfPHPSigchild(); parent::testExitCodeCommandFailed(); } public function testProcessIsSignaledIfStopped() { $this->skipIfPHPSigchild(); parent::testProcessIsSignaledIfStopped(); } public function testProcessWithTermSignal() { $this->skipIfPHPSigchild(); parent::testProcessWithTermSignal(); } public function testProcessIsNotSignaled() { $this->skipIfPHPSigchild(); parent::testProcessIsNotSignaled(); } public function testProcessWithoutTermSignal() { $this->skipIfPHPSigchild(); parent::testProcessWithoutTermSignal(); } public function testExitCodeText() { $this->skipIfPHPSigchild(); parent::testExitCodeText(); } public function testIsSuccessful() { $this->skipIfPHPSigchild(); parent::testIsSuccessful(); } public function testIsNotSuccessful() { $this->skipIfPHPSigchild(); parent::testIsNotSuccessful(); } /** * {@inheritdoc} */ protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) { return new Process($commandline, $cwd, $env, $stdin, $timeout, $options); } private function skipIfPHPSigchild() { if ($this->enabledSigchild) { $this->markTestSkipped('Your PHP has been compiled with --enable-sigchild, this test can not be executed'); } } } $vendorDir . '/instaclick/php-webdriver/lib/', 'Symfony\\Component\\Process\\' => $vendorDir . '/symfony/process/', 'Symfony\\Component\\Finder\\' => $vendorDir . '/symfony/finder/', 'Symfony\\Component\\EventDispatcher\\' => $vendorDir . '/symfony/event-dispatcher/', 'Symfony\\Component\\DomCrawler\\' => $vendorDir . '/symfony/dom-crawler/', 'Symfony\\Component\\CssSelector\\' => $vendorDir . '/symfony/css-selector/', 'Symfony\\Component\\BrowserKit\\' => $vendorDir . '/symfony/browser-kit/', 'Selenium' => $vendorDir . '/alexandresalome/php-selenium/src/', 'Guzzle\\Tests' => $vendorDir . '/guzzle/guzzle/tests/', 'Guzzle' => $vendorDir . '/guzzle/guzzle/src/', 'Goutte' => $vendorDir . '/fabpot/goutte/.', 'Buzz' => $vendorDir . '/kriswallsmith/buzz/lib/', 'Behat\\SahiClient' => $vendorDir . '/behat/sahi-client/src/', 'Behat\\Mink\\Driver' => array($vendorDir . '/behat/mink-browserkit-driver/src/', $vendorDir . '/behat/mink-goutte-driver/src/', $vendorDir . '/behat/mink-sahi-driver/src/', $vendorDir . '/behat/mink-selenium-driver/src/', $vendorDir . '/behat/mink-selenium2-driver/src/', $vendorDir . '/behat/mink-zombie-driver/src/'), 'Behat\\Mink' => $baseDir . '/src/', ); $path) { $loader->add($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } $loader->register(true); return $loader; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0 class loader * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier * @author Jordi Boggiano */ class ClassLoader { private $prefixes = array(); private $fallbackDirs = array(); private $useIncludePath = false; private $classMap = array(); public function getPrefixes() { return $this->prefixes; } public function getFallbackDirs() { return $this->fallbackDirs; } public function getClassMap() { return $this->classMap; } /** * @param array $classMap Class to filename map */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of classes, merging with any others previously set. * * @param string $prefix The classes prefix * @param array|string $paths The location(s) of the classes * @param bool $prepend Prepend the location(s) */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirs = array_merge( (array) $paths, $this->fallbackDirs ); } else { $this->fallbackDirs = array_merge( $this->fallbackDirs, (array) $paths ); } return; } if (!isset($this->prefixes[$prefix])) { $this->prefixes[$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixes[$prefix] = array_merge( (array) $paths, $this->prefixes[$prefix] ); } else { $this->prefixes[$prefix] = array_merge( $this->prefixes[$prefix], (array) $paths ); } } /** * Registers a set of classes, replacing any others previously set. * * @param string $prefix The classes prefix * @param array|string $paths The location(s) of the classes */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirs = (array) $paths; return; } $this->prefixes[$prefix] = (array) $paths; } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); } /** * Unregisters this instance as an autoloader. */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); } /** * Loads the given class or interface. * * @param string $class The name of the class * @return bool|null True, if loaded */ public function loadClass($class) { if ($file = $this->findFile($class)) { include $file; return true; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|null The path, if found */ public function findFile($class) { if ('\\' == $class[0]) { $class = substr($class, 1); } if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; $className = substr($class, $pos + 1); } else { // PEAR-like class name $classPath = null; $className = $class; } $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; foreach ($this->prefixes as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { return $dir . DIRECTORY_SEPARATOR . $classPath; } } } } foreach ($this->fallbackDirs as $dir) { if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { return $dir . DIRECTORY_SEPARATOR . $classPath; } } if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) { return $file; } return $this->classMap[$class] = false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * This script is used to generate the browser class using the Selenium * iedoc.xml file * * @author Alexandre Salomé */ use Selenium\Specification\Loader\XmlLoader; use Selenium\Specification\Dumper\BrowserDumper; use Selenium\Specification\Specification; require_once __DIR__.'/../vendor/autoload.php'; // Check parameter if (!isset($argv[1]) || !file_exists($argv[1])) { echo "Command usage: ./generate_browser.php \n"; echo "\n"; echo " is the XML file provided by Selenium containing all methods\n"; if (isset($argv[1])) { echo "\n"; echo "ERROR: The file provided does not exists !\n"; } exit; } $inputFile = $argv[1]; $outputFile = __DIR__.'/../src/Selenium/Browser.php'; $specification = new Specification(); $loader = new XmlLoader($specification); $loader->load($inputFile); $dumper = new BrowserDumper($specification); file_put_contents($outputFile, $dumper->dump()); Copyright (c) 2011 Alexandre Salomé 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * The base class of the browser. * * @author Alexandre Salomé */ class BaseBrowser { /** * Driver to the server * * @var Selenium\Driver */ protected $driver; /** * Start page of the browser * * @var string */ protected $startPage; /** * Type of browser, for Selenium * * @var string */ protected $type; /** * Instanciates a browser. * * @param Selenium\Driver $driver Driver of the browser * @param string $startPage The start page of the browser */ public function __construct(Driver $driver, $startPage, $type = '*firefox') { $this->driver = $driver; $this->startPage = $startPage; $this->type = $type; } /** * Starts the browser on the server. * * @return Selenium\Browser Fluid interface */ public function start() { $this->driver->start($this->type, $this->startPage); return $this; } /** * Stops the browser on the server. * * @return Selenium\Browser Fluid interface */ public function stop() { $this->driver->stop(); return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Browser class containing all methods of Selenium Server, with documentation. * * This class was generated, do not modify it. * * @author Alexandre Salomé */ class Browser extends BaseBrowser { /** * Clicks on a link, button, checkbox or radio button. If the click action * causes a new page to load (like a link usually does), call * waitForPageToLoad. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function click($locator) { $this->driver->action("click", $locator); return $this; } /** * Double clicks on a link, button, checkbox or radio button. If the double * click action * causes a new page to load (like a link usually does), call * waitForPageToLoad. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function doubleClick($locator) { $this->driver->action("doubleClick", $locator); return $this; } /** * Simulates opening the context menu for the specified element (as might * happen if the user "right-clicked" on the element). * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function contextMenu($locator) { $this->driver->action("contextMenu", $locator); return $this; } /** * Clicks on a link, button, checkbox or radio button. If the click action * causes a new page to load (like a link usually does), call * waitForPageToLoad. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function clickAt($locator, $coordString) { $this->driver->action("clickAt", $locator, $coordString); return $this; } /** * Doubleclicks on a link, button, checkbox or radio button. If the action * causes a new page to load (like a link usually does), call * waitForPageToLoad. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function doubleClickAt($locator, $coordString) { $this->driver->action("doubleClickAt", $locator, $coordString); return $this; } /** * Simulates opening the context menu for the specified element (as might * happen if the user "right-clicked" on the element). * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function contextMenuAt($locator, $coordString) { $this->driver->action("contextMenuAt", $locator, $coordString); return $this; } /** * Explicitly simulate an event, to trigger the corresponding * "onevent" * handler. * * @param string $locator an element locator * * @param string $eventName the event name, e.g. "focus" or "blur" * * @return Selenium\Browser Fluid interface */ public function fireEvent($locator, $eventName) { $this->driver->action("fireEvent", $locator, $eventName); return $this; } /** * Move the focus to the specified element; for example, if the element is * an input field, move the cursor to that field. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function focus($locator) { $this->driver->action("focus", $locator); return $this; } /** * Simulates a user pressing and releasing a key. * * @param string $locator an element locator * * @param string $keySequence Either be a string("\" followed by the numeric * keycode of the key to be pressed, normally the ASCII value of that key), * or a single character. For example: "w", "\119". * * @return Selenium\Browser Fluid interface */ public function keyPress($locator, $keySequence) { $this->driver->action("keyPress", $locator, $keySequence); return $this; } /** * Press the shift key and hold it down until doShiftUp() is called or a new * page is loaded. * * @return Selenium\Browser Fluid interface */ public function shiftKeyDown() { $this->driver->action("shiftKeyDown"); return $this; } /** * Release the shift key. * * @return Selenium\Browser Fluid interface */ public function shiftKeyUp() { $this->driver->action("shiftKeyUp"); return $this; } /** * Press the meta key and hold it down until doMetaUp() is called or a new * page is loaded. * * @return Selenium\Browser Fluid interface */ public function metaKeyDown() { $this->driver->action("metaKeyDown"); return $this; } /** * Release the meta key. * * @return Selenium\Browser Fluid interface */ public function metaKeyUp() { $this->driver->action("metaKeyUp"); return $this; } /** * Press the alt key and hold it down until doAltUp() is called or a new * page is loaded. * * @return Selenium\Browser Fluid interface */ public function altKeyDown() { $this->driver->action("altKeyDown"); return $this; } /** * Release the alt key. * * @return Selenium\Browser Fluid interface */ public function altKeyUp() { $this->driver->action("altKeyUp"); return $this; } /** * Press the control key and hold it down until doControlUp() is called or a * new page is loaded. * * @return Selenium\Browser Fluid interface */ public function controlKeyDown() { $this->driver->action("controlKeyDown"); return $this; } /** * Release the control key. * * @return Selenium\Browser Fluid interface */ public function controlKeyUp() { $this->driver->action("controlKeyUp"); return $this; } /** * Simulates a user pressing a key (without releasing it yet). * * @param string $locator an element locator * * @param string $keySequence Either be a string("\" followed by the numeric * keycode of the key to be pressed, normally the ASCII value of that key), * or a single character. For example: "w", "\119". * * @return Selenium\Browser Fluid interface */ public function keyDown($locator, $keySequence) { $this->driver->action("keyDown", $locator, $keySequence); return $this; } /** * Simulates a user releasing a key. * * @param string $locator an element locator * * @param string $keySequence Either be a string("\" followed by the numeric * keycode of the key to be pressed, normally the ASCII value of that key), * or a single character. For example: "w", "\119". * * @return Selenium\Browser Fluid interface */ public function keyUp($locator, $keySequence) { $this->driver->action("keyUp", $locator, $keySequence); return $this; } /** * Simulates a user hovering a mouse over the specified element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseOver($locator) { $this->driver->action("mouseOver", $locator); return $this; } /** * Simulates a user moving the mouse pointer away from the specified * element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseOut($locator) { $this->driver->action("mouseOut", $locator); return $this; } /** * Simulates a user pressing the left mouse button (without releasing it * yet) on * the specified element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseDown($locator) { $this->driver->action("mouseDown", $locator); return $this; } /** * Simulates a user pressing the right mouse button (without releasing it * yet) on * the specified element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseDownRight($locator) { $this->driver->action("mouseDownRight", $locator); return $this; } /** * Simulates a user pressing the left mouse button (without releasing it * yet) at * the specified location. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function mouseDownAt($locator, $coordString) { $this->driver->action("mouseDownAt", $locator, $coordString); return $this; } /** * Simulates a user pressing the right mouse button (without releasing it * yet) at * the specified location. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function mouseDownRightAt($locator, $coordString) { $this->driver->action("mouseDownRightAt", $locator, $coordString); return $this; } /** * Simulates the event that occurs when the user releases the mouse button * (i.e., stops * holding the button down) on the specified element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseUp($locator) { $this->driver->action("mouseUp", $locator); return $this; } /** * Simulates the event that occurs when the user releases the right mouse * button (i.e., stops * holding the button down) on the specified element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseUpRight($locator) { $this->driver->action("mouseUpRight", $locator); return $this; } /** * Simulates the event that occurs when the user releases the mouse button * (i.e., stops * holding the button down) at the specified location. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function mouseUpAt($locator, $coordString) { $this->driver->action("mouseUpAt", $locator, $coordString); return $this; } /** * Simulates the event that occurs when the user releases the right mouse * button (i.e., stops * holding the button down) at the specified location. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function mouseUpRightAt($locator, $coordString) { $this->driver->action("mouseUpRightAt", $locator, $coordString); return $this; } /** * Simulates a user pressing the mouse button (without releasing it yet) on * the specified element. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function mouseMove($locator) { $this->driver->action("mouseMove", $locator); return $this; } /** * Simulates a user pressing the mouse button (without releasing it yet) on * the specified element. * * @param string $locator an element locator * * @param string $coordString specifies the x,y position (i.e. - 10,20) of * the mouse event relative to the element returned by the locator. * * @return Selenium\Browser Fluid interface */ public function mouseMoveAt($locator, $coordString) { $this->driver->action("mouseMoveAt", $locator, $coordString); return $this; } /** * Sets the value of an input field, as though you typed it in. * *

Can also be used to set the value of combo boxes, check boxes, etc. In * these cases, * value should be the value of the option selected, not the visible * text.

* * @param string $locator an element locator * * @param string $value the value to type * * @return Selenium\Browser Fluid interface */ public function type($locator, $value) { $this->driver->action("type", $locator, $value); return $this; } /** * Simulates keystroke events on the specified element, as though you typed * the value key-by-key. * *

This is a convenience method for calling keyDown, keyUp, keyPress for * every character in the specified string; * this is useful for dynamic UI widgets (like auto-completing combo boxes) * that require explicit key events.

* *

Unlike the simple "type" command, which forces the specified value * into the page directly, this command * may or may not have any visible effect, even in cases where typing keys * would normally have a visible effect. * For example, if you use "typeKeys" on a form element, you may or may not * see the results of what you typed in * the field.

*

In some cases, you may need to use the simple "type" command to set * the value of the field and then the "typeKeys" command to * send the keystroke events corresponding to what you just typed.

* * @param string $locator an element locator * * @param string $value the value to type * * @return Selenium\Browser Fluid interface */ public function typeKeys($locator, $value) { $this->driver->action("typeKeys", $locator, $value); return $this; } /** * Set execution speed (i.e., set the millisecond length of a delay which * will follow each selenium operation). By default, there is no such * delay, i.e., * the delay is 0 milliseconds. * * @param string $value the number of milliseconds to pause after operation * * @return Selenium\Browser Fluid interface */ public function setSpeed($value) { $this->driver->action("setSpeed", $value); return $this; } /** * Get execution speed (i.e., get the millisecond length of the delay * following each selenium operation). By default, there is no such delay, * i.e., * the delay is 0 milliseconds. * * See also setSpeed. * * @return string the execution speed in milliseconds. */ public function getSpeed() { return $this->driver->getString("getSpeed"); } /** * Check a toggle-button (checkbox/radio) * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function check($locator) { $this->driver->action("check", $locator); return $this; } /** * Uncheck a toggle-button (checkbox/radio) * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function uncheck($locator) { $this->driver->action("uncheck", $locator); return $this; } /** * Select an option from a drop-down using an option locator. * *

* Option locators provide different ways of specifying options of an HTML * Select element (e.g. for selecting a specific option, or for asserting * that the selected option satisfies a specification). There are several * forms of Select Option Locator. *

*
    *
  • label=labelPattern: * matches options based on their labels, i.e. the visible text. (This * is the default.) *
      *
    • label=regexp:^[Oo]ther
    • *
    *
  • *
  • value=valuePattern: * matches options based on their values. *
      *
    • value=other
    • *
    * * *
  • *
  • id=id: * * matches options based on their ids. *
      *
    • id=option1
    • *
    *
  • *
  • index=index: * matches an option based on its index (offset from zero). *
      * *
    • index=2
    • *
    *
  • *
*

* If no option locator prefix is provided, the default behaviour is to * match on label. *

* * @param string $selectLocator an element locator * identifying a drop-down menu * * @param string $optionLocator an option locator (a label by default) * * @return Selenium\Browser Fluid interface */ public function select($selectLocator, $optionLocator) { $this->driver->action("select", $selectLocator, $optionLocator); return $this; } /** * Add a selection to the set of selected options in a multi-select element * using an option locator. * * @see #doSelect for details of option locators * * @param string $locator an element locator * identifying a multi-select box * * @param string $optionLocator an option locator (a label by default) * * @return Selenium\Browser Fluid interface */ public function addSelection($locator, $optionLocator) { $this->driver->action("addSelection", $locator, $optionLocator); return $this; } /** * Remove a selection from the set of selected options in a multi-select * element using an option locator. * * @see #doSelect for details of option locators * * @param string $locator an element locator * identifying a multi-select box * * @param string $optionLocator an option locator (a label by default) * * @return Selenium\Browser Fluid interface */ public function removeSelection($locator, $optionLocator) { $this->driver->action("removeSelection", $locator, $optionLocator); return $this; } /** * Unselects all of the selected options in a multi-select element. * * @param string $locator an element locator * identifying a multi-select box * * @return Selenium\Browser Fluid interface */ public function removeAllSelections($locator) { $this->driver->action("removeAllSelections", $locator); return $this; } /** * Submit the specified form. This is particularly useful for forms without * submit buttons, e.g. single-input "Search" forms. * * @param string $formLocator an element locator for * the form you want to submit * * @return Selenium\Browser Fluid interface */ public function submit($formLocator) { $this->driver->action("submit", $formLocator); return $this; } /** * Opens an URL in the test frame. This accepts both relative and absolute * URLs. * * The "open" command waits for the page to load before proceeding, * ie. the "AndWait" suffix is implicit. * * Note: The URL must be on the same domain as the runner HTML * due to security restrictions in the browser (Same Origin Policy). If you * need to open an URL on another domain, use the Selenium Server to start a * new browser session on that domain. * * @param string $url the URL to open; may be relative or absolute * * @return Selenium\Browser Fluid interface */ public function open($url) { $this->driver->action("open", $url); return $this; } /** * Opens a popup window (if a window with that ID isn't already open). * After opening the window, you'll need to select it using the selectWindow * command. * *

This command can also be a useful workaround for bug SEL-339. In some * cases, Selenium will be unable to intercept a call to window.open (if the * call occurs during or before the "onLoad" event, for example). * In those cases, you can force Selenium to notice the open window's name * by using the Selenium openWindow command, using * an empty (blank) url, like this: openWindow("", "myFunnyWindow").

* * @param string $url the URL to open, which can be blank * * @param string $windowID the JavaScript window ID of the window to select * * @return Selenium\Browser Fluid interface */ public function openWindow($url, $windowID) { $this->driver->action("openWindow", $url, $windowID); return $this; } /** * Selects a popup window using a window locator; once a popup window has * been selected, all * commands go to that window. To select the main window again, use null * as the target. * *

* * Window locators provide different ways of specifying the window object: * by title, by internal JavaScript "name," or by JavaScript variable. *

*
    *
  • title=My Special Window: * Finds the window using the text that appears in the title bar. Be * careful; * two windows can share the same title. If that happens, this locator will * just pick one. *
  • *
  • name=myWindow: * Finds the window using its internal JavaScript "name" property. This is * the second * parameter "windowName" passed to the JavaScript method window.open(url, * windowName, windowFeatures, replaceFlag) * (which Selenium intercepts). *
  • *
  • var=variableName: * Some pop-up windows are unnamed (anonymous), but are associated with a * JavaScript variable name in the current * application window, e.g. "window.foo = window.open(url);". In those * cases, you can open the window using * "var=foo". *
  • *
*

* If no window locator prefix is provided, we'll try to guess what you mean * like this:

*

1.) if windowID is null, (or the string "null") then it is assumed the * user is referring to the original window instantiated by the * browser).

*

2.) if the value of the "windowID" parameter is a JavaScript variable * name in the current application window, then it is assumed * that this variable contains the return value from a call to the * JavaScript window.open() method.

*

3.) Otherwise, selenium looks in a hash it maintains that maps string * names to window "names".

*

4.) If that fails, we'll try looping over all of the known * windows to try to find the appropriate "title". * Since "title" is not necessarily unique, this may have unexpected * behavior.

* *

If you're having trouble figuring out the name of a window that you * want to manipulate, look at the Selenium log messages * which identify the names of windows created via window.open (and * therefore intercepted by Selenium). You will see messages * like the following for each window as it is opened:

* *

debug: window.open call intercepted; window ID (which you can * use with selectWindow()) is "myNewWindow"

* *

In some cases, Selenium will be unable to intercept a call to * window.open (if the call occurs during or before the "onLoad" event, for * example). * (This is bug SEL-339.) In those cases, you can force Selenium to notice * the open window's name by using the Selenium openWindow command, using * an empty (blank) url, like this: openWindow("", "myFunnyWindow").

* * @param string $windowID the JavaScript window ID of the window to select * * @return Selenium\Browser Fluid interface */ public function selectWindow($windowID) { $this->driver->action("selectWindow", $windowID); return $this; } /** * Simplifies the process of selecting a popup window (and does not offer * functionality beyond what selectWindow() already provides). *
    *
  • If windowID is either not specified, or specified as * "null", the first non-top window is selected. The top window is the one * that would be selected by selectWindow() without providing a * windowID . This should not be used when more than one popup * window is in play.
  • *
  • Otherwise, the window will be looked up considering * windowID as the following in order: 1) the "name" of the * window, as specified to window.open(); 2) a javascript * variable which is a reference to a window; and 3) the title of the * window. This is the same ordered lookup performed by * selectWindow .
  • *
* * @param string $windowID an identifier for the popup window, which can * take on a number of different meanings * * @return Selenium\Browser Fluid interface */ public function selectPopUp($windowID) { $this->driver->action("selectPopUp", $windowID); return $this; } /** * Selects the main window. Functionally equivalent to using * selectWindow() and specifying no value for * windowID. * * @return Selenium\Browser Fluid interface */ public function deselectPopUp() { $this->driver->action("deselectPopUp"); return $this; } /** * Selects a frame within the current window. (You may invoke this command * multiple times to select nested frames.) To select the parent frame, use * "relative=parent" as a locator; to select the top frame, use * "relative=top". * You can also select a frame by its 0-based index number; select the first * frame with * "index=0", or the third frame with "index=2". * *

You may also use a DOM expression to identify the frame you want * directly, * like this: dom=frames["main"].frames["subframe"]

* * @param string $locator an element locator * identifying a frame or iframe * * @return Selenium\Browser Fluid interface */ public function selectFrame($locator) { $this->driver->action("selectFrame", $locator); return $this; } /** * Determine whether current/locator identify the frame containing this * running code. * *

This is useful in proxy injection mode, where this code runs in every * browser frame and window, and sometimes the selenium server needs to * identify * the "current" frame. In this case, when the test calls selectFrame, this * routine is called for each frame to figure out which one has been * selected. * The selected frame will return true, while all others will return * false.

* * @param string $currentFrameString starting frame * * @param string $target new frame (which might be relative to the current * one) * * @return boolean true if the new frame is this code's window */ public function getWhetherThisFrameMatchFrameExpression($currentFrameString, $target) { return $this->driver->getBoolean("getWhetherThisFrameMatchFrameExpression", $currentFrameString, $target); } /** * Determine whether currentWindowString plus target identify the window * containing this running code. * *

This is useful in proxy injection mode, where this code runs in every * browser frame and window, and sometimes the selenium server needs to * identify * the "current" window. In this case, when the test calls selectWindow, * this * routine is called for each window to figure out which one has been * selected. * The selected window will return true, while all others will return * false.

* * @param string $currentWindowString starting window * * @param string $target new window (which might be relative to the current * one, e.g., "_parent") * * @return boolean true if the new window is this code's window */ public function getWhetherThisWindowMatchWindowExpression($currentWindowString, $target) { return $this->driver->getBoolean("getWhetherThisWindowMatchWindowExpression", $currentWindowString, $target); } /** * Waits for a popup window to appear and load up. * * @param string $windowID the JavaScript window "name" of the window that * will appear (not the text of the title bar) If * unspecified, or specified as "null", this command will * wait for the first non-top window to appear (don't rely * on this if you are working with multiple popups * simultaneously). * * @param string $timeout a timeout in milliseconds, after which the action * will return with an error. If this value is not specified, * the default Selenium timeout will be used. See the * setTimeout() command. * * @return Selenium\Browser Fluid interface */ public function waitForPopUp($windowID, $timeout) { $this->driver->action("waitForPopUp", $windowID, $timeout); return $this; } /** *

* By default, Selenium's overridden window.confirm() function will * return true, as if the user had manually clicked OK; after running * this command, the next call to confirm() will return false, as if * the user had clicked Cancel. Selenium will then resume using the * default behavior for future confirmations, automatically returning * true (OK) unless/until you explicitly call this command for each * confirmation. *

*

* Take note - every time a confirmation comes up, you must * consume it with a corresponding getConfirmation, or else * the next selenium operation will fail. *

* * @return Selenium\Browser Fluid interface */ public function chooseCancelOnNextConfirmation() { $this->driver->action("chooseCancelOnNextConfirmation"); return $this; } /** *

* Undo the effect of calling chooseCancelOnNextConfirmation. Note * that Selenium's overridden window.confirm() function will normally * automatically * return true, as if the user had manually clicked OK, so you shouldn't * need to use this command unless for some reason you need to change * your mind prior to the next confirmation. After any confirmation, * Selenium will resume using the * default behavior for future confirmations, automatically returning * true (OK) unless/until you explicitly call chooseCancelOnNextConfirmation * for each * confirmation. *

*

* Take note - every time a confirmation comes up, you must * consume it with a corresponding getConfirmation, or else * the next selenium operation will fail. *

* * @return Selenium\Browser Fluid interface */ public function chooseOkOnNextConfirmation() { $this->driver->action("chooseOkOnNextConfirmation"); return $this; } /** * Instructs Selenium to return the specified answer string in response to * the next JavaScript prompt [window.prompt()]. * * @param string $answer the answer to give in response to the prompt pop-up * * @return Selenium\Browser Fluid interface */ public function answerOnNextPrompt($answer) { $this->driver->action("answerOnNextPrompt", $answer); return $this; } /** * Simulates the user clicking the "back" button on their browser. * * @return Selenium\Browser Fluid interface */ public function goBack() { $this->driver->action("goBack"); return $this; } /** * Simulates the user clicking the "Refresh" button on their browser. * * @return Selenium\Browser Fluid interface */ public function refresh() { $this->driver->action("refresh"); return $this; } /** * Simulates the user clicking the "close" button in the titlebar of a popup * window or tab. * * @return Selenium\Browser Fluid interface */ public function close() { $this->driver->action("close"); return $this; } /** * Has an alert occurred? * *

* This function never throws an exception *

* * @return boolean true if there is an alert */ public function isAlertPresent() { return $this->driver->getBoolean("isAlertPresent"); } /** * Has a prompt occurred? * *

* This function never throws an exception *

* * @return boolean true if there is a pending prompt */ public function isPromptPresent() { return $this->driver->getBoolean("isPromptPresent"); } /** * Has confirm() been called? * *

* This function never throws an exception *

* * @return boolean true if there is a pending confirmation */ public function isConfirmationPresent() { return $this->driver->getBoolean("isConfirmationPresent"); } /** * Retrieves the message of a JavaScript alert generated during the previous * action, or fail if there were no alerts. * *

Getting an alert has the same effect as manually clicking OK. If an * alert is generated but you do not consume it with getAlert, the next * Selenium action * will fail.

* *

Under Selenium, JavaScript alerts will NOT pop up a visible alert * dialog.

* *

Selenium does NOT support JavaScript alerts that are generated in a * page's onload() event handler. In this case a visible dialog WILL be * generated and Selenium will hang until someone manually clicks OK.

* * @return string The message of the most recent JavaScript alert */ public function getAlert() { return $this->driver->getString("getAlert"); } /** * Retrieves the message of a JavaScript confirmation dialog generated * during * the previous action. * *

* By default, the confirm function will return true, having the same effect * as manually clicking OK. This can be changed by prior execution of the * chooseCancelOnNextConfirmation command. *

*

* If an confirmation is generated but you do not consume it with * getConfirmation, * the next Selenium action will fail. *

* *

* NOTE: under Selenium, JavaScript confirmations will NOT pop up a visible * dialog. *

* *

* NOTE: Selenium does NOT support JavaScript confirmations that are * generated in a page's onload() event handler. In this case a visible * dialog WILL be generated and Selenium will hang until you manually click * OK. *

* * @return string the message of the most recent JavaScript confirmation * dialog */ public function getConfirmation() { return $this->driver->getString("getConfirmation"); } /** * Retrieves the message of a JavaScript question prompt dialog generated * during * the previous action. * *

Successful handling of the prompt requires prior execution of the * answerOnNextPrompt command. If a prompt is generated but you * do not get/verify it, the next Selenium action will fail.

* *

NOTE: under Selenium, JavaScript prompts will NOT pop up a visible * dialog.

* *

NOTE: Selenium does NOT support JavaScript prompts that are generated * in a * page's onload() event handler. In this case a visible dialog WILL be * generated and Selenium will hang until someone manually clicks OK.

* * @return string the message of the most recent JavaScript question prompt */ public function getPrompt() { return $this->driver->getString("getPrompt"); } /** * Gets the absolute URL of the current page. * * @return string the absolute URL of the current page */ public function getLocation() { return $this->driver->getString("getLocation"); } /** * Gets the title of the current page. * * @return string the title of the current page */ public function getTitle() { return $this->driver->getString("getTitle"); } /** * Gets the entire text of the page. * * @return string the entire text of the page */ public function getBodyText() { return $this->driver->getString("getBodyText"); } /** * Gets the (whitespace-trimmed) value of an input field (or anything else * with a value parameter). * For checkbox/radio elements, the value will be "on" or "off" depending on * whether the element is checked or not. * * @param string $locator an element locator * * @return string the element value, or "on/off" for checkbox/radio elements */ public function getValue($locator) { return $this->driver->getString("getValue", $locator); } /** * Gets the text of an element. This works for any element that contains * text. This command uses either the textContent (Mozilla-like browsers) or * the innerText (IE-like browsers) of the element, which is the rendered * text shown to the user. * * @param string $locator an element locator * * @return string the text of the element */ public function getText($locator) { return $this->driver->getString("getText", $locator); } /** * Briefly changes the backgroundColor of the specified element yellow. * Useful for debugging. * * @param string $locator an element locator * * @return Selenium\Browser Fluid interface */ public function highlight($locator) { $this->driver->action("highlight", $locator); return $this; } /** * Gets the result of evaluating the specified JavaScript snippet. The * snippet may * have multiple lines, but only the result of the last line will be * returned. * *

Note that, by default, the snippet will run in the context of the * "selenium" * object itself, so this will refer to the Selenium object. * Use window to * refer to the window of your application, e.g. * window.document.getElementById('foo')

* *

If you need to use * a locator to refer to a single element in your application page, you can * use this.browserbot.findElement("id=foo") where "id=foo" is * your locator.

* * @param string $script the JavaScript snippet to run * * @return string the results of evaluating the snippet */ public function getEval($script) { return $this->driver->getString("getEval", $script); } /** * Gets whether a toggle-button (checkbox/radio) is checked. Fails if the * specified element doesn't exist or isn't a toggle-button. * * @param string $locator an element locator * pointing to a checkbox or radio button * * @return boolean true if the checkbox is checked, false otherwise */ public function isChecked($locator) { return $this->driver->getBoolean("isChecked", $locator); } /** * Gets the text from a cell of a table. The cellAddress syntax * tableLocator.row.column, where row and column start at 0. * * @param string $tableCellAddress a cell address, e.g. "foo.1.4" * * @return string the text from the specified cell */ public function getTable($tableCellAddress) { return $this->driver->getString("getTable", $tableCellAddress); } /** * Gets all option labels (visible text) for selected options in the * specified select or multi-select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string[] an array of all selected option labels in the specified * select drop-down */ public function getSelectedLabels($selectLocator) { return $this->driver->getStringArray("getSelectedLabels", $selectLocator); } /** * Gets option label (visible text) for selected option in the specified * select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string the selected option label in the specified select * drop-down */ public function getSelectedLabel($selectLocator) { return $this->driver->getString("getSelectedLabel", $selectLocator); } /** * Gets all option values (value attributes) for selected options in the * specified select or multi-select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string[] an array of all selected option values in the specified * select drop-down */ public function getSelectedValues($selectLocator) { return $this->driver->getStringArray("getSelectedValues", $selectLocator); } /** * Gets option value (value attribute) for selected option in the specified * select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string the selected option value in the specified select * drop-down */ public function getSelectedValue($selectLocator) { return $this->driver->getString("getSelectedValue", $selectLocator); } /** * Gets all option indexes (option number, starting at 0) for selected * options in the specified select or multi-select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string[] an array of all selected option indexes in the specified * select drop-down */ public function getSelectedIndexes($selectLocator) { return $this->driver->getStringArray("getSelectedIndexes", $selectLocator); } /** * Gets option index (option number, starting at 0) for selected option in * the specified select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string the selected option index in the specified select * drop-down */ public function getSelectedIndex($selectLocator) { return $this->driver->getString("getSelectedIndex", $selectLocator); } /** * Gets all option element IDs for selected options in the specified select * or multi-select element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string[] an array of all selected option IDs in the specified * select drop-down */ public function getSelectedIds($selectLocator) { return $this->driver->getStringArray("getSelectedIds", $selectLocator); } /** * Gets option element ID for selected option in the specified select * element. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string the selected option ID in the specified select drop-down */ public function getSelectedId($selectLocator) { return $this->driver->getString("getSelectedId", $selectLocator); } /** * Determines whether some option in a drop-down menu is selected. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return boolean true if some option has been selected, false otherwise */ public function isSomethingSelected($selectLocator) { return $this->driver->getBoolean("isSomethingSelected", $selectLocator); } /** * Gets all option labels in the specified select drop-down. * * @param string $selectLocator an element locator * identifying a drop-down menu * * @return string[] an array of all option labels in the specified select * drop-down */ public function getSelectOptions($selectLocator) { return $this->driver->getStringArray("getSelectOptions", $selectLocator); } /** * Gets the value of an element attribute. The value of the attribute may * differ across browsers (this is the case for the "style" attribute, for * example). * * @param string $attributeLocator an element locator followed by an @ sign * and then the name of the attribute, e.g. "foo@bar" * * @return string the value of the specified attribute */ public function getAttribute($attributeLocator) { return $this->driver->getString("getAttribute", $attributeLocator); } /** * Verifies that the specified text pattern appears somewhere on the * rendered page shown to the user. * * @param string $pattern a pattern to match with * the text of the page * * @return boolean true if the pattern matches the text, false otherwise */ public function isTextPresent($pattern) { return $this->driver->getBoolean("isTextPresent", $pattern); } /** * Verifies that the specified element is somewhere on the page. * * @param string $locator an element locator * * @return boolean true if the element is present, false otherwise */ public function isElementPresent($locator) { return $this->driver->getBoolean("isElementPresent", $locator); } /** * Determines if the specified element is visible. An * element can be rendered invisible by setting the CSS "visibility" * property to "hidden", or the "display" property to "none", either for the * element itself or one if its ancestors. This method will fail if * the element is not present. * * @param string $locator an element locator * * @return boolean true if the specified element is visible, false otherwise */ public function isVisible($locator) { return $this->driver->getBoolean("isVisible", $locator); } /** * Determines whether the specified input element is editable, ie hasn't * been disabled. * This method will fail if the specified element isn't an input element. * * @param string $locator an element locator * * @return boolean true if the input element is editable, false otherwise */ public function isEditable($locator) { return $this->driver->getBoolean("isEditable", $locator); } /** * Returns the IDs of all buttons on the page. * *

If a given button has no ID, it will appear as "" in this array.

* * @return string[] the IDs of all buttons on the page */ public function getAllButtons() { return $this->driver->getStringArray("getAllButtons"); } /** * Returns the IDs of all links on the page. * *

If a given link has no ID, it will appear as "" in this array.

* * @return string[] the IDs of all links on the page */ public function getAllLinks() { return $this->driver->getStringArray("getAllLinks"); } /** * Returns the IDs of all input fields on the page. * *

If a given field has no ID, it will appear as "" in this array.

* * @return string[] the IDs of all field on the page */ public function getAllFields() { return $this->driver->getStringArray("getAllFields"); } /** * Returns an array of JavaScript property values from all known windows * having one. * * @param string $attributeName name of an attribute on the windows * * @return string[] the set of values of this attribute from all known * windows. */ public function getAttributeFromAllWindows($attributeName) { return $this->driver->getStringArray("getAttributeFromAllWindows", $attributeName); } /** * deprecated - use dragAndDrop instead * * @param string $locator an element locator * * @param string $movementsString offset in pixels from the current location * to which the element should be moved, e.g., "+70,-300" * * @return Selenium\Browser Fluid interface */ public function dragdrop($locator, $movementsString) { $this->driver->action("dragdrop", $locator, $movementsString); return $this; } /** * Configure the number of pixels between "mousemove" events during * dragAndDrop commands (default=10). *

Setting this value to 0 means that we'll send a "mousemove" event to * every single pixel * in between the start location and the end location; that can be very * slow, and may * cause some browsers to force the JavaScript to timeout.

* *

If the mouse speed is greater than the distance between the two * dragged objects, we'll * just send one "mousemove" at the start location and then one final one at * the end location.

* * @param string $pixels the number of pixels between "mousemove" events * * @return Selenium\Browser Fluid interface */ public function setMouseSpeed($pixels) { $this->driver->action("setMouseSpeed", $pixels); return $this; } /** * Returns the number of pixels between "mousemove" events during * dragAndDrop commands (default=10). * * @return number the number of pixels between "mousemove" events during * dragAndDrop commands (default=10) */ public function getMouseSpeed() { return $this->driver->getNumber("getMouseSpeed"); } /** * Drags an element a certain distance and then drops it * * @param string $locator an element locator * * @param string $movementsString offset in pixels from the current location * to which the element should be moved, e.g., "+70,-300" * * @return Selenium\Browser Fluid interface */ public function dragAndDrop($locator, $movementsString) { $this->driver->action("dragAndDrop", $locator, $movementsString); return $this; } /** * Drags an element and drops it on another element * * @param string $locatorOfObjectToBeDragged an element to be dragged * * @param string $locatorOfDragDestinationObject an element whose location * (i.e., whose center-most pixel) will be the point where * locatorOfObjectToBeDragged is dropped * * @return Selenium\Browser Fluid interface */ public function dragAndDropToObject($locatorOfObjectToBeDragged, $locatorOfDragDestinationObject) { $this->driver->action("dragAndDropToObject", $locatorOfObjectToBeDragged, $locatorOfDragDestinationObject); return $this; } /** * Gives focus to the currently selected window * * @return Selenium\Browser Fluid interface */ public function windowFocus() { $this->driver->action("windowFocus"); return $this; } /** * Resize currently selected window to take up the entire screen * * @return Selenium\Browser Fluid interface */ public function windowMaximize() { $this->driver->action("windowMaximize"); return $this; } /** * Returns the IDs of all windows that the browser knows about in an array. * * @return string[] Array of identifiers of all windows that the browser * knows about. */ public function getAllWindowIds() { return $this->driver->getStringArray("getAllWindowIds"); } /** * Returns the names of all windows that the browser knows about in an * array. * * @return string[] Array of names of all windows that the browser knows * about. */ public function getAllWindowNames() { return $this->driver->getStringArray("getAllWindowNames"); } /** * Returns the titles of all windows that the browser knows about in an * array. * * @return string[] Array of titles of all windows that the browser knows * about. */ public function getAllWindowTitles() { return $this->driver->getStringArray("getAllWindowTitles"); } /** * Returns the entire HTML source between the opening and * closing "html" tags. * * @return string the entire HTML source */ public function getHtmlSource() { return $this->driver->getString("getHtmlSource"); } /** * Moves the text cursor to the specified position in the given input * element or textarea. * This method will fail if the specified element isn't an input element or * textarea. * * @param string $locator an element locator * pointing to an input element or textarea * * @param string $position the numerical position of the cursor in the * field; position should be 0 to move the position to the beginning of the * field. You can also set the cursor to -1 to move it to the end of the * field. * * @return Selenium\Browser Fluid interface */ public function setCursorPosition($locator, $position) { $this->driver->action("setCursorPosition", $locator, $position); return $this; } /** * Get the relative index of an element to its parent (starting from 0). The * comment node and empty text node * will be ignored. * * @param string $locator an element locator * pointing to an element * * @return number of relative index of the element to its parent (starting * from 0) */ public function getElementIndex($locator) { return $this->driver->getNumber("getElementIndex", $locator); } /** * Check if these two elements have same parent and are ordered siblings in * the DOM. Two same elements will * not be considered ordered. * * @param string $locator1 an element locator * pointing to the first element * * @param string $locator2 an element locator * pointing to the second element * * @return boolean true if element1 is the previous sibling of element2, * false otherwise */ public function isOrdered($locator1, $locator2) { return $this->driver->getBoolean("isOrdered", $locator1, $locator2); } /** * Retrieves the horizontal position of an element * * @param string $locator an element locator * pointing to an element OR an element itself * * @return number of pixels from the edge of the frame. */ public function getElementPositionLeft($locator) { return $this->driver->getNumber("getElementPositionLeft", $locator); } /** * Retrieves the vertical position of an element * * @param string $locator an element locator * pointing to an element OR an element itself * * @return number of pixels from the edge of the frame. */ public function getElementPositionTop($locator) { return $this->driver->getNumber("getElementPositionTop", $locator); } /** * Retrieves the width of an element * * @param string $locator an element locator * pointing to an element * * @return number width of an element in pixels */ public function getElementWidth($locator) { return $this->driver->getNumber("getElementWidth", $locator); } /** * Retrieves the height of an element * * @param string $locator an element locator * pointing to an element * * @return number height of an element in pixels */ public function getElementHeight($locator) { return $this->driver->getNumber("getElementHeight", $locator); } /** * Retrieves the text cursor position in the given input element or * textarea; beware, this may not work perfectly on all browsers. * *

Specifically, if the cursor/selection has been cleared by JavaScript, * this command will tend to * return the position of the last location of the cursor, even though the * cursor is now gone from the page. This is filed as SEL-243.

* This method will fail if the specified element isn't an input element or * textarea, or there is no cursor in the element. * * @param string $locator an element locator * pointing to an input element or textarea * * @return number the numerical position of the cursor in the field */ public function getCursorPosition($locator) { return $this->driver->getNumber("getCursorPosition", $locator); } /** * Returns the specified expression. * *

This is useful because of JavaScript preprocessing. * It is used to generate commands like assertExpression and * waitForExpression.

* * @param string $expression the value to return * * @return string the value passed in */ public function getExpression($expression) { return $this->driver->getString("getExpression", $expression); } /** * Returns the number of nodes that match the specified xpath, eg. "//table" * would give * the number of tables. * * @param string $xpath the xpath expression to evaluate. do NOT wrap this * expression in a 'count()' function; we will do that for you. * * @return number the number of nodes that match the specified xpath */ public function getXpathCount($xpath) { return $this->driver->getNumber("getXpathCount", $xpath); } /** * Temporarily sets the "id" attribute of the specified element, so you can * locate it in the future * using its ID rather than a slow/complicated XPath. This ID will * disappear once the page is * reloaded. * * @param string $locator an element locator * pointing to an element * * @param string $identifier a string to be used as the ID of the specified * element * * @return Selenium\Browser Fluid interface */ public function assignId($locator, $identifier) { $this->driver->action("assignId", $locator, $identifier); return $this; } /** * Specifies whether Selenium should use the native in-browser * implementation * of XPath (if any native version is available); if you pass "false" to * this function, we will always use our pure-JavaScript xpath library. * Using the pure-JS xpath library can improve the consistency of xpath * element locators between different browser vendors, but the pure-JS * version is much slower than the native implementations. * * @param string $allow boolean, true means we'll prefer to use native * XPath; false means we'll only use JS XPath * * @return Selenium\Browser Fluid interface */ public function allowNativeXpath($allow) { $this->driver->action("allowNativeXpath", $allow); return $this; } /** * Specifies whether Selenium will ignore xpath attributes that have no * value, i.e. are the empty string, when using the non-native xpath * evaluation engine. You'd want to do this for performance reasons in IE. * However, this could break certain xpaths, for example an xpath that looks * for an attribute whose value is NOT the empty string. * * The hope is that such xpaths are relatively rare, but the user should * have the option of using them. Note that this only influences xpath * evaluation when using the ajaxslt engine (i.e. not "javascript-xpath"). * * @param string $ignore boolean, true means we'll ignore attributes without * value at the expense of xpath "correctness"; false * means we'll sacrifice speed for correctness. * * @return Selenium\Browser Fluid interface */ public function ignoreAttributesWithoutValue($ignore) { $this->driver->action("ignoreAttributesWithoutValue", $ignore); return $this; } /** * Runs the specified JavaScript snippet repeatedly until it evaluates to * "true". * The snippet may have multiple lines, but only the result of the last line * will be considered. * *

Note that, by default, the snippet will be run in the runner's test * window, not in the window * of your application. To get the window of your application, you can use * the JavaScript snippet * selenium.browserbot.getCurrentWindow(), and then * run your JavaScript in there

* * @param string $script the JavaScript snippet to run * * @param string $timeout a timeout in milliseconds, after which this * command will return with an error * * @return Selenium\Browser Fluid interface */ public function waitForCondition($script, $timeout) { $this->driver->action("waitForCondition", $script, $timeout); return $this; } /** * Specifies the amount of time that Selenium will wait for actions to * complete. * *

Actions that require waiting include "open" and the "waitFor*" * actions.

* The default timeout is 30 seconds. * * @param string $timeout a timeout in milliseconds, after which the action * will return with an error * * @return Selenium\Browser Fluid interface */ public function setTimeout($timeout) { $this->driver->action("setTimeout", $timeout); return $this; } /** * Waits for a new page to load. * *

You can use this command instead of the "AndWait" suffixes, * "clickAndWait", "selectAndWait", "typeAndWait" etc. * (which are only available in the JS API).

* *

Selenium constantly keeps track of new pages loading, and sets a * "newPageLoaded" * flag when it first notices a page load. Running any other Selenium * command after * turns the flag to false. Hence, if you want to wait for a page to load, * you must * wait immediately after a Selenium command that caused a page-load.

* * @param string $timeout a timeout in milliseconds, after which this * command will return with an error * * @return Selenium\Browser Fluid interface */ public function waitForPageToLoad($timeout) { $this->driver->action("waitForPageToLoad", $timeout); return $this; } /** * Waits for a new frame to load. * *

Selenium constantly keeps track of new pages and frames loading, * and sets a "newPageLoaded" flag when it first notices a page load.

* * See waitForPageToLoad for more information. * * @param string $frameAddress FrameAddress from the server side * * @param string $timeout a timeout in milliseconds, after which this * command will return with an error * * @return Selenium\Browser Fluid interface */ public function waitForFrameToLoad($frameAddress, $timeout) { $this->driver->action("waitForFrameToLoad", $frameAddress, $timeout); return $this; } /** * Return all cookies of the current page under test. * * @return string all cookies of the current page under test */ public function getCookie() { return $this->driver->getString("getCookie"); } /** * Returns the value of the cookie with the specified name, or throws an * error if the cookie is not present. * * @param string $name the name of the cookie * * @return string the value of the cookie */ public function getCookieByName($name) { return $this->driver->getString("getCookieByName", $name); } /** * Returns true if a cookie with the specified name is present, or false * otherwise. * * @param string $name the name of the cookie * * @return boolean true if a cookie with the specified name is present, or * false otherwise. */ public function isCookiePresent($name) { return $this->driver->getBoolean("isCookiePresent", $name); } /** * Create a new cookie whose path and domain are same with those of current * page * under test, unless you specified a path for this cookie explicitly. * * @param string $nameValuePair name and value of the cookie in a format * "name=value" * * @param string $optionsString options for the cookie. Currently supported * options include 'path', 'max_age' and 'domain'. the optionsString's * format is "path=/path/, max_age=60, domain=.foo.com". The order of * options are irrelevant, the unit of the value of 'max_age' is * second. Note that specifying a domain that isn't a subset of the current * domain will usually fail. * * @return Selenium\Browser Fluid interface */ public function createCookie($nameValuePair, $optionsString) { $this->driver->action("createCookie", $nameValuePair, $optionsString); return $this; } /** * Delete a named cookie with specified path and domain. Be careful; to * delete a cookie, you * need to delete it using the exact same path and domain that were used to * create the cookie. * If the path is wrong, or the domain is wrong, the cookie simply won't be * deleted. Also * note that specifying a domain that isn't a subset of the current domain * will usually fail. * * Since there's no way to discover at runtime the original path and domain * of a given cookie, * we've added an option called 'recurse' to try all sub-domains of the * current domain with * all paths that are a subset of the current path. Beware; this option can * be slow. In * big-O notation, it operates in O(n*m) time, where n is the number of dots * in the domain * name and m is the number of slashes in the path. * * @param string $name the name of the cookie to be deleted * * @param string $optionsString options for the cookie. Currently supported * options include 'path', 'domain' and 'recurse.' The optionsString's * format is "path=/path/, domain=.foo.com, recurse=true". The order of * options are irrelevant. Note that specifying a domain that isn't a subset * of the current domain will usually fail. * * @return Selenium\Browser Fluid interface */ public function deleteCookie($name, $optionsString) { $this->driver->action("deleteCookie", $name, $optionsString); return $this; } /** * Calls deleteCookie with recurse=true on all cookies visible to the * current page. * As noted on the documentation for deleteCookie, recurse=true can be much * slower * than simply deleting the cookies using a known domain/path. * * @return Selenium\Browser Fluid interface */ public function deleteAllVisibleCookies() { $this->driver->action("deleteAllVisibleCookies"); return $this; } /** * Sets the threshold for browser-side logging messages; log messages * beneath this threshold will be discarded. * Valid logLevel strings are: "debug", "info", "warn", "error" or "off". * To see the browser logs, you need to * either show the log window in GUI mode, or enable browser-side logging in * Selenium RC. * * @param string $logLevel one of the following: "debug", "info", "warn", * "error" or "off" * * @return Selenium\Browser Fluid interface */ public function setBrowserLogLevel($logLevel) { $this->driver->action("setBrowserLogLevel", $logLevel); return $this; } /** * Creates a new "script" tag in the body of the current test window, and * adds the specified text into the body of the command. Scripts run in * this way can often be debugged more easily than scripts executed using * Selenium's "getEval" command. Beware that JS exceptions thrown in these * script * tags aren't managed by Selenium, so you should probably wrap your script * in try/catch blocks if there is any chance that the script will throw * an exception. * * @param string $script the JavaScript snippet to run * * @return Selenium\Browser Fluid interface */ public function runScript($script) { $this->driver->action("runScript", $script); return $this; } /** * Defines a new function for Selenium to locate elements on the page. * For example, * if you define the strategy "foo", and someone runs click("foo=blah"), * we'll * run your function, passing you the string "blah", and click on the * element * that your function * returns, or throw an "Element not found" error if your function returns * null. * * We'll pass three arguments to your function: *
    *
  • locator: the string the user passed in
  • *
  • inWindow: the currently selected window
  • *
  • inDocument: the currently selected document
  • *
* The function must return null if the element can't be found. * * @param string $strategyName the name of the strategy to define; this * should use only letters [a-zA-Z] with no spaces or other punctuation. * * @param string $functionDefinition a string defining the body of a * function in JavaScript. For example: return * inDocument.getElementById(locator); * * @return Selenium\Browser Fluid interface */ public function addLocationStrategy($strategyName, $functionDefinition) { $this->driver->action("addLocationStrategy", $strategyName, $functionDefinition); return $this; } /** * Saves the entire contents of the current window canvas to a PNG file. * Contrast this with the captureScreenshot command, which captures the * contents of the OS viewport (i.e. whatever is currently being displayed * on the monitor), and is implemented in the RC only. Currently this only * works in Firefox when running in chrome mode, and in IE non-HTA using * the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly * borrowed from the Screengrab! Firefox extension. Please see * http://www.screengrab.org and http://snapsie.sourceforge.net/ for * details. * * @param string $filename the path to the file to persist the screenshot * as. No filename extension will be appended by default. * Directories will not be created if they do not exist, * and an exception will be thrown, possibly by native * code. * * @param string $kwargs a kwargs string that modifies the way the * screenshot is captured. Example: "background=#CCFFDD" . * Currently valid options:
*
background
the background CSS for * the HTML document. This may be useful to set for * capturing screenshots of less-than-ideal layouts, for * example where absolute positioning causes the * calculation of the canvas dimension to fail and a * black background is exposed (possibly obscuring black * text).
* * @return Selenium\Browser Fluid interface */ public function captureEntirePageScreenshot($filename, $kwargs) { $this->driver->action("captureEntirePageScreenshot", $filename, $kwargs); return $this; } /** * Executes a command rollup, which is a series of commands with a unique * name, and optionally arguments that control the generation of the set of * commands. If any one of the rolled-up commands fails, the rollup is * considered to have failed. Rollups may also contain nested rollups. * * @param string $rollupName the name of the rollup command * * @param string $kwargs keyword arguments string that influences how the * rollup expands into commands * * @return Selenium\Browser Fluid interface */ public function rollup($rollupName, $kwargs) { $this->driver->action("rollup", $rollupName, $kwargs); return $this; } /** * Loads script content into a new script tag in the Selenium document. This * differs from the runScript command in that runScript adds the script tag * to the document of the AUT, not the Selenium document. The following * entities in the script content are replaced by the characters they * represent: * * < * > * & * * The corresponding remove command is removeScript. * * @param string $scriptContent the Javascript content of the script to add * * @param string $scriptTagId (optional) the id of the new script tag. If * specified, and an element with this id already * exists, this operation will fail. * * @return Selenium\Browser Fluid interface */ public function addScript($scriptContent, $scriptTagId) { $this->driver->action("addScript", $scriptContent, $scriptTagId); return $this; } /** * Removes a script tag from the Selenium document identified by the given * id. Does nothing if the referenced tag doesn't exist. * * @param string $scriptTagId the id of the script element to remove. * * @return Selenium\Browser Fluid interface */ public function removeScript($scriptTagId) { $this->driver->action("removeScript", $scriptTagId); return $this; } /** * Allows choice of one of the available libraries. * * @param string $libraryName name of the desired library Only the following * three can be chosen:
  • "ajaxslt" - Google's library
  • *
  • "javascript-xpath" - Cybozu Labs' faster library
  • "default" * - The default library. Currently the default library is "ajaxslt" .
  • *
If libraryName isn't one of these three, then no change will be * made. * * @return Selenium\Browser Fluid interface */ public function useXpathLibrary($libraryName) { $this->driver->action("useXpathLibrary", $libraryName); return $this; } /** * Writes a message to the status bar and adds a note to the browser-side * log. * * @param string $context the message to be sent to the browser * * @return Selenium\Browser Fluid interface */ public function setContext($context) { $this->driver->action("setContext", $context); return $this; } /** * Sets a file input (upload) field to the file listed in fileLocator * * @param string $fieldLocator an element locator * * @param string $fileLocator a URL pointing to the specified file. Before * the file can be set in the input field (fieldLocator), Selenium RC may * need to transfer the file to the local machine before attaching the * file in a web page form. This is common in selenium grid configurations * where the RC server driving the browser is not the same machine that * started the test. Supported Browsers: Firefox ("*chrome") only. * * @return Selenium\Browser Fluid interface */ public function attachFile($fieldLocator, $fileLocator) { $this->driver->action("attachFile", $fieldLocator, $fileLocator); return $this; } /** * Captures a PNG screenshot to the specified file. * * @param string $filename the absolute path to the file to be written, e.g. * "c:\blah\screenshot.png" * * @return Selenium\Browser Fluid interface */ public function captureScreenshot($filename) { $this->driver->action("captureScreenshot", $filename); return $this; } /** * Capture a PNG screenshot. It then returns the file as a base 64 encoded * string. * * @return string The base 64 encoded string of the screen shot (PNG file) */ public function captureScreenshotToString() { return $this->driver->getString("captureScreenshotToString"); } /** * Downloads a screenshot of the browser current window canvas to a * based 64 encoded PNG file. The entire windows canvas is * captured, * including parts rendered outside of the current view port. * * Currently this only works in Mozilla and when running in chrome mode. * * @param string $kwargs A kwargs string that modifies the way the * screenshot is captured. Example: "background=#CCFFDD". This may be useful * to set for capturing screenshots of less-than-ideal layouts, for example * where absolute positioning causes the calculation of the canvas dimension * to fail and a black background is exposed (possibly obscuring black * text). * * @return string The base 64 encoded string of the page screenshot (PNG * file) */ public function captureEntirePageScreenshotToString($kwargs) { return $this->driver->getString("captureEntirePageScreenshotToString", $kwargs); } /** * Kills the running Selenium Server and all browser sessions. After you * run this command, you will no longer be able to send * commands to the server; you can't remotely start the server once it has * been stopped. Normally * you should prefer to run the "stop" command, which terminates the current * browser session, rather than * shutting down the entire server. * * @return Selenium\Browser Fluid interface */ public function shutDownSeleniumServer() { $this->driver->action("shutDownSeleniumServer"); return $this; } /** * Retrieve the last messages logged on a specific remote control. Useful * for error reports, especially * when running multiple remote controls in a distributed environment. The * maximum number of log messages * that can be retrieve is configured on remote control startup. * * @return Selenium\Browser Fluid interface */ public function retrieveLastRemoteControlLogs() { $this->driver->action("retrieveLastRemoteControlLogs"); return $this; } /** * Simulates a user pressing a key (without releasing it yet) by sending a * native operating system keystroke. * This function uses the java.awt.Robot class to send a keystroke; this * more accurately simulates typing * a key on the keyboard. It does not honor settings from the shiftKeyDown, * controlKeyDown, altKeyDown and * metaKeyDown commands, and does not target any particular HTML element. * To send a keystroke to a particular * element, focus on the element first before running this command. * * @param string $keycode an integer keycode number corresponding to a * java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing * as JavaScript keycodes! * * @return Selenium\Browser Fluid interface */ public function keyDownNative($keycode) { $this->driver->action("keyDownNative", $keycode); return $this; } /** * Simulates a user releasing a key by sending a native operating system * keystroke. * This function uses the java.awt.Robot class to send a keystroke; this * more accurately simulates typing * a key on the keyboard. It does not honor settings from the shiftKeyDown, * controlKeyDown, altKeyDown and * metaKeyDown commands, and does not target any particular HTML element. * To send a keystroke to a particular * element, focus on the element first before running this command. * * @param string $keycode an integer keycode number corresponding to a * java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing * as JavaScript keycodes! * * @return Selenium\Browser Fluid interface */ public function keyUpNative($keycode) { $this->driver->action("keyUpNative", $keycode); return $this; } /** * Simulates a user pressing and releasing a key by sending a native * operating system keystroke. * This function uses the java.awt.Robot class to send a keystroke; this * more accurately simulates typing * a key on the keyboard. It does not honor settings from the shiftKeyDown, * controlKeyDown, altKeyDown and * metaKeyDown commands, and does not target any particular HTML element. * To send a keystroke to a particular * element, focus on the element first before running this command. * * @param string $keycode an integer keycode number corresponding to a * java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing * as JavaScript keycodes! * * @return Selenium\Browser Fluid interface */ public function keyPressNative($keycode) { $this->driver->action("keyPressNative", $keycode); return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Client for the Selenium Server. * * @author Alexandre Salomé */ class Client { /** * Host of the Selenium Server * * @var string */ protected $host; /** * Port of the Selenium Server * * @var string */ protected $port; /** * Timeout for the server * * @var int */ protected $timeout; protected $browserClass = 'Selenium\Browser'; /** * Instanciates the client. * * @param string $host Host of the server * @param int $port Port of the server * @param int $timeout Timeout of the server */ public function __construct($host = 'localhost', $port = 4444, $timeout = 60) { $this->host = $host; $this->port = $port; $this->timeout = $timeout; } /** * Changes the class used for instanciation of browser. * * @var string $browserClass The browser class to use */ public function setBrowserClass($browserClass) { $this->browserClass = $browserClass; } /** * Creates a new browser instance. * * @param string $startPage The URL of the website to test * @param string $type Type of browser, for Selenium * * @return Selenium\Browser A browser instance */ public function getBrowser($startPage, $type = '*firefox') { $url = 'http://'.$this->host.':'.$this->port.'/selenium-server/driver/'; $driver = new Driver($url, $this->timeout); $class = $this->browserClass; return new $class($driver, $startPage, $type); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Driver for communication with Selenium server * * @author Alexandre Salomé */ class Driver { /** * URL to the server * * @var string */ protected $url; /** * Timeout of the server * * @var int */ protected $timeout; /** * Current session ID * * @var string */ protected $sessionId; /** * Instanciates the driver. * * @param string $url The URL of the server * @param int $timeout Timeout */ public function __construct($url, $timeout) { $this->url = $url; $this->timeout = $timeout; } /** * Starts a new session. * * @param string $type Type of browser * @param string $startUrl Start URL for the browser */ public function start($type = '*firefox', $startUrl = 'http://localhost') { if (null !== $this->sessionId) { throw new Exception("Session already started"); } $response = $this->doExecute('getNewBrowserSession', $type, $startUrl); if (preg_match('/^OK,(.*)$/', $response, $vars)) { $this->sessionId = $vars[1]; } else { throw new Exception("Invalid response from server : $response"); } } /** * Executes an action * * @param string $command Command to execute * @param string $target First parameter * @param string $value Second parameter * * @return void */ public function action($command, $target = null, $value = null) { $result = $this->doExecute($command, $target, $value); if ($result !== 'OK') { throw new Exception("Unexpected response from Selenium server : ".$result); } } /** * Executes a command on the server and returns a string. * * @param string $command The command to execute * @param string $target First parameter * @param string $value Second parameter * * @return string The result of the command as a string */ public function getString($command, $target = null, $value = null) { $result = $this->doExecute($command, $target, $value); if (!preg_match('/^OK,/', $result)) { throw new Exception("Unexpected response from Selenium server : ".$result); } return substr($result, 3); } /** * Executes a command on the server and returns an array of string. * * @param string $command Command to execute * @param string $target First parameter * @param string $value Second parameter * * @return array The result of the command as an array of string */ public function getStringArray($command, $target = null, $value = null) { $string = $this->getString($command, $target, $value); $result = array(); $length = strlen($string); $current = ''; $skip = false; for ($i = 0; $i < $length; $i++) { if (true === $skip) { $skip = false; continue; } $char = $string[$i]; if ($char === '\\') { $skip = true; continue; } if ($char === ',') { $result[] = $current; $curent = ''; continue; } $current .= $char; } return $result; } /** * Executes a command on the server and returns a number. * * @param string $command The command to execute * @param string $target First parameter * @param string $value Second parameter * * @return int The result of the command as a number */ public function getNumber($command, $target = null, $value = null) { $string = $this->getString($command, $target, $value); return (int) $string; } /** * Executes a command on the server and returns a boolean. * * @param string $command The command to execute * @param string $target First parameter * @param string $value Second parameter * * @return boolean The result of the command as a boolean */ public function getBoolean($command, $target = null, $value = null) { $string = $this->getString($command, $target, $value); return $string == 'true'; } /** * Stops the session. * * @return void */ public function stop() { if (null === $this->sessionId) { throw new Exception("Session not started"); } $this->doExecute('testComplete'); $this->sessionId = null; } /** * Executes a raw command on the server and integrate the current session * identifier if available. * * @param string $command Command to execute * @param string $target First argument * @param string $value Second argument * * @return string The raw result of the command */ protected function doExecute($command, $target = null, $value = null) { $query = array('cmd' => $command); if ($target !== null) { $query[1] = $target; } if ($value !== null) { $query[2] = $value; } if (null !== $this->sessionId) { $query['sessionId'] = $this->sessionId; } $query = http_build_query($query); $url = $this->url.'?'.$query; $context = stream_context_create(array( 'http' => array('timeout' => $this->timeout) )); $fp = @fopen($url, 'r', false, $context); if (false === $fp) { throw new Exception("Unable to connect ! "); } stream_set_blocking($fp, 1); stream_set_timeout($fp, $this->timeout); stream_socket_shutdown($fp, STREAM_SHUT_WR); $result = stream_get_contents($fp); fclose($fp); if (false === $result) { throw new Exception("Connection refused"); } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Base exception class used for every exception in the Selenium library * * @author Alexandre Salomé */ class Exception extends \Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Helping class for locating elements in Selenium. * * @author Alexandre Salomé */ class Locator { /** * Creates locator with @id or @name attribute. * * @param string $idOrName Attribute value * * @return string The locator string */ static public function IdOrName($idOrName) { return 'identifier='.$idOrName; } /** * Creates locator with @id attribute. * * @param string $id Attribute value * * @return string The locator string */ static public function id($id) { return 'id='.$id; } /** * Creates locator with @name attribute. * * @param string $id Attribute value * @param string $valuePattern Pattern for the field to locate * @param int $index Index of the element * * @return string The locator string */ static public function name($name, $valuePattern = null, $index = null) { $result = 'name='.$name; if (null !== $valuePattern) { $result .= ' value='.$valuePattern; } if (null !== $index) { $result .= ' index='.$index; } return $result; } /** * Creates locator with the Javascript DOM API (document.forms[1] for example). * * @param string $expression The DOM expression * * @return string The locator string */ static public function javascriptDom($expression) { return 'dom='.$expression; } /** * Creates locator with XPath selector * * @param string $xpath The XPath * * @return string The locator string */ static public function xpath($xpath) { return 'xpath='.$xpath; } /** * Creates locator for a link using a pattern. * * @param string $pattern The text pattern * * @return string The locator string */ static public function linkContaining($pattern) { return 'link='.$pattern; } /** * Creates locator with CSS selector * * @param string $css The CSS selector * * @return string The locator string */ static public function css($css) { return 'css='.$css; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Pattern helper for text matching in Selenium * * @author Alexandre Salomé */ class Pattern { /** * Generates a pattern from a Glob selector. * * @param string $pattern The Glob pattern * * @return string The selenium pattern */ static public function glob($pattern) { return 'glob:'.$pattern; } /** * Generates a pattern from a regexp. * * @param string $regext The regexp * * @return string The selenium pattern */ static public function regexp($regexp) { return 'regexp:'.$regexp; } /** * Generates a pattern from an exact value. * * @param string $string The value * * @return string The selenium pattern */ static public function exact($string) { return 'exact:'.$string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium\Specification\Dumper; use Selenium\Specification\Specification; use Selenium\Specification\Method; use Selenium\Specification\Dumper\MethodBuilder; /** * Dumps the Selenium specification in a class file * * @author Alexandre Salomé */ class BrowserDumper { /** * Specification of the client * * @var Selenium\Specification\Specification */ protected $specification; /** * Instanciates the dumper * * @param Selenium\Specification\Specification $specification The * specification to dump */ public function __construct(Specification $specification) { $this->specification = $specification; } /** * Dumps to source code * * @return string The sourcecode */ public function dump() { $methods = $this->specification->getMethods(); $result = ' * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium; /** * Browser class containing all methods of Selenium Server, with documentation. * * This class was generated, do not modify it. * * @author Alexandre Salomé */ class Browser extends BaseBrowser { '; foreach ($methods as $method) { $result .= $this->dumpMethod($method)."\n\n"; } $result .= "} "; return $result; } /** * Dumps a method. * * @param Selenium\Specification\Method $method Specification of a method */ protected function dumpMethod(Method $method) { $builder = new MethodBuilder(); $documentation = $method->getDescription()."\n\n"; $signature = array(); foreach ($method->getParameters() as $parameter) { $builder->addParameter($parameter->getName()); $documentation .= "@param string $".$parameter->getName()." ".$parameter->getDescription()."\n\n"; $signature[] = '$'.$parameter->getName(); } $signature = implode(', ', $signature); if ($method->isAction()) { $documentation .= '@return Selenium\Browser Fluid interface'; $body = '$this->driver->action("'.$method->getName().'"'. ($signature ? ', '.$signature : '') . ');'."\n"; $body .= "\n"; $body .= "return \$this;"; } else { $returnType = $method->getReturnType(); $documentation .= '@return '.$returnType.' '.$method->getReturnDescription(); if ($returnType === 'boolean') { $getMethod = 'getBoolean'; } elseif ($returnType === 'string') { $getMethod = 'getString'; } elseif ($returnType === 'string[]') { $getMethod = 'getStringArray'; } elseif ($returnType === 'number') { $getMethod = 'getNumber'; } $body = 'return $this->driver->'.$getMethod.'("'.$method->getName().'"'.($signature ? ', '.$signature : '').');'; } $builder->setName($method->getName()); $builder->setBody($body); $builder->setDocumentation($documentation); return $builder->buildCode(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium\Specification\Dumper; /** * Helper for building a PHP method * * @author Alexandre Salomé */ class MethodBuilder { /** * Documentation text of the method * * @var string */ protected $documentation; /** * Name of the method * * @var string */ protected $name; /** * Body of the method (contains calls, etc.) * * @var string */ protected $body; /** * Array of parameters (string prefixed with '$') * * @var array */ protected $parameters = array(); /** * Sets the documentation block. * * @param string $documentation A documentation text * * @return Selenium\Specification\Dumper\MethodBuilder Fluid interface */ public function setDocumentation($documentation) { $this->documentation = $documentation; return $this; } /** * Sets the name of the method. * * @param string $name A method name * * @return Selenium\Specification\Dumper\MethodBuilder Fluid interface */ public function setName($name) { $this->name = $name; return $this; } /** * Sets the body of the method. * * @param string $body A body * * @return Selenium\Specification\Dumper\MethodBuilder Fluid interface */ public function setBody($body) { $this->body = $body; return $this; } /** * Adds a parameter to the method. * * @param string $parameter A parameter name (without the '$') * * @return Selenium\Specification\Dumper\MethodBuilder Fluid interface */ public function addParameter($parameter) { $this->parameters[] = '$'.$parameter; return $this; } /** * Builds the PHP code for the method. * * @return string The method code */ public function buildCode() { $code = ''; if ($this->documentation) { $code .= ' /**'."\n"; $code .= ' * '.str_replace("\n", "\n * ", wordwrap($this->documentation, 73))."\n"; $code .= ' */'."\n"; } $code .= ' public function '.$this->name.'('.implode(', ', $this->parameters).')'."\n"; $code .= ' {'."\n"; $code .= ' '.str_replace("\n", "\n ", $this->body)."\n"; $code .= ' }'; return $code; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium\Specification\Loader; use Selenium\Specification\Specification; use Selenium\Specification\Method; use Selenium\Specification\Parameter; use Symfony\Component\DomCrawler\Crawler; /** * Loads a XML file into a specification object. * * @author Alexandre Salomé */ class XmlLoader { /** * @var Selenium\Specification\Specification */ protected $specification; public function __construct(Specification $specification) { $this->specification = $specification; } /** * Loads the specification from a XML file * * @param string $file Path to the file to load */ public function load($file) { if (!file_exists($file)) { throw new \RuntimeException(sprintf('The file "%s" does not exists', $file)); } $content = file_get_contents($file); // HACK: DOMNode seems to bug when a node is named "param" $content = str_replace('addContent($content, 'xml'); foreach ($crawler->filterXPath('//function') as $node) { $method = $this->getMethod($node); $this->specification->addMethod($method); } } /** * Returns a method in the current specification from a DOMNode * * @param DOMNode $node A DOMNode * * @return Selenium\Specification\Method */ public function getMethod(\DOMNode $node) { $crawler = new Crawler($node); $name = $crawler->attr('name'); // Initialize $method = new Method($name); // Type $method->setType( preg_match('/(^(get|is)|ToString$)/', $name) ? Method::TYPE_ACCESSOR : Method::TYPE_ACTION ); // Description $descriptions = $crawler->filterXPath('//comment'); if (count($descriptions) !== 1) { throw new \Exception('Only one comment expected'); } $descriptions->rewind(); $description = $this->getInner($descriptions->current()); $method->setDescription($description); // Parameters foreach ($crawler->filterXPath('//parameter') as $node) { $method->addParameter($this->getParameter($node)); } // Return $returnNodes = $crawler->filterXPath('//return'); if (count($returnNodes) > 1) { throw new \Exception("Should not be more than one return node"); } elseif (count($returnNodes) == 1) { $returnNodes->rewind(); list($type, $description) = $this->getReturn($returnNodes->current()); $method->setReturnType($type); $method->setReturnDescription($description); } return $method; } /** * Get return informations (type, description) from a DOMNode * * @param DOMNode $node The DOMNode to parse * * @return array First element is the type, second the description */ protected function getReturn(\DOMNode $node) { $crawler = new Crawler($node); $type = $crawler->attr('type'); $description = $this->getInner($node); return array($type, $description); } /** * Get a parameter model object from a DOMNode * * @param DOMNode $node A DOMNode to convert to specification parameter * * @return Selenium\Specification\Parameter The parameter model object */ protected function getParameter(\DOMNode $node) { $name = $node->getAttribute('name'); $parameter = new Parameter($name); $parameter->setDescription($this->getInner($node)); return $parameter; } /** * Get the inner content of a DOMNode. * * @param DOMNode $node A DOMNode instance * * @return string The inner content */ protected function getInner(\DOMNode $node) { $c14n = $node->C14N(); $begin = strpos($c14n, '>'); $end = strrpos($c14n, '<'); $content = substr($c14n, $begin + 1, $end - $begin - 1); return $content; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium\Specification; /** * Representation of a method of the Selenium server * * @author Alexandre Salomé */ class Method { const TYPE_ACCESSOR = 'accessor'; const TYPE_ACTION = 'action'; /** * Name of the method * * @var string */ protected $name; /** * Description or documentation of the method * * @var string */ protected $description; /** * Type of the method (action or accessor) * * @var string * * @see self::TYPE_* */ protected $type; /** * Parameters of the method * * @var array */ protected $parameters = array(); /** * Return type of the method * * @var string */ protected $returnType; /** * Description of the return value * * @var string */ protected $returnDescription; /** * Instanciates the method. * * @param string $name Name of the method */ public function __construct($name) { $this->name = $name; } /** * Returns the name of the method. * * @return string The name of the method */ public function getName() { return $this->name; } /** * Adds a parameter to the method. * * @param Selenium\Specification\Parameter $parameter Parameter to add */ public function addParameter(Parameter $parameter) { $this->parameters[] = $parameter; } /** * Defines the description of the method. * * @param string $description Description of the method */ public function setDescription($description) { $this->description = $description; } /** * Defines the type of method (action or accessor) * * @param string $type Type of the method */ public function setType($type) { $this->type = $type; } /** * Returns the type of method (action or accessor) * * @return string Type of the method */ public function getType() { return $this->type; } /** * Tests if the method is an action. * * @return boolean Result of the test */ public function isAction() { return $this->type === self::TYPE_ACTION; } /** * Tests if the method is an accessor. * * @return boolean Result of the test */ public function isAccessor() { return $this->type === self::TYPE_ACCESSOR; } /** * Defines the return value type of the method. */ public function setReturnType($returnType) { $this->returnType = $returnType; } /** * Defines the return value description of the method. */ public function setReturnDescription($returnDescription) { $this->returnDescription = $returnDescription; } /** * Returns parameters of the method. * * @return array An array of parameter objects */ public function getParameters() { return $this->parameters; } /** * Returns the description of the method. * * @return string The description text */ public function getDescription() { return $this->description; } /** * Returns the type of return value. * * @return string The return type */ public function getReturnType() { return $this->returnType; } /** * Returns the description of return value. * * @return string The description or return value */ public function getReturnDescription() { return $this->returnDescription; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium\Specification; /** * Representation of a Selenium method parameter * * @author Alexandre Salomé */ class Parameter { /** * Name of the parameter * * @var string */ protected $name; /** * Description of the parameter * * @var string */ protected $description; /** * Instanciates the parameter. * * @param string $name Name of the parameter */ public function __construct($name) { $this->name = $name; } /** * Returns the description of the parameter. * * @return string The description of the parameter */ public function getDescription() { return $this->description; } /** * Defines the parameter description. * * @param string The parameter description */ public function setDescription($description) { $this->description = $description; } /** * Returns the name of the parameter. * * @return string Name of the parameter */ public function getName() { return $this->name; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Selenium\Specification; /** * Representation of the Selenium specification * * @author Alexandre Salomé */ class Specification { /** * Collection of specified methods * * @var array */ protected $methods = array(); /** * Adds a method to the specification. * * @param Selenium\Specification\Method Method to add */ public function addMethod(Method $method) { $this->methods[] = $method; } /** * Returns all the methods in the specification. * * @return array An array of Selenium\Specification\Method objects */ public function getMethods() { return $this->methods; } } Copyright (c) 2012-2013 Konstantin Kudryashov 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Symfony2 BrowserKit driver. * * @author Konstantin Kudryashov */ class BrowserKitDriver implements DriverInterface { private $session; private $client; private $forms = array(); private $started = false; private $removeScriptFromUrl = true; private $removeHostFromUrl = false; /** * Initializes Goutte driver. * * @param Client $client BrowserKit client instance */ public function __construct(Client $client = null) { $this->client = $client; $this->client->followRedirects(true); } /** * Returns BrowserKit HTTP client instance. * * @return Client */ public function getClient() { return $this->client; } /** * Sets driver's current session. * * @param Session $session */ public function setSession(Session $session) { $this->session = $session; } /** * Tells driver to remove hostname from URL. * * @param Boolean $remove */ public function setRemoveHostFromUrl($remove = true) { $this->removeHostFromUrl = (bool) $remove; } /** * Tells driver to remove scriptname from URL. * * @param Boolean $remove */ public function setRemoveScriptFromUrl($remove = true) { $this->removeScriptFromUrl = (bool) $remove; } /** * Starts driver. */ public function start() { $this->started = true; } /** * Checks whether driver is started. * * @return Boolean */ public function isStarted() { return $this->started; } /** * Stops driver. */ public function stop() { $this->client->restart(); $this->started = false; $this->forms = array(); } /** * Resets driver. */ public function reset() { $this->client->restart(); $this->forms = array(); } /** * Visit specified URL. * * @param string $url url of the page */ public function visit($url) { $this->client->request('GET', $this->prepareUrl($url)); $this->forms = array(); } /** * Returns current URL address. * * @return string */ public function getCurrentUrl() { return $this->client->getRequest()->getUri(); } /** * Reloads current page. */ public function reload() { $this->client->reload(); $this->forms = array(); } /** * Moves browser forward 1 page. */ public function forward() { $this->client->forward(); $this->forms = array(); } /** * Moves browser backward 1 page. */ public function back() { $this->client->back(); $this->forms = array(); } /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) * * @throws UnsupportedDriverActionException */ public function switchToWindow($name = null) { throw new UnsupportedDriverActionException('Window management is not supported by %s', $this); } /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) * * @throws UnsupportedDriverActionException */ public function switchToIFrame($name = null) { throw new UnsupportedDriverActionException('iFrame management is not supported by %s', $this); } /** * Sets HTTP Basic authentication parameters * * @param string|Boolean $user user name or false to disable authentication * @param string $password password */ public function setBasicAuth($user, $password) { $this->client->setServerParameter('PHP_AUTH_USER', $user); $this->client->setServerParameter('PHP_AUTH_PW', $password); } /** * Sets specific request header on client. * * @param string $name * @param string $value */ public function setRequestHeader($name, $value) { switch (strtolower($name)) { case 'accept': $name = 'HTTP_ACCEPT'; break; case 'accept-charset': $name = 'HTTP_ACCEPT_CHARSET'; break; case 'accept-encoding': $name = 'HTTP_ACCEPT_ENCODING'; break; case 'accept-language': $name = 'HTTP_ACCEPT_LANGUAGE'; break; case 'connection': $name = 'HTTP_CONNECTION'; break; case 'host': $name = 'HTTP_HOST'; break; case 'user-agent': $name = 'HTTP_USER_AGENT'; break; case 'authorization': $name = 'PHP_AUTH_DIGEST'; break; } $this->client->setServerParameter($name, $value); } /** * Returns last response headers. * * @return array */ public function getResponseHeaders() { $headers = array(); $responseHeaders = trim($this->client->getResponse()->headers->__toString()); foreach (explode("\r\n", $responseHeaders) as $header) { list($name, $value) = array_map('trim', explode(':', $header, 2)); if (isset($headers[$name])) { $headers[$name] = array($headers[$name]); $headers[$name][] = $value; } else { $headers[$name] = $value; } } return $headers; } /** * Sets cookie. * * @param string $name * @param string $value */ public function setCookie($name, $value = null) { $jar = $this->client->getCookieJar(); if (null === $value) { if (null !== $jar->get($name)) { $jar->expire($name); } return; } $jar->set(new Cookie($name, $value)); } /** * Returns cookie by name. * * @param string $name * * @return string|null */ public function getCookie($name) { // Note that the following doesn't work well because // Symfony\Component\BrowserKit\CookieJar stores cookies by name, // path, AND domain and if you don't fill them all in correctly then // you won't get the value that you're expecting. // // $jar = $this->client->getCookieJar(); // // if (null !== $cookie = $jar->get($name)) { // return $cookie->getValue(); // } $allValues = $this->client->getCookieJar()->allValues($this->getCurrentUrl()); if (isset($allValues[$name])) { return $allValues[$name]; } else { return null; } } /** * Returns last response status code. * * @return integer */ public function getStatusCode() { return $this->client->getResponse()->getStatusCode(); } /** * Returns last response content. * * @return string */ public function getContent() { return $this->client->getResponse()->getContent(); } /** * Capture a screenshot of the current window. * * @throws UnsupportedDriverActionException */ public function getScreenshot() { throw new UnsupportedDriverActionException('Screenshots are not supported by %s', $this); } /** * Finds elements with specified XPath query. * * @param string $xpath * * @return array array of NodeElements */ public function find($xpath) { $nodes = $this->getCrawler()->filterXPath($xpath); $elements = array(); foreach ($nodes as $i => $node) { $elements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i + 1), $this->session); } return $elements; } /** * Returns element's tag name by it's XPath query. * * @param string $xpath * * @return string */ public function getTagName($xpath) { return $this->getCrawlerNode($this->getCrawler()->filterXPath($xpath)->eq(0))->nodeName; } /** * Returns element's text by it's XPath query. * * @param string $xpath * * @return string */ public function getText($xpath) { $text = $this->getCrawler()->filterXPath($xpath)->eq(0)->text(); $text = str_replace("\n", ' ', $text); $text = preg_replace('/ {2,}/', ' ', $text); return trim($text); } /** * Returns element's html by it's XPath query. * * @param string $xpath * * @return string */ public function getHtml($xpath) { $node = $this->getCrawlerNode($this->getCrawler()->filterXPath($xpath)->eq(0)); $text = $node->ownerDocument->saveXML($node); // cut the tag itself (making innerHTML out of outerHTML) $text = preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $text); return $text; } /** * Returns element's attribute by it's XPath query. * * @param string $xpath * @param string $name * * @return mixed */ public function getAttribute($xpath, $name) { $value = $this->getCrawler()->filterXPath($xpath)->eq(0)->attr($name); return '' !== $value ? $value : null; } /** * Returns element's value by it's XPath query. * * @param string $xpath * * @return mixed */ public function getValue($xpath) { if (in_array($this->getAttribute($xpath, 'type'), array('submit', 'image', 'button'))) { return $this->getAttribute($xpath, 'value'); } try { $field = $this->getFormField($xpath); } catch (\InvalidArgumentException $e) { return $this->getAttribute($xpath, 'value'); } $value = $field->getValue(); if ($field instanceof Field\ChoiceFormField && 'checkbox' === $field->getType()) { $value = '1' == $value; } return $value; } /** * Sets element's value by it's XPath query. * * @param string $xpath * @param string $value */ public function setValue($xpath, $value) { $this->getFormField($xpath)->setValue($value); } /** * Checks checkbox by it's XPath query. * * @param string $xpath */ public function check($xpath) { $this->getFormField($xpath)->tick(); } /** * Unchecks checkbox by it's XPath query. * * @param string $xpath */ public function uncheck($xpath) { $this->getFormField($xpath)->untick(); } /** * Selects option from select field located by it's XPath query. * * @param string $xpath * @param string $value * @param Boolean $multiple */ public function selectOption($xpath, $value, $multiple = false) { $field = $this->getFormField($xpath); if ($multiple) { $oldValue = (array) $field->getValue(); $oldValue[] = $value; $value = $oldValue; } $field->select($value); } /** * Clicks button or link located by it's XPath query. * * @param string $xpath * * @throws ElementNotFoundException * @throws DriverException */ public function click($xpath) { if (!count($nodes = $this->getCrawler()->filterXPath($xpath))) { throw new ElementNotFoundException( $this->session, 'link or button', 'xpath', $xpath ); } $node = $nodes->eq(0); $type = $this->getCrawlerNode($node)->nodeName; if ('a' === $type) { $this->client->click($node->link()); } elseif('input' === $type || 'button' === $type) { $form = $node->form(); $formId = $this->getFormNodeId($form->getFormNode()); if (isset($this->forms[$formId])) { $this->mergeForms($form, $this->forms[$formId]); } // remove empty file fields from request foreach ($form->getFiles() as $name => $field) { if (empty($field['name']) && empty($field['tmp_name'])) { $form->remove($name); } } $this->client->submit($form); } else { throw new DriverException(sprintf( 'Goutte driver supports clicking on inputs and links only. But "%s" provided', $type )); } $this->forms = array(); } /** * Checks whether checkbox checked located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isChecked($xpath) { return (bool) $this->getValue($xpath); } /** * Attaches file path to file field located by it's XPath query. * * @param string $xpath * @param string $path */ public function attachFile($xpath, $path) { $this->getFormField($xpath)->upload($path); } /** * Double-clicks button or link located by it's XPath query. * * @param string $xpath * * @throws UnsupportedDriverActionException */ public function doubleClick($xpath) { throw new UnsupportedDriverActionException('Double-clicking is not supported by %s', $this); } /** * Right-clicks button or link located by it's XPath query. * * @param string $xpath * * @throws UnsupportedDriverActionException */ public function rightClick($xpath) { throw new UnsupportedDriverActionException('Right-clicking is not supported by %s', $this); } /** * Simulates a mouse over on the element. * * @param string $xpath * * @throws UnsupportedDriverActionException */ public function mouseOver($xpath) { throw new UnsupportedDriverActionException('Mouse moving is not supported by %s', $this); } /** * Brings focus to element. * * @param string $xpath * * @throws UnsupportedDriverActionException */ public function focus($xpath) { throw new UnsupportedDriverActionException('Focus actions are not supported by %s', $this); } /** * Removes focus from element. * * @param string $xpath * * @throws UnsupportedDriverActionException */ public function blur($xpath) { throw new UnsupportedDriverActionException('Focus actions are not supported by %s', $this); } /** * Presses specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') * * @throws UnsupportedDriverActionException */ public function keyPress($xpath, $char, $modifier = null) { throw new UnsupportedDriverActionException('Keyboard actions are not supported by %s', $this); } /** * Pressed down specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') * * @throws UnsupportedDriverActionException */ public function keyDown($xpath, $char, $modifier = null) { throw new UnsupportedDriverActionException('Keyboard actions are not supported by %s', $this); } /** * Pressed up specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') * * @throws UnsupportedDriverActionException */ public function keyUp($xpath, $char, $modifier = null) { throw new UnsupportedDriverActionException('Keyboard actions are not supported by %s', $this); } /** * Executes JS script. * * @param string $script * * @throws UnsupportedDriverActionException */ public function executeScript($script) { throw new UnsupportedDriverActionException('JS scripts execution is not supported by %s', $this); } /** * Evaluates JS script. * * @param string $script * * @throws UnsupportedDriverActionException */ public function evaluateScript($script) { throw new UnsupportedDriverActionException('JS scripts execution is not supported by %s', $this); } /** * Waits some time or until JS condition turns true. * * @param integer $time time in milliseconds * @param string $condition JS condition * * @throws UnsupportedDriverActionException */ public function wait($time, $condition) { throw new UnsupportedDriverActionException('JS scripts execution is not supported by %s', $this); } /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) * * @throws UnsupportedDriverActionException */ public function resizeWindow($width, $height, $name = null) { throw new UnsupportedDriverActionException('Window resizing is not supported by %s', $this); } /** * Checks whether element visible located by it's XPath query. * * @param string $xpath * * @return Boolean * * @throws UnsupportedDriverActionException */ public function isVisible($xpath) { throw new UnsupportedDriverActionException('Element visibility check is not supported by %s', $this); } /** * Drag one element onto another. * * @param string $sourceXpath * @param string $destinationXpath * * @throws UnsupportedDriverActionException */ public function dragTo($sourceXpath, $destinationXpath) { throw new UnsupportedDriverActionException('Element dragging is not supported by %s', $this); } /** * Prepares URL for visiting. * Removes "*.php/" from urls and then passes it to GoutteDriver::visit(). * * @param string $url * * @return string */ protected function prepareUrl($url) { return preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', ($this->removeHostFromUrl ? '' : '$1').($this->removeScriptFromUrl ? '' : '$2'), $url ); } /** * Returns form field from XPath query. * * @param string $xpath * * @return FormField * * @throws ElementNotFoundException * @throws \LogicException */ protected function getFormField($xpath) { if (!count($crawler = $this->getCrawler()->filterXPath($xpath))) { throw new ElementNotFoundException( $this->session, 'form field', 'xpath', $xpath ); } $fieldNode = $this->getCrawlerNode($crawler); $fieldName = str_replace('[]', '', $fieldNode->getAttribute('name')); $formNode = $fieldNode; // we will access our element by name next, but that's not unique, so we need to know wich is ou element $elements = $this->getCrawler()->filterXPath('//*[@name=\''.$fieldNode->getAttribute('name').'\']'); $position = 0; if(count($elements) > 1) { // more than one element contains this name ! // so we need to find the position of $fieldNode foreach($elements as $key => $element) { if($element->getAttribute('id') == $fieldNode->getAttribute('id')) { $position = $key; } } } do { // use the ancestor form element if (null === $formNode = $formNode->parentNode) { throw new \LogicException('The selected node does not have a form ancestor.'); } } while ('form' != $formNode->nodeName); $formId = $this->getFormNodeId($formNode); // check if form already exists if (isset($this->forms[$formId])) { if (is_array($this->forms[$formId][$fieldName])) { return $this->forms[$formId][$fieldName][$position]; } return $this->forms[$formId][$fieldName]; } // find form button if (null === $buttonNode = $this->findFormButton($formNode)) { throw new ElementNotFoundException( $this->session, 'form submit button for field with xpath "'.$xpath.'"' ); } $this->forms[$formId] = new Form($buttonNode, $this->client->getRequest()->getUri()); if (is_array($this->forms[$formId][$fieldName])) { return $this->forms[$formId][$fieldName][$position]; } return $this->forms[$formId][$fieldName]; } /** * Returns form node unique identifier. * * @param \DOMElement $form * * @return mixed */ private function getFormNodeId(\DOMElement $form) { return md5($form->getLineNo() . $form->getNodePath() . $form->nodeValue); } /** * Finds form submit button inside form node. * * @param \DOMElement $form * * @return \DOMElement */ private function findFormButton(\DOMElement $form) { $document = new \DOMDocument('1.0', 'UTF-8'); $node = $document->importNode($form, true); $root = $document->appendChild($document->createElement('_root')); $root->appendChild($node); $xpath = new \DOMXPath($document); foreach ($xpath->query('descendant::input | descendant::button', $root) as $node) { if ('button' == $node->nodeName || in_array($node->getAttribute('type'), array('submit', 'button', 'image'))) { return $node; } } return null; } /** * Merges second form values into first one. * * @param Form $to merging target * @param Form $from merging source */ private function mergeForms(Form $to, Form $from) { foreach ($from->all() as $name => $field) { $fieldReflection = new \ReflectionObject($field); $nodeReflection = $fieldReflection->getProperty('node'); $valueReflection = $fieldReflection->getProperty('value'); $nodeReflection->setAccessible(true); $valueReflection->setAccessible(true); if (!($field instanceof Field\InputFormField && in_array( $nodeReflection->getValue($field)->getAttribute('type'), array('submit', 'button', 'image') ))) { $valueReflection->setValue($to[$name], $valueReflection->getValue($field)); } } } /** * Returns DOMNode from crawler instance. * * @param Crawler $crawler * @param integer $num number of node from crawler * * @return \DOMNode */ private function getCrawlerNode(Crawler $crawler, $num = 0) { foreach ($crawler as $i => $node) { if ($num == $i) { return $node; } } return null; } /** * Returns crawler instance (got from client). * * @return Crawler * * @throws DriverException */ private function getCrawler() { $crawler = $this->client->getCrawler(); if (null === $crawler) { throw new DriverException('Crawler can\'t be initialized. Did you started driver?'); } return $crawler; } } Copyright (c) 2012-2013 Konstantin Kudryashov 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Client overrides to support Mink functionality. */ class Client extends BaseClient { /** * Reads response meta tags to guess content-type charset. */ protected function createResponse(GuzzleResponse $response) { $body = $response->getBody(true); $statusCode = $response->getStatusCode(); $headers = $response->getHeaders()->getAll(); $contentType = $response->getContentType(); if (!$contentType || false === strpos($contentType, 'charset=')) { if (preg_match('/\]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) { $contentType .= ';charset='.$matches[1]; } } $headers['Content-Type'] = $contentType; return new Response($body, $statusCode, $headers); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Goutte driver. * * @author Konstantin Kudryashov */ class GoutteDriver extends BrowserKitDriver { /** * Initializes Goutte driver. * * @param Client $client HttpKernel client instance */ public function __construct(Client $client = null) { parent::__construct($client ?: new Client()); } /** * Sets HTTP Basic authentication parameters * * @param string|Boolean $user user name or false to disable authentication * @param string $password password */ public function setBasicAuth($user, $password) { $this->getClient()->setAuth($user, $password); } /** * Sets specific request header on client. * * @param string $name * @param string $value */ public function setRequestHeader($name, $value) { $this->getClient()->setHeader($name, $value); } /** * Returns last response headers. * * @return array */ public function getResponseHeaders() { return $this->getClient()->getResponse()->getHeaders(); } /** * {@inheritdoc} */ public function getStatusCode() { return $this->getClient()->getResponse()->getStatus(); } /** * Prepares URL for visiting. * * @param string $url * * @return string */ protected function prepareUrl($url) { return $url; } } Copyright (c) 2012-2013 Konstantin Kudryashov 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Sahi (JS) driver. * * @author Konstantin Kudryashov */ class SahiDriver implements DriverInterface { private $started = false; private $browserName; private $client; private $session; /** * Initializes Sahi driver. * * @param string $browserName browser to start (firefox, safari, ie, etc...) * @param Client $client Sahi client instance */ public function __construct($browserName, Client $client = null) { if (null === $client) { $client = new Client(); } $this->client = $client; $this->browserName = $browserName; } /** * Returns Sahi client instance. * * @return Client */ public function getClient() { return $this->client; } /** * Sets driver's current session. * * @param Session $session */ public function setSession(Session $session) { $this->session = $session; } /** * Starts driver. */ public function start() { $this->client->start($this->browserName); $this->started = true; } /** * Checks whether driver is started. * * @return Boolean */ public function isStarted() { return $this->started; } /** * Stops driver. */ public function stop() { $this->client->stop(); $this->started = false; } /** * Resets driver. */ public function reset() { try { $this->executeScript( '(function(){var c=document.cookie.split(";");for(var i=0;i-1?c[i].substr(0,e):c[i];document.cookie=n+"=;expires=Thu, 01 Jan 1970 00:00:00 GMT";}})()' ); } catch(\Exception $e) {} } /** * Visit specified URL. * * @param string $url url of the page */ public function visit($url) { $this->client->navigateTo($url, true); } /** * Returns current URL address. * * @return string */ public function getCurrentUrl() { return $this->evaluateScript('document.URL'); } /** * Reloads current page. */ public function reload() { $this->visit($this->getCurrentUrl()); } /** * Moves browser forward 1 page. */ public function forward() { $this->executeScript('history.forward()'); } /** * Moves browser backward 1 page. */ public function back() { $this->executeScript('history.back()'); } /** * Sets HTTP Basic authentication parameters * * @param string|Boolean $user user name or false to disable authentication * @param string $password password * * @throws UnsupportedDriverActionException */ public function setBasicAuth($user, $password) { throw new UnsupportedDriverActionException('HTTP Basic authentication is not supported by %s', $this); } /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) * * @throws UnsupportedDriverActionException */ public function switchToWindow($name = null) { throw new UnsupportedDriverActionException('Window management is broken in Sahi, so %s does not support switching into windows', $this); } /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) * * @throws UnsupportedDriverActionException */ public function switchToIFrame($name = null) { throw new UnsupportedDriverActionException('Sahi does not have ability to switch into iFrames, so %s does not support it too', $this); } /** * Sets specific request header on client. * * @param string $name * @param string $value * * @throws UnsupportedDriverActionException */ public function setRequestHeader($name, $value) { throw new UnsupportedDriverActionException('Request headers manipulation is not supported by %s', $this); } /** * Returns last response headers. * * @return array * * @throws UnsupportedDriverActionException */ public function getResponseHeaders() { throw new UnsupportedDriverActionException('Response headers manipulation is not supported by %s', $this); } /** * Sets cookie. * * @param string $name * @param string $value */ public function setCookie($name, $value = null) { if (null === $value) { try { $this->executeScript(sprintf('_sahi._deleteCookie("%s")', $name)); } catch (ConnectionException $e) {} } else { $value = str_replace('"', '\\"', $value); $this->executeScript(sprintf('_sahi._createCookie("%s", "%s")', $name, $value)); } } /** * Returns cookie by name. * * @param string $name * * @return string|null */ public function getCookie($name) { try { return urldecode($this->evaluateScript(sprintf('_sahi._cookie("%s")', $name))); } catch (ConnectionException $e) {} } /** * Returns last response status code. * * @return integer * * @throws UnsupportedDriverActionException */ public function getStatusCode() { throw new UnsupportedDriverActionException('Status code reading is not supported by %s', $this); } /** * Returns last response content. * * @return string */ public function getContent() { $html = $this->evaluateScript('document.getElementsByTagName("html")[0].innerHTML'); $html = $this->removeSahiInjectionFromText($html); return "\n$html\n"; } /** * Capture a screenshot of the current window. * * @throws UnsupportedDriverActionException */ public function getScreenshot() { throw new UnsupportedDriverActionException('Screenshots are not supported by %s', $this); } /** * Finds elements with specified XPath query. * * @param string $xpath * * @return array array of NodeElements */ public function find($xpath) { $jsXpath = $this->prepareXPath($xpath); $function = <<evaluateScript($function)); $elements = array(); for ($i = 0; $i < $count; $i++) { $elements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i + 1), $this->session); } return $elements; } /** * Returns element's tag name by it's XPath query. * * @param string $xpath * * @return string */ public function getTagName($xpath) { return strtolower($this->client->findByXPath($this->prepareXPath($xpath))->getName()); } /** * Returns element's text by it's XPath query. * * @param string $xpath * * @return string */ public function getText($xpath) { return $this->removeSahiInjectionFromText( $this->client->findByXPath($this->prepareXPath($xpath))->getText() ); } /** * Returns element's html by it's XPath query. * * @param string $xpath * * @return string */ public function getHtml($xpath) { return $this->client->findByXPath($this->prepareXPath($xpath))->getHTML(); } /** * Returns element's attribute by it's XPath query. * * @param string $xpath * @param string $name * * @return mixed */ public function getAttribute($xpath, $name) { return $this->client->findByXPath($this->prepareXPath($xpath))->getAttr($name); } /** * Returns element's value by it's XPath query. * * @param string $xpath * * @return mixed */ public function getValue($xpath) { $xpath = $this->prepareXPath($xpath); $tag = $this->getTagName($xpath); $type = $this->getAttribute($xpath, 'type'); $value = null; if ('radio' === $type) { $name = $this->getAttribute($xpath, 'name'); if (null !== $name) { $function = <<evaluateScript($function); } } elseif ('checkbox' === $type) { return $this->client->findByXPath($xpath)->isChecked(); } elseif ('select' === $tag && 'multiple' === $this->getAttribute($xpath, 'multiple')) { $name = $this->getAttribute($xpath, 'name'); $function = <<evaluateScript($function); if ('' === $value || false === $value) { return array(); } else { return explode(',', $value); } } return $this->client->findByXPath($xpath)->getValue(); } /** * Sets element's value by it's XPath query. * * @param string $xpath * @param string $value */ public function setValue($xpath, $value) { $type = $this->getAttribute($xpath, 'type'); if ('radio' === $type) { $this->selectRadioOption($xpath, $value); } elseif ('checkbox' === $type) { if ((Boolean) $value) { $this->client->findByXPath($this->prepareXPath($xpath))->check(); } else { $this->client->findByXPath($this->prepareXPath($xpath))->uncheck(); } } else { $this->client->findByXPath($this->prepareXPath($xpath))->setValue($value); } } /** * Checks checkbox by it's XPath query. * * @param string $xpath */ public function check($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->check(); } /** * Unchecks checkbox by it's XPath query. * * @param string $xpath */ public function uncheck($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->uncheck(); } /** * Checks whether checkbox checked located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isChecked($xpath) { return $this->client->findByXPath($this->prepareXPath($xpath))->isChecked(); } /** * Selects option from select field located by it's XPath query. * * @param string $xpath * @param string $value * @param Boolean $multiple */ public function selectOption($xpath, $value, $multiple = false) { $type = $this->getAttribute($xpath, 'type'); if ('radio' === $type) { $this->selectRadioOption($xpath, $value); } else { $this->client->findByXPath($this->prepareXPath($xpath))->choose($value, $multiple); } } /** * Clicks button or link located by it's XPath query. * * @param string $xpath */ public function click($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->click(); } /** * Double-clicks button or link located by it's XPath query. * * @param string $xpath */ public function doubleClick($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->doubleClick(); } /** * Right-clicks button or link located by it's XPath query. * * @param string $xpath */ public function rightClick($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->rightClick(); } /** * Attaches file path to file field located by it's XPath query. * * @param string $xpath * @param string $path */ public function attachFile($xpath, $path) { $this->client->findByXPath($this->prepareXPath($xpath))->setFile($path); } /** * Checks whether element visible located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isVisible($xpath) { return $this->client->findByXPath($this->prepareXPath($xpath))->isVisible(); } /** * Simulates a mouse over on the element. * * @param string $xpath */ public function mouseOver($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->mouseOver(); } /** * Brings focus to element. * * @param string $xpath */ public function focus($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->focus(); } /** * Removes focus from element. * * @param string $xpath */ public function blur($xpath) { $this->client->findByXPath($this->prepareXPath($xpath))->blur(); } /** * Presses specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyPress($xpath, $char, $modifier = null) { $this->client->findByXPath($this->prepareXPath($xpath))->keyPress( $char, strtoupper($modifier) ); } /** * Pressed down specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyDown($xpath, $char, $modifier = null) { $this->client->findByXPath($this->prepareXPath($xpath))->keyDown( $char, strtoupper($modifier) ); } /** * Pressed up specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyUp($xpath, $char, $modifier = null) { $this->client->findByXPath($this->prepareXPath($xpath))->keyUp( $char, strtoupper($modifier) ); } /** * Drag one element onto another. * * @param string $sourceXpath * @param string $destinationXpath */ public function dragTo($sourceXpath, $destinationXpath) { $from = $this->client->findByXPath($this->prepareXPath($sourceXpath)); $to = $this->client->findByXPath($this->prepareXPath($destinationXpath)); $from->dragDrop($to); } /** * Executes JS script. * * @param string $script */ public function executeScript($script) { $this->client->getConnection()->executeJavascript($script); } /** * Evaluates JS script. * * @param string $script * * @return mixed */ public function evaluateScript($script) { return $this->client->getConnection()->evaluateJavascript($script); } /** * Waits some time or until JS condition turns true. * * @param integer $time time in milliseconds * @param string $condition JS condition */ public function wait($time, $condition) { $this->client->wait($time, $condition); } /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) * * @throws UnsupportedDriverActionException */ public function resizeWindow($width, $height, $name = null) { throw new UnsupportedDriverActionException('Window resizing is not supported by %s', $this); } /** * Selects specific radio option. * * @param string $xpath xpath to one of the radio buttons * @param string $value value to be set */ private function selectRadioOption($xpath, $value) { $name = $this->getAttribute($this->prepareXPath($xpath), 'name'); if (null !== $name) { $function = <<executeScript($function); } } /** * Prepare XPath to be sent via Sahi proxy. * * @param string $xpath * * @return string */ private function prepareXPath($xpath) { return strtr($xpath, array('"' => '\\"')); } /** * Removes injected by Sahi code. * * @param string $string * * @return string */ private function removeSahiInjectionFromText($string) { $string = preg_replace(array( '/<\!--SAHI_INJECT_START--\>.*\<\!--SAHI_INJECT_END--\>/sU', '/\\/\*\<\!\[CDATA\[\*\/\/\*----\>\*\/__sahi.*\<\!--SAHI_INJECT_END--\>/sU' ), '', $string); $string = str_replace('/**/__sahiDebugStr__="";__sahiDebug__=function(s){__sahiDebugStr__+=(s+"\n");};/*--*//*]]>*/ /**/_sahi.createCookie(\'sahisid\', _sahi.sid);_sahi.loadXPathScript()/*--*//*]]>*/ /**/eval(_sahi.sendToServer("/_s_/dyn/Player_script/script.js"));/*--*//*]]>*/ ', '', $string); return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Selenium driver. * * @author Alexandre Salomé */ class SeleniumDriver implements DriverInterface { const MODIFIER_CTRL = 'ctrl'; const MODIFIER_ALT = 'alt'; const MODIFIER_SHIFT = 'shift'; const MODIFIER_META = 'meta'; /** * Default timeout for Selenium (in milliseconds) * * @var int */ private $timeout = 60000; /** * The current session * * @var Behat\Mink\Session */ private $session; /** * The selenium browser instance * * @var Selenium\Browser */ private $browser; /** * Flag indicating if the browser is started * * @var boolean */ private $started = false; /** * Instanciates the driver. * * @param string $browser Browser name * @param string $baseUrl Base URL for testing * @param Selenium\Client $client The client for getting a browser */ public function __construct($browser, $baseUrl, SeleniumClient $client) { $this->browser = $client->getBrowser($baseUrl, $browser); } /** * Returns Selenium browser instance. * * @return Selenium\Browser */ public function getBrowser() { return $this->browser; } /** * @see Behat\Mink\Driver\DriverInterface::setSession() */ public function setSession(Session $session) { $this->session = $session; } /** * @see Behat\Mink\Driver\DriverInterface::start() */ public function start() { $this->started = true; $this->browser->start(); } /** * @see Behat\Mink\Driver\DriverInterface::isStarted() */ public function isStarted() { return $this->started; } /** * @see Behat\Mink\Driver\DriverInterface::stop() */ public function stop() { if (true === $this->started) { $this->browser->stop(); } $this->started = false; } /** * @see Behat\Mink\Driver\DriverInterface::reset() */ public function reset() { $this->browser->deleteAllVisibleCookies(); } /** * @see Behat\Mink\Driver\DriverInterface::visit() */ public function visit($url) { $this->browser ->open($url) ->waitForPageToLoad($this->timeout) ; } /** * @see Behat\Mink\Driver\DriverInterface::getCurrentUrl() */ public function getCurrentUrl() { return $this->browser->getLocation(); } /** * @see Behat\Mink\Driver\DriverInterface::reload() */ public function reload() { $this->browser ->refresh() ->waitForPageToLoad($this->timeout) ; } /** * @see Behat\Mink\Driver\DriverInterface::forward() */ public function forward() { $this->browser ->runScript('history.forward()') ->waitForPageToLoad($this->timeout) ; } /** * @see Behat\Mink\Driver\DriverInterface::back() */ public function back() { $this->browser->goBack(); } /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) */ public function switchToWindow($name = null) { $this->browser->selectWindow($name ? $name : 'null'); } /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) */ public function switchToIFrame($name = null) { if ($name) { $this->browser->selectFrame('dom=window.frames["'.$name.'"]'); } else { $this->browser->selectFrame('relative=top'); } } /** * @see Behat\Mink\Driver\DriverInterface::setBasicAuth() */ public function setBasicAuth($user, $password) { throw new UnsupportedDriverActionException('Basic Auth is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::setRequestHeader() */ public function setRequestHeader($name, $value) { throw new UnsupportedDriverActionException('Request header is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::getResponseHeaders() */ public function getResponseHeaders() { throw new UnsupportedDriverActionException('Request header is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::setCookie() */ public function setCookie($name, $value = null) { if (null === $value) { $this->browser->deleteCookie($name, ''); } else { $this->browser->createCookie($name.'='.$value, ''); } } /** * @see Behat\Mink\Driver\DriverInterface::getCookie() */ public function getCookie($name) { if ($this->browser->isCookiePresent($name)) { return $this->browser->getCookieByName($name); } return null; } /** * @see Behat\Mink\Driver\DriverInterface::getStatusCode() */ public function getStatusCode() { throw new UnsupportedDriverActionException('Request header is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::getContent() */ public function getContent() { return $this->browser->getHtmlSource(); } /** * Capture a screenshot of the current window. * * @throws UnsupportedDriverActionException */ public function getScreenshot() { throw new UnsupportedDriverActionException('Screenshots are not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::find() */ public function find($xpath) { $nodes = $this->getCrawler()->filterXPath($xpath); $elements = array(); foreach ($nodes as $i => $node) { $elements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i + 1), $this->session); } return $elements; } /** * @see Behat\Mink\Driver\DriverInterface::getTagName() */ public function getTagName($xpath) { $nodes = $this->getCrawler()->filterXPath($xpath)->eq(0); $nodes->rewind(); $node = $nodes->current(); return $node->nodeName; } /** * @see Behat\Mink\Driver\DriverInterface::getText() */ public function getText($xpath) { $result = $this->browser->getText(SeleniumLocator::xpath($xpath)); return preg_replace("/[ \n]+/", " ", $result); } /** * @see Behat\Mink\Driver\DriverInterface::getHtml() */ public function getHtml($xpath) { $nodes = $this->getCrawler()->filterXPath($xpath)->eq(0); $nodes->rewind(); $node = $nodes->current(); $text = $node->C14N(); // cut the tag itself (making innerHTML out of outerHTML) $text = preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $text); return $text; } /** * @see Behat\Mink\Driver\DriverInterface::getAttribute() */ public function getAttribute($xpath, $name) { $result = $this->getCrawler()->filterXPath($xpath)->attr($name); if ('' === $result) { $result = null; } return $result; } /** * @see Behat\Mink\Driver\DriverInterface::getValue() */ public function getValue($xpath) { $xpathEscaped = str_replace('"', '\"', $xpath); $script = <<= 0) { value = "string:" + node.options.item(idx).value; } else { value = null; } } } else { value = "string:" + node.getAttribute('value'); } value JS; $value = $this->browser->getEval($script); if (null === $value) { return null; } elseif (preg_match('/^string:(.*)$/', $value, $vars)) { return $vars[1]; } elseif (preg_match('/^boolean:(.*)$/', $value, $vars)) { return 'true' === strtolower($vars[1]); } elseif (preg_match('/^array:(.*)$/', $value, $vars)) { if ('' === trim($vars[1])) { return array(); } return explode(',', $vars[1]); } } /** * @see Behat\Mink\Driver\DriverInterface::setValue() */ public function setValue($xpath, $value) { $this->browser->type(SeleniumLocator::xpath($xpath), $value); } /** * @see Behat\Mink\Driver\DriverInterface::check() */ public function check($xpath) { $this->browser->check(SeleniumLocator::xpath($xpath)); } /** * @see Behat\Mink\Driver\DriverInterface::uncheck() */ public function uncheck($xpath) { $this->browser->uncheck(SeleniumLocator::xpath($xpath)); } /** * @see Behat\Mink\Driver\DriverInterface::selectOption() */ public function selectOption($xpath, $value, $multiple = false) { $xpathEscaped = str_replace('"', '\"', $xpath); $valueEscaped = str_replace('"', '\"', $value); $multipleJS = $multiple ? 'true' : 'false'; $script = <<browser->getEval($script); } /** * @see Behat\Mink\Driver\DriverInterface::click() */ public function click($xpath) { $this->browser->click(SeleniumLocator::xpath($xpath)); $readyState = $this->browser->getEval('window.document.readyState'); if ($readyState == 'loading' || $readyState == 'interactive') { $this->browser->waitForPageToLoad($this->timeout); } $this->getCurrentUrl(); } /** * @see Behat\Mink\Driver\DriverInterface::isChecked() */ public function isChecked($xpath) { return $this->browser->isChecked(SeleniumLocator::xpath($xpath)); } /** * @see Behat\Mink\Driver\DriverInterface::attachFile() */ public function attachFile($xpath, $path) { $this->browser->attachFile(SeleniumLocator::xpath($xpath), 'file://'.$path); } /** * @see Behat\Mink\Driver\DriverInterface::doubleClick() */ public function doubleClick($xpath) { $this->browser->doubleClick(SeleniumLocator::xpath($xpath)); } /** * @see Behat\Mink\Driver\DriverInterface::rightClick() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function rightClick($xpath) { throw new UnsupportedDriverActionException('Right click is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::mouseOver() */ public function mouseOver($xpath) { $this->browser->mouseOver(SeleniumLocator::xpath($xpath)); } /** * @see Behat\Mink\Driver\DriverInterface::focus() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function focus($xpath) { throw new UnsupportedDriverActionException('Focus is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::blur() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function blur($xpath) { throw new UnsupportedDriverActionException('Blur is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::keyPress() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function keyPress($xpath, $char, $modifier = null) { $this->keyDownModifier($modifier); $this->browser->keyPress(SeleniumLocator::xpath($xpath), $char); $this->keyUpModifier($modifier); } /** * @see Behat\Mink\Driver\DriverInterface::keyPress() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function keyDown($xpath, $char, $modifier = null) { $this->keyDownModifier($modifier); $this->browser->keyDown(SeleniumLocator::xpath($xpath), $char); $this->keyUpModifier($modifier); } /** * @see Behat\Mink\Driver\DriverInterface::keyPress() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function keyUp($xpath, $char, $modifier = null) { $this->keyDownModifier($modifier); $this->browser->keyUp(SeleniumLocator::xpath($xpath), $char); $this->keyUpModifier($modifier); } /** * @see Behat\Mink\Driver\DriverInterface::executeScript() */ public function executeScript($script) { $this->browser->runScript($script); } /** * @see Behat\Mink\Driver\DriverInterface::evaluateScript() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function evaluateScript($script) { throw new UnsupportedDriverActionException('Evaluate script is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::wait() */ public function wait($time, $condition) { try { $this->browser->waitForCondition('with (selenium.browserbot.getCurrentWindow()) { '."\n".$condition."\n }", $time); } catch (SeleniumException $e) {} } /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) * * @throws UnsupportedDriverActionException */ public function resizeWindow($width, $height, $name = null) { throw new UnsupportedDriverActionException('Window resizing is not supported by %s', $this); } /** * @see Behat\Mink\Driver\DriverInterface::isVisible() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function isVisible($xpath) { return $this->browser->isVisible(SeleniumLocator::xpath($xpath)); } /** * @see Behat\Mink\Driver\DriverInterface::dragTo() * * @throws Behat\Mink\Exception\UnsupportedDriverActionException action is not supported by this driver */ public function dragTo($sourceXpath, $destinationXpath) { $this->browser->dragAndDropToObject(SeleniumLocator::xpath($sourceXpath), SeleniumLocator::xpath($destinationXpath)); } /** * Returns crawler instance (got from client). * * @return Symfony\Component\DomCrawler\Crawler * * @throws Behat\Mink\Exception\DriverException if can't init crawler (no page is opened) */ private function getCrawler() { $content = ''.$this->browser->getHtmlSource().''; $contentType = null; // get content-type from meta tag if (preg_match('/\]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $content, $matches)) { $contentType = 'text/html;charset='.$matches[1]; } $crawler = new Crawler(); $crawler->addContent($content, $contentType); return $crawler; } /** * Handles the key down of a keyboard modifier * * @param string $modifier The modifier to handle (see self::MODIFIER_*) */ protected function keyDownModifier($modifier) { switch ($modifier) { case self::MODIFIER_CTRL: throw new UnsupportedDriverActionException('Ctrl key is not supported by %s', $this); case self::MODIFIER_ALT: $this->browser->altKeyDown(); break; case self::MODIFIER_SHIFT: $this->browser->shiftKeyDown(); break; case self::MODIFIER_META: $this->browser->metaKeyDown(); break; } } /** * Handles the key up of a keyboard modifier * * @param string $modifier The modifier to handle (see self::MODIFIER_*) */ protected function keyUpModifier($modifier) { switch ($modifier) { case self::MODIFIER_CTRL: throw new UnsupportedDriverActionException('Ctrl key is not supported by %s', $this); case self::MODIFIER_ALT: $this->browser->altKeyUp(); break; case self::MODIFIER_SHIFT: $this->browser->shiftKeyUp(); break; case self::MODIFIER_META: $this->browser->metaKeyUp(); break; } } } /* Syn - a Standalone Synthetic Event Library. Syn is used to simulate user actions such as typing, clicking, dragging the mouse. It creates synthetic events and performs default event behavior. http://jupiterjs.com/news/syn-a-standalone-synthetic-event-library */(function(){var e=function(k,a){for(var b in a)k[b]=a[b];return k},j={msie:!(!window.attachEvent||window.opera),opera:!!window.opera,webkit:-1=h}try{window.event=a}catch(e){}return 0>=c.sourceIndex||c.fireEvent&&c.fireEvent("on"+g,a)},create:{page:{event:function(a,b,c){var g= f.helpers.getWindow(c).document||document,d;if(g.createEvent)d=g.createEvent("Events"),d.initEvent(a,!0,!0);else try{d=m(a,b,c)}catch(l){}return d}},focus:{event:function(a,b,c){f.onParents(c,function(a){if(f.isFocusable(a)){if("html"!==a.nodeName.toLowerCase())a.focus(),l=a;else if(l)a=f.helpers.getWindow(c).document,a===window.document&&(a.activeElement?a.activeElement.blur():l.blur(),l=null);return!1}});return!0}}},support:{clickChanges:!1,clickSubmits:!1,keypressSubmits:!1,mouseupSubmits:!1,radioClickChanges:!1, focusChanges:!1,linkHrefJS:!1,keyCharacters:!1,backspaceWorks:!1,mouseDownUpClicks:!1,tabKeyTabs:!1,keypressOnAnchorClicks:!1,optionClickBubbles:!1,ready:0},trigger:function(a,b,d){b||(b={});var l=f.create,h=l[a]&&l[a].setup,e=g.test(a)?"key":c.test(a)?"page":"mouse",i=l[a]||{},e=l[e],l=d;2===f.support.ready&&h&&h(a,b,d);h=b._autoPrevent;delete b._autoPrevent;if(i.event)i=i.event(a,b,d);else{b=e.options?e.options(a,b,d):b;if(!f.support.changeBubbles&&/option/i.test(d.nodeName))l=d.parentNode;i=e.event(a, b,l);i=f.dispatch(i,l,a,h)}i&&2===f.support.ready&&f.defaults[a]&&f.defaults[a].call(d,b,h);return i},eventSupported:function(a){var b=document.createElement("div"),a="on"+a,c=a in b;c||(b.setAttribute(a,"return;"),c="function"===typeof b[a]);return c}});e(f.init.prototype,{then:function(a,b,c,g){f.autoDelay&&this.delay();var d=f.args(b,c,g),l=this;this.queue.unshift(function(b){if("function"===typeof this[a])this.element=d.element||b,this[a](d.options,this.element,function(a,b){d.callback&&d.callback.apply(l, arguments);l.done.apply(l,arguments)});else return this.result=f.trigger(a,d.options,d.element),d.callback&&d.callback.call(this,d.element,this.result),this});return this},delay:function(a,b){"function"===typeof a&&(b=a,a=null);var a=a||600,c=this;this.queue.unshift(function(){setTimeout(function(){b&&b.apply(c,[]);c.done.apply(c,arguments)},a)});return this},done:function(a,b){b&&(this.element=b);this.queue.length&&this.queue.pop().call(this,this.element,a)},_click:function(a,b,c,g){f.helpers.addOffset(a, b);f.trigger("mousedown",a,b);setTimeout(function(){f.trigger("mouseup",a,b);!f.support.mouseDownUpClicks||g?(f.trigger("click",a,b),c(!0)):(f.create.click.setup("click",a,b),f.defaults.click.call(b),setTimeout(function(){c(!0)},1))},1)},_rightClick:function(a,b,c){f.helpers.addOffset(a,b);var g=e(e({},f.mouse.browser.right.mouseup),a);f.trigger("mousedown",g,b);setTimeout(function(){f.trigger("mouseup",g,b);f.mouse.browser.right.contextmenu&&f.trigger("contextmenu",e(e({},f.mouse.browser.right.contextmenu), a),b);c(!0)},1)},_dblclick:function(a,b,c){f.helpers.addOffset(a,b);var g=this;this._click(a,b,function(){setTimeout(function(){g._click(a,b,function(){f.trigger("dblclick",a,b);c(!0)},!0)},2)})}});for(var j="click,dblclick,move,drag,key,type,rightClick".split(","),v=function(a){f[a]=function(b,c,g){return f("_"+a,b,c,g)};f.init.prototype[a]=function(b,c,g){return this.then("_"+a,b,c,g)}},r=0;r"; document.documentElement.appendChild(h);b=h.firstChild;i=b.childNodes[0];a=b.childNodes[2];d=b.getElementsByTagName("select")[0];i.checked=!1;i.onchange=function(){Syn.support.clickChanges=!0};Syn.trigger("click",{},i);Syn.support.clickChecks=i.checked;i.checked=!1;Syn.trigger("change",{},i);Syn.support.changeChecks=i.checked;b.onsubmit=function(a){a.preventDefault&&a.preventDefault();Syn.support.clickSubmits=!0;return!1};Syn.trigger("click",{},a);b.childNodes[1].onchange=function(){Syn.support.radioClickChanges= !0};Syn.trigger("click",{},b.childNodes[1]);Syn.bind(h,"click",function(){Syn.support.optionClickBubbles=!0;Syn.unbind(h,"click",arguments.callee)});Syn.trigger("click",{},d.firstChild);Syn.support.changeBubbles=Syn.eventSupported("change");h.onclick=function(){Syn.support.mouseDownUpClicks=!0};Syn.trigger("mousedown",{},h);Syn.trigger("mouseup",{},h);document.documentElement.removeChild(h);window.__synthTest=e;Syn.support.ready++}else setTimeout(arguments.callee,1)})()})(!0); (function(){Syn.key.browsers={webkit:{prevent:{keyup:[],keydown:["char","keypress"],keypress:["char"]},character:{keydown:[0,"key"],keypress:["char","char"],keyup:[0,"key"]},specialChars:{keydown:[0,"char"],keyup:[0,"char"]},navigation:{keydown:[0,"key"],keyup:[0,"key"]},special:{keydown:[0,"key"],keyup:[0,"key"]},tab:{keydown:[0,"char"],keyup:[0,"char"]},"pause-break":{keydown:[0,"key"],keyup:[0,"key"]},caps:{keydown:[0,"key"],keyup:[0,"key"]},escape:{keydown:[0,"key"],keyup:[0,"key"]},"num-lock":{keydown:[0, "key"],keyup:[0,"key"]},"scroll-lock":{keydown:[0,"key"],keyup:[0,"key"]},print:{keyup:[0,"key"]},"function":{keydown:[0,"key"],keyup:[0,"key"]},"\r":{keydown:[0,"key"],keypress:["char","key"],keyup:[0,"key"]}},gecko:{prevent:{keyup:[],keydown:["char"],keypress:["char"]},character:{keydown:[0,"key"],keypress:["char",0],keyup:[0,"key"]},specialChars:{keydown:[0,"key"],keypress:[0,"key"],keyup:[0,"key"]},navigation:{keydown:[0,"key"],keypress:[0,"key"],keyup:[0,"key"]},special:{keydown:[0,"key"],keyup:[0, "key"]},"\t":{keydown:[0,"key"],keypress:[0,"key"],keyup:[0,"key"]},"pause-break":{keydown:[0,"key"],keypress:[0,"key"],keyup:[0,"key"]},caps:{keydown:[0,"key"],keyup:[0,"key"]},escape:{keydown:[0,"key"],keypress:[0,"key"],keyup:[0,"key"]},"num-lock":{keydown:[0,"key"],keyup:[0,"key"]},"scroll-lock":{keydown:[0,"key"],keyup:[0,"key"]},print:{keyup:[0,"key"]},"function":{keydown:[0,"key"],keyup:[0,"key"]},"\r":{keydown:[0,"key"],keypress:[0,"key"],keyup:[0,"key"]}},msie:{prevent:{keyup:[],keydown:["char", "keypress"],keypress:["char"]},character:{keydown:[null,"key"],keypress:[null,"char"],keyup:[null,"key"]},specialChars:{keydown:[null,"char"],keyup:[null,"char"]},navigation:{keydown:[null,"key"],keyup:[null,"key"]},special:{keydown:[null,"key"],keyup:[null,"key"]},tab:{keydown:[null,"char"],keyup:[null,"char"]},"pause-break":{keydown:[null,"key"],keyup:[null,"key"]},caps:{keydown:[null,"key"],keyup:[null,"key"]},escape:{keydown:[null,"key"],keypress:[null,"key"],keyup:[null,"key"]},"num-lock":{keydown:[null, "key"],keyup:[null,"key"]},"scroll-lock":{keydown:[null,"key"],keyup:[null,"key"]},print:{keyup:[null,"key"]},"function":{keydown:[null,"key"],keyup:[null,"key"]},"\r":{keydown:[null,"key"],keypress:[null,"key"],keyup:[null,"key"]}},opera:{prevent:{keyup:[],keydown:[],keypress:["char"]},character:{keydown:[null,"key"],keypress:[null,"char"],keyup:[null,"key"]},specialChars:{keydown:[null,"char"],keypress:[null,"char"],keyup:[null,"char"]},navigation:{keydown:[null,"key"],keypress:[null,"key"]},special:{keydown:[null, "key"],keypress:[null,"key"],keyup:[null,"key"]},tab:{keydown:[null,"char"],keypress:[null,"char"],keyup:[null,"char"]},"pause-break":{keydown:[null,"key"],keypress:[null,"key"],keyup:[null,"key"]},caps:{keydown:[null,"key"],keyup:[null,"key"]},escape:{keydown:[null,"key"],keypress:[null,"key"]},"num-lock":{keyup:[null,"key"],keydown:[null,"key"],keypress:[null,"key"]},"scroll-lock":{keydown:[null,"key"],keypress:[null,"key"],keyup:[null,"key"]},print:{},"function":{keydown:[null,"key"],keypress:[null, "key"],keyup:[null,"key"]},"\r":{keydown:[null,"key"],keypress:[null,"key"],keyup:[null,"key"]}}};Syn.mouse.browsers={webkit:{right:{mousedown:{button:2,which:3},mouseup:{button:2,which:3},contextmenu:{button:2,which:3}},left:{mousedown:{button:0,which:1},mouseup:{button:0,which:1},click:{button:0,which:1}}},opera:{right:{mousedown:{button:2,which:3},mouseup:{button:2,which:3}},left:{mousedown:{button:0,which:1},mouseup:{button:0,which:1},click:{button:0,which:1}}},msie:{right:{mousedown:{button:2}, mouseup:{button:2},contextmenu:{button:0}},left:{mousedown:{button:1},mouseup:{button:1},click:{button:0}}},chrome:{right:{mousedown:{button:2,which:3},mouseup:{button:2,which:3},contextmenu:{button:2,which:3}},left:{mousedown:{button:0,which:1},mouseup:{button:0,which:1},click:{button:0,which:1}}},gecko:{left:{mousedown:{button:0,which:1},mouseup:{button:0,which:1},click:{button:0,which:1}},right:{mousedown:{button:2,which:3},mouseup:{button:2,which:3},contextmenu:{button:2,which:3}}}};Syn.key.browser= function(){if(Syn.key.browsers[window.navigator.userAgent])return Syn.key.browsers[window.navigator.userAgent];for(var e in Syn.browser)if(Syn.browser[e]&&Syn.key.browsers[e])return Syn.key.browsers[e];return Syn.key.browsers.gecko}();Syn.mouse.browser=function(){if(Syn.mouse.browsers[window.navigator.userAgent])return Syn.mouse.browsers[window.navigator.userAgent];for(var e in Syn.browser)if(Syn.browser[e]&&Syn.mouse.browsers[e])return Syn.mouse.browsers[e];return Syn.mouse.browsers.gecko}()})(!0); (function(){var e=Syn.helpers,j=Syn,m=function(a){if(void 0!==a.selectionStart)return document.activeElement&&document.activeElement!=a&&a.selectionStart==a.selectionEnd&&0==a.selectionStart?{start:a.value.length,end:a.value.length}:{start:a.selectionStart,end:a.selectionEnd};try{if("input"==a.nodeName.toLowerCase()){var b=e.getWindow(a).document.selection.createRange(),d=a.createTextRange();d.setEndPoint("EndToStart",b);var g=d.text.length;return{start:g,end:g+b.text.length}}var b=e.getWindow(a).document.selection.createRange(), d=b.duplicate(),c=b.duplicate(),l=b.duplicate();c.collapse();l.collapse(!1);c.moveStart("character",-1);l.moveStart("character",-1);d.moveToElementText(a);d.setEndPoint("EndToEnd",b);var g=d.text.length-b.text.length,f=d.text.length;0!=g&&""==c.text&&(g+=2);0!=f&&""==l.text&&(f+=2);return{start:g,end:f}}catch(h){return{start:a.value.length,end:a.value.length}}},h=function(a){for(var a=e.getWindow(a).document,b=[],d=a.getElementsByTagName("*"),g=d.length,c=0;cthis.value.length?this.value.length:c.end+1):Syn.selectText(this,c.end+1>this.value.length?this.value.length:c.end+1))},up:function(){if(/select/i.test(this.nodeName))this.selectedIndex=this.selectedIndex?this.selectedIndex-1:0},down:function(){/select/i.test(this.nodeName)&& (Syn.changeOnBlur(this,"selectedIndex",this.selectedIndex),this.selectedIndex+=1)},shift:function(){return null}}});e.extend(Syn.create,{keydown:{setup:function(a,b,d){-1!=e.inArray(b,Syn.key.kinds.special)&&(Syn.key[b+"Key"]=d)}},keypress:{setup:function(a,b,d){j.support.keyCharacters&&!j.support.keysOnNotFocused&&d.focus()}},keyup:{setup:function(a,b){-1!=e.inArray(b,Syn.key.kinds.special)&&(Syn.key[b+"Key"]=null)}},key:{options:function(a,b){b="object"!=typeof b?{character:b}:b;b=e.extend({},b); b.character&&(e.extend(b,j.key.options(b.character,a)),delete b.character);return b=e.extend({ctrlKey:!!Syn.key.ctrlKey,altKey:!!Syn.key.altKey,shiftKey:!!Syn.key.shiftKey,metaKey:!!Syn.key.metaKey},b)},event:function(a,b,d){var g=e.getWindow(d).document||document;if(g.createEvent){var c;try{c=g.createEvent("KeyEvents"),c.initKeyEvent(a,!0,!0,window,b.ctrlKey,b.altKey,b.shiftKey,b.metaKey,b.keyCode,b.charCode)}catch(l){c=e.createBasicStandardEvent(a,b,g)}c.synthetic=!0}else try{c=e.createEventObject.apply(this, arguments),e.extend(c,b)}catch(f){}return c}}});var i={enter:"\r",backspace:"\u0008",tab:"\t",space:" "};e.extend(Syn.init.prototype,{_key:function(a,b,d){if(/-up$/.test(a)&&-1!=e.inArray(a.replace("-up",""),Syn.key.kinds.special))Syn.trigger("keyup",a.replace("-up",""),b),d(!0,b);else{var g=Syn.typeable.test(b.nodeName)&&m(b),c=i[a]||a,l=Syn.trigger("keydown",c,b),a=Syn.key.getDefault,f=Syn.key.browser.prevent,h,j=Syn.key.options(c,"keypress");l?j?(l=Syn.trigger("keypress",j,b))&&(h=a(c).call(b, j,e.getWindow(b),c,void 0,g)):h=a(c).call(b,j,e.getWindow(b),c,void 0,g):j&&-1==e.inArray("keypress",f.keydown)&&Syn.trigger("keypress",j,b);h&&h.nodeName&&(b=h);null!==h?setTimeout(function(){Syn.trigger("keyup",Syn.key.options(c,"keyup"),b);d(l,b)},1):d(l,b);return b}},_type:function(a,b,d){var g=a.match(/(\[[^\]]+\])|([^\[])/g),c=this,e=function(a,h){var i=g.shift();i?(h=h||b,1a.clientY||0>a.clientX)?b:d},j=function(a,b,d){var f=e(b,d);Syn.trigger(a,b,f||d);return f},m=function(a,b,d){var f=e(a,b);if(d!=f&&f&&d){var h=Syn.helpers.extend({},a);h.relatedTarget=f;Syn.trigger("mouseout",h,d);h.relatedTarget=d;Syn.trigger("mouseover", h,f)}Syn.trigger("mousemove",a,f||b);return f},h=function(a,b,d,f,h){var i=new Date,j=b.clientX-a.clientX,u=b.clientY-a.clientY,n=Syn.helpers.getWindow(f),o=e(a,f),p=n.document.createElement("div"),s=0;move=function(){var e=new Date,t=Syn.helpers.scrollOffset(n),e=(0==s?0:e-i)/d,q={clientX:j*e+a.clientX,clientY:u*e+a.clientY};s++;1>e?(Syn.helpers.extend(p.style,{left:q.clientX+t.left+2+"px",top:q.clientY+t.top+2+"px"}),o=m(q,f,o),setTimeout(arguments.callee,15)):(o=m(b,f,o),n.document.body.removeChild(p), h())};Syn.helpers.extend(p.style,{height:"5px",width:"5px",backgroundColor:"red",position:"absolute",zIndex:19999,fontSize:"1px"});n.document.body.appendChild(p);move()},i=function(a,b,d,f,e){j("mousedown",a,f);h(a,b,d,f,function(){j("mouseup",b,f);e()})},a=function(a){var a=Syn.jquery()(a),b=a.offset();return{pageX:b.left+a.width()/2,pageY:b.top+a.height()/2}},b=function(b,c,d){var f=/(\d+)[x ](\d+)/,e=/(\d+)X(\d+)/,h=/([+-]\d+)[xX ]([+-]\d+)/;"string"==typeof b&&h.test(b)&&d&&(d=a(d),b=b.match(h), b={pageX:d.pageX+parseInt(b[1]),pageY:d.pageY+parseInt(b[2])});"string"==typeof b&&f.test(b)&&(b=b.match(f),b={pageX:parseInt(b[1]),pageY:parseInt(b[2])});"string"==typeof b&&e.test(b)&&(b=b.match(e),b={clientX:parseInt(b[1]),clientY:parseInt(b[2])});"string"==typeof b&&(b=Syn.jquery()(b,c.document)[0]);b.nodeName&&(b=a(b));b.pageX&&(c=Syn.helpers.scrollOffset(c),b={clientX:b.pageX-c.left,clientY:b.pageY-c.top});return b},d=function(a,b,d){if(0>a.clientY){var f=Syn.helpers.scrollOffset(d);Syn.helpers.scrollDimensions(d); var e=f.top+a.clientY-100,h=e-f.top;0 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Selenium2 driver. * * @author Pete Otaqui */ class Selenium2Driver implements DriverInterface { /** * The current Mink session * @var Behat\Mink\Session */ private $session; /** * Whether the browser has been started * @var Boolean */ private $started = false; /** * The WebDriver instance * @var WebDriver */ private $webDriver; /** * The WebDriverSession instance * @var WebDriverSession */ private $wdSession; /** * Instantiates the driver. * * @param string $browser Browser name * @param array $desiredCapabilities The desired capabilities * @param string $wdHost The WebDriver host */ public function __construct($browserName = 'firefox', $desiredCapabilities = null, $wdHost = 'http://localhost:4444/wd/hub') { $this->setBrowserName($browserName); $this->setDesiredCapabilities($desiredCapabilities); $this->setWebDriver(new WebDriver($wdHost)); } /** * Sets the browser name * * @param string $browserName the name of the browser to start, default is 'firefox' */ protected function setBrowserName($browserName = 'firefox') { $this->browserName = $browserName; } /** * Sets the desired capabilities - called on construction. If null is provided, will set the * defaults as dsesired. * * See http://code.google.com/p/selenium/wiki/DesiredCapabilities * * @param array $desiredCapabilities an array of capabilities to pass on to the WebDriver server */ public function setDesiredCapabilities($desiredCapabilities = null) { if (null === $desiredCapabilities) { $desiredCapabilities = self::getDefaultCapabilities(); } if (isset($desiredCapabilities['firefox'])) { foreach ($desiredCapabilities['firefox'] as $capability => $value) { switch ($capability) { case 'profile': $desiredCapabilities['firefox_'.$capability] = base64_encode(file_get_contents($value)); break; default: $desiredCapabilities['firefox_'.$capability] = $value; } } unset($desiredCapabilities['firefox']); } if (isset($desiredCapabilities['chrome'])) { foreach ($desiredCapabilities['chrome'] as $capability => $value) { $desiredCapabilities['chrome.'.$capability] = $value; } unset($desiredCapabilities['chrome']); } $this->desiredCapabilities = $desiredCapabilities; } /** * Sets the WebDriver instance * * @param WebDriver $webDriver An instance of the WebDriver class */ public function setWebDriver(WebDriver $webDriver) { $this->webDriver = $webDriver; } /** * Gets the WebDriverSession instance * * @param WebDriverSession $webDriver An instance of the WebDriverSession class */ public function getWebDriverSession() { return $this->wdSession; } /** * Returns the default capabilities * * @return array */ public static function getDefaultCapabilities() { return array( 'browserName' => 'firefox', 'version' => '9', 'platform' => 'ANY', 'browserVersion' => '9', 'browser' => 'firefox', 'name' => 'Behat Test', 'deviceOrientation' => 'portrait', 'deviceType' => 'tablet' ); } /** * Makes sure that the Syn event library has been injected into the current page, * and return $this for a fluid interface, * $this->withSyn()->executeJsOnXpath($xpath, $script); * * @return mixed */ protected function withSyn() { $hasSyn = $this->wdSession->execute(array( 'script' => 'return typeof window["Syn"]!=="undefined"', 'args' => array() )); if (!$hasSyn) { $synJs = file_get_contents(dirname(__FILE__).DIRECTORY_SEPARATOR.'Selenium2'.DIRECTORY_SEPARATOR.'syn.js'); $this->wdSession->execute(array( 'script' => $synJs, 'args' => array() )); } return $this; } /** * Creates some options for key events * * @param string $event the type of event ('keypress', 'keydown', 'keyup'); * @param string $char the character or code * @param string $modifier=null one of 'shift', 'alt', 'ctrl' or 'meta' * * @return string a json encoded options array for Syn */ protected static function charToOptions($event, $char, $modifier=null) { $ord = ord($char); if (is_numeric($char)) { $ord = $char; $char = chr($char); } $options = array( 'keyCode' => $ord, 'charCode' => $ord ); if ($modifier) { $options[$modifier.'Key'] = 1; } return json_encode($options); } /** * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will * be replaced with a reference to the result of the $xpath query * * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); * * @param string $xpath the xpath to search with * @param string $script the script to execute * @param Boolean $sync whether to run the script synchronously (default is TRUE) * * @return mixed */ protected function executeJsOnXpath($xpath, $script, $sync = true) { $element = $this->wdSession->element('xpath', $xpath); $elementID = $element->getID(); $subscript = "arguments[0]"; $script = str_replace('{{ELEMENT}}', $subscript, $script); $execute = ($sync) ? 'execute' : 'execute_async'; return $this->wdSession->$execute(array( 'script' => $script, 'args' => array(array('ELEMENT' => $elementID)) )); } /** * @see Behat\Mink\Driver\DriverInterface::setSession() */ public function setSession(Session $session) { $this->session = $session; } /** * Starts driver. */ public function start() { $this->wdSession = $this->webDriver->session($this->browserName, $this->desiredCapabilities); if (!$this->wdSession) { throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); } $this->started = true; } /** * Checks whether driver is started. * * @return Boolean */ public function isStarted() { return $this->started; } /** * Stops driver. */ public function stop() { if (!$this->wdSession) { throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); } $this->started = false; try { $this->wdSession->close(); } catch (UnknownError $e) { throw new DriverException('Could not close connection'); } } /** * Resets driver. */ public function reset() { $this->wdSession->deleteAllCookies(); } /** * Visit specified URL. * * @param string $url url of the page */ public function visit($url) { $this->wdSession->open($url); } /** * Returns current URL address. * * @return string */ public function getCurrentUrl() { return $this->wdSession->url(); } /** * Reloads current page. */ public function reload() { $this->wdSession->refresh(); } /** * Moves browser forward 1 page. */ public function forward() { $this->wdSession->forward(); } /** * Moves browser backward 1 page. */ public function back() { $this->wdSession->back(); } /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) */ public function switchToWindow($name = null) { $this->wdSession->focusWindow($name ? $name : ''); } /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) */ public function switchToIFrame($name = null) { $this->wdSession->frame(array('id' => $name)); } /** * Sets HTTP Basic authentication parameters * * @param string|false $user user name or false to disable authentication * @param string $password password */ public function setBasicAuth($user, $password) { throw new UnsupportedDriverActionException('Basic Auth is not supported by %s', $this); } /** * Sets specific request header on client. * * @param string $name * @param string $value */ public function setRequestHeader($name, $value) { throw new UnsupportedDriverActionException('Request header is not supported by %s', $this); } /** * Returns last response headers. * * @return array */ public function getResponseHeaders() { throw new UnsupportedDriverActionException('Response header is not supported by %s', $this); } /** * Sets cookie. * * @param string $name * @param string $value */ public function setCookie($name, $value = null) { if (null === $value) { $this->wdSession->deleteCookie($name); return; } $cookieArray = array( 'name' => $name, 'value' => (string) $value, 'secure' => false, // thanks, chibimagic! ); $this->wdSession->setCookie($cookieArray); } /** * Returns cookie by name. * * @param string $name * * @return string|null */ public function getCookie($name) { $cookies = $this->wdSession->getAllCookies(); foreach ($cookies as $cookie) { if ($cookie['name'] === $name) { return urldecode($cookie['value']); } } } /** * Returns last response status code. * * @return integer */ public function getStatusCode() { throw new UnsupportedDriverActionException('Status code is not supported by %s', $this); } /** * Returns last response content. * * @return string */ public function getContent() { return $this->wdSession->source(); } /** * Capture a screenshot of the current window. * * @return string screenshot of MIME type image/* depending * on driver (e.g., image/png, image/jpeg) */ public function getScreenshot() { return base64_decode($this->wdSession->screenshot()); } /** * Finds elements with specified XPath query. * * @param string $xpath * * @return array array of Behat\Mink\Element\NodeElement */ public function find($xpath) { $nodes = $this->wdSession->elements('xpath', $xpath); $elements = array(); foreach ($nodes as $i => $node) { $elements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i+1), $this->session); } return $elements; } /** * Returns element's tag name by it's XPath query. * * @param string $xpath * * @return string */ public function getTagName($xpath) { return $this->wdSession->element('xpath', $xpath)->name(); } /** * Returns element's text by it's XPath query. * * @param string $xpath * * @return string */ public function getText($xpath) { $node = $this->wdSession->element('xpath', $xpath); $text = $node->text(); $text = (string) str_replace(array("\r", "\r\n", "\n"), ' ', $text); return $text; } /** * Returns element's html by it's XPath query. * * @param string $xpath * * @return string */ public function getHtml($xpath) { return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerHTML;'); } /** * Returns element's attribute by it's XPath query. * * @param string $xpath * * @return mixed */ public function getAttribute($xpath, $name) { $attribute = $this->wdSession->element('xpath', $xpath)->attribute($name); if ('' !== $attribute) { return $attribute; } } /** * Returns element's value by it's XPath query. * * @param string $xpath * * @return mixed */ public function getValue($xpath) { $script = <<= 0) { value = "string:" + node.options.item(idx).value; } else { value = null; } } } else { attributeValue = node.getAttribute('value'); if (attributeValue != null) { value = "string:" + attributeValue; } else if (node.value) { value = "string:" + node.value; } else { return null; } } return value; JS; $value = $this->executeJsOnXpath($xpath, $script); if ($value) { if (preg_match('/^string:(.*)$/ms', $value, $vars)) { return $vars[1]; } if (preg_match('/^boolean:(.*)$/', $value, $vars)) { return 'true' === strtolower($vars[1]); } if (preg_match('/^array:(.*)$/', $value, $vars)) { if ('' === trim($vars[1])) { return array(); } return explode(',', $vars[1]); } } } /** * Sets element's value by it's XPath query. * * @param string $xpath * @param string $value */ public function setValue($xpath, $value) { $element = $this->wdSession->element('xpath', $xpath); $elementname = strtolower($element->name()); if ( $elementname == 'textarea' || ($elementname == 'input' && strtolower($element->attribute('type')) != 'file') ) { $element->clear(); } $element->value(array('value' => array($value))); } /** * Checks checkbox by it's XPath query. * * @param string $xpath */ public function check($xpath) { $this->executeJsOnXpath($xpath, '{{ELEMENT}}.checked = true'); } /** * Unchecks checkbox by it's XPath query. * * @param string $xpath */ public function uncheck($xpath) { $this->executeJsOnXpath($xpath, '{{ELEMENT}}.checked = false'); } /** * Checks whether checkbox checked located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isChecked($xpath) { return $this->wdSession->element('xpath', $xpath)->selected(); } /** * Selects option from select field located by it's XPath query. * * @param string $xpath * @param string $value * @param Boolean $multiple */ public function selectOption($xpath, $value, $multiple = false) { $valueEscaped = str_replace('"', '\"', $value); $multipleJS = $multiple ? 'true' : 'false'; $script = <<executeJsOnXpath($xpath, $script); } /** * Clicks button or link located by it's XPath query. * * @param string $xpath */ public function click($xpath) { $this->wdSession->element('xpath', $xpath)->click(''); } /** * Double-clicks button or link located by it's XPath query. * * @param string $xpath */ public function doubleClick($xpath) { $script = 'Syn.dblclick({{ELEMENT}})'; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Right-clicks button or link located by it's XPath query. * * @param string $xpath */ public function rightClick($xpath) { $script = 'Syn.rightClick({{ELEMENT}})'; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Attaches file path to file field located by it's XPath query. * * @param string $xpath * @param string $path */ public function attachFile($xpath, $path) { $this->wdSession->element('xpath', $xpath)->value(array('value'=>str_split($path))); } /** * Checks whether element visible located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isVisible($xpath) { return $this->wdSession->element('xpath', $xpath)->displayed(); } /** * Simulates a mouse over on the element. * * @param string $xpath */ public function mouseOver($xpath) { $script = 'Syn.trigger("mouseover", {}, {{ELEMENT}})'; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Brings focus to element. * * @param string $xpath */ public function focus($xpath) { $script = 'Syn.trigger("focus", {}, {{ELEMENT}})'; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Removes focus from element. * * @param string $xpath */ public function blur($xpath) { $script = 'Syn.trigger("blur", {}, {{ELEMENT}})'; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Presses specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyPress($xpath, $char, $modifier = null) { $options = self::charToOptions('keypress', $char, $modifier); $script = "Syn.trigger('keypress', $options, {{ELEMENT}})"; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Pressed down specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyDown($xpath, $char, $modifier = null) { $options = self::charToOptions('keydown', $char, $modifier); $script = "Syn.trigger('keydown', $options, {{ELEMENT}})"; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Pressed up specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyUp($xpath, $char, $modifier = null) { $options = self::charToOptions('keyup', $char, $modifier); $script = "Syn.trigger('keyup', $options, {{ELEMENT}})"; $this->withSyn()->executeJsOnXpath($xpath, $script); } /** * Drag one element onto another. * * @param string $sourceXpath * @param string $destinationXpath */ public function dragTo($sourceXpath, $destinationXpath) { $source = $this->wdSession->element('xpath', $sourceXpath); $destination = $this->wdSession->element('xpath', $destinationXpath); $this->wdSession->moveto(array( 'element' => $source->getID() )); $script = <<withSyn()->executeJsOnXpath($sourceXpath, $script); $this->wdSession->buttondown(); $this->wdSession->moveto(array( 'element' => $destination->getID() )); $this->wdSession->buttonup(); $script = <<withSyn()->executeJsOnXpath($destinationXpath, $script); } /** * Executes JS script. * * @param string $script */ public function executeScript($script) { $this->wdSession->execute(array('script' => $script, 'args' => array())); } /** * Evaluates JS script. * * @param string $script * * @return mixed script return value */ public function evaluateScript($script) { return $this->wdSession->execute(array('script' => $script, 'args' => array())); } /** * Waits some time or until JS condition turns true. * * @param integer $time time in milliseconds * @param string $condition JS condition */ public function wait($time, $condition) { $script = "return $condition;"; $start = 1000 * microtime(true); $end = $start + $time; while (1000 * microtime(true) < $end && !$this->wdSession->execute(array('script' => $script, 'args' => array()))) { sleep(0.1); } } /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) */ public function resizeWindow($width, $height, $name = null) { return $this->wdSession->window($name ? $name : '')->postSize(array('width' => $width, 'height' => $height)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * The connection to the node TCP server. * * @author Pascal Cremer */ class Connection { /** * @var string */ private $host = null; /** * @var integer */ private $port = null; /** * Initializes connection instance. * * @param string $host zombie.js server host * @param integer $port zombie.js server port */ public function __construct($host = '127.0.0.1', $port = 8124) { $this->host = $host; $this->port = intval($port); } /** * Returns connection host. * * @return string */ public function getHost() { return $this->host; } /** * Returns connection port. * * @return string */ public function getPort() { return $this->port; } /** * Sends a payload string of Javascript code to the Zombie Node.js server. * * @param string $js String of Javascript code * * @return string */ public function socketSend($js) { $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (false === @socket_connect($socket, $this->host, $this->port)) { $errno = socket_last_error(); throw new \RuntimeException( sprintf("Could not establish connection: %s (%s)", socket_strerror($errno), $errno) ); } socket_write($socket, $js, strlen($js)); socket_shutdown($socket, 1); $out = ''; while($o = socket_read($socket, 2048)) { $out .= $o; } socket_close($socket); return $out; } } socketSend($str); break; case 'json': $result = json_decode($conn->socketSend("stream.end(JSON.stringify({$str}))")); break; default: break; } return $result; } protected function getServerScript() { $js = <<<'JS' var net = require('net') , zombie = require('%modules_path%zombie') , browser = null , pointers = [] , buffer = "" , host = '%host%' , port = %port%; var versionCompare = function(v1, v2, op) { var normalize = function(versionString) { return versionString .split(".") .map(function(digit) { return parseInt(digit, 10) }) .reduce(function(previousValue, currentValue, index, arr){ return previousValue + currentValue*Math.pow(10000, arr.length-index); }, 0); }; return eval(normalize(v1) + " " + op + " " + normalize(v2)); } var zombieVersion = require('%modules_path%zombie/package').version; if (false == versionCompare("1.4.1", zombieVersion, ">=")) { throw new Error("Your zombie.js version is not compatible with this driver. Please use a version <= 1.4.1"); } net.createServer(function (stream) { stream.setEncoding('utf8'); stream.allowHalfOpen = true; stream.on('data', function (data) { buffer += data; }); stream.on('end', function () { if (browser == null) { browser = new zombie.Browser(); // Clean up old pointers pointers = []; } eval(buffer); buffer = ""; }); }).listen(port, host, function() { console.log('server started on ' + host + ':' + port); }); JS; return $js; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Behat\Mink\Driver\NodeJS; use Behat\Mink\Driver\NodeJS\Connection; use Symfony\Component\Process\ProcessBuilder; use Symfony\Component\Process\Process; /** * Abstract base class to start and connect to a NodeJS server process. * * @author Pascal Cremer */ abstract class Server { /** * @var string */ protected $host; /** * @var int */ protected $port; /** * @var string */ protected $nodeBin; /** * @var string */ protected $serverPath; /** * @var int */ protected $threshold; /** * @var string The full path to the NodeJS modules directory. */ protected $nodeModulesPath; /** * @var Symfony\Component\Process\Process */ protected $process; /** * @var Behat\Mink\Driver\NodeJS\Connection */ protected $connection; /** * Constructor * * @param string $host The server host * @param int $port The server port * @param string $nodeBin Path to NodeJS binary * @param string $serverPath Path to server script * @param int $threshold Threshold value in micro seconds * @param string $nodeModulesPath Path to node_modules directory */ public function __construct( $host = '127.0.0.1', $port = 8124, $nodeBin = null, $serverPath = null, $threshold = 2000000, $nodeModulesPath = '' ) { if (null === $nodeBin) { $nodeBin = 'node'; } $this->host = $host; $this->port = intval($port); $this->nodeBin = $nodeBin; $this->nodeModulesPath = $nodeModulesPath; if (null === $serverPath) { $serverPath = $this->createTemporaryServer(); } $this->serverPath = $serverPath; $this->threshold = intval($threshold); $this->process = null; $this->connection = null; } /** * Destructor * * Make sure that current process is stopped */ public function __destruct() { $this->stop(); } /** * Setter host * * @param string $host The server host */ public function setHost($host) { $this->host = $host; } /** * Getter host * * @return string The server host */ public function getHost() { return $this->host; } /** * Setter port * * @param int $port The server port */ public function setPort($port) { $this->port = intval($port); } /** * Getter port * * @return int The server port */ public function getPort() { return $this->port; } /** * Setter NodeJS binary path * * @param string $nodeBin Path to NodeJS binary */ public function setNodeBin($nodeBin) { $this->nodeBin = $nodeBin; } /** * Getter NodeJS binary path * * @return string Path to NodeJS binary */ public function getNodeBin() { return $this->nodeBin; } /** * Setter NodeJS modules path * * @param string $nodeBin Path to NodeJS modules. */ public function setNodeModulesPath($nodeModulesPath) { if (!is_dir($nodeModulesPath) || !preg_match('/\/$/', $nodeModulesPath)) { throw new \InvalidArgumentException(sprintf( "Node modules path '%s' is not a directory and/or does not end with a trailing '/'", $nodeModulesPath )); } $this->nodeModulesPath = $nodeModulesPath; $this->serverPath = $this->createTemporaryServer(); } /** * Getter NodeJS modules path. * * @return string Path to NodeJS binary. */ public function getNodeModulesPath() { return $this->nodeModulesPath; } /** * Setter server script path * * @param string $serverPath Path to server script */ public function setServerPath($serverPath) { $this->serverPath = $serverPath; } /** * Getter server script path * * @return string Path to server script */ public function getServerPath() { return $this->serverPath; } /** * Setter theshold value * * @param int $theshold Threshold value in micro seconds */ public function setThreshold($threshold) { $this->threshold = intval($threshold); } /** * Getter threshold value * * @return int Threshold value in micro seconds */ public function getThreshold() { return $this->threshold; } /** * Getter process object * * @return Symfony\Component\Process\Process The process object */ public function getProcess() { return $this->process; } /** * Getter connection object * * @return Behat\Mink\Driver\NodeJS\Connection */ public function getConnection() { return $this->connection; } /** * Starts the server process * * @param Symfony\Component\Process\Process $process A process object * * @throws \RuntimeException */ public function start(Process $process = null) { // Check if the server script exists at given path if (false === $this->serverPath || false === is_file($this->serverPath)) { throw new \RuntimeException(sprintf( "Could not find server script at path '%s'", $this->serverPath )); } // Create process object if neccessary if (null === $process) { $processBuilder = new ProcessBuilder(array( $this->nodeBin, $this->serverPath, )); $process = $processBuilder->getProcess(); } $this->process = $process; // Start server process $this->process->start(); $this->connection = null; // Wait for the server to start up $time = 0; $successString = sprintf("server started on %s:%s", $this->host, $this->port); while ($this->process->isRunning() && $time < $this->threshold) { if ($successString == trim($this->process->getOutput())) { $this->connection = new Connection($this->host, $this->port); break; } usleep(1000); $time += 1000; } // Make sure the server is ready or throw an exception otherwise $this->checkAvailability(); } /** * Stops the server process * @link https://github.com/symfony/Process */ public function stop() { if (null === $this->process) { return; } if (!$this->isRunning()) { return; } if (null !== $this->getConnection()) { // Force a 'clean' exit // See: http://stackoverflow.com/a/5266208/187954 $this->doEvalJS($this->getConnection(), 'process.exit(0);'); $this->process->stop(); $this->process = null; } } /** * Restarts the server process * * @param Symfony\Component\Process\Process $process A process object */ public function restart(Process $process = null) { $this->stop(); $this->start($process); } /** * Checks if the server process is running * * @link https://github.com/symfony/Process */ public function isRunning() { if (null === $this->process) { return false; } return $this->process->isRunning(); } /** * Checks the availabilty of the server triggers the evaluation * of a string of JavaScript code by {{Behat\Mink\Driver\NodeJS\Server::doEvalJS()}} * * @param string $str String of JavaScript code * @param string $returnType Whether it should be eval'ed as * JavaScript or wrapped in a JSON response * * @return string The eval'ed response */ public function evalJS($str, $returnType = 'js') { $this->checkAvailability(); return $this->doEvalJS($this->connection, $str, $returnType); } /** * Inherited classes will implement this method to prepare a string of * JavaScript code for evaluation by the server and sending it over * the server connection socket * * @param Behat\Mink\Driver\NodeJS\Connection $conn The server connection * @param string $str String of JavaScript code * @param string $returnType The return type * * @return string The eval'ed response */ abstract protected function doEvalJS(Connection $conn, $str, $returnType = 'js'); /** * Checks whether server connection and server process are still available * and running * * @throws \RuntimeException */ protected function checkAvailability() { if (null === $this->connection) { if (null === $this->process) { throw new \RuntimeException( "No connection available. Did you start the server?" ); } if ($this->process->isRunning()) { $this->stop(); throw new \RuntimeException(sprintf( "Server did not respond in time: (%s) [Stopped]", $this->process->getExitCode() )); } } if (!$this->process->isRunning()) { throw new \RuntimeException(sprintf( "Server process has been terminated: (%s) [%s]", $this->process->getExitCode(), $this->process->getErrorOutput() )); } } /** * Creates a temporary server script * * @return string Path to the temporary server script */ protected function createTemporaryServer() { $serverScript = strtr($this->getServerScript(), array( '%host%' => $this->host, '%port%' => $this->port, '%modules_path%' => $this->nodeModulesPath, )); $serverPath = tempnam(sys_get_temp_dir(), 'mink_nodejs_server'); file_put_contents($serverPath, $serverScript); return $serverPath; } /** * Inherited classes will implement this method to provide the JavaScript * code which powers the server script * * @return string The server's JavaScript code */ abstract protected function getServerScript(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Zombie (JS) driver. * * @author Pascal Cremer */ class ZombieDriver implements DriverInterface { private $started = false; private $nativeRefs = array(); private $server = null; /** * Constructor. * * @param mixed $v,... Either the connection parameters for creating * the server * string $host - The server's host * int $port - The port to connect to * or a valid ZombieServer object instance */ public function __construct() { $numArgs = func_num_args(); if (0 === $numArgs) { throw new \InvalidArgumentException( "You must either provide connection parameters or a ZombieServer object" ); } $argList = func_get_args(); $first = array_shift($argList); if ($first instanceof ZombieServer) { $this->server = $first; return; } else if (is_object($first)) { throw new \InvalidArgumentException( "Invalid first argument" ); } $params = array(); $params['host'] = (string)$first; $second = array_shift($argList); if (null !== $second) { $params['port'] = intval($second); } $params = array_replace(array( 'host' => '127.0.0.1', 'port' => 8124, ), $params); $this->server = new ZombieServer($params['host'], $params['port']); } /** * Returns Zombie.js server. * * @return ZombieServer */ public function getServer() { return $this->server; } /** * Sets driver's current session. * * @param Session $session */ public function setSession(Session $session) { $this->session = $session; } /** * Starts driver. */ public function start() { if ($this->server) { $this->server->start(); } $this->started = true; } /** * Checks whether driver is started. * * @return Boolean */ public function isStarted() { return $this->started; } /** * Stops driver. */ public function stop() { if ($this->server) { $this->server->stop(); } $this->started = false; } /** * Resets driver. */ public function reset() { // Cleanup cached references $this->nativeRefs = array(); $js = <<server->evalJS($js); } /** * Visit specified URL. * * @param string $url url of the page * * @throws DriverException */ public function visit($url) { // Cleanup cached references $this->nativeRefs = array(); $js = <<server->evalJS($js); } /** * Returns current URL address. * * @return string */ public function getCurrentUrl() { return $this->server->evalJS('browser.location.toString()', 'json'); } /** * Reloads current page. */ public function reload() { $this->visit($this->getCurrentUrl()); } /** * Moves browser forward 1 page. */ public function forward() { $this->server->evalJS("browser.window.history.forward(); browser.wait(function() { stream.end(); })"); } /** * Moves browser backward 1 page. */ public function back() { $this->server->evalJS("browser.window.history.back(); browser.wait(function() { stream.end(); })"); } /** * Switches to specific browser window. * * @param string $name window name (null for switching back to main window) * * @throws UnsupportedDriverActionException */ public function switchToWindow($name = null) { throw new UnsupportedDriverActionException('Window management is not supported by %s', $this); } /** * Switches to specific iFrame. * * @param string $name iframe name (null for switching back) * * @throws UnsupportedDriverActionException */ public function switchToIFrame($name = null) { throw new UnsupportedDriverActionException('iFrame management is not supported by %s', $this); } /** * Sets HTTP Basic authentication parameters * * @param string|Boolean $user user name or false to disable authentication * @param string $password password */ public function setBasicAuth($user, $password) { $this->server->evalJS("browser.credentials = { credentials: { schema: 'basic', username: '{$user}', password: '{$password}'}};stream.end();"); } /** * Sets specific request header on client. * * @param string $name * @param string $value * * @throws UnsupportedDriverActionException */ public function setRequestHeader($name, $value) { if (strtolower($name) === 'user-agent') { $this->server->evalJS("browser.userAgent = '$value';stream.end();"); return; } throw new UnsupportedDriverActionException('Request header "' . $name . '" manipulation is not supported by %s', $this); } /** * Returns last response headers. * * @return array */ public function getResponseHeaders() { return (array)$this->server->evalJS('browser.lastResponse.headers', 'json'); } /** * Sets cookie. * * @param string $name * @param string $value */ public function setCookie($name, $value = null) { $js = "browser.cookies(browser.window.location.hostname, '/')"; $js .= (null === $value) ? ".remove('{$name}')" : ".set('{$name}', '{$value}')"; $this->server->evalJS($js, 'json'); } /** * Returns cookie by name. * * @param string $name * * @return string|null */ public function getCookie($name) { $js = <<server->evalJS($js); if (empty($res)) { return null; } return $res; } /** * Returns last response status code. * * @return integer */ public function getStatusCode() { return (int)$this->server->evalJS('browser.statusCode', 'json'); } /** * Returns last response content. * * @return string */ public function getContent() { return html_entity_decode($this->server->evalJS('browser.html()', 'json')); } /** * Capture a screenshot of the current window. * * @throws UnsupportedDriverActionException */ public function getScreenshot() { throw new UnsupportedDriverActionException('Screenshots are not supported by %s', $this); } /** * Finds elements with specified XPath query. * * @param string $xpath * * @return array array of NodeElements */ public function find($xpath) { $xpathEncoded = json_encode($xpath); $js = <<server->evalJS($js)); $elements = array(); foreach ($refs as $i => $ref) { $subXpath = sprintf('(%s)[%d]', $xpath, $i + 1); $this->nativeRefs[md5($subXpath)] = $ref; $elements[] = new NodeElement($subXpath, $this->session); // first node ref also matches the original xpath if (0 === $i) { $this->nativeRefs[md5($xpath)] = $ref; } } return $elements; } /** * Returns element's tag name by it's XPath query. * * @param string $xpath * * @return string */ public function getTagName($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return null; } return strtolower($this->server->evalJS("{$ref}.tagName", 'json')); } /** * Returns element's text by it's XPath query. * * @param string $xpath * * @return string */ public function getText($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return null; } return trim($this->server->evalJS("{$ref}.textContent.replace(/\s+/g, ' ')", 'json')); } /** * Returns element's html by it's XPath query. * * @param string $xpath * * @return string */ public function getHtml($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return null; } return $this->server->evalJS("{$ref}.innerHTML", 'json'); } /** * Returns element's attribute by it's XPath query. * * @param string $xpath * @param string $name * * @return mixed */ public function getAttribute($xpath, $name) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return null; } $out = $this->server->evalJS("{$ref}.getAttribute('{$name}')", 'json'); return empty($out) ? null : $out; } /** * Returns element's value by it's XPath query. * * @param string $xpath * * @return mixed */ public function getValue($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return null; } $js = <<= 0) { value = node.options.item(idx).value; } else { value = null; } } } else { value = node.getAttribute('value'); } stream.end(JSON.stringify(value)); JS; return json_decode($this->server->evalJS($js)); } /** * Sets element's value by it's XPath query. * * @param string $xpath * @param string $value */ public function setValue($xpath, $value) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $value = json_encode($value); $js = <<server->evalJS($js); } /** * Checks checkbox by it's XPath query. * * @param string $xpath */ public function check($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $this->server->evalJS("browser.check({$ref});stream.end();"); } /** * Unchecks checkbox by it's XPath query. * * @param string $xpath */ public function uncheck($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $this->server->evalJS("browser.uncheck({$ref});stream.end();"); } /** * Checks whether checkbox checked located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isChecked($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return false; } return (boolean)$this->server->evalJS("{$ref}.checked", 'json'); } /** * Selects option from select field located by it's XPath query. * * @param string $xpath * @param string $value * @param Boolean $multiple */ public function selectOption($xpath, $value, $multiple = false) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $value = json_encode($value); $js = <<server->evalJS($js); } /** * Clicks button or link located by it's XPath query. * * @param string $xpath * * @throws DriverException */ public function click($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $js = <<server->evalJS($js); if (!empty($out)) { throw new DriverException(sprintf('Error while clicking button: [%s]', $out)); } } /** * Double-clicks button or link located by it's XPath query. * * @param string $xpath */ public function doubleClick($xpath) { $this->triggerBrowserEvent("dblclick", $xpath); } /** * Right-clicks button or link located by it's XPath query. * * @param string $xpath */ public function rightClick($xpath) { $this->triggerBrowserEvent("contextmenu", $xpath); } /** * Attaches file path to file field located by it's XPath query. * * @param string $xpath * @param string $path */ public function attachFile($xpath, $path) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $path = json_encode($path); $this->server->evalJS("browser.attach({$ref}, {$path});stream.end();"); } /** * Checks whether element visible located by it's XPath query. * * @param string $xpath * * @return Boolean */ public function isVisible($xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } // This is kind of a workaround, because the current version of // Zombie.js does not fully support the DOMElement's style attribute $hiddenXpath = json_encode("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none')]"); return (0 == (int)$this->server->evalJS("browser.xpath({$hiddenXpath}, {$ref}).value.length", 'json')); } /** * Simulates a mouse over on the element. * * @param string $xpath */ public function mouseOver($xpath) { $this->triggerBrowserEvent("mouseover", $xpath); } /** * Brings focus to element. * * @param string $xpath */ public function focus($xpath) { $this->triggerBrowserEvent("focus", $xpath); } /** * Removes focus from element. * * @param string $xpath */ public function blur($xpath) { $this->triggerBrowserEvent("blur", $xpath); } /** * Presses specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyPress($xpath, $char, $modifier = null) { $this->triggerKeyEvent("keypress", $xpath, $char, $modifier); } /** * Pressed down specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyDown($xpath, $char, $modifier = null) { $this->triggerKeyEvent("keydown", $xpath, $char, $modifier); } /** * Pressed up specific keyboard key. * * @param string $xpath * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ public function keyUp($xpath, $char, $modifier = null) { $this->triggerKeyEvent("keyup", $xpath, $char, $modifier); } /** * Drag one element onto another. * * @param string $sourceXpath * @param string $destinationXpath */ public function dragTo($sourceXpath, $destinationXpath) { throw new UnsupportedDriverActionException('Dragging is not supported by %s', $this); } /** * Executes JS script. * * @param string $script */ public function executeScript($script) { $script = json_encode($script); $this->server->evalJS("browser.evaluate({$script})"); } /** * Evaluates JS script. * * @param string $script * * @return mixed */ public function evaluateScript($script) { $script = json_encode($script); return $this->server->evalJS("browser.evaluate({$script})", 'json'); } /** * Waits some time or until JS condition turns true. * * @param integer $time time in milliseconds * @param string $condition JS condition */ public function wait($time, $condition) { $js = <<server->evalJS($js); } /** * Set the dimensions of the window. * * @param integer $width set the window width, measured in pixels * @param integer $height set the window height, measured in pixels * @param string $name window name (null for the main window) * * @throws UnsupportedDriverActionException */ public function resizeWindow($width, $height, $name = null) { throw new UnsupportedDriverActionException('Window resizing is not supported by %s', $this); } /** * Triggers (fires) a Zombie.js browser event. * * @param string $event The name of the event * @param string $xpath The xpath of the element to trigger this event * * @throws DriverException */ protected function triggerBrowserEvent($event, $xpath) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $js = <<server->evalJS($js); if (!empty($out)) { throw new DriverException(sprintf("Error while processing event '%s'", $event)); } } /** * Triggers a keyboard event * * @param string $name The event name * @param string $xpath The xpath of the element to trigger this event on * @param mixed $char could be either char ('b') or char-code (98) * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') */ protected function triggerKeyEvent($name, $xpath, $char, $modifier) { if (!$ref = $this->getNativeRefForXPath($xpath)) { return; } $char = is_numeric($char) ? $char : ord($char); $isCtrlKeyArg = ($modifier == 'ctrl') ? "true" : "false"; $isAltKeyArg = ($modifier == 'alt') ? "true" : "false"; $isShiftKeyArg = ($modifier == 'shift') ? "true" : "false"; $isMetaKeyArg = ($modifier == 'meta') ? "true" : "false"; $js = <<server->evalJS($js); } /** * Tries to fetch a native reference to a node that might have been cached * by the server. If it can't be found, the method performs a search. * * Searching the native reference by the MD5 hash of its xpath feels kinda * hackish, but it'll boost performance and prevents a lot of boilerplate * Javascript code. * * @param string $xpath * * @return string|null */ protected function getNativeRefForXPath($xpath) { $hash = md5($xpath); if (!isset($this->nativeRefs[$hash])) { $res = $this->find($xpath); if (1 > count($res)) { return null; } } return sprintf('pointers[%s]', $this->nativeRefs[$hash]); } } Copyright (c) 2011-2013 Konstantin Kudryashov 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. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract Accessor. * * @author Konstantin Kudryashov */ abstract class AbstractAccessor { /** * Sahi Driver instance. * * @var Driver */ protected $con; /** * Initialize Accessor. * * @param Connection $con Sahi Connection */ public function __construct(Connection $con) { $this->con = $con; } /** * Set Sahi Connection. * * @param Connection $con Sahi Connection */ public function setConnection(Connection $con) { $this->con = $con; } /** * Return Accessor active connection instance. * * @return Connection */ public function getConnection() { return $this->con; } /** * Perform check on radio */ public function check() { $this->con->executeStep(sprintf('_sahi._check(%s)', $this->getAccessor())); } /** * Return true if checkbox/radio checked. * * @return boolean */ public function isChecked() { return "true" === $this->con->evaluateJavascript(sprintf('%s.checked', $this->getAccessor())); } /** * Perform uncheck on radio */ public function uncheck() { $this->con->executeStep(sprintf('_sahi._uncheck(%s)', $this->getAccessor())); } /** * Return selected text from selectbox. * * @return string */ public function getSelectedText() { return $this->con->evaluateJavascript(sprintf('_sahi._getSelectedText(%s)', $this->getAccessor())); } /** * Choose option in select box. * * @param string $val option value */ public function choose($val, $isMultiple = null) { $arguments = array('"' . str_replace('"', '\"', $val) . '"'); if (null !== $isMultiple) { $arguments[] = (bool) $isMultiple ? 'true' : 'false'; } $this->con->executeStep( sprintf('_sahi._setSelected(%s, %s)', $this->getAccessor(), implode(', ', $arguments)) ); } /** * Emulate setting filepath in a file input. * * @param string $path file path */ public function setFile($path) { $this->con->executeStep( sprintf('_sahi._setFile(%s, "%s")', $this->getAccessor(), str_replace('"', '\"', $path)) ); } /** * Perform click on element. */ public function click() { $this->con->executeStep(sprintf('_sahi._click(%s)', $this->getAccessor())); } /** * Perform right-click on element. */ public function rightClick() { $this->con->executeStep(sprintf('_sahi._rightClick(%s)', $this->getAccessor())); } /** * Perform double-click on element. */ public function doubleClick() { $this->con->executeStep(sprintf('_sahi._doubleClick(%s)', $this->getAccessor())); } /** * Perform mouse-over on element. */ public function mouseOver() { $this->con->executeStep(sprintf('_sahi._mouseOver(%s)', $this->getAccessor())); } /** * Bring focus to element. */ public function focus() { $this->con->executeStep(sprintf('_sahi._focus(%s)', $this->getAccessor())); } /** * Remove focus from element. */ public function removeFocus() { $this->con->executeStep(sprintf('_sahi._removeFocus(%s)', $this->getAccessor())); } /** * Blur element. */ public function blur() { $this->con->executeStep(sprintf('_sahi._blur(%s)', $this->getAccessor())); } /** * Drag'n'Drop current element onto another. * * @param AbstractAccessor $to destination element */ public function dragDrop(AbstractAccessor $to) { $this->con->executeStep(sprintf('_sahi._dragDrop(%s, %s)', $this->getAccessor(), $to->getAccessor())); } /** * Drag'n'Drop current element into X,Y. * * @param integer $x X * @param integer $y Y * @param boolean $relative relativity of position */ public function dragDropXY($x, $y, $relative = null) { $arguments = array($x, $y); if (null !== $relative) { $arguments[] = (bool) $relative ? 'true' : 'false'; } $this->con->executeStep( sprintf('_sahi._dragDropXY(%s, %s)', $this->getAccessor(), implode(', ', $arguments)) ); } /** * Simulate event. * * @param string $event notify event on object */ public function simulateEvent($event) { $this->con->executeStep(sprintf('_sahi._simulateEvent(%s, %s)', $this->getAccessor(), $event)); } /** * Simulate keypress event. * * @param string $charInfo a char (eg. ‘b’) OR charCode (eg. 98) OR array(13,13) for pressing ENTER * @param string $combo CTRL|ALT|SHIFT|META */ public function keyPress($charInfo, $combo = null) { $this->con->executeStep( sprintf('_sahi._keyPress(%s, %s)', $this->getAccessor(), $this->getKeyArgumentsString($charInfo, $combo)) ); } /** * Simulate keypress event. * * @param string $charInfo a char (eg. ‘b’) OR charCode (eg. 98) OR array(13,13) for pressing ENTER * @param string $combo CTRL|ALT|SHIFT|META */ public function keyDown($charInfo, $combo = null) { $this->con->executeStep( sprintf('_sahi._keyDown(%s, %s)', $this->getAccessor(), $this->getKeyArgumentsString($charInfo, $combo)) ); } /** * Simulate keypress event. * * @param string $charInfo a char (eg. ‘b’) OR charCode (eg. 98) OR array(13,13) for pressing ENTER * @param string $combo CTRL|ALT|SHIFT|META */ public function keyUp($charInfo, $combo = null) { $this->con->executeStep( sprintf('_sahi._keyUp(%s, %s)', $this->getAccessor(), $this->getKeyArgumentsString($charInfo, $combo)) ); } /** * Set text value. * * @param string $val value */ public function setValue($val) { $this->con->executeStep( sprintf('_sahi._setValue(%s, "%s")', $this->getAccessor(), str_replace('"', '\"', $val)) ); } /** * Return current text value. * * @return string */ public function getValue() { return $this->con->evaluateJavascript(sprintf('%s.value', $this->getAccessor())); } /** * Return node name. * * @return string */ public function getName() { return $this->con->evaluateJavascript(sprintf('%s.nodeName', $this->getAccessor())); } /** * Return attribute value. * * @param string $attr attribute name * * @return string */ public function getAttr($attr) { return $this->con->evaluateJavascript(sprintf('%s.getAttribute("%s")', $this->getAccessor(), $attr)); } /** * Return inner text of element. * * @return string */ public function getText() { return $this->con->evaluateJavascript(sprintf('_sahi._getText(%s)', $this->getAccessor())); } /** * Return inner text of element. * * @return string */ public function getHTML() { return $this->con->evaluateJavascript(sprintf('%s.innerHTML', $this->getAccessor())); } /** * Highlight element. */ public function highlight() { $this->con->executeStep(sprintf('_sahi._highlight(%s)', $this->getAccessor())); } /** * Return true if the element is visible on the user interface. * * @return boolean */ public function isVisible() { return 'true' === $this->con->evaluateJavascript(sprintf('_sahi._isVisible(%s)', $this->getAccessor())); } /** * Return true if the element is visible on the user interface. * * @return boolean */ public function isExists() { return 'true' === $this->con->evaluateJavascript(sprintf('_sahi._exists(%s)', $this->getAccessor())); } /** * Return accessor string. * * @return string */ abstract public function getAccessor(); /** * Return accessor string. * * @return string */ public function __toString() { return $this->getAccessor(); } /** * Return Key arguments string. * * @param string $charInfo a char (eg. ‘b’) OR charCode (eg. 98) OR array(13,13) for pressing ENTER * @param string $combo CTRL|ALT|SHIFT|META * * @return string */ private function getKeyArgumentsString($charInfo, $combo) { $arguments = array(); if (is_array($charInfo)) { $arguments[] = '[' . implode(',', $charInfo) . ']'; } elseif (is_string($charInfo)) { $arguments[] = '"' . $charInfo . '"'; } else { $arguments[] = $charInfo; } if (null !== $combo) { $arguments[] = '"' . $combo . '"'; } return implode(', ', $arguments); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract Element Accessor. * * @author Konstantin Kudryashov */ abstract class AbstractDomAccessor extends AbstractRelationalAccessor { /** * Element identifier * * @var string */ protected $id; /** * Initialize Accessor. * * @param string $id element identifier (if null - "0" will be used) * @param array $relations relations array array('near' => accessor, 'under' => accessor) * @param Connection $con Sahi connection */ public function __construct($id, array $relations, Connection $con) { parent::__construct($con); foreach ($relations as $relation => $accessor) { $this->$relation($accessor); } $this->id = $id; } /** * Return DOM element type. * * @return string */ abstract public function getType(); /** * {@inheritdoc} */ public function getAccessor() { return sprintf('_sahi._%s(%s)', $this->getType(), $this->getArgumentsString()); } /** * Return comma separated Sahi DOM arguments. * * @return string */ protected function getArgumentsString() { $arguments = array($this->getIdentifierArgumentString()); if ($this->hasRelations()) { $arguments[] = $this->getRelationArgumentsString(); } return implode(', ', $arguments); } /** * Convert identificator to JavaScript id instruction. * * @param mixed $id element identificator * * @return string JavaScript id instruction */ private function getIdentifierArgumentString() { if (null === $this->id) { return '0'; } if (is_float($this->id) || is_int($this->id)) { return $this->id; } return '"' . $this->id . '"'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract Relational Accessor. * * @author Konstantin Kudryashov */ abstract class AbstractRelationalAccessor extends AbstractAccessor { /** * DOM relations. * * @var array */ private $relations = array(); /** * Add _in DOM relation. * * @param AbstractAccessor $accessor accessor for relation * * @return AbstractRelationalAccessor */ public function in(AbstractAccessor $accessor) { $this->relations[] = sprintf('_sahi._in(%s)', $accessor->getAccessor()); return $this; } /** * Add _near DOM relation. * * @param AbstractAccessor $accessor accessor for relation * * @return AbstractRelationalAccessor */ public function near(AbstractAccessor $accessor) { $this->relations[] = sprintf('_sahi._near(%s)', $accessor->getAccessor()); return $this; } /** * Add _under DOM relation. * * @param AbstractAccessor $accessor accessor for relation * * @return AbstractRelationalAccessor */ public function under(AbstractAccessor $accessor) { $this->relations[] = sprintf('_sahi._under(%s)', $accessor->getAccessor()); return $this; } /** * Return true if accessor has relations. * * @return boolean */ public function hasRelations() { return 0 < count($this->relations); } /** * Return relations Sahi arguments. * * @return string */ protected function getRelationArgumentsString() { return implode(', ', $this->relations); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * By Class Name Accessor. * * @author Konstantin Kudryashov */ class ByClassNameAccessor extends AbstractRelationalAccessor { /** * Tag class * * @var string */ protected $class; /** * Tag name * * @var string */ protected $tag; /** * Initialize Accessor. * * @param string $class tag class name * @param string $tag tag name * @param array $relations relations * @param Connection $con Sahi connection */ public function __construct($class, $tag, array $relations, Connection $con) { parent::__construct($con); foreach ($relations as $relation => $accessor) { $this->$relation($accessor); } $this->class = $class; $this->tag = $tag; } /** * {@inheritdoc} */ public function getAccessor() { $arguments = array(); $arguments[] = '"' . $this->class . '"'; $arguments[] = '"' . $this->tag . '"'; if ($this->hasRelations()) { $arguments[] = $this->getRelationArgumentsString(); } return sprintf('_sahi._byClassName(%s)', implode(', ', $arguments)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * By Id Accessor. * * @author Konstantin Kudryashov */ class ByIdAccessor extends AbstractAccessor { /** * Element ID * * @var string */ protected $id; /** * Initialize Accessor. * * @param string $id element ID * @param Connection $con Sahi connection */ public function __construct($id, Connection $con) { parent::__construct($con); $this->id = $id; } /** * {@inheritdoc} */ public function getAccessor() { return sprintf('_sahi._byId("%s")', $this->id); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * By Text Accessor. * * @author Konstantin Kudryashov */ class ByTextAccessor extends AbstractAccessor { /** * Tag text * * @var string */ protected $text; /** * Tag name * * @var string */ protected $tag; /** * Initialize Accessor. * * @param string $text tag text * @param string $tag tag name * @param Connection $con Sahi connection */ public function __construct($text, $tag, Connection $con) { parent::__construct($con); $this->text = $text; $this->tag = $tag; } /** * {@inheritdoc} */ public function getAccessor() { return sprintf('_sahi._byText("%s", "%s")', $this->text, $this->tag); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * By XPath Accessor. * * @author Konstantin Kudryashov */ class ByXPathAccessor extends AbstractRelationalAccessor { /** * XPath expression * * @var string */ protected $xpath; /** * Initialize Accessor. * * @param string $xpath XPath expression * @param Connection $con Sahi connection */ public function __construct($xpath, array $relations, Connection $con) { parent::__construct($con); foreach ($relations as $relation => $accessor) { $this->$relation($accessor); } $this->xpath = $xpath; } /** * {@inheritdoc} */ public function getAccessor() { $arguments = array(); $arguments[] = '"' . $this->xpath . '"'; if ($this->hasRelations()) { $arguments[] = $this->getRelationArgumentsString(); } return sprintf('_sahi._byXPath(%s)', implode(', ', $arguments)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Div Element Accessor. * * @author Konstantin Kudryashov */ class DivAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'div'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * DOM Accessor. * * @author Konstantin Kudryashov */ class DomAccessor extends AbstractAccessor { /** * Element ID * * @var string */ protected $id; /** * Initialize Accessor. * * @param string $string DOM expression * @param Connection $con Sahi connection */ public function __construct($string, Connection $con) { parent::__construct($con); $this->id = $string; } /** * {@inheritdoc} */ public function getAccessor() { return sprintf('_sahi._accessor("%s")', str_replace('"', '\"', $this->id)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Button Element Accessor. * * @author Konstantin Kudryashov */ class ButtonAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'button'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Checkbox Element Accessor. * * @author Konstantin Kudryashov */ class CheckboxAccessor extends RadioAccessor { /** * {@inheritdoc} */ public function getType() { return 'checkbox'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * File Element Accessor. * * @author Konstantin Kudryashov */ class FileAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'file'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Hidden Element Accessor. * * @author Konstantin Kudryashov */ class HiddenAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'hidden'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * ImageSubmitButton Element Accessor. * * @author Konstantin Kudryashov */ class ImageSubmitButtonAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'imageSubmitButton'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Option Element Accessor. * * @author Konstantin Kudryashov */ class OptionAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'option'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Password Element Accessor. * * @author Konstantin Kudryashov */ class PasswordAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'password'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Radio Element Accessor. * * @author Konstantin Kudryashov */ class RadioAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'radio'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Reset Element Accessor. * * @author Konstantin Kudryashov */ class ResetAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'reset'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Select Element Accessor. * * @author Konstantin Kudryashov */ class SelectAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'select'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Submit Element Accessor. * * @author Konstantin Kudryashov */ class SubmitAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'submit'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Textarea Element Accessor. * * @author Konstantin Kudryashov */ class TextareaAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'textarea'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Textbox Element Accessor. * * @author Konstantin Kudryashov */ class TextboxAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'textbox'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Heading Element Accessor (h1, h2, h3, ...). * * @author Konstantin Kudryashov */ class HeadingAccessor extends AbstractDomAccessor { /** * Heading level * * @var integer */ private $level = 1; /** * Initialize Heading accessor. * * @param integer $level heading level (1 for H1, 2 for H2 etc.) * @param string $id element identifier (if null - "0" will be used) * @param array $relations relations array array('near' => accessor, 'under' => accessor) * @param Connection $con Sahi connection */ public function __construct($level, $id, array $relations, Connection $con) { parent::__construct($id, $relations, $con); if (null !== $level) { $this->level = $level; } } /** * Return heading level. * * @return integer */ public function getLevel() { return $this->level; } /** * {@inheritdoc} */ public function getType() { return 'heading' . $this->getLevel(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Image Element Accessor. * * @author Konstantin Kudryashov */ class ImageAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'image'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Label Element Accessor. * * @author Konstantin Kudryashov */ class LabelAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'label'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Link Element Accessor. * * @author Konstantin Kudryashov */ class LinkAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'link'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * ListItem Element Accessor. * * @author Konstantin Kudryashov */ class ListItemAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'listItem'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Span Element Accessor. * * @author Konstantin Kudryashov */ class SpanAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'span'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Cell Element Accessor. * * @author Konstantin Kudryashov */ class CellAccessor extends AbstractDomAccessor { private $table; private $rowText; private $colText; /** * Initialize Accessor. * * @param string|array $id simple element identifier or array of (Table, rowText, colText) * @param array $relations relations array array('near' => accessor, 'under' => accessor) * @param Connection $con Sahi connection */ public function __construct($id, array $relations, Connection $con) { if (is_array($id)) { if (!($id[0] instanceof TableAccessor) || !isset($id[0]) || !isset($id[1])) { throw new \InvalidArgumentException( 'Cell table identificator must have type: array(TableAccessor, "rowText", "colText")' ); } if (count($relations)) { throw new \InvalidArgumentException( 'Can not use relations in cell accessor, that depends on table accessor' ); } $this->table = $id[0]; $this->rowText = $id[1]; $this->colText = $id[2]; parent::__construct(null, $relations, $con); } else { parent::__construct($id, $relations, $con); } } /** * {@inheritdoc} */ public function getAccessor() { if (null !== $this->table) { $arguments = array(); $arguments[] = $this->table->getAccessor(); $arguments[] = '"' . str_replace('"', '\"', $this->rowText) . '"'; $arguments[] = '"' . str_replace('"', '\"', $this->colText) . '"'; if ($this->hasRelations()) { $arguments[] = $this->getRelationArgumentsString(); } return sprintf('_sahi._%s(%s)', $this->getType(), implode(', ', $arguments)); } return sprintf('_sahi._%s(%s)', $this->getType(), $this->getArgumentsString()); } /** * {@inheritdoc} */ public function getType() { return 'cell'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Row Element Accessor. * * @author Konstantin Kudryashov */ class RowAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'row'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Table Element Accessor. * * @author Konstantin Kudryashov */ class TableAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'table'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * TableHeader Element Accessor. * * @author Konstantin Kudryashov */ class TableHeaderAccessor extends AbstractDomAccessor { /** * {@inheritdoc} */ public function getType() { return 'tableHeader'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Sahi client. * * @author Konstantin Kudryashov */ class Client { /** * Sahi Driver instance. * * @var Driver */ protected $con; private $started = false; private $browserAutoruned = false; /** * Initialize Accessor. * * @param Connection $con Sahi Connection */ public function __construct(Connection $con = null) { if (null === $con) { $con = new Connection(); } $this->con = $con; } /** * Start Sahi browser session. * * @param string $browserName (firefox, ie, safari, chrome, opera) */ public function start($browserName = null) { if ($this->started) { throw new Exception\ConnectionException('Client is already started'); } if (!$this->con->isProxyStarted()) { throw new Exception\ConnectionException('Sahi proxy seems not running'); } // open browser if connection uses custom SID (not defaultly autogenerated) if (!$this->con->isCustomSidProvided()) { if (null === $browserName) { throw new \InvalidArgumentException('Specify browser to run in'); } $this->con->start($browserName); $limit = 600; while (!$this->con->isReady()) { usleep(100000); if (--$limit <= 0) { throw new Exception\ConnectionException( 'Connection time limit reached' ); } } $this->browserAutoruned = true; } elseif (!$this->con->isReady()) { throw new Exception\ConnectionException(sprintf( "Can not connect to Sahi session with id \"%s\".\n". "Start Sahi session in target browser by opening:\n". "http://sahi.example.com/_s_/dyn/Driver_start?sahisid=%s&startUrl=http://sahi.example.com/_s_/dyn/Driver_initialized", $this->con->getSid(), $this->con->getSid() )); } $this->started = true; } /** * Stop Sahi browser session. */ public function stop() { if (!$this->started) { throw new Exception\ConnectionException('Client is not started'); } if ($this->browserAutoruned) { $this->con->stop(); } $this->started = false; } /** * Set Sahi Connection. * * @param Connection $con Sahi Connection */ public function setConnection(Connection $con) { $this->con = $con; } /** * Return Accessor active con instance. * * @return Connection */ public function getConnection() { return $this->con; } /** * Navigates to the given URL. * * @param string $url URL * @param boolean $reload force reload */ public function navigateTo($url, $reload = null) { $arguments = array('"' . str_replace('"', '\\"', $url) . '"'); if (null !== $reload) { $arguments[] = (bool) $reload ? 'true' : 'false'; } $this->con->executeStep(sprintf('_sahi._navigateTo(%s)', implode(', ', $arguments))); } /** * Set speed of execution (in milli seconds). * * @param integer $speed speed in milli seconds */ public function setSpeed($speed) { $this->con->executeCommand('setSpeed', array('speed' => $speed)); } /** * Waits for timeInMilliseconds ms or till the condition is satisfied on the browser, * which ever is sooner. * * @param integer $time time in milliseconds * @param string $condition JS condition */ public function wait($time, $condition) { $conditionResult = false; while ($time > 0 && 'true' !== $conditionResult) { usleep(100); $time -= 100; // don't throw exceptions try { $conditionResult = $this->con->evaluateJavascript($condition); } catch (\Exception $e) { $conditionResult = false; } } } /** * Find element on page by specified class & tag. * * @param string $class tag class * @param string $tag tag name * @param array $relations tag relations (near, in, under) * * @return ByClassNameAccessor */ public function findByClassName($class, $tag, array $relations = array()) { return new Accessor\ByClassNameAccessor($class, $tag, $relations, $this->con); } /** * Find element by id. * * @param string $id element id * * @return ByIdAccessor */ public function findById($id) { return new Accessor\ByIdAccessor($id, $this->con); } /** * Find element by it's text. * * @param string $text tag text * @param string $tag tag name * * @return ByTextAccessor */ public function findByText($text, $tag) { return new Accessor\ByTextAccessor($text, $tag, $this->con); } /** * Find element by it's text. * * @param string $xpath XPath expression * @param array $relations tag relations (near, in, under) * * @return ByXPathAccessor */ public function findByXPath($xpath, array $relations = array()) { return new Accessor\ByXPathAccessor($xpath, $relations, $this->con); } /** * Find DIV element. * * @param string $id element identifier * @param string $tag tag name * @param array $relations tag relations (near, in, under) * * @return DivAccessor */ public function findDiv($id = null, array $relations = array()) { return new Accessor\DivAccessor($id, $relations, $this->con); } /** * Find element by it's JS DOM representation. * * @param string $dom DOM expression * * @return DomAccessor */ public function findDom($dom) { return new Accessor\DomAccessor($dom, $this->con); } /** * Find heading element (h1, h2, h3) * * @param integer $level heading level 1 for h1, 2 for h2, ... * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return HeadingAccessor */ public function findHeader($level = 1, $id = null, array $relations = array()) { return new Accessor\HeadingAccessor($level, $id, $relations, $this->con); } /** * Find img element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return ImageAccessor */ public function findImage($id = null, array $relations = array()) { return new Accessor\ImageAccessor($id, $relations, $this->con); } /** * Find label element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return LabelAccessor */ public function findLabel($id = null, array $relations = array()) { return new Accessor\LabelAccessor($id, $relations, $this->con); } /** * Find a element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return LinkAccessor */ public function findLink($id = null, array $relations = array()) { return new Accessor\LinkAccessor($id, $relations, $this->con); } /** * Find li element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return ListItemAccessor */ public function findListItem($id = null, array $relations = array()) { return new Accessor\ListItemAccessor($id, $relations, $this->con); } /** * Find span element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return SpanAccessor */ public function findSpan($id = null, array $relations = array()) { return new Accessor\SpanAccessor($id, $relations, $this->con); } /** * Find button element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return ButtonAccessor */ public function findButton($id = null, array $relations = array()) { return new Accessor\Form\ButtonAccessor($id, $relations, $this->con); } /** * Find checkbox element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return CheckboxAccessor */ public function findCheckbox($id = null, array $relations = array()) { return new Accessor\Form\CheckboxAccessor($id, $relations, $this->con); } /** * Find file element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return FileAccessor */ public function findFile($id = null, array $relations = array()) { return new Accessor\Form\FileAccessor($id, $relations, $this->con); } /** * Find hidden element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return HiddenAccessor */ public function findHidden($id = null, array $relations = array()) { return new Accessor\Form\HiddenAccessor($id, $relations, $this->con); } /** * Find image submit button element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return ImageSubmitButtonAccessor */ public function findImageSubmitButton($id = null, array $relations = array()) { return new Accessor\Form\ImageSubmitButtonAccessor($id, $relations, $this->con); } /** * Find select option element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return OptionAccessor */ public function findOption($id = null, array $relations = array()) { return new Accessor\Form\OptionAccessor($id, $relations, $this->con); } /** * Find password element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return PasswordAccessor */ public function findPassword($id = null, array $relations = array()) { return new Accessor\Form\PasswordAccessor($id, $relations, $this->con); } /** * Find radio button element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return RadioAccessor */ public function findRadio($id = null, array $relations = array()) { return new Accessor\Form\RadioAccessor($id, $relations, $this->con); } /** * Find reset button element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return ResetAccessor */ public function findReset($id = null, array $relations = array()) { return new Accessor\Form\ResetAccessor($id, $relations, $this->con); } /** * Find select element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return SelectAccessor */ public function findSelect($id = null, array $relations = array()) { return new Accessor\Form\SelectAccessor($id, $relations, $this->con); } /** * Find submit element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return SubmitAccessor */ public function findSubmit($id = null, array $relations = array()) { return new Accessor\Form\SubmitAccessor($id, $relations, $this->con); } /** * Find textarea element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return TextareaAccessor */ public function findTextarea($id = null, array $relations = array()) { return new Accessor\Form\TextareaAccessor($id, $relations, $this->con); } /** * Find textbox element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return TextboxAccessor */ public function findTextbox($id = null, array $relations = array()) { return new Accessor\Form\TextboxAccessor($id, $relations, $this->con); } /** * Find table cell element. * * @param string|array $id simple element identifier or array of (Table, rowText, colText) * @param array $relations tag relations (near, in, under) * * @return CellAccessor */ public function findCell($id = null, array $relations = array()) { return new Accessor\Table\CellAccessor($id, $relations, $this->con); } /** * Find table row element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return RowAccessor */ public function findRow($id = null, array $relations = array()) { return new Accessor\Table\RowAccessor($id, $relations, $this->con); } /** * Find table header element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return TableHeaderAccessor */ public function findTableHeader($id = null, array $relations = array()) { return new Accessor\Table\TableHeaderAccessor($id, $relations, $this->con); } /** * Find table element. * * @param string $id element identifier * @param array $relations tag relations (near, in, under) * * @return TableAccessor */ public function findTable($id = null, array $relations = array()) { return new Accessor\Table\TableAccessor($id, $relations, $this->con); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Sahi Connection Driver. * * @author Konstantin Kudryashov */ class Connection { /** * Sahi SID * * @var integer */ private $sid; /** * Is custom SID provided to connection * * @var Boolean */ private $customSidProvided = false; /** * Sahi proxy hostname * * @var string */ private $host; /** * Sahi proxy port number * * @var integer */ private $port; /** * Time limit to connection. * * @var integer */ private $limit; /** * HTTP Browser instance. * * @var BrowserInterface */ protected $browser; /** * Initialize Sahi Driver. * * @param string $sid Sahi SID * @param string $host Sahi proxy host * @param integer $port Sahi proxy port * @param BuzzBrowser $browser HTTP browser instance * @param integer $limit Time limit to connection */ public function __construct($sid = null, $host = 'localhost', $port = 9999, Buzz\Browser $browser = null, $limit = 600) { $this->limit = $limit; if (null !== $sid) { $this->customSidProvided = true; } $this->sid = $sid; $this->host = $host; $this->port = $port; if (null === $browser) { $client = new Buzz\Client\Curl(); $this->browser = new Buzz\Browser($client); } else { $this->browser = $browser; } } /** * Checks that connection used custom SID. * * @return Boolean */ public function isCustomSidProvided() { return $this->customSidProvided; } /** * Returns current connection SID. * * @return string */ public function getSid() { return $this->sid; } /** * Starts browser. * * @param string $browserName (firefox, ie, safari, chrome, opera) */ public function start($browserName) { if (!$this->customSidProvided) { $this->sid = uniqid(); $this->executeCommand('launchPreconfiguredBrowser', array('browserType' => $browserName)); } } /** * Stop browser. */ public function stop() { if (!$this->customSidProvided) { $this->executeCommand('kill'); // sometimes, firefox is not fast enought at closing // and next instance can't be created since previous // still running. So - wait 1 second. sleep(1); } } /** * Checks whether Sahi proxy were started. * * @return Boolean */ public function isProxyStarted() { return 200 === $this->post( sprintf('http://%s:%d/_s_/spr/blank.htm', $this->host, $this->port) )->getStatusCode(); } /** * Checks whether connection is ready. * * @return Boolean */ public function isReady() { return 'true' === $this->executeCommand('isReady'); } /** * Return HTTP Browser instance. * * @return Browser */ public function getBrowser() { return $this->browser; } /** * Execute Sahi command & returns its response. * * @param string $command Sahi command * @param array $parameters parameters * * @return string command response */ public function executeCommand($command, array $parameters = array()) { $content = $this->post( sprintf('http://%s:%d/_s_/dyn/Driver_%s', $this->host, $this->port, $command), array_merge($parameters, array('sahisid' => $this->sid)) )->getContent(); if (false !== strpos($content, 'SAHI_ERROR')) { throw new Exception\ConnectionException('Sahi proxy error'); } return $content; } /** * Execute Sahi step. * * @param string $step step command * @param integer $limit time limit (value of 10 === 1 second) * * @throws BrowserException if step execution has errors */ public function executeStep($step, $limit = null) { $this->executeCommand('setStep', array('step' => $step)); $limit = $limit ?: $this->limit; $check = 'false'; while ('true' !== $check) { usleep(100000); if (--$limit <= 0) { throw new Exception\ConnectionException( 'Command execution time limit reached: `' . $step . '`' ); } $check = $this->executeCommand('doneStep'); if (0 === mb_strpos($check, 'error:')) { throw new Exception\ConnectionException($check); } } } /** * Evaluates JS expression on the browser and returns it's value. * * @param string $expression JS expression * @param integer $limit time limit (value of 10 === 1 second) * * @return string|null */ public function evaluateJavascript($expression, $limit = null) { $key = '___lastValue___' . uniqid(); $this->executeStep( sprintf("_sahi.setServerVarPlain(%s, %s)", "'" . $key . "'", $expression), $limit ); $resp = $this->executeCommand('getVariable', array('key' => $key)); return 'null' === $resp ? null : $resp; } /** * Execute JS expression on the browser. * * @param string $expression JS expression */ public function executeJavascript($expression, $limit = null) { $this->executeStep(sprintf("_sahi._call(%s)", $expression), $limit); } /** * Send POST request to specified URL. * * @param string $url URL * @param array $query POST query parameters * * @return string response */ private function post($url, array $query = array()) { return $this->browser->post($url, array(), $this->prepareQueryString($query)); } /** * Convert array parameters to POST parameters. * * @param array $query parameters * * @return string query string (key1=val1&key2=val2) */ private function prepareQueryString(array $query) { $items = array(); foreach ($query as $key => $val) { $items[] = $key . '=' . urlencode($val); } return implode('&', $items); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Driver Exception. * * @author Konstantin Kudryashov */ abstract class AbstractException extends \Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Accessor Exception. * * @author Konstantin Kudryashov */ class AccessorException extends AbstractException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Sahi Connection Exception. * * @author Konstantin Kudryashov */ class ConnectionException extends AbstractException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * UniversalClassLoader implements a "universal" autoloader for PHP 5.3. * * It is able to load classes that use either: * * * The technical interoperability standards for PHP 5.3 namespaces and * class names (http://groups.google.com/group/php-standards/web/psr-0-final-proposal); * * * The PEAR naming convention for classes (http://pear.php.net/). * * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be * looked for in a list of locations to ease the vendoring of a sub-set of * classes for large projects. * * Example usage: * * $loader = new UniversalClassLoader(); * * // register classes with namespaces * $loader->registerNamespaces(array( * 'Symfony\Component' => __DIR__.'/component', * 'Symfony' => __DIR__.'/framework', * )); * * // register a library using the PEAR naming convention * $loader->registerPrefixes(array( * 'Swift_' => __DIR__.'/Swift', * )); * * // activate the autoloader * $loader->register(); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * @author Fabien Potencier */ class UniversalClassLoader { protected $namespaces = array(); protected $prefixes = array(); public function getNamespaces() { return $this->namespaces; } public function getPrefixes() { return $this->prefixes; } /** * Registers an array of namespaces * * @param array $namespaces An array of namespaces (namespaces as keys and locations as values) */ public function registerNamespaces(array $namespaces) { $this->namespaces = array_merge($this->namespaces, $namespaces); } /** * Registers a namespace. * * @param string $namespace The namespace * @param string $path The location of the namespace */ public function registerNamespace($namespace, $path) { $this->namespaces[$namespace] = $path; } /** * Registers an array of classes using the PEAR naming convention. * * @param array $classes An array of classes (prefixes as keys and locations as values) */ public function registerPrefixes(array $classes) { $this->prefixes = array_merge($this->prefixes, $classes); } /** * Registers a set of classes using the PEAR naming convention. * * @param string $prefix The classes prefix * @param string $path The location of the classes */ public function registerPrefix($prefix, $path) { $this->prefixes[$prefix] = $path; } /** * Registers this instance as an autoloader. */ public function register() { spl_autoload_register(array($this, 'loadClass')); } /** * Loads the given class or interface. * * @param string $class The name of the class */ public function loadClass($class) { if (false !== ($pos = strripos($class, '\\'))) { // namespaced class name $namespace = substr($class, 0, $pos); foreach ($this->namespaces as $ns => $dir) { if (0 === strpos($namespace, $ns)) { $class = substr($class, $pos + 1); $file = $dir.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $namespace).DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php'; if (file_exists($file)) { require $file; } return; } } } else { // PEAR-like class name foreach ($this->prefixes as $prefix => $dir) { if (0 === strpos($class, $prefix)) { $file = $dir.DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php'; if (file_exists($file)) { require $file; } return; } } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Goutte; use Symfony\Component\BrowserKit\Client as BaseClient; use Symfony\Component\BrowserKit\Response; use Guzzle\Http\Exception\CurlException; use Guzzle\Http\Exception\BadResponseException; use Guzzle\Http\Message\Response as GuzzleResponse; use Guzzle\Http\ClientInterface as GuzzleClientInterface; use Guzzle\Http\Client as GuzzleClient; use Guzzle\Http\Message\EntityEnclosingRequestInterface; /** * Client. * * @package Goutte * @author Fabien Potencier * @author Michael Dowling */ class Client extends BaseClient { const VERSION = '0.2'; protected $headers = array(); protected $auth = null; protected $client; public function setClient(GuzzleClientInterface $client) { $this->client = $client; return $this; } public function getClient() { if (!$this->client) { $this->client = new GuzzleClient('', array('redirect.disable' => true)); } return $this->client; } public function setHeader($name, $value) { $this->headers[$name] = $value; return $this; } public function setAuth($user, $password = '', $type = CURLAUTH_BASIC) { $this->auth = array( 'user' => $user, 'password' => $password, 'type' => $type ); return $this; } protected function doRequest($request) { $headers = array(); foreach ($request->getServer() as $key => $val) { $key = ucfirst(strtolower(str_replace(array('_', 'HTTP-'), array('-', ''), $key))); if (!isset($headers[$key])) { $headers[$key] = $val; } } $body = null; if (!in_array($request->getMethod(), array('GET','HEAD'))) { if (null !== $request->getContent()) { $body = $request->getContent(); } else { $body = $request->getParameters(); } } $guzzleRequest = $this->getClient()->createRequest( $request->getMethod(), $request->getUri(), $headers, $body ); foreach ($this->headers as $name => $value) { $guzzleRequest->setHeader($name, $value); } if ($this->auth !== null) { $guzzleRequest->setAuth( $this->auth['user'], $this->auth['password'], $this->auth['type'] ); } foreach ($this->getCookieJar()->allRawValues($request->getUri()) as $name => $value) { $guzzleRequest->addCookie($name, $value); } if ('POST' == $request->getMethod()) { $this->addPostFiles($guzzleRequest, $request->getFiles()); } $guzzleRequest->getParams()->set('redirect.disable', true); $curlOptions = $guzzleRequest->getCurlOptions(); if (!$curlOptions->hasKey(CURLOPT_TIMEOUT)) { $curlOptions->set(CURLOPT_TIMEOUT, 30); } // Let BrowserKit handle redirects try { $response = $guzzleRequest->send(); } catch (CurlException $e) { if (!strpos($e->getMessage(), 'redirects')) { throw $e; } $response = $e->getResponse(); } catch (BadResponseException $e) { $response = $e->getResponse(); } return $this->createResponse($response); } protected function addPostFiles($request, array $files, $arrayName = '') { if (!$request instanceof EntityEnclosingRequestInterface) { return; } foreach ($files as $name => $info) { if (!empty($arrayName)) { $name = $arrayName . '[' . $name . ']'; } if (is_array($info)) { if (isset($info['tmp_name'])) { if ('' !== $info['tmp_name']) { $request->addPostFile($name, $info['tmp_name']); } else { continue; } } else { $this->addPostFiles($request, $info, $name); } } else { $request->addPostFile($name, $info); } } } protected function createResponse(GuzzleResponse $response) { return new Response($response->getBody(true), $response->getStatusCode(), $response->getHeaders()->getAll()); } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Goutte; use Symfony\Component\Finder\Finder; /** * The Compiler class compiles the Goutte utility. * * @author Fabien Potencier */ class Compiler { public function compile($pharFile = 'goutte.phar') { if (file_exists($pharFile)) { unlink($pharFile); } $phar = new \Phar($pharFile, 0, 'Goutte'); $phar->setSignatureAlgorithm(\Phar::SHA1); $phar->startBuffering(); // CLI Component files foreach ($this->getFiles() as $file) { $path = str_replace(__DIR__.'/', '', $file); $phar->addFromString($path, file_get_contents($file)); } // Stubs $phar['_cli_stub.php'] = $this->getCliStub(); $phar['_web_stub.php'] = $this->getWebStub(); $phar->setDefaultStub('_cli_stub.php', '_web_stub.php'); $phar->stopBuffering(); // $phar->compressFiles(\Phar::GZ); unset($phar); } protected function getCliStub() { return "getLicense()." require_once __DIR__.'/vendor/autoload.php'; __HALT_COMPILER();"; } protected function getWebStub() { return " * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */'; } protected function getFiles() { $files = array( 'LICENSE', 'vendor/autoload.php', 'Goutte/Client.php', ); $dirs = array( 'vendor/composer', 'vendor/symfony', 'vendor/guzzle' ); $iterator = Finder::create()->files()->name('*.php')->in($dirs); return array_merge($files, iterator_to_array($iterator)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Goutte\Tests; use Goutte\Client; use Symfony\Component\BrowserKit\Cookie; use Guzzle\Http\Message\Response as GuzzleResponse; use Guzzle\Http\Client as GuzzleClient; use Guzzle\Plugin\Mock\MockPlugin; use Guzzle\Plugin\History\HistoryPlugin; use Guzzle\Http\Message\Response; use Guzzle\Http\Message\PostFile; /** * Goutte Client Test * * @author Michael Dowling */ class ClientTest extends \PHPUnit_Framework_TestCase { protected $history; protected $mockPlugin; protected function getGuzzle() { $this->historyPlugin = new HistoryPlugin(); $this->mockPlugin = new MockPlugin(); $this->mockPlugin->addResponse(new GuzzleResponse(200, null, '

Hi

')); $guzzle = new GuzzleClient('', array('redirect.disable' => true)); $guzzle->getEventDispatcher()->addSubscriber($this->mockPlugin); $guzzle->getEventDispatcher()->addSubscriber($this->historyPlugin); return $guzzle; } public function testCreatesDefaultClient() { $client = new Client(); $this->assertInstanceOf('Guzzle\\Http\\ClientInterface', $client->getClient()); } public function testUsesCustomClient() { $guzzle = new GuzzleClient(); $client = new Client(); $this->assertSame($client, $client->setClient($guzzle)); $this->assertSame($guzzle, $client->getClient()); } public function testUsesCustomHeaders() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $client->setHeader('X-Test', 'test'); $crawler = $client->request('GET', 'http://www.example.com/'); $this->assertEquals('test', $this->historyPlugin->getLastRequest()->getHeader('X-Test')); } public function testCustomUserAgent() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $client->setHeader('User-Agent', 'foo'); $crawler = $client->request('GET', 'http://www.example.com/'); $this->assertEquals('foo', $this->historyPlugin->getLastRequest()->getHeader('User-Agent')); } public function testUsesAuth() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $client->setAuth('me', '**'); $crawler = $client->request('GET', 'http://www.example.com/'); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals('me', $request->getUsername()); $this->assertEquals('**', $request->getPassword()); } public function testUsesCookies() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $client->getCookieJar()->set(new Cookie('test', '123')); $crawler = $client->request('GET', 'http://www.example.com/'); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals('123', $request->getCookie('test')); } public function testUsesPostFiles() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $files = array( 'test' => array( 'name' => 'test.txt', 'tmp_name' => __FILE__ ) ); $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals(array( 'test' => array( new PostFile('test', __FILE__, 'text/x-php') ) ), $request->getPostFiles()); } public function testUsesPostNamedFiles() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $files = array( 'test' => __FILE__ ); $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals(array( 'test' => array( new PostFile('test', __FILE__, 'text/x-php') ) ), $request->getPostFiles()); } public function testUsesPostFilesNestedFields() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $files = array( 'form' => array( 'test' => array( 'name' => 'test.txt', 'tmp_name' => __FILE__ ), ), ); $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals(array( 'form[test]' => array( new PostFile('form[test]', __FILE__, 'text/x-php') ) ), $request->getPostFiles()); } public function testUsesPostFilesOnClientSide() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $files = array( 'test' => __FILE__, ); $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals(array( 'test' => array( new PostFile('test', __FILE__, 'text/x-php') ) ), $request->getPostFiles()); } public function testUsesPostFilesUploadError() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $files = array( 'test' => array( 'name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0, ), ); $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals(array(), $request->getPostFiles()); } public function testUsesCurlOptions() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $crawler = $client->request('GET', 'http://www.example.com/'); $request = $this->historyPlugin->getLastRequest(); $this->assertEquals(0, $request->getCurlOptions()->get(CURLOPT_MAXREDIRS)); $this->assertEquals(30, $request->getCurlOptions()->get(CURLOPT_TIMEOUT)); } public function testCreatesResponse() { $guzzle = $this->getGuzzle(); $client = new Client(); $client->setClient($guzzle); $crawler = $client->request('GET', 'http://www.example.com/'); $this->assertEquals('Hi', $crawler->filter('p')->text()); } public function testHandlesRedirectsCorrectly() { $guzzle = $this->getGuzzle(); $this->mockPlugin->clearQueue(); $this->mockPlugin->addResponse(new GuzzleResponse(301, array( 'Location' => 'http://www.example.com/' ))); $this->mockPlugin->addResponse(new GuzzleResponse(200, null, '

Test

')); $client = new Client(); $client->setClient($guzzle); $crawler = $client->request('GET', 'http://www.example.com/'); $this->assertEquals('Test', $crawler->filter('p')->text()); // Ensure that two requests were sent $this->assertEquals(2, count($this->historyPlugin)); } } Copyright (c) 2010-2013 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. client = $client ?: new FileGetContents(); $this->factory = $factory ?: new Factory(); } public function get($url, $headers = array()) { return $this->call($url, RequestInterface::METHOD_GET, $headers); } public function post($url, $headers = array(), $content = '') { return $this->call($url, RequestInterface::METHOD_POST, $headers, $content); } public function head($url, $headers = array()) { return $this->call($url, RequestInterface::METHOD_HEAD, $headers); } public function patch($url, $headers = array(), $content = '') { return $this->call($url, RequestInterface::METHOD_PATCH, $headers, $content); } public function put($url, $headers = array(), $content = '') { return $this->call($url, RequestInterface::METHOD_PUT, $headers, $content); } public function delete($url, $headers = array(), $content = '') { return $this->call($url, RequestInterface::METHOD_DELETE, $headers, $content); } /** * Sends a request. * * @param string $url The URL to call * @param string $method The request method to use * @param array $headers An array of request headers * @param string $content The request content * * @return Response The response object */ public function call($url, $method, $headers = array(), $content = '') { $request = $this->factory->createRequest($method); if (!$url instanceof Url) { $url = new Url($url); } $url->applyToRequest($request); $request->addHeaders($headers); $request->setContent($content); return $this->send($request); } /** * Sends a form request. * * @param string $url The URL to submit to * @param array $fields An array of fields * @param string $method The request method to use * @param array $headers An array of request headers * * @return Response The response object */ public function submit($url, array $fields, $method = RequestInterface::METHOD_POST, $headers = array()) { $request = $this->factory->createFormRequest(); if (!$url instanceof Url) { $url = new Url($url); } $url->applyToRequest($request); $request->addHeaders($headers); $request->setMethod($method); $request->setFields($fields); return $this->send($request); } /** * Sends a request. * * @param RequestInterface $request A request object * @param MessageInterface $response A response object * * @return MessageInterface The response */ public function send(RequestInterface $request, MessageInterface $response = null) { if (null === $response) { $response = $this->factory->createResponse(); } if ($this->listener) { $this->listener->preSend($request); } $this->client->send($request, $response); $this->lastRequest = $request; $this->lastResponse = $response; if ($this->listener) { $this->listener->postSend($request, $response); } return $response; } public function getLastRequest() { return $this->lastRequest; } public function getLastResponse() { return $this->lastResponse; } public function setClient(ClientInterface $client) { $this->client = $client; } public function getClient() { return $this->client; } public function setMessageFactory(FactoryInterface $factory) { $this->factory = $factory; } public function getMessageFactory() { return $this->factory; } public function setListener(ListenerInterface $listener) { $this->listener = $listener; } public function getListener() { return $this->listener; } public function addListener(ListenerInterface $listener) { if (!$this->listener) { $this->listener = $listener; } elseif ($this->listener instanceof ListenerChain) { $this->listener->addListener($listener); } else { $this->listener = new ListenerChain(array( $this->listener, $listener, )); } } } ignoreErrors = $ignoreErrors; } public function getIgnoreErrors() { return $this->ignoreErrors; } public function setMaxRedirects($maxRedirects) { $this->maxRedirects = $maxRedirects; } public function getMaxRedirects() { return $this->maxRedirects; } public function setTimeout($timeout) { $this->timeout = $timeout; } public function getTimeout() { return $this->timeout; } public function setVerifyPeer($verifyPeer) { $this->verifyPeer = $verifyPeer; } public function getVerifyPeer() { return $this->verifyPeer; } } setHeaders(static::getLastHeaders(rtrim(substr($raw, 0, $pos)))); $response->setContent(substr($raw, $pos)); } /** * Sets options on a cURL resource based on a request. */ static private function setOptionsFromRequest($curl, RequestInterface $request) { $options = array( CURLOPT_CUSTOMREQUEST => $request->getMethod(), CURLOPT_URL => $request->getHost().$request->getResource(), CURLOPT_HTTPHEADER => $request->getHeaders(), ); switch ($request->getMethod()) { case RequestInterface::METHOD_HEAD: $options[CURLOPT_NOBODY] = true; break; case RequestInterface::METHOD_GET: $options[CURLOPT_HTTPGET] = true; break; case RequestInterface::METHOD_POST: case RequestInterface::METHOD_PUT: case RequestInterface::METHOD_DELETE: case RequestInterface::METHOD_PATCH: $options[CURLOPT_POSTFIELDS] = $fields = static::getPostFields($request); // remove the content-type header if (is_array($fields)) { $options[CURLOPT_HTTPHEADER] = array_filter($options[CURLOPT_HTTPHEADER], function($header) { return 0 !== stripos($header, 'Content-Type: '); }); } break; } curl_setopt_array($curl, $options); } /** * Returns a value for the CURLOPT_POSTFIELDS option. * * @return string|array A post fields value */ static private function getPostFields(RequestInterface $request) { if (!$request instanceof FormRequestInterface) { return $request->getContent(); } $fields = $request->getFields(); $multipart = false; foreach ($fields as $name => $value) { if ($value instanceof FormUploadInterface) { $multipart = true; if ($file = $value->getFile()) { // replace value with upload string $fields[$name] = '@'.$file; if ($contentType = $value->getContentType()) { $fields[$name] .= ';type='.$contentType; } } else { return $request->getContent(); } } } return $multipart ? $fields : http_build_query($fields); } /** * A helper for getting the last set of headers. * * @param string $raw A string of many header chunks * * @return array An array of header lines */ static private function getLastHeaders($raw) { $headers = array(); foreach (preg_split('/(\\r?\\n)/', $raw) as $header) { if ($header) { $headers[] = $header; } else { $headers = array(); } } return $headers; } /** * Stashes a cURL option to be set on send, when the resource is created. * * If the supplied value it set to null the option will be removed. * * @param integer $option The option * @param mixed $value The value * * @see curl_setopt() */ public function setOption($option, $value) { if (null === $value) { unset($this->options[$option]); } else { $this->options[$option] = $value; } } /** * Prepares a cURL resource to send a request. */ protected function prepare($curl, RequestInterface $request, array $options = array()) { static::setOptionsFromRequest($curl, $request); // apply settings from client curl_setopt($curl, CURLOPT_TIMEOUT, $this->getTimeout()); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 0 < $this->getMaxRedirects()); curl_setopt($curl, CURLOPT_MAXREDIRS, $this->getMaxRedirects()); curl_setopt($curl, CURLOPT_FAILONERROR, !$this->getIgnoreErrors()); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->getVerifyPeer()); // apply additional options curl_setopt_array($curl, $options + $this->options); } } array( // values from the request 'method' => $request->getMethod(), 'header' => implode("\r\n", $request->getHeaders()), 'content' => $request->getContent(), 'protocol_version' => $request->getProtocolVersion(), // values from the current client 'ignore_errors' => $this->getIgnoreErrors(), 'max_redirects' => $this->getMaxRedirects(), 'timeout' => $this->getTimeout(), ), 'ssl' => array( 'verify_peer' => $this->getVerifyPeer(), ), ); } } lastCurl)) { curl_close($this->lastCurl); } $this->lastCurl = static::createCurlHandle(); $this->prepare($this->lastCurl, $request, $options); $data = curl_exec($this->lastCurl); if (false === $data) { $errorMsg = curl_error($this->lastCurl); $errorNo = curl_errno($this->lastCurl); throw new \RuntimeException($errorMsg, $errorNo); } static::populateResponse($this->lastCurl, $data, $response); } /** * Introspects the last cURL request. * * @see curl_getinfo() */ public function getInfo($opt = 0) { if (!is_resource($this->lastCurl)) { throw new \LogicException('There is no cURL resource'); } return curl_getinfo($this->lastCurl, $opt); } public function __destruct() { if (is_resource($this->lastCurl)) { curl_close($this->lastCurl); } } } setCookieJar($cookieJar); } } public function setCookieJar(CookieJar $cookieJar) { $this->cookieJar = $cookieJar; } public function getCookieJar() { return $this->cookieJar; } /** * @see ClientInterface * * @throws RuntimeException If file_get_contents() fires an error */ public function send(RequestInterface $request, MessageInterface $response) { if ($cookieJar = $this->getCookieJar()) { $cookieJar->clearExpiredCookies(); $cookieJar->addCookieHeaders($request); } $context = stream_context_create($this->getStreamContextArray($request)); $url = $request->getHost().$request->getResource(); $level = error_reporting(0); $content = file_get_contents($url, 0, $context); error_reporting($level); if (false === $content) { $error = error_get_last(); throw new \RuntimeException($error['message']); } $response->setHeaders($this->filterHeaders((array) $http_response_header)); $response->setContent($content); if ($cookieJar) { $cookieJar->processSetCookieHeaders($request, $response); } } private function filterHeaders(array $headers) { $filtered = array(); foreach ($headers as $header) { if (0 === stripos($header, 'http/')) { $filtered = array(); } $filtered[] = $header; } return $filtered; } } queue[] = array($request, $response, $options); } public function flush() { if (false === $curlm = curl_multi_init()) { throw new \RuntimeException('Unable to create a new cURL multi handle'); } // prepare a cURL handle for each entry in the queue foreach ($this->queue as $i => &$queue) { list($request, $response, $options) = $queue; $curl = $queue[] = static::createCurlHandle(); $this->prepare($curl, $request, $options); curl_multi_add_handle($curlm, $curl); } $active = null; do { $mrc = curl_multi_exec($curlm, $active); } while (CURLM_CALL_MULTI_PERFORM == $mrc); while ($active && CURLM_OK == $mrc) { if (-1 != curl_multi_select($curlm)) { do { $mrc = curl_multi_exec($curlm, $active); } while (CURLM_CALL_MULTI_PERFORM == $mrc); } } // populate the responses while (list($request, $response, $options, $curl) = array_shift($this->queue)) { static::populateResponse($curl, curl_multi_getcontent($curl), $response); curl_multi_remove_handle($curlm, $curl); } curl_multi_close($curlm); } } username = $username; $this->password = $password; } public function preSend(RequestInterface $request) { $request->addHeader('Authorization: Basic '.base64_encode($this->username.':'.$this->password)); } public function postSend(RequestInterface $request, MessageInterface $response) { } } callable = $callable; } public function preSend(RequestInterface $request) { call_user_func($this->callable, $request); } public function postSend(RequestInterface $request, MessageInterface $response) { call_user_func($this->callable, $request, $response); } } request = $request; $this->response = $response; $this->duration = $duration; } public function getRequest() { return $this->request; } public function getResponse() { return $this->response; } public function getDuration() { return $this->duration; } } addEntry(new Entry($request, $response, $duration)); } public function addEntry(Entry $entry) { array_push($this->entries, $entry); $this->entries = array_slice($this->entries, $this->getLimit() * -1); end($this->entries); } public function getEntries() { return $this->entries; } public function getLast() { return end($this->entries); } public function getLastRequest() { return $this->getLast()->getRequest(); } public function getLastResponse() { return $this->getLast()->getResponse(); } public function clear() { $this->entries = array(); } public function count() { return count($this->entries); } public function setLimit($limit) { $this->limit = $limit; } public function getLimit() { return $this->limit; } public function getIterator() { return new \ArrayIterator(array_reverse($this->entries)); } } journal = $journal; } public function getJournal() { return $this->journal; } public function preSend(RequestInterface $request) { $this->startTime = microtime(true); } public function postSend(RequestInterface $request, MessageInterface $response) { $this->journal->record($request, $response, microtime(true) - $this->startTime); } } listeners = $listeners; } public function addListener(ListenerInterface $listener) { $this->listeners[] = $listener; } public function getListeners() { return $this->listeners; } public function preSend(RequestInterface $request) { foreach ($this->listeners as $listener) { $listener->preSend($request); } } public function postSend(RequestInterface $request, MessageInterface $response) { foreach ($this->listeners as $listener) { $listener->postSend($request, $response); } } } logger = $logger; $this->prefix = $prefix; } public function preSend(RequestInterface $request) { $this->startTime = microtime(true); } public function postSend(RequestInterface $request, MessageInterface $response) { $seconds = microtime(true) - $this->startTime; call_user_func($this->logger, sprintf('%sSent "%s %s%s" in %dms', $this->prefix, $request->getMethod(), $request->getHost(), $request->getResource(), round($seconds * 1000))); } } getHeaders() as $header) { if (0 === stripos($header, $needle)) { $values[] = trim(substr($header, strlen($needle))); } } if (false === $glue) { return $values; } else { return count($values) ? implode($glue, $values) : null; } } /** * Returns a header's attributes. * * @param string $name A header name * * @return array An associative array of attributes */ public function getHeaderAttributes($name) { $attributes = array(); foreach ($this->getHeader($name, false) as $header) { if (false !== strpos($header, ';')) { // remove header value list(, $header) = explode(';', $header, 2); // loop through attribute key=value pairs foreach (array_map('trim', explode(';', trim($header))) as $pair) { list($key, $value) = explode('=', $pair); $attributes[$key] = $value; } } } return $attributes; } /** * Returns the value of a particular header attribute. * * @param string $header A header name * @param string $attribute An attribute name * * @return string|null The value of the attribute or null if it isn't set */ public function getHeaderAttribute($header, $attribute) { $attributes = $this->getHeaderAttributes($header); if (isset($attributes[$attribute])) { return $attributes[$attribute]; } } /** * Returns the current message as a DOMDocument. * * @return DOMDocument */ public function toDomDocument() { $revert = libxml_use_internal_errors(true); $document = new \DOMDocument('1.0', $this->getHeaderAttribute('Content-Type', 'charset') ?: 'UTF-8'); $document->loadHTML($this->getContent()); libxml_use_internal_errors($revert); return $document; } public function setHeaders(array $headers) { $this->headers = $this->flattenHeaders($headers); } public function addHeader($header) { $this->headers[] = $header; } public function addHeaders(array $headers) { $this->headers = array_merge($this->headers, $this->flattenHeaders($headers)); } public function getHeaders() { return $this->headers; } public function setContent($content) { $this->content = $content; } public function getContent() { return $this->content; } public function __toString() { $string = implode("\r\n", $this->getHeaders())."\r\n"; if ($content = $this->getContent()) { $string .= "\r\n$content\r\n"; } return $string; } protected function flattenHeaders(array $headers) { $flattened = array(); foreach ($headers as $key => $header) { if (is_int($key)) { $flattened[] = $header; } else { $flattened[] = $key.': '.$header; } } return $flattened; } } setField('user[name]', 'Kris Wallsmith'); * $request->setField('user[image]', new FormUpload('/path/to/image.jpg')); * * @author Marc Weistroff * @author Kris Wallsmith */ class FormRequest extends Request implements FormRequestInterface { private $fields = array(); private $boundary; /** * Constructor. * * Defaults to POST rather than GET. */ public function __construct($method = self::METHOD_POST, $resource = '/', $host = null) { parent::__construct($method, $resource, $host); } /** * Sets the value of a form field. * * If the value is an array it will be flattened and one field value will * be added for each leaf. */ public function setField($name, $value) { if (is_array($value)) { $this->addFields(array($name => $value)); return; } if ('[]' == substr($name, -2)) { $this->fields[substr($name, 0, -2)][] = $value; } else { $this->fields[$name] = $value; } } public function addFields(array $fields) { foreach ($this->flattenArray($fields) as $name => $value) { $this->setField($name, $value); } } public function setFields(array $fields) { $this->fields = array(); $this->addFields($fields); } public function getFields() { return $this->fields; } public function getResource() { $resource = parent::getResource(); if (!$this->isSafe() || !$this->fields) { return $resource; } // append the query string $resource .= false === strpos($resource, '?') ? '?' : '&'; $resource .= http_build_query($this->fields); return $resource; } public function setContent($content) { throw new \BadMethodCallException('It is not permitted to set the content.'); } public function getHeaders() { $headers = parent::getHeaders(); if ($this->isSafe()) { return $headers; } if ($this->isMultipart()) { $headers[] = 'Content-Type: multipart/form-data; boundary='.$this->getBoundary(); } else { $headers[] = 'Content-Type: application/x-www-form-urlencoded'; } return $headers; } public function getContent() { if ($this->isSafe()) { return; } if (!$this->isMultipart()) { return http_build_query($this->fields); } $content = ''; foreach ($this->fields as $name => $values) { $content .= '--'.$this->getBoundary()."\r\n"; if ($values instanceof FormUploadInterface) { if (!$values->getFilename()) { throw new \LogicException(sprintf('Form upload at "%s" does not include a filename.', $name)); } $values->setName($name); $content .= (string) $values; } else { foreach (is_array($values) ? $values : array($values) as $value) { $content .= "Content-Disposition: form-data; name=\"$name\"\r\n"; $content .= "\r\n"; $content .= $value."\r\n"; } } } $content .= '--'.$this->getBoundary().'--'; return $content; } // private private function flattenArray(array $values, $prefix = '', $format = '%s') { $flat = array(); foreach ($values as $name => $value) { $flatName = $prefix.sprintf($format, $name); if (is_array($value)) { $flat += $this->flattenArray($value, $flatName, '[%s]'); } else { $flat[$flatName] = $value; } } return $flat; } private function isSafe() { return in_array($this->getMethod(), array(self::METHOD_GET, self::METHOD_HEAD)); } private function isMultipart() { foreach ($this->fields as $name => $value) { if (is_object($value) && $value instanceof FormUploadInterface) { return true; } } return false; } private function getBoundary() { if (!$this->boundary) { $this->boundary = sha1(rand(11111, 99999).time().uniqid()); } return $this->boundary; } } */ interface FormRequestInterface extends RequestInterface { /** * Returns an array of field names and values. * * @return array A array of names and values */ function getFields(); /** * Sets the form fields for the current request. * * @param array $fields An array of field names and values */ function setFields(array $fields); } loadContent($file); } $this->contentType = $contentType; } public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } public function getFilename() { if ($this->filename) { return $this->filename; } elseif ($this->file) { return basename($this->file); } } public function setFilename($filename) { $this->filename = $filename; } public function getContentType() { return $this->contentType ?: $this->detectContentType() ?: 'application/octet-stream'; } public function setContentType($contentType) { $this->contentType = $contentType; } /** * Prepends Content-Disposition and Content-Type headers. */ public function getHeaders() { $headers = array('Content-Disposition: form-data'); if ($name = $this->getName()) { $headers[0] .= sprintf('; name="%s"', $name); } if ($filename = $this->getFilename()) { $headers[0] .= sprintf('; filename="%s"', $filename); } if ($contentType = $this->getContentType()) { $headers[] = 'Content-Type: '.$contentType; } return array_merge($headers, parent::getHeaders()); } /** * Loads the content from a file. */ public function loadContent($file) { $this->file = $file; parent::setContent(null); } public function setContent($content) { parent::setContent($content); $this->file = null; } public function getFile() { return $this->file; } public function getContent() { return $this->file ? file_get_contents($this->file) : parent::getContent(); } // private private function detectContentType() { if (!class_exists('finfo', false)) { return false; } $finfo = new \finfo(FILEINFO_MIME_TYPE); return $this->file ? $finfo->file($this->file) : $finfo->buffer(parent::getContent()); } } */ interface MessageInterface { /** * Returns a header value. * * @param string $name A header name * @param string|boolean $glue Glue for implode, or false to return an array * * @return string|array|null The header value(s) */ function getHeader($name, $glue = "\r\n"); /** * Returns an array of header lines. * * @return array An array of header lines (integer indexes, e.g. ["Header: value"]) */ function getHeaders(); /** * Sets all headers on the current message. * * Headers can be complete ["Header: value"] pairs or an associative array ["Header" => "value"] * * @param array $headers An array of header lines */ function setHeaders(array $headers); /** * Adds a header to this message. * * @param string $header A header line */ function addHeader($header); /** * Adds a set of headers to this message. * * Headers can be complete ["Header: value"] pairs or an associative array ["Header" => "value"] * * @param array $headers Headers */ function addHeaders(array $headers); /** * Returns the content of the message. * * @return string The message content */ function getContent(); /** * Sets the content of the message. * * @param string $content The message content */ function setContent($content); /** * Returns the message document. * * @return string The message */ function __toString(); } method = strtoupper($method); $this->resource = $resource; $this->host = $host; } public function setHeaders(array $headers) { parent::setHeaders(array()); foreach ($this->flattenHeaders($headers) as $header) { $this->addHeader($header); } } public function addHeader($header) { if (0 === stripos(substr($header, -8), 'HTTP/1.') && 3 == count($parts = explode(' ', $header))) { list($method, $resource, $protocolVersion) = $parts; $this->setMethod($method); $this->setResource($resource); $this->setProtocolVersion((float) substr($protocolVersion, 5)); } else { parent::addHeader($header); } } public function setMethod($method) { $this->method = strtoupper($method); } public function getMethod() { return $this->method; } public function setResource($resource) { $this->resource = $resource; } public function getResource() { return $this->resource; } public function setHost($host) { $this->host = $host; } public function getHost() { return $this->host; } public function setProtocolVersion($protocolVersion) { $this->protocolVersion = $protocolVersion; } public function getProtocolVersion() { return $this->protocolVersion; } /** * A convenience method for getting the full URL of the current request. * * @return string */ public function getUrl() { return $this->getHost().$this->getResource(); } /** * A convenience method for populating the current request from a URL. * * @param Url|string $url An URL */ public function fromUrl($url) { if (!$url instanceof Url) { $url = new Url($url); } $url->applyToRequest($this); } /** * Returns true if the current request is secure. * * @return boolean */ public function isSecure() { return 'https' == parse_url($this->getHost(), PHP_URL_SCHEME); } /** * Merges cookie headers on the way out. */ public function getHeaders() { return $this->mergeCookieHeaders(parent::getHeaders()); } /** * Returns a string representation of the current request. * * @return string */ public function __toString() { $string = sprintf("%s %s HTTP/%.1f\r\n", $this->getMethod(), $this->getResource(), $this->getProtocolVersion()); if ($host = $this->getHost()) { $string .= 'Host: '.$host."\r\n"; } if ($parent = trim(parent::__toString())) { $string .= $parent."\r\n"; } return $string; } // private private function mergeCookieHeaders(array $headers) { $cookieHeader = null; $needle = 'Cookie:'; foreach ($headers as $i => $header) { if (0 !== stripos($header, $needle)) { continue; } if (null === $cookieHeader) { $cookieHeader = $i; } else { $headers[$cookieHeader] .= '; '.trim(substr($header, strlen($needle))); unset($headers[$i]); } } return array_values($headers); } } */ interface RequestInterface extends MessageInterface { const METHOD_OPTIONS = 'OPTIONS'; const METHOD_GET = 'GET'; const METHOD_HEAD = 'HEAD'; const METHOD_POST = 'POST'; const METHOD_PUT = 'PUT'; const METHOD_DELETE = 'DELETE'; const METHOD_PATCH = 'PATCH'; /** * Returns the HTTP method of the current request. * * @return string An HTTP method */ function getMethod(); /** * Sets the HTTP method of the current request. * * @param string $method The request method */ function setMethod($method); /** * Returns the resource portion of the request line. * * @return string The resource requested */ function getResource(); /** * Sets the resource for the current request. * * @param string $resource The resource being requested */ function setResource($resource); /** * Returns the protocol version of the current request. * * @return float The protocol version */ function getProtocolVersion(); /** * Returns the value of the host header. * * @return string|null The host */ function getHost(); /** * Sets the host for the current request. * * @param string $host The host */ function setHost($host); /** * Checks if the current request is secure. * * @return Boolean True if the request is secure */ function isSecure(); } protocolVersion) { $this->parseStatusLine(); } return $this->protocolVersion ?: null; } /** * Returns the status code of the current response. * * @return integer */ public function getStatusCode() { if (null === $this->statusCode) { $this->parseStatusLine(); } return $this->statusCode ?: null; } /** * Returns the reason phrase for the current response. * * @return string */ public function getReasonPhrase() { if (null === $this->reasonPhrase) { $this->parseStatusLine(); } return $this->reasonPhrase ?: null; } public function setHeaders(array $headers) { parent::setHeaders($headers); $this->resetStatusLine(); } public function addHeader($header) { parent::addHeader($header); $this->resetStatusLine(); } public function addHeaders(array $headers) { parent::addHeaders($headers); $this->resetStatusLine(); } /** * Is response invalid? * * @return Boolean */ public function isInvalid() { return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; } /** * Is response informative? * * @return Boolean */ public function isInformational() { return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; } /** * Is response successful? * * @return Boolean */ public function isSuccessful() { return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; } /** * Is the response a redirect? * * @return Boolean */ public function isRedirection() { return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; } /** * Is there a client error? * * @return Boolean */ public function isClientError() { return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; } /** * Was there a server side error? * * @return Boolean */ public function isServerError() { return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; } /** * Is the response OK? * * @return Boolean */ public function isOk() { return 200 === $this->getStatusCode(); } /** * Is the reponse forbidden? * * @return Boolean */ public function isForbidden() { return 403 === $this->getStatusCode(); } /** * Is the response a not found error? * * @return Boolean */ public function isNotFound() { return 404 === $this->getStatusCode(); } /** * Is the response empty? * * @return Boolean */ public function isEmpty() { return in_array($this->getStatusCode(), array(201, 204, 304)); } // private private function parseStatusLine() { $headers = $this->getHeaders(); if (isset($headers[0]) && 3 == count($parts = explode(' ', $headers[0], 3))) { $this->protocolVersion = (float) $parts[0]; $this->statusCode = (integer) $parts[1]; $this->reasonPhrase = $parts[2]; } else { $this->protocolVersion = $this->statusCode = $this->reasonPhrase = false; } } private function resetStatusLine() { $this->protocolVersion = $this->statusCode = $this->reasonPhrase = null; } } createdAt = time(); } /** * Returns true if the current cookie matches the supplied request. * * @return boolean */ public function matchesRequest(RequestInterface $request) { // domain if (!$this->matchesDomain(parse_url($request->getHost(), PHP_URL_HOST))) { return false; } // path if (!$this->matchesPath($request->getResource())) { return false; } // secure if ($this->hasAttribute(static::ATTR_SECURE) && !$request->isSecure()) { return false; } return true; } /** * Returns true of the current cookie has expired. * * Checks the max-age and expires attributes. * * @return boolean Whether the current cookie has expired */ public function isExpired() { $maxAge = $this->getAttribute(static::ATTR_MAX_AGE); if ($maxAge && time() - $this->getCreatedAt() > $maxAge) { return true; } $expires = $this->getAttribute(static::ATTR_EXPIRES); if ($expires && strtotime($expires) < time()) { return true; } return false; } /** * Returns true if the current cookie matches the supplied domain. * * @param string $domain A domain hostname * * @return boolean */ public function matchesDomain($domain) { $cookieDomain = $this->getAttribute(static::ATTR_DOMAIN); if (0 === strpos($cookieDomain, '.')) { $pattern = '/\b'.preg_quote(substr($cookieDomain, 1), '/').'$/i'; return (boolean) preg_match($pattern, $domain); } else { return 0 == strcasecmp($cookieDomain, $domain); } } /** * Returns true if the current cookie matches the supplied path. * * @param string $path A path * * @return boolean */ public function matchesPath($path) { $needle = $this->getAttribute(static::ATTR_PATH); return null === $needle || 0 === strpos($path, $needle); } /** * Populates the current cookie with data from the supplied Set-Cookie header. * * @param string $header A Set-Cookie header * @param string $issuingDomain The domain that issued the header */ public function fromSetCookieHeader($header, $issuingDomain) { list($this->name, $header) = explode('=', $header, 2); if (false === strpos($header, ';')) { $this->value = $header; $header = null; } else { list($this->value, $header) = explode(';', $header, 2); } $this->clearAttributes(); foreach (array_map('trim', explode(';', trim($header))) as $pair) { if (false === strpos($pair, '=')) { $name = $pair; $value = null; } else { list($name, $value) = explode('=', $pair); } $this->setAttribute($name, $value); } if (!$this->getAttribute(static::ATTR_DOMAIN)) { $this->setAttribute(static::ATTR_DOMAIN, $issuingDomain); } } /** * Formats a Cookie header for the current cookie. * * @return string An HTTP request Cookie header */ public function toCookieHeader() { return 'Cookie: '.$this->getName().'='.$this->getValue(); } public function setName($name) { $this->name = $name; } public function getName() { return $this->name; } public function setValue($value) { $this->value = $value; } public function getValue() { return $this->value; } public function setAttributes(array $attributes) { // attributes are case insensitive $this->attributes = array_change_key_case($attributes); } public function setAttribute($name, $value) { $this->attributes[strtolower($name)] = $value; } public function getAttributes() { return $this->attributes; } public function getAttribute($name) { $name = strtolower($name); if (isset($this->attributes[$name])) { return $this->attributes[$name]; } } public function hasAttribute($name) { return array_key_exists($name, $this->attributes); } public function clearAttributes() { $this->setAttributes(array()); } public function setCreatedAt($createdAt) { $this->createdAt = $createdAt; } public function getCreatedAt() { return $this->createdAt; } } cookies = array(); foreach ($cookies as $cookie) { $this->addCookie($cookie); } } public function getCookies() { return $this->cookies; } /** * Adds a cookie to the current cookie jar. * * @param Cookie $cookie A cookie object */ public function addCookie(Cookie $cookie) { $this->cookies[] = $cookie; } /** * Adds Cookie headers to the supplied request. * * @param RequestInterface $request A request object */ public function addCookieHeaders(RequestInterface $request) { foreach ($this->cookies as $cookie) { if ($cookie->matchesRequest($request)) { $request->addHeader($cookie->toCookieHeader()); } } } /** * Processes Set-Cookie headers from a request/response pair. * * @param RequestInterface $request A request object * @param MessageInterface $response A response object */ public function processSetCookieHeaders(RequestInterface $request, MessageInterface $response) { foreach ($response->getHeader('Set-Cookie', false) as $header) { $cookie = new Cookie(); $cookie->fromSetCookieHeader($header, parse_url($request->getHost(), PHP_URL_HOST)); $this->addCookie($cookie); } } /** * Removes expired cookies. */ public function clearExpiredCookies() { foreach ($this->cookies as $i => $cookie) { if ($cookie->isExpired()) { unset($this->cookies[$i]); } } // reset array keys $this->cookies = array_values($this->cookies); } } 80, 'https' => 443, ); private $url; private $components; /** * Constructor. * * @param string $url The URL * * @throws InvalidArgumentException If the URL is invalid */ public function __construct($url) { $components = parse_url($url); if (false === $components) { throw new \InvalidArgumentException(sprintf('The URL "%s" is invalid.', $url)); } // support scheme-less URLs if (!isset($components['host']) && isset($components['path'])) { $pos = strpos($components['path'], '/'); if (false === $pos) { $components['host'] = $components['path']; unset($components['path']); } elseif (0 !== $pos) { list($host, $path) = explode('/', $components['path'], 2); $components['host'] = $host; $components['path'] = '/'.$path; } } // default port if (isset($components['scheme']) && !isset($components['port']) && isset(self::$defaultPorts[$components['scheme']])) { $components['port'] = self::$defaultPorts[$components['scheme']]; } $this->url = $url; $this->components = $components; } public function getScheme() { return $this->parseUrl('scheme'); } public function getHostname() { return $this->parseUrl('host'); } public function getPort() { return $this->parseUrl('port'); } public function getUser() { return $this->parseUrl('user'); } public function getPassword() { return $this->parseUrl('pass'); } public function getPath() { return $this->parseUrl('path'); } public function getQueryString() { return $this->parseUrl('query'); } public function getFragment() { return $this->parseUrl('fragment'); } /** * Returns a host string that combines scheme, hostname and port. * * @return string A host value for an HTTP message */ public function getHost() { if ($hostname = $this->parseUrl('host')) { $host = $scheme = $this->parseUrl('scheme', 'http'); $host .= '://'; $host .= $hostname; $port = $this->parseUrl('port'); if ($port && (!isset(self::$defaultPorts[$scheme]) || self::$defaultPorts[$scheme] != $port)) { $host .= ':'.$port; } return $host; } } /** * Returns a resource string that combines path and query string. * * @return string A resource value for an HTTP message */ public function getResource() { $resource = $this->parseUrl('path', '/'); if ($query = $this->parseUrl('query')) { $resource .= '?'.$query; } return $resource; } /** * Returns a formatted URL. */ public function format($pattern) { static $map = array( 's' => 'getScheme', 'u' => 'getUser', 'a' => 'getPassword', 'h' => 'getHostname', 'o' => 'getPort', 'p' => 'getPath', 'q' => 'getQueryString', 'f' => 'getFragment', 'H' => 'getHost', 'R' => 'getResource', ); $url = ''; $parts = str_split($pattern); while ($part = current($parts)) { if (isset($map[$part])) { $method = $map[$part]; $url .= $this->$method(); } elseif ('\\' == $part) { $url .= next($parts); } elseif (!ctype_alpha($part)) { $url .= $part; } else { throw new \InvalidArgumentException(sprintf('The format character "%s" is invalid.', $part)); } next($parts); } return $url; } /** * Applies the current URL to the supplied request. */ public function applyToRequest(RequestInterface $request) { $request->setResource($this->getResource()); $request->setHost($this->getHost()); } private function parseUrl($component = null, $default = null) { if (null === $component) { return $this->components; } elseif (isset($this->components[$component])) { return $this->components[$component]; } else { return $default; } } } Copyright (c) 2010-2011 Kris Wallsmith 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. Copyright (c) 2011 Michael Dowling, https://github.com/mtdowling 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. registerNamespaces(array( 'Guzzle' => 'phar://guzzle.phar/src', 'Symfony\\Component\\EventDispatcher' => 'phar://guzzle.phar/vendor/symfony/event-dispatcher', 'Doctrine' => 'phar://guzzle.phar/vendor/doctrine/common/lib', 'Monolog' => 'phar://guzzle.phar/vendor/monolog/monolog/src' )); $classLoader->register(); __HALT_COMPILER(); Using composer at ${cmd.composer} Using composer at ${cmd.composer} Composer is installed locally using git at ${cmd.git} found git at ${cmd.git} On branch ${head} working directory clean ${git.status} ChangeLog Match: ${version.changelog} Guzzle\Common\Version Match: ${version.version} releasing: phing -Dnew.version=3.0.x -Dhead=master release -- BEGINNING RELEASE FOR ${new.version} Tip: to create a new release, do: phing -Dnew.version=[TAG] -Dhead=[BRANCH] release * @license http://claylo.mit-license.org/2012/ MIT License */ require_once 'phing/Task.php'; class ComposerLintTask extends Task { protected $dir = null; protected $file = null; protected $passthru = false; protected $composer = null; /** * The setter for the dir * * @param string $str Directory to crawl recursively for composer files */ public function setDir($str) { $this->dir = $str; } /** * The setter for the file * * @param string $str Individual file to validate */ public function setFile($str) { $this->file = $str; } /** * Whether to use PHP's passthru() function instead of exec() * * @param boolean $passthru If passthru shall be used */ public function setPassthru($passthru) { $this->passthru = (bool) $passthru; } /** * Composer to execute. If unset, will attempt composer.phar in project * basedir, and if that fails, will attempt global composer * installation. * * @param string $str Individual file to validate */ public function setComposer($str) { $this->file = $str; } /** * The init method: do init steps */ public function init() { // nothing needed here } /** * The main entry point */ public function main() { if ($this->composer === null) { $this->findComposer(); } $files = array(); if (!empty($this->file) && file_exists($this->file)) { $files[] = $this->file; } if (!empty($this->dir)) { $found = $this->findFiles(); foreach ($found as $file) { $files[] = $this->dir . DIRECTORY_SEPARATOR . $file; } } foreach ($files as $file) { $cmd = $this->composer . ' validate ' . $file; $cmd = escapeshellcmd($cmd); if ($this->passthru) { $retval = null; passthru($cmd, $retval); if ($retval == 1) { throw new BuildException('invalid composer.json'); } } else { $out = array(); $retval = null; exec($cmd, $out, $retval); if ($retval == 1) { $err = join("\n", $out); throw new BuildException($err); } else { $this->log($out[0]); } } } } /** * Find the composer.json files using Phing's directory scanner * * @return array */ protected function findFiles() { $ds = new DirectoryScanner(); $ds->setBasedir($this->dir); $ds->setIncludes(array('**/composer.json')); $ds->scan(); return $ds->getIncludedFiles(); } /** * Find composer installation * */ protected function findComposer() { $basedir = $this->project->getBasedir(); $php = $this->project->getProperty('php.interpreter'); if (file_exists($basedir . '/composer.phar')) { $this->composer = "$php $basedir/composer.phar"; } else { $out = array(); exec('which composer', $out); if (empty($out)) { throw new BuildException( 'Could not determine composer location.' ); } $this->composer = $out[0]; } } } * @license http://claylo.mit-license.org/2012/ MIT License */ require_once 'phing/Task.php'; require_once 'PEAR/PackageFileManager2.php'; require_once 'PEAR/PackageFileManager/File.php'; require_once 'PEAR/Packager.php'; class GuzzlePearPharPackageTask extends Task { private $dir; private $version; private $deploy = true; private $makephar = true; private $subpackages = array(); public function setVersion($str) { $this->version = $str; } public function getVersion() { return $this->version; } public function setDeploy($deploy) { $this->deploy = (bool) $deploy; } public function getDeploy() { return $this->deploy; } public function setMakephar($makephar) { $this->makephar = (bool) $makephar; } public function getMakephar() { return $this->makephar; } private $basedir; private $guzzleinfo; private $changelog_release_date; private $changelog_notes = '-'; public function main() { $this->basedir = $this->getProject()->getBasedir(); if (!is_dir((string) $this->basedir.'/.subsplit')) { throw new BuildException('PEAR packaging requires .subsplit directory'); } // main composer file $composer_file = file_get_contents((string) $this->basedir.'/.subsplit/composer.json'); $this->guzzleinfo = json_decode($composer_file, true); // make sure we have a target $pearwork = (string) $this->basedir . '/build/pearwork'; if (!is_dir($pearwork)) { mkdir($pearwork, 0777, true); } $pearlogs = (string) $this->basedir . '/build/artifacts/logs'; if (!is_dir($pearlogs)) { mkdir($pearlogs, 0777, true); } $version = $this->getVersion(); $this->grabChangelog(); if ($version[0] == '2') { $this->log('building single PEAR package'); $this->buildSinglePackage(); } else { // $this->log("building PEAR subpackages"); // $this->createSubPackages(); // $this->log("building PEAR bundle package"); $this->buildSinglePackage(); } if ($this->getMakephar()) { $this->log("building PHAR"); $this->getProject()->executeTarget('package-phar'); } if ($this->getDeploy()) { $this->doDeployment(); } } public function doDeployment() { $basedir = (string) $this->basedir; $this->log('beginning PEAR/PHAR deployment'); chdir($basedir . '/build/pearwork'); if (is_dir($basedir . '/build/pearwork/guzzle.github.com')) { exec('rm -rf guzzle.github.com'); } passthru('git clone git@github.com:guzzle/guzzle.github.com'); // add PEAR packages foreach (scandir($basedir . '/build/pearwork') as $file) { if (substr($file, -4) == '.tgz') { passthru('pirum add guzzle.github.com/pear '.$file); } } // if we have a new phar, add it if ($this->getMakephar() && file_exists($basedir.'/build/artifacts/guzzle.phar')) { rename($basedir.'/build/artifacts/guzzle.phar', $basedir.'/build/pearwork/guzzle.github.com/guzzle.phar'); } // add and commit chdir($basedir . '/build/pearwork/guzzle.github.com'); passthru('git add --all .'); passthru('git commit -m "Pushing PEAR/PHAR release for '.$this->getVersion().'" && git push'); } public function buildSinglePackage() { $v = $this->getVersion(); $apiversion = $v[0] . '.0.0'; $opts = array( 'packagedirectory' => (string) $this->basedir . '/.subsplit/src/', 'filelistgenerator' => 'file', 'ignore' => array('*composer.json'), 'baseinstalldir' => '/', 'packagefile' => 'package.xml' //'outputdirectory' => (string) $this->basedir . '/build/pearwork/' ); $pfm = new PEAR_PackageFileManager2(); $e = $pfm->setOptions($opts); $pfm->addRole('md', 'doc'); $pfm->setPackage('Guzzle'); $pfm->setSummary("Object-oriented PHP HTTP Client for PHP 5.3+"); $pfm->setDescription($this->guzzleinfo['description']); $pfm->setPackageType('php'); $pfm->setChannel('guzzlephp.org/pear'); $pfm->setAPIVersion($apiversion); $pfm->setReleaseVersion($this->getVersion()); $pfm->setAPIStability('stable'); $pfm->setReleaseStability('stable'); $pfm->setNotes($this->changelog_notes); $pfm->setPackageType('php'); $pfm->setLicense('MIT', 'http://github.com/guzzle/guzzle/blob/master/LICENSE'); $pfm->addMaintainer('lead', 'mtdowling', 'Michael Dowling', 'mtdowling@gmail.com', 'yes'); $pfm->setDate($this->changelog_release_date); $pfm->generateContents(); $phpdep = $this->guzzleinfo['require']['php']; $phpdep = str_replace('>=', '', $phpdep); $pfm->setPhpDep($phpdep); $pfm->addExtensionDep('required', 'curl'); $pfm->setPearinstallerDep('1.4.6'); $pfm->addPackageDepWithChannel('required', 'EventDispatcher', 'pear.symfony.com', '2.1.0'); if (!empty($this->subpackages)) { foreach ($this->subpackages as $package) { $pkg = dirname($package); $pkg = str_replace('/', '_', $pkg); $pfm->addConflictingPackageDepWithChannel($pkg, 'guzzlephp.org/pear', false, $apiversion); } } ob_start(); $startdir = getcwd(); chdir((string) $this->basedir . '/build/pearwork'); echo "DEBUGGING GENERATED PACKAGE FILE\n"; $result = $pfm->debugPackageFile(); if ($result) { $out = $pfm->writePackageFile(); echo "\n\n\nWRITE PACKAGE FILE RESULT:\n"; var_dump($out); // load up package file and build package $packager = new PEAR_Packager(); echo "\n\n\nBUILDING PACKAGE FROM PACKAGE FILE:\n"; $dest_package = $packager->package($opts['packagedirectory'].'package.xml'); var_dump($dest_package); } else { echo "\n\n\nDEBUGGING RESULT:\n"; var_dump($result); } echo "removing package.xml"; unlink($opts['packagedirectory'].'package.xml'); $log = ob_get_clean(); file_put_contents((string) $this->basedir . '/build/artifacts/logs/pear_package.log', $log); chdir($startdir); } public function createSubPackages() { $version = $this->getVersion(); $this->findComponents(); foreach ($this->subpackages as $package) { $baseinstalldir = dirname($package); $dir = (string) $this->basedir.'/.subsplit/src/' . $baseinstalldir; $composer_file = file_get_contents((string) $this->basedir.'/.subsplit/src/'. $package); $package_info = json_decode($composer_file, true); $this->log('building ' . $package_info['target-dir'] . ' subpackage'); $this->buildSubPackage($dir, $baseinstalldir, $package_info); } } public function buildSubPackage($dir, $baseinstalldir, $info) { $package = str_replace('/', '_', $baseinstalldir); $opts = array( 'packagedirectory' => $dir, 'filelistgenerator' => 'file', 'ignore' => array('*composer.json', '*package.xml'), 'baseinstalldir' => '/' . $info['target-dir'], 'packagefile' => 'package.xml' ); $pfm = new PEAR_PackageFileManager2(); $e = $pfm->setOptions($opts); $pfm->setPackage($package); $pfm->setSummary($info['description']); $pfm->setDescription($info['description']); $pfm->setPackageType('php'); $pfm->setChannel('guzzlephp.org/pear'); $pfm->setAPIVersion('3.0.0'); $pfm->setReleaseVersion($this->getVersion()); $pfm->setAPIStability('stable'); $pfm->setReleaseStability('stable'); $pfm->setNotes($this->changelog_notes); $pfm->setPackageType('php'); $pfm->setLicense('MIT', 'http://github.com/guzzle/guzzle/blob/master/LICENSE'); $pfm->addMaintainer('lead', 'mtdowling', 'Michael Dowling', 'mtdowling@gmail.com', 'yes'); $pfm->setDate($this->changelog_release_date); $pfm->generateContents(); $phpdep = $this->guzzleinfo['require']['php']; $phpdep = str_replace('>=', '', $phpdep); $pfm->setPhpDep($phpdep); $pfm->setPearinstallerDep('1.4.6'); foreach ($info['require'] as $type => $version) { if ($type == 'php') { continue; } if ($type == 'symfony/event-dispatcher') { $pfm->addPackageDepWithChannel('required', 'EventDispatcher', 'pear.symfony.com', '2.1.0'); } if ($type == 'ext-curl') { $pfm->addExtensionDep('required', 'curl'); } if (substr($type, 0, 6) == 'guzzle') { $gdep = str_replace('/', ' ', $type); $gdep = ucwords($gdep); $gdep = str_replace(' ', '_', $gdep); $pfm->addPackageDepWithChannel('required', $gdep, 'guzzlephp.org/pear', $this->getVersion()); } } // can't have main Guzzle package AND sub-packages $pfm->addConflictingPackageDepWithChannel('Guzzle', 'guzzlephp.org/pear', false, $apiversion); ob_start(); $startdir = getcwd(); chdir((string) $this->basedir . '/build/pearwork'); echo "DEBUGGING GENERATED PACKAGE FILE\n"; $result = $pfm->debugPackageFile(); if ($result) { $out = $pfm->writePackageFile(); echo "\n\n\nWRITE PACKAGE FILE RESULT:\n"; var_dump($out); // load up package file and build package $packager = new PEAR_Packager(); echo "\n\n\nBUILDING PACKAGE FROM PACKAGE FILE:\n"; $dest_package = $packager->package($opts['packagedirectory'].'/package.xml'); var_dump($dest_package); } else { echo "\n\n\nDEBUGGING RESULT:\n"; var_dump($result); } echo "removing package.xml"; unlink($opts['packagedirectory'].'/package.xml'); $log = ob_get_clean(); file_put_contents((string) $this->basedir . '/build/artifacts/logs/pear_package_'.$package.'.log', $log); chdir($startdir); } public function findComponents() { $ds = new DirectoryScanner(); $ds->setBasedir((string) $this->basedir.'/.subsplit/src'); $ds->setIncludes(array('**/composer.json')); $ds->scan(); $files = $ds->getIncludedFiles(); $this->subpackages = $files; } public function grabChangelog() { $cl = file((string) $this->basedir.'/.subsplit/CHANGELOG.md'); $notes = ''; $in_version = false; $release_date = null; foreach ($cl as $line) { $line = trim($line); if (preg_match('/^\* '.$this->getVersion().' \(([0-9\-]+)\)$/', $line, $matches)) { $release_date = $matches[1]; $in_version = true; continue; } if ($in_version && empty($line) && empty($notes)) { continue; } if ($in_version && ! empty($line)) { $notes .= $line."\n"; } if ($in_version && empty($line) && !empty($notes)) { $in_version = false; } } $this->changelog_release_date = $release_date; if (! empty($notes)) { $this->changelog_notes = $notes; } } } * @license http://claylo.mit-license.org/2012/ MIT License */ require_once 'phing/tasks/ext/git/GitBaseTask.php'; // base - base of tree to split out // subIndicatorFile - composer.json, package.xml? class GuzzleSubSplitTask extends GitBaseTask { /** * What git repository to pull from and publish to */ protected $remote = null; /** * Publish for comma-separated heads instead of all heads */ protected $heads = null; /** * Publish for comma-separated tags instead of all tags */ protected $tags = null; /** * Base of the tree RELATIVE TO .subsplit working dir */ protected $base = null; /** * The presence of this file will indicate that the directory it resides * in is at the top level of a split. */ protected $subIndicatorFile = 'composer.json'; /** * Do everything except actually send the update. */ protected $dryRun = null; /** * Do not sync any heads. */ protected $noHeads = false; /** * Do not sync any tags. */ protected $noTags = false; /** * The splits we found in the heads */ protected $splits; public function setRemote($str) { $this->remote = $str; } public function getRemote() { return $this->remote; } public function setHeads($str) { $this->heads = explode(',', $str); } public function getHeads() { return $this->heads; } public function setTags($str) { $this->tags = explode(',', $str); } public function getTags() { return $this->tags; } public function setBase($str) { $this->base = $str; } public function getBase() { return $this->base; } public function setSubIndicatorFile($str) { $this->subIndicatorFile = $str; } public function getSubIndicatorFile() { return $this->subIndicatorFile; } public function setDryRun($bool) { $this->dryRun = (bool) $bool; } public function getDryRun() { return $this->dryRun; } public function setNoHeads($bool) { $this->noHeads = (bool) $bool; } public function getNoHeads() { return $this->noHeads; } public function setNoTags($bool) { $this->noTags = (bool) $bool; } public function getNoTags() { return $this->noTags; } /** * GitClient from VersionControl_Git */ protected $client = null; /** * The main entry point */ public function main() { $repo = $this->getRepository(); if (empty($repo)) { throw new BuildException('"repository" is a required parameter'); } $remote = $this->getRemote(); if (empty($remote)) { throw new BuildException('"remote" is a required parameter'); } chdir($repo); $this->client = $this->getGitClient(false, $repo); // initalized yet? if (!is_dir('.subsplit')) { $this->subsplitInit(); } else { // update $this->subsplitUpdate(); } // find all splits based on heads requested $this->findSplits(); // check that GitHub has the repos $this->verifyRepos(); // execute the subsplits $this->publish(); } public function publish() { $this->log('DRY RUN ONLY FOR NOW'); $base = $this->getBase(); $base = rtrim($base, '/') . '/'; $org = $this->getOwningTarget()->getProject()->getProperty('github.org'); $splits = array(); $heads = $this->getHeads(); foreach ($heads as $head) { foreach ($this->splits[$head] as $component => $meta) { $splits[] = $base . $component . ':git@github.com:'. $org.'/'.$meta['repo']; } $cmd = 'git subsplit publish '; $cmd .= escapeshellarg(implode(' ', $splits)); if ($this->getNoHeads()) { $cmd .= ' --no-heads'; } else { $cmd .= ' --heads='.$head; } if ($this->getNoTags()) { $cmd .= ' --no-tags'; } else { if ($this->getTags()) { $cmd .= ' --tags=' . escapeshellarg(implode(' ', $this->getTags())); } } passthru($cmd); } } /** * Runs `git subsplit update` */ public function subsplitUpdate() { $repo = $this->getRepository(); $this->log('git-subsplit update...'); $cmd = $this->client->getCommand('subsplit'); $cmd->addArgument('update'); try { $output = $cmd->execute(); } catch (Exception $e) { throw new BuildException('git subsplit update failed'. $e); } chdir($repo . '/.subsplit'); passthru('php ../composer.phar update --dev'); chdir($repo); } /** * Runs `git subsplit init` based on the remote repository. */ public function subsplitInit() { $remote = $this->getRemote(); $cmd = $this->client->getCommand('subsplit'); $this->log('running git-subsplit init ' . $remote); $cmd->setArguments(array( 'init', $remote )); try { $output = $cmd->execute(); } catch (Exception $e) { throw new BuildException('git subsplit init failed'. $e); } $this->log(trim($output), Project::MSG_INFO); $repo = $this->getRepository(); chdir($repo . '/.subsplit'); passthru('php ../composer.phar install --dev'); chdir($repo); } /** * Find the composer.json files using Phing's directory scanner * * @return array */ protected function findSplits() { $this->log("checking heads for subsplits"); $repo = $this->getRepository(); $base = $this->getBase(); $splits = array(); $heads = $this->getHeads(); if (!empty($base)) { $base = '/' . ltrim($base, '/'); } else { $base = '/'; } chdir($repo . '/.subsplit'); foreach ($heads as $head) { $splits[$head] = array(); // check each head requested *BEFORE* the actual subtree split command gets it passthru("git checkout '$head'"); $ds = new DirectoryScanner(); $ds->setBasedir($repo . '/.subsplit' . $base); $ds->setIncludes(array('**/'.$this->subIndicatorFile)); $ds->scan(); $files = $ds->getIncludedFiles(); // Process the files we found foreach ($files as $file) { $pkg = file_get_contents($repo . '/.subsplit' . $base .'/'. $file); $pkg_json = json_decode($pkg, true); $name = $pkg_json['name']; $component = str_replace('/composer.json', '', $file); // keep this for split cmd $tmpreponame = explode('/', $name); $reponame = $tmpreponame[1]; $splits[$head][$component]['repo'] = $reponame; $nscomponent = str_replace('/', '\\', $component); $splits[$head][$component]['desc'] = "[READ ONLY] Subtree split of $nscomponent: " . $pkg_json['description']; } } // go back to how we found it passthru("git checkout master"); chdir($repo); $this->splits = $splits; } /** * Based on list of repositories we determined we *should* have, talk * to GitHub and make sure they're all there. * */ protected function verifyRepos() { $this->log('verifying GitHub target repos'); $github_org = $this->getOwningTarget()->getProject()->getProperty('github.org'); $github_creds = $this->getOwningTarget()->getProject()->getProperty('github.basicauth'); if ($github_creds == 'username:password') { $this->log('Skipping GitHub repo checks. Update github.basicauth in build.properties to verify repos.', 1); return; } $ch = curl_init('https://api.github.com/orgs/'.$github_org.'/repos?type=all'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERPWD, $github_creds); // change this when we know we can use our bundled CA bundle! curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); curl_close($ch); $repos = json_decode($result, true); $existing_repos = array(); // parse out the repos we found on GitHub foreach ($repos as $repo) { $tmpreponame = explode('/', $repo['full_name']); $reponame = $tmpreponame[1]; $existing_repos[$reponame] = $repo['description']; } $heads = $this->getHeads(); foreach ($heads as $head) { foreach ($this->splits[$head] as $component => $meta) { $reponame = $meta['repo']; if (!isset($existing_repos[$reponame])) { $this->log("Creating missing repo $reponame"); $payload = array( 'name' => $reponame, 'description' => $meta['desc'], 'homepage' => 'http://www.guzzlephp.org/', 'private' => true, 'has_issues' => false, 'has_wiki' => false, 'has_downloads' => true, 'auto_init' => false ); $ch = curl_init('https://api.github.com/orgs/'.$github_org.'/repos'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERPWD, $github_creds); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); // change this when we know we can use our bundled CA bundle! curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); echo "Response code: ".curl_getinfo($ch, CURLINFO_HTTP_CODE)."\n"; curl_close($ch); } else { $this->log("Repo $reponame exists", 2); } } } } } * @license http://claylo.mit-license.org/2012/ MIT License */ require_once 'phing/Task.php'; class NodeServerTask extends Task { protected $cmd = null; protected $action = 'start'; protected $serverfile = 'tests/Guzzle/Tests/Http/server.js'; /** * The setter for the start command * * @param string $str How to start the node server */ public function setCmd($str) { $this->cmd = $str; } public function getCmd() { return $this->cmd; } /** * The setter for the action * * @param string $str Start up or shutdown */ public function setAction($str) { $this->action = $str; } public function getAction() { return $this->action; } public function main() { $cmd = $this->getCmd(); $action = $this->getAction(); if (empty($cmd)) { throw new BuildException('"cmd" is a required parameter'); } if ($action == 'start') { $this->startServer(); } else { $this->stopServer(); } } protected function startServer() { chdir(__DIR__ . '/../..'); $serverfile = $this->serverfile; $cmd = $this->getCmd(); // resolve $HOME directory if ($cmd[0] == '~') { $cmd = $_ENV["HOME"] . substr($cmd, 1); } $fp = @fsockopen('127.0.0.1', 8124, $errno, $errstr, 1); if (! $fp) { // need to start node server $cmd = escapeshellcmd($cmd . ' ' . $serverfile); $this->log('starting node test server with '.$cmd); exec($cmd . ' &> /dev/null &'); sleep(2); $fp = @fsockopen('127.0.0.1', 8124, $errno, $errstr, 1); } // test it again if (! $fp) { $this->log('could not start node server'); } else { fclose($fp); $this->log('node test server running'); } } protected function stopServer() { exec('ps axo "pid,command"', $out); $nodeproc = false; foreach ($out as $proc) { if (strpos($proc, $this->serverfile) !== false) { $nodeproc = $proc; break; } } if ($nodeproc) { $proc = trim($nodeproc); $space = strpos($proc, ' '); $pid = substr($proc, 0, $space); $killed = posix_kill($pid, 9); if ($killed) { $this->log('test server stopped'); } else { $this->log('test server appears immortal'); } } else { $this->log('could not find test server in process list'); } } }