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());
+ }
+}