From 6c4affe4548c2d0b1bbf4f264742130af1904b57 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sun, 13 Oct 2013 12:28:16 +0200 Subject: [PATCH] Closed #10 --- config.ini | 22 +- public_html/media/css/core.css | 3 + public_html/media/css/post-view.css | 44 +++- public_html/media/js/post-view.js | 118 +++++++++- public_html/media/js/upload.js | 3 +- src/Controllers/PostController.php | 328 +++++++++++++++++++++------- src/Models/Privilege.php | 12 +- src/Views/layout-normal.phtml | 2 +- src/Views/post-view.phtml | 138 ++++++++---- src/core.php | 1 + 10 files changed, 534 insertions(+), 137 deletions(-) diff --git a/config.ini b/config.ini index 1d0dc8c2..8e46504e 100644 --- a/config.ini +++ b/config.ini @@ -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 diff --git a/public_html/media/css/core.css b/public_html/media/css/core.css index 2a975143..2c6e4d98 100644 --- a/public_html/media/css/core.css +++ b/public_html/media/css/core.css @@ -96,6 +96,9 @@ body { #sidebar h1 { margin-top: 0; +} + +h1, h2, h3 { font-weight: normal; } diff --git a/public_html/media/css/post-view.css b/public_html/media/css/post-view.css index dc074ca6..a972aa79 100644 --- a/public_html/media/css/post-view.css +++ b/public_html/media/css/post-view.css @@ -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; +} diff --git a/public_html/media/js/post-view.js b/public_html/media/js/post-view.js index d3021213..cd6ad426 100644 --- a/public_html/media/js/post-view.js +++ b/public_html/media/js/post-view.js @@ -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); + }); }); diff --git a/public_html/media/js/upload.js b/public_html/media/js/upload.js index 54f1f301..3583fd28 100644 --- a/public_html/media/js/upload.js +++ b/public_html/media/js/upload.js @@ -154,7 +154,8 @@ $(function() postDom.show(); var tagItOptions = - { caseSensitive: true, + { + caseSensitive: true, availableTags: tags, placeholderText: $('.tags input').attr('placeholder') }; diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index 8e3e515d..2322b4f8 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -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; @@ -317,7 +527,7 @@ class PostController $dstWidth = $this->config->browsing->thumbWidth; $dstHeight = $this->config->browsing->thumbHeight; - switch($post->mime_type) + switch ($post->mime_type) { case 'image/jpeg': $srcImage = imagecreatefromjpeg($srcPath); @@ -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; - } } diff --git a/src/Models/Privilege.php b/src/Models/Privilege.php index 4925e5d1..e6111939 100644 --- a/src/Models/Privilege.php +++ b/src/Models/Privilege.php @@ -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; } diff --git a/src/Views/layout-normal.phtml b/src/Views/layout-normal.phtml index 84dcd7dc..18febca9 100644 --- a/src/Views/layout-normal.phtml +++ b/src/Views/layout-normal.phtml @@ -66,7 +66,7 @@
- +
diff --git a/src/Views/post-view.phtml b/src/Views/post-view.phtml index 7c2d9dbe..6b154640 100644 --- a/src/Views/post-view.phtml +++ b/src/Views/post-view.phtml @@ -27,7 +27,6 @@