Ver Fonte

Initial import

Jon Wenmoth há 12 anos atrás
pai
commit
541a59a2c1

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.DS_Store

BIN
bin/phantomjs


+ 23 - 0
composer.json

@@ -0,0 +1,23 @@
+{
+    "name": "jonnyw/php-phantomjs",
+    "description": "Execute PhantomJS commands through PHP",
+    "authors": [
+        {
+            "name": "Jonny Wenmoth",
+            "email": "contact@jonnyw.me"
+        }
+    ],
+    "minimum-stability": "stable",
+    "require": {
+    	"php": ">=5.3.0"
+    },
+    "autoload": {
+        "psr-0": {
+            "JonnyW\\PhantomJs\\": "src"
+        },
+		"classmap": ["src/"]
+    },
+	"bin": [
+        "bin/phantomjs"
+    ]
+}

+ 14 - 0
phpunit.xml.dist

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit bootstrap="./test/bootstrap.php" colors="true">
+    <testsuites>
+        <testsuite name="PhantomJS Test Suite">
+            <directory suffix="Test.php">./test/</directory>
+        </testsuite>
+    </testsuites>
+    <filter>
+        <whitelist>
+            <directory suffix=".php">./src/</directory>
+        </whitelist>
+    </filter>
+</phpunit>

+ 299 - 0
src/JonnyW/PhantomJs/Client.php

@@ -0,0 +1,299 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs;
+
+use  JonnyW\PhantomJs\ClientInterface;
+use  JonnyW\PhantomJs\Exception\NoPhantomJsException;
+use  JonnyW\PhantomJs\Exception\CommandFailedException;
+use  JonnyW\PhantomJs\Exception\NotWriteableException;
+use  JonnyW\PhantomJs\Exception\InvalidUrlException;
+use  JonnyW\PhantomJs\Response;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class Client implements ClientInterface
+{
+	/**
+	 * Path to phantomJS executable
+	 *
+	 * @var string
+	 */
+	protected $phantomJS;
+	
+	/**
+	 * Client instance
+	 *
+	 * @var JonnyW\PhantomJs\ClientInterface
+	 */
+	private static $instance;
+
+	/**
+	 * Internal constructor
+	 *
+	 * @return void
+	 */
+	public function __construct()
+	{
+		$this->phantomJS 	= 'bin/phantomjs';
+		$this->timeout 		= 5000;
+	}
+	
+	/**
+	 * Get singleton instance
+	 *
+	 * @return JonnyW\PhantomJs\ClientInterface
+	 */
+	public static function getInstance()
+	{
+		if(!self::$instance instanceof ClientInterface) {
+			self::$instance = new Client();
+		}
+
+		return self::$instance;
+	}
+	
+	/**
+	 * Open page and return HTML
+	 *
+	 * @param string $url
+	 * @return string
+	 */
+	public function open($url)
+	{
+		return $this->request($url, $this->openCmd);
+	}
+	
+	/**
+	 * Screen capture URL
+	 *
+	 * @param string $url
+	 * @pram string $file
+	 * @return string
+	 */
+	public function capture($url, $file)
+	{
+		if(!is_writable(dirname($file))) {
+			throw new NotWriteableException(sprintf('Path is not writeable by PhantomJs: %s', $file));
+		}
+	
+		$cmd = sprintf($this->captureCmd, $file);
+	
+		return $this->request($url, $cmd);
+	}
+	
+	/**
+	 * Set new PhantomJs path
+	 *
+	 * @param string $path
+	 * @return JonnyW\PhantomJs\ClientInterface
+	 */
+	public function setPhantomJs($path)
+	{
+		if(!file_exists($path) || !is_executable($path)) {
+			throw new NoPhantomJsException(sprintf('PhantomJs file does not exist or is not executable: %s', $path));
+		}
+	
+		$this->phantomJS = $path;
+		
+		return $this;
+	}
+	
+	/**
+	 * Set timeout period (in milliseconds)
+	 *
+	 * @param int $period
+	 * @return JonnyW\PhantomJs\ClientInterface
+	 */
+	public function setTimeout($period)
+	{
+		$this->timeout = $period;
+		
+		return $this;
+	}
+
+	/**
+	 * Call PhantomJs command
+	 *
+	 * @param string $url
+	 * @param string $cmd
+	 * @return JonnyW\PhantomJs\Response
+	 */
+	protected function request($url, $cmd)
+	{
+		// Validate URL
+		if(!filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
+			throw new InvalidUrlException(sprintf('Invalid URL provided: %s', $url));
+		}
+	
+		// Validate PhantomJS executable
+		if(!file_exists($this->phantomJS) || !is_executable($this->phantomJS)) {
+			throw new NoPhantomJsException(sprintf('PhantomJs file does not exist or is not executable: %s', $this->phantomJS));
+		}
+	
+		try {
+			
+			$script = false;
+			
+			$data = sprintf(
+				$this->wrapper,
+				$this->timeout,
+				$url, 
+				$cmd
+			);
+	
+			$script 	= $this->writeScript($data);
+			$cmd 		= escapeshellcmd(sprintf("%s %s", $this->phantomJS, $script));
+			
+			$data = shell_exec($cmd);
+			$data = $this->parse($data);
+	
+			$this->removeScript($script);
+			
+			$response = new Response($data);
+		}
+		catch(NotWriteableException $e) {
+			throw $e;
+		}
+		catch(\Exception $e) {
+		
+			$this->removeScript($script);
+		
+			throw new CommandFailedException(sprintf('Error when executing PhantomJs command: %s - %s', $cmd, $e->getMessage()));
+		}
+
+		return $response;
+	}
+	
+	/**
+	 * Write temporary script file and
+	 * return path to file
+	 * 
+	 * @param string $data
+	 * @return JonnyW\PhantomJs\ClientInterface
+	 */
+	protected function writeScript($data)
+	{
+		$file = tempnam('/tmp', 'phantomjs');
+		
+		// Could not create tmp file
+		if(!$file || !is_writable($file)) {
+			throw new NotWriteableException('Could not create tmp file on system. Please check your tmp directory and make sure it is writeable.');
+		}
+
+		// Could not write script data to tmp file
+		if(file_put_contents($file, $data) === false) {
+		
+			$this->removeScript($file);
+		
+			throw new NotWriteableException(sprintf('Could not write data to tmp file: %s. Please check your tmp directory and make sure it is writeable.', $file));
+		}
+		
+		return $file;
+	}
+	
+	/**
+	 * Remove temporary script file
+	 *
+	 * @param string $file
+	 * @return JonnyW\PhantomJs\ClientInterface
+	 */
+	protected function removeScript($file)
+	{
+		if($file && file_exists($file)) {
+			unlink($file);
+		}
+		
+		return $this;
+	}
+	
+	/**
+	 * If data from JSON string format
+	 * and return array
+	 * 
+	 * @param string $data
+	 * @return array
+	 */
+	protected function parse($data)
+	{
+		// Data is invalid
+		if($data === null || !is_string($data)) {
+			return array();
+		}
+		
+		// Not a JSON string
+		if(substr($data, 0, 1) !== '{') {
+			return array();
+		}
+		
+		// Return decoded JSON string
+		return (array) json_decode($data, true);
+	}
+	
+	/**
+	 * PhantomJs base wrapper
+	 *
+	 * @var string
+	 */
+	protected $wrapper = <<<EOF
+	
+	var page = require('webpage').create(),
+		response = {};
+
+	page.settings.resourceTimeout = %1\$s;		
+	page.onResourceTimeout = function(e) {
+		response 		= e;
+		response.status = e.errorCode;
+	};
+	
+	page.onResourceReceived = function (r) {
+		if(!response.status) response = r;
+	};
+	
+	page.open('%2\$s', function (status) {
+	
+		if(status === 'success') {
+			%3\$s
+		}
+
+		console.log(JSON.stringify(response, undefined, 4));
+		phantom.exit();
+	});
+EOF;
+	
+	/**
+	 * PhantomJs screen capture 
+	 * command template
+	 *
+	 * @var string
+	 */
+	protected $captureCmd = <<<EOF
+	
+			page.render('%1\$s');
+	
+			response.content = page.evaluate(function () {
+				return document.getElementsByTagName('html')[0].innerHTML
+			});
+EOF;
+
+	/**
+	 * PhantomJs page open
+	 * command template
+	 *
+	 * @var string
+	 */
+	protected $openCmd = <<<EOF
+	
+			response.content = page.evaluate(function () {
+				return document.getElementsByTagName('html')[0].innerHTML
+			});
+EOF;
+}

+ 43 - 0
src/JonnyW/PhantomJs/ClientInterface.php

@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ClientInterface
+{
+	
+	/**
+	 * Open page and return HTML
+	 *
+	 * @param string $url
+	 * @return string
+	 */
+	public function open($url);
+	
+	/**
+	 * Screen capture URL
+	 *
+	 * @param string $url
+	 * @pram string $file
+	 * @return string
+	 */
+	public function capture($url, $file);
+	
+	/**
+	 * Set new PhantomJs path
+	 *
+	 * @param string $path
+	 * @return JonnyW\PhantomJs\ClientInterface
+	 */
+	public function setPhantomJs($path);
+}

+ 19 - 0
src/JonnyW/PhantomJs/Exception/CommandFailedException.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Exception;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class CommandFailedException extends \Exception
+{
+
+}

+ 19 - 0
src/JonnyW/PhantomJs/Exception/InvalidUrlException.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Exception;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class InvalidUrlException extends \Exception
+{
+
+}

+ 19 - 0
src/JonnyW/PhantomJs/Exception/NoPhantomJsException.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Exception;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class NoPhantomJsException extends \Exception
+{
+
+}

+ 19 - 0
src/JonnyW/PhantomJs/Exception/NotWriteableException.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Exception;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class NotWriteableException extends \Exception
+{
+
+}

+ 229 - 0
src/JonnyW/PhantomJs/Response.php

@@ -0,0 +1,229 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs;
+
+use JonnyW\PhantomJs\ResponseInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class Response implements ResponseInterface
+{	
+	/**
+	 * Http headers array
+	 *
+	 * @var array
+	 */
+	protected $headers;
+	
+	/**
+	 * Response int
+	 *
+	 * @var string
+	 */
+	protected $status;
+	
+	/**
+	 * Response body
+	 *
+	 * @var string
+	 */
+	protected $content;
+	
+	/** 
+	 * Response content type header
+	 * 
+	 * @var string
+	 */
+	protected $contentType;
+	
+	/**
+	 * Requested URL
+	 *
+	 * @var string
+	 */
+	protected $url;
+	
+	/**
+	 * Redirected URL
+	 *
+	 * @var string
+	 */
+	protected $redirectUrl;
+	
+	/**
+	 * Request time string
+	 * 
+	 * @var string
+	 */
+	protected $time;
+	
+	/** 
+	 * Internal constructor
+	 *
+	 * @return void
+	 */
+	public function __construct(array $data)
+	{
+		$this->headers = array();
+
+		// Set headers array
+		if(isset($data['headers'])) {
+			$this->setHeaders((array) $data['headers']);
+		}
+		
+		// Set status
+		if(isset($data['status'])) {
+			$this->status = $data['status'];
+		}
+		
+		// Set content
+		if(isset($data['content'])) {
+			$this->content = $data['content'];
+		}
+		
+		// Set content type string
+		if(isset($data['contentType'])) {
+			$this->contentType = $data['contentType'];
+		}
+				
+		// Set request URL
+		if(isset($data['url'])) {
+			$this->url = $data['url'];
+		}
+		
+		// Set redirect URL
+		if(isset($data['redirectURL'])) {
+			$this->redirectUrl = $data['redirectURL'];
+		}
+		
+		// Set time string
+		if(isset($data['time'])) {
+			$this->time = $data['time'];
+		}
+	}
+	
+	/**
+	 * Set headers array
+	 *
+	 * @param array $headers
+	 * @return 
+	 */
+	protected function setHeaders(array $headers)
+	{
+		foreach($headers as $header) {
+			
+			if(isset($header['name']) && isset($header['value'])) {
+				$this->headers[$header['name']] = $header['value'];
+			}
+		}
+		
+		return $this;
+	}
+	
+	/** 
+	 * Get HTTP headers array
+	 *
+	 * @return array
+	 */
+	public function getHeaders()
+	{
+		return (array) $this->headers;	
+	}
+	
+	/**
+	 * Get HTTP header value for code 
+	 *
+	 * @praam string $$code
+	 * @return mixed
+	 */
+	public function getHeader($code)
+	{
+		if(isset($this->headers[$code])) {
+			return $this->headers[$code];
+		}
+		
+		return null;
+	}
+	
+	/**
+	 * Get response status code
+	 *
+	 * @return int|null
+	 */
+	public function getStatus()
+	{
+		return (int) $this->status;
+	}
+	
+	/**
+	 * Get page content from respone
+	 *
+	 * @return string
+	 */
+	public function getContent()
+	{
+		return $this->content;
+	}
+	
+	/**
+	 * Get content type header
+	 *
+	 * @return string
+	 */
+	public function getContentType()
+	{
+		return $this->contentType;
+	}
+	
+	/** 
+	 * Get request URL
+	 *
+	 * @return string
+	 */
+	public function getUrl()
+	{
+		return $this->url;
+	}
+	
+	/**
+	 * Get redirect URL (if redirected)
+	 *
+	 * @return string
+	 */
+	public function getRedirectUrl()
+	{
+		return $this->redirectUrl;
+	}
+	
+	/**
+	 * Is response a redirect
+	 *  - Checks status codes
+	 *
+	 * @return boolean
+	 */
+	public function isRedirect()
+	{
+		$status = $this->getStatus();
+	
+		return (bool) ($status >= 300 && $status < 307);
+	}
+	
+	/**
+	 * Get time string
+	 *
+	 * @return string
+	 */
+	public function getTime()
+	{
+		return $this->time;
+	}
+}

+ 83 - 0
src/JonnyW/PhantomJs/ResponseInterface.php

@@ -0,0 +1,83 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+ 
+namespace JonnyW\PhantomJs;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ResponseInterface
+{	
+	/** 
+	 * Get HTTP headers array
+	 *
+	 * @return array
+	 */
+	public function getHeaders();
+	
+	/**
+	 * Get HTTP header value for code 
+	 *
+	 * @praam string $$code
+	 * @return mixed
+	 */
+	public function getHeader($code);
+	
+	/**
+	 * Get response status code
+	 *
+	 * @return int|null
+	 */
+	public function getStatus();
+	
+	/**
+	 * Get page content from respone
+	 *
+	 * @return string
+	 */
+	public function getContent();
+	
+	/**
+	 * Get content type header
+	 *
+	 * @return string
+	 */
+	public function getContentType();
+	
+	/** 
+	 * Get request URL
+	 *
+	 * @return string
+	 */
+	public function getUrl();
+	
+	/**
+	 * Get redirect URL (if redirected)
+	 *
+	 * @return string
+	 */
+	public function getRedirectUrl();
+	
+	/**
+	 * Is response a redirect
+	 *  - Checks status codes
+	 *
+	 * @return boolean
+	 */
+	public function isRedirect();
+	
+	/**
+	 * Get time string
+	 *
+	 * @return string
+	 */
+	public function getTime();
+}

+ 142 - 0
test/JonnyW/PhantomJs/Test/ClientTest.php

@@ -0,0 +1,142 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Test;
+
+use JonnyW\PhantomJs\Client;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ClientTest extends \PHPUnit_Framework_TestCase
+{
+	/**
+	 * Setup tests
+	 *
+	 * @return void
+	 */
+	protected function setUp()
+    {
+        parent::setUp();
+        
+        $this->client = Client::getInstance();
+    }
+
+	/**
+	 * Test invalid URL's throw exception
+	 *
+     * @dataProvider provideInvalidHosts
+     *
+     * @param string $host
+	 * @return void
+     */
+    public function testInvalidUrl($host)
+    {
+        $this->setExpectedException('JonnyW\\PhantomJs\\Exception\\InvalidUrlException');
+
+        $this->client->open($host);
+    }
+	
+	/**
+	 * Invalid host data providers
+	 *
+	 * @return array
+	 */
+    public function provideInvalidHosts()
+    {
+        return array(
+            array('invalid_url'),
+            array('invalid_url.test')
+        );
+    }
+    
+    /**
+     * Test exception is thrown when 
+     * PhantomJS executable cannot be run
+     *
+     * @return void
+     */
+    public function testBinNotExecutable()
+    {
+	    $this->setExpectedException('JonnyW\\PhantomJs\\Exception\\NoPhantomJsException');
+
+        $this->client->setPhantomJs('/path/does/not/exist/phantomjs');
+    }
+
+	/**
+     * Test exception is thrown when capture
+     * path is not writeable
+     *
+     * @return void
+     */
+    public function testPathNotWriteable()
+    {
+	    $this->setExpectedException('JonnyW\\PhantomJs\\Exception\\NotWriteableException');
+
+        $this->client->capture('http://google.com', 'path/does/not/exist/phantoms.png');
+    }
+
+	/**
+	 * Test open page
+	 *
+	 * @return void
+	 */
+	public function testOpenPage()
+	{
+		$client 	= $this->getMock('JonnyW\PhantomJs\Client', array('request'));
+		$response 	= $this->getMock('JonnyW\PhantomJs\Response', null, array(array('content' => 'test')));
+		
+        $client->expects($this->once())
+            ->method('request')
+            ->will($this->returnValue($response));
+		
+		$actual = $client->open('http://jonnyw.me');
+		
+		$this->assertSame($response, $actual);
+	}
+	
+	/**
+	 * Test screen capture page
+	 *
+	 * @return void
+	 */
+	public function testCapturePage()
+	{
+		$client 	= $this->getMock('JonnyW\PhantomJs\Client', array('request'));
+		$response 	= $this->getMock('JonnyW\PhantomJs\Response', null, array(array('content' => 'test')));
+		
+        $client->expects($this->once())
+            ->method('request')
+            ->will($this->returnValue($response));
+		
+		$actual = $client->capture('http://jonnyw.me', '/tmp/testing.png');
+		
+		$this->assertSame($response, $actual);
+	}
+	
+	/**
+	 * Test page is redirect
+	 *
+	 * @return void
+	 */
+	public function testRedirectPage()
+	{
+		$client 	= $this->getMock('JonnyW\PhantomJs\Client', array('request'));
+		$response 	= $this->getMock('JonnyW\PhantomJs\Response', null, array(array('content' => 'test', 'status' => 301, 'redirectUrl'	=> 'http://google.com')));
+		
+        $client->expects($this->once())
+            ->method('request')
+            ->will($this->returnValue($response));
+		
+		$actual = $client->open('http://jonnyw.me');
+		
+		$this->assertTrue($response->isRedirect());
+	}
+}

+ 15 - 0
test/bootstrap.php

@@ -0,0 +1,15 @@
+<?php
+
+if(!$loader = @include __DIR__.'/../vendor/autoload.php') {
+    echo <<<EOM
+You must set up the project dependencies by running the following commands:
+
+    curl -s http://getcomposer.org/installer | php
+    php composer.phar install
+
+EOM;
+
+    exit(1);
+}
+
+$loader->add('JonnyW\PhantomJs\Test', __DIR__);