Added API controller

This commit is contained in:
Marcin Kurczewski 2014-05-17 10:10:38 +02:00
parent e95b8d93d8
commit 03a6809510
18 changed files with 285 additions and 72 deletions

@ -1 +1 @@
Subproject commit 78597486abb1bd6344a64a70c87d9deca14b7ff7
Subproject commit 36a45be354e5a623a21f21d48c63b11e98b95ddf

View file

@ -1,7 +1,7 @@
<?php
final class Api
{
public static function run($job, $jobArgs)
public static function run(IJob $job, $jobArgs)
{
$user = Auth::getCurrentUser();
@ -91,4 +91,20 @@ final class Api
elseif (!$job->hasArgument($item))
throw new ApiJobUnsatisfiedException($job, $item);
}
public static function getAllJobClassNames()
{
$pathToJobs = Core::getConfig()->rootDir . DS . 'src' . DS . 'Api' . DS . 'Jobs';
$directory = new RecursiveDirectoryIterator($pathToJobs);
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+Job\.php$/i');
$files = array_keys(iterator_to_array($regex));
\Chibi\Util\Reflection::loadClasses($files);
return array_filter(get_declared_classes(), function($x)
{
$class = new ReflectionClass($x);
return !$class->isAbstract() and $class->isSubClassOf('AbstractJob');
});
}
}

View file

@ -2,7 +2,7 @@
/**
* Used for serializing files output from jobs
*/
class ApiFileOutput
class ApiFileOutput implements ISerializable
{
public $fileContent;
public $fileName;
@ -16,4 +16,15 @@ class ApiFileOutput
$this->lastModified = filemtime($filePath);
$this->mimeType = mime_content_type($filePath);
}
public function serializeToArray()
{
return
[
'name ' => $this->fileName,
'modification-time' => $this->lastModified,
'mime-type' => $this->mimeType,
'content' => base64_encode(gzencode($this->fileContent)),
];
}
}

View file

@ -16,6 +16,17 @@ abstract class AbstractJob implements IJob
public abstract function getRequiredArguments();
public function getName()
{
$name = get_called_class();
$name = str_replace('Job', '', $name);
$name = TextCaseConverter::convert(
$name,
TextCaseConverter::UPPER_CAMEL_CASE,
TextCaseConverter::SPINAL_CASE);
return $name;
}
public function getRequiredPrivileges()
{
return false;

View file

@ -0,0 +1,59 @@
<?php
class ApiController extends AbstractController
{
public function runAction()
{
$context = Core::getContext();
try
{
if (!Auth::isLoggedIn())
{
$auth = InputHelper::get('auth');
if ($auth)
{
Auth::login($auth['user'], $auth['pass'], false);
}
}
$jobName = InputHelper::get('name');
$jobArgs = InputHelper::get('args');
$job = $this->jobFromName($jobName);
if (!$job)
throw new SimpleException('Unknown job: ' . $jobName);
if (isset($_FILES['args']))
{
foreach (array_keys($_FILES['args']['name']) as $key)
{
$jobArgs[$key] = new ApiFileInput(
$_FILES['args']['tmp_name'][$key],
$_FILES['args']['name'][$key]);
}
}
$context->transport->status = Api::run($job, $jobArgs);
}
catch (Exception $e)
{
Messenger::fail($e->getMessage());
}
$this->renderAjax();
}
private function jobFromName($jobName)
{
$jobClassNames = Api::getAllJobClassNames();
foreach ($jobClassNames as $className)
{
$job = (new ReflectionClass($className))->newInstance();
if ($job->getName() == $jobName)
return $job;
$job = null;
}
return null;
}
}

View file

@ -96,8 +96,8 @@ class CustomMarkdown extends \Michelf\MarkdownExtra
$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
$codeblock = preg_replace('/\n/', '<br/>', $codeblock);
$codeblock = preg_replace('/\t/', '&tab;', $codeblock);
$codeblock = preg_replace('/ /', '&nbsp;', $codeblock);
#$codeblock = preg_replace('/\t/', '&tab;', $codeblock);
#$codeblock = preg_replace('/ /', '&nbsp;', $codeblock);
$codeblock = "<pre><code>$codeblock\n</code></pre>";
return "\n\n".$this->hashBlock($codeblock)."\n\n";

View file

@ -151,51 +151,6 @@ class TextHelper
return self::stripUnits($string, 1000, ['', 'K', 'M']);
}
public static function removeUnsafeKeys(&$input, $regex)
{
if (is_array($input))
{
foreach ($input as $key => $val)
{
if (preg_match($regex, $key))
unset($input[$key]);
else
self::removeUnsafeKeys($input[$key], $regex);
}
}
elseif (is_object($input))
{
foreach ($input as $key => $val)
{
if (preg_match($regex, $key))
unset($input->$key);
else
self::removeUnsafeKeys($input->$key, $regex);
}
}
}
public static function jsonEncode($obj, $illegalKeysRegex = '')
{
if (is_array($obj))
$set = function($key, $val) use ($obj) { $obj[$key] = $val; };
else
$set = function($key, $val) use ($obj) { $obj->$key = $val; };
foreach ($obj as $key => $val)
{
if ($val instanceof Exception)
{
$set($key, ['message' => $val->getMessage(), 'trace' => explode("\n", $val->getTraceAsString())]);
}
}
if (!empty($illegalKeysRegex))
self::removeUnsafeKeys($obj, $illegalKeysRegex);
return json_encode($obj, JSON_UNESCAPED_UNICODE);
}
public static function parseMarkdown($text, $simple = false)
{
if ($simple)

5
src/ISerializable.php Normal file
View file

@ -0,0 +1,5 @@
<?php
interface ISerializable
{
public function serializeToArray();
}

View file

@ -20,6 +20,16 @@ final class CommentEntity extends AbstractEntity implements IValidatable
$this->commenterId = TextHelper::toIntegerOrNull($row['commenter_id']);
}
public function serializeToArray()
{
return
[
'text' => $this->getText(),
'comment-time' => $this->getCreationTime(),
'commenter' => $this->getCommenter() ? $this->getCommenter()->getName() : null,
];
}
public function validate()
{
$config = Core::getConfig();

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
final class PostEntity extends AbstractEntity implements IValidatable
final class PostEntity extends AbstractEntity implements IValidatable, ISerializable
{
private $type;
private $name;
@ -52,6 +52,28 @@ final class PostEntity extends AbstractEntity implements IValidatable
$this->setSafety(new PostSafety($row['safety']));
}
public function serializeToArray()
{
return
[
'name' => $this->getName(),
'id' => $this->getId(),
'orig-name' => $this->getOriginalName(),
'file-hash' => $this->getFileHash(),
'file-size' => $this->getFileSize(),
'mime-type' => $this->getMimeType(),
'is-hidden' => $this->isHidden(),
'creation-time' => $this->getCreationTime(),
'image-width' => $this->getImageWidth(),
'image-height' => $this->getImageHeight(),
'uploader' => $this->getUploader() ? $this->getUploader()->getName() : null,
'comments' => array_map(function($comment) { return $comment->serializeToArray(); }, $this->getComments()),
'tags' => array_map(function($tag) { return $tag->getName(); }, $this->getTags()),
'type' => $this->getType()->toInteger(),
'safety' => $this->getSafety()->toInteger(),
];
}
public function validate()
{
if (empty($this->getType()))

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
final class TagEntity extends AbstractEntity implements IValidatable
final class TagEntity extends AbstractEntity implements IValidatable, ISerializable
{
private $name;
@ -19,6 +19,15 @@ final class TagEntity extends AbstractEntity implements IValidatable
$this->setCache('post_count', (int) $row['post_count']);
}
public function serializeToArray()
{
return
[
'name' => $this->getName(),
'post-count' => $this->getPostCount(),
];
}
public function validate()
{
$minLength = Core::getConfig()->tags->minLength;

View file

@ -31,6 +31,17 @@ final class TokenEntity extends AbstractEntity implements IValidatable
$this->expires = $row['expires'];
}
public function serializeToArray()
{
return
[
'user' => $this->getUser(),
'text' => $this->getText(),
'is-used' => $this->isUser(),
'expiration-time' => $this->getExpirationTime(),
];
}
public function validate()
{
if (empty($this->token))

View file

@ -2,7 +2,7 @@
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
final class UserEntity extends AbstractEntity implements IValidatable
final class UserEntity extends AbstractEntity implements IValidatable, ISerializable
{
private $name;
private $passSalt;
@ -42,6 +42,18 @@ final class UserEntity extends AbstractEntity implements IValidatable
$this->settings = new UserSettings($row['settings']);
}
public function serializeToArray()
{
return
[
'name' => $this->getName(),
'join-time' => $this->getJoinTime(),
'last-login-time' => $this->getLastLoginTime(),
'access-rank' => $this->getAccessRank()->toInteger(),
'is-banned' => $this->isBanned(),
];
}
public function validate()
{
$this->validateUserName();

View file

@ -0,0 +1,60 @@
<?php
class RecursiveSerializer implements ISerializable
{
public function __construct($input)
{
$this->input = $input;
}
public function serializeToArray()
{
return
$output = $this->traverse($this->input);
}
private function traverse($input)
{
if (is_array($input))
{
foreach ($input as $key => $val)
{
$input[$key] = $this->traverse($input[$key]);
}
return $input;
}
elseif ($input instanceof ISerializable)
{
return $input->serializeToArray();
}
elseif ($input instanceof Exception)
{
return $this->serializeException($input);
}
elseif (is_object($input))
{
foreach ($input as $key => $val)
{
$input->$key = $this->traverse($input->$key);
}
return $input;
}
return $input;
}
private function serializePost(PostEntity $post)
{
return
[
'name' => $post->getName(),
];
}
private function serializeException(Exception $exception)
{
return
[
'message' => $exception->getMessage(),
'trace' => explode("\n", $exception->getTraceAsString())
];
}
}

View file

@ -1,3 +1,6 @@
<?php
\Chibi\Util\Headers::set('Content-Type', 'application/json');
echo TextHelper::jsonEncode($this->context->transport, '/.*(email|confirm|pass|salt)/i');
if (!headers_sent())
\Chibi\Util\Headers::set('Content-Type', 'application/json');
$serializer = new RecursiveSerializer($this->context->transport);
$array = $serializer->serializeToArray();
echo json_encode($array, JSON_UNESCAPED_UNICODE);

View file

@ -1,4 +1,6 @@
<?php
\Chibi\Router::register(['ApiController', 'runAction'], null, '/api');
\Chibi\Router::register(['StaticPagesController', 'mainPageView'], 'GET', '');
\Chibi\Router::register(['StaticPagesController', 'mainPageView'], 'GET', '/index');
\Chibi\Router::register(['StaticPagesController', 'helpView'], 'GET', '/help');

View file

@ -9,27 +9,11 @@ abstract class AbstractFullApiTest extends AbstractTest
{
return get_class($job);
}, $this->testedJobs);
$allJobs = $this->getAllJobs();
$allJobs = Api::getAllJobClassNames();
foreach ($allJobs as $x)
{
if (!in_array($x, $testedJobs))
$this->assert->fail($x . ' appears to be untested');
}
}
protected function getAllJobs()
{
$pathToJobs = Core::getConfig()->rootDir . DS . 'src' . DS . 'Api' . DS . 'Jobs';
$directory = new RecursiveDirectoryIterator($pathToJobs);
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+Job\.php$/i');
$files = array_keys(iterator_to_array($regex));
\Chibi\Util\Reflection::loadClasses($files);
return array_filter(get_declared_classes(), function($x)
{
$class = new ReflectionClass($x);
return !$class->isAbstract() and $class->isSubClassOf('AbstractJob');
});
}
}

View file

@ -0,0 +1,43 @@
<?php
class ApiControllerTest extends AbstractTest
{
public function testRunning()
{
Core::getConfig()->registration->needEmailForRegistering = false;
Core::getConfig()->registration->needEmailForUploading = false;
$user = $this->userMocker->mockSingle();
$this->grantAccess('addPost');
$this->grantAccess('addPostTags');
$this->grantAccess('addPostContent');
$_GET =
[
'auth' => ['pass' => 'sekai', 'user' => $user->getName()],
'name' => 'add-post',
'args' => ['new-tag-names' => ['test', 'test2', 'test3']],
];
$tmpPath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
copy($this->testSupport->getPath('image.jpg'), $tmpPath);
Core::getContext()->transport = new StdClass;
$_FILES =
[
'args' =>
[
'name' => ['new-post-content' => 'image.jpg'],
'tmp_name' => ['new-post-content' => $tmpPath],
],
];
ob_start();
$apiController = new ApiController();
$apiController->runAction();
$output = ob_get_contents();
ob_end_clean();
$this->assert->areEqual(1, PostModel::getCount());
}
}