Skip to content

Commit 79e2c83

Browse files
committed
Initial commit
0 parents  commit 79e2c83

File tree

12 files changed

+578
-0
lines changed

12 files changed

+578
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
/composer.lock

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
language: php
2+
php:
3+
- 5.3
4+
- 5.6
5+
- hhvm
6+
install:
7+
- composer install --prefer-source --no-interaction
8+
script:
9+
- phpunit --coverage-text

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Christian Lück
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 furnished
10+
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
21+
THE SOFTWARE.

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# clue/viewvc-api-react [![Build Status](https://travis-ci.org/clue/php-viewvc-api-react.svg?branch=master)](https://travis-ci.org/clue/php-viewvc-api-react)
2+
3+
Simple, async API-like access to your [ViewVC](http://viewvc.org/) web interface (Subversion/CVS browser), built on top of [React PHP](http://reactphp.org/).
4+
5+
> Note: This project is in early alpha stage! Feel free to report any issues you encounter.
6+
7+
## Quickstart example
8+
9+
Once [installed](#install), you can use the following code to fetch a directory
10+
listing from the given ViewVC URL:
11+
12+
```php
13+
$loop = React\EventLoop\Factory::create();
14+
$browser = new Clue\React\Buzz\Browser($loop);
15+
$client = new Client('http://example.com/viewvc/', $browser);
16+
17+
$client->fetchDirectory('/')->then(function ($files) {
18+
echo 'Files: ' . implode(', ', $files) . PHP_EOL;
19+
});
20+
21+
$loop->run();
22+
```
23+
24+
See also the [examples](examples).
25+
26+
## Usage
27+
28+
### Client
29+
30+
The `Client` is responsible for assembling and sending HTTP requests to the remote ViewVC web interface.
31+
It requires a [`Browser`](https://github.com/clue/php-buzz-react#browser) object
32+
bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage)
33+
in order to handle async requests:
34+
35+
```php
36+
$loop = React\EventLoop\Factory::create();
37+
$browser = new Clue\React\Buzz\Browser($loop);
38+
39+
$client = new Client($url, $browser);
40+
```
41+
42+
If you need custom DNS or proxy settings, you can explicitly pass a
43+
custom [`Browser`](https://github.com/clue/php-buzz-react#browser) instance.
44+
45+
#### Actions
46+
47+
ViewVC does not officially expose an API. However, its REST-like URLs make it
48+
easy to construct the right requests and scrape the results from its HTML
49+
output.
50+
All public methods resemble these respective actions otherwise available in the
51+
ViewVC web interface.
52+
53+
```php
54+
$client->fetchDirectory($path, $revision = null);
55+
$client->fetchFile($path, $revision = null);
56+
$client->fetchPatch($path, $r1, $r2);
57+
58+
// many more…
59+
```
60+
61+
Listing all available actions is out of scope here, please refer to the [class outline](src/Client.php).
62+
63+
#### Processing
64+
65+
Sending requests is async (non-blocking), so you can actually send multiple requests in parallel.
66+
ViewVC will respond to each request with a response message, the order is not guaranteed.
67+
Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a request is fulfilled (i.e. either successfully resolved or rejected with an error).
68+
69+
```php
70+
$client->fetchFile($path)->then(
71+
function ($contents) {
72+
// file contents received
73+
},
74+
function (Exception $e) {
75+
// an error occured while executing the request
76+
}
77+
});
78+
```
79+
80+
## Install
81+
82+
The recommended way to install this library is [through composer](http://getcomposer.org). [New to composer?](http://getcomposer.org/doc/00-intro.md)
83+
84+
```JSON
85+
{
86+
"require": {
87+
"clue/viewvc-api-react": "dev-master"
88+
}
89+
}
90+
```
91+
92+
## License
93+
94+
MIT

composer.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "clue/viewvc-api-react",
3+
"description": "Simple, async access to ViewVC web interface (Subversion/CVS browser)",
4+
"keywords": ["ViewVC", "Subversion", "CVS", "ReactPHP", "async"],
5+
"homepage": "https://github.com/clue/php-viewvc-api-react",
6+
"license": "MIT",
7+
"authors": [
8+
{
9+
"name": "Christian Lück",
10+
"email": "[email protected]"
11+
}
12+
],
13+
"autoload": {
14+
"psr-4": { "Clue\\React\\ViewVcApi\\": "src/" }
15+
},
16+
"require": {
17+
"php": ">=5.3",
18+
"react/event-loop": "~0.4.0|~0.3.0",
19+
"clue/buzz-react": "~0.2.0",
20+
"ext-simplexml": "*",
21+
"ext-tidy": "*"
22+
},
23+
"require-dev": {
24+
"clue/block-react": "~0.1.0"
25+
}
26+
}

examples/directory.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use Clue\React\ViewVcApi\Client;
4+
use React\EventLoop\Factory as LoopFactory;
5+
use Clue\React\Buzz\Browser;
6+
7+
require __DIR__ . '/../vendor/autoload.php';
8+
9+
$url = 'https://svn.apache.org/viewvc/';
10+
$path = isset($argv[1]) ? $argv[1] : '/';
11+
12+
$loop = LoopFactory::create();
13+
14+
// $dns = '10.52.166.2';
15+
// $resolver = new React\Dns\Resolver\Factory();
16+
// $resolver = $resolver->createCached($dns, $loop);
17+
// $connector = new React\SocketClient\Connector($loop, $resolver);
18+
19+
// $socks = new \Clue\React\Socks\Client($loop, '127.0.0.1', 9050);
20+
// $socks->setResolveLocal(false);
21+
// $connector = $socks->createConnector();
22+
23+
// $sender = Clue\React\Buzz\Io\Sender::createFromLoopConnectors($loop, $connector);
24+
$sender = null;
25+
26+
$browser = new Browser($loop, $sender);
27+
28+
$client = new Client($url, $browser);
29+
30+
$client->fetchDirectory($path)->then('var_dump', 'var_dump');
31+
32+
$loop->run();

phpunit.xml.dist

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit bootstrap="tests/bootstrap.php"
4+
colors="true"
5+
convertErrorsToExceptions="true"
6+
convertNoticesToExceptions="true"
7+
convertWarningsToExceptions="true"
8+
>
9+
<testsuites>
10+
<testsuite name="ViewVC Test Suite">
11+
<directory>./tests/</directory>
12+
</testsuite>
13+
</testsuites>
14+
<filter>
15+
<whitelist>
16+
<directory>./src/</directory>
17+
</whitelist>
18+
</filter>
19+
</phpunit>

src/Client.php

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace Clue\React\ViewVcApi;
4+
5+
use RuntimeException;
6+
use UnderflowException;
7+
use SimpleXMLElement;
8+
use InvalidArgumentException;
9+
use Clue\React\Buzz\Browser;
10+
use Clue\React\Buzz\Message\Response;
11+
use React\Promise\Deferred;
12+
13+
class Client
14+
{
15+
private $url;
16+
private $brower;
17+
18+
public function __construct($url, Browser $browser, Parser $parser = null)
19+
{
20+
if ($parser === null) {
21+
$parser = new Parser();
22+
}
23+
24+
// TODO: do not follow redirects
25+
// $browser = $this->browser->withOptions(array(
26+
// 'follow_redirects' => false
27+
// ));
28+
29+
$this->url = $url;
30+
$this->browser = $browser;
31+
$this->parser = $parser;
32+
}
33+
34+
public function fetchFile($path, $revision = null)
35+
{
36+
if (substr($path, -1) === '/') {
37+
return $this->reject(new InvalidArgumentException('File path MUST NOT end with trailing slash'));
38+
}
39+
40+
$url = $path . '?view=co';
41+
if ($revision !== null) {
42+
$url .= '&revision=' . $revision;
43+
}
44+
45+
// TODO: fetching a directory redirects to path with trailing slash
46+
// TODO: status returns 200 OK, but displays an error message anyways..
47+
// TODO: see not-a-file.html
48+
// TODO: reject all paths with trailing slashes
49+
50+
return $this->fetch($url);
51+
}
52+
53+
public function fetchDirectory($path, $revision = null)
54+
{
55+
if (substr($path, -1) !== '/') {
56+
return $this->reject(new InvalidArgumentException('Directory path MUST end with trailing slash'));
57+
}
58+
59+
$url = $path;
60+
61+
if ($revision !== null) {
62+
$url .= '?pathrev=' . $revision;
63+
}
64+
65+
// TODO: path MUST end with trailing slash
66+
// TODO: accessing files will redirect to file with relative location URL (not supported by clue/buzz-react)
67+
68+
return $this->fetchXml($url)->then(function (SimpleXMLElement $xml) {
69+
// TODO: reject if this is a file, instead of directory => contains "Log of" instead of "Index of"
70+
// TODO: see is-a-file.html
71+
72+
return $xml;
73+
})->then(array($this->parser, 'parseDirectoryListing'));
74+
}
75+
76+
public function fetchPatch($path, $r1, $r2)
77+
{
78+
$url = $path . '?view=patch&r1=' . $r1 . '&r2=' . $r2;
79+
80+
return $this->fetch($url);
81+
}
82+
83+
public function fetchRevisionPrevious($path, $revision)
84+
{
85+
return $this->fetchAllPreviousRevisions($path)->then(function ($revisions) use ($revision) {
86+
if (!isset($revisions[$revision])) {
87+
throw new UnderflowException('Unable to find previous version of given revision');
88+
}
89+
90+
return $revisions[$revision];
91+
});
92+
}
93+
94+
public function fetchAllPreviousRevisions($path)
95+
{
96+
return $this->fetchLogXml($path)->then(array($this->parser, 'parseLogRevisions'));
97+
}
98+
99+
private function fetchLogXml($path)
100+
{
101+
$url = $path . '?view=log';
102+
103+
return $this->fetchXml($url);
104+
}
105+
106+
private function fetchXml($url)
107+
{
108+
return $this->fetch($url)->then(function ($html) {
109+
// clean up HTML to safe XML
110+
$html = tidy_repair_string($html, array(
111+
'output-xml' => true,
112+
'input-xml' => true,
113+
));
114+
115+
// clean up namespace declaration
116+
$html = str_replace('xmlns=', 'ns=', $html);
117+
118+
return new SimpleXMLElement($html);
119+
});
120+
}
121+
122+
private function fetch($url)
123+
{
124+
return $this->browser->get($this->url . $url)->then(
125+
function (Response $response) {
126+
return (string)$response->getBody();
127+
},
128+
function ($error) {
129+
throw new RuntimeException('Unable to fetch from ViewVC', 0, $error);
130+
}
131+
);
132+
}
133+
134+
private function reject($with)
135+
{
136+
$deferred = new Deferred();
137+
$deferred->reject($with);
138+
139+
return $deferred->promise();
140+
}
141+
}

0 commit comments

Comments
 (0)