Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add CSRF token support for OpenRefine 3.3 onwards #8

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 166 additions & 40 deletions src/Keboola/OpenRefine/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use GuzzleHttp\Psr7\Response;
use Keboola\Csv\CsvFile;
use Keboola\Temp\Temp;
use Psr\Http\Message\ResponseInterface;

class Client
{
Expand All @@ -21,17 +22,36 @@ class Client
*/
protected $temp;

/**
* the CSRF token
*
* @var null|string
*/
protected $csrfToken;

/**
* OpenRefine server version
*
* @var null|string
*/
private static $version = null;

/**
* Minimum version of OpenRefine for which the CSRF token must be used
*/
protected const MIN_VERSION_FOR_CSRF = '3.3';

/**
* Client constructor.
*
* @param string $host
* @param string $port
* @param Temp|null $temp
*/
public function __construct(string $host = "localhost", string $port = "3333", ?Temp $temp = null)
public function __construct(string $host = 'localhost', string $port = '3333', ?Temp $temp = null)
{
$this->client = new \GuzzleHttp\Client([
"base_uri" => "http://" . $host . ":" . $port . "/command/core/",
'base_uri' => 'http://' . $host . ':' . $port . '/command/core/',
]);
if (!$temp) {
$temp = new Temp();
Expand All @@ -42,40 +62,39 @@ public function __construct(string $host = "localhost", string $port = "3333", ?
public function createProject(CsvFile $file, string $name): string
{
if ($file->getColumnsCount() === 0) {
throw new Exception("Empty file");
throw new Exception('Empty file');
}

try {
$response = $this->client->request(
"POST",
"create-project-from-upload",
$response = $this->post(
'create-project-from-upload',
[
"multipart" => [
'multipart' => [
[
"name" => "project-file",
"contents" => fopen($file->getPathname(), "r"),
'name' => 'project-file',
'contents' => fopen($file->getPathname(), 'r'),
],
[
"name" => "project-name",
"contents" => $name,
'name' => 'project-name',
'contents' => $name,
],
],
"allow_redirects" => false,
'allow_redirects' => false,
]
);
} catch (ServerException $e) {
$response = $e->getResponse();
if ($response && $response->getReasonPhrase() === 'GC overhead limit exceeded') {
throw new Exception("OpenRefine is out of memory. Data set too large.");
throw new Exception('OpenRefine is out of memory. Data set too large.');
}
throw $e;
}

if ($response->getStatusCode() !== 302) {
throw new Exception("Cannot create project: {$response->getStatusCode()}");
}
$url = $response->getHeader("Location")[0];
$projectId = substr($url, strrpos($url, "=") + 1);
$url = $response->getHeader('Location')[0];
$projectId = substr($url, strrpos($url, '=') + 1);
return $projectId;
}

Expand All @@ -87,20 +106,19 @@ public function createProject(CsvFile $file, string $name): string
public function applyOperations(string $projectId, array $operations): void
{
try {
$response = $this->client->request(
"POST",
"apply-operations",
$response = $this->post(
'apply-operations',
[
"form_params" => [
"project" => $projectId,
"operations" => json_encode($operations),
'form_params' => [
'project' => $projectId,
'operations' => json_encode($operations),
],
]
);
} catch (ServerException $e) {
$response = $e->getResponse();
if ($response && $response->getReasonPhrase() === 'GC overhead limit exceeded') {
throw new Exception("OpenRefine is out of memory. Data set too large.");
throw new Exception('OpenRefine is out of memory. Data set too large.');
}
throw $e;
}
Expand All @@ -116,20 +134,20 @@ public function applyOperations(string $projectId, array $operations): void

public function exportRowsToCsv(string $projectId): CsvFile
{
$response = $this->client->request("POST", "export-rows", [
"form_params" => [
"project" => $projectId,
"format" => "csv",
$response = $this->post('export-rows', [
'form_params' => [
'project' => $projectId,
'format' => 'csv',
],
]);
if ($response->getStatusCode() !== 200) {
throw new Exception("Cannot export rows: ({$response->getStatusCode()}) {$response->getBody()}");
}

$fileName = $this->temp->createFile("data.csv", true)->getPathname();
$handle = fopen($fileName, "w");
$fileName = $this->temp->createFile('data.csv', true)->getPathname();
$handle = fopen($fileName, 'w');
if (!$handle) {
throw new Exception("Cannot open file " . $fileName . " for writing.");
throw new Exception('Cannot open file ' . $fileName . ' for writing.');
}
$buffer = $response->getBody()->read(1000);
while ($buffer !== '') {
Expand All @@ -147,7 +165,7 @@ public function exportRowsToCsv(string $projectId): CsvFile
*/
public function getProjectMetadata(string $projectId)
{
$response = $this->client->request("GET", "get-project-metadata?project={$projectId}");
$response = $this->client->request('GET', "get-project-metadata?project={$projectId}");
if ($this->isResponseError($response)) {
throw new Exception("Project not found: {$this->getResponseError($response)}");
}
Expand All @@ -157,9 +175,9 @@ public function getProjectMetadata(string $projectId)

public function deleteProject(string $projectId): void
{
$response = $this->client->request("POST", "delete-project", [
"form_params" => [
"project" => $projectId,
$response = $this->post('delete-project', [
'form_params' => [
'project' => $projectId,
],
]);

Expand All @@ -172,11 +190,11 @@ public function deleteProject(string $projectId): void
}
}

protected function isResponseError(Response $response): bool
protected function isResponseError(ResponseInterface $response): bool
{
$decodedResponse = json_decode($response->getBody()->__toString(), true);
if (isset($decodedResponse["status"]) && $decodedResponse["status"] === "error" ||
isset($decodedResponse["code"]) && $decodedResponse["code"] === "error"
if (isset($decodedResponse['status']) && $decodedResponse['status'] === 'error' ||
isset($decodedResponse['code']) && $decodedResponse['code'] === 'error'
) {
return true;
}
Expand All @@ -190,11 +208,119 @@ protected function isResponseError(Response $response): bool
protected function getResponseError(Response $response)
{
$decodedResponse = json_decode($response->getBody()->__toString(), true);
if (isset($decodedResponse["status"])) {
return $decodedResponse["status"];
if (isset($decodedResponse['status'])) {
return $decodedResponse['status'];
}
if (isset($decodedResponse['code'])) {
return $decodedResponse['code'];
}
}

/**
* Sets the OpenRefine version calling the get-version endpoint
*
* @return void
*/
private function setVersion(): void
{
if (is_null(self::$version)) {
self::$version = '0.0';
$response = $this->client->request('GET', 'get-version');
if (!$this->isResponseError($response)) {
$decodedResponse = json_decode($response->getBody()->__toString(), true);
if (array_key_exists('version', $decodedResponse)) {
self::$version = $decodedResponse['version'];
}
}
}
if (isset($decodedResponse["code"])) {
return $decodedResponse["code"];
}

/**
* Gets the OpenRefine version
*
* @return string|null
*/
protected function getVersion(): ?string
{
if (is_null(self::$version)) {
$this->setVersion();
}
return self::$version;
}

/**
* Gets the CSRF token
*
* @return string|null
*/
protected function getCsrfToken(): ?string
{
try {
$this->setCsrfToken();
} catch (Exception $e) {
$this->csrfToken = '';
}
return $this->csrfToken;
}

/**
* Sets the CSRF token calling the get-csrf-token endpoint
*
* @return void
*/
protected function setCsrfToken(): void
{
$token = '';
$error = false;
$version = is_null(self::getVersion()) ? '0.0' : self::getVersion();
if (is_null($this->csrfToken) && version_compare($version, self::MIN_VERSION_FOR_CSRF, '>=')) {
$response = $this->client->request('GET', 'get-csrf-token');
if (!$this->isResponseError($response)) {
$decodedResponse = json_decode($response->getBody()->__toString(), true);
if (array_key_exists('token', $decodedResponse)) {
$token = $decodedResponse['token'];
} else {
$error = true;
}
} else {
$error = true;
}
}
if ($error) {
throw new Exception('Cannot GET the CSRF token');
}
$this->csrfToken = $token;
}

/**
* Does the post request using the client and setting the CSRF token if needed
*
* @param string $endpoint
* @param array $params
* @return ResponseInterface
*/
protected function post(string $endpoint, array $params = []): ResponseInterface
{
$version = is_null(self::getVersion()) ? '0.0' : self::getVersion();
if (version_compare($version, self::MIN_VERSION_FOR_CSRF, '>=')) {
$this->csrfToken = $this->getCsrfToken();
if ($this->csrfToken !== '') {
if (stristr($endpoint, 'create') !== false) {
$endpoint .= '?csrf_token='.$this->csrfToken;
} else if (array_key_exists('multipart', $params)) {
array_push($params['multipart'], [
'name' => 'csrf_token',
'contents' => $this->csrfToken,
]);
} else if (array_key_exists('form_params', $params)) {
$params['form_params']['csrf_token'] = $this->csrfToken;
} else {
$params['csrf_token'] = $this->csrfToken;
}
}
// The CSRF token is a one timer, forget it to get a new one
$this->csrfToken = null;
}
return $this->client->request('POST', $endpoint, $params);
}
}
10 changes: 5 additions & 5 deletions tests/Keboola/OpenRefine/ApplyOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ class ApplyOperationsTest extends \PHPUnit_Framework_TestCase
{
public function testApplyOperationsSuccess(): void
{
$client = new Client(getenv("OPENREFINE_HOST"), getenv("OPENREFINE_PORT"));
$client = new Client(getenv('OPENREFINE_HOST'), getenv('OPENREFINE_PORT'));
$temp = new Temp();
$fileInfo = $temp->createFile("file.csv");
$fileInfo = $temp->createFile('file.csv');
$csv = new CsvFile($fileInfo->getPathname());
$csv->writeRow(["col1", "col2"]);
$csv->writeRow(["A", "B"]);
$projectId = $client->createProject($csv, "test");
$csv->writeRow(['col1', 'col2']);
$csv->writeRow(['A', 'B']);
$projectId = $client->createProject($csv, 'test');

$operationsJSON = <<<JSON
[
Expand Down
26 changes: 13 additions & 13 deletions tests/Keboola/OpenRefine/CreateProjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,29 @@ class CreateProjectTest extends \PHPUnit_Framework_TestCase
{
public function testCreateProjectSuccess(): void
{
$client = new Client(getenv("OPENREFINE_HOST"), getenv("OPENREFINE_PORT"));
$client = new Client(getenv('OPENREFINE_HOST'), getenv('OPENREFINE_PORT'));
$temp = new Temp();
$fileInfo = $temp->createFile("file.csv");
$fileInfo = $temp->createFile('file.csv');
$csv = new CsvFile($fileInfo->getPathname());
$csv->writeRow(["col1", "col2"]);
$csv->writeRow(["A", "B"]);
$projectId = $client->createProject($csv, "test");
$this->assertNotNull($projectId, "Did not return project id");
$this->assertRegExp("/^[0-9]*$/", $projectId);
$csv->writeRow(['col1', 'col2']);
$csv->writeRow(['A', 'B']);
$projectId = $client->createProject($csv, 'test');
$this->assertNotNull($projectId, 'Did not return project id');
$this->assertRegExp('/^[0-9]*$/', $projectId);
$this->assertGreaterThan(0, $projectId);
$this->assertEquals("test", $client->getProjectMetadata($projectId)["name"]);
$this->assertEquals('test', $client->getProjectMetadata($projectId)['name']);
$outCsv = $client->exportRowsToCsv($projectId);
$this->assertEquals("col1,col2\nA,B\n", file_get_contents($outCsv->getPathname()));
$this->assertEquals('col1,col2\nA,B\n', file_get_contents($outCsv->getPathname()));
}

public function testsCreateProjectEmptyFile(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage("Empty file");
$client = new Client(getenv("OPENREFINE_HOST"), getenv("OPENREFINE_PORT"));
$this->expectExceptionMessage('Empty file');
$client = new Client(getenv('OPENREFINE_HOST'), getenv('OPENREFINE_PORT'));
$temp = new Temp();
$fileInfo = $temp->createFile("empty_file.csv");
$fileInfo = $temp->createFile('empty_file.csv');
$csv = new CsvFile($fileInfo->getPathname());
$client->createProject($csv, "test");
$client->createProject($csv, 'test');
}
}
10 changes: 5 additions & 5 deletions tests/Keboola/OpenRefine/DeleteProjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ class DeleteProjectTest extends \PHPUnit_Framework_TestCase
{
public function testDeleteProjectSuccess(): void
{
$client = new Client(getenv("OPENREFINE_HOST"), getenv("OPENREFINE_PORT"));
$client = new Client(getenv('OPENREFINE_HOST'), getenv('OPENREFINE_PORT'));
$temp = new Temp();
$fileInfo = $temp->createFile("file.csv");
$fileInfo = $temp->createFile('file.csv');
$csv = new CsvFile($fileInfo->getPathname());
$csv->writeRow(["col1", "col2"]);
$csv->writeRow(["A", "B"]);
$projectId = $client->createProject($csv, "test");
$csv->writeRow(['col1', 'col2']);
$csv->writeRow(['A', 'B']);
$projectId = $client->createProject($csv, 'test');
$client->deleteProject($projectId);
}
}
Loading