From 03a6809510bb0f5e0303cc7dcb8572f791be25ee Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 17 May 2014 10:10:38 +0200 Subject: [PATCH] Added API controller --- lib/chibi-core | 2 +- src/Api/Api.php | 18 +++++- src/Api/ApiFileOutput.php | 13 +++- src/Api/Jobs/AbstractJob.php | 11 ++++ src/Controllers/ApiController.php | 59 ++++++++++++++++++ src/CustomMarkdown.php | 4 +- src/Helpers/TextHelper.php | 45 -------------- src/ISerializable.php | 5 ++ src/Models/Entities/CommentEntity.php | 10 ++++ src/Models/Entities/PostEntity.php | 24 +++++++- src/Models/Entities/TagEntity.php | 11 +++- src/Models/Entities/TokenEntity.php | 11 ++++ src/Models/Entities/UserEntity.php | 14 ++++- src/RecursiveSerializer.php | 60 +++++++++++++++++++ src/Views/layout-json.phtml | 7 ++- src/routes.php | 2 + tests/Tests/AbstractFullApiTest.php | 18 +----- .../ControllerTests/ApiControllerTest.php | 43 +++++++++++++ 18 files changed, 285 insertions(+), 72 deletions(-) create mode 100644 src/Controllers/ApiController.php create mode 100644 src/ISerializable.php create mode 100644 src/RecursiveSerializer.php create mode 100644 tests/Tests/ControllerTests/ApiControllerTest.php diff --git a/lib/chibi-core b/lib/chibi-core index 78597486..36a45be3 160000 --- a/lib/chibi-core +++ b/lib/chibi-core @@ -1 +1 @@ -Subproject commit 78597486abb1bd6344a64a70c87d9deca14b7ff7 +Subproject commit 36a45be354e5a623a21f21d48c63b11e98b95ddf diff --git a/src/Api/Api.php b/src/Api/Api.php index 67d92863..05fe4fd2 100644 --- a/src/Api/Api.php +++ b/src/Api/Api.php @@ -1,7 +1,7 @@ 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'); + }); + } } diff --git a/src/Api/ApiFileOutput.php b/src/Api/ApiFileOutput.php index b0baf473..9800237f 100644 --- a/src/Api/ApiFileOutput.php +++ b/src/Api/ApiFileOutput.php @@ -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)), + ]; + } } diff --git a/src/Api/Jobs/AbstractJob.php b/src/Api/Jobs/AbstractJob.php index 54096b55..ac2efe68 100644 --- a/src/Api/Jobs/AbstractJob.php +++ b/src/Api/Jobs/AbstractJob.php @@ -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; diff --git a/src/Controllers/ApiController.php b/src/Controllers/ApiController.php new file mode 100644 index 00000000..8488807a --- /dev/null +++ b/src/Controllers/ApiController.php @@ -0,0 +1,59 @@ +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; + } +} diff --git a/src/CustomMarkdown.php b/src/CustomMarkdown.php index 4acd7023..3778a14c 100644 --- a/src/CustomMarkdown.php +++ b/src/CustomMarkdown.php @@ -96,8 +96,8 @@ class CustomMarkdown extends \Michelf\MarkdownExtra $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); $codeblock = preg_replace('/\n/', '
', $codeblock); - $codeblock = preg_replace('/\t/', '&tab;', $codeblock); - $codeblock = preg_replace('/ /', ' ', $codeblock); + #$codeblock = preg_replace('/\t/', '&tab;', $codeblock); + #$codeblock = preg_replace('/ /', ' ', $codeblock); $codeblock = "
$codeblock\n
"; return "\n\n".$this->hashBlock($codeblock)."\n\n"; diff --git a/src/Helpers/TextHelper.php b/src/Helpers/TextHelper.php index a5832a88..71f803ef 100644 --- a/src/Helpers/TextHelper.php +++ b/src/Helpers/TextHelper.php @@ -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) diff --git a/src/ISerializable.php b/src/ISerializable.php new file mode 100644 index 00000000..1e24a40b --- /dev/null +++ b/src/ISerializable.php @@ -0,0 +1,5 @@ +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(); diff --git a/src/Models/Entities/PostEntity.php b/src/Models/Entities/PostEntity.php index a780c76b..7c6cce6b 100644 --- a/src/Models/Entities/PostEntity.php +++ b/src/Models/Entities/PostEntity.php @@ -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())) diff --git a/src/Models/Entities/TagEntity.php b/src/Models/Entities/TagEntity.php index a40f1662..e5aa6a87 100644 --- a/src/Models/Entities/TagEntity.php +++ b/src/Models/Entities/TagEntity.php @@ -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; diff --git a/src/Models/Entities/TokenEntity.php b/src/Models/Entities/TokenEntity.php index f99fe368..8bdb84a0 100644 --- a/src/Models/Entities/TokenEntity.php +++ b/src/Models/Entities/TokenEntity.php @@ -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)) diff --git a/src/Models/Entities/UserEntity.php b/src/Models/Entities/UserEntity.php index b09e5ea2..50eb808b 100644 --- a/src/Models/Entities/UserEntity.php +++ b/src/Models/Entities/UserEntity.php @@ -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(); diff --git a/src/RecursiveSerializer.php b/src/RecursiveSerializer.php new file mode 100644 index 00000000..3223a5e8 --- /dev/null +++ b/src/RecursiveSerializer.php @@ -0,0 +1,60 @@ +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()) + ]; + } +} diff --git a/src/Views/layout-json.phtml b/src/Views/layout-json.phtml index 9a200cbb..52eaa891 100644 --- a/src/Views/layout-json.phtml +++ b/src/Views/layout-json.phtml @@ -1,3 +1,6 @@ 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); diff --git a/src/routes.php b/src/routes.php index 05e462cd..c38346ba 100644 --- a/src/routes.php +++ b/src/routes.php @@ -1,4 +1,6 @@ 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'); - }); - } } diff --git a/tests/Tests/ControllerTests/ApiControllerTest.php b/tests/Tests/ControllerTests/ApiControllerTest.php new file mode 100644 index 00000000..508b150f --- /dev/null +++ b/tests/Tests/ControllerTests/ApiControllerTest.php @@ -0,0 +1,43 @@ +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()); + } +}