Merge branch 'api'

This commit is contained in:
Marcin Kurczewski 2014-05-18 22:52:04 +02:00
commit 021514aabd
281 changed files with 15747 additions and 3530 deletions

View file

@ -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

View file

@ -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` |

View file

@ -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.

View file

@ -1,6 +1,6 @@
<?php
require_once 'src/core.php';
$config = getConfig();
$config = Core::getConfig();
$fontsPath = TextHelper::absolutePath($config->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

@ -1 +1 @@
Subproject commit 45c662d0a4b32e09399b5b68ac53aaa3f1a29911
Subproject commit a6c610e5c68220cf672debe765752662807c0d39

@ -1 +1 @@
Subproject commit 22910a186efbcb9bc86a3ae3eb6f4aff34096406
Subproject commit 6bb18c1c6ed7ea952ae7a8dab792d5364a334201

View file

@ -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

View file

@ -1,211 +1,5 @@
<?php
$startTime = microtime(true);
require_once '../src/core.php';
$query = rtrim($_SERVER['REQUEST_URI'], '/');
//prepare context
$context = new StdClass;
$context->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();

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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();

View file

@ -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,8 +191,13 @@ function split(val)
function retrieveTags(searchTerm, cb)
{
var options = { search: searchTerm };
$.getJSON('/tags-autocomplete?json', options, function(data)
var options =
{
url: '/tags-autocomplete',
data: { search: searchTerm }
};
getJSON(options)
.success(function(data)
{
var tags = $.map(data.tags.slice(0, 15), function(tag)
{
@ -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 = $('<ul>');
$.each(data.tags, function(i, tag)

View file

@ -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');

View file

@ -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');

View file

@ -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);

View file

@ -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()

View file

@ -1,26 +1,19 @@
<?php
require_once __DIR__ . '/../src/core.php';
function usage()
{
echo 'Usage: ' . basename(__FILE__);
echo ' QUERY' . PHP_EOL;
return true;
}
Access::disablePrivilegeChecking();
array_shift($argv);
if (empty($argv))
usage() and die;
$query = array_shift($argv);
$posts = Model_Post::getEntities($query, null, null);
$posts = PostSearchService::getEntities($query, null, null);
foreach ($posts as $post)
{
echo implode("\t",
[
$post->id,
$post->name,
Model_Post::getFullPath($post->name),
$post->mimeType,
$post->getId(),
$post->getName(),
$post->tryGetWorkingFullPath(),
$post->getMimeType(),
]). PHP_EOL;
}

View file

@ -1,6 +1,8 @@
<?php
require_once __DIR__ . '/../src/core.php';
Access::disablePrivilegeChecking();
function usage()
{
echo 'Usage: ' . basename(__FILE__);
@ -33,7 +35,7 @@ switch ($action)
$func = function($name) use ($dir)
{
echo $name . PHP_EOL;
$srcPath = Model_Post::getFullPath($name);
$srcPath = PostModel::getFullPath($name);
$dstPath = $dir . DS . $name;
rename($srcPath, $dstPath);
};
@ -43,7 +45,7 @@ switch ($action)
$func = function($name)
{
echo $name . PHP_EOL;
$srcPath = Model_Post::getFullPath($name);
$srcPath = PostModel::getFullPath($name);
unlink($srcPath);
};
break;
@ -53,13 +55,13 @@ switch ($action)
}
$names = [];
foreach (R::findAll('post') as $post)
foreach (PostSearchService::getEntities(null, null, null) as $post)
{
$names []= $post->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);

View file

@ -1,6 +1,8 @@
<?php
require_once __DIR__ . '/../src/core.php';
Access::disablePrivilegeChecking();
function usage()
{
echo 'Usage: ' . basename(__FILE__);
@ -14,10 +16,10 @@ if (empty($argv))
function printUser($user)
{
echo 'ID: ' . $user->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)
{
if (!$user->getConfirmedEmail()
and !$user->getLastLoginTime()
and ((time() - $user->getJoinTime()) > 21 * 24 * 60 * 60))
{
$func($user);
}
}

View file

@ -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;
if ($user === null)
$user = Auth::getCurrentUser();
$minAccessRank = AccessRank::Admin;
$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];
elseif (isset(self::$privileges[$privilege->primary][null]))
$minAccessRank = self::$privileges[$privilege->primary][null];
return $user->getAccessRank()->toInteger() >= $minAccessRank->toInteger();
}
return intval($user->accessRank) >= $minAccessRank;
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);
});
}
public static function getAllDefinedSubPrivileges($privilege)
{
if (!isset(self::$privileges[$privilege]))
return null;
return self::$privileges[$privilege];
}
Access::init();
public static function disablePrivilegeChecking()
{
self::$checkPrivileges = false;
}
public static function enablePrivilegeChecking()
{
self::$checkPrivileges = true;
}
}

4
src/AccessException.php Normal file
View file

@ -0,0 +1,4 @@
<?php
class AccessException extends SimpleException
{
}

117
src/Api/Api.php Normal file
View file

@ -0,0 +1,117 @@
<?php
final class Api
{
public static function getUrl()
{
return \Chibi\Router::linkTo(['ApiController', 'runAction']);
}
public static function run(IJob $job, $jobArgs)
{
$user = Auth::getCurrentUser();
return \Chibi\Database::transaction(function() use ($job, $jobArgs)
{
$job->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');
});
}
}

30
src/Api/ApiFileInput.php Normal file
View file

@ -0,0 +1,30 @@
<?php
/**
* Used for serializing files passed in POST requests to job arguments
*/
class ApiFileInput
{
public $filePath;
public $fileName;
public $originalPath;
public function __construct($filePath, $fileName)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
$this->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);
}
}

30
src/Api/ApiFileOutput.php Normal file
View file

@ -0,0 +1,30 @@
<?php
/**
* Used for serializing files output from jobs
*/
class ApiFileOutput implements ISerializable
{
public $fileContent;
public $fileName;
public $lastModified;
public $mimeType;
public function __construct($filePath, $fileName)
{
$this->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)),
];
}
}

View file

@ -0,0 +1,10 @@
<?php
class ApiJobUnsatisfiedException extends SimpleException
{
public function __construct(IJob $job, $arg = null)
{
parent::__construct('%s cannot be run due to unsatisfied execution conditions (%s).',
get_class($job),
$arg);
}
}

View file

@ -0,0 +1,8 @@
<?php
class ApiMissingArgumentException extends SimpleException
{
public function __construct($argumentName)
{
parent::__construct('Expected argument "' . $argumentName . '" was not specified');
}
}

View file

@ -0,0 +1,41 @@
<?php
class CommentRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->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);
}
}

View file

@ -0,0 +1,9 @@
<?php
interface IEntityRetriever
{
public function __construct(IJob $job);
public function getJob();
public function tryRetrieve();
public function retrieve();
public function getRequiredArguments();
}

View file

@ -0,0 +1,45 @@
<?php
class PostRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->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);
}
}

View file

@ -0,0 +1,41 @@
<?php
class SafePostRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->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);
}
}

View file

@ -0,0 +1,45 @@
<?php
class UserRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->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);
}
}

View file

@ -0,0 +1,50 @@
<?php
class JobPager
{
private $job;
public function __construct(IJob $job)
{
$this->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;
}
}

View file

@ -0,0 +1,72 @@
<?php
class JobArgs
{
const ARG_ANONYMOUS = 'anonymous';
const ARG_PAGE_NUMBER = 'page-number';
const ARG_QUERY = 'query';
const ARG_TOKEN = 'token';
const ARG_USER_ENTITY = 'user';
#const ARG_USER_ID = 'user-id';
const ARG_USER_NAME = 'user-name';
const ARG_USER_EMAIL = 'user-email';
const ARG_POST_ENTITY = 'post';
const ARG_POST_ID = 'post-id';
const ARG_POST_NAME = 'post-name';
const ARG_TAG_NAME = 'tag-name';
const ARG_TAG_NAMES = 'tag-names';
const ARG_COMMENT_ENTITY = 'comment';
const ARG_COMMENT_ID = 'comment-id';
const ARG_LOG_ID = 'log-id';
const ARG_NEW_TEXT = 'new-text';
const ARG_NEW_STATE = 'new-state';
const ARG_NEW_POST_CONTENT = 'new-post-content';
const ARG_NEW_POST_CONTENT_URL = 'new-post-content-url';
const ARG_NEW_RELATED_POST_IDS = 'new-related-post-ids';
const ARG_NEW_SAFETY = 'new-safety';
const ARG_NEW_SOURCE = 'new-source';
const ARG_NEW_THUMB_CONTENT = 'new-thumb-content';
const ARG_NEW_TAG_NAMES = 'new-tag-names';
const ARG_NEW_ACCESS_RANK = 'new-access-rank';
const ARG_NEW_EMAIL = 'new-email';
const ARG_NEW_USER_NAME = 'new-user-name';
const ARG_NEW_PASSWORD = 'new-password';
const ARG_NEW_SETTINGS = 'new-settings';
const ARG_NEW_POST_SCORE = 'new-post-score';
const ARG_SOURCE_TAG_NAME = 'source-tag-name';
const ARG_TARGET_TAG_NAME = 'target-tag-name';
public static function Alternative()
{
return JobArgsAlternative::factory(func_get_args());
}
public static function Conjunction()
{
return JobArgsConjunction::factory(func_get_args());
}
public static function Optional()
{
return JobArgsOptional::factory(func_get_args());
}
public static function getInternalArguments()
{
return
[
self::ARG_POST_ENTITY,
self::ARG_USER_ENTITY,
self::ARG_COMMENT_ENTITY
];
}
}

View file

@ -0,0 +1,25 @@
<?php
class JobArgsAlternative extends JobArgsNestedStruct
{
/**
* simplifies the structure as much as possible
* and returns new class or existing args.
*/
public static function factory(array $args)
{
$finalArgs = [];
foreach ($args as $arg)
{
if ($arg instanceof self)
$finalArgs = array_merge($finalArgs, $arg->args);
elseif ($arg !== null)
$finalArgs []= $arg;
}
if (count($finalArgs) == 1)
return $finalArgs[0];
else
return new self($finalArgs);
}
}

View file

@ -0,0 +1,25 @@
<?php
class JobArgsConjunction extends JobArgsNestedStruct
{
/**
* Simplifies the structure as much as possible
* and returns new class or existing args.
*/
public static function factory(array $args)
{
$finalArgs = [];
foreach ($args as $arg)
{
if ($arg instanceof self)
$finalArgs = array_merge($finalArgs, $arg->args);
elseif ($arg !== null)
$finalArgs []= $arg;
}
if (count($finalArgs) == 1)
return $finalArgs[0];
else
return new self($finalArgs);
}
}

View file

@ -0,0 +1,19 @@
<?php
class JobArgsNestedStruct
{
public $args;
protected function __construct(array $args)
{
usort($args, function($arg1, $arg2)
{
return strnatcasecmp(serialize($arg1), serialize($arg2));
});
$this->args = $args;
}
public static function factory(array $args)
{
throw new BadMethodCallException('Not implemented');
}
}

View file

@ -0,0 +1,20 @@
<?php
class JobArgsOptional extends JobArgsNestedStruct
{
/**
* Simplifies the structure as much as possible
* and returns new class or existing args.
*/
public static function factory(array $args)
{
$args = array_filter($args, function($arg)
{
return $arg !== null;
});
if (count($args) == 0)
return null;
else
return new self($args);
}
}

View file

@ -0,0 +1,83 @@
<?php
abstract class AbstractJob implements IJob
{
const CONTEXT_NORMAL = 1;
const CONTEXT_BATCH_EDIT = 2;
const CONTEXT_BATCH_ADD = 3;
protected $arguments = [];
protected $context = self::CONTEXT_NORMAL;
protected $subJobs;
public function prepare()
{
}
public abstract function execute();
public abstract function getRequiredArguments();
public function isAvailableToPublic()
{
return true;
}
public function getName()
{
$name = get_called_class();
$name = str_replace('Job', '', $name);
$name = TextCaseConverter::convert(
$name,
TextCaseConverter::UPPER_CAMEL_CASE,
TextCaseConverter::SPINAL_CASE);
return $name;
}
public function addSubJob(IJob $subJob)
{
$this->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;
}
}

View file

@ -0,0 +1,57 @@
<?php
class AddCommentJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,47 @@
<?php
class DeleteCommentJob extends AbstractJob
{
protected $commentRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,52 @@
<?php
class EditCommentJob extends AbstractJob
{
protected $commentRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,60 @@
<?php
class ListCommentsJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,63 @@
<?php
class PreviewCommentJob extends AbstractJob
{
protected $commentRetriever;
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,34 @@
<?php
class GetPropertyJob extends AbstractJob
{
public function execute()
{
return PropertyModel::get($this->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;
}
}

19
src/Api/Jobs/IJob.php Normal file
View file

@ -0,0 +1,19 @@
<?php
interface IJob
{
public function prepare();
public function execute();
public function getRequiredArguments();
public function getRequiredMainPrivilege();
public function getRequiredSubPrivileges();
public function isAuthenticationRequired();
public function isConfirmedEmailRequired();
public function isAvailableToPublic();
public function getArgument($key);
public function getArguments();
public function hasArgument($key);
public function setArgument($key, $value);
public function setArguments(array $arguments);
}

View file

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

View file

@ -0,0 +1,80 @@
<?php
class GetLogJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,44 @@
<?php
class ListLogsJob extends AbstractJob
{
public function execute()
{
$path = TextHelper::absolutePath(Core::getConfig()->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;
}
}

View file

@ -0,0 +1,88 @@
<?php
class AddPostJob extends AbstractJob
{
public function __construct()
{
$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 = 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;
}
}

View file

@ -0,0 +1,46 @@
<?php
class DeletePostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,66 @@
<?php
class EditPostContentJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,68 @@
<?php
class EditPostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,75 @@
<?php
class EditPostRelationsJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,61 @@
<?php
class EditPostSafetyJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,61 @@
<?php
class EditPostSourceJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,78 @@
<?php
class EditPostTagsJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,56 @@
<?php
class EditPostThumbJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,60 @@
<?php
class FeaturePostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,53 @@
<?php
class FlagPostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,62 @@
<?php
class GetPostContentJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,54 @@
<?php
class GetPostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,65 @@
<?php
class GetPostThumbJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,59 @@
<?php
class ListPostsJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,47 @@
<?php
class ScorePostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,55 @@
<?php
class TogglePostFavoriteJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,88 @@
<?php
class TogglePostTagJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,55 @@
<?php
class TogglePostVisibilityJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,59 @@
<?php
class ListRelatedTagsJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,57 @@
<?php
class ListTagsJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,44 @@
<?php
class MergeTagsJob extends AbstractJob
{
public function execute()
{
$sourceTag = $this->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;
}
}

View file

@ -0,0 +1,44 @@
<?php
class RenameTagsJob extends AbstractJob
{
public function execute()
{
$sourceTag = $this->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;
}
}

View file

@ -0,0 +1,49 @@
<?php
class AcceptUserRegistrationJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,102 @@
<?php
class ActivateUserEmailJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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);
}
}

View file

@ -0,0 +1,87 @@
<?php
class AddUserJob extends AbstractJob
{
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,47 @@
<?php
class DeleteUserJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,59 @@
<?php
class EditUserAccessRankJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,82 @@
<?php
class EditUserEmailJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,88 @@
<?php
class EditUserJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,61 @@
<?php
class EditUserNameJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,60 @@
<?php
class EditUserPasswordJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,56 @@
<?php
class EditUserSettingsJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,53 @@
<?php
class FlagUserJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,40 @@
<?php
class GetUserJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,41 @@
<?php
class GetUserSettingsJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,57 @@
<?php
class ListUsersJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,100 @@
<?php
class PasswordResetJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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);
}
}

View file

@ -0,0 +1,58 @@
<?php
class ToggleUserBanJob extends AbstractJob
{
protected $userRetriever;
public function __construct()
{
$this->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;
}
}

95
src/Assert.php Normal file
View file

@ -0,0 +1,95 @@
<?php
class Assert
{
public function throws($callback, $expectedMessage)
{
$success = false;
try
{
$callback();
$success = true;
}
catch (Exception $e)
{
if (stripos($e->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);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,82 @@
<?php
class AbstractController
{
protected $assets;
private $layoutName;
private static $isRendered;
public function isAjax()
{
return isset($_SERVER['HTTP_X_AJAX']);
}
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,61 @@
<?php
class ApiController extends AbstractController
{
public function runAction()
{
$context = Core::getContext();
try
{
if (!Auth::isLoggedIn())
{
$auth = InputHelper::get('auth');
if ($auth)
{
Auth::login($auth['user'], $auth['pass'], false);
}
}
$jobName = InputHelper::get('name');
$jobArgs = InputHelper::get('args');
$job = $this->jobFromName($jobName);
if (!$job)
throw new SimpleException('Unknown job: ' . $jobName);
if (!$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;
}
}

View file

@ -1,59 +1,35 @@
<?php
class AuthController
class AuthController extends AbstractController
{
public function loginAction()
public function loginView()
{
$context = getContext();
$context->handleExceptions = true;
//check if already logged in
if (Auth::isLoggedIn())
{
self::redirectAfterLog();
return;
$this->redirectToLastVisitedUrl('auth');
else
$this->renderView('auth-login');
}
if (!InputHelper::get('submit'))
return;
public function loginAction()
{
try
{
$suppliedName = InputHelper::get('name');
$suppliedPassword = InputHelper::get('password');
$remember = boolval(InputHelper::get('remember'));
$dbUser = Auth::login($suppliedName, $suppliedPassword, $remember);
self::redirectAfterLog();
Auth::login($suppliedName, $suppliedPassword, $remember);
}
catch (SimpleException $e)
{
Messenger::fail($e->getMessage());
$this->renderView('auth-login');
}
$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');
}
}

View file

@ -1,106 +1,97 @@
<?php
class CommentController
class CommentController extends AbstractController
{
public function listAction($page)
public function listView($page = 1)
{
Access::assert(Privilege::ListComments);
$ret = Api::run(
new ListCommentsJob(),
[
JobArgs::ARG_PAGE_NUMBER => $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();
if (InputHelper::get('sender') == 'preview')
{
$comment = Api::run(
new PreviewCommentJob(),
[
JobArgs::ARG_POST_ID => InputHelper::get('post-id'),
JobArgs::ARG_NEW_TEXT => InputHelper::get('text')
]);
$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());
Core::getContext()->transport->textPreview = $comment->getTextMarkdown();
$this->renderAjax();
}
else
$comment->setCommenter(null);
$comment->commentDate = time();
$comment->text = $text;
if (InputHelper::get('sender') != 'preview')
{
CommentModel::save($comment);
LogHelper::log('{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
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();
}
$context->transport->textPreview = $comment->getText();
}
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/');
}
}

View file

@ -0,0 +1,30 @@
<?php
class ErrorController extends AbstractController
{
public function simpleExceptionView(Exception $exception)
{
if ($exception instanceof SimpleNotFoundException)
\Chibi\Util\Headers::setCode(404);
else
\Chibi\Util\Headers::setCode(400);
Messenger::fail($exception->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');
}
}

View file

@ -1,52 +0,0 @@
<?php
class IndexController
{
public function indexAction()
{
$context = getContext();
$context->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;
}
}

Some files were not shown because too many files have changed in this diff Show more