This commit is contained in:
Marcin Kurczewski 2013-10-13 12:28:16 +02:00
parent be4a2615dd
commit 6c4affe454
10 changed files with 534 additions and 137 deletions

View file

@ -37,14 +37,26 @@ Kind regards,
[privileges]
uploadPost=registered
viewPost=anonymous
viewPost.sketchy=registered
viewPost.unsafe=registered
listPosts=anonymous
listPosts.sketchy=registered
listPosts.unsafe=registered
listPosts.hidden=nobody
listUsers=registered
favoritePost=registered
listComments=anonymous
viewPost=anonymous
viewPost.sketchy=registered
viewPost.unsafe=registered
viewPost.hidden=admin
retrievePost=anonymous
favoritePost=registered
editPostSafety.own=registered
editPostSafety.all=moderator
editPostTags.own=registered
editPostTags.all=registered
editPostThumb.own=moderator
editPostThumb.all=moderator
hidePost.own=moderator
hidePost.all=moderator
deletePost.own=moderator
deletePost.all=moderator
listComments=anonymous
listTags=anonymous

View file

@ -96,6 +96,9 @@ body {
#sidebar h1 {
margin-top: 0;
}
h1, h2, h3 {
font-weight: normal;
}

View file

@ -73,12 +73,15 @@ i.icon-dl {
margin-right: 0.5em;
}
.options nav ul {
.options ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.favorites p {
margin: 0;
}
.favorites ul {
list-style-type: none;
margin: 0;
@ -90,3 +93,42 @@ i.icon-dl {
.favorites a {
margin: 2px;
}
.inactive {
opacity: .5;
}
form.edit {
display: none;
padding: 0.5em 1em;
border: 1px solid #eee;
border-bottom: 0;
padding-bottom: 0;
margin: 1em 0;
}
form.edit>div {
margin-bottom: 0.5em;
}
form.edit input[type=checkbox],
form.edit label {
vertical-align: middle;
line-height: 33px;
}
form.edit label.left {
display: inline-block;
width: 5em;
float: left;
}
form.edit .safety label:not(.left) {
margin-right: 0.75em;
}
form.edit>div {
clear: left;
}
ul.tagit {
display: block;
vertical-align: middle;
margin: 0;
font-size: 1em;
}

View file

@ -1,20 +1,122 @@
$(function()
{
$('.add-fav a, .rem-fav a').click(function(e)
$('.add-fav a, .rem-fav a, .hide a, .unhide a').click(function(e)
{
e.preventDefault();
var url = $(this).attr('href');
url += '?json';
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url, function(data)
{
if (data['errorMessage'])
{
alert(data['errorMessage']);
}
else
if (data['success'])
{
window.location.reload();
}
else
{
alert(data['errorMessage']);
aDom.removeClass('inactive');
}
});
});
$('.delete a').click(function(e)
{
e.preventDefault();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
//todo: move this string literal to html
if (confirm(aDom.attr('data-confirm-text')))
{
var url = $(this).attr('href') + '?json';
$.get(url, function(data)
{
if (data['success'])
{
window.location.href = aDom.attr('data-redirect-url');
}
else
{
alert(data['errorMessage']);
aDom.removeClass('inactive');
}
});
}
else
{
aDom.removeClass('inactive');
}
});
$('li.edit a').click(function(e)
{
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var tags = [];
$.getJSON('/tags?json', function(data)
{
tags = data['tags'];
var tagItOptions =
{
caseSensitive: true,
availableTags: tags,
placeholderText: $('.tags input').attr('placeholder')
};
$('.tags input').tagit(tagItOptions);
e.preventDefault();
$('form.edit').slideDown();
});
});
$('form.edit').submit(function(e)
{
e.preventDefault();
var formDom = $(this);
if (formDom.hasClass('inactive'))
return;
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
var url = formDom.attr('action') + '?json';
var fd = new FormData(formDom[0]);
var ajaxData =
{
url: url,
data: fd,
processData: false,
contentType: false,
type: 'POST',
success: function(data)
{
if (data['success'])
{
window.location.reload();
}
else
{
alert(data['errorMessage']);
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
}
};
$.ajax(ajaxData);
});
});

View file

@ -154,7 +154,8 @@ $(function()
postDom.show();
var tagItOptions =
{ caseSensitive: true,
{
caseSensitive: true,
availableTags: tags,
placeholderText: $('.tags input').attr('placeholder')
};

View file

@ -8,6 +8,60 @@ class PostController
$callback();
}
private static function locatePost($key)
{
if (is_numeric($key))
{
$post = R::findOne('post', 'id = ?', [$key]);
if (!$post)
throw new SimpleException('Invalid post ID "' . $key . '"');
}
else
{
$post = R::findOne('post', 'name = ?', [$key]);
if (!$post)
throw new SimpleException('Invalid post name "' . $key . '"');
}
return $post;
}
private static function serializeTags($post)
{
$x = [];
foreach ($post->sharedTag as $tag)
$x []= $tag->name;
natcasesort($x);
$x = join('', $x);
return md5($x);
}
private static function handleUploadErrors($file)
{
switch ($file['error'])
{
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
throw new SimpleException('File is too big (maximum size allowed: ' . ini_get('upload_max_filesize') . ')');
case UPLOAD_ERR_FORM_SIZE:
throw new SimpleException('File is too big than it was allowed in HTML form');
case UPLOAD_ERR_PARTIAL:
throw new SimpleException('File transfer was interrupted');
case UPLOAD_ERR_NO_FILE:
throw new SimpleException('No file was uploaded');
case UPLOAD_ERR_NO_TMP_DIR:
throw new SimpleException('Server misconfiguration error: missing temporary folder');
case UPLOAD_ERR_CANT_WRITE:
throw new SimpleException('Server misconfiguration error: cannot write to disk');
case UPLOAD_ERR_EXTENSION:
throw new SimpleException('Server misconfiguration error: upload was canceled by an extension');
default:
throw new SimpleException('Generic file upload error (id: ' . $file['error'] . ')');
}
if (!is_uploaded_file($file['tmp_name']))
throw new SimpleException('Generic file upload error');
}
/**
@ -37,7 +91,6 @@ class PostController
$this->context->subTitle = 'browsing posts';
$page = intval($page);
$postsPerPage = intval($this->config->browsing->postsPerPage);
PrivilegesHelper::confirmWithException($this->context->user, Privilege::ListPosts);
$buildDbQuery = function($dbQuery)
@ -54,6 +107,9 @@ class PostController
foreach ($allowedSafety as $s)
$dbQuery->put($s);
if (!PrivilegesHelper::confirm($this->context->user, Privilege::ListPosts, 'hidden'))
$dbQuery->andNot('hidden');
//todo construct WHERE based on filters
//todo construct ORDER based on filers
@ -82,6 +138,19 @@ class PostController
/**
* @route /favorites
* @route /favorites/{page}
* @validate page \d*
*/
public function favoritesAction($page = 1)
{
$this->listAction('favmin:1', $page);
$this->context->viewName = 'post-list';
}
/**
* @route /post/upload
*/
@ -90,15 +159,17 @@ class PostController
$this->context->stylesheets []= 'upload.css';
$this->context->scripts []= 'upload.js';
$this->context->subTitle = 'upload';
PrivilegesHelper::confirmWithException($this->context->user, Privilege::UploadPost);
if (isset($_FILES['file']))
{
/* safety */
$suppliedSafety = intval(InputHelper::get('safety'));
if (!in_array($suppliedSafety, PostSafety::getAll()))
throw new SimpleException('Invalid safety type "' . $suppliedSafety . '"');
/* tags */
$suppliedTags = InputHelper::get('tags');
$suppliedTags = preg_split('/[,;\s+]/', $suppliedTags);
$suppliedTags = array_filter($suppliedTags);
@ -109,32 +180,26 @@ class PostController
if (empty($suppliedTags))
throw new SimpleException('No tags set');
$suppliedFile = $_FILES['file'];
switch ($suppliedFile['error'])
$dbTags = [];
foreach ($suppliedTags as $tag)
{
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
throw new SimpleException('File is too big (maximum size allowed: ' . ini_get('upload_max_filesize') . ')');
case UPLOAD_ERR_FORM_SIZE:
throw new SimpleException('File is too big than it was allowed in HTML form');
case UPLOAD_ERR_PARTIAL:
throw new SimpleException('File transfer was interrupted');
case UPLOAD_ERR_NO_FILE:
throw new SimpleException('No file was uploaded');
case UPLOAD_ERR_NO_TMP_DIR:
throw new SimpleException('Server misconfiguration error: missing temporary folder');
case UPLOAD_ERR_CANT_WRITE:
throw new SimpleException('Server misconfiguration error: cannot write to disk');
case UPLOAD_ERR_EXTENSION:
throw new SimpleException('Server misconfiguration error: upload was canceled by an extension');
default:
throw new SimpleException('Generic file upload error (id: ' . $suppliedFile['error'] . ')');
$dbTag = R::findOne('tag', 'name = ?', [$tag]);
if (!$dbTag)
{
$dbTag = R::dispense('tag');
$dbTag->name = $tag;
R::store($dbTag);
}
$dbTags []= $dbTag;
}
if (!is_uploaded_file($suppliedFile['tmp_name']))
throw new SimpleException('Generic file upload error');
#$mimeType = $suppliedFile['type'];
/* file contents */
$suppliedFile = $_FILES['file'];
self::handleUploadErrors($suppliedFile);
/* file details */
$mimeType = mime_content_type($suppliedFile['tmp_name']);
$imageWidth = null;
$imageHeight = null;
@ -166,19 +231,8 @@ class PostController
}
while (file_exists($path));
$dbTags = [];
foreach ($suppliedTags as $tag)
{
$dbTag = R::findOne('tag', 'name = ?', [$tag]);
if (!$dbTag)
{
$dbTag = R::dispense('tag');
$dbTag->name = $tag;
R::store($dbTag);
}
$dbTags []= $dbTag;
}
/* db storage */
$dbPost = R::dispense('post');
$dbPost->type = $postType;
$dbPost->name = $name;
@ -187,6 +241,7 @@ class PostController
$dbPost->file_size = filesize($suppliedFile['tmp_name']);
$dbPost->mime_type = $mimeType;
$dbPost->safety = $suppliedSafety;
$dbPost->hidden = false;
$dbPost->upload_date = time();
$dbPost->image_width = $imageWidth;
$dbPost->image_height = $imageHeight;
@ -201,6 +256,141 @@ class PostController
}
}
/**
* @route /post/edit/{id}
*/
public function editAction($id)
{
$post = self::locatePost($id);
R::preload($post, ['uploader' => 'user']);
$edited = false;
$secondary = $post->uploader->id == $this->context->user->id ? 'own' : 'all';
/* safety */
$suppliedSafety = InputHelper::get('safety');
if ($suppliedSafety !== null)
{
PrivilegesHelper::confirmWithException($this->context->user, Privilege::EditPostSafety, $secondary);
$suppliedSafety = intval($suppliedSafety);
if (!in_array($suppliedSafety, PostSafety::getAll()))
throw new SimpleException('Invalid safety type "' . $suppliedSafety . '"');
$post->safety = $suppliedSafety;
$edited = true;
}
/* tags */
$suppliedTags = InputHelper::get('tags');
if ($suppliedTags !== null)
{
PrivilegesHelper::confirmWithException($this->context->user, Privilege::EditPostTags, $secondary);
$currentToken = self::serializeTags($post);
if (InputHelper::get('tags-token') != $currentToken)
throw new SimpleException('Someone else has changed the tags in the meantime');
$suppliedTags = preg_split('/[,;\s+]/', $suppliedTags);
$suppliedTags = array_filter($suppliedTags);
$suppliedTags = array_unique($suppliedTags);
foreach ($suppliedTags as $tag)
if (!preg_match('/^[a-zA-Z0-9_-]+$/i', $tag))
throw new SimpleException('Invalid tag "' . $tag . '"');
if (empty($suppliedTags))
throw new SimpleException('No tags set');
$dbTags = [];
foreach ($suppliedTags as $tag)
{
$dbTag = R::findOne('tag', 'name = ?', [$tag]);
if (!$dbTag)
{
$dbTag = R::dispense('tag');
$dbTag->name = $tag;
R::store($dbTag);
}
$dbTags []= $dbTag;
}
$post->sharedTag = $dbTags;
$edited = true;
}
/* thumbnail */
if (isset($_FILES['thumb']))
{
PrivilegesHelper::confirmWithException($this->context->user, Privilege::EditPostThumb, $secondary);
$suppliedFile = $_FILES['thumb'];
self::handleUploadErrors($suppliedFile);
$mimeType = mime_content_type($suppliedFile['tmp_name']);
if (!in_array($mimeType, ['image/gif', 'image/png', 'image/jpeg']))
throw new SimpleException('Invalid thumbnail type "' . $mimeType . '"');
list ($imageWidth, $imageHeight) = getimagesize($suppliedFile['tmp_name']);
if ($imageWidth != $this->config->browsing->thumbWidth)
throw new SimpleException('Invalid thumbnail width (should be ' . $this->config->browsing->thumbWidth . ')');
if ($imageWidth != $this->config->browsing->thumbHeight)
throw new SimpleException('Invalid thumbnail width (should be ' . $this->config->browsing->thumbHeight . ')');
$path = $this->config->main->thumbsPath . DS . $post->name;
move_uploaded_file($suppliedFile['tmp_name'], $path);
}
/* db storage */
if ($edited)
R::store($post);
$this->context->transport->success = true;
}
/**
* @route /post/hide/{id}
*/
public function hideAction($id)
{
$post = self::locatePost($id);
$secondary = $post->uploader->id == $this->context->user->id ? 'own' : 'all';
PrivilegesHelper::confirmWithException($this->context->user, Privilege::HidePost, $secondary);
$post->hidden = true;
R::store($post);
$this->context->transport->success = true;
}
/**
* @route /post/unhide/{id}
*/
public function unhideAction($id)
{
$post = self::locatePost($id);
$secondary = $post->uploader->id == $this->context->user->id ? 'own' : 'all';
PrivilegesHelper::confirmWithException($this->context->user, Privilege::HidePost, $secondary);
$post->hidden = false;
R::store($post);
$this->context->transport->success = true;
}
/**
* @route /post/delete/{id}
*/
public function deleteAction($id)
{
$post = self::locatePost($id);
$secondary = $post->uploader->id == $this->context->user->id ? 'own' : 'all';
PrivilegesHelper::confirmWithException($this->context->user, Privilege::DeletePost, $secondary);
//remove stuff from auxiliary tables
$post->ownFavoritee = [];
$post->sharedTag = [];
R::store($post);
R::trash($post);
$this->context->transport->success = true;
}
/**
* @route /post/add-fav/{id}
* @route /post/fav-add/{id}
@ -260,12 +450,31 @@ class PostController
$post = self::locatePost($id);
R::preload($post, ['favoritee' => 'user', 'uploader' => 'user', 'tag']);
$prevPost = R::findOne('post', 'id < ? ORDER BY id DESC LIMIT 1', [$id]);
$nextPost = R::findOne('post', 'id > ? ORDER BY id ASC LIMIT 1', [$id]);
if ($post->hidden)
PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewPost, 'hidden');
PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewPost);
PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewPost, PostSafety::toString($post->safety));
$buildNextPostQuery = function($dbQuery, $id, $next)
{
$dbQuery->select('id')
->from('post')
->where($next ? 'id > ?' : 'id < ?')
->put($id);
if (!PrivilegesHelper::confirm($this->context->user, Privilege::ListPosts, 'hidden'))
$dbQuery->andNot('hidden');
$dbQuery->orderBy($next ? 'id asc' : 'id desc')
->limit(1);
};
$prevPostQuery = R::$f->begin();
$buildNextPostQuery($prevPostQuery, $id, false);
$prevPost = $prevPostQuery->get('row');
$nextPostQuery = R::$f->begin();
$buildNextPostQuery($nextPostQuery, $id, true);
$nextPost = $nextPostQuery->get('row');
$favorite = false;
if ($this->context->loggedIn)
foreach ($post->ownFavoritee as $fav)
@ -291,8 +500,9 @@ class PostController
$this->context->subTitle = 'showing @' . $post->id;
$this->context->favorite = $favorite;
$this->context->transport->post = $post;
$this->context->transport->prevPostId = $prevPost ? $prevPost->id : null;
$this->context->transport->nextPostId = $nextPost ? $nextPost->id : null;
$this->context->transport->prevPostId = $prevPost ? $prevPost['id'] : null;
$this->context->transport->nextPostId = $nextPost ? $nextPost['id'] : null;
$this->context->transport->tagsToken = self::serializeTags($post);
}
@ -309,7 +519,7 @@ class PostController
PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewPost);
PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewPost, PostSafety::toString($post->safety));
$path = $this->config->main->thumbsPath . DS . $post->name . '.png';
$path = $this->config->main->thumbsPath . DS . $post->name;
if (!file_exists($path))
{
$srcPath = $this->config->main->filesPath . DS . $post->name;
@ -389,34 +599,4 @@ class PostController
$this->context->transport->mimeType = $post->mimeType;
$this->context->transport->filePath = $path;
}
/**
* @route /favorites
* @route /favorites/{page}
* @validate page \d*
*/
public function favoritesAction($page = 1)
{
$this->listAction('favmin:1', $page);
$this->context->viewName = 'post-list';
}
public static function locatePost($key)
{
if (is_numeric($key))
{
$post = R::findOne('post', 'id = ?', [$key]);
if (!$post)
throw new SimpleException('Invalid post ID "' . $key . '"');
}
else
{
$post = R::findOne('post', 'name = ?', [$key]);
if (!$post)
throw new SimpleException('Invalid post name "' . $key . '"');
}
return $post;
}
}

View file

@ -6,7 +6,13 @@ class Privilege extends Enum
const ViewPost = 3;
const RetrievePost = 4;
const FavoritePost = 5;
const ListUsers = 6;
const ListComments = 7;
const ListTags = 8;
const EditPostSafety = 6;
const EditPostTags = 7;
const EditPostThumb = 8;
const HidePost = 9;
const DeletePost = 10;
const ListUsers = 11;
const ListComments = 12;
const ListTags = 13;
}

View file

@ -66,7 +66,7 @@
<form name="search" action="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>" method="get">
<input type="search" name="query" placeholder="Search&hellip;" value="<?php echo isset($this->context->transport->searchQuery) ? $this->context->transport->searchQuery : '' ?>">
</form>
</i>
</li>
</ul>
<div class="clear"></div>
</div>

View file

@ -27,7 +27,6 @@
<div class="sidebar-unit tags">
<h1>tags (<?php echo count($this->context->transport->post->sharedTag) ?>)</h1>
<!-- todo: edit tags -->
<ul>
<?php foreach ($this->context->transport->post->sharedTag as $tag): ?>
<li>
@ -107,7 +106,6 @@
<div class="sidebar-unit options">
<h1>options</h1>
<nav>
<ul>
<?php if (PrivilegesHelper::confirm($this->context->user, Privilege::FavoritePost)): ?>
<?php if (!$this->context->favorite): ?>
@ -125,39 +123,54 @@
<?php endif ?>
<?php endif ?>
<!--
<li>
<a href="#">
Edit tags
</a>
</li>
<?php
$secondary = $this->context->transport->post->uploader->id == $this->context->user->id ? 'own' : 'all';
$editPostPrivileges = [
Privilege::EditPostSafety,
Privilege::EditPostTags,
Privilege::EditPostThumb,
];
$editPostPrivileges = array_fill_keys($editPostPrivileges, false);
foreach (array_keys($editPostPrivileges) as $privilege)
{
if (PrivilegesHelper::confirm($this->context->user, $privilege, $secondary))
$editPostPrivileges[$privilege] = true;
}
$canEditAnything = count(array_filter($editPostPrivileges)) > 0;
?>
<li>
<?php if ($canEditAnything): ?>
<li class="edit">
<a href="#">
Change safety
Edit
</a>
</li>
<?php endif ?>
<li>
<a href="#">
Show on main page
<?php if (PrivilegesHelper::confirm($this->context->user, Privilege::HidePost, $secondary)): ?>
<?php if ($this->context->transport->post->hidden): ?>
<li class="unhide">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'unhide', ['id' => $this->context->transport->post->id]) ?>">
Unhide
</a>
</li>
<?php else: ?>
<li class="hide">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'hide', ['id' => $this->context->transport->post->id]) ?>">
Hide
</a>
</li>
<?php endif ?>
<?php endif ?>
<li>
<a href="#">
Hide from main page
<?php if (PrivilegesHelper::confirm($this->context->user, Privilege::DeletePost, $secondary)): ?>
<li class="delete">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'delete', ['id' => $this->context->transport->post->id]) ?>" data-confirm-text="Are you sure?" data-redirect-url="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>">
Delete
</a>
</li>
<li>
<a href="#">
Remove
</a>
</li>
-->
<?php endif ?>
</ul>
</nav>
<script type="text/javascript">
if (!$('.options ul li').length)
@ -174,4 +187,41 @@
<embed width="<?php echo $this->context->transport->post->image_width ?>" height="<?php echo $this->context->transport->post->image_height ?>" type="application/x-shockwave-flash" src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $this->context->transport->post->name]) ?>"/>
<?php endif ?>
</div>
<?php if ($canEditAnything): ?>
<form action="<?php echo \Chibi\UrlHelper::route('post', 'edit', ['id' => $this->context->transport->post->id]) ?>" method="post" enctype="multiplart/form-data" class="edit">
<h1>edit post</h1>
<?php if ($editPostPrivileges[Privilege::EditPostSafety]): ?>
<div class="safety">
<label class="left">Safety:</label>
<?php foreach (PostSafety::getAll() as $safety): ?>
<label>
<input type="radio" name="safety" value="<?php echo $safety ?>" <?php if ($this->context->transport->post->safety == $safety) echo 'checked="checked"' ?>/>
&nbsp;<?php echo PostSafety::toString($safety) ?>
</label>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($editPostPrivileges[Privilege::EditPostTags]): ?>
<div class="tags">
<label class="left" for="tags">Tags:</label>
<input type="text" name="tags" id="tags" placeholder="enter some tags&hellip;" value="<?php echo join(',', array_map(function($tag) { return $tag->name; }, $this->context->transport->post->sharedTag)) ?>"/>
</div>
<input type="hidden" name="tags-token" id="tags-token" value="<?php echo $this->context->transport->tagsToken ?>"/>
<?php endif ?>
<?php if ($editPostPrivileges[Privilege::EditPostThumb]): ?>
<div class="thumb">
<label class="left" for="thumb">Thumb:</label>
<input type="file" name="thumb" id="thumb"/>
</div>
<?php endif ?>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Submit</button>
</div>
</form>
<?php endif ?>
</div>

View file

@ -40,6 +40,7 @@ function configFactory()
$config = configFactory();
R::setup('sqlite:' . $config->main->dbPath);
#R::dependencies(['tag' => ['post'], 'post' => ['user']]);
//wire models
\Chibi\AutoLoader::init([$config->chibi->userCodeDir, __DIR__]);