├── .gitignore
├── dist
└── reload.phar
├── .travis.yml
├── src
├── Response
│ ├── ResponseWebSocketFrame.php
│ ├── Response.php
│ └── ResponseWebSocket.php
├── Protocol
│ ├── WebSocket
│ │ ├── MessageQueue.php
│ │ ├── WebSocket.php
│ │ └── Frame.php
│ ├── WebSocketProtocol.php
│ ├── LivereloadProtocol.php
│ └── HttpProtocol.php
├── Command
│ ├── InitCommand.php
│ └── RunCommand.php
└── Application
│ └── ServerApplication.php
├── box.json
├── Tests
├── Response
│ ├── ResponseWebSocketFrameTest.php
│ └── ResponseWebSocketTest.php
├── Protocol
│ ├── WebSocket
│ │ ├── WebSocketTest.php
│ │ ├── MessageQueueTest.php
│ │ └── FrameTest.php
│ └── HttpProtocolTest.php
└── Application
│ └── ServerApplicationTest.php
├── composer.json
├── phpunit.xml.dist
├── bin
└── reload
├── LICENSE
├── README.md
└── web
└── js
└── livereload.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /phpunit.xml
3 | /build
4 | /composer.lock
5 |
--------------------------------------------------------------------------------
/dist/reload.phar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RickySu/php-livereload/HEAD/dist/reload.phar
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 5.4
4 | - 5.5
5 | - 5.6
6 | - hhvm
7 | before_script:
8 | - composer self-update
9 | - composer install
10 | - composer require satooshi/php-coveralls
11 | script:
12 | - phpunit
13 | after_script:
14 | - php vendor/bin/coveralls -v
15 |
--------------------------------------------------------------------------------
/src/Response/ResponseWebSocketFrame.php:
--------------------------------------------------------------------------------
1 | frame = $frame;
18 | }
19 |
20 | public function __toString()
21 | {
22 | return $this->frame->encode();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/box.json:
--------------------------------------------------------------------------------
1 | {
2 | "chmod": "0755",
3 | "directories": ["src"],
4 | "extract": true,
5 | "files": [
6 | "LICENSE",
7 | "web/js/livereload.js"
8 | ],
9 | "finder": [
10 | {
11 | "name": "*.php",
12 | "exclude": [
13 | "Tests"
14 | ],
15 | "in": "vendor"
16 | }
17 | ],
18 | "git-commit": "git-commit",
19 | "git-version": "git-version",
20 | "main": "bin/reload",
21 | "output": "dist/reload.phar",
22 | "web": false,
23 | "stub": true
24 | }
25 |
--------------------------------------------------------------------------------
/src/Response/Response.php:
--------------------------------------------------------------------------------
1 | headers->set('Content-Type', "$type; charset=$charset");
17 | }
18 |
19 | public function __toString()
20 | {
21 | $this->headers->set('Content-Length', strlen($this->getContent()));
22 |
23 | return parent::__toString();
24 | }
25 | }
--------------------------------------------------------------------------------
/Tests/Response/ResponseWebSocketFrameTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($frame->encode(), (string) $response);
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/Tests/Response/ResponseWebSocketTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(101, $response->getStatusCode());
17 | $this->assertEquals('Upgrade', $response->headers->get('Connection'));
18 | $this->assertEquals('websocket', $response->headers->get('Upgrade'));
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/Response/ResponseWebSocket.php:
--------------------------------------------------------------------------------
1 | headers->set('Connection', 'Upgrade');
17 | $this->headers->set('Upgrade', 'websocket');
18 | $this->headers->remove('Content-Length');
19 | $this->headers->remove('Cache-Control');
20 | $this->headers->remove('Date');
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rickysu/php-livereload",
3 | "description": "a livereload server implement by php",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Ricky Su",
8 | "email": "ricky@ez2.us"
9 | }
10 | ],
11 | "require": {
12 | "symfony/console": "^2.6||^3.0",
13 | "react/socket": "^0.4.0",
14 | "symfony/finder": "^2.6||^3.0",
15 | "symfony/http-foundation": "^2.6||^3.0"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit": "^4.8"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "PHPLivereload\\": "src"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "PHPLivereload\\Tests\\": "Tests"
28 | }
29 | },
30 | "bin": ["bin/reload"]
31 | }
32 |
--------------------------------------------------------------------------------
/src/Protocol/WebSocket/MessageQueue.php:
--------------------------------------------------------------------------------
1 | data = $data;
16 | }
17 |
18 | public function clear()
19 | {
20 | $this->data = '';
21 | }
22 | public function add($data)
23 | {
24 | $this->data.=$data;
25 | }
26 |
27 | public function __toString()
28 | {
29 | return $this->getAll();
30 | }
31 |
32 | public function getAll()
33 | {
34 | return $this->data;
35 | }
36 |
37 | public function shift($size)
38 | {
39 | if ($size > strlen($this->data)) {
40 | return false;
41 | }
42 |
43 | $message = substr($this->data, 0, $size);
44 |
45 | $this->data = substr($this->data, $size);
46 |
47 | return $message;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ./Tests/
16 |
17 |
18 |
19 |
20 |
21 | ./
22 |
23 | ./Tests
24 | ./vendor
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/bin/reload:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | files()->in(__DIR__.'/../src/Command/');
25 |
26 | $application = new Application('php livereload', $appVersion);
27 |
28 | foreach ($finder as $file) {
29 | $class = 'PHPLivereload\\Command\\'.substr($file->getFileName(), 0, - (strlen($file->getExtension())+1));
30 | $application->add(new $class);
31 | }
32 |
33 | $application->run();
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Ricky Su
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Tests/Protocol/WebSocket/WebSocketTest.php:
--------------------------------------------------------------------------------
1 | headers->set('Upgrade', 'websocket');
22 | $request->headers->set('Sec-WebSocket-Version', 13);
23 | $request->headers->set('Sec-WebSocket-Key', $key);
24 | $websocket = new WebSocket();
25 | $response = $websocket->handshake($request);
26 | $this->assertTrue($response instanceof ResponseWebSocket);
27 | $this->assertEquals($acceptKey, $response->headers->get('Sec-WebSocket-Accept'));
28 | }
29 |
30 | public function provider_test_getHandshakeReponse()
31 | {
32 | return array(
33 | array('8MXSz0cH4mjNrI2d+w9Mbw==', 'JDvj7/uPnMEWPrAUvYR9SD+T2XI='),
34 | array('MAMGHM22VDXYF+s6CW2RUw==', 'TG3bsTWUBOalA6WXocR+xP4DSA0='),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Command/InitCommand.php:
--------------------------------------------------------------------------------
1 | setName('livereload:init')
16 | ->setAliases(array('init'))
17 | ->setDescription('Initialize livereload.json.')
18 | ->addOption('force', '-f', InputOption::VALUE_NONE, 'force rewrite livereload.json')
19 | ;
20 | }
21 |
22 | protected function execute(InputInterface $input, OutputInterface $output)
23 | {
24 | $forceRewrite = $input->getOption('force');
25 | if(!$forceRewrite && file_exists('livereload.json')){
26 | $output->writeln("livereload.json file exists.\nplease use --force to overwrite.");
27 | return;
28 | }
29 | $this->writeConfig($input, $output);
30 | }
31 |
32 | protected function writeConfig(InputInterface $input, OutputInterface $output)
33 | {
34 | $json = <<writeln("livereload.json is generated.");
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Protocol/WebSocket/WebSocket.php:
--------------------------------------------------------------------------------
1 | messageQueue = new MessageQueue();
21 | }
22 |
23 | /**
24 | * @return Frame
25 | */
26 | public function onMessage($data)
27 | {
28 | $this->messageQueue->add($data);
29 |
30 | return Frame::parse($this->messageQueue);
31 | }
32 |
33 | public function handshake($request)
34 | {
35 | $key = $request->headers->get('Sec-WebSocket-Key');
36 | if(
37 | strtolower($request->headers->get('Upgrade')) != 'websocket' ||
38 | !$this->checkProtocolVrsion($request) ||
39 | !$this->checkSecKey($key)
40 | ){
41 | return false;
42 | }
43 | $acceptKey = $this->generateAcceptKey($key);
44 | $response = new ResponseWebSocket();
45 | $response->headers->set('Sec-WebSocket-Accept', $acceptKey);
46 |
47 | return $response;
48 | }
49 |
50 | protected function generateAcceptKey($key)
51 | {
52 | return base64_encode(sha1($key.static::MAGIC_GUID, true));
53 | }
54 |
55 | protected function checkSecKey($key)
56 | {
57 | return strlen(base64_decode($key)) == 16;
58 | }
59 |
60 | protected function checkProtocolVrsion($request)
61 | {
62 | return $request->headers->get('Sec-WebSocket-Version', 0) >= static::MIN_WS_VERSION;
63 | }
64 |
65 | //put your code here
66 | }
67 |
--------------------------------------------------------------------------------
/src/Protocol/WebSocketProtocol.php:
--------------------------------------------------------------------------------
1 | app = $app;
19 | $this->conn = $conn;
20 | $this->initEvent();
21 | $this->handshake($request);
22 | $this->app->getOutput()->writeln(strftime('%T')." - info - Browser connected", OutputInterface::VERBOSITY_VERBOSE);
23 | new LivereloadProtocol($conn, $app);
24 | }
25 |
26 | protected function handshake(HttpFoundation\Request $request)
27 | {
28 | if (!($handshakeResponse = $this->websocket->handshake($request))) {
29 | $this->conn->write(new Response('bad protocol', 400), true);
30 | return;
31 | }
32 | $this->conn->write($handshakeResponse);
33 | }
34 |
35 | protected function initEvent()
36 | {
37 | $this->websocket = new WebSocket\WebSocket();
38 | $this->conn->on('data', function($data){
39 | $this->onData($data);
40 | });
41 | }
42 |
43 | protected function onData($data)
44 | {
45 | $frame = $this->websocket->onMessage($data);
46 | if(!($frame instanceof WebSocket\Frame)) {
47 | return;
48 | }
49 | if(($command = json_decode($frame->getData(), true)) === null){
50 | return;
51 | }
52 | $this->conn->emit('command', array($command));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/RunCommand.php:
--------------------------------------------------------------------------------
1 | setName('server:run')
16 | ->setAliases(array('sr'))
17 | ->setDescription('Starts a live reload server.')
18 | ->addArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:35729')
19 | ->addOption('config', '-c', InputOption::VALUE_OPTIONAL, 'Path to livereload.json')
20 | ->addOption('no-watch', '', InputOption::VALUE_NONE, 'Disable watching')
21 | ;
22 | }
23 |
24 | protected function execute(InputInterface $input, OutputInterface $output)
25 | {
26 | $noWatching = $input->getOption('no-watch');
27 | $address = $input->getArgument('address', '127.0.0.1:35729');
28 | list($host, $port) = explode(':', $address);
29 | $output->writeln(sprintf("Server running on http://%s\n", $input->getArgument('address')));
30 | $output->writeln('Quit the server with CONTROL-C.');
31 | $output->writeln('');
32 | $app = new ServerApplication($host, $port);
33 | if(!$noWatching){
34 | $config = $this->loadConfig($input, $output);
35 | $app->watching($config['period'], $config);
36 | }
37 | $app->setOutput($output);
38 | $app->run();
39 | }
40 |
41 | protected function loadConfig(InputInterface $input, OutputInterface $output)
42 | {
43 | $configFile = $input->getOption('config');
44 | if($configFile === null){
45 | $configFile = 'livereload.json';
46 | }
47 | if(!file_exists($configFile)){
48 | throw new \Exception("$configFile not found.");
49 | }
50 |
51 | $config = json_decode(file_get_contents($configFile), true);
52 | return $config;
53 | }
54 | }
--------------------------------------------------------------------------------
/Tests/Protocol/WebSocket/MessageQueueTest.php:
--------------------------------------------------------------------------------
1 | $property;
12 | }
13 | public function __set($name, $value)
14 | {
15 | $property = str_replace('_','', $name);
16 | $this->$property = $value;
17 | }
18 | }
19 |
20 | /**
21 | * Description of Message
22 | *
23 | * @author ricky
24 | */
25 | class MessageQueueTest extends \PHPUnit_Framework_TestCase
26 | {
27 |
28 | protected $messageQueue;
29 |
30 | protected function setUp()
31 | {
32 | $this->messageQueue = new myMessageQueue();
33 | }
34 |
35 | public function test_add()
36 | {
37 | $token = md5(microtime().rand());
38 | $this->messageQueue->add($token);
39 | $this->messageQueue->add($token);
40 | $this->assertEquals($token.$token, $this->messageQueue->_data);
41 | }
42 |
43 | public function test_clear()
44 | {
45 | $token = md5(microtime().rand());
46 | $this->messageQueue->add($token);
47 | $this->messageQueue->clear();
48 | $this->assertEmpty($this->messageQueue->_data);
49 | }
50 |
51 | public function test_getAll()
52 | {
53 | $token = md5(microtime().rand());
54 | $this->messageQueue->clear();
55 | $this->messageQueue->add($token);
56 | $this->assertEquals($token, $this->messageQueue->getAll());
57 | }
58 |
59 | public function test__toString()
60 | {
61 | $token = md5(microtime().rand());
62 | $this->messageQueue->clear();
63 | $this->messageQueue->add($token);
64 | $this->assertEquals($this->messageQueue, $this->messageQueue->getAll());
65 | }
66 |
67 | public function test_shift()
68 | {
69 | $token = md5(microtime().rand());
70 | $this->messageQueue->clear();
71 | $this->messageQueue->add($token);
72 | $part = $this->messageQueue->shift(4);
73 | $this->assertEquals(substr($token, 0, 4), $part);
74 | $this->assertEquals(substr($token, 4), $this->messageQueue->getAll());
75 | }
76 |
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/src/Protocol/LivereloadProtocol.php:
--------------------------------------------------------------------------------
1 | conn = $conn;
18 | $this->app = $app;
19 | $this->app->addClient($this);
20 | $this->initEvent();
21 | }
22 |
23 | public function reload($file, $config)
24 | {
25 | $this->app->getOutput()->writeln(strftime('%T')." - info - Browser reload $file", OutputInterface::VERBOSITY_VERBOSE);
26 | $this->sendCommand(array(
27 | 'command' => 'reload',
28 | 'path' => $file,
29 | 'liveCSS' => $config['liveCSS'],
30 | ));
31 | }
32 |
33 | protected function shutdown()
34 | {
35 | $this->conn->removeAllListeners();
36 | $this->app->getOutput()->writeln(strftime('%T')." - info - Browser disconnected", OutputInterface::VERBOSITY_VERBOSE);
37 | $this->app->removeClient($this);
38 | unset($this->app);
39 | unset($this->conn);
40 | }
41 |
42 | protected function initEvent()
43 | {
44 | $this->conn->on('command', function($command){
45 | $this->dispatchCommand($command);
46 | });
47 | $this->conn->on('close', function(){
48 | $this->shutdown();
49 | });
50 | }
51 |
52 | protected function sendRaw($data)
53 | {
54 | $response = new ResponseWebSocketFrame(WebSocket\Frame::generate($data));
55 | $this->conn->write($response);
56 | }
57 |
58 | protected function sendCommand($command)
59 | {
60 | $this->sendRaw(json_encode($command));
61 | }
62 |
63 | protected function dispatchCommand($command)
64 | {
65 | switch($command['command']){
66 | case 'hello':
67 | $this->processCommandHello($command);
68 | break;
69 | default:
70 | //$this->conn->end();
71 | }
72 | }
73 |
74 | protected function processCommandHello($command)
75 | {
76 | if($this->connected){
77 | return;
78 | }
79 | $this->app->getOutput()->writeln(strftime('%T')." - info - Livereload protocol initialized.", OutputInterface::VERBOSITY_VERBOSE);
80 | $this->connected = true;
81 | $this->sendCommand(array(
82 | 'command' => 'hello',
83 | 'protocols' => array(
84 | 'http://livereload.com/protocols/official-7',
85 | ),
86 | 'serverName' => 'php-livereload',
87 | ));
88 | }
89 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Livereload
2 |
3 | [](https://travis-ci.org/RickySu/php-livereload)
4 | [](https://coveralls.io/r/RickySu/php-livereload?branch=master)
5 |
6 | php-livereload is a livereload server written in PHP.
7 |
8 | php-livereload uses [livereload.js](https://github.com/livereload/livereload-js) -- a JavaScript file implementing the client side of the LiveReload protocol.
9 |
10 | ## Install
11 |
12 | Install php-livereload from [composer](http://getcomposer.org).
13 |
14 | ```JSON
15 | {
16 | "require": {
17 | "rickysu/php-livereload": "dev-master"
18 | }
19 | }
20 | ```
21 |
22 | Get the command-line php-livereload
23 |
24 | $ curl -O https://raw.github.com/RickySu/php-livereload/master/dist/reload.phar
25 | $ chmod +x reload.phar
26 | $ sudo mv reload.phar /usr/bin
27 |
28 | Install [LiveReload Safari/Chrome/Firefox extension](http://feedback.livereload.com/knowledgebase/articles/86242-how-do-i-install-and-use-the-browser-extensions-)
29 |
30 | ## Tests
31 |
32 | To run the test suite, you need install the dependencies via composer, then
33 | run PHPUnit.
34 |
35 | $ composer install
36 | $ phpunit
37 |
38 | ## Using php-livereload
39 | define a livereload.json in your project root.
40 |
41 | livereload.json
42 |
43 | ```JSON
44 | {
45 | "period": 1,
46 | "watch": {
47 | "web/css/": "*.css",
48 | "web/js/": "*.js",
49 | "web/img/": "\\.png|gif|jpg$"
50 | }
51 | }
52 | ```
53 |
54 | * period: monitor file changes every 1 second.
55 | * watch: file and folder you want to watch
56 |
57 | #### Initialize a default livereload.json file.
58 |
59 | ```
60 | $ php bin/reload livereload:init
61 | ```
62 |
63 | #### Running Server.
64 |
65 | ```
66 | $ php bin/reload server:run
67 | ```
68 |
69 | #### Rolling Your Own Live Reload
70 |
71 | If you would like to trigger the live reload server yourself, simply POST files to the URL: `http://localhost:35729/changed`.
72 | Or if you rather roll your own live reload implementation use the following example:
73 |
74 | ```
75 | # notify a single change
76 | curl http://localhost:35729/changed?files=style.css
77 |
78 | # notify using a longer path
79 | curl http://localhost:35729/changed?files=js/app.js
80 |
81 | # notify multiple changes, comma or space delimited
82 | curl http://localhost:35729/changed?files=index.html,style.css,docs/docco.css
83 | ```
84 |
85 | Or you can bulk the information into a POST request, with body as a JSON array of files.
86 |
87 | ```
88 | curl -X POST http://localhost:35729/changed -d '{ "files": ["style.css", "app.js"] }'
89 |
90 | # from a JSON file
91 | node -pe 'JSON.stringify({ files: ["some.css", "files.css"] })' > files.json
92 | curl -X POST -d @files.json http://localhost:35729
93 | ```
94 |
95 | ## License
96 |
97 | MIT, see LICENSE.
98 |
--------------------------------------------------------------------------------
/Tests/Protocol/WebSocket/FrameTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($frameData, $closeFrame->encode());
19 | }
20 |
21 | /**
22 | * @dataProvider provider_test_generate
23 | */
24 | public function test_generate($expededEncodedData, $data)
25 | {
26 | $frame = Frame::generate($data);
27 | $this->assertEquals(md5($expededEncodedData), md5($frame->encode()));
28 | }
29 |
30 | public function provider_test_generate()
31 | {
32 | $tinyData = md5(microtime().rand());
33 | $tinyDataEncoded = pack("CC", 0x81, strlen($tinyData)).$tinyData;
34 | $mediumData = str_pad(md5(microtime().rand()), rand(126, 65535), md5(microtime().rand())); //16 bit
35 | $mediumDataEncoded = pack("CCn", 0x81, 126, strlen($mediumData)).$mediumData;
36 | $hugeData = str_pad(md5(microtime().rand()), rand(65536, 100000), md5(microtime().rand())); //64 bit
37 | $hugeDataEncoded = pack("CCNN", 0x81, 127, strlen($hugeData)>>32, strlen($hugeData) & 0xffffffff).$hugeData;
38 | return array(
39 | array($tinyDataEncoded, $tinyData),
40 | array($mediumDataEncoded, $mediumData),
41 | array($hugeDataEncoded, $hugeData),
42 | );
43 | }
44 |
45 | public function provider_test_parse()
46 | {
47 | $applyMask = function($maskingKey, $data)
48 | {
49 | $applied = '';
50 | $maskingKeyLength = strlen($maskingKey);
51 | for ($i = 0, $len = strlen($data); $i < $len; $i++) {
52 | $applied .= $data[$i] ^ $maskingKey[$i % $maskingKeyLength];
53 | }
54 | return $applied;
55 | };
56 |
57 | $maskgingKey = pack("nn", rand(0, 65535), rand(0, 65535));
58 | $tinyData = md5(microtime().rand());
59 | $tinyDataEncoded = pack("CC", 0x81, strlen($tinyData)|0x80).$maskgingKey.$applyMask($maskgingKey, $tinyData);
60 |
61 | $maskgingKey = pack("nn", rand(0, 65535), rand(0, 65535));
62 | $mediumData = str_pad(md5(microtime().rand()), rand(126, 65535), md5(microtime().rand())); //16 bit
63 | $mediumDataEncoded = pack("CCn", 0x81, 126|0x80, strlen($mediumData)).$maskgingKey.$applyMask($maskgingKey, $mediumData);
64 |
65 | $maskgingKey = pack("nn", rand(0, 65535), rand(0, 65535));
66 | $hugeData = str_pad(md5(microtime().rand()), rand(65536, 100000), md5(microtime().rand())); //64 bit
67 | $hugeDataEncoded = pack("CCNN", 0x81, 127|0x80, strlen($hugeData)>>32, strlen($hugeData) & 0xffffffff).$maskgingKey.$applyMask($maskgingKey, $hugeData);
68 | return array(
69 | array($tinyDataEncoded, $tinyData),
70 | array($mediumDataEncoded, $mediumData),
71 | array($hugeDataEncoded, $hugeData),
72 | );
73 | }
74 |
75 | /**
76 | * @dataProvider provider_test_parse
77 | */
78 | public function test_parse($encodedData, $expededData)
79 | {
80 | $frame = Frame::parse($encodedData);
81 | $this->assertEquals(md5($expededData), md5($frame->getData()));
82 | }
83 |
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/src/Application/ServerApplication.php:
--------------------------------------------------------------------------------
1 | true,
17 | );
18 | protected $watchConfig;
19 | protected $watchingFiles = array();
20 |
21 | public function __construct($host = '127.0.0.1', $port = 35729)
22 | {
23 | $this->initLoop();
24 | $this->initServer($host, $port);
25 | }
26 |
27 | public function setOutput(OutputInterface $output)
28 | {
29 | $this->output = $output;
30 | }
31 |
32 | /**
33 | *
34 | * @return OutputInterface
35 | */
36 | public function getOutput()
37 | {
38 | return $this->output;
39 | }
40 |
41 | public function run()
42 | {
43 | $this->loop->run();
44 | }
45 |
46 | public function getConfig()
47 | {
48 | return $this->config;
49 | }
50 |
51 | public function watching($time, $config)
52 | {
53 | $this->watchConfig = $config;
54 | $this->scanFiles();
55 | $this->loop->addPeriodicTimer($time, function(){
56 | $this->watchingFileChange();
57 | });
58 | }
59 |
60 | protected function scanFiles($scanNewFile = true)
61 | {
62 | foreach($this->watchConfig['watch'] as $path => $file){
63 | $finder = new Finder();
64 | try{
65 | foreach($finder->in($path)->name($file)->followLinks() as $file){
66 | if($file->getRealPath() && !isset($this->watchingFiles[$file->getRealpath()])){
67 | $this->watchingFiles[$file->getRealpath()] = $scanNewFile?$file->getMTime():0;
68 | }
69 | }
70 | }
71 | catch(\InvalidArgumentException $e){
72 | continue;
73 | }
74 | }
75 | }
76 |
77 | protected function watchingFileChange()
78 | {
79 | $this->scanFiles(false);
80 | foreach($this->watchingFiles as $file => $time){
81 | $mtime = @filemtime($file);
82 | if($mtime && $mtime > $time){
83 | $this->watchingFiles[$file] = $mtime;
84 | $this->reloadFile($file);
85 | }
86 | }
87 | }
88 |
89 | protected function initLoop()
90 | {
91 | $this->loop = LoopFactory::create();
92 | }
93 |
94 | protected function initServer($host, $port)
95 | {
96 | $socket = new SocketServer($this->loop);
97 | $socket->listen($port, $host);
98 | return new Protocol\HttpProtocol($socket, $this);
99 | }
100 |
101 | public function addClient(Protocol\LivereloadProtocol $client)
102 | {
103 | if(!in_array($client, $this->clients)){
104 | $this->clients[] = $client;
105 | }
106 | }
107 |
108 | public function removeClient(Protocol\LivereloadProtocol $client)
109 | {
110 | $index = array_search($client, $this->clients, true);
111 | if($index === false){
112 | return;
113 | }
114 | unset($this->clients[$index]);
115 | }
116 |
117 | public function reloadFile($file)
118 | {
119 | foreach($this->clients as $client){
120 | $client->reload($file, $this->config);
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/src/Protocol/HttpProtocol.php:
--------------------------------------------------------------------------------
1 | app = $app;
18 | $this->initEvent($socket);
19 | }
20 |
21 | protected function initEvent(SocketServer $socket)
22 | {
23 | $socket->on('connection', function(SocketConnection $conn){
24 | $this->onConnect($conn);
25 | });
26 | }
27 |
28 | protected function onConnect(SocketConnection $conn)
29 | {
30 | $conn->on('data', function($data) use($conn){
31 | $this->onData($conn, $data);
32 | });
33 | }
34 |
35 | protected function onData(SocketConnection $conn, $data)
36 | {
37 | $request = $this->doHttpHandshake($data);
38 | $this->handleRequest($conn, $request);
39 | }
40 |
41 | protected function handleRequest(SocketConnection $conn, Request $request)
42 | {
43 | switch($request->getPathInfo()){
44 | case '/livereload':
45 | $this->initWebSocket($conn, $request);
46 | break;
47 | case '/livereload.js':
48 | $this->serveFile($conn, __DIR__.'/../../web/js/livereload.js');
49 | break;
50 | case '/changed':
51 | $this->notifyChanged($conn, $request);
52 | break;
53 | default:
54 | $this->serve404Error($conn);
55 | }
56 | }
57 |
58 | protected function initWebSocket(SocketConnection $conn, Request $request)
59 | {
60 | $conn->removeAllListeners('data');
61 | return new WebSocketProtocol($conn, $this->app, $request);
62 | }
63 |
64 | protected function getRequestChangedFiles(Request $request)
65 | {
66 | if(($files = $request->query->get('files')) != null){
67 | return explode(',', $files);
68 | }
69 | $requestJson = json_decode($request->getContent(), true);
70 | return isset($requestJson['files'])?(is_array($requestJson['files'])?$requestJson['files']:[$requestJson['files']]):[];
71 | }
72 |
73 | protected function notifyChanged(SocketConnection $conn, Request $request)
74 | {
75 | foreach($this->getRequestChangedFiles($request) as $file){
76 | $this->app->getOutput()->writeln(strftime('%T')." - info - Receive request reload $file", OutputInterface::VERBOSITY_VERBOSE);
77 | $this->app->reloadFile($file);
78 | }
79 | $response = new Response(json_encode(array('status' => true)));
80 | $conn->write($response);
81 | }
82 |
83 | protected function serveFile(SocketConnection $conn, $file)
84 | {
85 | if(($path = realpath($file)) === null){
86 | return ;
87 | }
88 | $content = file_get_contents($file);
89 | $response = new Response($content);
90 | $response->setContentType('text/plain', 'utf-8');
91 | $conn->write($response);
92 | }
93 |
94 | protected function serve404Error(SocketConnection $conn)
95 | {
96 | $response = new Response('file not found.', Response::HTTP_NOT_FOUND);
97 | $conn->write($response);
98 | $conn->end();
99 | }
100 |
101 | protected function doHttpHandshake($data)
102 | {
103 | $pos = strpos($data, "\r\n\r\n");
104 | if($pos === false){
105 | return false;
106 | }
107 | $body = substr($data, $pos + 4);
108 | $rawHeaders = explode("\r\n", substr($data, 0, $pos));
109 | $requestLine = $this->parseRequest(array_shift($rawHeaders));
110 | $headers = $this->parseHeaders($rawHeaders);
111 | return Request::create($requestLine['uri'], $requestLine['method'], array(), array(), array(), $headers, $body);
112 | }
113 |
114 | protected function parseRequest($rawRequest)
115 | {
116 | return array_combine(array('method', 'uri', 'protocol'), explode(' ', $rawRequest));
117 | }
118 |
119 | protected function parseHeaders($rawHeaders)
120 | {
121 | $headers = array();
122 | foreach($rawHeaders as $headerLine){
123 | if(($pos = strpos($headerLine, ':')) === false){
124 | continue;
125 | }
126 | $headers['HTTP_'.str_replace('-', '_', trim(strtoupper(substr($headerLine, 0, $pos))))] = trim(substr($headerLine, $pos + 1));
127 | }
128 | return $headers;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/Protocol/WebSocket/Frame.php:
--------------------------------------------------------------------------------
1 | decode($data);
41 | if (!$frame->isCoalesced()) {
42 | return $frameSize;
43 | }
44 | $data->shift($frameSize);
45 |
46 | return $frame;
47 | }
48 |
49 | /**
50 | *
51 | * @param string $data
52 | * @return Frame
53 | */
54 | public static function generate($data)
55 | {
56 | $frame = new static($data);
57 |
58 | return $frame;
59 | }
60 |
61 | /**
62 | *
63 | * @param string $data
64 | * @return Frame
65 | */
66 | public static function close($data)
67 | {
68 | $frame = new static($data, true, static::OP_CLOSE);
69 | $frame->setClosed();
70 |
71 | return $frame;
72 | }
73 |
74 | protected function __construct($data = null, $final = true, $opcode = self::OP_TEXT)
75 | {
76 | $this->firstByte = ($final ? 0x80 : 0) + $opcode;
77 | $this->appendData($data);
78 | }
79 |
80 | public function setClosed($isClosed = true)
81 | {
82 | $this->isClosed = $isClosed;
83 | }
84 |
85 | public function isClosed()
86 | {
87 | return $this->isClosed;
88 | }
89 |
90 | public function getOpcode()
91 | {
92 | return $this->firstByte & 0x7f;
93 | }
94 |
95 | public function setFirstByte($byte)
96 | {
97 | $this->firstByte = $byte;
98 | }
99 |
100 | public function setSecondByte($byte)
101 | {
102 | $this->secondByte = $byte;
103 | }
104 |
105 | public function setData($data)
106 | {
107 | $this->data = '';
108 | $this->appendData($data);
109 | }
110 |
111 | public function appendData($data)
112 | {
113 | $this->data.=$data;
114 | $this->updatePayloadLength($this->data, $this->secondByte, $this->extendedPayload);
115 | }
116 |
117 | public function setMask($mask)
118 | {
119 | $mask &= 1;
120 | $this->secondByte |= ($mask << 7);
121 | }
122 |
123 | public function isMask()
124 | {
125 | return ($this->secondByte >> 7) == 1;
126 | }
127 |
128 | public function generateMaskingKey()
129 | {
130 | $maskingKey = '';
131 | for ($i = 0; $i < static::MASK_LENGTH; $i++) {
132 | $maskingKey .= pack('C', rand(0, 255));
133 | }
134 |
135 | return $maskingKey;
136 | }
137 |
138 | protected function applyMask($maskingKey, $payload = null)
139 | {
140 | $applied = '';
141 | for ($i = 0, $len = strlen($payload); $i < $len; $i++) {
142 | $applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH];
143 | }
144 |
145 | return $applied;
146 | }
147 |
148 | protected function maskPayload($payload)
149 | {
150 | if (!$this->isMask()) {
151 | return $payload;
152 | }
153 | $maskingKey = $this->generateMaskingKey();
154 |
155 | return $maskingKey . $this->applyMask($maskingKey, $payload);
156 | }
157 |
158 | public function getPayloadLength($encodedData)
159 | {
160 | $length = $this->secondByte & 0x7f;
161 | if ($length < 126) {
162 | return [$length, 0];
163 | }
164 |
165 | if ($length == 126) { // with 2 bytes extended payload length
166 | if (($packedPayloadLength = substr($encodedData, 2, 2)) === false) {
167 | return [0, 0];
168 | }
169 |
170 | return [unpack("n", $packedPayloadLength)[1] + 2, 2];
171 | }
172 |
173 | if ($length == 127) { //with 8 bytes extended payload length
174 | if (($packedPayloadLength = substr($encodedData, 2, 8)) === false) {
175 | return [0, 0];
176 | }
177 | $payloadLength = unpack("N2", $packedPayloadLength);
178 |
179 | return [($packedPayloadLength[1] << 32) | $packedPayloadLength[2] + 8, 8];
180 | }
181 | }
182 |
183 | public function decode($encodedData)
184 | {
185 | $this->isCoalesced = false;
186 | if (strlen($encodedData) <= 2) {
187 | return static::DECODE_STATUS_MORE_DATA;
188 | }
189 | $bytes = unpack("C2", $encodedData);
190 | $this->setFirstByte($bytes[1]);
191 | $this->setSecondByte($bytes[2]);
192 |
193 | if (!$this->verifyPayload()) {
194 | return static::DECODE_STATUS_ERROR;
195 | }
196 | list($payloadLength, $extendedPayloadBytes) = $this->getPayloadLength($encodedData);
197 | $totalFramLength = 2 + $payloadLength;
198 | if ($this->isMask()) {
199 | $totalFramLength += static::MASK_LENGTH;
200 | }
201 | if ($payloadLength == 0 || strlen($encodedData) < $totalFramLength) {
202 | return static::DECODE_STATUS_MORE_DATA;
203 | }
204 | $maskingKey = substr($encodedData, 2 + $extendedPayloadBytes, static::MASK_LENGTH);
205 | $data = $this->applyMask($maskingKey, substr($encodedData, 2 + $extendedPayloadBytes + static::MASK_LENGTH));
206 | $this->setData($data);
207 | if (strlen($encodedData) >= $totalFramLength) {
208 | $this->isCoalesced = true;
209 | }
210 |
211 | return $totalFramLength;
212 | }
213 |
214 | public function isCoalesced()
215 | {
216 | return $this->isCoalesced;
217 | }
218 |
219 | public function getRSV1()
220 | {
221 | return ($this->firstByte & 4) > 0;
222 | }
223 |
224 | public function getRSV2()
225 | {
226 | return ($this->firstByte & 2) > 0;
227 | }
228 |
229 | public function getRSV3()
230 | {
231 | return ($this->firstByte & 1) > 0;
232 | }
233 |
234 | protected function verifyPayload()
235 | {
236 | if ($this->getRSV1() && $this->getRSV2() && $this->getRSV3()) {
237 | return false;
238 | }
239 |
240 | return true;
241 | }
242 |
243 | public function getData()
244 | {
245 | return $this->data;
246 | }
247 |
248 | public function encode()
249 | {
250 | $this->isCoalesced = true;
251 |
252 | return pack('CC', $this->firstByte, $this->secondByte) . $this->extendedPayload . $this->maskPayload($this->data);
253 | }
254 |
255 | protected function updatePayloadLength($data, &$secondByte, &$extendedPayload)
256 | {
257 | $secondByte &= 0x80;
258 | $size = strlen($data);
259 | if ($size < 126) {
260 | $secondByte |= $size;
261 |
262 | return;
263 | }
264 |
265 | if ($size <= 65535) { //use 2 bytes extended payload
266 | $secondByte |= 126;
267 | $extendedPayload = pack("n", $size);
268 |
269 | return;
270 | }
271 |
272 | //use 4 bytes extended payload
273 | $secondByte |= 127;
274 | $extendedPayload = pack("NN", $size >> 32, $size & 0xffffffff);
275 | }
276 |
277 | }
278 |
--------------------------------------------------------------------------------
/Tests/Protocol/HttpProtocolTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('\\PHPLivereload\\Application\\ServerApplication')
18 | ->disableOriginalConstructor()
19 | ->getMock();
20 | $socket = $this->getMockBuilder('\\React\\Socket\\Server')
21 | ->disableOriginalConstructor()
22 | ->getMock();
23 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
24 | ->setMethods(array('initEvent'))
25 | ->disableOriginalConstructor()
26 | ->getMock();
27 | $httpProtocol->expects($this->any())
28 | ->method('initEvent')
29 | ->will($this->returnCallback(function() use(&$calls){
30 | $calls[] = 'initEvent';
31 | }));
32 |
33 | $reflectedClass = new \ReflectionClass($httpProtocol);
34 | $constructor = $reflectedClass->getConstructor();
35 | $constructor->invoke($httpProtocol, $socket, $app);
36 | $this->assertEquals($app, $this->getObjectAttribute($httpProtocol, 'app'));
37 | $this->assertEquals(array('initEvent'), $calls);
38 | }
39 |
40 | public function test_initEvent()
41 | {
42 | $calls = [];
43 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection')
44 | ->disableOriginalConstructor()
45 | ->getMock();
46 | $socket = $this->getMockBuilder('\\React\\Socket\\Server')
47 | ->setMethods(array('on'))
48 | ->disableOriginalConstructor()
49 | ->getMock();
50 | $socket->expects($this->any())
51 | ->method('on')
52 | ->will($this->returnCallback(function($event, $callback) use(&$calls, $socketConn){
53 | $calls[] = 'on';
54 | $callback($socketConn);
55 | $this->assertEquals('connection', $event);
56 | }));
57 |
58 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
59 | ->setMethods(array('onConnect'))
60 | ->disableOriginalConstructor()
61 | ->getMock();
62 | $httpProtocol->expects($this->any())
63 | ->method('onConnect')
64 | ->will($this->returnCallback(function($conn) use(&$calls, $socketConn){
65 | $calls[] = 'onConnect';
66 | $this->assertEquals($socketConn, $conn);
67 | }));
68 |
69 | $reflectedClass = new \ReflectionClass($httpProtocol);
70 | $method = $reflectedClass->getMethod('initEvent');
71 | $method->setAccessible(true);
72 | $method->invoke($httpProtocol, $socket);
73 | $this->assertEquals(array('on', 'onConnect'), $calls);
74 | }
75 |
76 | public function test_onConnect()
77 | {
78 | $calls = [];
79 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection')
80 | ->setMethods(array('on'))
81 | ->disableOriginalConstructor()
82 | ->getMock();
83 | $socketConn->expects($this->any())
84 | ->method('on')
85 | ->will($this->returnCallback(function($event, $callback) use(&$calls, $socketConn){
86 | $calls[] = 'on';
87 | $callback('dataForReceive');
88 | $this->assertEquals('data', $event);
89 | }));
90 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
91 | ->setMethods(array('onData'))
92 | ->disableOriginalConstructor()
93 | ->getMock();
94 | $httpProtocol->expects($this->any())
95 | ->method('onData')
96 | ->will($this->returnCallback(function($conn, $data) use(&$calls, $socketConn){
97 | $calls[] = 'onData';
98 | $this->assertEquals('dataForReceive', $data);
99 | $this->assertEquals($socketConn, $conn);
100 | }));
101 | $reflectedClass = new \ReflectionClass($httpProtocol);
102 | $method = $reflectedClass->getMethod('onConnect');
103 | $method->setAccessible(true);
104 | $method->invoke($httpProtocol, $socketConn);
105 | $this->assertEquals(array('on', 'onData'), $calls);
106 | }
107 |
108 | public function test_onData()
109 | {
110 | $calls = [];
111 | $request = new Request();
112 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection')
113 | ->disableOriginalConstructor()
114 | ->getMock();
115 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
116 | ->setMethods(array('doHttpHandshake', 'handleRequest'))
117 | ->disableOriginalConstructor()
118 | ->getMock();
119 | $httpProtocol->expects($this->any())
120 | ->method('doHttpHandshake')
121 | ->will($this->returnCallback(function($data) use(&$calls, $request){
122 | $calls[] = 'doHttpHandshake';
123 | $this->assertEquals('dataForReceive', $data);
124 | return $request;
125 | }));
126 | $httpProtocol->expects($this->any())
127 | ->method('handleRequest')
128 | ->will($this->returnCallback(function($conn, $requestForTest) use(&$calls, $socketConn, $request){
129 | $calls[] = 'handleRequest';
130 | $this->assertEquals($socketConn, $conn);
131 | $this->assertEquals($request, $requestForTest);
132 | }));
133 | $reflectedClass = new \ReflectionClass($httpProtocol);
134 | $method = $reflectedClass->getMethod('onData');
135 | $method->setAccessible(true);
136 | $method->invoke($httpProtocol, $socketConn, 'dataForReceive');
137 | $this->assertEquals(array('doHttpHandshake', 'handleRequest'), $calls);
138 | }
139 |
140 | public function test_handleRequest()
141 | {
142 | $calls = [];
143 | $pathInfo = '';
144 |
145 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection')
146 | ->disableOriginalConstructor()
147 | ->getMock();
148 |
149 | $request = $this->getMockBuilder('\\Symfony\\Component\\HttpFoundation\\Request')
150 | ->setMethods(array('getPathInfo'))
151 | ->disableOriginalConstructor()
152 | ->getMock();
153 | $request->expects($this->any())
154 | ->method('getPathInfo')
155 | ->will($this->returnCallback(function() use(&$pathInfo){
156 | return $pathInfo;
157 | }));
158 |
159 | $methods = array('initWebSocket', 'serveFile', 'notifyChanged', 'serve404Error');
160 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
161 | ->setMethods($methods)
162 | ->disableOriginalConstructor()
163 | ->getMock();
164 | foreach($methods as $method){
165 | $httpProtocol->expects($this->any())
166 | ->method($method)
167 | ->will($this->returnCallback(function() use(&$calls, $method){
168 | $calls[] = $method;
169 | }));
170 | }
171 |
172 | $reflectedClass = new \ReflectionClass($httpProtocol);
173 | $method = $reflectedClass->getMethod('handleRequest');
174 | $method->setAccessible(true);
175 |
176 | $pathInfo = '/livereload';
177 | $calls = [];
178 | $method->invoke($httpProtocol, $socketConn, $request);
179 | $this->assertEquals(array('initWebSocket'), $calls);
180 |
181 | $pathInfo = '/livereload.js';
182 | $calls = [];
183 | $method->invoke($httpProtocol, $socketConn, $request);
184 | $this->assertEquals(array('serveFile'), $calls);
185 |
186 | $pathInfo = '/changed';
187 | $calls = [];
188 | $method->invoke($httpProtocol, $socketConn, $request);
189 | $this->assertEquals(array('notifyChanged'), $calls);
190 |
191 | $pathInfo = '/asjjahkhakjs'; //error 404
192 | $calls = [];
193 | $method->invoke($httpProtocol, $socketConn, $request);
194 | $this->assertEquals(array('serve404Error'), $calls);
195 | }
196 |
197 | public function test_serveFile()
198 | {
199 | $calls = [];
200 | $data = md5(microtime().rand());
201 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
202 | ->disableOriginalConstructor()
203 | ->getMock();
204 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection')
205 | ->disableOriginalConstructor()
206 | ->setMethods(array('write'))
207 | ->getMock();
208 | $socketConn->expects($this->any())
209 | ->method('write')
210 | ->will($this->returnCallback(function(Response $response) use($data, &$calls){
211 | $calls[] = 'write';
212 | $this->assertEquals($data, $response->getContent());
213 | $this->assertEquals('text/plain; charset=utf-8', $response->headers->get('Content-Type'));
214 | }));
215 | $reflectedClass = new \ReflectionClass($httpProtocol);
216 | $method = $reflectedClass->getMethod('serveFile');
217 | $method->setAccessible(true);
218 |
219 | $tempname = tempnam(sys_get_temp_dir(), 'livereloadtest-');
220 | file_put_contents($tempname, $data);
221 | $method->invoke($httpProtocol, $socketConn, $tempname);
222 | unlink($tempname);
223 | $this->assertEquals(array('write'), $calls);
224 | }
225 |
226 | public function test_serve404Error()
227 | {
228 | $calls = [];
229 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol')
230 | ->disableOriginalConstructor()
231 | ->getMock();
232 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection')
233 | ->disableOriginalConstructor()
234 | ->setMethods(array('write', 'end'))
235 | ->getMock();
236 | $socketConn->expects($this->any())
237 | ->method('write')
238 | ->will($this->returnCallback(function(Response $response) use(&$calls){
239 | $calls[] = 'write';
240 | $this->assertEquals(Response::HTTP_NOT_FOUND ,$response->getStatusCode());
241 | }));
242 | $socketConn->expects($this->any())
243 | ->method('end')
244 | ->will($this->returnCallback(function() use(&$calls){
245 | $calls[] = 'end';
246 | }));
247 | $reflectedClass = new \ReflectionClass($httpProtocol);
248 | $method = $reflectedClass->getMethod('serve404Error');
249 | $method->setAccessible(true);
250 |
251 | $method->invoke($httpProtocol, $socketConn);
252 | $this->assertEquals(array('write', 'end'), $calls);
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/Tests/Application/ServerApplicationTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('\\PHPLivereload\\Application\\ServerApplication')
23 | ->disableOriginalConstructor()
24 | ->setMethods(array('initLoop', 'initServer'))
25 | ->getMock();
26 | $app->expects($this->any())
27 | ->method('initLoop')
28 | ->will($this->returnCallback(function() use(&$calls){
29 | $calls[] = 'initLoop';
30 | }));
31 | $app->expects($this->any())
32 | ->method('initServer')
33 | ->will($this->returnCallback(function($hostForTest, $portForTest) use(&$calls, $host, $port){
34 | $calls[] = 'initServer';
35 | $this->assertEquals($host, $hostForTest);
36 | $this->assertEquals($port, $portForTest);
37 | }));
38 |
39 | $reflectedClass = new \ReflectionClass($app);
40 | $constructor = $reflectedClass->getConstructor();
41 | $constructor->invoke($app, $host, $port);
42 | $this->assertEquals(array('initLoop', 'initServer'), $calls);
43 | }
44 |
45 | public function test_run()
46 | {
47 | $calls = array();
48 | $app = new MockServerApplication();
49 | $loop = $this->getMock('\\React\\EventLoop\\StreamSelectLoop', array('run'));
50 | $loop->expects($this->any())
51 | ->method('run')
52 | ->will($this->returnCallback(function() use(&$calls){
53 | $calls[] = 'run';
54 | }));
55 | $reflectedClass = new \ReflectionClass($app);
56 | $loopProperty = $reflectedClass->getProperty('loop');
57 | $loopProperty->setAccessible(true);
58 | $loopProperty->setValue($app, $loop);
59 | $app->run();
60 | $this->assertEquals(array('run'), $calls);
61 | }
62 |
63 | public function test_getConfig()
64 | {
65 | $config = md5(microtime().rand());
66 | $app = new MockServerApplication();
67 | $reflectedClass = new \ReflectionClass($app);
68 | $configProperty = $reflectedClass->getProperty('config');
69 | $configProperty->setAccessible(true);
70 | $configProperty->setValue($app, $config);
71 | $this->assertEquals($config, $app->getConfig());
72 | }
73 |
74 | public function test_initLoop()
75 | {
76 | $app = new MockServerApplication();
77 | $reflectedClass = new \ReflectionClass($app);
78 | $initLoop = $reflectedClass->getMethod('initLoop');
79 | $initLoop->setAccessible(true);
80 | $initLoop->invoke($app);
81 | $loopProperty = $reflectedClass->getProperty('loop');
82 | $loopProperty->setAccessible(true);
83 | $loop = $loopProperty->getValue($app);
84 | $this->assertTrue($loop instanceof \React\EventLoop\LoopInterface);
85 | }
86 |
87 | public function test_addClient()
88 | {
89 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
90 | ->disableOriginalConstructor()
91 | ->getMock();
92 | $client2 = clone $client1;
93 | $app = new MockServerApplication();
94 | $app->addClient($client1);
95 | $reflectedClass = new \ReflectionClass($app);
96 | $clientsProperty = $reflectedClass->getProperty('clients');
97 | $clientsProperty->setAccessible(true);
98 | $clients = $clientsProperty->getValue($app);
99 | $this->assertEquals(array($client1), $clients);
100 | $app->addClient($client2);
101 | $clients = $clientsProperty->getValue($app);
102 | $this->assertEquals(array($client1, $client2), $clients);
103 | $app->addClient($client1);
104 | $clients = $clientsProperty->getValue($app);
105 | $this->assertEquals(array($client1, $client2), $clients);
106 | }
107 |
108 | public function test_removeClient()
109 | {
110 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
111 | ->disableOriginalConstructor()
112 | ->setMockClassName('client1')
113 | ->getMock();
114 | $client2 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
115 | ->disableOriginalConstructor()
116 | ->setMockClassName('client2')
117 | ->getMock();
118 | $client3 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
119 | ->disableOriginalConstructor()
120 | ->setMockClassName('client3')
121 | ->getMock();
122 | $app = new MockServerApplication();
123 | $app->addClient($client1);
124 | $app->addClient($client2);
125 | $app->addClient($client3);
126 | $app->removeClient($client2);
127 | $reflectedClass = new \ReflectionClass($app);
128 | $clientsProperty = $reflectedClass->getProperty('clients');
129 | $clientsProperty->setAccessible(true);
130 | $clients = $clientsProperty->getValue($app);
131 | $this->assertEquals(array($client1, $client3), array_values($clients));
132 | }
133 |
134 | public function test_removeClient_client_is_the_first_element()
135 | {
136 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
137 | ->disableOriginalConstructor()
138 | ->setMockClassName('client1')
139 | ->getMock();
140 | $client2 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
141 | ->disableOriginalConstructor()
142 | ->setMockClassName('client2')
143 | ->getMock();
144 | $client3 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
145 | ->disableOriginalConstructor()
146 | ->setMockClassName('client3')
147 | ->getMock();
148 | $app = new MockServerApplication();
149 | $app->addClient($client1);
150 | $app->addClient($client2);
151 | $app->addClient($client3);
152 | $app->removeClient($client1);
153 | $reflectedClass = new \ReflectionClass($app);
154 | $clientsProperty = $reflectedClass->getProperty('clients');
155 | $clientsProperty->setAccessible(true);
156 | $clients = $clientsProperty->getValue($app);
157 | $this->assertEquals(array($client2, $client3), array_values($clients));
158 | }
159 |
160 | public function test_reloadFile()
161 | {
162 | $calls = array();
163 | $file = md5(microtime().rand());
164 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
165 | ->disableOriginalConstructor()
166 | ->setMockClassName('client1')
167 | ->setMethods(array('reload'))
168 | ->getMock();
169 | $client1->expects($this->any())
170 | ->method('reload')
171 | ->will($this->returnCallback(function() use(&$calls){
172 | $calls[] = 'client1';
173 | }));
174 | $client2 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
175 | ->disableOriginalConstructor()
176 | ->setMockClassName('client2')
177 | ->setMethods(array('reload'))
178 | ->getMock();
179 | $client2->expects($this->any())
180 | ->method('reload')
181 | ->will($this->returnCallback(function() use(&$calls){
182 | $calls[] = 'client2';
183 | }));
184 | $client3 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol')
185 | ->disableOriginalConstructor()
186 | ->setMockClassName('client3')
187 | ->setMethods(array('reload'))
188 | ->getMock();
189 | $client3->expects($this->any())
190 | ->method('reload')
191 | ->will($this->returnCallback(function() use(&$calls){
192 | $calls[] = 'client3';
193 | }));
194 | $app = new MockServerApplication();
195 | $app->addClient($client1);
196 | $app->addClient($client2);
197 | $app->addClient($client3);
198 | $app->reloadFile($file);
199 | $this->assertEquals(array('client1', 'client2', 'client3'), $calls);
200 | }
201 |
202 | public function test_setOutput()
203 | {
204 | $serverApp = new MockServerApplication();
205 | $output = $this->getMock('\\Symfony\\Component\\Console\\Output\\OutputInterface');
206 | $serverApp->setOutput($output);
207 | $this->assertEquals($output, $this->getObjectAttribute($serverApp, 'output'));
208 | }
209 |
210 | public function test_getOutput()
211 | {
212 | $serverApp = new MockServerApplication();
213 | $output = $this->getMock('\\Symfony\\Component\\Console\\Output\\OutputInterface');
214 | $reflectedClass = new \ReflectionClass($serverApp);
215 | $properity = $reflectedClass->getProperty('output');
216 | $properity->setAccessible(true);
217 | $properity->setValue($serverApp, $output);
218 | $this->assertEquals($output, $serverApp->getOutput());
219 | }
220 |
221 | public function test_watching()
222 | {
223 | $calls = [];
224 | $testConfig = array(1, 2, 3, 4);
225 | $serverApp = $this->getMock('\\PHPLivereload\\Application\\ServerApplication', array('scanFiles', 'watchingFileChange'), array(), '', false);
226 | $serverApp->expects($this->any())
227 | ->method('scanFiles')
228 | ->will($this->returnCallback(function() use(&$calls){
229 | $calls[] = 'scanFiles';
230 | }));
231 | $serverApp->expects($this->any())
232 | ->method('watchingFileChange')
233 | ->will($this->returnCallback(function() use(&$calls){
234 | $calls[] = 'watchingFileChange';
235 | }));
236 | $loop = $this->getMock('\\React\\EventLoop\\StreamSelectLoop', array('addPeriodicTimer'));
237 | $loop->expects($this->any())
238 | ->method('addPeriodicTimer')
239 | ->will($this->returnCallback(function($time, $callback) use(&$calls){
240 | $calls[] = 'addPeriodicTimer';
241 | $this->assertEquals(123, $time);
242 | $callback();
243 | }));
244 | $reflectedClass = new \ReflectionClass($serverApp);
245 | $properity = $reflectedClass->getProperty('loop');
246 | $properity->setAccessible(true);
247 | $properity->setValue($serverApp, $loop);
248 | $serverApp->watching(123, $testConfig);
249 | $this->assertEquals(array('scanFiles', 'addPeriodicTimer', 'watchingFileChange'), $calls);
250 | }
251 |
252 | public function test_initServer()
253 | {
254 | $loop = LoopFactory::create();
255 | $serverApp = new MockServerApplication();
256 | $reflectedClass = new \ReflectionClass($serverApp);
257 | $properity = $reflectedClass->getProperty('loop');
258 | $properity->setAccessible(true);
259 | $properity->setValue($serverApp, $loop);
260 | $method = $reflectedClass->getMethod('initServer');
261 | $method->setAccessible(true);
262 | $result = $method->invoke($serverApp, '127.0.0.1', 8888);
263 | $this->assertTrue($result instanceof Protocol\HttpProtocol);
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/web/js/livereload.js:
--------------------------------------------------------------------------------
1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag");
307 | return;
308 | }
309 | this.reloader = new Reloader(this.window, this.console, Timer);
310 | this.connector = new Connector(this.options, this.WebSocket, Timer, {
311 | connecting: (function(_this) {
312 | return function() {};
313 | })(this),
314 | socketConnected: (function(_this) {
315 | return function() {};
316 | })(this),
317 | connected: (function(_this) {
318 | return function(protocol) {
319 | var _base;
320 | if (typeof (_base = _this.listeners).connect === "function") {
321 | _base.connect();
322 | }
323 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ").");
324 | return _this.analyze();
325 | };
326 | })(this),
327 | error: (function(_this) {
328 | return function(e) {
329 | if (e instanceof ProtocolError) {
330 | if (typeof console !== "undefined" && console !== null) {
331 | return console.log("" + e.message + ".");
332 | }
333 | } else {
334 | if (typeof console !== "undefined" && console !== null) {
335 | return console.log("LiveReload internal error: " + e.message);
336 | }
337 | }
338 | };
339 | })(this),
340 | disconnected: (function(_this) {
341 | return function(reason, nextDelay) {
342 | var _base;
343 | if (typeof (_base = _this.listeners).disconnect === "function") {
344 | _base.disconnect();
345 | }
346 | switch (reason) {
347 | case 'cannot-connect':
348 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec.");
349 | case 'broken':
350 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec.");
351 | case 'handshake-timeout':
352 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec.");
353 | case 'handshake-failed':
354 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec.");
355 | case 'manual':
356 | break;
357 | case 'error':
358 | break;
359 | default:
360 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec.");
361 | }
362 | };
363 | })(this),
364 | message: (function(_this) {
365 | return function(message) {
366 | switch (message.command) {
367 | case 'reload':
368 | return _this.performReload(message);
369 | case 'alert':
370 | return _this.performAlert(message);
371 | }
372 | };
373 | })(this)
374 | });
375 | }
376 |
377 | LiveReload.prototype.on = function(eventName, handler) {
378 | return this.listeners[eventName] = handler;
379 | };
380 |
381 | LiveReload.prototype.log = function(message) {
382 | return this.console.log("" + message);
383 | };
384 |
385 | LiveReload.prototype.performReload = function(message) {
386 | var _ref, _ref1;
387 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2)));
388 | return this.reloader.reload(message.path, {
389 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true,
390 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true,
391 | originalPath: message.originalPath || '',
392 | overrideURL: message.overrideURL || '',
393 | serverURL: "http://" + this.options.host + ":" + this.options.port
394 | });
395 | };
396 |
397 | LiveReload.prototype.performAlert = function(message) {
398 | return alert(message.message);
399 | };
400 |
401 | LiveReload.prototype.shutDown = function() {
402 | var _base;
403 | this.connector.disconnect();
404 | this.log("LiveReload disconnected.");
405 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0;
406 | };
407 |
408 | LiveReload.prototype.hasPlugin = function(identifier) {
409 | return !!this.pluginIdentifiers[identifier];
410 | };
411 |
412 | LiveReload.prototype.addPlugin = function(pluginClass) {
413 | var plugin;
414 | if (this.hasPlugin(pluginClass.identifier)) {
415 | return;
416 | }
417 | this.pluginIdentifiers[pluginClass.identifier] = true;
418 | plugin = new pluginClass(this.window, {
419 | _livereload: this,
420 | _reloader: this.reloader,
421 | _connector: this.connector,
422 | console: this.console,
423 | Timer: Timer,
424 | generateCacheBustUrl: (function(_this) {
425 | return function(url) {
426 | return _this.reloader.generateCacheBustUrl(url);
427 | };
428 | })(this)
429 | });
430 | this.plugins.push(plugin);
431 | this.reloader.addPlugin(plugin);
432 | };
433 |
434 | LiveReload.prototype.analyze = function() {
435 | var plugin, pluginData, pluginsData, _i, _len, _ref;
436 | if (!(this.connector.protocol >= 7)) {
437 | return;
438 | }
439 | pluginsData = {};
440 | _ref = this.plugins;
441 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
442 | plugin = _ref[_i];
443 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {};
444 | pluginData.version = plugin.constructor.version;
445 | }
446 | this.connector.sendCommand({
447 | command: 'info',
448 | plugins: pluginsData,
449 | url: this.window.location.href
450 | });
451 | };
452 |
453 | return LiveReload;
454 |
455 | })();
456 |
457 | }).call(this);
458 |
459 | },{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){
460 | (function() {
461 | var Options;
462 |
463 | exports.Options = Options = (function() {
464 | function Options() {
465 | this.host = null;
466 | this.port = 35729;
467 | this.snipver = null;
468 | this.ext = null;
469 | this.extver = null;
470 | this.mindelay = 1000;
471 | this.maxdelay = 60000;
472 | this.handshake_timeout = 5000;
473 | }
474 |
475 | Options.prototype.set = function(name, value) {
476 | if (typeof value === 'undefined') {
477 | return;
478 | }
479 | if (!isNaN(+value)) {
480 | value = +value;
481 | }
482 | return this[name] = value;
483 | };
484 |
485 | return Options;
486 |
487 | })();
488 |
489 | Options.extract = function(document) {
490 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1;
491 | _ref = document.getElementsByTagName('script');
492 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
493 | element = _ref[_i];
494 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) {
495 | options = new Options();
496 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) {
497 | options.host = mm[1];
498 | if (mm[2]) {
499 | options.port = parseInt(mm[2], 10);
500 | }
501 | }
502 | if (m[2]) {
503 | _ref1 = m[2].split('&');
504 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
505 | pair = _ref1[_j];
506 | if ((keyAndValue = pair.split('=')).length > 1) {
507 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('='));
508 | }
509 | }
510 | }
511 | return options;
512 | }
513 | }
514 | return null;
515 | };
516 |
517 | }).call(this);
518 |
519 | },{}],6:[function(require,module,exports){
520 | (function() {
521 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError,
522 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
523 |
524 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6';
525 |
526 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7';
527 |
528 | exports.ProtocolError = ProtocolError = (function() {
529 | function ProtocolError(reason, data) {
530 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\".";
531 | }
532 |
533 | return ProtocolError;
534 |
535 | })();
536 |
537 | exports.Parser = Parser = (function() {
538 | function Parser(handlers) {
539 | this.handlers = handlers;
540 | this.reset();
541 | }
542 |
543 | Parser.prototype.reset = function() {
544 | return this.protocol = null;
545 | };
546 |
547 | Parser.prototype.process = function(data) {
548 | var command, e, message, options, _ref;
549 | try {
550 | if (this.protocol == null) {
551 | if (data.match(/^!!ver:([\d.]+)$/)) {
552 | this.protocol = 6;
553 | } else if (message = this._parseMessage(data, ['hello'])) {
554 | if (!message.protocols.length) {
555 | throw new ProtocolError("no protocols specified in handshake message");
556 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) {
557 | this.protocol = 7;
558 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) {
559 | this.protocol = 6;
560 | } else {
561 | throw new ProtocolError("no supported protocols found");
562 | }
563 | }
564 | return this.handlers.connected(this.protocol);
565 | } else if (this.protocol === 6) {
566 | message = JSON.parse(data);
567 | if (!message.length) {
568 | throw new ProtocolError("protocol 6 messages must be arrays");
569 | }
570 | command = message[0], options = message[1];
571 | if (command !== 'refresh') {
572 | throw new ProtocolError("unknown protocol 6 command");
573 | }
574 | return this.handlers.message({
575 | command: 'reload',
576 | path: options.path,
577 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true
578 | });
579 | } else {
580 | message = this._parseMessage(data, ['reload', 'alert']);
581 | return this.handlers.message(message);
582 | }
583 | } catch (_error) {
584 | e = _error;
585 | if (e instanceof ProtocolError) {
586 | return this.handlers.error(e);
587 | } else {
588 | throw e;
589 | }
590 | }
591 | };
592 |
593 | Parser.prototype._parseMessage = function(data, validCommands) {
594 | var e, message, _ref;
595 | try {
596 | message = JSON.parse(data);
597 | } catch (_error) {
598 | e = _error;
599 | throw new ProtocolError('unparsable JSON', data);
600 | }
601 | if (!message.command) {
602 | throw new ProtocolError('missing "command" key', data);
603 | }
604 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) {
605 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data);
606 | }
607 | return message;
608 | };
609 |
610 | return Parser;
611 |
612 | })();
613 |
614 | }).call(this);
615 |
616 | },{}],7:[function(require,module,exports){
617 | (function() {
618 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl;
619 |
620 | splitUrl = function(url) {
621 | var hash, index, params;
622 | if ((index = url.indexOf('#')) >= 0) {
623 | hash = url.slice(index);
624 | url = url.slice(0, index);
625 | } else {
626 | hash = '';
627 | }
628 | if ((index = url.indexOf('?')) >= 0) {
629 | params = url.slice(index);
630 | url = url.slice(0, index);
631 | } else {
632 | params = '';
633 | }
634 | return {
635 | url: url,
636 | params: params,
637 | hash: hash
638 | };
639 | };
640 |
641 | pathFromUrl = function(url) {
642 | var path;
643 | url = splitUrl(url).url;
644 | if (url.indexOf('file://') === 0) {
645 | path = url.replace(/^file:\/\/(localhost)?/, '');
646 | } else {
647 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/');
648 | }
649 | return decodeURIComponent(path);
650 | };
651 |
652 | pickBestMatch = function(path, objects, pathFunc) {
653 | var bestMatch, object, score, _i, _len;
654 | bestMatch = {
655 | score: 0
656 | };
657 | for (_i = 0, _len = objects.length; _i < _len; _i++) {
658 | object = objects[_i];
659 | score = numberOfMatchingSegments(path, pathFunc(object));
660 | if (score > bestMatch.score) {
661 | bestMatch = {
662 | object: object,
663 | score: score
664 | };
665 | }
666 | }
667 | if (bestMatch.score > 0) {
668 | return bestMatch;
669 | } else {
670 | return null;
671 | }
672 | };
673 |
674 | numberOfMatchingSegments = function(path1, path2) {
675 | var comps1, comps2, eqCount, len;
676 | path1 = path1.replace(/^\/+/, '').toLowerCase();
677 | path2 = path2.replace(/^\/+/, '').toLowerCase();
678 | if (path1 === path2) {
679 | return 10000;
680 | }
681 | comps1 = path1.split('/').reverse();
682 | comps2 = path2.split('/').reverse();
683 | len = Math.min(comps1.length, comps2.length);
684 | eqCount = 0;
685 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) {
686 | ++eqCount;
687 | }
688 | return eqCount;
689 | };
690 |
691 | pathsMatch = function(path1, path2) {
692 | return numberOfMatchingSegments(path1, path2) > 0;
693 | };
694 |
695 | IMAGE_STYLES = [
696 | {
697 | selector: 'background',
698 | styleNames: ['backgroundImage']
699 | }, {
700 | selector: 'border',
701 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage']
702 | }
703 | ];
704 |
705 | exports.Reloader = Reloader = (function() {
706 | function Reloader(window, console, Timer) {
707 | this.window = window;
708 | this.console = console;
709 | this.Timer = Timer;
710 | this.document = this.window.document;
711 | this.importCacheWaitPeriod = 200;
712 | this.plugins = [];
713 | }
714 |
715 | Reloader.prototype.addPlugin = function(plugin) {
716 | return this.plugins.push(plugin);
717 | };
718 |
719 | Reloader.prototype.analyze = function(callback) {
720 | return results;
721 | };
722 |
723 | Reloader.prototype.reload = function(path, options) {
724 | var plugin, _base, _i, _len, _ref;
725 | this.options = options;
726 | if ((_base = this.options).stylesheetReloadTimeout == null) {
727 | _base.stylesheetReloadTimeout = 15000;
728 | }
729 | _ref = this.plugins;
730 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
731 | plugin = _ref[_i];
732 | if (plugin.reload && plugin.reload(path, options)) {
733 | return;
734 | }
735 | }
736 | if (options.liveCSS) {
737 | if (path.match(/\.css$/i)) {
738 | if (this.reloadStylesheet(path)) {
739 | return;
740 | }
741 | }
742 | }
743 | if (options.liveImg) {
744 | if (path.match(/\.(jpe?g|png|gif)$/i)) {
745 | this.reloadImages(path);
746 | return;
747 | }
748 | }
749 | return this.reloadPage();
750 | };
751 |
752 | Reloader.prototype.reloadPage = function() {
753 | return this.window.document.location.reload();
754 | };
755 |
756 | Reloader.prototype.reloadImages = function(path) {
757 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results;
758 | expando = this.generateUniqueString();
759 | _ref = this.document.images;
760 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
761 | img = _ref[_i];
762 | if (pathsMatch(path, pathFromUrl(img.src))) {
763 | img.src = this.generateCacheBustUrl(img.src, expando);
764 | }
765 | }
766 | if (this.document.querySelectorAll) {
767 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
768 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames;
769 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]");
770 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
771 | img = _ref2[_k];
772 | this.reloadStyleImages(img.style, styleNames, path, expando);
773 | }
774 | }
775 | }
776 | if (this.document.styleSheets) {
777 | _ref3 = this.document.styleSheets;
778 | _results = [];
779 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
780 | styleSheet = _ref3[_l];
781 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando));
782 | }
783 | return _results;
784 | }
785 | };
786 |
787 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) {
788 | var e, rule, rules, styleNames, _i, _j, _len, _len1;
789 | try {
790 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
791 | } catch (_error) {
792 | e = _error;
793 | }
794 | if (!rules) {
795 | return;
796 | }
797 | for (_i = 0, _len = rules.length; _i < _len; _i++) {
798 | rule = rules[_i];
799 | switch (rule.type) {
800 | case CSSRule.IMPORT_RULE:
801 | this.reloadStylesheetImages(rule.styleSheet, path, expando);
802 | break;
803 | case CSSRule.STYLE_RULE:
804 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
805 | styleNames = IMAGE_STYLES[_j].styleNames;
806 | this.reloadStyleImages(rule.style, styleNames, path, expando);
807 | }
808 | break;
809 | case CSSRule.MEDIA_RULE:
810 | this.reloadStylesheetImages(rule, path, expando);
811 | }
812 | }
813 | };
814 |
815 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) {
816 | var newValue, styleName, value, _i, _len;
817 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) {
818 | styleName = styleNames[_i];
819 | value = style[styleName];
820 | if (typeof value === 'string') {
821 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) {
822 | return function(match, src) {
823 | if (pathsMatch(path, pathFromUrl(src))) {
824 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")";
825 | } else {
826 | return match;
827 | }
828 | };
829 | })(this));
830 | if (newValue !== value) {
831 | style[styleName] = newValue;
832 | }
833 | }
834 | }
835 | };
836 |
837 | Reloader.prototype.reloadStylesheet = function(path) {
838 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1;
839 | links = (function() {
840 | var _i, _len, _ref, _results;
841 | _ref = this.document.getElementsByTagName('link');
842 | _results = [];
843 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
844 | link = _ref[_i];
845 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) {
846 | _results.push(link);
847 | }
848 | }
849 | return _results;
850 | }).call(this);
851 | imported = [];
852 | _ref = this.document.getElementsByTagName('style');
853 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
854 | style = _ref[_i];
855 | if (style.sheet) {
856 | this.collectImportedStylesheets(style, style.sheet, imported);
857 | }
858 | }
859 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) {
860 | link = links[_j];
861 | this.collectImportedStylesheets(link, link.sheet, imported);
862 | }
863 | if (this.window.StyleFix && this.document.querySelectorAll) {
864 | _ref1 = this.document.querySelectorAll('style[data-href]');
865 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
866 | style = _ref1[_k];
867 | links.push(style);
868 | }
869 | }
870 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets");
871 | match = pickBestMatch(path, links.concat(imported), (function(_this) {
872 | return function(l) {
873 | return pathFromUrl(_this.linkHref(l));
874 | };
875 | })(this));
876 | if (match) {
877 | if (match.object.rule) {
878 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href);
879 | this.reattachImportedRule(match.object);
880 | } else {
881 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object)));
882 | this.reattachStylesheetLink(match.object);
883 | }
884 | } else {
885 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one");
886 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) {
887 | link = links[_l];
888 | this.reattachStylesheetLink(link);
889 | }
890 | }
891 | return true;
892 | };
893 |
894 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) {
895 | var e, index, rule, rules, _i, _len;
896 | try {
897 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
898 | } catch (_error) {
899 | e = _error;
900 | }
901 | if (rules && rules.length) {
902 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) {
903 | rule = rules[index];
904 | switch (rule.type) {
905 | case CSSRule.CHARSET_RULE:
906 | continue;
907 | case CSSRule.IMPORT_RULE:
908 | result.push({
909 | link: link,
910 | rule: rule,
911 | index: index,
912 | href: rule.href
913 | });
914 | this.collectImportedStylesheets(link, rule.styleSheet, result);
915 | break;
916 | default:
917 | break;
918 | }
919 | }
920 | }
921 | };
922 |
923 | Reloader.prototype.waitUntilCssLoads = function(clone, func) {
924 | var callbackExecuted, executeCallback, poll;
925 | callbackExecuted = false;
926 | executeCallback = (function(_this) {
927 | return function() {
928 | if (callbackExecuted) {
929 | return;
930 | }
931 | callbackExecuted = true;
932 | return func();
933 | };
934 | })(this);
935 | clone.onload = (function(_this) {
936 | return function() {
937 | _this.console.log("LiveReload: the new stylesheet has finished loading");
938 | _this.knownToSupportCssOnLoad = true;
939 | return executeCallback();
940 | };
941 | })(this);
942 | if (!this.knownToSupportCssOnLoad) {
943 | (poll = (function(_this) {
944 | return function() {
945 | if (clone.sheet) {
946 | _this.console.log("LiveReload is polling until the new CSS finishes loading...");
947 | return executeCallback();
948 | } else {
949 | return _this.Timer.start(50, poll);
950 | }
951 | };
952 | })(this))();
953 | }
954 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback);
955 | };
956 |
957 | Reloader.prototype.linkHref = function(link) {
958 | return link.href || link.getAttribute('data-href');
959 | };
960 |
961 | Reloader.prototype.reattachStylesheetLink = function(link) {
962 | var clone, parent;
963 | if (link.__LiveReload_pendingRemoval) {
964 | return;
965 | }
966 | link.__LiveReload_pendingRemoval = true;
967 | if (link.tagName === 'STYLE') {
968 | clone = this.document.createElement('link');
969 | clone.rel = 'stylesheet';
970 | clone.media = link.media;
971 | clone.disabled = link.disabled;
972 | } else {
973 | clone = link.cloneNode(false);
974 | }
975 | clone.href = this.generateCacheBustUrl(this.linkHref(link));
976 | parent = link.parentNode;
977 | if (parent.lastChild === link) {
978 | parent.appendChild(clone);
979 | } else {
980 | parent.insertBefore(clone, link.nextSibling);
981 | }
982 | return this.waitUntilCssLoads(clone, (function(_this) {
983 | return function() {
984 | var additionalWaitingTime;
985 | if (/AppleWebKit/.test(navigator.userAgent)) {
986 | additionalWaitingTime = 5;
987 | } else {
988 | additionalWaitingTime = 200;
989 | }
990 | return _this.Timer.start(additionalWaitingTime, function() {
991 | var _ref;
992 | if (!link.parentNode) {
993 | return;
994 | }
995 | link.parentNode.removeChild(link);
996 | clone.onreadystatechange = null;
997 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0;
998 | });
999 | };
1000 | })(this));
1001 | };
1002 |
1003 | Reloader.prototype.reattachImportedRule = function(_arg) {
1004 | var href, index, link, media, newRule, parent, rule, tempLink;
1005 | rule = _arg.rule, index = _arg.index, link = _arg.link;
1006 | parent = rule.parentStyleSheet;
1007 | href = this.generateCacheBustUrl(rule.href);
1008 | media = rule.media.length ? [].join.call(rule.media, ', ') : '';
1009 | newRule = "@import url(\"" + href + "\") " + media + ";";
1010 | rule.__LiveReload_newHref = href;
1011 | tempLink = this.document.createElement("link");
1012 | tempLink.rel = 'stylesheet';
1013 | tempLink.href = href;
1014 | tempLink.__LiveReload_pendingRemoval = true;
1015 | if (link.parentNode) {
1016 | link.parentNode.insertBefore(tempLink, link);
1017 | }
1018 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) {
1019 | return function() {
1020 | if (tempLink.parentNode) {
1021 | tempLink.parentNode.removeChild(tempLink);
1022 | }
1023 | if (rule.__LiveReload_newHref !== href) {
1024 | return;
1025 | }
1026 | parent.insertRule(newRule, index);
1027 | parent.deleteRule(index + 1);
1028 | rule = parent.cssRules[index];
1029 | rule.__LiveReload_newHref = href;
1030 | return _this.Timer.start(_this.importCacheWaitPeriod, function() {
1031 | if (rule.__LiveReload_newHref !== href) {
1032 | return;
1033 | }
1034 | parent.insertRule(newRule, index);
1035 | return parent.deleteRule(index + 1);
1036 | });
1037 | };
1038 | })(this));
1039 | };
1040 |
1041 | Reloader.prototype.generateUniqueString = function() {
1042 | return 'livereload=' + Date.now();
1043 | };
1044 |
1045 | Reloader.prototype.generateCacheBustUrl = function(url, expando) {
1046 | var hash, oldParams, originalUrl, params, _ref;
1047 | if (expando == null) {
1048 | expando = this.generateUniqueString();
1049 | }
1050 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params;
1051 | if (this.options.overrideURL) {
1052 | if (url.indexOf(this.options.serverURL) < 0) {
1053 | originalUrl = url;
1054 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url);
1055 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url);
1056 | }
1057 | }
1058 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) {
1059 | return "" + sep + expando;
1060 | });
1061 | if (params === oldParams) {
1062 | if (oldParams.length === 0) {
1063 | params = "?" + expando;
1064 | } else {
1065 | params = "" + oldParams + "&" + expando;
1066 | }
1067 | }
1068 | return url + params + hash;
1069 | };
1070 |
1071 | return Reloader;
1072 |
1073 | })();
1074 |
1075 | }).call(this);
1076 |
1077 | },{}],8:[function(require,module,exports){
1078 | (function() {
1079 | var CustomEvents, LiveReload, k;
1080 |
1081 | CustomEvents = require('./customevents');
1082 |
1083 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window);
1084 |
1085 | for (k in window) {
1086 | if (k.match(/^LiveReloadPlugin/)) {
1087 | LiveReload.addPlugin(window[k]);
1088 | }
1089 | }
1090 |
1091 | LiveReload.addPlugin(require('./less'));
1092 |
1093 | LiveReload.on('shutdown', function() {
1094 | return delete window.LiveReload;
1095 | });
1096 |
1097 | LiveReload.on('connect', function() {
1098 | return CustomEvents.fire(document, 'LiveReloadConnect');
1099 | });
1100 |
1101 | LiveReload.on('disconnect', function() {
1102 | return CustomEvents.fire(document, 'LiveReloadDisconnect');
1103 | });
1104 |
1105 | CustomEvents.bind(document, 'LiveReloadShutDown', function() {
1106 | return LiveReload.shutDown();
1107 | });
1108 |
1109 | }).call(this);
1110 |
1111 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){
1112 | (function() {
1113 | var Timer;
1114 |
1115 | exports.Timer = Timer = (function() {
1116 | function Timer(func) {
1117 | this.func = func;
1118 | this.running = false;
1119 | this.id = null;
1120 | this._handler = (function(_this) {
1121 | return function() {
1122 | _this.running = false;
1123 | _this.id = null;
1124 | return _this.func();
1125 | };
1126 | })(this);
1127 | }
1128 |
1129 | Timer.prototype.start = function(timeout) {
1130 | if (this.running) {
1131 | clearTimeout(this.id);
1132 | }
1133 | this.id = setTimeout(this._handler, timeout);
1134 | return this.running = true;
1135 | };
1136 |
1137 | Timer.prototype.stop = function() {
1138 | if (this.running) {
1139 | clearTimeout(this.id);
1140 | this.running = false;
1141 | return this.id = null;
1142 | }
1143 | };
1144 |
1145 | return Timer;
1146 |
1147 | })();
1148 |
1149 | Timer.start = function(timeout, func) {
1150 | return setTimeout(func, timeout);
1151 | };
1152 |
1153 | }).call(this);
1154 |
1155 | },{}]},{},[8]);
1156 |
--------------------------------------------------------------------------------