diff --git a/data/config.ini b/data/config.ini
index ec8b4010..0e3e4310 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -1,14 +1,11 @@
-[chibi]
-enableCache=1
-
[main]
dbDriver = "sqlite"
dbLocation = "./data/db.sqlite"
dbUser = "test"
dbPass = "test"
filesPath = "./data/files/"
-thumbsPath = "./data/thumbs/"
-logsPath = "./data/logs/"
+thumbsPath = "./public_html/thumbs/"
+logsPath = "./data/logs/{yyyy}-{mm}.log"
mediaPath = "./public_html/media/"
title = "szurubooru"
salt = "1A2/$_4xVa"
@@ -17,6 +14,7 @@ salt = "1A2/$_4xVa"
featuredPostMaxDays=7
debugQueries=0
logAnonymousUploads=1
+githubLink = http://github.com/rr-/szurubooru
[help]
title=Help
@@ -42,6 +40,14 @@ showDislikedPostsDefault=1
maxSearchTokens=4
maxRelatedPosts=50
+[tags]
+minLength = 1
+maxLength = 64
+regex = "/^[()\[\]a-zA-Z0-9_.-]+$/i"
+
+[posts]
+maxSourceLength = 200
+
[comments]
minLength = 5
maxLength = 2000
@@ -70,17 +76,31 @@ passwordResetEmailSubject = "{host} - password reset"
passwordResetEmailBody = "Hello,{nl}{nl}You received this e-mail because someone requested a password reset for user with this e-mail address at {host}. If it's you, visit {link} to finish password reset process, otherwise you may ignore and delete this e-mail.{nl}{nl}Kind regards,{nl}{host} mailing system"
[privileges]
-uploadPost=registered
+registerAccount=anonymous
+;registerAccount=nobody
+
listPosts=anonymous
+listPosts.safe=anonymous
listPosts.sketchy=registered
listPosts.unsafe=registered
listPosts.hidden=moderator
viewPost=anonymous
+viewPost.safe=anonymous
viewPost.sketchy=registered
viewPost.unsafe=registered
viewPost.hidden=moderator
retrievePost=anonymous
favoritePost=registered
+
+addPost=registered
+addPostSafety=registered
+addPostTags=registered
+addPostThumb=power-user
+addPostSource=registered
+addPostRelations=power-user
+addPostContent=registered
+
+editPost=registered
editPostSafety.own=registered
editPostSafety.all=moderator
editPostTags=registered
@@ -88,7 +108,8 @@ editPostThumb=moderator
editPostSource=moderator
editPostRelations.own=registered
editPostRelations.all=moderator
-editPostFile=moderator
+editPostContent=moderator
+
massTag.own=registered
massTag.all=power-user
hidePost=moderator
@@ -99,16 +120,17 @@ flagPost=registered
listUsers=registered
viewUser=registered
-viewUserEmail.all=admin
viewUserEmail.own=registered
-changeUserPassword.own=registered
-changeUserPassword.all=admin
-changeUserEmail.own=registered
-changeUserEmail.all=admin
-changeUserAccessRank=admin
-changeUserName=moderator
-changeUserSettings.all=nobody
-changeUserSettings.own=registered
+viewUserEmail.all=admin
+editUserPassword.own=registered
+editUserPassword.all=admin
+editUserEmail.own=registered
+editUserEmail.all=admin
+editUserEmailNoConfirm=admin
+editUserAccessRank=admin
+editUserName=moderator
+editUserSettings.own=registered
+editUserSettings.all=nobody
acceptUserRegistration=moderator
banUser.own=nobody
banUser.all=admin
diff --git a/data/help.md b/data/help.md
index 80a1d8b8..c0ff3a65 100644
--- a/data/help.md
+++ b/data/help.md
@@ -39,8 +39,8 @@ Command | Description
[search]id:1,2,3[/search] | having specific post ID | `ids` |
[search]idmin:5[/search] | posts with ID greater than or equal to @5 | `id_min` |
[search]idmax:5[/search] | posts with ID less than or equal to @5 | `id_max` |
-[search]type:img[/search] | only image posts | - |
-[search]type:swf[/search] | only Flash posts | - |
+[search]type:img[/search] | only image posts | `type:image` |
+[search]type:flash[/search] | only Flash posts | `type:swf` |
[search]type:yt[/search] | only Youtube posts | `type:youtube` |
[search]special:liked[/search] | posts liked by currently logged in user | `special:likes`, `special:like` |
[search]special:disliked[/search] | posts disliked by currently logged in user | `special:dislikes`, `special:dislike` |
diff --git a/data/privacy.md b/data/privacy.md
index 21ebc3e2..0b253a3e 100644
--- a/data/privacy.md
+++ b/data/privacy.md
@@ -8,4 +8,4 @@ Your actions related to posts (uploading, tagging, etc.) are logged, along with
# Cookies
-Cookies are used to store your session data and browsing preferences, such as endless scrolling or visibility of NSFW posts.
+Cookies are used to store your session data in order to keep you logged in and personalize your web experience.
diff --git a/init.php b/init.php
index 6102642a..6849d60c 100644
--- a/init.php
+++ b/init.php
@@ -1,6 +1,6 @@
main->mediaPath . DS . 'fonts');
$libPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'lib');
@@ -26,6 +26,9 @@ function download($source, $destination = null)
return $content;
}
+$version = exec('git describe --tags --always --dirty');
+$branch = exec('git rev-parse --abbrev-ref HEAD');
+PropertyModel::set(PropertyModel::EngineVersion, $version . '@' . $branch);
//jQuery
diff --git a/lib/chibi-core b/lib/chibi-core
index 45c662d0..a6c610e5 160000
--- a/lib/chibi-core
+++ b/lib/chibi-core
@@ -1 +1 @@
-Subproject commit 45c662d0a4b32e09399b5b68ac53aaa3f1a29911
+Subproject commit a6c610e5c68220cf672debe765752662807c0d39
diff --git a/lib/chibi-sql b/lib/chibi-sql
index 22910a18..6bb18c1c 160000
--- a/lib/chibi-sql
+++ b/lib/chibi-sql
@@ -1 +1 @@
-Subproject commit 22910a186efbcb9bc86a3ae3eb6f4aff34096406
+Subproject commit 6bb18c1c6ed7ea952ae7a8dab792d5364a334201
diff --git a/public_html/.htaccess b/public_html/.htaccess
index 725416c0..4dc8c6ec 100644
--- a/public_html/.htaccess
+++ b/public_html/.htaccess
@@ -1,10 +1,14 @@
DirectorySlash Off
Options -Indexes
+ErrorDocument 403 /fatal-error/403
+ErrorDocument 404 /fatal-error/404
+ErrorDocument 500 /fatal-error/500
+
RewriteEngine On
-ErrorDocument 403 /dispatch.php?request=error/http&code=403
-ErrorDocument 404 /dispatch.php?request=error/http&code=404
-ErrorDocument 500 /dispatch.php?request=error/http&code=500
+RewriteCond %{DOCUMENT_ROOT}/thumbs/$1.thumb -f
+RewriteRule ^/?post/(.*)/thumb/?$ /thumbs/$1.thumb
+RewriteRule ^/?thumbs/(.*).thumb - [L,T=image/jpeg]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
diff --git a/public_html/dispatch.php b/public_html/dispatch.php
index 281f5100..7a8f9dd1 100644
--- a/public_html/dispatch.php
+++ b/public_html/dispatch.php
@@ -1,211 +1,5 @@
startTime = $startTime;
-$context->query = $query;
-
-function renderView()
-{
- $context = getContext();
- \Chibi\View::render($context->layoutName, $context);
-}
-
-function getContext()
-{
- global $context;
- return $context;
-}
-
-$context->simpleControllerName = null;
-$context->simpleActionName = null;
-
-\Chibi\Router::setObserver(function($route, $args)
-{
- $context = getContext();
- $context->route = $route;
- list ($className, $methodName) = $route->destination;
-
- $context->simpleControllerName = TextCaseConverter::convert(
- str_replace('Controller', '', $className),
- TextCaseConverter::CAMEL_CASE,
- TextCaseConverter::SPINAL_CASE);
-
- $context->simpleActionName = TextCaseConverter::convert(
- str_replace('Action', '', $methodName),
- TextCaseConverter::CAMEL_CASE,
- TextCaseConverter::SPINAL_CASE);
-
- $context->viewName = sprintf(
- '%s-%s',
- $context->simpleControllerName,
- $context->simpleActionName);
-});
-
-foreach (['GET', 'POST'] as $method)
-{
- \Chibi\Router::register(['IndexController', 'indexAction'], $method, '');
- \Chibi\Router::register(['IndexController', 'indexAction'], $method, '/index');
- \Chibi\Router::register(['IndexController', 'helpAction'], $method, '/help');
- \Chibi\Router::register(['IndexController', 'helpAction'], $method, '/help/{tab}');
- \Chibi\Router::register(['LogController', 'listAction'], $method, '/logs');
- \Chibi\Router::register(['LogController', 'viewAction'], $method, '/log/{name}', ['name' => '[0-9a-zA-Z._-]+']);
- \Chibi\Router::register(['LogController', 'viewAction'], $method, '/log/{name}/{page}', ['name' => '[0-9a-zA-Z._-]+', 'page' => '\d*']);
- \Chibi\Router::register(['LogController', 'viewAction'], $method, '/log/{name}/{page}/{filter}', ['name' => '[0-9a-zA-Z._-]+', 'page' => '\d*', 'filter' => '.*']);
- \Chibi\Router::register(['AuthController', 'loginAction'], $method, '/auth/login');
- \Chibi\Router::register(['AuthController', 'logoutAction'], $method, '/auth/logout');
- \Chibi\Router::register(['AuthController', 'loginAction'], 'POST', '/auth/login');
- \Chibi\Router::register(['AuthController', 'logoutAction'], 'POST', '/auth/logout');
- \Chibi\Router::register(['CommentController', 'listAction'], $method, '/comments');
- \Chibi\Router::register(['CommentController', 'listAction'], $method, '/comments/{page}', ['page' => '\d+']);
- \Chibi\Router::register(['CommentController', 'addAction'], $method, '/post/{postId}/add-comment', ['postId' => '\d+']);
- \Chibi\Router::register(['CommentController', 'deleteAction'], $method, '/comment/{id}/delete', ['id' => '\d+']);
- \Chibi\Router::register(['CommentController', 'editAction'], $method, '/comment/{id}/edit', ['id' => '\d+']);
-
- $postValidation =
- [
- 'tag' => '[^\/]*',
- 'enable' => '0|1',
- 'source' => 'posts|mass-tag',
- 'query' => '[^\/]*',
- 'additionalInfo' => '[^\/]*',
- 'score' => '-1|0|1',
- ];
-
- \Chibi\Router::register(['PostController', 'uploadAction'], $method, '/posts/upload', $postValidation);
- \Chibi\Router::register(['PostController', 'listAction'], $method, '/{source}', $postValidation);
- \Chibi\Router::register(['PostController', 'listAction'], $method, '/{source}/{query}', $postValidation);
- \Chibi\Router::register(['PostController', 'listAction'], $method, '/{source}/{query}/{page}', $postValidation);
- \Chibi\Router::register(['PostController', 'listAction'], $method, '/{source}/{additionalInfo}/{query}/{page}', $postValidation);
- \Chibi\Router::register(['PostController', 'toggleTagAction'], $method, '/post/{id}/toggle-tag/{tag}/{enable}', $postValidation);
- \Chibi\Router::register(['PostController', 'favoritesAction'], $method, '/favorites', $postValidation);
- \Chibi\Router::register(['PostController', 'favoritesAction'], $method, '/favorites/{page}', $postValidation);
- \Chibi\Router::register(['PostController', 'upvotedAction'], $method, '/upvoted', $postValidation);
- \Chibi\Router::register(['PostController', 'upvotedAction'], $method, '/upvoted/{page}', $postValidation);
- \Chibi\Router::register(['PostController', 'randomAction'], $method, '/random', $postValidation);
- \Chibi\Router::register(['PostController', 'randomAction'], $method, '/random/{page}', $postValidation);
- \Chibi\Router::register(['PostController', 'viewAction'], $method, '/post/{id}', $postValidation);
- \Chibi\Router::register(['PostController', 'retrieveAction'], $method, '/post/{name}/retrieve', $postValidation);
- \Chibi\Router::register(['PostController', 'thumbAction'], $method, '/post/{name}/thumb', $postValidation);
- \Chibi\Router::register(['PostController', 'removeFavoriteAction'], $method, '/post/{id}/rem-fav', $postValidation);
- \Chibi\Router::register(['PostController', 'addFavoriteAction'], $method, '/post/{id}/add-fav', $postValidation);
- \Chibi\Router::register(['PostController', 'deleteAction'], $method, '/post/{id}/delete', $postValidation);
- \Chibi\Router::register(['PostController', 'hideAction'], $method, '/post/{id}/hide', $postValidation);
- \Chibi\Router::register(['PostController', 'unhideAction'], $method, '/post/{id}/unhide', $postValidation);
- \Chibi\Router::register(['PostController', 'editAction'], $method, '/post/{id}/edit', $postValidation);
- \Chibi\Router::register(['PostController', 'flagAction'], $method, '/post/{id}/flag', $postValidation);
- \Chibi\Router::register(['PostController', 'featureAction'], $method, '/post/{id}/feature', $postValidation);
- \Chibi\Router::register(['PostController', 'scoreAction'], $method, '/post/{id}/score/{score}', $postValidation);
-
- $tagValidation =
- [
- 'page' => '\d*',
- 'filter' => '[^\/]+',
- ];
-
- \Chibi\Router::register(['TagController', 'listAction'], $method, '/tags', $tagValidation);
- \Chibi\Router::register(['TagController', 'listAction'], $method, '/tags/{filter}', $tagValidation);
- \Chibi\Router::register(['TagController', 'listAction'], $method, '/tags/{page}', $tagValidation);
- \Chibi\Router::register(['TagController', 'listAction'], $method, '/tags/{filter}/{page}', $tagValidation);
- \Chibi\Router::register(['TagController', 'autoCompleteAction'], $method, '/tags-autocomplete', $tagValidation);
- \Chibi\Router::register(['TagController', 'relatedAction'], $method, '/tags-related', $tagValidation);
- \Chibi\Router::register(['TagController', 'mergeAction'], $method, '/tags-merge', $tagValidation);
- \Chibi\Router::register(['TagController', 'renameAction'], $method, '/tags-rename', $tagValidation);
- \Chibi\Router::register(['TagController', 'massTagRedirectAction'], $method, '/mass-tag-redirect', $tagValidation);
-
- $userValidations =
- [
- 'name' => '[^\/]+',
- 'page' => '\d*',
- 'tab' => 'favs|uploads',
- 'filter' => '[^\/]+',
- ];
-
- \Chibi\Router::register(['UserController', 'registrationAction'], $method, '/register', $userValidations);
- \Chibi\Router::register(['UserController', 'viewAction'], $method, '/user/{name}/{tab}', $userValidations);
- \Chibi\Router::register(['UserController', 'viewAction'], $method, '/user/{name}/{tab}/{page}', $userValidations);
- \Chibi\Router::register(['UserController', 'listAction'], $method, '/users', $userValidations);
- \Chibi\Router::register(['UserController', 'listAction'], $method, '/users/{page}', $userValidations);
- \Chibi\Router::register(['UserController', 'listAction'], $method, '/users/{filter}', $userValidations);
- \Chibi\Router::register(['UserController', 'listAction'], $method, '/users/{filter}/{page}', $userValidations);
- \Chibi\Router::register(['UserController', 'flagAction'], $method, '/user/{name}/flag', $userValidations);
- \Chibi\Router::register(['UserController', 'banAction'], $method, '/user/{name}/ban', $userValidations);
- \Chibi\Router::register(['UserController', 'unbanAction'], $method, '/user/{name}/unban', $userValidations);
- \Chibi\Router::register(['UserController', 'acceptRegistrationAction'], $method, '/user/{name}/accept-registration', $userValidations);
- \Chibi\Router::register(['UserController', 'deleteAction'], $method, '/user/{name}/delete', $userValidations);
- \Chibi\Router::register(['UserController', 'settingsAction'], $method, '/user/{name}/settings', $userValidations);
- \Chibi\Router::register(['UserController', 'editAction'], $method, '/user/{name}/edit', $userValidations);
- \Chibi\Router::register(['UserController', 'activationAction'], $method, '/activation/{token}', $userValidations);
- \Chibi\Router::register(['UserController', 'activationProxyAction'], $method, '/activation-proxy', $userValidations);
- \Chibi\Router::register(['UserController', 'activationProxyAction'], $method, '/activation-proxy/{token}', $userValidations);
- \Chibi\Router::register(['UserController', 'passwordResetAction'], $method, '/password-reset/{token}', $userValidations);
- \Chibi\Router::register(['UserController', 'passwordResetProxyAction'], $method, '/password-reset-proxy', $userValidations);
- \Chibi\Router::register(['UserController', 'passwordResetProxyAction'], $method, '/password-reset-proxy/{token}', $userValidations);
- \Chibi\Router::register(['UserController', 'toggleSafetyAction'], $method, '/user/toggle-safety/{safety}', $userValidations);
-}
-
-Assets::setTitle($config->main->title);
-
-$context->handleExceptions = false;
-$context->json = isset($_GET['json']);
-$context->layoutName = $context->json
- ? 'layout-json'
- : 'layout-normal';
-$context->viewName = '';
-$context->transport = new StdClass;
-
-session_start();
-if (!Auth::isLoggedIn())
- Auth::tryAutoLogin();
-
-register_shutdown_function(function()
-{
- $error = error_get_last();
- if ($error !== null)
- \Chibi\Util\Headers::setCode(400);
-});
-
-try
-{
- try
- {
- \Chibi\Router::run($query);
- renderView();
- AuthController::observeWorkFinish();
- }
- catch (\Chibi\UnhandledRouteException $e)
- {
- throw new SimpleNotFoundException($query . ' not found.');
- }
-}
-catch (\Chibi\MissingViewFileException $e)
-{
- $context->json = true;
- $context->layoutName = 'layout-json';
- renderView();
-}
-catch (SimpleException $e)
-{
- if ($e instanceof SimpleNotFoundException)
- \Chibi\Util\Headers::setCode(404);
- else
- \Chibi\Util\Headers::setCode(400);
- Messenger::message($e->getMessage(), false);
- if (!$context->handleExceptions)
- $context->viewName = 'message';
- renderView();
-}
-catch (Exception $e)
-{
- \Chibi\Util\Headers::setCode(400);
- Messenger::message($e->getMessage());
- $context->transport->exception = $e;
- $context->transport->queries = \Chibi\Database::getLogs();
- $context->viewName = 'error-exception';
- renderView();
-}
+$dispatcher = new Dispatcher();
+$dispatcher->run();
diff --git a/public_html/media/css/core.css b/public_html/media/css/core.css
index cff6ad70..e587f43e 100644
--- a/public_html/media/css/core.css
+++ b/public_html/media/css/core.css
@@ -145,6 +145,12 @@ footer span:not(:last-of-type):after {
footer a {
color: silver;
}
+footer .left {
+ float: left;
+}
+footer .right {
+ float: right;
+}
@@ -254,12 +260,12 @@ button {
box-sizing: border-box !important;
vertical-align: middle;
line-height: 24px;
- height: 34px;
+ padding: 3px 5px;
+ height: 30px;
}
label,
input,
select {
- padding: 5px;
font-family: inherit;
font-size: 11pt;
}
@@ -290,7 +296,7 @@ button:hover {
}
.form-row {
- margin: 0.25em 0;
+ margin: 0 0 0.5em 0;
clear: left;
}
.input-wrapper {
@@ -313,12 +319,19 @@ ul.tagit {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
+ min-height: 32px;
+}
+ul.tagit li.tagit-new {
+ padding: 1px 0 !important;
+}
+ul.tagit li.tagit-choice {
+ padding: 1px 20px 1px 5px !important;
}
ul.tagit input {
border: 0 !important;
line-height: auto !important;
height: auto !important;
- margin: -4px 0 !important;
+ padding: 0 !important;
}
.related-tags {
padding: 0.5em;
diff --git a/public_html/media/css/post-upload.css b/public_html/media/css/post-upload.css
index 61f6ecfe..64446bd7 100644
--- a/public_html/media/css/post-upload.css
+++ b/public_html/media/css/post-upload.css
@@ -74,7 +74,7 @@
.post .ops a {
cursor: pointer;
- margin-left: 0.5em;
+ margin-left: 1.5em;
vertical-align: middle;
}
.post a span {
@@ -117,11 +117,11 @@
.post .file-name strong {
overflow: hidden;
text-overflow: ellipsis;
- max-width: 50%;
+ max-width: 100%;
white-space: pre;
display: inline-block;
- vertical-align: middle;
- padding: 0.5em 0;
+ vertical-align: text-bottom;
+ line-height: 30px;
}
.safety-safe {
@@ -155,6 +155,9 @@ ul.tagit {
.post .form-wrapper {
overflow: hidden;
}
+.post form {
+ margin-top: 0;
+}
#lightbox {
display: none;
diff --git a/public_html/media/css/post-view.css b/public_html/media/css/post-view.css
index 97be76a2..029b61df 100644
--- a/public_html/media/css/post-view.css
+++ b/public_html/media/css/post-view.css
@@ -23,13 +23,17 @@ embed {
margin: 0;
padding: 0;
}
-#sidebar .tags li {
+#sidebar .tags li a {
overflow: hidden;
text-overflow: ellipsis;
+ display: inline-block;
+ max-width: 90%;
+ vertical-align: text-bottom;
}
#sidebar .tags li .count {
padding-left: 0.5em;
color: silver;
+ vertical-align: text-bottom;
}
#around {
diff --git a/public_html/media/css/static-api.css b/public_html/media/css/static-api.css
new file mode 100644
index 00000000..2a31b85a
--- /dev/null
+++ b/public_html/media/css/static-api.css
@@ -0,0 +1,18 @@
+#content pre {
+ background: ghostwhite;
+ padding: 0.5em;
+ border-left: 0.2em solid silver;
+}
+
+#content table {
+ border-spacing: 0;
+ border-collapse: collapsue;
+}
+#content th,
+#content td {
+ text-align: left;
+ padding: 0.2em 0.5em;
+}
+#content tbody:nth-child(2n) {
+ background: #fafafa;
+}
diff --git a/public_html/media/css/index-help.css b/public_html/media/css/static-help.css
similarity index 100%
rename from public_html/media/css/index-help.css
rename to public_html/media/css/static-help.css
diff --git a/public_html/media/css/index-index.css b/public_html/media/css/static-main.css
similarity index 100%
rename from public_html/media/css/index-index.css
rename to public_html/media/css/static-main.css
diff --git a/public_html/media/js/comment-edit.js b/public_html/media/js/comment-edit.js
index 5023067f..c8a3d920 100644
--- a/public_html/media/js/comment-edit.js
+++ b/public_html/media/js/comment-edit.js
@@ -20,7 +20,7 @@ $(function()
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
- var url = formDom.attr('action') + '?json';
+ var url = formDom.attr('action');
var fd = new FormData(formDom[0]);
var preview = false;
@@ -36,7 +36,6 @@ $(function()
data: fd,
processData: false,
contentType: false,
- type: 'POST',
success: function(data)
{
@@ -51,7 +50,7 @@ $(function()
formDom.find('.preview').hide();
var cb = function()
{
- $.get(window.location.href, function(data)
+ $.get(window.location.href).success(function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
@@ -84,7 +83,7 @@ $(function()
}
};
- $.ajax(ajaxData);
+ postJSON(ajaxData);
});
$('.comment .edit a').bindOnce('edit-comment', 'click', function(e)
@@ -100,7 +99,7 @@ $(function()
if (formDom.length == 0)
{
- $.get($(this).attr('href'), function(data)
+ $.get($(this).attr('href')).success(function(data)
{
var otherForm = $(data).find('form.edit-comment');
otherForm.hide();
diff --git a/public_html/media/js/core.js b/public_html/media/js/core.js
index 41ff3972..d029c3e4 100644
--- a/public_html/media/js/core.js
+++ b/public_html/media/js/core.js
@@ -32,6 +32,24 @@ function rememberLastSearchQuery()
}
//core functionalities, prototypes
+function getJSON(data)
+{
+ if (typeof(data.headers) === 'undefined')
+ data.headers = {};
+ data.headers['X-Ajax'] = '1';
+ data.type = 'GET';
+ return $.ajax(data);
+};
+
+function postJSON(data)
+{
+ if (typeof(data.headers) === 'undefined')
+ data.headers = {};
+ data.headers['X-Ajax'] = '1';
+ data.type = 'POST';
+ return $.ajax(data);
+};
+
$.fn.hasAttr = function(name)
{
return this.attr(name) !== undefined;
@@ -50,34 +68,6 @@ $.fn.bindOnce = function(name, eventName, callback)
-//safety trigger
-$(function()
-{
- $('.safety a').click(function(e)
- {
- e.preventDefault();
-
- var aDom = $(this);
- if (aDom.hasClass('inactive'))
- return;
- aDom.addClass('inactive');
-
- var url = $(this).attr('href') + '?json';
- $.get(url).success(function(data)
- {
- window.location.reload();
- }).error(function(xhr)
- {
- alert(xhr.responseJSON
- ? xhr.responseJSON.message
- : 'Fatal error');
- aDom.removeClass('inactive');
- });
- });
-});
-
-
-
//basic event listeners
$(function()
{
@@ -111,8 +101,8 @@ $(function()
return;
aDom.addClass('inactive');
- var url = $(this).attr('href') + '?json';
- $.post(url, {submit: 1}).success(function(data)
+ var url = $(this).attr('href');
+ postJSON({ url: url }).success(function(data)
{
if (aDom.hasAttr('data-redirect-url'))
window.location.href = aDom.attr('data-redirect-url');
@@ -201,21 +191,26 @@ function split(val)
function retrieveTags(searchTerm, cb)
{
- var options = { search: searchTerm };
- $.getJSON('/tags-autocomplete?json', options, function(data)
+ var options =
{
- var tags = $.map(data.tags.slice(0, 15), function(tag)
+ url: '/tags-autocomplete',
+ data: { search: searchTerm }
+ };
+ getJSON(options)
+ .success(function(data)
{
- var ret =
+ var tags = $.map(data.tags.slice(0, 15), function(tag)
{
- label: tag.name + ' (' + tag.count + ')',
- value: tag.name,
- };
- return ret;
- });
+ var ret =
+ {
+ label: tag.name + ' (' + tag.count + ')',
+ value: tag.name,
+ };
+ return ret;
+ });
- cb(tags);
- });
+ cb(tags);
+ });
}
$(function()
@@ -277,8 +272,17 @@ function attachTagIt(target)
{
var targetTagit = ui.tag.parents('.tagit');
var context = target.tagit('assignedTags');
- options = { context: context, tag: ui.tagLabel };
- if (targetTagit.siblings('.related-tags:eq(0)').data('for') == options.tag)
+ var options =
+ {
+ url: '/tags-related',
+ data:
+ {
+ context: context,
+ tag: ui.tagLabel
+ }
+ };
+
+ if (targetTagit.siblings('.related-tags:eq(0)').data('for') == options.data.tag)
{
targetTagit.siblings('.related-tags').slideUp(function()
{
@@ -287,7 +291,7 @@ function attachTagIt(target)
return;
}
- $.getJSON('/tags-related?json', options, function(data)
+ getJSON(options).success(function(data)
{
var list = $('
');
$.each(data.tags, function(i, tag)
diff --git a/public_html/media/js/paginator-endless.js b/public_html/media/js/paginator-endless.js
index e02584b4..c9bdcc36 100644
--- a/public_html/media/js/paginator-endless.js
+++ b/public_html/media/js/paginator-endless.js
@@ -15,7 +15,7 @@ function scrolled()
if (pageNext != null && pageNext != pageDone)
{
$(document).data('page-done', pageNext);
- $.get(pageNext, [], function(response)
+ $.get(pageNext).success(function(response)
{
var dom = $(response);
var nextPage = dom.find('.paginator .next:not(.disabled) a').attr('href');
diff --git a/public_html/media/js/post-list.js b/public_html/media/js/post-list.js
index 99303353..dcef3ac9 100644
--- a/public_html/media/js/post-list.js
+++ b/public_html/media/js/post-list.js
@@ -12,9 +12,9 @@ $(function()
aDom.addClass('inactive');
var enable = !aDom.parents('.post').hasClass('tagged');
- var url = $(this).attr('href') + '?json';
- url = url.replace('_enable_', enable ? '1' : '0');
- $.get(url, {submit: 1}).success(function(data)
+ var url = $(this).attr('href');
+ url = url.replace(/\/[01]\/?$/, '/' + (enable ? '1' : '0'));
+ postJSON({ url: url }).success(function(data)
{
aDom.removeClass('inactive');
aDom.parents('.post').removeClass('tagged');
diff --git a/public_html/media/js/post-upload.js b/public_html/media/js/post-upload.js
index 40d18c93..30a26f65 100644
--- a/public_html/media/js/post-upload.js
+++ b/public_html/media/js/post-upload.js
@@ -90,7 +90,7 @@ $(function()
}
var postDom = posts.first();
- var url = postDom.find('form').attr('action') + '?json';
+ var url = postDom.find('form').attr('action');
var fd = new FormData(postDom.find('form').get(0));
fd.append('file', postDom.data('file'));
@@ -104,7 +104,6 @@ $(function()
processData: false,
contentType: false,
dataType: 'json',
- type: 'POST',
success: function(data)
{
postDom.slideUp(function()
@@ -125,7 +124,7 @@ $(function()
}
};
- $.ajax(ajaxData);
+ postJSON(ajaxData);
}
function uploadFinished()
@@ -166,6 +165,7 @@ $(function()
{
handleInputs(files, function(postDom, file)
{
+ postDom.data('url', '');
postDom.data('file', file);
$('.file-name strong', postDom).text(file.name);
@@ -198,11 +198,13 @@ $(function()
handleInputs(urls, function(postDom, url)
{
postDom.data('url', url);
+ postDom.data('file', '');
postDom.find('[name=source]').val(url);
if (matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/))
{
postDom.find('.file-name strong').text(url);
- $.getJSON('http://gdata.youtube.com/feeds/api/videos/' + matches[1] + '?v=2&alt=jsonc', function(data)
+ var url = 'http://gdata.youtube.com/feeds/api/videos/' + matches[1] + '?v=2&alt=jsonc';
+ getJSON({ url: url }).success(function(data)
{
postDom.find('.file-name strong')
.text(data.data.title);
diff --git a/public_html/media/js/post-view.js b/public_html/media/js/post-view.js
index 34526e65..66bedcaa 100644
--- a/public_html/media/js/post-view.js
+++ b/public_html/media/js/post-view.js
@@ -67,7 +67,7 @@ $(function()
$('.comments.unit a.simple-action').data('callback', function()
{
- $.get(window.location.href, function(data)
+ $.get(window.location.href).success(function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
@@ -76,7 +76,7 @@ $(function()
$('#sidebar a.simple-action').data('callback', function()
{
- $.get(window.location.href, function(data)
+ $.get(window.location.href).success(function(data)
{
$('#sidebar').replaceWith($(data).find('#sidebar'));
$('body').trigger('dom-update');
@@ -97,7 +97,7 @@ $(function()
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
- var url = formDom.attr('action') + '?json';
+ var url = formDom.attr('action');
var fd = new FormData(formDom[0]);
var ajaxData =
@@ -112,7 +112,7 @@ $(function()
{
disableExitConfirmation();
- $.get(window.location.href, function(data)
+ $.get(window.location.href).success(function(data)
{
$('#sidebar').replaceWith($(data).find('#sidebar'));
$('#edit-token').replaceWith($(data).find('#edit-token'));
@@ -132,7 +132,7 @@ $(function()
}
};
- $.ajax(ajaxData);
+ postJSON(ajaxData);
});
Mousetrap.bind('a', function()
diff --git a/data/thumbs/.gitignore b/public_html/thumbs/.gitignore
similarity index 100%
rename from data/thumbs/.gitignore
rename to public_html/thumbs/.gitignore
diff --git a/scripts/find-posts.php b/scripts/find-posts.php
index 25fd64dc..c19b8fdc 100644
--- a/scripts/find-posts.php
+++ b/scripts/find-posts.php
@@ -1,26 +1,19 @@
id,
- $post->name,
- Model_Post::getFullPath($post->name),
- $post->mimeType,
+ $post->getId(),
+ $post->getName(),
+ $post->tryGetWorkingFullPath(),
+ $post->getMimeType(),
]). PHP_EOL;
}
diff --git a/scripts/process-detached-files.php b/scripts/process-detached-files.php
index 7ccdb058..8a9551a9 100644
--- a/scripts/process-detached-files.php
+++ b/scripts/process-detached-files.php
@@ -1,6 +1,8 @@
name;
+ $names []= $post->getName();
}
$names = array_flip($names);
-$config = getConfig();
+$config = Core::getConfig();
foreach (glob(TextHelper::absolutePath($config->main->filesPath) . DS . '*') as $name)
{
$name = basename($name);
diff --git a/scripts/process-old-users.php b/scripts/process-old-users.php
index 7bb227ea..95bbed06 100644
--- a/scripts/process-old-users.php
+++ b/scripts/process-old-users.php
@@ -1,6 +1,8 @@
id . PHP_EOL;
- echo 'Name: ' . $user->name . PHP_EOL;
- echo 'E-mail: ' . $user->email_unconfirmed . PHP_EOL;
- echo 'Date joined: ' . date('Y-m-d H:i:s', $user->join_date) . PHP_EOL;
+ echo 'ID: ' . $user->getId() . PHP_EOL;
+ echo 'Name: ' . $user->getName() . PHP_EOL;
+ echo 'E-mail: ' . $user->getUnconfirmedEmail() . PHP_EOL;
+ echo 'Date joined: ' . date('Y-m-d H:i:s', $user->getJoinTime()) . PHP_EOL;
echo PHP_EOL;
}
@@ -32,7 +34,7 @@ switch ($action)
$func = function($user)
{
printUser($user);
- Model_User::remove($user);
+ UserModel::remove($user);
};
break;
@@ -40,8 +42,13 @@ switch ($action)
die('Unknown action' . PHP_EOL);
}
-$rows = R::find('user', 'email_confirmed IS NULL AND DATETIME(join_date) < DATETIME("now", "-21 days")');
-foreach ($rows as $user)
+$users = UserSearchService::getEntities(null, null, null);
+foreach ($users as $user)
{
- $func($user);
+ if (!$user->getConfirmedEmail()
+ and !$user->getLastLoginTime()
+ and ((time() - $user->getJoinTime()) > 21 * 24 * 60 * 60))
+ {
+ $func($user);
+ }
}
diff --git a/src/Access.php b/src/Access.php
index 97610391..290a3230 100644
--- a/src/Access.php
+++ b/src/Access.php
@@ -2,101 +2,119 @@
class Access
{
private static $privileges = [];
+ private static $checkPrivileges = true;
public static function init()
{
self::$privileges = [];
- foreach (getConfig()->privileges as $key => $minAccessRankName)
+ foreach (Core::getConfig()->privileges as $key => $minAccessRankName)
{
if (strpos($key, '.') === false)
$key .= '.';
list ($privilegeName, $subPrivilegeName) = explode('.', $key);
+ $minAccessRank = new AccessRank(TextHelper::resolveConstant($minAccessRankName, 'AccessRank'));
- $privilegeName = TextCaseConverter::convert($privilegeName,
- TextCaseConverter::CAMEL_CASE,
- TextCaseConverter::SPINAL_CASE);
- $subPrivilegeName = TextCaseConverter::convert($subPrivilegeName,
- TextCaseConverter::CAMEL_CASE,
- TextCaseConverter::SPINAL_CASE);
+ if (!in_array($privilegeName, Privilege::getAllConstants()))
+ throw new Exception('Invalid privilege name in config: ' . $privilegeName);
- $key = rtrim($privilegeName . '.' . $subPrivilegeName, '.');
-
- $minAccessRank = TextHelper::resolveConstant($minAccessRankName, 'AccessRank');
- self::$privileges[$key] = $minAccessRank;
-
- if (!isset(self::$privileges[$privilegeName]) or
- self::$privileges[$privilegeName] > $minAccessRank)
+ if (!isset(self::$privileges[$privilegeName]))
{
- self::$privileges[$privilegeName] = $minAccessRank;
+ self::$privileges[$privilegeName] = [];
+ self::$privileges[$privilegeName][null] = $minAccessRank;
}
+
+ self::$privileges[$privilegeName][$subPrivilegeName] = $minAccessRank;
}
}
- public static function check($privilege, $subPrivilege = null)
+ public static function check(Privilege $privilege, $user = null)
{
- if (php_sapi_name() == 'cli')
+ if (!self::$checkPrivileges)
return true;
- $user = Auth::getCurrentUser();
- $minAccessRank = AccessRank::Admin;
+ if ($user === null)
+ $user = Auth::getCurrentUser();
- $key = TextCaseConverter::convert(Privilege::toString($privilege),
- TextCaseConverter::CAMEL_CASE,
- TextCaseConverter::SPINAL_CASE);
+ $minAccessRank = new AccessRank(AccessRank::Nobody);
- if (isset(self::$privileges[$key]))
- {
- $minAccessRank = self::$privileges[$key];
- }
- if ($subPrivilege != null)
- {
- $key2 = $key . '.' . strtolower($subPrivilege);
- if (isset(self::$privileges[$key2]))
- {
- $minAccessRank = self::$privileges[$key2];
- }
- }
+ if (isset(self::$privileges[$privilege->primary][$privilege->secondary]))
+ $minAccessRank = self::$privileges[$privilege->primary][$privilege->secondary];
- return intval($user->accessRank) >= $minAccessRank;
+ elseif (isset(self::$privileges[$privilege->primary][null]))
+ $minAccessRank = self::$privileges[$privilege->primary][null];
+
+ return $user->getAccessRank()->toInteger() >= $minAccessRank->toInteger();
+ }
+
+ public static function checkEmailConfirmation($user = null)
+ {
+ if (!self::$checkPrivileges)
+ return true;
+
+ if ($user === null)
+ $user = Auth::getCurrentUser();
+
+ if (!$user->getConfirmedEmail())
+ return false;
+ return true;
}
public static function assertAuthentication()
{
if (!Auth::isLoggedIn())
- throw new SimpleException('Not logged in');
+ self::fail('Not logged in');
}
- public static function assert($privilege, $subPrivilege = null)
+ public static function assert(Privilege $privilege, $user = null)
{
- if (!self::check($privilege, $subPrivilege))
- throw new SimpleException('Insufficient privileges');
+ if (!self::check($privilege, $user))
+ self::fail('Insufficient privileges (' . $privilege->toDisplayString() . ')');
}
- public static function assertEmailConfirmation()
+ public static function assertEmailConfirmation($user = null)
{
- $user = Auth::getCurrentUser();
- if (!$user->emailConfirmed)
- throw new SimpleException('Need e-mail address confirmation to continue');
+ if (!self::checkEmailConfirmation($user))
+ self::fail('Need e-mail address confirmation to continue');
+ }
+
+ public static function fail($message)
+ {
+ throw new AccessException($message);
}
public static function getIdentity($user)
{
if (!$user)
return 'all';
- return $user->id == Auth::getCurrentUser()->id ? 'own' : 'all';
+ return $user->getId() == Auth::getCurrentUser()->getId() ? 'own' : 'all';
}
public static function getAllowedSafety()
{
- if (php_sapi_name() == 'cli')
+ if (!self::$checkPrivileges)
return PostSafety::getAll();
return array_filter(PostSafety::getAll(), function($safety)
{
- return Access::check(Privilege::ListPosts, PostSafety::toString($safety))
- and Auth::getCurrentUser()->hasEnabledSafety($safety);
+ return Access::check(new Privilege(Privilege::ListPosts, $safety->toString()))
+ and Auth::getCurrentUser()->getSettings()->hasEnabledSafety($safety);
});
}
-}
-Access::init();
+ public static function getAllDefinedSubPrivileges($privilege)
+ {
+ if (!isset(self::$privileges[$privilege]))
+ return null;
+ return self::$privileges[$privilege];
+ }
+
+ public static function disablePrivilegeChecking()
+ {
+ self::$checkPrivileges = false;
+ }
+
+ public static function enablePrivilegeChecking()
+ {
+ self::$checkPrivileges = true;
+ }
+}
diff --git a/src/AccessException.php b/src/AccessException.php
new file mode 100644
index 00000000..f53a3afe
--- /dev/null
+++ b/src/AccessException.php
@@ -0,0 +1,4 @@
+setArguments($jobArgs);
+
+ self::checkArguments($job);
+
+ $job->prepare();
+
+ self::checkPrivileges($job);
+
+ return $job->execute();
+ });
+ }
+
+ public static function runMultiple($jobs)
+ {
+ $statuses = [];
+ \Chibi\Database::transaction(function() use ($jobs, &$statuses)
+ {
+ foreach ($jobs as $jobItem)
+ {
+ list ($job, $jobArgs) = $jobItem;
+ $statuses []= self::run($job, $jobArgs);
+ }
+ });
+ return $statuses;
+ }
+
+ public static function checkArguments(IJob $job)
+ {
+ self::runArgumentCheck($job, $job->getRequiredArguments());
+ }
+
+ public static function checkPrivileges(IJob $job)
+ {
+ if ($job->isAuthenticationRequired())
+ Access::assertAuthentication();
+
+ if ($job->isConfirmedEmailRequired())
+ Access::assertEmailConfirmation();
+
+ $mainPrivilege = $job->getRequiredMainPrivilege();
+ $subPrivileges = $job->getRequiredSubPrivileges();
+ if (!is_array($subPrivileges))
+ $subPrivileges = [$subPrivileges];
+
+ if ($mainPrivilege !== null)
+ {
+ Access::assert(new Privilege($mainPrivilege));
+ foreach ($subPrivileges as $subPrivilege)
+ Access::assert(new Privilege($mainPrivilege, $subPrivilege));
+ }
+ }
+
+ private static function runArgumentCheck(IJob $job, $item)
+ {
+ if (is_array($item))
+ throw new Exception('Argument definition cannot be an array.');
+ elseif ($item instanceof JobArgsNestedStruct)
+ {
+ if ($item instanceof JobArgsAlternative)
+ {
+ $success = false;
+ foreach ($item->args as $subItem)
+ {
+ try
+ {
+ self::runArgumentCheck($job, $subItem);
+ $success = true;
+ }
+ catch (ApiJobUnsatisfiedException $e)
+ {
+ }
+ }
+ if (!$success)
+ throw new ApiJobUnsatisfiedException($job);
+ }
+ elseif ($item instanceof JobArgsConjunction)
+ {
+ foreach ($item->args as $subItem)
+ !self::runArgumentCheck($job, $subItem);
+ }
+ }
+ elseif ($item === null)
+ return;
+ 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');
+ });
+ }
+}
diff --git a/src/Api/ApiFileInput.php b/src/Api/ApiFileInput.php
new file mode 100644
index 00000000..d7059d86
--- /dev/null
+++ b/src/Api/ApiFileInput.php
@@ -0,0 +1,30 @@
+originalPath = $tmpPath;
+
+ //php "security" bullshit
+ if (is_uploaded_file($filePath))
+ move_uploaded_file($filePath, $tmpPath);
+ else
+ copy($filePath, $tmpPath);
+
+ $this->filePath = $tmpPath;
+ $this->fileName = $fileName;
+ }
+
+ public function __destruct()
+ {
+ TransferHelper::remove($this->originalPath);
+ }
+}
diff --git a/src/Api/ApiFileOutput.php b/src/Api/ApiFileOutput.php
new file mode 100644
index 00000000..9800237f
--- /dev/null
+++ b/src/Api/ApiFileOutput.php
@@ -0,0 +1,30 @@
+fileContent = file_get_contents($filePath);
+ $this->fileName = $fileName;
+ $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/ApiJobUnsatisfiedException.php b/src/Api/ApiJobUnsatisfiedException.php
new file mode 100644
index 00000000..1acdbb0e
--- /dev/null
+++ b/src/Api/ApiJobUnsatisfiedException.php
@@ -0,0 +1,10 @@
+job = $job;
+ }
+
+ public function getJob()
+ {
+ return $this->job;
+ }
+
+ public function tryRetrieve()
+ {
+ if ($this->job->hasArgument(JobArgs::ARG_COMMENT_ENTITY))
+ return $this->job->getArgument(JobArgs::ARG_COMMENT_ENTITY);
+
+ if ($this->job->hasArgument(JobArgs::ARG_COMMENT_ID))
+ return CommentModel::getById($this->job->getArgument(JobArgs::ARG_COMMENT_ID));
+
+ return null;
+ }
+
+ public function retrieve()
+ {
+ $comment = $this->tryRetrieve();
+ if ($comment)
+ return $comment;
+ throw new ApiJobUnsatisfiedException($this->job);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Alternative(
+ JobArgs::ARG_COMMENT_ID,
+ JobArgs::ARG_COMMENT_ENTITY);
+ }
+}
diff --git a/src/Api/Common/EntityRetrievers/IEntityRetriever.php b/src/Api/Common/EntityRetrievers/IEntityRetriever.php
new file mode 100644
index 00000000..06fbd89e
--- /dev/null
+++ b/src/Api/Common/EntityRetrievers/IEntityRetriever.php
@@ -0,0 +1,9 @@
+job = $job;
+ }
+
+ public function getJob()
+ {
+ return $this->job;
+ }
+
+ public function tryRetrieve()
+ {
+ if ($this->job->hasArgument(JobArgs::ARG_POST_ENTITY))
+ return $this->job->getArgument(JobArgs::ARG_POST_ENTITY);
+
+ if ($this->job->hasArgument(JobArgs::ARG_POST_ID))
+ return PostModel::getById($this->job->getArgument(JobArgs::ARG_POST_ID));
+
+ if ($this->job->hasArgument(JobArgs::ARG_POST_NAME))
+ return PostModel::getByName($this->job->getArgument(JobArgs::ARG_POST_NAME));
+
+ return null;
+ }
+
+ public function retrieve()
+ {
+ $post = $this->tryRetrieve();
+ if ($post)
+ return $post;
+ throw new ApiJobUnsatisfiedException($this->job);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Alternative(
+ JobArgs::ARG_POST_ID,
+ JobArgs::ARG_POST_NAME,
+ JobArgs::ARG_POST_ENTITY);
+ }
+}
diff --git a/src/Api/Common/EntityRetrievers/SafePostRetriever.php b/src/Api/Common/EntityRetrievers/SafePostRetriever.php
new file mode 100644
index 00000000..4000c70b
--- /dev/null
+++ b/src/Api/Common/EntityRetrievers/SafePostRetriever.php
@@ -0,0 +1,41 @@
+job = $job;
+ }
+
+ public function getJob()
+ {
+ return $this->job;
+ }
+
+ public function tryRetrieve()
+ {
+ if ($this->job->hasArgument(JobArgs::ARG_POST_ENTITY))
+ return $this->job->getArgument(JobArgs::ARG_POST_ENTITY);
+
+ if ($this->job->hasArgument(JobArgs::ARG_POST_NAME))
+ return PostModel::getByName($this->job->getArgument(JobArgs::ARG_POST_NAME));
+
+ return null;
+ }
+
+ public function retrieve()
+ {
+ $post = $this->tryRetrieve();
+ if ($post)
+ return $post;
+ throw new ApiJobUnsatisfiedException($this->job);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Alternative(
+ JobArgs::ARG_POST_NAME,
+ JobArgs::ARG_POST_ENTITY);
+ }
+}
diff --git a/src/Api/Common/EntityRetrievers/UserRetriever.php b/src/Api/Common/EntityRetrievers/UserRetriever.php
new file mode 100644
index 00000000..ee2d97f6
--- /dev/null
+++ b/src/Api/Common/EntityRetrievers/UserRetriever.php
@@ -0,0 +1,45 @@
+job = $job;
+ }
+
+ public function getJob()
+ {
+ return $this->job;
+ }
+
+ public function tryRetrieve()
+ {
+ if ($this->job->hasArgument(JobArgs::ARG_USER_ENTITY))
+ return $this->job->getArgument(JobArgs::ARG_USER_ENTITY);
+
+ if ($this->job->hasArgument(JobArgs::ARG_USER_EMAIL))
+ return UserModel::getByEmail($this->job->getArgument(JobArgs::ARG_USER_EMAIL));
+
+ if ($this->job->hasArgument(JobArgs::ARG_USER_NAME))
+ return UserModel::getByName($this->job->getArgument(JobArgs::ARG_USER_NAME));
+
+ return null;
+ }
+
+ public function retrieve()
+ {
+ $user = $this->tryRetrieve();
+ if ($user)
+ return $user;
+ throw new ApiJobUnsatisfiedException($this->job);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Alternative(
+ JobArgs::ARG_USER_NAME,
+ JobArgs::ARG_USER_EMAIL,
+ JobArgs::ARG_USER_ENTITY);
+ }
+}
diff --git a/src/Api/Common/JobPager.php b/src/Api/Common/JobPager.php
new file mode 100644
index 00000000..13fd0c65
--- /dev/null
+++ b/src/Api/Common/JobPager.php
@@ -0,0 +1,50 @@
+pageSize = 20;
+ $this->job = $job;
+ }
+
+ public function setPageSize($newPageSize)
+ {
+ $this->pageSize = $newPageSize;
+ }
+
+ public function getPageSize()
+ {
+ return $this->pageSize;
+ }
+
+ public function getPageNumber()
+ {
+ if ($this->job->hasArgument(JobArgs::ARG_PAGE_NUMBER))
+ return (int) $this->job->getArgument(JobArgs::ARG_PAGE_NUMBER);
+ return 1;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Optional(JobArgs::ARG_PAGE_NUMBER);
+ }
+
+ public function serialize($entities, $totalEntityCount)
+ {
+ $pageSize = $this->getPageSize();
+ $pageNumber = $this->getPageNumber();
+
+ $pageCount = (int) ceil($totalEntityCount / $pageSize);
+ $pageNumber = $this->getPageNumber();
+ $pageNumber = min($pageCount, $pageNumber);
+
+ $ret = new StdClass;
+ $ret->entities = $entities;
+ $ret->entityCount = (int) $totalEntityCount;
+ $ret->page = (int) $pageNumber;
+ $ret->pageCount = (int) $pageCount;
+ return $ret;
+ }
+}
diff --git a/src/Api/JobArgs/JobArgs.php b/src/Api/JobArgs/JobArgs.php
new file mode 100644
index 00000000..9c802bd4
--- /dev/null
+++ b/src/Api/JobArgs/JobArgs.php
@@ -0,0 +1,72 @@
+args);
+ elseif ($arg !== null)
+ $finalArgs []= $arg;
+ }
+
+ if (count($finalArgs) == 1)
+ return $finalArgs[0];
+ else
+ return new self($finalArgs);
+ }
+}
diff --git a/src/Api/JobArgs/JobArgsConjunction.php b/src/Api/JobArgs/JobArgsConjunction.php
new file mode 100644
index 00000000..4571bfd7
--- /dev/null
+++ b/src/Api/JobArgs/JobArgsConjunction.php
@@ -0,0 +1,25 @@
+args);
+ elseif ($arg !== null)
+ $finalArgs []= $arg;
+ }
+
+ if (count($finalArgs) == 1)
+ return $finalArgs[0];
+ else
+ return new self($finalArgs);
+ }
+}
diff --git a/src/Api/JobArgs/JobArgsNestedStruct.php b/src/Api/JobArgs/JobArgsNestedStruct.php
new file mode 100644
index 00000000..ca272157
--- /dev/null
+++ b/src/Api/JobArgs/JobArgsNestedStruct.php
@@ -0,0 +1,19 @@
+args = $args;
+ }
+
+ public static function factory(array $args)
+ {
+ throw new BadMethodCallException('Not implemented');
+ }
+}
diff --git a/src/Api/JobArgs/JobArgsOptional.php b/src/Api/JobArgs/JobArgsOptional.php
new file mode 100644
index 00000000..b2a341a5
--- /dev/null
+++ b/src/Api/JobArgs/JobArgsOptional.php
@@ -0,0 +1,20 @@
+subJobs []= $subJob;
+ }
+
+ public function getSubJobs()
+ {
+ return $this->subJobs;
+ }
+
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ public function setContext($context)
+ {
+ $this->context = $context;
+ }
+
+ public function getArgument($key)
+ {
+ if (!$this->hasArgument($key))
+ throw new ApiMissingArgumentException($key);
+
+ return $this->arguments[$key];
+ }
+
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ public function hasArgument($key)
+ {
+ return isset($this->arguments[$key]);
+ }
+
+ public function setArgument($key, $value)
+ {
+ $this->arguments[$key] = $value;
+ }
+
+ public function setArguments(array $arguments)
+ {
+ $this->arguments = $arguments;
+ }
+}
diff --git a/src/Api/Jobs/CommentJobs/AddCommentJob.php b/src/Api/Jobs/CommentJobs/AddCommentJob.php
new file mode 100644
index 00000000..4356673d
--- /dev/null
+++ b/src/Api/Jobs/CommentJobs/AddCommentJob.php
@@ -0,0 +1,57 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $user = Auth::getCurrentUser();
+ $text = $this->getArgument(JobArgs::ARG_NEW_TEXT);
+
+ $comment = CommentModel::spawn();
+ $comment->setCommenter($user);
+ $comment->setPost($post);
+ $comment->setCreationTime(time());
+ $comment->setText($text);
+
+ CommentModel::save($comment);
+ Logger::log('{user} commented on {post}', [
+ 'user' => TextHelper::reprUser($user),
+ 'post' => TextHelper::reprPost($comment->getPost())]);
+
+ return $comment;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_TEXT);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::AddComment;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return Core::getConfig()->registration->needEmailForCommenting;
+ }
+}
diff --git a/src/Api/Jobs/CommentJobs/DeleteCommentJob.php b/src/Api/Jobs/CommentJobs/DeleteCommentJob.php
new file mode 100644
index 00000000..e86d90f5
--- /dev/null
+++ b/src/Api/Jobs/CommentJobs/DeleteCommentJob.php
@@ -0,0 +1,47 @@
+commentRetriever = new CommentRetriever($this);
+ }
+
+ public function execute()
+ {
+ $comment = $this->commentRetriever->retrieve();
+ $post = $comment->getPost();
+
+ CommentModel::remove($comment);
+
+ Logger::log('{user} removed comment from {post}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post)]);
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->commentRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::DeleteComment;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->commentRetriever->retrieve()->getCommenter());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return true;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/CommentJobs/EditCommentJob.php b/src/Api/Jobs/CommentJobs/EditCommentJob.php
new file mode 100644
index 00000000..c2cf5ce6
--- /dev/null
+++ b/src/Api/Jobs/CommentJobs/EditCommentJob.php
@@ -0,0 +1,52 @@
+commentRetriever = new CommentRetriever($this);
+ }
+
+ public function execute()
+ {
+ $comment = $this->commentRetriever->retrieve();
+
+ $comment->setCreationTime(time());
+ $comment->setText($this->getArgument(JobArgs::ARG_NEW_TEXT));
+
+ CommentModel::save($comment);
+ Logger::log('{user} edited comment in {post}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($comment->getPost())]);
+
+ return $comment;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->commentRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_TEXT);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::EditComment;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->commentRetriever->retrieve()->getCommenter());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return true;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/CommentJobs/ListCommentsJob.php b/src/Api/Jobs/CommentJobs/ListCommentsJob.php
new file mode 100644
index 00000000..d334b749
--- /dev/null
+++ b/src/Api/Jobs/CommentJobs/ListCommentsJob.php
@@ -0,0 +1,60 @@
+pager = new JobPager($this);
+ $this->pager->setPageSize(Core::getConfig()->comments->commentsPerPage);
+ }
+
+ public function getPager()
+ {
+ return $this->pager;
+ }
+
+ public function execute()
+ {
+ $pageSize = $this->pager->getPageSize();
+ $page = $this->pager->getPageNumber();
+ $query = 'comment_min:1 order:comment_date,desc';
+
+ $posts = PostSearchService::getEntities($query, $pageSize, $page);
+ $postCount = PostSearchService::getEntityCount($query);
+
+ PostModel::preloadTags($posts);
+ PostModel::preloadComments($posts);
+ $comments = [];
+ foreach ($posts as $post)
+ $comments = array_merge($comments, $post->getComments());
+ CommentModel::preloadCommenters($comments);
+
+ return $this->pager->serialize($posts, $postCount);
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->pager->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ListComments;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/CommentJobs/PreviewCommentJob.php b/src/Api/Jobs/CommentJobs/PreviewCommentJob.php
new file mode 100644
index 00000000..52e47151
--- /dev/null
+++ b/src/Api/Jobs/CommentJobs/PreviewCommentJob.php
@@ -0,0 +1,63 @@
+commentRetriever = new CommentRetriever($this);
+ $this->postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = Auth::getCurrentUser();
+ $text = $this->getArgument(JobArgs::ARG_NEW_TEXT);
+
+ $comment = $this->commentRetriever->tryRetrieve();
+ if (!$comment)
+ {
+ $post = $this->postRetriever->retrieve();
+ $comment = CommentModel::spawn();
+ $comment->setPost($post);
+ }
+
+ $comment->setCommenter($user);
+ $comment->setCreationTime(time());
+ $comment->setText($text);
+
+ $comment->validate();
+
+ return $comment;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ JobArgs::ARG_NEW_TEXT,
+ JobArgs::Alternative(
+ $this->commentRetriever->getRequiredArguments(),
+ $this->postRetriever->getRequiredArguments()));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::AddComment;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return Core::getConfig()->registration->needEmailForCommenting;
+ }
+}
diff --git a/src/Api/Jobs/GetPropertyJob.php b/src/Api/Jobs/GetPropertyJob.php
new file mode 100644
index 00000000..d0eda7e4
--- /dev/null
+++ b/src/Api/Jobs/GetPropertyJob.php
@@ -0,0 +1,34 @@
+getArgument(JobArgs::ARG_QUERY));
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::ARG_QUERY;
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return null;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+
+}
diff --git a/src/Api/Jobs/IJob.php b/src/Api/Jobs/IJob.php
new file mode 100644
index 00000000..47c15ca2
--- /dev/null
+++ b/src/Api/Jobs/IJob.php
@@ -0,0 +1,19 @@
+pager = new JobPager($this);
+ $this->pager->setPageSize(Core::getConfig()->browsing->logsPerPage);
+ }
+
+ public function getPager()
+ {
+ return $this->pager;
+ }
+
+ public function execute()
+ {
+ $pageSize = $this->pager->getPageSize();
+ $page = $this->pager->getPageNumber();
+ $name = $this->getArgument(JobArgs::ARG_LOG_ID);
+ $query = $this->hasArgument(JobArgs::ARG_QUERY)
+ ? $this->getArgument(JobArgs::ARG_QUERY)
+ : '';
+
+ //parse input
+ $page = max(1, intval($page));
+ $name = str_replace(['/', '\\'], '', $name); //paranoia mode
+ $path = TextHelper::absolutePath(dirname(Core::getConfig()->main->logsPath) . DS . $name);
+ if (!file_exists($path))
+ throw new SimpleNotFoundException('Specified log doesn\'t exist');
+
+ //load lines
+ $lines = file_get_contents($path);
+ $lines = trim($lines);
+ $lines = explode(PHP_EOL, str_replace(["\r", "\n"], PHP_EOL, $lines));
+ $lines = array_reverse($lines);
+
+ if (!empty($query))
+ {
+ $lines = array_filter($lines, function($line) use ($query)
+ {
+ return stripos($line, $query) !== false;
+ });
+ }
+
+ $lineCount = count($lines);
+ $lines = array_slice($lines, ($page - 1) * $pageSize, $pageSize);
+
+ return $this->pager->serialize($lines, $lineCount);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->pager->getRequiredArguments(),
+ JobArgs::ARG_LOG_ID,
+ JobArgs::Optional(JobArgs::ARG_QUERY));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ViewLog;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/LogJobs/ListLogsJob.php b/src/Api/Jobs/LogJobs/ListLogsJob.php
new file mode 100644
index 00000000..466cadb6
--- /dev/null
+++ b/src/Api/Jobs/LogJobs/ListLogsJob.php
@@ -0,0 +1,44 @@
+main->logsPath);
+
+ $logs = [];
+ foreach (glob(dirname($path) . DS . '*.log') as $log)
+ $logs []= basename($log);
+
+ usort($logs, function($a, $b)
+ {
+ return strnatcasecmp($b, $a); //reverse natcasesort
+ });
+
+ return $logs;
+ }
+
+ public function getRequiredArguments()
+ {
+ return null;
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ListLogs;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/AddPostJob.php b/src/Api/Jobs/PostJobs/AddPostJob.php
new file mode 100644
index 00000000..27b2dc5b
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/AddPostJob.php
@@ -0,0 +1,88 @@
+addSubJob(new EditPostSafetyJob());
+ $this->addSubJob(new EditPostTagsJob());
+ $this->addSubJob(new EditPostSourceJob());
+ $this->addSubJob(new EditPostRelationsJob());
+ $this->addSubJob(new EditPostContentJob());
+ $this->addSubJob(new EditPostThumbJob());
+ }
+
+ public function execute()
+ {
+ $post = PostModel::spawn();
+
+ $anonymous = false;
+ if ($this->hasArgument(JobArgs::ARG_ANONYMOUS))
+ $anonymous = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_ANONYMOUS));
+
+ if (Auth::isLoggedIn() and !$anonymous)
+ $post->setUploader(Auth::getCurrentUser());
+
+ PostModel::forgeId($post);
+
+ $arguments = $this->getArguments();
+ $arguments[JobArgs::ARG_POST_ENTITY] = $post;
+
+ Logger::bufferChanges();
+ foreach ($this->getSubJobs() as $subJob)
+ {
+ $subJob->setContext(AbstractJob::CONTEXT_BATCH_ADD);
+ try
+ {
+ Api::run($subJob, $arguments);
+ }
+ catch (ApiJobUnsatisfiedException $e)
+ {
+ }
+ finally
+ {
+ Logger::discardBuffer();
+ }
+ }
+
+ //save the post to db if everything went okay
+ PostModel::save($post);
+
+ Logger::log('{user} added {post} (tags: {tags}, safety: {safety}, source: {source})', [
+ 'user' => ($anonymous and !Core::getConfig()->misc->logAnonymousUploads)
+ ? TextHelper::reprUser(UserModel::getAnonymousName())
+ : TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'tags' => TextHelper::reprTags($post->getTags()),
+ 'safety' => $post->getSafety()->toString(),
+ 'source' => $post->getSource()]);
+
+ Logger::flush();
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Optional(JobArgs::ARG_ANONYMOUS);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::AddPost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return Core::getConfig()->registration->needEmailForUploading;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/DeletePostJob.php b/src/Api/Jobs/PostJobs/DeletePostJob.php
new file mode 100644
index 00000000..62e8c0af
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/DeletePostJob.php
@@ -0,0 +1,46 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+
+ PostModel::remove($post);
+
+ Logger::log('{user} deleted {post}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post)]);
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->postRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::DeletePost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return true;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostContentJob.php b/src/Api/Jobs/PostJobs/EditPostContentJob.php
new file mode 100644
index 00000000..91fd7d94
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostContentJob.php
@@ -0,0 +1,66 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+
+ if ($this->hasArgument(JobArgs::ARG_NEW_POST_CONTENT_URL))
+ {
+ $url = $this->getArgument(JobArgs::ARG_NEW_POST_CONTENT_URL);
+ $post->setContentFromUrl($url);
+ }
+ else
+ {
+ $file = $this->getArgument(JobArgs::ARG_NEW_POST_CONTENT);
+ $post->setContentFromPath($file->filePath, $file->fileName);
+ }
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ PostModel::save($post);
+
+ Logger::log('{user} changed contents of {post}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post)]);
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::Alternative(
+ JobArgs::ARG_NEW_POST_CONTENT,
+ JobArgs::ARG_NEW_POST_CONTENT_URL));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::AddPostContent
+ : Privilege::EditPostContent;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostJob.php b/src/Api/Jobs/PostJobs/EditPostJob.php
new file mode 100644
index 00000000..283ccb90
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostJob.php
@@ -0,0 +1,68 @@
+postRetriever = new PostRetriever($this);
+ $this->addSubJob(new EditPostSafetyJob());
+ $this->addSubJob(new EditPostTagsJob());
+ $this->addSubJob(new EditPostSourceJob());
+ $this->addSubJob(new EditPostRelationsJob());
+ $this->addSubJob(new EditPostContentJob());
+ $this->addSubJob(new EditPostThumbJob());
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+
+ $arguments = $this->getArguments();
+ $arguments[JobArgs::ARG_POST_ENTITY] = $post;
+
+ Logger::bufferChanges();
+ foreach ($this->getSubJobs() as $subJob)
+ {
+ $subJob->setContext(self::CONTEXT_BATCH_EDIT);
+
+ try
+ {
+ Api::run($subJob, $arguments);
+ }
+ catch (ApiJobUnsatisfiedException $e)
+ {
+ }
+ }
+
+ PostModel::save($post);
+ Logger::flush();
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->postRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::EditPost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostRelationsJob.php b/src/Api/Jobs/PostJobs/EditPostRelationsJob.php
new file mode 100644
index 00000000..4030c200
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostRelationsJob.php
@@ -0,0 +1,75 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $relatedPostIds = $this->getArgument(JobArgs::ARG_NEW_RELATED_POST_IDS);
+
+ if (!is_array($relatedPostIds))
+ throw new SimpleException('Expected array');
+
+ $relatedPosts = PostModel::getAllByIds($relatedPostIds);
+
+ $oldRelatedIds = array_map(function($post) { return $post->getId(); }, $post->getRelations());
+ $post->setRelations($relatedPosts);
+ $newRelatedIds = array_map(function($post) { return $post->getId(); }, $post->getRelations());
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ PostModel::save($post);
+
+ foreach (array_diff($oldRelatedIds, $newRelatedIds) as $post2id)
+ {
+ Logger::log('{user} removed relation between {post} and {post2}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'post2' => TextHelper::reprPost($post2id)]);
+ }
+
+ foreach (array_diff($newRelatedIds, $oldRelatedIds) as $post2id)
+ {
+ Logger::log('{user} added relation between {post} and {post2}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'post2' => TextHelper::reprPost($post2id)]);
+ }
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_RELATED_POST_IDS);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::AddPostRelations
+ : Privilege::EditPostRelations;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostSafetyJob.php b/src/Api/Jobs/PostJobs/EditPostSafetyJob.php
new file mode 100644
index 00000000..afdf0635
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostSafetyJob.php
@@ -0,0 +1,61 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $newSafety = new PostSafety($this->getArgument(JobArgs::ARG_NEW_SAFETY));
+
+ $oldSafety = $post->getSafety();
+ $post->setSafety($newSafety);
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ PostModel::save($post);
+
+ if ($oldSafety != $newSafety)
+ {
+ Logger::log('{user} changed safety of {post} to {safety}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'safety' => $post->getSafety()->toString()]);
+ }
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_SAFETY);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::AddPostSafety
+ : Privilege::EditPostSafety;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostSourceJob.php b/src/Api/Jobs/PostJobs/EditPostSourceJob.php
new file mode 100644
index 00000000..843e30d9
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostSourceJob.php
@@ -0,0 +1,61 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $newSource = $this->getArgument(JobArgs::ARG_NEW_SOURCE);
+
+ $oldSource = $post->getSource();
+ $post->setSource($newSource);
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ PostModel::save($post);
+
+ if ($oldSource != $newSource)
+ {
+ Logger::log('{user} changed source of {post} to {source}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'source' => $post->getSource()]);
+ }
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_SOURCE);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::AddPostSource
+ : Privilege::EditPostSource;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostTagsJob.php b/src/Api/Jobs/PostJobs/EditPostTagsJob.php
new file mode 100644
index 00000000..187c6b5f
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostTagsJob.php
@@ -0,0 +1,78 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $tagNames = $this->getArgument(JobArgs::ARG_NEW_TAG_NAMES);
+
+ if (!is_array($tagNames))
+ throw new SimpleException('Expected array');
+
+ $tags = TagModel::spawnFromNames($tagNames);
+
+ $oldTags = array_map(function($tag) { return $tag->getName(); }, $post->getTags());
+ $post->setTags($tags);
+ $newTags = array_map(function($tag) { return $tag->getName(); }, $post->getTags());
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ {
+ PostModel::save($post);
+ TagModel::removeUnused();
+ }
+
+ foreach (array_diff($oldTags, $newTags) as $tag)
+ {
+ Logger::log('{user} untagged {post} with {tag}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'tag' => TextHelper::reprTag($tag)]);
+ }
+
+ foreach (array_diff($newTags, $oldTags) as $tag)
+ {
+ Logger::log('{user} tagged {post} with {tag}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'tag' => TextHelper::reprTag($tag)]);
+ }
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_TAG_NAMES);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::AddPostTags
+ : Privilege::EditPostTags;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/EditPostThumbJob.php b/src/Api/Jobs/PostJobs/EditPostThumbJob.php
new file mode 100644
index 00000000..ab57cff1
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/EditPostThumbJob.php
@@ -0,0 +1,56 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $file = $this->getArgument(JobArgs::ARG_NEW_THUMB_CONTENT);
+
+ $post->setCustomThumbnailFromPath($file->filePath);
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ PostModel::save($post);
+
+ Logger::log('{user} changed thumb of {post}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post)]);
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_THUMB_CONTENT);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::AddPostThumb
+ : Privilege::EditPostThumb;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/FeaturePostJob.php b/src/Api/Jobs/PostJobs/FeaturePostJob.php
new file mode 100644
index 00000000..19ddd8e6
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/FeaturePostJob.php
@@ -0,0 +1,60 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+
+ PropertyModel::set(PropertyModel::FeaturedPostId, $post->getId());
+ PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time());
+
+ $anonymous = false;
+ if ($this->hasArgument(JobArgs::ARG_ANONYMOUS))
+ $anonymous = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_ANONYMOUS));
+
+ PropertyModel::set(PropertyModel::FeaturedPostUserName,
+ $anonymous
+ ? null
+ : Auth::getCurrentUser()->getName());
+
+ Logger::log('{user} featured {post} on main page', [
+ 'user' => TextHelper::reprPost(PropertyModel::get(PropertyModel::FeaturedPostUserName)),
+ 'post' => TextHelper::reprPost($post)]);
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::Optional(JobArgs::ARG_ANONYMOUS));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::FeaturePost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return true;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/FlagPostJob.php b/src/Api/Jobs/PostJobs/FlagPostJob.php
new file mode 100644
index 00000000..bebb6fad
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/FlagPostJob.php
@@ -0,0 +1,53 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $key = TextHelper::reprPost($post);
+
+ $flagged = SessionHelper::get('flagged', []);
+ if (in_array($key, $flagged))
+ throw new SimpleException('You already flagged this post');
+ $flagged []= $key;
+ SessionHelper::set('flagged', $flagged);
+
+ Logger::log('{user} flagged {post} for moderator attention', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post)]);
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->postRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::FlagPost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/GetPostContentJob.php b/src/Api/Jobs/PostJobs/GetPostContentJob.php
new file mode 100644
index 00000000..36f52537
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/GetPostContentJob.php
@@ -0,0 +1,62 @@
+postRetriever = new SafePostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $config = Core::getConfig();
+
+ $path = $post->tryGetWorkingFullPath();
+ if (!$path)
+ throw new SimpleNotFoundException('Post file does not exist');
+
+ $fileName = sprintf('%s_%s_%s.%s',
+ $config->main->title,
+ $post->getId(),
+ join(',', array_map(function($tag) { return $tag->getName(); }, $post->getTags())),
+ TextHelper::resolveMimeType($post->getMimeType()) ?: 'dat');
+ $fileName = preg_replace('/[[:^print:]]/', '', $fileName);
+
+ return new ApiFileOutput($path, $fileName);
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->postRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ViewPost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ $post = $this->postRetriever->retrieve();
+ $privileges = [];
+
+ if ($post->isHidden())
+ $privileges []= 'hidden';
+
+ $privileges []= $post->getSafety()->toString();
+
+ return $privileges;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/GetPostJob.php b/src/Api/Jobs/PostJobs/GetPostJob.php
new file mode 100644
index 00000000..7d6b6ba2
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/GetPostJob.php
@@ -0,0 +1,54 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+
+ CommentModel::preloadCommenters($post->getComments());
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ null);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ViewPost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ $post = $this->postRetriever->retrieve();
+ $privileges = [];
+
+ if ($post->isHidden())
+ $privileges []= 'hidden';
+
+ $privileges []= $post->getSafety()->toString();
+
+ return $privileges;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/GetPostThumbJob.php b/src/Api/Jobs/PostJobs/GetPostThumbJob.php
new file mode 100644
index 00000000..1298226d
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/GetPostThumbJob.php
@@ -0,0 +1,65 @@
+postRetriever = new SafePostRetriever($this);
+ }
+
+ public function execute()
+ {
+ //optimize - save extra query to DB
+ if ($this->hasArgument(JobArgs::ARG_POST_NAME))
+ $name = $this->getArgument(JobArgs::ARG_POST_NAME);
+ else
+ {
+ $post = $this->postRetriever->retrieve();
+ $name = $post->getName();
+ }
+
+ $path = PostModel::tryGetWorkingThumbPath($name);
+ if (!$path)
+ {
+ $post = PostModel::getByName($name);
+ $post = $this->postRetriever->retrieve();
+
+ $post->generateThumb();
+ $path = PostModel::tryGetWorkingThumbPath($name);
+
+ if (!$path)
+ {
+ $path = Core::getConfig()->main->mediaPath . DS . 'img' . DS . 'thumb.jpg';
+ $path = TextHelper::absolutePath($path);
+ }
+ }
+
+ return new ApiFileOutput($path, 'thumbnail.jpg');
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->postRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return null;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/ListPostsJob.php b/src/Api/Jobs/PostJobs/ListPostsJob.php
new file mode 100644
index 00000000..8c97c8fd
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/ListPostsJob.php
@@ -0,0 +1,59 @@
+pager = new JobPager($this);
+ $this->pager->setPageSize(Core::getConfig()->browsing->postsPerPage);
+ }
+
+ public function getPager()
+ {
+ return $this->pager;
+ }
+
+ public function execute()
+ {
+ $pageSize = $this->pager->getPageSize();
+ $page = $this->pager->getPageNumber();
+ $query = $this->hasArgument(JobArgs::ARG_QUERY)
+ ? $this->getArgument(JobArgs::ARG_QUERY)
+ : '';
+
+ $posts = PostSearchService::getEntities($query, $pageSize, $page);
+ $postCount = PostSearchService::getEntityCount($query);
+
+ PostModel::preloadTags($posts);
+
+ return $this->pager->serialize($posts, $postCount);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->pager->getRequiredArguments(),
+ JobArgs::Optional(JobArgs::ARG_QUERY));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ListPosts;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/ScorePostJob.php b/src/Api/Jobs/PostJobs/ScorePostJob.php
new file mode 100644
index 00000000..fd29915e
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/ScorePostJob.php
@@ -0,0 +1,47 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $score = TextHelper::toInteger($this->getArgument(JobArgs::ARG_NEW_POST_SCORE));
+
+ UserModel::updateUserScore(Auth::getCurrentUser(), $post, $score);
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_POST_SCORE);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ScorePost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return true;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/TogglePostFavoriteJob.php b/src/Api/Jobs/PostJobs/TogglePostFavoriteJob.php
new file mode 100644
index 00000000..e9ca282f
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/TogglePostFavoriteJob.php
@@ -0,0 +1,55 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $favorite = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_NEW_STATE));
+
+ if ($favorite)
+ {
+ UserModel::updateUserScore(Auth::getCurrentUser(), $post, 1);
+ UserModel::addToUserFavorites(Auth::getCurrentUser(), $post);
+ }
+ else
+ {
+ UserModel::removeFromUserFavorites(Auth::getCurrentUser(), $post);
+ }
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_STATE);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::FavoritePost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return true;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/TogglePostTagJob.php b/src/Api/Jobs/PostJobs/TogglePostTagJob.php
new file mode 100644
index 00000000..c4c314cc
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/TogglePostTagJob.php
@@ -0,0 +1,88 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $tagName = $this->getArgument(JobArgs::ARG_TAG_NAME);
+ $enable = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_NEW_STATE));
+ $post = $this->postRetriever->retrieve();
+
+ $tags = $post->getTags();
+
+ if ($enable)
+ {
+ $tag = TagModel::tryGetByName($tagName);
+ if ($tag === null)
+ {
+ $tag = TagModel::spawn();
+ $tag->setName($tagName);
+ TagModel::save($tag);
+ }
+
+ $tags []= $tag;
+ }
+ else
+ {
+ foreach ($tags as $i => $tag)
+ if ($tag->getName() == $tagName)
+ unset($tags[$i]);
+ }
+
+ $post->setTags($tags);
+ PostModel::save($post);
+ TagModel::removeUnused();
+
+ if ($enable)
+ {
+ Logger::log('{user} tagged {post} with {tag}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'tag' => TextHelper::reprTag($tag)]);
+ }
+ else
+ {
+ Logger::log('{user} untagged {post} with {tag}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post),
+ 'tag' => TextHelper::reprTag($tag)]);
+ }
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::Conjunction(
+ JobArgs::ARG_TAG_NAME,
+ Jobargs::ARG_NEW_STATE));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::EditPostTags;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/PostJobs/TogglePostVisibilityJob.php b/src/Api/Jobs/PostJobs/TogglePostVisibilityJob.php
new file mode 100644
index 00000000..5e0c3a95
--- /dev/null
+++ b/src/Api/Jobs/PostJobs/TogglePostVisibilityJob.php
@@ -0,0 +1,55 @@
+postRetriever = new PostRetriever($this);
+ }
+
+ public function execute()
+ {
+ $post = $this->postRetriever->retrieve();
+ $visible = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_NEW_STATE));
+
+ $post->setHidden(!$visible);
+ PostModel::save($post);
+
+ Logger::log(
+ $visible
+ ? '{user} unhidden {post}'
+ : '{user} hidden {post}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'post' => TextHelper::reprPost($post)]);
+
+ return $post;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->postRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_STATE);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::HidePost;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/TagJobs/ListRelatedTagsJob.php b/src/Api/Jobs/TagJobs/ListRelatedTagsJob.php
new file mode 100644
index 00000000..ea6e77c3
--- /dev/null
+++ b/src/Api/Jobs/TagJobs/ListRelatedTagsJob.php
@@ -0,0 +1,59 @@
+pager = new JobPager($this);
+ $this->pager->setPageSize(Core::getConfig()->browsing->tagsRelated);
+ }
+
+ public function getPager()
+ {
+ return $this->pager;
+ }
+
+ public function execute()
+ {
+ $pageSize = $this->pager->getPageSize();
+ $page = $this->pager->getPageNumber();
+ $tag = $this->getArgument(JobArgs::ARG_TAG_NAME);
+ $otherTags = $this->hasArgument(JobArgs::ARG_TAG_NAMES) ? $this->getArgument(JobArgs::ARG_TAG_NAMES) : [];
+
+ $tags = TagSearchService::getRelatedTags($tag);
+ $tagCount = count($tags);
+ $tags = array_filter($tags, function($tag) use ($otherTags) { return !in_array($tag->getName(), $otherTags); });
+ $tags = array_slice($tags, 0, $pageSize);
+
+ return $this->pager->serialize($tags, $tagCount);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->pager->getRequiredArguments(),
+ Jobargs::ARG_TAG_NAME,
+ JobArgs::Optional(JobArgs::ARG_TAG_NAMES));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ListTags;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/TagJobs/ListTagsJob.php b/src/Api/Jobs/TagJobs/ListTagsJob.php
new file mode 100644
index 00000000..202892c1
--- /dev/null
+++ b/src/Api/Jobs/TagJobs/ListTagsJob.php
@@ -0,0 +1,57 @@
+pager = new JobPager($this);
+ $this->pager->setPageSize(Core::getConfig()->browsing->tagsPerPage);
+ }
+
+ public function getPager()
+ {
+ return $this->pager;
+ }
+
+ public function execute()
+ {
+ $pageSize = $this->pager->getPageSize();
+ $page = $this->pager->getPageNumber();
+ $query = $this->hasArgument(JobArgs::ARG_QUERY)
+ ? $this->getArgument(JobArgs::ARG_QUERY)
+ : '';
+
+ $tags = TagSearchService::getEntities($query, $pageSize, $page);
+ $tagCount = TagSearchService::getEntityCount($query);
+
+ return $this->pager->serialize($tags, $tagCount);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->pager->getRequiredArguments(),
+ JobArgs::Optional(JobArgs::ARG_QUERY));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ListTags;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/TagJobs/MergeTagsJob.php b/src/Api/Jobs/TagJobs/MergeTagsJob.php
new file mode 100644
index 00000000..fda233d6
--- /dev/null
+++ b/src/Api/Jobs/TagJobs/MergeTagsJob.php
@@ -0,0 +1,44 @@
+getArgument(JobArgs::ARG_SOURCE_TAG_NAME);
+ $targetTag = $this->getArgument(JobArgs::ARG_TARGET_TAG_NAME);
+
+ TagModel::removeUnused();
+ TagModel::merge($sourceTag, $targetTag);
+
+ Logger::log('{user} merged {source} with {target}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'source' => TextHelper::reprTag($sourceTag),
+ 'target' => TextHelper::reprTag($targetTag)]);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ JobArgs::ARG_SOURCE_TAG_NAME,
+ JobArgs::ARG_TARGET_TAG_NAME);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::MergeTags;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/TagJobs/RenameTagsJob.php b/src/Api/Jobs/TagJobs/RenameTagsJob.php
new file mode 100644
index 00000000..2b0673c8
--- /dev/null
+++ b/src/Api/Jobs/TagJobs/RenameTagsJob.php
@@ -0,0 +1,44 @@
+getArgument(JobArgs::ARG_SOURCE_TAG_NAME);
+ $targetTag = $this->getArgument(JobArgs::ARG_TARGET_TAG_NAME);
+
+ TagModel::removeUnused();
+ TagModel::rename($sourceTag, $targetTag);
+
+ Logger::log('{user} renamed {source} to {target}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'source' => TextHelper::reprTag($sourceTag),
+ 'target' => TextHelper::reprTag($targetTag)]);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ JobArgs::ARG_SOURCE_TAG_NAME,
+ JobArgs::ARG_TARGET_TAG_NAME);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::RenameTags;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/AcceptUserRegistrationJob.php b/src/Api/Jobs/UserJobs/AcceptUserRegistrationJob.php
new file mode 100644
index 00000000..06b702aa
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/AcceptUserRegistrationJob.php
@@ -0,0 +1,49 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+
+ $user->setStaffConfirmed(true);
+ UserModel::save($user);
+
+ Logger::log('{user} confirmed {subject}\'s account', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($user)]);
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->userRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::AcceptUserRegistration;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/ActivateUserEmailJob.php b/src/Api/Jobs/UserJobs/ActivateUserEmailJob.php
new file mode 100644
index 00000000..d2090c7f
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/ActivateUserEmailJob.php
@@ -0,0 +1,102 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ if (!$this->hasArgument(JobArgs::ARG_TOKEN))
+ {
+ $user = $this->userRetriever->retrieve();
+
+ if (empty($user->getUnconfirmedEmail()))
+ {
+ if (!empty($user->getConfirmedEmail()))
+ throw new SimpleException('E-mail was already confirmed; activation skipped');
+ else
+ throw new SimpleException('This user has no e-mail specified; activation cannot proceed');
+ }
+
+ self::sendEmail($user);
+
+ return $user;
+ }
+ else
+ {
+ $tokenText = $this->getArgument(JobArgs::ARG_TOKEN);
+ $token = TokenModel::getByToken($tokenText);
+ TokenModel::checkValidity($token);
+
+ $user = $token->getUser();
+ $user->confirmEmail();
+ $token->setUsed(true);
+ TokenModel::save($token);
+ UserModel::save($user);
+
+ Logger::log('{subject} just activated account', [
+ 'subject' => TextHelper::reprUser($user)]);
+
+ return $user;
+ }
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Alternative(
+ JobArgs::ARG_TOKEN,
+ $this->userRetriever->getRequiredArguments());
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return null;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+
+ public function isAvailableToPublic()
+ {
+ return false;
+ }
+
+ public static function sendEmail($user)
+ {
+ $regConfig = Core::getConfig()->registration;
+
+ if (!$regConfig->confirmationEmailEnabled)
+ {
+ $user->confirmEmail();
+ return;
+ }
+
+ $mail = new Mail();
+ $mail->body = $regConfig->confirmationEmailBody;
+ $mail->subject = $regConfig->confirmationEmailSubject;
+ $mail->senderName = $regConfig->confirmationEmailSenderName;
+ $mail->senderEmail = $regConfig->confirmationEmailSenderEmail;
+ $mail->recipientEmail = $user->getUnconfirmedEmail();
+
+ return Mailer::sendMailWithTokenLink(
+ $user,
+ ['UserController', 'activationAction'],
+ $mail);
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/AddUserJob.php b/src/Api/Jobs/UserJobs/AddUserJob.php
new file mode 100644
index 00000000..990f6363
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/AddUserJob.php
@@ -0,0 +1,87 @@
+addSubJob(new EditUserAccessRankJob());
+ $this->addSubJob(new EditUserNameJob());
+ $this->addSubJob(new EditUserPasswordJob());
+ $this->addSubJob(new EditUserEmailJob());
+ }
+
+ public function execute()
+ {
+ $firstUser = UserModel::getCount() == 0;
+
+ $user = UserModel::spawn();
+ $user->setJoinTime(time());
+ $user->setStaffConfirmed($firstUser);
+ UserModel::forgeId($user);
+
+ if ($firstUser)
+ {
+ $user->setAccessRank(new AccessRank(AccessRank::Admin));
+ }
+ else
+ {
+ $user->setAccessRank(new AccessRank(AccessRank::Registered));
+ }
+
+ $arguments = $this->getArguments();
+ $arguments[JobArgs::ARG_USER_ENTITY] = $user;
+
+ Logger::bufferChanges();
+ foreach ($this->getSubJobs() as $subJob)
+ {
+ $subJob->setContext(self::CONTEXT_BATCH_ADD);
+
+ try
+ {
+ Api::run($subJob, $arguments);
+ }
+ catch (ApiJobUnsatisfiedException $e)
+ {
+ }
+ finally
+ {
+ Logger::discardBuffer();
+ }
+ }
+
+ //save the user to db if everything went okay
+ UserModel::save($user);
+ EditUserEmailJob::observeSave($user);
+
+ Logger::log('{subject} just signed up', [
+ 'subject' => TextHelper::reprUser($user)]);
+
+ Logger::flush();
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return null;
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::RegisterAccount;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/DeleteUserJob.php b/src/Api/Jobs/UserJobs/DeleteUserJob.php
new file mode 100644
index 00000000..7ffa7b09
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/DeleteUserJob.php
@@ -0,0 +1,47 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+
+ $name = $user->getName();
+ UserModel::remove($user);
+
+ Logger::log('{user} removed {subject}\'s account', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($name)]);
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->userRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::DeleteUser;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/EditUserAccessRankJob.php b/src/Api/Jobs/UserJobs/EditUserAccessRankJob.php
new file mode 100644
index 00000000..7a975763
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/EditUserAccessRankJob.php
@@ -0,0 +1,59 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+ $newAccessRank = new AccessRank($this->getArgument(JobArgs::ARG_NEW_ACCESS_RANK));
+
+ $oldAccessRank = $user->getAccessRank();
+ if ($oldAccessRank == $newAccessRank)
+ return $user;
+
+ $user->setAccessRank($newAccessRank);
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ UserModel::save($user);
+
+ Logger::log('{user} changed {subject}\'s access rank to {rank}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($user),
+ 'rank' => $newAccessRank->toDisplayString()]);
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_ACCESS_RANK);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::EditUserAccessRank;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/EditUserEmailJob.php b/src/Api/Jobs/UserJobs/EditUserEmailJob.php
new file mode 100644
index 00000000..7957ec97
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/EditUserEmailJob.php
@@ -0,0 +1,82 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ if (Core::getConfig()->registration->needEmailForRegistering)
+ if (!$this->hasArgument(JobArgs::ARG_NEW_EMAIL) or empty($this->getArgument(JobArgs::ARG_NEW_EMAIL)))
+ throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.');
+
+ $user = $this->userRetriever->retrieve();
+ $newEmail = $this->getArgument(JobArgs::ARG_NEW_EMAIL);
+
+ $oldEmail = $user->getConfirmedEmail();
+ if ($oldEmail == $newEmail)
+ return $user;
+
+ $user->setUnconfirmedEmail($newEmail);
+ $user->setConfirmedEmail(null);
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ {
+ UserModel::save($user);
+ self::observeSave($user);
+ }
+
+ Logger::log('{user} changed {subject}\'s e-mail to {mail}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($user),
+ 'mail' => $newEmail]);
+
+ return $user;
+ }
+
+ public static function observeSave($user)
+ {
+ if (Access::check(new Privilege(Privilege::EditUserEmailNoConfirm), $user))
+ {
+ $user->confirmEmail();
+ }
+ else
+ {
+ if (!empty($user->getUnconfirmedEmail()))
+ ActivateUserEmailJob::sendEmail($user);
+ }
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_EMAIL);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::RegisterAccount
+ : Privilege::EditUserEmail;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/EditUserJob.php b/src/Api/Jobs/UserJobs/EditUserJob.php
new file mode 100644
index 00000000..90fd516e
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/EditUserJob.php
@@ -0,0 +1,88 @@
+userRetriever = new UserRetriever($this);
+ $this->addSubJob(new EditUserAccessRankJob());
+ $this->addSubJob(new EditUserNameJob());
+ $this->addSubJob(new EditUserPasswordJob());
+ $this->addSubJob(new EditUserEmailJob());
+ }
+
+ public function canEditAnything($user)
+ {
+ $this->privileges = [];
+ foreach ($this->getSubJobs() as $subJob)
+ {
+ try
+ {
+ $subJob->setArgument(JobArgs::ARG_USER_ENTITY, $user);
+ Api::checkPrivileges($subJob);
+ return true;
+ }
+ catch (AccessException $e)
+ {
+ }
+ }
+ return false;
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+
+ $arguments = $this->getArguments();
+ $arguments[JobArgs::ARG_USER_ENTITY] = $user;
+
+ Logger::bufferChanges();
+ foreach ($this->getSubJobs() as $subJob)
+ {
+ $subJob->setContext(self::CONTEXT_BATCH_EDIT);
+
+ try
+ {
+ Api::run($subJob, $arguments);
+ }
+ catch (ApiJobUnsatisfiedException $e)
+ {
+ }
+ }
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ {
+ UserModel::save($user);
+ EditUserEmailJob::observeSave($user);
+ Logger::flush();
+ }
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->userRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return null;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/EditUserNameJob.php b/src/Api/Jobs/UserJobs/EditUserNameJob.php
new file mode 100644
index 00000000..f543d35f
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/EditUserNameJob.php
@@ -0,0 +1,61 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+ $newName = $this->getArgument(JobArgs::ARG_NEW_USER_NAME);
+
+ $oldName = $user->getName();
+ if ($oldName == $newName)
+ return $user;
+
+ $user->setName($newName);
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ UserModel::save($user);
+
+ Logger::log('{user} renamed {old} to {new}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'old' => TextHelper::reprUser($oldName),
+ 'new' => TextHelper::reprUser($newName)]);
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_USER_NAME);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::RegisterAccount
+ : Privilege::EditUserName;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/EditUserPasswordJob.php b/src/Api/Jobs/UserJobs/EditUserPasswordJob.php
new file mode 100644
index 00000000..ee0a6c6b
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/EditUserPasswordJob.php
@@ -0,0 +1,60 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+ $newPassword = $this->getArgument(JobArgs::ARG_NEW_PASSWORD);
+
+ $oldPasswordHash = $user->getPasswordHash();
+ $user->setPassword($newPassword);
+ $newPasswordHash = $user->getPasswordHash();
+ if ($oldPasswordHash == $newPasswordHash)
+ return $user;
+
+ if ($this->getContext() == self::CONTEXT_NORMAL)
+ UserModel::save($user);
+
+ Logger::log('{user} changed {subject}\'s password', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($user)]);
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_PASSWORD);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return $this->getContext() == self::CONTEXT_BATCH_ADD
+ ? Privilege::RegisterAccount
+ : Privilege::EditUserPassword;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/EditUserSettingsJob.php b/src/Api/Jobs/UserJobs/EditUserSettingsJob.php
new file mode 100644
index 00000000..48a49026
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/EditUserSettingsJob.php
@@ -0,0 +1,56 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $newSettings = $this->getArgument(JobArgs::ARG_NEW_SETTINGS);
+
+ if (!is_array($newSettings))
+ throw new SimpleException('Expected array');
+
+ $user = $this->userRetriever->retrieve();
+ foreach ($newSettings as $key => $value)
+ {
+ $user->getSettings()->set($key, $value);
+ }
+
+ if ($user->getAccessRank()->toInteger() == AccessRank::Anonymous)
+ return $user;
+
+ return UserModel::save($user);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_SETTINGS);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::EditUserSettings;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/FlagUserJob.php b/src/Api/Jobs/UserJobs/FlagUserJob.php
new file mode 100644
index 00000000..31a9e462
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/FlagUserJob.php
@@ -0,0 +1,53 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+ $key = TextHelper::reprUser($user);
+
+ $flagged = SessionHelper::get('flagged', []);
+ if (in_array($key, $flagged))
+ throw new SimpleException('You already flagged this user');
+ $flagged []= $key;
+ SessionHelper::set('flagged', $flagged);
+
+ Logger::log('{user} flagged {subject} for moderator attention', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($user)]);
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->userRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::FlagUser;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/GetUserJob.php b/src/Api/Jobs/UserJobs/GetUserJob.php
new file mode 100644
index 00000000..fb601922
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/GetUserJob.php
@@ -0,0 +1,40 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ return $this->userRetriever->retrieve();
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->userRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ViewUser;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/GetUserSettingsJob.php b/src/Api/Jobs/UserJobs/GetUserSettingsJob.php
new file mode 100644
index 00000000..5f78f906
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/GetUserSettingsJob.php
@@ -0,0 +1,41 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+ return $user->getSettings()->getAllAsArray();
+ }
+
+ public function getRequiredArguments()
+ {
+ return $this->userRetriever->getRequiredArguments();
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::EditUserSettings;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/ListUsersJob.php b/src/Api/Jobs/UserJobs/ListUsersJob.php
new file mode 100644
index 00000000..443fa97d
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/ListUsersJob.php
@@ -0,0 +1,57 @@
+pager = new JobPager($this);
+ $this->pager->setPageSize(Core::getConfig()->browsing->usersPerPage);
+ }
+
+ public function getPager()
+ {
+ return $this->pager;
+ }
+
+ public function execute()
+ {
+ $pageSize = $this->pager->getPageSize();
+ $page = $this->pager->getPageNumber();
+ $filter = $this->hasArgument(JobArgs::ARG_QUERY)
+ ? $this->getArgument(JobArgs::ARG_QUERY)
+ : '';
+
+ $users = UserSearchService::getEntities($filter, $pageSize, $page);
+ $userCount = UserSearchService::getEntityCount($filter);
+
+ return $this->pager->serialize($users, $userCount);
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->pager->getRequiredArguments(),
+ JobArgs::Optional(JobArgs::ARG_QUERY));
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::ListUsers;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/PasswordResetJob.php b/src/Api/Jobs/UserJobs/PasswordResetJob.php
new file mode 100644
index 00000000..36b249b1
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/PasswordResetJob.php
@@ -0,0 +1,100 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ if (!$this->hasArgument(JobArgs::ARG_TOKEN))
+ {
+ $user = $this->userRetriever->retrieve();
+
+ if (empty($user->getConfirmedEmail()))
+ throw new SimpleException('This user has no e-mail confirmed; password reset cannot proceed');
+
+ self::sendEmail($user);
+
+ return $user;
+ }
+ else
+ {
+ $tokenText = $this->getArgument(JobArgs::ARG_TOKEN);
+ $token = TokenModel::getByToken($tokenText);
+ TokenModel::checkValidity($token);
+
+ $alphabet = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
+ $newPassword = join('', array_map(function($x) use ($alphabet)
+ {
+ return $alphabet[$x];
+ }, array_rand($alphabet, 8)));
+
+ $user = $token->getUser();
+ $user->setPassword($newPassword);
+ $token->setUsed(true);
+ TokenModel::save($token);
+ UserModel::save($user);
+
+ Logger::log('{subject} just reset password', [
+ 'subject' => TextHelper::reprUser($user)]);
+
+ $x = new StdClass;
+ $x->user = $user;
+ $x->newPassword = $newPassword;
+ return $x;
+ }
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Alternative(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_TOKEN);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return null;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return null;
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+
+ public function isAvailableToPublic()
+ {
+ return false;
+ }
+
+ public static function sendEmail($user)
+ {
+ $regConfig = Core::getConfig()->registration;
+
+ $mail = new Mail();
+ $mail->body = $regConfig->passwordResetEmailBody;
+ $mail->subject = $regConfig->passwordResetEmailSubject;
+ $mail->senderName = $regConfig->passwordResetEmailSenderName;
+ $mail->senderEmail = $regConfig->passwordResetEmailSenderEmail;
+ $mail->recipientEmail = $user->getConfirmedEmail();
+
+ return Mailer::sendMailWithTokenLink(
+ $user,
+ ['UserController', 'passwordResetAction'],
+ $mail);
+ }
+}
diff --git a/src/Api/Jobs/UserJobs/ToggleUserBanJob.php b/src/Api/Jobs/UserJobs/ToggleUserBanJob.php
new file mode 100644
index 00000000..df493d70
--- /dev/null
+++ b/src/Api/Jobs/UserJobs/ToggleUserBanJob.php
@@ -0,0 +1,58 @@
+userRetriever = new UserRetriever($this);
+ }
+
+ public function execute()
+ {
+ $user = $this->userRetriever->retrieve();
+ $banned = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_NEW_STATE));
+
+ if ($banned)
+ $user->ban();
+ else
+ $user->unban();
+ UserModel::save($user);
+
+ Logger::log(
+ $banned
+ ? '{user} banned {subject}'
+ : '{user} unbanned {subject}', [
+ 'user' => TextHelper::reprUser(Auth::getCurrentUser()),
+ 'subject' => TextHelper::reprUser($user)]);
+
+ return $user;
+ }
+
+ public function getRequiredArguments()
+ {
+ return JobArgs::Conjunction(
+ $this->userRetriever->getRequiredArguments(),
+ JobArgs::ARG_NEW_STATE);
+ }
+
+ public function getRequiredMainPrivilege()
+ {
+ return Privilege::BanUser;
+ }
+
+ public function getRequiredSubPrivileges()
+ {
+ return Access::getIdentity($this->userRetriever->retrieve());
+ }
+
+ public function isAuthenticationRequired()
+ {
+ return false;
+ }
+
+ public function isConfirmedEmailRequired()
+ {
+ return false;
+ }
+}
diff --git a/src/Assert.php b/src/Assert.php
new file mode 100644
index 00000000..8b59d660
--- /dev/null
+++ b/src/Assert.php
@@ -0,0 +1,95 @@
+getMessage(), $expectedMessage) === false)
+ {
+ $this->fail('Assertion failed. Expected: "' . $expectedMessage . '", got: "' . $e->getMessage() . '"'
+ . PHP_EOL . $e->getTraceAsString() . PHP_EOL . '---' . PHP_EOL);
+ }
+ }
+ if ($success)
+ $this->fail('Assertion failed. Expected exception, got nothing');
+ }
+
+ public function doesNotThrow($callback)
+ {
+ try
+ {
+ $ret = $callback();
+ }
+ catch (Exception $e)
+ {
+ $this->fail('Assertion failed. Expected nothing, got exception: "' . $e->getMessage() . '"'
+ . PHP_EOL . $e->getTraceAsString() . PHP_EOL . '---' . PHP_EOL);
+ }
+ return $ret;
+ }
+
+ public function isNull($actual)
+ {
+ if ($actual !== null)
+ $this->fail('Assertion failed. Expected: NULL, got: "' . $this->dumpVar($actual) . '"');
+ }
+
+ public function isNotNull($actual)
+ {
+ if ($actual === null)
+ $this->fail('Assertion failed. Expected: not NULL, got: "' . $this->dumpVar($actual) . '"');
+ }
+
+ public function isTrue($actual)
+ {
+ return $this->areEqual(1, intval(boolval($actual)));
+ }
+
+ public function isFalse($actual)
+ {
+ return $this->areEqual(0, intval(boolval($actual)));
+ }
+
+ public function areEqual($expected, $actual)
+ {
+ if ($expected !== $actual)
+ $this->fail('Assertion failed. Expected: "' . $this->dumpVar($expected) . '", got: "' . $this->dumpVar($actual) . '"');
+ }
+
+ public function areEquivalent($expected, $actual)
+ {
+ if ($expected != $actual)
+ $this->fail('Assertion failed. Expected: "' . $this->dumpVar($expected) . '", got: "' . $this->dumpVar($actual) . '"');
+ }
+
+ public function areNotEqual($expected, $actual)
+ {
+ if ($expected === $actual)
+ $this->fail('Assertion failed. Specified objects are equal');
+ }
+
+ public function areNotEquivalent($expected, $actual)
+ {
+ if ($expected == $actual)
+ $this->fail('Assertion failed. Specified objects are equivalent');
+ }
+
+ public function dumpVar($var)
+ {
+ ob_start();
+ var_dump($var);
+ return trim(ob_get_clean());
+ }
+
+ public function fail($message)
+ {
+ throw new SimpleException($message);
+ }
+}
diff --git a/src/Auth.php b/src/Auth.php
index 0d5ecab0..be352350 100644
--- a/src/Auth.php
+++ b/src/Auth.php
@@ -10,25 +10,25 @@ class Auth
public static function login($name, $password, $remember)
{
- $config = getConfig();
- $context = getContext();
+ $config = Core::getConfig();
+ $context = Core::getContext();
- $dbUser = UserModel::findByNameOrEmail($name, false);
- if ($dbUser === null)
- throw new SimpleException('Invalid username');
+ $user = UserModel::tryGetByEmail($name);
+ if ($user === null)
+ $user = UserModel::getByName($name);
- $passwordHash = UserModel::hashPassword($password, $dbUser->passSalt);
- if ($passwordHash != $dbUser->passHash)
+ $passwordHash = UserModel::hashPassword($password, $user->getPasswordSalt());
+ if ($passwordHash != $user->getPasswordHash())
throw new SimpleException('Invalid password');
- if (!$dbUser->staffConfirmed and $config->registration->staffActivation)
+ if (!$user->isStaffConfirmed() and $config->registration->staffActivation)
throw new SimpleException('Staff hasn\'t confirmed your registration yet');
- if ($dbUser->banned)
+ if ($user->isBanned())
throw new SimpleException('You are banned');
if ($config->registration->needEmailForRegistering)
- Access::requireEmail($dbUser);
+ Access::assertEmailConfirmation($user);
if ($remember)
{
@@ -36,14 +36,17 @@ class Auth
setcookie('auth', TextHelper::encrypt($token), time() + 365 * 24 * 3600, '/');
}
- self::setCurrentUser($dbUser);
+ self::setCurrentUser($user);
- $dbUser->lastLoginDate = time();
- UserModel::save($dbUser);
+ $user->setLastLoginTime(time());
+ UserModel::save($user);
}
public static function tryAutoLogin()
{
+ if (self::isLoggedIn())
+ return;
+
if (!isset($_COOKIE['auth']))
return;
@@ -73,14 +76,14 @@ class Auth
}
else
{
- $_SESSION['logged-in'] = $user->accessRank != AccessRank::Anonymous;
+ $_SESSION['logged-in'] = $user->getAccessRank()->toInteger() != AccessRank::Anonymous;
$_SESSION['user'] = serialize($user);
}
}
public static function getCurrentUser()
{
- return self::isLoggedIn()
+ return isset($_SESSION['user'])
? unserialize($_SESSION['user'])
: self::getAnonymousUser();
}
@@ -88,8 +91,9 @@ class Auth
private static function getAnonymousUser()
{
$dummy = UserModel::spawn();
- $dummy->name = UserModel::getAnonymousName();
- $dummy->accessRank = AccessRank::Anonymous;
+ $dummy->setId(null);
+ $dummy->setName(UserModel::getAnonymousName());
+ $dummy->setAccessRank(new AccessRank(AccessRank::Anonymous));
return $dummy;
}
}
diff --git a/src/Controllers/AbstractController.php b/src/Controllers/AbstractController.php
new file mode 100644
index 00000000..8bfed787
--- /dev/null
+++ b/src/Controllers/AbstractController.php
@@ -0,0 +1,82 @@
+switchLayout('layout-normal');
+
+ $this->assets = new Assets();
+ $this->assets->setTitle(Core::getConfig()->main->title);
+ }
+
+ public function __destruct()
+ {
+ if ($this->isAjax())
+ $this->renderAjax();
+ }
+
+ public function renderAjax()
+ {
+ $this->switchLayout('layout-json');
+ $this->renderView(null);
+ }
+
+ public function renderFile()
+ {
+ $this->switchLayout('layout-file');
+ $this->renderView(null);
+ }
+
+ public function renderView($viewName)
+ {
+ //no matter which controller runs it (including ErrorController), render only once
+ if (self::isRendered())
+ return;
+
+ self::markAsRendered();
+ $context = Core::getContext();
+ if ($viewName !== null)
+ $context->viewName = $viewName;
+ View::renderTopLevel($this->layoutName, $this->assets);
+ }
+
+
+ protected function redirectToLastVisitedUrl($filter = null)
+ {
+ $targetUrl = SessionHelper::getLastVisitedUrl($filter);
+ if (!$targetUrl)
+ $targetUrl = \Chibi\Router::linkTo(['StaticPagesController', 'mainPageView']);
+ $this->redirect($targetUrl);
+ }
+
+ protected function redirect($url)
+ {
+ if (!$this->isAjax())
+ \Chibi\Util\Url::forward($url);
+ }
+
+
+ private static function isRendered()
+ {
+ return self::$isRendered;
+ }
+
+ private static function markAsRendered()
+ {
+ self::$isRendered = true;
+ }
+
+ private function switchLayout($layoutName)
+ {
+ $this->layoutName = $layoutName;
+ }
+}
diff --git a/src/Controllers/ApiController.php b/src/Controllers/ApiController.php
new file mode 100644
index 00000000..5a8d5c43
--- /dev/null
+++ b/src/Controllers/ApiController.php
@@ -0,0 +1,61 @@
+jobFromName($jobName);
+ if (!$job)
+ throw new SimpleException('Unknown job: ' . $jobName);
+ if (!$job->isAvailableToPublic())
+ throw new SimpleException('This job is unavailable for public.');
+
+ 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/Controllers/AuthController.php b/src/Controllers/AuthController.php
index 88c0106c..9f215d12 100644
--- a/src/Controllers/AuthController.php
+++ b/src/Controllers/AuthController.php
@@ -1,59 +1,35 @@
redirectToLastVisitedUrl('auth');
+ else
+ $this->renderView('auth-login');
+ }
+
public function loginAction()
{
- $context = getContext();
- $context->handleExceptions = true;
-
- //check if already logged in
- if (Auth::isLoggedIn())
+ try
{
- self::redirectAfterLog();
- return;
+ $suppliedName = InputHelper::get('name');
+ $suppliedPassword = InputHelper::get('password');
+ $remember = boolval(InputHelper::get('remember'));
+ Auth::login($suppliedName, $suppliedPassword, $remember);
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
+ $this->renderView('auth-login');
}
- if (!InputHelper::get('submit'))
- return;
-
- $suppliedName = InputHelper::get('name');
- $suppliedPassword = InputHelper::get('password');
- $remember = boolval(InputHelper::get('remember'));
- $dbUser = Auth::login($suppliedName, $suppliedPassword, $remember);
- self::redirectAfterLog();
+ $this->redirectToLastVisitedUrl('auth');
}
public function logoutAction()
{
- $context = getContext();
- $context->viewName = null;
- $context->layoutName = null;
Auth::logout();
- \Chibi\Util\Url::forward(\Chibi\Router::linkTo(['IndexController', 'indexAction']));
- exit;
- }
-
- public static function observeWorkFinish()
- {
- if (strpos(\Chibi\Util\Headers::get('Content-Type'), 'text/html') === false)
- return;
- if (\Chibi\Util\Headers::getCode() != 200)
- return;
- $context = getContext();
- if ($context->simpleControllerName == 'auth')
- return;
- $_SESSION['login-redirect-url'] = $context->query;
- }
-
- private static function redirectAfterLog()
- {
- if (isset($_SESSION['login-redirect-url']))
- {
- \Chibi\Util\Url::forward(\Chibi\Util\Url::makeAbsolute($_SESSION['login-redirect-url']));
- unset($_SESSION['login-redirect-url']);
- exit;
- }
- \Chibi\Util\Url::forward(\Chibi\Router::linkTo(['IndexController', 'indexAction']));
- exit;
+ $this->redirectToLastVisitedUrl('auth');
}
}
diff --git a/src/Controllers/CommentController.php b/src/Controllers/CommentController.php
index 05cceb5e..32f88f1f 100644
--- a/src/Controllers/CommentController.php
+++ b/src/Controllers/CommentController.php
@@ -1,106 +1,97 @@
$page,
+ ]);
- $page = max(1, intval($page));
- $commentsPerPage = intval(getConfig()->comments->commentsPerPage);
- $searchQuery = 'comment_min:1 order:comment_date,desc';
-
- $posts = PostSearchService::getEntities($searchQuery, $commentsPerPage, $page);
- $postCount = PostSearchService::getEntityCount($searchQuery);
- $pageCount = ceil($postCount / $commentsPerPage);
- PostModel::preloadTags($posts);
- PostModel::preloadComments($posts);
- $comments = [];
- foreach ($posts as $post)
- $comments = array_merge($comments, $post->getComments());
- CommentModel::preloadCommenters($comments);
-
- $context = getContext();
- $context->postGroups = true;
- $context->transport->posts = $posts;
- $context->transport->paginator = new StdClass;
- $context->transport->paginator->page = $page;
- $context->transport->paginator->pageCount = $pageCount;
- $context->transport->paginator->entityCount = $postCount;
- $context->transport->paginator->entities = $posts;
- $context->transport->paginator->params = func_get_args();
+ $context = Core::getContext();
+ $context->transport->posts = $ret->entities;
+ $context->transport->paginator = $ret;
+ $this->renderView('comment-list');
}
- public function addAction($postId)
+ public function addAction()
{
- $context = getContext();
- Access::assert(Privilege::AddComment);
- if (getConfig()->registration->needEmailForCommenting)
- Access::assertEmailConfirmation();
-
- $post = PostModel::findById($postId);
- $context->transport->post = $post;
-
- if (!InputHelper::get('submit'))
- return;
-
- $text = InputHelper::get('text');
- $text = CommentModel::validateText($text);
-
- $comment = CommentModel::spawn();
- $comment->setPost($post);
- if (Auth::isLoggedIn())
- $comment->setCommenter(Auth::getCurrentUser());
- else
- $comment->setCommenter(null);
- $comment->commentDate = time();
- $comment->text = $text;
-
- if (InputHelper::get('sender') != 'preview')
+ if (InputHelper::get('sender') == 'preview')
{
- CommentModel::save($comment);
- LogHelper::log('{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
+ $comment = Api::run(
+ new PreviewCommentJob(),
+ [
+ JobArgs::ARG_POST_ID => InputHelper::get('post-id'),
+ JobArgs::ARG_NEW_TEXT => InputHelper::get('text')
+ ]);
+
+ Core::getContext()->transport->textPreview = $comment->getTextMarkdown();
+ $this->renderAjax();
}
- $context->transport->textPreview = $comment->getText();
+ else
+ {
+ Api::run(
+ new AddCommentJob(),
+ [
+ JobArgs::ARG_POST_ID => InputHelper::get('post-id'),
+ JobArgs::ARG_NEW_TEXT => InputHelper::get('text')
+ ]);
+
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToLastVisitedUrl();
+ }
+ }
+
+ public function editView($id)
+ {
+ Core::getContext()->transport->comment = CommentModel::getById($id);
+ $this->renderView('comment-edit');
}
public function editAction($id)
{
- $context = getContext();
- $comment = CommentModel::findById($id);
- $context->transport->comment = $comment;
-
- Access::assert(
- Privilege::EditComment,
- Access::getIdentity($comment->getCommenter()));
-
- if (!InputHelper::get('submit'))
- return;
-
- $text = InputHelper::get('text');
- $text = CommentModel::validateText($text);
-
- $comment->text = $text;
-
- if (InputHelper::get('sender') != 'preview')
+ if (InputHelper::get('sender') == 'preview')
{
- CommentModel::save($comment);
- LogHelper::log('{user} edited comment in {post}', [
- 'post' => TextHelper::reprPost($comment->getPost())]);
+ $comment = Api::run(
+ new PreviewCommentJob(),
+ [
+ JobArgs::ARG_COMMENT_ID => $id,
+ JobArgs::ARG_NEW_TEXT => InputHelper::get('text')
+ ]);
+
+ Core::getContext()->transport->textPreview = $comment->getTextMarkdown();
+ $this->renderAjax();
+ }
+ else
+ {
+ Api::run(
+ new EditCommentJob(),
+ [
+ JobArgs::ARG_COMMENT_ID => $id,
+ JobArgs::ARG_NEW_TEXT => InputHelper::get('text')
+ ]);
+
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToLastVisitedUrl('comment/');
}
- $context->transport->textPreview = $comment->getText();
}
public function deleteAction($id)
{
- $comment = CommentModel::findById($id);
+ $comment = Api::run(
+ new DeleteCommentJob(),
+ [
+ JobArgs::ARG_COMMENT_ID => $id,
+ ]);
- Access::assert(
- Privilege::DeleteComment,
- Access::getIdentity($comment->getCommenter()));
-
- CommentModel::remove($comment);
-
- LogHelper::log('{user} removed comment from {post}', [
- 'post' => TextHelper::reprPost($comment->getPost())]);
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToLastVisitedUrl('comment/');
}
}
diff --git a/src/Controllers/ErrorController.php b/src/Controllers/ErrorController.php
new file mode 100644
index 00000000..144f9331
--- /dev/null
+++ b/src/Controllers/ErrorController.php
@@ -0,0 +1,30 @@
+getMessage());
+
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->renderView('message');
+ }
+
+ public function seriousExceptionView(Exception $exception)
+ {
+ \Chibi\Util\Headers::setCode(400);
+ Messenger::fail($exception->getMessage());
+ $context->transport->exception = $exception;
+ $context->transport->queries = \Chibi\Database::getLogs();
+
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->renderView('error-exception');
+ }
+}
diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php
deleted file mode 100644
index a76726dc..00000000
--- a/src/Controllers/IndexController.php
+++ /dev/null
@@ -1,52 +0,0 @@
-transport->postCount = PostModel::getCount();
-
- $featuredPost = $this->getFeaturedPost();
- if ($featuredPost)
- {
- $context->featuredPost = $featuredPost;
- $context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
- $context->featuredPostUser = UserModel::findByNameOrEmail(
- PropertyModel::get(PropertyModel::FeaturedPostUserName),
- false);
- }
- }
-
- public function helpAction($tab = null)
- {
- $config = getConfig();
- $context = getContext();
- if (empty($config->help->paths) or empty($config->help->title))
- throw new SimpleException('Help is disabled');
- $tab = $tab ?: array_keys($config->help->subTitles)[0];
- if (!isset($config->help->paths[$tab]))
- throw new SimpleException('Invalid tab');
- $context->path = TextHelper::absolutePath($config->help->paths[$tab]);
- $context->tab = $tab;
- }
-
- private function getFeaturedPost()
- {
- $config = getConfig();
- $featuredPostRotationTime = $config->misc->featuredPostMaxDays * 24 * 3600;
-
- $featuredPostId = PropertyModel::get(PropertyModel::FeaturedPostId);
- $featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
-
- //check if too old
- if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
- return PropertyModel::featureNewPost();
-
- //check if post was deleted
- $featuredPost = PostModel::findById($featuredPostId, false);
- if (!$featuredPost)
- return PropertyModel::featureNewPost();
-
- return $featuredPost;
- }
-}
diff --git a/src/Controllers/LogController.php b/src/Controllers/LogController.php
index 76542767..d12b2ce9 100644
--- a/src/Controllers/LogController.php
+++ b/src/Controllers/LogController.php
@@ -1,73 +1,39 @@
main->logsPath);
-
- $logs = [];
- foreach (glob($path . DS . '*.log') as $log)
- $logs []= basename($log);
-
- usort($logs, function($a, $b)
- {
- return strnatcasecmp($b, $a); //reverse natcasesort
- });
-
- $context->transport->logs = $logs;
+ $ret = Api::run(new ListLogsJob(), []);
+ Core::getContext()->transport->logs = $ret;
+ $this->renderView('log-list');
}
- public function viewAction($name, $page = 1, $filter = '')
+ public function logView($name, $page = 1, $filter = '')
{
- $context = getContext();
//redirect requests in form of ?query=... to canonical address
$formQuery = InputHelper::get('query');
if ($formQuery !== null)
{
- \Chibi\Util\Url::forward(
- \Chibi\Router::linkTo(
- ['LogController', 'viewAction'],
- [
- 'name' => $name,
- 'filter' => $formQuery,
- 'page' => 1
- ]));
- exit;
+ $this->redirect(\Chibi\Router::linkTo(
+ ['LogController', 'logView'],
+ [
+ 'name' => $name,
+ 'filter' => $formQuery,
+ 'page' => 1
+ ]));
+ return;
}
- Access::assert(Privilege::ViewLog);
-
- //parse input
- $page = max(1, intval($page));
- $name = str_replace(['/', '\\'], '', $name); //paranoia mode
- $path = TextHelper::absolutePath(getConfig()->main->logsPath . DS . $name);
- if (!file_exists($path))
- throw new SimpleNotFoundException('Specified log doesn\'t exist');
-
- //load lines
- $lines = file_get_contents($path);
- $lines = explode(PHP_EOL, str_replace(["\r", "\n"], PHP_EOL, $lines));
- $lines = array_reverse($lines);
-
- if (!empty($filter))
- {
- $lines = array_filter($lines, function($line) use ($filter)
- {
- return stripos($line, $filter) !== false;
- });
- }
-
- $lineCount = count($lines);
- $logsPerPage = intval(getConfig()->browsing->logsPerPage);
- $pageCount = ceil($lineCount / $logsPerPage);
- $page = min($pageCount, $page);
-
- $lines = array_slice($lines, ($page - 1) * $logsPerPage, $logsPerPage);
+ $ret = Api::run(
+ new GetLogJob(),
+ [
+ JobArgs::ARG_PAGE_NUMBER => $page,
+ JobArgs::ARG_LOG_ID => $name,
+ JobArgs::ARG_QUERY => $filter,
+ ]);
//stylize important lines
+ $lines = $ret->entities;
foreach ($lines as &$line)
if (strpos($line, 'flag') !== false)
$line = '**' . $line . '**';
@@ -77,13 +43,11 @@ class LogController
$lines = TextHelper::parseMarkdown($lines, true);
$lines = trim($lines);
- $context->transport->paginator = new StdClass;
- $context->transport->paginator->page = $page;
- $context->transport->paginator->pageCount = $pageCount;
- $context->transport->paginator->entityCount = $lineCount;
- $context->transport->paginator->entities = $lines;
+ $context = Core::getContext();
+ $context->transport->paginator = $ret;
$context->transport->lines = $lines;
$context->transport->filter = $filter;
$context->transport->name = $name;
+ $this->renderView('log-view');
}
}
diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php
index a2d90b99..f980fa10 100644
--- a/src/Controllers/PostController.php
+++ b/src/Controllers/PostController.php
@@ -1,315 +1,266 @@
viewName = 'post-list-wrapper';
+ $context = Core::getContext();
$context->source = $source;
$context->additionalInfo = $additionalInfo;
- $context->handleExceptions = true;
- //redirect requests in form of /posts/?query=... to canonical address
- $formQuery = InputHelper::get('query');
- if ($formQuery !== null)
+ try
{
- $context->transport->searchQuery = $formQuery;
- $context->transport->lastSearchQuery = $formQuery;
- if (strpos($formQuery, '/') !== false)
- throw new SimpleException('Search query contains invalid characters');
+ $query = trim($query);
+ $context->transport->searchQuery = $query;
+ $context->transport->lastSearchQuery = $query;
+ if ($source == 'mass-tag')
+ {
+ Access::assert(new Privilege(Privilege::MassTag));
+ $context->massTagTag = $additionalInfo;
+ $context->massTagQuery = $query;
- $url = \Chibi\Router::linkTo(['PostController', 'listAction'], [
- 'source' => $source,
- 'additionalInfo' => $additionalInfo,
- 'query' => $formQuery]);
- \Chibi\Util\Url::forward($url);
- exit;
+ if (!Access::check(new Privilege(Privilege::MassTag, 'all')))
+ $query = trim($query . ' submit:' . Auth::getCurrentUser()->getName());
+ }
+
+ $ret = Api::run(
+ new ListPostsJob(),
+ [
+ JobArgs::ARG_PAGE_NUMBER => $page,
+ JobArgs::ARG_QUERY => $query
+ ]);
+
+ $context->transport->posts = $ret->entities;
+ $context->transport->paginator = $ret;
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
}
- $query = trim($query);
- $page = max(1, intval($page));
- $postsPerPage = intval(getConfig()->browsing->postsPerPage);
+ $this->renderView('post-list-wrapper');
+ }
+
+ public function listRedirectAction($source = 'posts')
+ {
+ $context = Core::getContext();
+ $query = trim(InputHelper::get('query'));
$context->transport->searchQuery = $query;
$context->transport->lastSearchQuery = $query;
- Access::assert(Privilege::ListPosts);
- if ($source == 'mass-tag')
- {
- Access::assert(Privilege::MassTag);
- $context->massTagTag = $additionalInfo;
- $context->massTagQuery = $query;
+ if (strpos($query, '/') !== false)
+ throw new SimpleException('Search query contains invalid characters');
- if (!Access::check(Privilege::MassTag, 'all'))
- $query = trim($query . ' submit:' . Auth::getCurrentUser()->name);
- }
+ $params = [];
+ $params['source'] = $source;
+ if ($query)
+ $params['query'] = $query;
+ #if ($additionalInfo)
+ # $params['additionalInfo'] = $additionalInfo;
+ $params['page'] = 1;
- $posts = PostSearchService::getEntities($query, $postsPerPage, $page);
- $postCount = PostSearchService::getEntityCount($query);
- $pageCount = ceil($postCount / $postsPerPage);
- $page = min($pageCount, $page);
- PostModel::preloadTags($posts);
+ $url = \Chibi\Router::linkTo(['PostController', 'listView'], $params);
+ $this->redirect($url);
+ }
- $context->transport->paginator = new StdClass;
- $context->transport->paginator->page = $page;
- $context->transport->paginator->pageCount = $pageCount;
- $context->transport->paginator->entityCount = $postCount;
- $context->transport->paginator->entities = $posts;
- $context->transport->posts = $posts;
+ public function favoritesView($page = 1)
+ {
+ $this->listView('favmin:1', $page, 'favorites');
+ }
+
+ public function upvotedView($page = 1)
+ {
+ $this->listView('scoremin:1', $page, 'upvoted');
+ }
+
+ public function randomView($page = 1)
+ {
+ $this->listView('order:random', $page, 'random');
}
public function toggleTagAction($id, $tag, $enable)
{
- $context = getContext();
- $tagName = $tag;
- $post = PostModel::findByIdOrName($id);
- $context->transport->post = $post;
-
- if (!InputHelper::get('submit'))
- return;
-
- Access::assert(
+ Access::assert(new Privilege(
Privilege::MassTag,
- Access::getIdentity($post->getUploader()));
+ Access::getIdentity(PostModel::getById($id)->getUploader())));
- $tags = $post->getTags();
+ Api::run(
+ new TogglePostTagJob(),
+ [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_TAG_NAME => $tag,
+ JobArgs::ARG_NEW_STATE => $enable,
+ ]);
- if (!$enable)
- {
- foreach ($tags as $i => $tag)
- if ($tag->name == $tagName)
- unset($tags[$i]);
-
- LogHelper::log('{user} untagged {post} with {tag}', [
- 'post' => TextHelper::reprPost($post),
- 'tag' => TextHelper::reprTag($tag)]);
- }
- elseif ($enable)
- {
- $tag = TagModel::findByName($tagName, false);
- if ($tag === null)
- {
- $tag = TagModel::spawn();
- $tag->name = $tagName;
- TagModel::save($tag);
- }
-
- $tags []= $tag;
- LogHelper::log('{user} tagged {post} with {tag}', [
- 'post' => TextHelper::reprPost($post),
- 'tag' => TextHelper::reprTag($tag)]);
- }
-
- $post->setTags($tags);
-
- PostModel::save($post);
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToLastVisitedUrl();
}
- public function favoritesAction($page = 1)
+ public function uploadView()
{
- $this->listAction('favmin:1', $page);
- }
-
- public function upvotedAction($page = 1)
- {
- $this->listAction('scoremin:1', $page);
- }
-
- public function randomAction($page = 1)
- {
- $this->listAction('order:random', $page);
+ $this->renderView('post-upload');
}
public function uploadAction()
{
- $context = getContext();
- Access::assert(Privilege::UploadPost);
- if (getConfig()->registration->needEmailForUploading)
- Access::assertEmailConfirmation();
+ $jobArgs =
+ [
+ JobArgs::ARG_ANONYMOUS => InputHelper::get('anonymous'),
+ JobArgs::ARG_NEW_SAFETY => InputHelper::get('safety'),
+ JobArgs::ARG_NEW_TAG_NAMES => $this->splitTags(InputHelper::get('tags')),
+ JobArgs::ARG_NEW_SOURCE => InputHelper::get('source'),
+ ];
- if (!InputHelper::get('submit'))
- return;
-
- \Chibi\Database::transaction(function() use ($context)
+ if (!empty(InputHelper::get('url')))
{
- $post = PostModel::spawn();
- LogHelper::bufferChanges();
+ $jobArgs[JobArgs::ARG_NEW_POST_CONTENT_URL] = InputHelper::get('url');
+ }
+ elseif (!empty($_FILES['file']['name']))
+ {
+ $file = $_FILES['file'];
+ TransferHelper::handleUploadErrors($file);
- //basic stuff
- $anonymous = InputHelper::get('anonymous');
- if (Auth::isLoggedIn() and !$anonymous)
- $post->setUploader(Auth::getCurrentUser());
+ $jobArgs[JobArgs::ARG_NEW_POST_CONTENT] = new ApiFileInput(
+ $file['tmp_name'],
+ $file['name']);
+ }
- //store the post to get the ID in the logs
- PostModel::forgeId($post);
+ Api::run(new AddPostJob(), $jobArgs);
- //do the edits
- $this->doEdit($post, true);
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToPostList();
+ }
- //this basically means that user didn't specify file nor url
- if (empty($post->type))
- throw new SimpleException('No post type detected; upload faled');
+ public function editView($id)
+ {
+ $post = Api::run(new GetPostJob(), [
+ JobArgs::ARG_POST_ID => $id]);
- //clean edit log
- LogHelper::setBuffer([]);
-
- //log
- $fmt = ($anonymous and !getConfig()->misc->logAnonymousUploads)
- ? '{anon}'
- : '{user}';
- $fmt .= ' added {post} (tags: {tags}, safety: {safety}, source: {source})';
- LogHelper::log($fmt, [
- 'post' => TextHelper::reprPost($post),
- 'tags' => TextHelper::reprTags($post->getTags()),
- 'safety' => PostSafety::toString($post->safety),
- 'source' => $post->source]);
-
- //finish
- LogHelper::flush();
- PostModel::save($post);
- });
+ $context = Core::getContext()->transport->post = $post;
+ $this->renderView('post-edit');
}
public function editAction($id)
{
- $context = getContext();
- $post = PostModel::findByIdOrName($id);
- $context->transport->post = $post;
-
- if (!InputHelper::get('submit'))
- return;
+ $post = PostModel::getByIdOrName($id);
$editToken = InputHelper::get('edit-token');
if ($editToken != $post->getEditToken())
throw new SimpleException('This post was already edited by someone else in the meantime');
- LogHelper::bufferChanges();
- $this->doEdit($post, false);
- LogHelper::flush();
+ $jobArgs =
+ [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_NEW_SAFETY => InputHelper::get('safety'),
+ JobArgs::ARG_NEW_TAG_NAMES => $this->splitTags(InputHelper::get('tags')),
+ JobArgs::ARG_NEW_SOURCE => InputHelper::get('source'),
+ JobArgs::ARG_NEW_RELATED_POST_IDS => $this->splitPostIds(InputHelper::get('relations')),
+ ];
- PostModel::save($post);
- TagModel::removeUnused();
+ if (!empty(InputHelper::get('url')))
+ {
+ $jobArgs[JobArgs::ARG_NEW_POST_CONTENT_URL] = InputHelper::get('url');
+ }
+ elseif (!empty($_FILES['file']['name']))
+ {
+ $file = $_FILES['file'];
+ TransferHelper::handleUploadErrors($file);
+
+ $jobArgs[JobArgs::ARG_NEW_POST_CONTENT] = new ApiFileInput(
+ $file['tmp_name'],
+ $file['name']);
+ }
+
+ if (!empty($_FILES['thumb']['name']))
+ {
+ $file = $_FILES['thumb'];
+ TransferHelper::handleUploadErrors($file);
+
+ $jobArgs[JobArgs::ARG_NEW_THUMB_CONTENT] = new ApiFileInput(
+ $file['tmp_name'],
+ $file['name']);
+ }
+
+ Api::run(new EditPostJob(), $jobArgs);
+
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToGenericView($id);
}
public function flagAction($id)
{
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::FlagPost, Access::getIdentity($post->getUploader()));
+ Api::run(new FlagPostJob(), [JobArgs::ARG_POST_ID => $id]);
- if (!InputHelper::get('submit'))
- return;
-
- $key = TextHelper::reprPost($post);
-
- $flagged = SessionHelper::get('flagged', []);
- if (in_array($key, $flagged))
- throw new SimpleException('You already flagged this post');
- $flagged []= $key;
- SessionHelper::set('flagged', $flagged);
-
- LogHelper::log('{user} flagged {post} for moderator attention', ['post' => TextHelper::reprPost($post)]);
+ if ($this->isAjax())
+ $this->renderAjax();
+ else
+ $this->redirectToGenericView($id);
}
public function hideAction($id)
{
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::HidePost, Access::getIdentity($post->getUploader()));
-
- if (!InputHelper::get('submit'))
- return;
-
- $post->setHidden(true);
- PostModel::save($post);
-
- LogHelper::log('{user} hidden {post}', ['post' => TextHelper::reprPost($post)]);
+ Api::run(new TogglePostVisibilityJob(), [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_NEW_STATE => false]);
+ $this->redirectToGenericView($id);
}
public function unhideAction($id)
{
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::HidePost, Access::getIdentity($post->getUploader()));
-
- if (!InputHelper::get('submit'))
- return;
-
- $post->setHidden(false);
- PostModel::save($post);
-
- LogHelper::log('{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]);
+ Api::run(new TogglePostVisibilityJob(), [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_NEW_STATE => true]);
+ $this->redirectToGenericView($id);
}
public function deleteAction($id)
{
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::DeletePost, Access::getIdentity($post->getUploader()));
-
- if (!InputHelper::get('submit'))
- return;
-
- PostModel::remove($post);
-
- LogHelper::log('{user} deleted {post}', ['post' => TextHelper::reprPost($id)]);
+ Api::run(new DeletePostJob(), [
+ JobArgs::ARG_POST_ID => $id]);
+ $this->redirectToPostList();
}
public function addFavoriteAction($id)
{
- $context = getContext();
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::FavoritePost, Access::getIdentity($post->getUploader()));
- Access::assertAuthentication();
-
- if (!InputHelper::get('submit'))
- return;
-
- UserModel::updateUserScore(Auth::getCurrentUser(), $post, 1);
- UserModel::addToUserFavorites(Auth::getCurrentUser(), $post);
+ Api::run(new TogglePostFavoriteJob(), [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_NEW_STATE => true]);
+ $this->redirectToGenericView($id);
}
public function removeFavoriteAction($id)
{
- $context = getContext();
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::FavoritePost, Access::getIdentity($post->getUploader()));
- Access::assertAuthentication();
-
- if (!InputHelper::get('submit'))
- return;
-
- UserModel::removeFromUserFavorites(Auth::getCurrentUser(), $post);
+ Api::run(new TogglePostFavoriteJob(), [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_NEW_STATE => false]);
+ $this->redirectToGenericView($id);
}
public function scoreAction($id, $score)
{
- $context = getContext();
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::ScorePost, Access::getIdentity($post->getUploader()));
- Access::assertAuthentication();
-
- if (!InputHelper::get('submit'))
- return;
-
- UserModel::updateUserScore(Auth::getCurrentUser(), $post, $score);
+ Api::run(new ScorePostJob(), [
+ JobArgs::ARG_POST_ID => $id,
+ JobArgs::ARG_NEW_POST_SCORE => $score]);
+ $this->redirectToGenericView($id);
}
public function featureAction($id)
{
- $context = getContext();
- $post = PostModel::findByIdOrName($id);
- Access::assert(Privilege::FeaturePost, Access::getIdentity($post->getUploader()));
- PropertyModel::set(PropertyModel::FeaturedPostId, $post->id);
- PropertyModel::set(PropertyModel::FeaturedPostDate, time());
- PropertyModel::set(PropertyModel::FeaturedPostUserName, Auth::getCurrentUser()->name);
- LogHelper::log('{user} featured {post} on main page', ['post' => TextHelper::reprPost($post)]);
+ Api::run(new FeaturePostJob(), [
+ JobArgs::ARG_POST_ID => $id]);
+ $this->redirectToGenericView($id);
}
- public function viewAction($id)
+ public function genericView($id)
{
- $context = getContext();
- $post = PostModel::findByIdOrName($id);
- CommentModel::preloadCommenters($post->getComments());
+ $context = Core::getContext();
- if ($post->hidden)
- Access::assert(Privilege::ViewPost, 'hidden');
- Access::assert(Privilege::ViewPost);
- Access::assert(Privilege::ViewPost, PostSafety::toString($post->safety));
+ $post = Api::run(new GetPostJob(), [
+ JobArgs::ARG_POST_ID => $id]);
try
{
@@ -327,212 +278,77 @@ class PostController
$context->transport->lastSearchQuery, $id);
}
- $favorite = Auth::getCurrentUser()->hasFavorited($post);
- $score = Auth::getCurrentUser()->getScore($post);
+ //todo:
+ //move these to PostEntity when implementing ApiController
+ $isUserFavorite = Auth::getCurrentUser()->hasFavorited($post);
+ $userScore = Auth::getCurrentUser()->getScore($post);
$flagged = in_array(TextHelper::reprPost($post), SessionHelper::get('flagged', []));
- $context->favorite = $favorite;
- $context->score = $score;
+ $context->isUserFavorite = $isUserFavorite;
+ $context->userScore = $userScore;
$context->flagged = $flagged;
$context->transport->post = $post;
$context->transport->prevPostId = $prevPostId ? $prevPostId : null;
$context->transport->nextPostId = $nextPostId ? $nextPostId : null;
+
+ $this->renderView('post-view');
}
- public function thumbAction($name, $width = null, $height = null)
+ public function fileView($name)
{
- $context = getContext();
- $path = PostModel::getThumbCustomPath($name, $width, $height);
- if (!file_exists($path))
- {
- $path = PostModel::getThumbDefaultPath($name, $width, $height);
- if (!file_exists($path))
- {
- $post = PostModel::findByIdOrName($name);
- Access::assert(Privilege::ListPosts);
- Access::assert(Privilege::ListPosts, PostSafety::toString($post->safety));
- $post->generateThumb($width, $height);
- if (!file_exists($path))
- {
- $path = getConfig()->main->mediaPath . DS . 'img' . DS . 'thumb.jpg';
- $path = TextHelper::absolutePath($path);
- }
- }
- }
+ $ret = Api::run(new GetPostContentJob(), [JobArgs::ARG_POST_NAME => $name]);
- if (!is_readable($path))
- throw new SimpleException('Thumbnail file is not readable');
-
- $context->layoutName = 'layout-file';
- $context->transport->cacheDaysToLive = 365;
- $context->transport->mimeType = 'image/jpeg';
- $context->transport->fileHash = 'thumb' . md5($name . filemtime($path));
- $context->transport->filePath = $path;
- }
-
- public function retrieveAction($name)
- {
- $post = PostModel::findByName($name, true);
- $config = getConfig();
- $context = getContext();
-
- Access::assert(Privilege::RetrievePost);
- Access::assert(Privilege::RetrievePost, PostSafety::toString($post->safety));
-
- $path = $config->main->filesPath . DS . $post->name;
- $path = TextHelper::absolutePath($path);
- if (!file_exists($path))
- throw new SimpleNotFoundException('Post file does not exist');
- if (!is_readable($path))
- throw new SimpleException('Post file is not readable');
-
- $fn = sprintf('%s_%s_%s.%s',
- $config->main->title,
- $post->id,
- join(',', array_map(function($tag) { return $tag->name; }, $post->getTags())),
- TextHelper::resolveMimeType($post->mimeType) ?: 'dat');
- $fn = preg_replace('/[[:^print:]]/', '', $fn);
-
- $ttl = 60 * 60 * 24 * 14;
-
- $context->layoutName = 'layout-file';
+ $context = Core::getContext();
$context->transport->cacheDaysToLive = 14;
- $context->transport->customFileName = $fn;
- $context->transport->mimeType = $post->mimeType;
- $context->transport->fileHash = 'post' . $post->fileHash;
- $context->transport->filePath = $path;
+ $context->transport->customFileName = $ret->fileName;
+ $context->transport->mimeType = $ret->mimeType;
+ $context->transport->fileHash = 'post' . md5(substr($ret->fileContent, 0, 4096));
+ $context->transport->fileContent = $ret->fileContent;
+ $context->transport->lastModified = $ret->lastModified;
+ $this->renderFile();
}
- private function doEdit($post, $isNew)
+ public function thumbView($name)
{
- /* safety */
- $suppliedSafety = InputHelper::get('safety');
- if ($suppliedSafety !== null)
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostSafety, Access::getIdentity($post->getUploader()));
+ $ret = Api::run(new GetPostThumbJob(), [JobArgs::ARG_POST_NAME => $name]);
- $oldSafety = $post->safety;
- $post->setSafety($suppliedSafety);
- $newSafety = $post->safety;
+ $context = Core::getContext();
+ $context->transport->cacheDaysToLive = 365;
+ $context->transport->customFileName = $ret->fileName;
+ $context->transport->mimeType = 'image/jpeg';
+ $context->transport->fileHash = 'thumb' . md5(substr($ret->fileContent, 0, 4096));
+ $context->transport->fileContent = $ret->fileContent;
+ $context->transport->lastModified = $ret->lastModified;
+ $this->renderFile();
+ }
- if ($oldSafety != $newSafety)
- {
- LogHelper::log('{user} changed safety of {post} to {safety}', [
- 'post' => TextHelper::reprPost($post),
- 'safety' => PostSafety::toString($post->safety)]);
- }
- }
- /* tags */
- $suppliedTags = InputHelper::get('tags');
- if ($suppliedTags !== null)
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostTags, Access::getIdentity($post->getUploader()));
+ private function splitPostIds($string)
+ {
+ $ids = preg_split('/\D/', trim($string));
+ $ids = array_filter($ids, function($x) { return $x != ''; });
+ $ids = array_map('intval', $ids);
+ $ids = array_unique($ids);
+ return $ids;
+ }
- $oldTags = array_map(function($tag) { return $tag->name; }, $post->getTags());
- $post->setTagsFromText($suppliedTags);
- $newTags = array_map(function($tag) { return $tag->name; }, $post->getTags());
+ private function splitTags($string)
+ {
+ $tags = preg_split('/[,;\s]+/', trim($string));
+ $tags = array_filter($tags, function($x) { return $x != ''; });
+ $tags = array_unique($tags);
+ return $tags;
+ }
- foreach (array_diff($oldTags, $newTags) as $tag)
- {
- LogHelper::log('{user} untagged {post} with {tag}', [
- 'post' => TextHelper::reprPost($post),
- 'tag' => TextHelper::reprTag($tag)]);
- }
+ private function redirectToPostList()
+ {
+ $this->redirect(\Chibi\Router::linkTo(['PostController', 'listView']));
+ }
- foreach (array_diff($newTags, $oldTags) as $tag)
- {
- LogHelper::log('{user} tagged {post} with {tag}', [
- 'post' => TextHelper::reprPost($post),
- 'tag' => TextHelper::reprTag($tag)]);
- }
- }
-
- /* source */
- $suppliedSource = InputHelper::get('source');
- if ($suppliedSource !== null)
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostSource, Access::getIdentity($post->getUploader()));
-
- $oldSource = $post->source;
- $post->setSource($suppliedSource);
- $newSource = $post->source;
-
- if ($oldSource != $newSource)
- {
- LogHelper::log('{user} changed source of {post} to {source}', [
- 'post' => TextHelper::reprPost($post),
- 'source' => $post->source]);
- }
- }
-
- /* relations */
- $suppliedRelations = InputHelper::get('relations');
- if ($suppliedRelations !== null)
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostRelations, Access::getIdentity($post->getUploader()));
-
- $oldRelatedIds = array_map(function($post) { return $post->id; }, $post->getRelations());
- $post->setRelationsFromText($suppliedRelations);
- $newRelatedIds = array_map(function($post) { return $post->id; }, $post->getRelations());
-
- foreach (array_diff($oldRelatedIds, $newRelatedIds) as $post2id)
- {
- LogHelper::log('{user} removed relation between {post} and {post2}', [
- 'post' => TextHelper::reprPost($post),
- 'post2' => TextHelper::reprPost($post2id)]);
- }
-
- foreach (array_diff($newRelatedIds, $oldRelatedIds) as $post2id)
- {
- LogHelper::log('{user} added relation between {post} and {post2}', [
- 'post' => TextHelper::reprPost($post),
- 'post2' => TextHelper::reprPost($post2id)]);
- }
- }
-
- /* file contents */
- if (!empty($_FILES['file']['name']))
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostFile, Access::getIdentity($post->getUploader()));
-
- $suppliedFile = $_FILES['file'];
- TransferHelper::handleUploadErrors($suppliedFile);
-
- $post->setContentFromPath($suppliedFile['tmp_name'], $suppliedFile['name']);
-
- if (!$isNew)
- LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);
- }
- elseif (InputHelper::get('url'))
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostFile, Access::getIdentity($post->getUploader()));
-
- $url = InputHelper::get('url');
- $post->setContentFromUrl($url);
-
- if (!$isNew)
- LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);
- }
-
- /* thumbnail */
- if (!empty($_FILES['thumb']['name']))
- {
- if (!$isNew)
- Access::assert(Privilege::EditPostThumb, Access::getIdentity($post->getUploader()));
-
- $suppliedFile = $_FILES['thumb'];
- TransferHelper::handleUploadErrors($suppliedFile);
-
- $post->setCustomThumbnailFromPath($srcPath = $suppliedFile['tmp_name']);
-
- LogHelper::log('{user} changed thumb of {post}', ['post' => TextHelper::reprPost($post)]);
- }
+ private function redirectToGenericView($id)
+ {
+ $this->redirect(\Chibi\Router::linkTo(
+ ['PostController', 'genericView'],
+ ['id' => $id]));
}
}
diff --git a/src/Controllers/StaticPagesController.php b/src/Controllers/StaticPagesController.php
new file mode 100644
index 00000000..be3b91b9
--- /dev/null
+++ b/src/Controllers/StaticPagesController.php
@@ -0,0 +1,50 @@
+transport->postCount = PostModel::getCount();
+ $context->transport->postSpaceUsage = PostModel::getSpaceUsage();
+
+ PostModel::featureRandomPostIfNecessary();
+ $featuredPost = PostModel::getFeaturedPost();
+ if ($featuredPost)
+ {
+ $context->featuredPost = $featuredPost;
+ $context->featuredPostUnixTime = PropertyModel::get(PropertyModel::FeaturedPostUnixTime);
+ $context->featuredPostUser = UserModel::tryGetByName(
+ PropertyModel::get(PropertyModel::FeaturedPostUserName));
+ }
+
+ $this->renderView('static-main');
+ }
+
+ public function helpView($tab = null)
+ {
+ $config = Core::getConfig();
+ $context = Core::getContext();
+
+ if (empty($config->help->paths) or empty($config->help->title))
+ throw new SimpleException('Help is disabled');
+
+ $tab = $tab ?: array_keys($config->help->subTitles)[0];
+ if (!isset($config->help->paths[$tab]))
+ throw new SimpleException('Invalid tab');
+
+ $context->path = TextHelper::absolutePath($config->help->paths[$tab]);
+ $context->tab = $tab;
+
+ $this->renderView('static-help');
+ }
+
+ public function apiDocsView()
+ {
+ $this->renderView('static-api');
+ }
+
+ public function fatalErrorView($code = null)
+ {
+ throw new SimpleException('Error ' . $code . ' while retrieving ' . $_SERVER['REQUEST_URI']);
+ }
+}
diff --git a/src/Controllers/TagController.php b/src/Controllers/TagController.php
index 26d23a91..a1ff0056 100644
--- a/src/Controllers/TagController.php
+++ b/src/Controllers/TagController.php
@@ -1,160 +1,163 @@
viewName = 'tag-list-wrapper';
- Access::assert(Privilege::ListTags);
+ $ret = Api::run(
+ new ListTagsJob(),
+ [
+ JobArgs::ARG_PAGE_NUMBER => $page,
+ JobArgs::ARG_QUERY => $filter,
+ ]);
- $suppliedFilter = $filter ?: 'order:alpha,asc';
- $page = max(1, intval($page));
- $tagsPerPage = intval(getConfig()->browsing->tagsPerPage);
-
- $tags = TagSearchService::getEntitiesRows($suppliedFilter, $tagsPerPage, $page);
- $tagCount = TagSearchService::getEntityCount($suppliedFilter);
- $pageCount = ceil($tagCount / $tagsPerPage);
- $page = min($pageCount, $page);
-
- $context->filter = $suppliedFilter;
- $context->transport->tags = $tags;
-
- if ($context->json)
- {
- $context->transport->tags = array_values(array_map(function($tag) {
- return ['name' => $tag['name'], 'count' => $tag['post_count']];
- }, $context->transport->tags));
- }
- else
- {
- $context->highestUsage = TagSearchService::getMostUsedTag()['post_count'];
- $context->transport->paginator = new StdClass;
- $context->transport->paginator->page = $page;
- $context->transport->paginator->pageCount = $pageCount;
- $context->transport->paginator->entityCount = $tagCount;
- $context->transport->paginator->entities = $tags;
- }
+ $context = Core::getContext();
+ $context->highestUsage = TagSearchService::getMostUsedTag()->getPostCount();
+ $context->filter = $filter;
+ $context->transport->tags = $ret->entities;
+ $context->transport->paginator = $ret;
+ $this->renderViewWithSource('tag-list-wrapper', 'list');
}
- public function autoCompleteAction()
+ public function autoCompleteView()
{
- $context = getContext();
- Access::assert(Privilege::ListTags);
+ $filter = InputHelper::get('search');
+ $filter .= ' order:popularity,desc';
- $suppliedSearch = InputHelper::get('search');
-
- $filter = $suppliedSearch . ' order:popularity,desc';
- $tags = TagSearchService::getEntitiesRows($filter, 15, 1);
+ $job = new ListTagsJob();
+ $job->getPager()->setPageSize(15);
+ $ret = Api::run(
+ $job,
+ [
+ JobArgs::ARG_QUERY => $filter,
+ JobArgs::ARG_PAGE_NUMBER => 1,
+ ]);
+ $context = Core::getContext();
$context->transport->tags =
array_values(array_map(
function($tag)
{
return [
- 'name' => $tag['name'],
- 'count' => $tag['post_count']
+ 'name' => $tag->getName(),
+ 'count' => $tag->getPostCount(),
];
- }, $tags));
+ }, $ret->entities));
+
+ $this->renderAjax();
}
- public function relatedAction()
+ public function relatedView()
{
- $context = getContext();
- Access::assert(Privilege::ListTags);
+ $otherTags = (array) InputHelper::get('context');
+ $tag = InputHelper::get('tag');
- $suppliedContext = (array) InputHelper::get('context');
- $suppliedTag = InputHelper::get('tag');
-
- $limit = intval(getConfig()->browsing->tagsRelated);
- $tags = TagSearchService::getRelatedTagRows($suppliedTag, $suppliedContext, $limit);
+ $ret = Api::run(
+ (new ListRelatedTagsJob),
+ [
+ JobArgs::ARG_TAG_NAME => $tag,
+ JobArgs::ARG_TAG_NAMES => $otherTags,
+ JobArgs::ARG_PAGE_NUMBER => 1
+ ]);
+ $context = Core::getContext();
$context->transport->tags =
array_values(array_map(
function($tag)
{
return [
- 'name' => $tag['name'],
- 'count' => $tag['post_count']
+ 'name' => $tag->getName(),
+ 'count' => $tag->getPostCount(),
];
- }, $tags));
+ }, $ret->entities));
+
+ $this->renderAjax();
+ }
+
+ public function mergeView()
+ {
+ $this->renderViewWithSource('tag-list-wrapper', 'merge');
}
public function mergeAction()
{
- $context = getContext();
- $context->viewName = 'tag-list-wrapper';
- $context->handleExceptions = true;
+ try
+ {
+ Api::run(
+ new MergeTagsJob(),
+ [
+ JobArgs::ARG_SOURCE_TAG_NAME => InputHelper::get('source-tag'),
+ JobArgs::ARG_TARGET_TAG_NAME => InputHelper::get('target-tag'),
+ ]);
- Access::assert(Privilege::MergeTags);
- if (!InputHelper::get('submit'))
- return;
+ Messenger::success('Tags merged successfully.');
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
+ }
- TagModel::removeUnused();
+ $this->renderViewWithSource('tag-list-wrapper', 'merge');
+ }
- $suppliedSourceTag = InputHelper::get('source-tag');
- $suppliedSourceTag = TagModel::validateTag($suppliedSourceTag);
-
- $suppliedTargetTag = InputHelper::get('target-tag');
- $suppliedTargetTag = TagModel::validateTag($suppliedTargetTag);
-
- TagModel::merge($suppliedSourceTag, $suppliedTargetTag);
-
- LogHelper::log('{user} merged {source} with {target}', [
- 'source' => TextHelper::reprTag($suppliedSourceTag),
- 'target' => TextHelper::reprTag($suppliedTargetTag)]);
-
- Messenger::message('Tags merged successfully.');
+ public function renameView()
+ {
+ $this->renderViewWithSource('tag-list-wrapper', 'rename');
}
public function renameAction()
{
- $context = getContext();
- $context->viewName = 'tag-list-wrapper';
- $context->handleExceptions = true;
+ try
+ {
+ Api::run(
+ new RenameTagsJob(),
+ [
+ JobArgs::ARG_SOURCE_TAG_NAME => InputHelper::get('source-tag'),
+ JobArgs::ARG_TARGET_TAG_NAME => InputHelper::get('target-tag'),
+ ]);
- Access::assert(Privilege::MergeTags);
- if (!InputHelper::get('submit'))
- return;
+ Messenger::success('Tag renamed successfully.');
+ }
+ catch (Exception $e)
+ {
+ Messenger::fail($e->getMessage());
+ }
- TagModel::removeUnused();
+ $this->renderViewWithSource('tag-list-wrapper', 'rename');
+ }
- $suppliedSourceTag = InputHelper::get('source-tag');
- $suppliedSourceTag = TagModel::validateTag($suppliedSourceTag);
-
- $suppliedTargetTag = InputHelper::get('target-tag');
- $suppliedTargetTag = TagModel::validateTag($suppliedTargetTag);
-
- TagModel::rename($suppliedSourceTag, $suppliedTargetTag);
-
- LogHelper::log('{user} renamed {source} to {target}', [
- 'source' => TextHelper::reprTag($suppliedSourceTag),
- 'target' => TextHelper::reprTag($suppliedTargetTag)]);
-
- Messenger::message('Tag renamed successfully.');
+ public function massTagRedirectView()
+ {
+ Access::assert(new Privilege(Privilege::MassTag));
+ $this->renderViewWithSource('tag-list-wrapper', 'mass-tag');
}
public function massTagRedirectAction()
{
- $context = getContext();
- $context->viewName = 'tag-list-wrapper';
-
- Access::assert(Privilege::MassTag);
- if (!InputHelper::get('submit'))
- return;
-
+ Access::assert(new Privilege(Privilege::MassTag));
$suppliedOldPage = intval(InputHelper::get('old-page'));
$suppliedOldQuery = InputHelper::get('old-query');
$suppliedQuery = InputHelper::get('query');
$suppliedTag = InputHelper::get('tag');
- $params = [
+ $params =
+ [
'source' => 'mass-tag',
- 'query' => $suppliedQuery ?: ' ',
- 'additionalInfo' => $suppliedTag ? TagModel::validateTag($suppliedTag) : '',
+ 'query' => $suppliedQuery ?: '',
+ 'additionalInfo' => $suppliedTag ? $suppliedTag : '',
];
+
if ($suppliedOldPage != 0 and $suppliedOldQuery == $suppliedQuery)
$params['page'] = $suppliedOldPage;
- \Chibi\Util\Url::forward(\Chibi\Router::linkTo(['PostController', 'listAction'], $params));
- exit;
+
+ $url = \Chibi\Router::linkTo(['PostController', 'listView'], $params);
+ $this->redirect($url);
+ }
+
+
+ private function renderViewWithSource($viewName, $source)
+ {
+ $context = Core::getContext();
+ $context->source = $source;
+ $this->renderView($viewName);
}
}
diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php
index a81a8887..d26f3a45 100644
--- a/src/Controllers/UserController.php
+++ b/src/Controllers/UserController.php
@@ -1,618 +1,402 @@
$page,
+ JobArgs::ARG_QUERY => $filter,
+ ]);
- $suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
- $page = max(1, intval($page));
- $usersPerPage = intval(getConfig()->browsing->usersPerPage);
-
- $users = UserSearchService::getEntities($suppliedFilter, $usersPerPage, $page);
- $userCount = UserSearchService::getEntityCount($suppliedFilter);
- $pageCount = ceil($userCount / $usersPerPage);
- $page = min($pageCount, $page);
-
- $context->filter = $suppliedFilter;
- $context->transport->users = $users;
- $context->transport->paginator = new StdClass;
- $context->transport->paginator->page = $page;
- $context->transport->paginator->pageCount = $pageCount;
- $context->transport->paginator->entityCount = $userCount;
- $context->transport->paginator->entities = $users;
- $context->transport->paginator->params = func_get_args();
+ $context = Core::getContext();
+ $context->filter = $filter;
+ $context->transport->users = $ret->entities;
+ $context->transport->paginator = $ret;
+ $this->renderView('user-list');
}
- public function flagAction($name)
+ public function genericView($identifier, $tab = 'favs', $page = 1)
{
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::FlagUser,
- Access::getIdentity($user));
-
- if (!InputHelper::get('submit'))
- return;
-
- $key = TextHelper::reprUser($user);
-
- $flagged = SessionHelper::get('flagged', []);
- if (in_array($key, $flagged))
- throw new SimpleException('You already flagged this user');
- $flagged []= $key;
- SessionHelper::set('flagged', $flagged);
-
- LogHelper::log('{user} flagged {subject} for moderator attention', [
- 'subject' => TextHelper::reprUser($user)]);
+ $this->prepareGenericView($identifier, $tab, $page);
+ $this->renderView('user-view');
}
- public function banAction($name)
+ public function settingsAction($identifier)
{
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::BanUser,
- Access::getIdentity($user));
+ $this->prepareGenericView($identifier, 'settings');
- if (!InputHelper::get('submit'))
- return;
-
- $user->banned = true;
- UserModel::save($user);
-
- LogHelper::log('{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]);
- }
-
- public function unbanAction($name)
- {
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::BanUser,
- Access::getIdentity($user));
-
- if (!InputHelper::get('submit'))
- return;
-
- $user->banned = false;
- UserModel::save($user);
-
- LogHelper::log('{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]);
- }
-
- public function acceptRegistrationAction($name)
- {
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::AcceptUserRegistration);
-
- if (!InputHelper::get('submit'))
- return;
-
- $user->staffConfirmed = true;
- UserModel::save($user);
- LogHelper::log('{user} confirmed {subject}\'s account', ['subject' => TextHelper::reprUser($user)]);
- }
-
- public function deleteAction($name)
- {
- $context = getContext();
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::ViewUser,
- Access::getIdentity($user));
- Access::assert(
- Privilege::DeleteUser,
- Access::getIdentity($user));
-
- $this->loadUserView($user);
- $context->transport->tab = 'delete';
-
- $context->suppliedCurrentPassword = $suppliedCurrentPassword = InputHelper::get('current-password');
-
- if (!InputHelper::get('submit'))
- return;
-
- $name = $user->name;
- if (Auth::getCurrentUser()->id == $user->id)
- {
- $suppliedPasswordHash = UserModel::hashPassword($suppliedCurrentPassword, $user->passSalt);
- if ($suppliedPasswordHash != $user->passHash)
- throw new SimpleException('Must supply valid password');
- }
-
- $oldId = $user->id;
- UserModel::remove($user);
- if ($oldId == Auth::getCurrentUser()->id)
- Auth::logOut();
-
- \Chibi\Util\Url::forward(\Chibi\Router::linkTo(['IndexController', 'indexAction']));
- LogHelper::log('{user} removed {subject}\'s account', ['subject' => TextHelper::reprUser($name)]);
- exit;
- }
-
- public function settingsAction($name)
- {
- $context = getContext();
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::ViewUser,
- Access::getIdentity($user));
- Access::assert(
- Privilege::ChangeUserSettings,
- Access::getIdentity($user));
-
- $this->loadUserView($user);
- $context->transport->tab = 'settings';
-
- if (!InputHelper::get('submit'))
- return;
-
- $suppliedSafety = InputHelper::get('safety');
- if (!is_array($suppliedSafety))
- $suppliedSafety = [];
- foreach (PostSafety::getAll() as $safety)
- $user->enableSafety($safety, in_array($safety, $suppliedSafety));
-
- $user->enableEndlessScrolling(InputHelper::get('endless-scrolling'));
- $user->enablePostTagTitles(InputHelper::get('post-tag-titles'));
- $user->enableHidingDislikedPosts(InputHelper::get('hide-disliked-posts'));
-
- if ($user->accessRank != AccessRank::Anonymous)
- UserModel::save($user);
- if ($user->id == Auth::getCurrentUser()->id)
- Auth::setCurrentUser($user);
- Messenger::message('Browsing settings updated!');
- }
-
- public function editAction($name)
- {
- $context = getContext();
try
{
- $user = UserModel::findByNameOrEmail($name);
- Access::assert(
- Privilege::ViewUser,
- Access::getIdentity($user));
+ $suppliedSafety = InputHelper::get('safety');
+ $desiredSafety = PostSafety::makeFlags($suppliedSafety);
- $this->loadUserView($user);
- $context->transport->tab = 'edit';
+ $user = Api::run(
+ new EditUserSettingsJob(),
+ $this->appendUserIdentifierArgument(
+ [
+ JobArgs::ARG_NEW_SETTINGS =>
+ [
+ UserSettings::SETTING_SAFETY => $desiredSafety,
+ UserSettings::SETTING_ENDLESS_SCROLLING => InputHelper::get('endless-scrolling'),
+ UserSettings::SETTING_POST_TAG_TITLES => InputHelper::get('post-tag-titles'),
+ UserSettings::SETTING_HIDE_DISLIKED_POSTS => InputHelper::get('hide-disliked-posts'),
+ ]
+ ], $identifier));
- $context->suppliedCurrentPassword = $suppliedCurrentPassword = InputHelper::get('current-password');
- $context->suppliedName = $suppliedName = InputHelper::get('name');
- $context->suppliedPassword1 = $suppliedPassword1 = InputHelper::get('password1');
- $context->suppliedPassword2 = $suppliedPassword2 = InputHelper::get('password2');
- $context->suppliedEmail = $suppliedEmail = InputHelper::get('email');
- $context->suppliedAccessRank = $suppliedAccessRank = InputHelper::get('access-rank');
- $currentPasswordHash = $user->passHash;
-
- if (!InputHelper::get('submit'))
- return;
-
- $confirmMail = false;
- LogHelper::bufferChanges();
-
- if ($suppliedName != '' and $suppliedName != $user->name)
- {
- Access::assert(
- Privilege::ChangeUserName,
- Access::getIdentity($user));
-
- $suppliedName = UserModel::validateUserName($suppliedName);
- $oldName = $user->name;
- $user->name = $suppliedName;
- LogHelper::log('{user} renamed {old} to {new}', [
- 'old' => TextHelper::reprUser($oldName),
- 'new' => TextHelper::reprUser($suppliedName)]);
- }
-
- if ($suppliedPassword1 != '')
- {
- Access::assert(
- Privilege::ChangeUserPassword,
- Access::getIdentity($user));
-
- if ($suppliedPassword1 != $suppliedPassword2)
- throw new SimpleException('Specified passwords must be the same');
- $suppliedPassword = UserModel::validatePassword($suppliedPassword1);
- $user->passHash = UserModel::hashPassword($suppliedPassword, $user->passSalt);
- LogHelper::log('{user} changed {subject}\'s password', ['subject' => TextHelper::reprUser($user)]);
- }
-
- if ($suppliedEmail != '' and $suppliedEmail != $user->emailConfirmed)
- {
- Access::assert(
- Privilege::ChangeUserEmail,
- Access::getIdentity($user));
-
- $suppliedEmail = UserModel::validateEmail($suppliedEmail);
- if (Auth::getCurrentUser()->id == $user->id)
- {
- $user->emailUnconfirmed = $suppliedEmail;
- if (!empty($user->emailUnconfirmed))
- $confirmMail = true;
- LogHelper::log('{user} changed e-mail to {mail}', ['mail' => $suppliedEmail]);
- }
- else
- {
- $user->emailUnconfirmed = null;
- $user->emailConfirmed = $suppliedEmail;
- LogHelper::log('{user} changed {subject}\'s e-mail to {mail}', [
- 'subject' => TextHelper::reprUser($user),
- 'mail' => $suppliedEmail]);
- }
- }
-
- if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->accessRank)
- {
- Access::assert(
- Privilege::ChangeUserAccessRank,
- Access::getIdentity($user));
-
- $suppliedAccessRank = UserModel::validateAccessRank($suppliedAccessRank);
- $user->accessRank = $suppliedAccessRank;
- LogHelper::log('{user} changed {subject}\'s access rank to {rank}', [
- 'subject' => TextHelper::reprUser($user),
- 'rank' => AccessRank::toString($suppliedAccessRank)]);
- }
-
- if (Auth::getCurrentUser()->id == $user->id)
- {
- $suppliedPasswordHash = UserModel::hashPassword($suppliedCurrentPassword, $user->passSalt);
- if ($suppliedPasswordHash != $currentPasswordHash)
- throw new SimpleException('Must supply valid current password');
- }
- UserModel::save($user);
- if (Auth::getCurrentUser()->id == $user->id)
+ Core::getContext()->transport->user = $user;
+ if ($user->getId() == Auth::getCurrentUser()->getId())
Auth::setCurrentUser($user);
- if ($confirmMail)
- self::sendEmailChangeConfirmation($user);
-
- LogHelper::flush();
- $message = 'Account settings updated!';
- if ($confirmMail)
- $message .= ' You will be sent an e-mail address confirmation message soon.';
- Messenger::message($message);
+ Messenger::success('Browsing settings updated!');
}
- catch (Exception $e)
+ catch (SimpleException $e)
{
- $context->transport->user = UserModel::findByNameOrEmail($name);
- throw $e;
+ Messenger::fail($e->getMessage());
}
+
+ $this->renderView('user-view');
}
- public function viewAction($name, $tab = 'favs', $page)
+ public function editAction($identifier)
{
- $context = getContext();
- $postsPerPage = intval(getConfig()->browsing->postsPerPage);
- $user = UserModel::findByNameOrEmail($name);
- if ($tab === null)
- $tab = 'favs';
- if ($page === null)
- $page = 1;
+ $this->prepareGenericView($identifier, 'edit');
- Access::assert(
- Privilege::ViewUser,
- Access::getIdentity($user));
+ try
+ {
+ $this->requirePasswordConfirmation();
- $this->loadUserView($user);
+ if (InputHelper::get('password1') != InputHelper::get('password2'))
+ throw new SimpleException('Specified passwords must be the same');
- $query = '';
- if ($tab == 'uploads')
- $query = 'submit:' . $user->name;
- elseif ($tab == 'favs')
- $query = 'fav:' . $user->name;
- else
- throw new SimpleException('Wrong tab');
+ $args =
+ [
+ JobArgs::ARG_NEW_USER_NAME => InputHelper::get('name'),
+ JobArgs::ARG_NEW_PASSWORD => InputHelper::get('password1'),
+ JobArgs::ARG_NEW_EMAIL => InputHelper::get('email'),
+ JobArgs::ARG_NEW_ACCESS_RANK => InputHelper::get('access-rank'),
+ ];
+ $args = $this->appendUserIdentifierArgument($args, $identifier);
- $page = max(1, $page);
- $posts = PostSearchService::getEntities($query, $postsPerPage, $page);
- $postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page);
- $pageCount = ceil($postCount / $postsPerPage);
- PostModel::preloadTags($posts);
+ $args = array_filter($args);
+ $user = Api::run(new EditUserJob(), $args);
- $context->transport->tab = $tab;
- $context->transport->lastSearchQuery = $query;
- $context->transport->paginator = new StdClass;
- $context->transport->paginator->page = $page;
- $context->transport->paginator->pageCount = $pageCount;
- $context->transport->paginator->entityCount = $postCount;
- $context->transport->paginator->entities = $posts;
- $context->transport->posts = $posts;
+ if (Auth::getCurrentUser()->getId() == $user->getId())
+ Auth::setCurrentUser($user);
+
+ $message = 'Account settings updated!';
+ if (Mailer::getMailCounter() > 0)
+ $message .= ' You will be sent an e-mail address confirmation message soon.';
+
+ Messenger::success($message);
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
+ }
+
+ $this->renderView('user-view');
+ }
+
+ public function deleteAction($identifier)
+ {
+ $this->prepareGenericView($identifier, 'delete');
+
+ try
+ {
+ $this->requirePasswordConfirmation();
+
+ Api::run(
+ new DeleteUserJob(),
+ $this->appendUserIdentifierArgument([], $identifier));
+
+ $user = UserModel::tryGetById(Auth::getCurrentUser()->getId());
+ if (!$user)
+ Auth::logOut();
+
+ $this->redirectToMainPage();
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
+ $this->renderView('user-view');
+ }
}
public function toggleSafetyAction($safety)
{
+ $safety = new PostSafety($safety);
+ $safety->validate();
+
$user = Auth::getCurrentUser();
+ $user->getSettings()->enableSafety($safety, !$user->getSettings()->hasEnabledSafety($safety));
+ $desiredSafety = $user->getSettings()->get(UserSettings::SETTING_SAFETY);
- Access::assert(
- Privilege::ChangeUserSettings,
- Access::getIdentity($user));
+ $user = Api::run(
+ new EditUserSettingsJob(),
+ [
+ JobArgs::ARG_USER_ENTITY => Auth::getCurrentUser(),
+ JobArgs::ARG_NEW_SETTINGS => [UserSettings::SETTING_SAFETY => $desiredSafety],
+ ]);
- if (!in_array($safety, PostSafety::getAll()))
- throw new SimpleExcetpion('Invalid safety');
-
- $user->enableSafety($safety, !$user->hasEnabledSafety($safety));
-
- if ($user->accessRank != AccessRank::Anonymous)
- UserModel::save($user);
Auth::setCurrentUser($user);
+ $this->redirectToLastVisitedUrl();
+ }
+
+ public function flagAction($identifier)
+ {
+ Api::run(
+ new FlagUserJob(),
+ $this->appendUserIdentifierArgument([], $identifier));
+ $this->redirectToGenericView($identifier);
+ }
+
+ public function banAction($identifier)
+ {
+ Api::run(
+ new ToggleUserBanJob(),
+ $this->appendUserIdentifierArgument([
+ JobArgs::ARG_NEW_STATE => true
+ ], $identifier));
+ $this->redirectToGenericView($identifier);
+ }
+
+ public function unbanAction($identifier)
+ {
+ Api::run(
+ new ToggleUserBanJob(),
+ $this->appendUserIdentifierArgument([
+ JobArgs::ARG_NEW_STATE => true
+ ], $identifier));
+ $this->redirectToGenericView($identifier);
+ }
+
+ public function acceptRegistrationAction($identifier)
+ {
+ Api::run(
+ new AcceptUserRegistrationJob(),
+ $this->appendUserIdentifierArgument([], $identifier));
+ $this->redirectToGenericView($identifier);
+ }
+
+ public function registrationView()
+ {
+ if (Auth::isLoggedIn())
+ $this->redirectToMainPage();
+ $this->renderView('user-registration');
}
public function registrationAction()
{
- $context = getContext();
- $context->handleExceptions = true;
-
- //check if already logged in
- if (Auth::isLoggedIn())
+ try
{
- \Chibi\Util\Url::forward(\Chibi\Router::linkTo(['IndexController', 'indexAction']));
- exit;
+ if (InputHelper::get('password1') != InputHelper::get('password2'))
+ throw new SimpleException('Specified passwords must be the same');
+
+ $user = Api::run(new AddUserJob(),
+ [
+ JobArgs::ARG_NEW_USER_NAME => InputHelper::get('name'),
+ JobArgs::ARG_NEW_PASSWORD => InputHelper::get('password1'),
+ JobArgs::ARG_NEW_EMAIL => InputHelper::get('email'),
+ ]);
+
+ if (!$this->isAnyAccountActivationNeeded())
+ {
+ Auth::setCurrentUser($user);
+ }
+
+ $message = 'Congratulations, your account was created.';
+ if (Mailer::getMailCounter() > 0)
+ {
+ $message .= ' Please wait for activation e-mail.';
+ if (Core::getConfig()->registration->staffActivation)
+ $message .= ' After this, your registration must be confirmed by staff.';
+ }
+ elseif (Core::getConfig()->registration->staffActivation)
+ $message .= ' Your registration must be now confirmed by staff.';
+
+ Messenger::success($message);
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
}
- $suppliedName = InputHelper::get('name');
- $suppliedPassword1 = InputHelper::get('password1');
- $suppliedPassword2 = InputHelper::get('password2');
- $suppliedEmail = InputHelper::get('email');
- $context->suppliedName = $suppliedName;
- $context->suppliedPassword1 = $suppliedPassword1;
- $context->suppliedPassword2 = $suppliedPassword2;
- $context->suppliedEmail = $suppliedEmail;
+ $this->renderView('user-registration');
+ }
- if (!InputHelper::get('submit'))
- return;
+ public function activationView()
+ {
+ $this->assets->setSubTitle('account activation');
+ $this->renderView('user-select');
+ }
- $suppliedName = UserModel::validateUserName($suppliedName);
+ public function activationAction($tokenText)
+ {
+ $this->assets->setSubTitle('account activation');
+ $identifier = InputHelper::get('identifier');
- if ($suppliedPassword1 != $suppliedPassword2)
- throw new SimpleException('Specified passwords must be the same');
- $suppliedPassword = UserModel::validatePassword($suppliedPassword1);
-
- $suppliedEmail = UserModel::validateEmail($suppliedEmail);
- if (empty($suppliedEmail) and getConfig()->registration->needEmailForRegistering)
- throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.');
-
- //register the user
- $dbUser = UserModel::spawn();
- $dbUser->name = $suppliedName;
- $dbUser->passHash = UserModel::hashPassword($suppliedPassword, $dbUser->passSalt);
- $dbUser->emailUnconfirmed = $suppliedEmail;
-
- $dbUser->joinDate = time();
- if (UserModel::getCount() == 0)
+ try
{
- //very first user
- $dbUser->accessRank = AccessRank::Admin;
- $dbUser->staffConfirmed = true;
- $dbUser->emailUnconfirmed = null;
- $dbUser->emailConfirmed = $suppliedEmail;
+ if (empty($tokenText))
+ {
+ Api::run(
+ new ActivateUserEmailJob(),
+ $this->appendUserIdentifierArgument([], $identifier));
+
+ Messenger::success('Activation e-mail resent.');
}
else
{
- $dbUser->accessRank = AccessRank::Registered;
- $dbUser->staffConfirmed = false;
- $dbUser->staffConfirmed = null;
+ $user = Api::run(new ActivateUserEmailJob(), [
+ JobArgs::ARG_TOKEN => $tokenText ]);
+
+ $message = 'Activation completed successfully.';
+ if (Core::getConfig()->registration->staffActivation)
+ $message .= ' However, your account still must be confirmed by staff.';
+ Messenger::success($message);
+
+ if (!Core::getConfig()->registration->staffActivation)
+ Auth::setCurrentUser($user);
+ }
+ }
+ catch (SimpleException $e)
+ {
+ Messenger::fail($e->getMessage());
}
- //save the user to db if everything went okay
- UserModel::save($dbUser);
-
- if (!empty($dbUser->emailUnconfirmed))
- self::sendEmailChangeConfirmation($dbUser);
-
- $message = 'Congratulations, your account was created.';
- if (!empty($context->mailSent))
- {
- $message .= ' Please wait for activation e-mail.';
- if (getConfig()->registration->staffActivation)
- $message .= ' After this, your registration must be confirmed by staff.';
- }
- elseif (getConfig()->registration->staffActivation)
- $message .= ' Your registration must be now confirmed by staff.';
-
- LogHelper::log('{subject} just signed up', ['subject' => TextHelper::reprUser($dbUser)]);
- Messenger::message($message);
-
- if (!getConfig()->registration->needEmailForRegistering and !getConfig()->registration->staffActivation)
- {
- Auth::setCurrentUser($dbUser);
- }
+ $this->renderView('message');
}
- public function activationAction($token)
+ public function passwordResetView()
{
- $context = getContext();
- $context->viewName = 'message';
- Assets::setSubTitle('account activation');
-
- $dbToken = TokenModel::findByToken($token);
- TokenModel::checkValidity($dbToken);
-
- $dbUser = $dbToken->getUser();
- $dbUser->emailConfirmed = $dbUser->emailUnconfirmed;
- $dbUser->emailUnconfirmed = null;
- $dbToken->used = true;
- TokenModel::save($dbToken);
- UserModel::save($dbUser);
-
- LogHelper::log('{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]);
- $message = 'Activation completed successfully.';
- if (getConfig()->registration->staffActivation)
- $message .= ' However, your account still must be confirmed by staff.';
- Messenger::message($message);
-
- if (!getConfig()->registration->staffActivation)
- {
- Auth::setCurrentUser($dbUser);
- }
+ $this->assets->setSubTitle('password reset');
+ $this->renderView('user-select');
}
- public function passwordResetAction($token)
+ public function passwordResetAction($tokenText)
{
- $context = getContext();
- $context->viewName = 'message';
- Assets::setSubTitle('password reset');
+ $this->assets->setSubTitle('password reset');
+ $identifier = InputHelper::get('identifier');
- $dbToken = TokenModel::findByToken($token);
- TokenModel::checkValidity($dbToken);
-
- $alphabet = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
- $randomPassword = join('', array_map(function($x) use ($alphabet)
+ try
{
- return $alphabet[$x];
- }, array_rand($alphabet, 8)));
+ if (empty($tokenText))
+ {
+ Api::run(
+ new PasswordResetJob(),
+ $this->appendUserIdentifierArgument([], $identifier));
- $dbUser = $dbToken->getUser();
- $dbUser->passHash = UserModel::hashPassword($randomPassword, $dbUser->passSalt);
- $dbToken->used = true;
- TokenModel::save($dbToken);
- UserModel::save($dbUser);
-
- LogHelper::log('{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]);
- $message = 'Password reset successful. Your new password is **' . $randomPassword . '**.';
- Messenger::message($message);
-
- Auth::setCurrentUser($dbUser);
- }
-
- public function passwordResetProxyAction()
- {
- $context = getContext();
- $context->viewName = 'user-select';
- Assets::setSubTitle('password reset');
-
- if (!InputHelper::get('submit'))
- return;
-
- $name = InputHelper::get('name');
- $user = UserModel::findByNameOrEmail($name);
- if (empty($user->emailConfirmed))
- throw new SimpleException('This user has no e-mail confirmed; password reset cannot proceed');
-
- self::sendPasswordResetConfirmation($user);
- Messenger::message('E-mail sent. Follow instructions to reset password.');
- }
-
- public function activationProxyAction()
- {
- $context = getContext();
- $context->viewName = 'user-select';
- Assets::setSubTitle('account activation');
-
- if (!InputHelper::get('submit'))
- return;
-
- $name = InputHelper::get('name');
- $user = UserModel::findByNameOrEmail($name);
- if (empty($user->emailUnconfirmed))
- {
- if (!empty($user->emailConfirmed))
- throw new SimpleException('E-mail was already confirmed; activation skipped');
+ Messenger::success('E-mail sent. Follow instructions to reset password.');
+ }
else
- throw new SimpleException('This user has no e-mail specified; activation cannot proceed');
+ {
+ $ret = Api::run(new PasswordResetJob(), [ JobArgs::ARG_TOKEN => $tokenText ]);
+
+ Messenger::success(sprintf(
+ 'Password reset successful. Your new password is **%s**.',
+ $ret->newPassword));
+
+ Auth::setCurrentUser($ret->user);
+ }
}
- self::sendEmailChangeConfirmation($user);
- Messenger::message('Activation e-mail resent.');
- }
-
- private function loadUserView($user)
- {
- $context = getContext();
- $flagged = in_array(TextHelper::reprUser($user), SessionHelper::get('flagged', []));
- $context->flagged = $flagged;
- $context->transport->user = $user;
- $context->handleExceptions = true;
- $context->viewName = 'user-view';
- }
-
- private static function sendTokenizedEmail(
- $user,
- $body,
- $subject,
- $senderName,
- $senderEmail,
- $recipientEmail,
- $linkActionName)
- {
- //prepare unique user token
- $token = TokenModel::spawn();
- $token->setUser($user);
- $token->token = TokenModel::forgeUnusedToken();
- $token->used = false;
- $token->expires = null;
- TokenModel::save($token);
-
- getContext()->mailSent = true;
- $tokens = [];
- $tokens['host'] = $_SERVER['HTTP_HOST'];
- $tokens['token'] = $token->token; //gosh this code looks so silly
- $tokens['nl'] = PHP_EOL;
- if ($linkActionName !== null)
- $tokens['link'] = \Chibi\Router::linkTo(['UserController', $linkActionName], ['token' => $token->token]);
-
- $body = wordwrap(TextHelper::replaceTokens($body, $tokens), 70);
- $subject = TextHelper::replaceTokens($subject, $tokens);
- $senderName = TextHelper::replaceTokens($senderName, $tokens);
- $senderEmail = TextHelper::replaceTokens($senderEmail, $tokens);
-
- if (empty($recipientEmail))
- throw new SimpleException('Destination e-mail address was not found');
-
- $messageId = $_SERVER['REQUEST_TIME'] . md5($_SERVER['REQUEST_TIME']) . '@' . $_SERVER['HTTP_HOST'];
-
- $headers = [];
- $headers []= sprintf('MIME-Version: 1.0');
- $headers []= sprintf('Content-Transfer-Encoding: 7bit');
- $headers []= sprintf('Date: %s', date('r', $_SERVER['REQUEST_TIME']));
- $headers []= sprintf('Message-ID: <%s>', $messageId);
- $headers []= sprintf('From: %s <%s>', $senderName, $senderEmail);
- $headers []= sprintf('Reply-To: %s', $senderEmail);
- $headers []= sprintf('Return-Path: %s', $senderEmail);
- $headers []= sprintf('Subject: %s', $subject);
- $headers []= sprintf('Content-Type: text/plain; charset=utf-8', $subject);
- $headers []= sprintf('X-Mailer: PHP/%s', phpversion());
- $headers []= sprintf('X-Originating-IP: %s', $_SERVER['SERVER_ADDR']);
- $encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
- mail($recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), '-f' . $senderEmail);
-
- LogHelper::log('Sending e-mail with subject "{subject}" to {mail}', [
- 'subject' => $subject,
- 'mail' => $recipientEmail]);
- }
-
- private static function sendEmailChangeConfirmation($user)
- {
- $regConfig = getConfig()->registration;
- if (!$regConfig->confirmationEmailEnabled)
+ catch (SimpleException $e)
{
- $user->emailConfirmed = $user->emailUnconfirmed;
- $user->emailUnconfirmed = null;
- return;
+ Messenger::fail($e->getMessage());
}
- return self::sendTokenizedEmail(
- $user,
- $regConfig->confirmationEmailBody,
- $regConfig->confirmationEmailSubject,
- $regConfig->confirmationEmailSenderName,
- $regConfig->confirmationEmailSenderEmail,
- $user->emailUnconfirmed,
- 'activationAction');
+ $this->renderView('message');
}
- private static function sendPasswordResetConfirmation($user)
- {
- $regConfig = getConfig()->registration;
- return self::sendTokenizedEmail(
- $user,
- $regConfig->passwordResetEmailBody,
- $regConfig->passwordResetEmailSubject,
- $regConfig->passwordResetEmailSenderName,
- $regConfig->passwordResetEmailSenderEmail,
- $user->emailConfirmed,
- 'passwordResetAction');
+ private function prepareGenericView($identifier, $tab, $page = 1)
+ {
+ $user = Api::run(
+ new GetUserJob(),
+ $this->appendUserIdentifierArgument([], $identifier));
+
+ $flagged = in_array(TextHelper::reprUser($user), SessionHelper::get('flagged', []));
+
+ if ($tab == 'uploads')
+ $query = 'submit:' . $user->getName();
+ elseif ($tab == 'favs')
+ $query = 'fav:' . $user->getName();
+
+ elseif ($tab == 'delete')
+ {
+ Access::assert(new Privilege(
+ Privilege::DeleteUser,
+ Access::getIdentity($user)));
+ }
+ elseif ($tab == 'settings')
+ {
+ Access::assert(new Privilege(
+ Privilege::EditUserSettings,
+ Access::getIdentity($user)));
+ }
+ elseif ($tab == 'edit' and !(new EditUserJob)->canEditAnything(Auth::getCurrentUser()))
+ Access::fail();
+
+ $context = Core::getContext();
+ $context->flagged = $flagged;
+ $context->transport->tab = $tab;
+ $context->transport->user = $user;
+
+ if (isset($query))
+ {
+ $ret = Api::run(
+ new ListPostsJob(),
+ [
+ JobArgs::ARG_PAGE_NUMBER => $page,
+ JobArgs::ARG_QUERY => $query
+ ]);
+
+ $context->transport->posts = $ret->entities;
+ $context->transport->paginator = $ret;
+ $context->transport->lastSearchQuery = $query;
+ }
+ }
+
+
+ private function isAnyAccountActivationNeeded()
+ {
+ $config = Core::getConfig();
+ return ($config->registration->needEmailForRegistering
+ or $config->registration->staffActivation);
+ }
+
+ private function requirePasswordConfirmation()
+ {
+ $user = Core::getContext()->transport->user;
+ if (Auth::getCurrentUser()->getId() == $user->getId())
+ {
+ $suppliedPassword = InputHelper::get('current-password');
+ $suppliedPasswordHash = UserModel::hashPassword($suppliedPassword, $user->getPasswordSalt());
+ if ($suppliedPasswordHash != $user->getPasswordHash())
+ throw new SimpleException('Must supply valid password');
+ }
+ }
+
+ private function appendUserIdentifierArgument(array $arguments, $userIdentifier)
+ {
+ if (strpos($userIdentifier, '@') !== false)
+ $arguments[JobArgs::ARG_USER_EMAIL] = $userIdentifier;
+ else
+ $arguments[JobArgs::ARG_USER_NAME] = $userIdentifier;
+ return $arguments;
+ }
+
+ private function redirectToMainPage()
+ {
+ $this->redirect(\Chibi\Router::linkTo(['StaticPagesController', 'mainPageView']));
+ exit;
+ }
+
+ private function redirectToGenericView($identifier)
+ {
+ $this->redirect(\Chibi\Router::linkTo(
+ ['UserController', 'genericView'],
+ ['identifier' => $identifier]));
}
}
diff --git a/src/CustomMarkdown.php b/src/CustomMarkdown.php
index 2cc80f6c..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";
@@ -127,7 +127,7 @@ class CustomMarkdown extends \Michelf\MarkdownExtra
protected function doPosts($text)
{
- $link = \Chibi\Router::linkTo(['PostController', 'viewAction'], ['id' => '_post_']);
+ $link = \Chibi\Router::linkTo(['PostController', 'genericView'], ['id' => '_post_']);
return preg_replace_callback('/(?:(?hashPart('' . $x[0] . '
');
@@ -136,7 +136,7 @@ class CustomMarkdown extends \Michelf\MarkdownExtra
protected function doTags($text)
{
- $link = \Chibi\Router::linkTo(['PostController', 'listAction'], ['query' => '_query_']);
+ $link = \Chibi\Router::linkTo(['PostController', 'listView'], ['query' => '_query_']);
return preg_replace_callback('/(?:(?hashPart('' . $x[0] . '');
@@ -145,7 +145,7 @@ class CustomMarkdown extends \Michelf\MarkdownExtra
protected function doUsers($text)
{
- $link = \Chibi\Router::linkTo(['UserController', 'viewAction'], ['name' => '_name_']);
+ $link = \Chibi\Router::linkTo(['UserController', 'genericView'], ['name' => '_name_']);
return preg_replace_callback('/(?:(?hashPart('' . $x[0] . '');
@@ -154,7 +154,7 @@ class CustomMarkdown extends \Michelf\MarkdownExtra
protected function doSearchPermalinks($text)
{
- $link = \Chibi\Router::linkTo(['PostController', 'listAction'], ['query' => '_query_']);
+ $link = \Chibi\Router::linkTo(['PostController', 'listView'], ['query' => '_query_']);
return preg_replace_callback('{\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]}is', function($x) use ($link)
{
return $this->hashPart('' . $x[1] . '');
diff --git a/src/Dispatcher.php b/src/Dispatcher.php
new file mode 100644
index 00000000..f557a2d9
--- /dev/null
+++ b/src/Dispatcher.php
@@ -0,0 +1,77 @@
+retrieveQuery();
+
+ $context = Core::getContext();
+ $context->query = $query;
+ $context->transport = new StdClass;
+
+ $this->setRouterObserver();
+ $this->ensureResponseCodeUponFail();
+
+ SessionHelper::init();
+ if (!Auth::isLoggedIn())
+ Auth::tryAutoLogin();
+
+ $this->routeAndHandleErrors($query);
+ }
+
+ private function routeAndHandleErrors($query)
+ {
+ try
+ {
+ \Chibi\Router::run($query);
+ }
+ catch (\Chibi\UnhandledRouteException $e)
+ {
+ $errorController = new ErrorController;
+ $errorController->simpleExceptionView(new SimpleNotFoundException($query . ' not found.'));
+ }
+ catch (SimpleException $e)
+ {
+ $errorController = new ErrorController;
+ $errorController->simpleExceptionView($e);
+ }
+ catch (SimpleException $e)
+ {
+ $errorController = new ErrorController;
+ $errorController->seriousExceptionView($e);
+ }
+ }
+
+ private function ensureResponseCodeUponFail()
+ {
+ register_shutdown_function(function()
+ {
+ $error = error_get_last();
+ if ($error !== null)
+ \Chibi\Util\Headers::setCode(400);
+ });
+ }
+
+ private function retrieveQuery()
+ {
+ if (isset($_SERVER['REDIRECT_URL']))
+ return $this->parseRawHttpQuery($_SERVER['REDIRECT_URL']);
+ else
+ return $this->parseRawHttpQuery($_SERVER['REQUEST_URI']);
+ }
+
+ private function parseRawHttpQuery($rawHttpQuery)
+ {
+ return rtrim($rawHttpQuery, '/');
+ }
+
+ private function setRouterObserver()
+ {
+ \Chibi\Router::setObserver(function($route, $args)
+ {
+ $context = Core::getContext();
+ $context->route = $route;
+ });
+ }
+}
+
diff --git a/src/Enum.php b/src/Enum.php
deleted file mode 100644
index 6987502c..00000000
--- a/src/Enum.php
+++ /dev/null
@@ -1,24 +0,0 @@
-getConstants();
- return array_search($constant, $constants);
- }
-
- public static function toDisplayString($constant)
- {
- return TextCaseConverter::convert(static::toString($constant),
- TextCaseConverter::SNAKE_CASE,
- TextCaseConverter::BLANK_CASE);
- }
-
- public static function getAll()
- {
- $cls = new ReflectionClass(get_called_class());
- $constants = $cls->getConstants();
- return array_values($constants);
- }
-}
diff --git a/src/Enums/AbstractEnum.php b/src/Enums/AbstractEnum.php
new file mode 100644
index 00000000..907f5fb6
--- /dev/null
+++ b/src/Enums/AbstractEnum.php
@@ -0,0 +1,19 @@
+toString(),
+ TextCaseConverter::SPINAL_CASE,
+ TextCaseConverter::BLANK_CASE);
+ }
+
+ public static function getAllConstants()
+ {
+ $cls = new ReflectionClass(get_called_class());
+ $constants = $cls->getConstants();
+ return array_values($constants);
+ }
+}
diff --git a/src/Enums/AccessRank.php b/src/Enums/AccessRank.php
new file mode 100644
index 00000000..42ce194f
--- /dev/null
+++ b/src/Enums/AccessRank.php
@@ -0,0 +1,50 @@
+accessRank = $accessRank;
+ }
+
+ public function toInteger()
+ {
+ return $this->accessRank;
+ }
+
+ public function toString()
+ {
+ switch ($this->accessRank)
+ {
+ case self::Anonymous: return 'anonymous';
+ case self::Registered: return 'registered';
+ case self::PowerUser: return 'power-user';
+ case self::Moderator: return 'moderator';
+ case self::Admin: return 'admin';
+ case self::Nobody: return 'nobody';
+ }
+ return null;
+ }
+
+ public static function getAll()
+ {
+ return array_map(function($constantName)
+ {
+ return new self($constantName);
+ }, self::getAllConstants());
+ }
+
+ public function validate()
+ {
+ if (!in_array($this->accessRank, self::getAllConstants()))
+ throw new SimpleException('Invalid access rank "%s"', $this->accessRank);
+ }
+}
diff --git a/src/Enums/IEnum.php b/src/Enums/IEnum.php
new file mode 100644
index 00000000..2e0cecd8
--- /dev/null
+++ b/src/Enums/IEnum.php
@@ -0,0 +1,6 @@
+safety = $safety;
+ }
+
+ public function toInteger()
+ {
+ return $this->safety;
+ }
+
+ public function toFlag()
+ {
+ return pow(2, $this->safety - 1);
+ }
+
+ public function toString()
+ {
+ switch ($this->safety)
+ {
+ case self::Safe: return 'safe';
+ case self::Sketchy: return 'sketchy';
+ case self::Unsafe: return 'unsafe';
+ }
+ return null;
+ }
+
+ public static function makeFlags($safetyCodes)
+ {
+ if (!is_array($safetyCodes))
+ return 0;
+
+ $flags = 0;
+ foreach (self::getAll() as $safety)
+ if (in_array($safety->toInteger(), $safetyCodes))
+ $flags |= $safety->toFlag();
+ return $flags;
+ }
+
+ public static function getAll()
+ {
+ return array_map(function($constantName)
+ {
+ return new self($constantName);
+ }, self::getAllConstants());
+ }
+
+ public function validate()
+ {
+ if (!in_array($this->safety, self::getAllConstants()))
+ throw new SimpleException('Invalid safety type "%s"', $this->safety);
+ }
+}
diff --git a/src/Enums/PostType.php b/src/Enums/PostType.php
new file mode 100644
index 00000000..8ff6cd28
--- /dev/null
+++ b/src/Enums/PostType.php
@@ -0,0 +1,38 @@
+type = $type;
+ }
+
+ public function toInteger()
+ {
+ return $this->type;
+ }
+
+ public function toString()
+ {
+ switch ($this->type)
+ {
+ case self::Image: return 'image';
+ case self::Flash: return 'flash';
+ case self::Youtube: return 'youtube';
+ case self::Video: return 'video';
+ }
+ return null;
+ }
+
+ public function validate()
+ {
+ if (!in_array($this->type, self::getAllConstants()))
+ throw new SimpleException('Invalid post type "%s"', $this->type);
+ }
+}
diff --git a/src/Enums/Privilege.php b/src/Enums/Privilege.php
new file mode 100644
index 00000000..478ba8fd
--- /dev/null
+++ b/src/Enums/Privilege.php
@@ -0,0 +1,79 @@
+primary = $primary;
+ $this->secondary = strtolower($secondary);
+ }
+
+ public function toString()
+ {
+ $string = $this->primary;
+ if ($this->secondary)
+ $string .= '.' . $this->secondary;
+ return $string;
+ }
+
+ public function toDisplayString()
+ {
+ return $this->toString();
+ }
+}
diff --git a/src/Helpers/Assets.php b/src/Helpers/Assets.php
index 50d555ce..678de702 100644
--- a/src/Helpers/Assets.php
+++ b/src/Helpers/Assets.php
@@ -1,47 +1,41 @@
subTitle = $text;
}
- public static function setSubTitle($text)
+ public function setPageThumb($path)
{
- self::$subTitle = $text;
+ $this->pageThumb = $path;
}
- public static function setPageThumb($path)
- {
- self::$pageThumb = $path;
- }
-
- public static function addStylesheet($path)
+ public function addStylesheet($path)
{
return parent::addStylesheet('/media/css/' . $path);
}
- public static function addScript($path)
+ public function addScript($path)
{
return parent::addScript('/media/js/' . $path);
}
- public static function transformHtml($html)
+ public function transformHtml($html)
{
- self::$title = isset(self::$subTitle)
- ? sprintf('%s – %s', self::$title, self::$subTitle)
- : self::$title;
+ $this->title = isset($this->subTitle)
+ ? sprintf('%s – %s', $this->title, $this->subTitle)
+ : $this->title;
$html = parent::transformHtml($html);
- $headSnippet = '';
+ $headSnippet = '';
$headSnippet .= '';
- if (!empty(self::$pageThumb))
- $headSnippet .= '';
+ if (!empty($this->pageThumb))
+ $headSnippet .= '';
$bodySnippet = '