From d8997edc5784ee48d04c0df22b769da7f379819c Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Fri, 22 Nov 2013 21:20:56 +0100 Subject: [PATCH] Refactor of controllers and models - Most of model-related code moved from controllers to model classes, much fewer calls to R::whatever() in controllers - Post editing and uploading shares the same code, thus making implementing stuff easier in the future - Added support for default bean wiring, no more calls to R::preload() all over the place - More robust concurrent post editing detection --- data/config.ini | 2 + src/Controllers/CommentController.php | 9 +- src/Controllers/IndexController.php | 2 +- src/Controllers/PostController.php | 585 ++++++++------------------ src/Controllers/TagController.php | 10 +- src/Controllers/UserController.php | 40 +- src/Models/AbstractModel.php | 30 ++ src/Models/Model_Comment.php | 9 + src/Models/Model_Post.php | 367 +++++++++++++++- src/Models/Model_Tag.php | 33 +- src/Models/Model_User.php | 220 +++++++--- src/Models/Privilege.php | 1 + src/Views/post-edit.phtml | 2 +- src/core.php | 2 +- 14 files changed, 785 insertions(+), 527 deletions(-) diff --git a/data/config.ini b/data/config.ini index f095ef33..4306e13e 100644 --- a/data/config.ini +++ b/data/config.ini @@ -90,6 +90,8 @@ editPostThumb=moderator editPostSource=moderator editPostRelations.own=registered editPostRelations.all=moderator +editPostFile.all=moderator +editPostFile.own=moderator hidePost.own=moderator hidePost.all=moderator deletePost.own=moderator diff --git a/src/Controllers/CommentController.php b/src/Controllers/CommentController.php index 8f371f86..173d6a64 100644 --- a/src/Controllers/CommentController.php +++ b/src/Controllers/CommentController.php @@ -25,7 +25,6 @@ class CommentController $page = max(1, min($pageCount, $page)); $comments = Model_Comment::getEntities(null, $commentsPerPage, $page); - R::preload($comments, ['commenter' => 'user', 'post', 'post.uploader' => 'user']); $this->context->postGroups = true; $this->context->transport->paginator = new StdClass; $this->context->transport->paginator->page = $page; @@ -55,7 +54,7 @@ class CommentController $text = InputHelper::get('text'); $text = Model_Comment::validateText($text); - $comment = R::dispense('comment'); + $comment = Model_Comment::create(); $comment->post = $post; if ($this->context->loggedIn) $comment->commenter = $this->context->user; @@ -63,7 +62,7 @@ class CommentController $comment->text = $text; if (InputHelper::get('sender') != 'preview') { - R::store($comment); + Model_Comment::save($comment); LogHelper::logEvent('comment-add', '{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]); } $this->context->transport->textPreview = $comment->getText(); @@ -80,10 +79,10 @@ class CommentController public function deleteAction($id) { $comment = Model_Comment::locate($id); - R::preload($comment, ['commenter' => 'user']); PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->commenter)); + Model_Comment::remove($comment); + LogHelper::logEvent('comment-del', '{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->post)]); - R::trash($comment); StatusHelper::success(); } } diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index bc96f343..06a64213 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -9,7 +9,7 @@ class IndexController { $this->context->subTitle = 'home'; $this->context->stylesheets []= 'index-index.css'; - $this->context->transport->postCount = R::$f->begin()->select('count(1)')->as('count')->from('post')->get('row')['count']; + $this->context->transport->postCount = Model_Post::getAllPostCount(); $featuredPostRotationTime = $this->config->misc->featuredPostMaxDays * 24 * 3600; diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index 3f9a15ad..9fde9c38 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -8,13 +8,18 @@ class PostController $callback(); } - private static function serializeTags($post) + private static function serializePost($post) { $x = []; foreach ($post->sharedTag as $tag) - $x []= $tag->name; + $x []= TextHelper::reprTag($tag->name); + foreach ($post->via('crossref')->sharedPost as $relatedPost) + $x []= TextHelper::reprPost($relatedPost); + $x []= $post->safety; + $x []= $post->source; + $x []= $post->file_hash; natcasesort($x); - $x = join('', $x); + $x = join(' ', $x); return md5($x); } @@ -120,7 +125,6 @@ class PostController public function toggleTagAction($id, $tag) { $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); $this->context->transport->post = $post; $tagRow = Model_Tag::locate($tag, false); @@ -146,7 +150,7 @@ class PostController $dbTags = Model_Tag::insertOrUpdate($tags); $post->sharedTag = $dbTags; - R::store($post); + Model_Post::save($post); StatusHelper::success(); } } @@ -192,173 +196,38 @@ class PostController if (InputHelper::get('submit')) { - /* file contents */ - if (isset($_FILES['file'])) + R::transaction(function() { - $suppliedFile = $_FILES['file']; - self::handleUploadErrors($suppliedFile); - $origName = basename($suppliedFile['name']); - $sourcePath = $suppliedFile['tmp_name']; - } - elseif (InputHelper::get('url')) - { - $url = InputHelper::get('url'); - $origName = $url; - if (!preg_match('/^https?:\/\//', $url)) - throw new SimpleException('Invalid URL "' . $url . '"'); + $post = Model_Post::create(); - if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $url, $matches)) - { - $origName = $matches[1]; - $postType = PostType::Youtube; - $sourcePath = null; - } - else - { - $sourcePath = tempnam(sys_get_temp_dir(), 'upload') . '.dat'; + //basic stuff + $anonymous = InputHelper::get('anonymous'); + if ($this->context->loggedIn and !$anonymous) + $post->uploader = $this->context->user; - //warning: low level sh*t ahead - //download the URL $url into $sourcePath - $maxBytes = TextHelper::stripBytesUnits(ini_get('upload_max_filesize')); - set_time_limit(0); - $urlFP = fopen($url, 'rb'); - if (!$urlFP) - throw new SimpleException('Cannot open URL for reading'); - $sourceFP = fopen($sourcePath, 'w+b'); - if (!$sourceFP) - { - fclose($urlFP); - throw new SimpleException('Cannot open file for writing'); - } - try - { - while (!feof($urlFP)) - { - $buffer = fread($urlFP, 4 * 1024); - if (fwrite($sourceFP, $buffer) === false) - throw new SimpleException('Cannot write into file'); - fflush($sourceFP); - if (ftell($sourceFP) > $maxBytes) - throw new SimpleException('File is too big (maximum allowed size: ' . TextHelper::useBytesUnits($maxBytes) . ')'); - } - } - finally - { - fclose($urlFP); - fclose($sourceFP); - } - } - } + //store the post to get the ID in the logs + Model_Post::save($post); + //log + LogHelper::bufferChanges(); + $fmt = ($anonymous and !$this->config->misc->logAnonymousUploads) + ? 'someone' + : '{user}'; + $fmt .= ' added {post}'; + LogHelper::logEvent('post-new', $fmt, ['post' => TextHelper::reprPost($post)]); - /* file details */ - $mimeType = null; - if ($sourcePath) - { - if (function_exists('mime_content_type')) - $mimeType = mime_content_type($sourcePath); - else - $mimeType = $suppliedFile['type']; - } - $imageWidth = null; - $imageHeight = null; - switch ($mimeType) - { - case 'image/gif': - case 'image/png': - case 'image/jpeg': - $postType = PostType::Image; - list ($imageWidth, $imageHeight) = getimagesize($sourcePath); - break; - case 'application/x-shockwave-flash': - $postType = PostType::Flash; - list ($imageWidth, $imageHeight) = getimagesize($sourcePath); - break; - default: - if (!isset($postType)) - throw new SimpleException('Invalid file type "' . $mimeType . '"'); - } + //after logging basic info, do the editing stuff + $this->doEdit($post, true); - if ($sourcePath) - { - $fileSize = filesize($sourcePath); - $fileHash = md5_file($sourcePath); - $duplicatedPost = R::findOne('post', 'file_hash = ?', [$fileHash]); - if ($duplicatedPost !== null) - throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); - } - else - { - $fileSize = 0; - $fileHash = null; - if ($postType == PostType::Youtube) - { - $duplicatedPost = R::findOne('post', 'orig_name = ?', [$origName]); - if ($duplicatedPost !== null) - throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); - } - } + //this basically means that user didn't specify file nor url + if (empty($post->type)) + throw new SimpleException('No post type detected; upload faled'); - do - { - $name = md5(mt_rand() . uniqid()); - $path = $this->config->main->filesPath . DS . $name; - } - while (file_exists($path)); + LogHelper::flush(); - - /* safety */ - $suppliedSafety = InputHelper::get('safety'); - $suppliedSafety = Model_Post::validateSafety($suppliedSafety); - - /* tags */ - $suppliedTags = InputHelper::get('tags'); - $suppliedTags = Model_Tag::validateTags($suppliedTags); - $dbTags = Model_Tag::insertOrUpdate($suppliedTags); - - /* source */ - $suppliedSource = InputHelper::get('source'); - $suppliedSource = Model_Post::validateSource($suppliedSource); - - /* anonymous */ - $anonymous = InputHelper::get('anonymous'); - - /* db storage */ - $dbPost = R::dispense('post'); - $dbPost->type = $postType; - $dbPost->name = $name; - $dbPost->orig_name = $origName; - $dbPost->file_hash = $fileHash; - $dbPost->file_size = $fileSize; - $dbPost->mime_type = $mimeType; - $dbPost->safety = $suppliedSafety; - $dbPost->source = $suppliedSource; - $dbPost->hidden = false; - $dbPost->upload_date = time(); - $dbPost->image_width = $imageWidth; - $dbPost->image_height = $imageHeight; - if ($this->context->loggedIn and !$anonymous) - $dbPost->uploader = $this->context->user; - $dbPost->ownFavoritee = []; - $dbPost->sharedTag = $dbTags; - - if ($sourcePath) - { - if (is_uploaded_file($sourcePath)) - move_uploaded_file($sourcePath, $path); - else - rename($sourcePath, $path); - } - R::store($dbPost); - - $fmt = ($anonymous and !$this->config->misc->logAnonymousUploads) - ? 'someone' - : '{user}'; - $fmt .= ' added {post} tagged with {tags} marked as {safety}'; - LogHelper::logEvent('post-new', $fmt, [ - 'post' => TextHelper::reprPost($dbPost), - 'tags' => join(', ', array_map(['TextHelper', 'reprTag'], $dbTags)), - 'safety' => PostSafety::toString($dbPost->safety)]); + //finish + Model_Post::save($post); + }); StatusHelper::success(); } @@ -372,111 +241,21 @@ class PostController public function editAction($id) { $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); $this->context->transport->post = $post; if (InputHelper::get('submit')) { + $editToken = InputHelper::get('edit-token'); + if ($editToken != self::serializePost($post)) + throw new SimpleException('This post was already edited by someone else in the meantime'); + LogHelper::bufferChanges(); + $this->doEdit($post, false); + LogHelper::flush(); - /* safety */ - $suppliedSafety = InputHelper::get('safety'); - if ($suppliedSafety !== null and $suppliedSafety != $post->safety) - { - PrivilegesHelper::confirmWithException(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); - $suppliedSafety = Model_Post::validateSafety($suppliedSafety); - $post->safety = $suppliedSafety; - $edited = true; - LogHelper::logEvent('post-edit', '{user} changed safety for {post} to {safety}', ['post' => TextHelper::reprPost($post), 'safety' => PostSafety::toString($post->safety)]); - } - - - /* tags */ - $suppliedTags = InputHelper::get('tags'); - if ($suppliedTags !== null) - { - PrivilegesHelper::confirmWithException(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); - $currentToken = self::serializeTags($post); - if (InputHelper::get('tags-token') != $currentToken) - throw new SimpleException('Someone else has changed the tags in the meantime'); - - $suppliedTags = Model_Tag::validateTags($suppliedTags); - $dbTags = Model_Tag::insertOrUpdate($suppliedTags); - - $oldTags = array_map(function($tag) { return $tag->name; }, $post->sharedTag); - $post->sharedTag = $dbTags; - $edited = true; - - foreach (array_diff($oldTags, $suppliedTags) as $tag) - LogHelper::logEvent('post-tag-del', '{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); - foreach (array_diff($suppliedTags, $oldTags) as $tag) - LogHelper::logEvent('post-tag-add', '{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); - } - - - /* thumbnail */ - if (!empty($_FILES['thumb']['name'])) - { - PrivilegesHelper::confirmWithException(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); - $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 . '.custom'; - move_uploaded_file($suppliedFile['tmp_name'], $path); - LogHelper::logEvent('post-edit', '{user} added custom thumb for {post}', ['post' => TextHelper::reprPost($post)]); - } - - - /* source */ - $suppliedSource = InputHelper::get('source'); - if ($suppliedSource !== null and $suppliedSource != $post->source) - { - PrivilegesHelper::confirmWithException(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); - $suppliedSource = Model_Post::validateSource($suppliedSource); - $post->source = $suppliedSource; - $edited = true; - LogHelper::logEvent('post-edit', '{user} changed source for {post} to {source}', ['post' => TextHelper::reprPost($post), 'source' => $post->source]); - } - - - /* relations */ - $suppliedRelations = InputHelper::get('relations'); - if ($suppliedRelations !== null) - { - PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); - $relatedIds = array_filter(preg_split('/\D/', $suppliedRelations)); - $relatedPosts = []; - foreach ($relatedIds as $relatedId) - { - if ($relatedId == $post->id) - continue; - if (count($relatedPosts) > $this->config->browsing->maxRelatedPosts) - throw new SimpleException('Too many related posts (maximum: ' . $this->config->browsing->maxRelatedPosts . ')'); - $relatedPosts []= Model_Post::locate($relatedId); - } - - $oldRelatedIds = array_map(function($post) { return $post->id; }, $post->via('crossref')->sharedPost); - $post->via('crossref')->sharedPost = $relatedPosts; - - foreach (array_diff($oldRelatedIds, $relatedIds) as $post2id) - LogHelper::logEvent('post-relation-del', '{user} removed relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]); - foreach (array_diff($relatedIds, $oldRelatedIds) as $post2id) - LogHelper::logEvent('post-relation-add', '{user} added relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]); - } - - R::store($post); + Model_Post::save($post); Model_Tag::removeUnused(); - LogHelper::flush(); StatusHelper::success(); } } @@ -514,13 +293,12 @@ class PostController public function hideAction($id) { $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); if (InputHelper::get('submit')) { - $post->hidden = true; - R::store($post); + $post->setHidden(true); + Model_Post::save($post); LogHelper::logEvent('post-hide', '{user} hidden {post}', ['post' => TextHelper::reprPost($post)]); StatusHelper::success(); @@ -535,13 +313,12 @@ class PostController public function unhideAction($id) { $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); if (InputHelper::get('submit')) { - $post->hidden = false; - R::store($post); + $post->setHidden(false); + Model_Post::save($post); LogHelper::logEvent('post-unhide', '{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]); StatusHelper::success(); @@ -556,23 +333,11 @@ class PostController public function deleteAction($id) { $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); if (InputHelper::get('submit')) { - //remove stuff from auxiliary tables - R::trashAll(R::find('postscore', 'post_id = ?', [$post->id])); - R::trashAll(R::find('crossref', 'post_id = ? OR post2_id = ?', [$post->id, $post->id])); - foreach ($post->ownComment as $comment) - { - $comment->post = null; - R::store($comment); - } - $post->ownFavoritee = []; - $post->sharedTag = []; - R::store($post); - R::trash($post); + Model_Post::remove($post); LogHelper::logEvent('post-delete', '{user} deleted {post}', ['post' => TextHelper::reprPost($id)]); StatusHelper::success(); @@ -588,7 +353,6 @@ class PostController public function addFavoriteAction($id) { $post = Model_Post::locate($id); - R::preload($post, ['favoritee' => 'user']); PrivilegesHelper::confirmWithException(Privilege::FavoritePost); if (InputHelper::get('submit')) @@ -596,12 +360,8 @@ class PostController if (!$this->context->loggedIn) throw new SimpleException('Not logged in'); - foreach ($post->via('favoritee')->sharedUser as $fav) - if ($fav->id == $this->context->user->id) - throw new SimpleException('Already in favorites'); - - $post->link('favoritee')->user = $this->context->user; - R::store($post); + $this->context->user->addToFavorites($post); + Model_User::save($this->context->user); StatusHelper::success(); } } @@ -613,7 +373,6 @@ class PostController public function remFavoriteAction($id) { $post = Model_Post::locate($id); - R::preload($post, ['favoritee' => 'user']); PrivilegesHelper::confirmWithException(Privilege::FavoritePost); if (InputHelper::get('submit')) @@ -621,16 +380,8 @@ class PostController if (!$this->context->loggedIn) throw new SimpleException('Not logged in'); - $finalKey = null; - foreach ($post->ownFavoritee as $key => $fav) - if ($fav->user->id == $this->context->user->id) - $finalKey = $key; - - if ($finalKey === null) - throw new SimpleException('Not in favorites'); - - unset ($post->ownFavoritee[$finalKey]); - R::store($post); + $this->context->user->remFromFavorites($post); + Model_User::save($this->context->user); StatusHelper::success(); } } @@ -651,15 +402,8 @@ class PostController if (!$this->context->loggedIn) throw new SimpleException('Not logged in'); - $p = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->context->user->id]); - if (!$p) - { - $p = R::dispense('postscore'); - $p->post = $post; - $p->user = $this->context->user; - } - $p->score = $score; - R::store($p); + $this->context->user->score($post, $score); + Model_User::save($this->context->user); StatusHelper::success(); } } @@ -690,7 +434,6 @@ class PostController { $post = Model_Post::locate($id); R::preload($post, [ - 'uploader' => 'user', 'tag', 'comment', 'ownComment.commenter' => 'user']); @@ -728,19 +471,8 @@ class PostController $buildNextPostQuery($nextPostQuery, $id, true); $nextPost = $nextPostQuery->get('row'); - $favorite = false; - $score = null; - if ($this->context->loggedIn) - { - foreach ($post->ownFavoritee as $fav) - if ($fav->user->id == $this->context->user->id) - $favorite = true; - - $s = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->context->user->id]); - if ($s) - $score = intval($s->score); - } - + $favorite = $this->context->user->hasFavorited($post); + $score = $this->context->user->getScore($post); $flagged = in_array(TextHelper::reprPost($post), SessionHelper::get('flagged', [])); $this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $post->name]); @@ -754,7 +486,7 @@ class PostController $this->context->transport->post = $post; $this->context->transport->prevPostId = $prevPost ? $prevPost['id'] : null; $this->context->transport->nextPostId = $nextPost ? $nextPost['id'] : null; - $this->context->transport->tagsToken = self::serializeTags($post); + $this->context->transport->editToken = self::serializePost($post); } @@ -765,98 +497,25 @@ class PostController */ public function thumbAction($name, $width = null, $height = null) { - $dstWidth = $width === null ? $this->config->browsing->thumbWidth : $width; - $dstHeight = $height === null ? $this->config->browsing->thumbHeight : $height; - $dstWidth = min(1000, max(1, $dstWidth)); - $dstHeight = min(1000, max(1, $dstHeight)); - - $this->context->layoutName = 'layout-file'; - - $path = $this->config->main->thumbsPath . DS . $name . '.custom'; - if (!file_exists($path)) - $path = $this->config->main->thumbsPath . DS . $name . '-' . $dstWidth . 'x' . $dstHeight . '.default'; + $path = Model_Post::getThumbCustomPath($name, $width, $height); if (!file_exists($path)) { - $post = Model_Post::locate($name); - - PrivilegesHelper::confirmWithException(Privilege::ListPosts); - PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety)); - $srcPath = $this->config->main->filesPath . DS . $post->name; - - if ($post->type == PostType::Youtube) + $path = Model_Post::getThumbDefaultPath($name, $width, $height); + if (!file_exists($path)) { - $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg'; - $contents = file_get_contents('http://img.youtube.com/vi/' . $post->orig_name . '/mqdefault.jpg'); - file_put_contents($tmpPath, $contents); - if (file_exists($tmpPath)) - $srcImage = imagecreatefromjpeg($tmpPath); + $post = Model_Post::locate($name); + PrivilegesHelper::confirmWithException(Privilege::ListPosts); + PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety)); + $post->makeThumb($width, $height); + if (!file_exists($path)) + $path = $this->config->main->mediaPath . DS . 'img' . DS . 'thumb.jpg'; } - else switch ($post->mime_type) - { - case 'image/jpeg': - $srcImage = imagecreatefromjpeg($srcPath); - break; - case 'image/png': - $srcImage = imagecreatefrompng($srcPath); - break; - case 'image/gif': - $srcImage = imagecreatefromgif($srcPath); - break; - case 'application/x-shockwave-flash': - $srcImage = null; - exec('which dump-gnash', $tmp, $exitCode); - if ($exitCode == 0) - { - $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; - exec('dump-gnash --screenshot last --screenshot-file ' . $tmpPath . ' -1 -r1 --max-advances 15 ' . $srcPath); - if (file_exists($tmpPath)) - $srcImage = imagecreatefrompng($tmpPath); - } - if (!$srcImage) - { - exec('which swfrender', $tmp, $exitCode); - if ($exitCode == 0) - { - $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; - exec('swfrender ' . $srcPath . ' -o ' . $tmpPath); - if (file_exists($tmpPath)) - $srcImage = imagecreatefrompng($tmpPath); - } - } - break; - default: - break; - } - - if (isset($srcImage)) - { - switch ($this->config->browsing->thumbStyle) - { - case 'outside': - $dstImage = ThumbnailHelper::cropOutside($srcImage, $dstWidth, $dstHeight); - break; - case 'inside': - $dstImage = ThumbnailHelper::cropInside($srcImage, $dstWidth, $dstHeight); - break; - default: - throw new SimpleException('Unknown thumbnail crop style'); - } - - imagejpeg($dstImage, $path); - imagedestroy($srcImage); - imagedestroy($dstImage); - } - else - { - $path = $this->config->main->mediaPath . DS . 'img' . DS . 'thumb.jpg'; - } - - if (isset($tmpPath)) - unlink($tmpPath); } + if (!is_readable($path)) throw new SimpleException('Thumbnail file is not readable'); + $this->context->layoutName = 'layout-file'; $this->context->transport->cacheDaysToLive = 30; $this->context->transport->mimeType = 'image/jpeg'; $this->context->transport->fileHash = 'thumb' . md5($name . filemtime($path)); @@ -873,7 +532,6 @@ class PostController { $this->context->layoutName = 'layout-file'; $post = Model_Post::locate($name, true); - R::preload($post, ['tag']); PrivilegesHelper::confirmWithException(Privilege::RetrievePost); PrivilegesHelper::confirmWithException(Privilege::RetrievePost, PostSafety::toString($post->safety)); @@ -902,4 +560,117 @@ class PostController $this->context->transport->fileHash = 'post' . $post->file_hash; $this->context->transport->filePath = $path; } + + + + private function doEdit($post, $isNew) + { + /* file contents */ + if (isset($_FILES['file'])) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $suppliedFile = $_FILES['file']; + self::handleUploadErrors($suppliedFile); + + $srcPath = $suppliedFile['tmp_name']; + $post->setContentFromPath($srcPath); + + if (!$isNew) + LogHelper::logEvent('post-edit', '{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]); + } + elseif (InputHelper::get('url')) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $url = InputHelper::get('url'); + $post->setContentFromUrl($url); + + if (!$isNew) + LogHelper::logEvent('post-edit', '{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]); + } + + /* safety */ + $suppliedSafety = InputHelper::get('safety'); + if ($suppliedSafety !== null) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $oldSafety = $post->safety; + $post->setSafety($suppliedSafety); + $newSafety = $post->safety; + + if ($oldSafety != $newSafety) + LogHelper::logEvent('post-edit', '{user} changed safety for {post} to {safety}', ['post' => TextHelper::reprPost($post), 'safety' => PostSafety::toString($post->safety)]); + } + + /* tags */ + $suppliedTags = InputHelper::get('tags'); + if ($suppliedTags !== null) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $oldTags = array_map(function($tag) { return $tag->name; }, $post->sharedTag); + $post->setTagsFromText($suppliedTags); + $newTags = array_map(function($tag) { return $tag->name; }, $post->sharedTag); + + foreach (array_diff($oldTags, $newTags) as $tag) + LogHelper::logEvent('post-tag-del', '{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); + + foreach (array_diff($newTags, $oldTags) as $tag) + LogHelper::logEvent('post-tag-add', '{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); + } + + /* source */ + $suppliedSource = InputHelper::get('source'); + if ($suppliedSource !== null) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $oldSource = $post->source; + $post->setSource($suppliedSource); + $newSource = $post->source; + + if ($oldSource != $newSource) + LogHelper::logEvent('post-edit', '{user} changed source for {post} to {source}', ['post' => TextHelper::reprPost($post), 'source' => $post->source]); + } + + /* relations */ + $suppliedRelations = InputHelper::get('relations'); + if ($suppliedRelations !== null) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $oldRelatedIds = array_map(function($post) { return $post->id; }, $post->via('crossref')->sharedPost); + $post->setRelationsFromText($suppliedRelations); + $newRelatedIds = array_map(function($post) { return $post->id; }, $post->via('crossref')->sharedPost); + + foreach (array_diff($oldRelatedIds, $newRelatedIds) as $post2id) + LogHelper::logEvent('post-relation-del', '{user} removed relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]); + + foreach (array_diff($newRelatedIds, $oldRelatedIds) as $post2id) + LogHelper::logEvent('post-relation-add', '{user} added relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]); + } + + /* thumbnail */ + if (!empty($_FILES['thumb']['name'])) + { + if (!$isNew) + PrivilegesHelper::confirmWithException(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + + $suppliedFile = $_FILES['thumb']; + self::handleUploadErrors($suppliedFile); + + $srcPath = $suppliedFile['tmp_name']; + $post->setCustomThumbnailFromPath($srcPath); + + LogHelper::logEvent('post-edit', '{user} changed thumb for {post}', ['post' => TextHelper::reprPost($post)]); + } + } } diff --git a/src/Controllers/TagController.php b/src/Controllers/TagController.php index dd17ca4e..880151cf 100644 --- a/src/Controllers/TagController.php +++ b/src/Controllers/TagController.php @@ -41,6 +41,8 @@ class TagController PrivilegesHelper::confirmWithException(Privilege::MergeTags); if (InputHelper::get('submit')) { + Model_Tag::removeUnused(); + $suppliedSourceTag = InputHelper::get('source-tag'); $suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag); $sourceTag = Model_Tag::locate($suppliedSourceTag); @@ -60,9 +62,9 @@ class TagController if ($postTag->id == $sourceTag->id) unset($post->sharedTag[$key]); $post->sharedTag []= $targetTag; - R::store($post); + Model_Post::save($post); } - R::trash($sourceTag); + Model_Tag::remove($sourceTag); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list')); LogHelper::logEvent('tag-merge', '{user} merged {source} with {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]); @@ -83,6 +85,8 @@ class TagController PrivilegesHelper::confirmWithException(Privilege::MergeTags); if (InputHelper::get('submit')) { + Model_Tag::removeUnused(); + $suppliedSourceTag = InputHelper::get('source-tag'); $suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag); $sourceTag = Model_Tag::locate($suppliedSourceTag); @@ -95,7 +99,7 @@ class TagController throw new SimpleException('Target tag already exists'); $sourceTag->name = $suppliedTargetTag; - R::store($sourceTag); + Model_Tag::save($sourceTag); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list')); LogHelper::logEvent('tag-rename', '{user} renamed {source} to {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]); diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php index a2227bea..beb5ed5c 100644 --- a/src/Controllers/UserController.php +++ b/src/Controllers/UserController.php @@ -184,7 +184,7 @@ class UserController if (InputHelper::get('submit')) { $user->banned = true; - R::store($user); + Model_User::save($user); LogHelper::logEvent('ban', '{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]); StatusHelper::success(); @@ -205,7 +205,7 @@ class UserController if (InputHelper::get('submit')) { $user->banned = false; - R::store($user); + Model_User::save($user); LogHelper::logEvent('unban', '{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]); StatusHelper::success(); @@ -225,7 +225,7 @@ class UserController if (InputHelper::get('submit')) { $user->staff_confirmed = true; - R::store($user); + Model_User::save($user); LogHelper::logEvent('reg-accept', '{user} confirmed account for {subject}', ['subject' => TextHelper::reprUser($user)]); StatusHelper::success(); } @@ -257,22 +257,11 @@ class UserController if ($suppliedPasswordHash != $user->pass_hash) throw new SimpleException('Must supply valid password'); } - R::trashAll(R::find('postscore', 'user_id = ?', [$user->id])); - foreach ($user->alias('commenter')->ownComment as $comment) - { - $comment->commenter = null; - R::store($comment); - } - foreach ($user->alias('uploader')->ownPost as $post) - { - $post->uploader = null; - R::store($post); - } - $user->ownFavoritee = []; - if ($user->id == $this->context->user->id) + + $oldId = $user->id; + Model_User::remove($user); + if ($oldId == $this->context->user->id) AuthController::doLogOut(); - R::store($user); - R::trash($user); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index')); LogHelper::logEvent('user-del', '{user} removed account for {subject}', ['subject' => TextHelper::reprUser($name)]); @@ -305,7 +294,7 @@ class UserController $user->enableEndlessScrolling(InputHelper::get('endless-scrolling')); - R::store($user); + Model_User::save($user); if ($user->id == $this->context->user->id) $this->context->user = $user; AuthController::doReLog(); @@ -394,7 +383,7 @@ class UserController if ($suppliedPasswordHash != $currentPasswordHash) throw new SimpleException('Must supply valid current password'); } - R::store($user); + Model_User::save($user); if ($confirmMail) self::sendEmailChangeConfirmation($user); @@ -478,7 +467,7 @@ class UserController AuthController::doReLog(); if (!$this->context->user->anonymous) - R::store($this->context->user); + Model_User::save($this->context->user); StatusHelper::success(); } @@ -523,9 +512,8 @@ class UserController throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.'); //register the user - $dbUser = R::dispense('user'); + $dbUser = Model_User::create(); $dbUser->name = $suppliedName; - $dbUser->pass_salt = md5(mt_rand() . uniqid()); $dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt); $dbUser->email_unconfirmed = $suppliedEmail; @@ -546,7 +534,7 @@ class UserController } //save the user to db if everything went okay - R::store($dbUser); + Model_User::save($dbUser); if (!empty($dbUser->email_unconfirmed)) self::sendEmailChangeConfirmation($dbUser); @@ -589,7 +577,7 @@ class UserController $dbUser->email_unconfirmed = null; $dbToken->used = true; R::store($dbToken); - R::store($dbUser); + Model_User::save($dbUser); LogHelper::logEvent('user-activation', '{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]); $message = 'Activation completed successfully.'; @@ -626,7 +614,7 @@ class UserController $dbUser->pass_hash = Model_User::hashPassword($randomPassword, $dbUser->pass_salt); $dbToken->used = true; R::store($dbToken); - R::store($dbUser); + Model_User::save($dbUser); LogHelper::logEvent('user-pass-reset', '{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]); $message = 'Password reset successful. Your new password is **' . $randomPassword . '**.'; diff --git a/src/Models/AbstractModel.php b/src/Models/AbstractModel.php index 08f46eaa..f066b2c8 100644 --- a/src/Models/AbstractModel.php +++ b/src/Models/AbstractModel.php @@ -50,4 +50,34 @@ abstract class AbstractModel extends RedBean_SimpleModel $dbQuery->from($table); return intval($dbQuery->get('row')['count']); } + + public static function create() + { + return R::dispense(static::getTableName()); + } + + public static function remove($entity) + { + R::trash($entity); + } + + public static function save($entity) + { + R::store($entity); + } + + /* FUSE methods for RedBeanPHP, preventing some aliasing errors */ + public function open() + { + $this->preload(); + } + + public function after_update() + { + $this->preload(); + } + + public function preload() + { + } } diff --git a/src/Models/Model_Comment.php b/src/Models/Model_Comment.php index a1894128..e6bc2847 100644 --- a/src/Models/Model_Comment.php +++ b/src/Models/Model_Comment.php @@ -11,6 +11,13 @@ class Model_Comment extends AbstractModel return 'Model_Comment_QueryBuilder'; } + public function preload() + { + R::preload($this->bean, ['commenter' => 'user', 'post', 'post.uploader' => 'user']); + } + + + public static function locate($key, $throw = true) { $comment = R::findOne(self::getTableName(), 'id = ?', [$key]); @@ -23,6 +30,8 @@ class Model_Comment extends AbstractModel return $comment; } + + public static function validateText($text) { $text = trim($text); diff --git a/src/Models/Model_Post.php b/src/Models/Model_Post.php index 8833bd1f..22229554 100644 --- a/src/Models/Model_Post.php +++ b/src/Models/Model_Post.php @@ -1,6 +1,30 @@ bean, ['uploader' => 'user','favoritee' => 'user']); + } + + + public static function locate($key, $disallowNumeric = false, $throw = true) { if (is_numeric($key) and !$disallowNumeric) @@ -26,6 +50,42 @@ class Model_Post extends AbstractModel return $post; } + public static function create() + { + $post = R::dispense(self::getTableName()); + $post->hidden = false; + $post->upload_date = time(); + do + { + $post->name = md5(mt_rand() . uniqid()); + } + while (file_exists(self::getFullPath($post->name))); + return $post; + } + + public static function remove($post) + { + //remove stuff from auxiliary tables + R::trashAll(R::find('postscore', 'post_id = ?', [$post->id])); + R::trashAll(R::find('crossref', 'post_id = ? OR post2_id = ?', [$post->id, $post->id])); + foreach ($post->ownComment as $comment) + { + $comment->post = null; + R::store($comment); + } + $post->ownFavoritee = []; + $post->sharedTag = []; + R::store($post); + R::trash($post); + } + + public static function save($post) + { + R::store($post); + } + + + public static function validateSafety($safety) { $safety = intval($safety); @@ -47,14 +107,48 @@ class Model_Post extends AbstractModel return $source; } - public static function getTableName() + private static function validateThumbSize($width, $height) { - return 'post'; + $width = $width === null ? self::$config->browsing->thumbWidth : $width; + $height = $height === null ? self::$config->browsing->thumbHeight : $height; + $width = min(1000, max(1, $width)); + $height = min(1000, max(1, $height)); + return [$width, $height]; } - public static function getQueryBuilder() + public static function getAllPostCount() { - return 'Model_Post_QueryBuilder'; + return R::$f + ->begin() + ->select('count(1)') + ->as('count') + ->from(self::getTableName()) + ->get('row')['count']; + } + + private static function getThumbPathTokenized($text, $name, $width = null, $height = null) + { + list ($width, $height) = self::validateThumbSize($width, $height); + + return TextHelper::replaceTokens($text, [ + 'fullpath' => self::$config->main->thumbsPath . DS . $name, + 'width' => $width, + 'height' => $height]); + } + + public static function getThumbCustomPath($name, $width = null, $height = null) + { + return self::getThumbPathTokenized('{fullpath}.custom', $name, $width, $height); + } + + public static function getThumbDefaultPath($name, $width = null, $height = null) + { + return self::getThumbPathTokenized('{fullpath}-{width}x{height}.default', $name, $width, $height); + } + + public static function getFullPath($name) + { + return self::$config->main->filesPath . DS . $name; } public function isTaggedWith($tagName) @@ -65,4 +159,269 @@ class Model_Post extends AbstractModel return true; return false; } + + + + public function setHidden($hidden) + { + $this->hidden = boolval($hidden); + } + + + + public function setSafety($safety) + { + $this->safety = self::validateSafety($safety); + } + + + + public function setSource($source) + { + $this->source = self::validateSource($source); + } + + + + public function setTagsFromText($tagsText) + { + $tagNames = Model_Tag::validateTags($tagsText); + $dbTags = Model_Tag::insertOrUpdate($tagNames); + + $this->sharedTag = $dbTags; + } + + + + public function setRelationsFromText($relationsText) + { + $relatedIds = array_filter(preg_split('/\D/', $relationsText)); + + $relatedPosts = []; + foreach ($relatedIds as $relatedId) + { + if ($relatedId == $this->id) + continue; + + if (count($relatedPosts) > self::$config->browsing->maxRelatedPosts) + throw new SimpleException('Too many related posts (maximum: ' . self::$config->browsing->maxRelatedPosts . ')'); + + $relatedPosts []= self::locate($relatedId); + } + + $this->bean->via('crossref')->sharedPost = $relatedPosts; + } + + + + public function setCustomThumbnailFromPath($srcPath) + { + $mimeType = mime_content_type($srcPath); + if (!in_array($mimeType, ['image/gif', 'image/png', 'image/jpeg'])) + throw new SimpleException('Invalid thumbnail type "' . $mimeType . '"'); + + list ($imageWidth, $imageHeight) = getimagesize($srcPath); + if ($imageWidth != self::$config->browsing->thumbWidth) + throw new SimpleException('Invalid thumbnail width (should be ' . self::$config->browsing->thumbWidth . ')'); + if ($imageWidth != self::$config->browsing->thumbHeight) + throw new SimpleException('Invalid thumbnail width (should be ' . self::$config->browsing->thumbHeight . ')'); + + $dstPath = self::getThumbCustomPath($this->name); + + if (is_uploaded_file($srcPath)) + move_uploaded_file($srcPath, $dstPath); + else + rename($srcPath, $dstPath); + } + + + + public function setContentFromPath($srcPath) + { + $this->mime_type = mime_content_type($srcPath); + switch ($this->mime_type) + { + case 'image/gif': + case 'image/png': + case 'image/jpeg': + list ($imageWidth, $imageHeight) = getimagesize($srcPath); + $this->type = PostType::Image; + $this->image_width = $imageWidth; + $this->image_height = $imageHeight; + break; + case 'application/x-shockwave-flash': + list ($imageWidth, $imageHeight) = getimagesize($srcPath); + $this->type = PostType::Flash; + $this->image_width = $imageWidth; + $this->image_height = $imageHeight; + break; + default: + throw new SimpleException('Invalid file type "' . $this->mime_type . '"'); + } + + $this->orig_name = basename($srcPath); + $this->file_size = filesize($srcPath); + $this->file_hash = md5_file($srcPath); + $duplicatedPost = R::findOne('post', 'file_hash = ?', [$this->file_hash]); + if ($duplicatedPost !== null) + throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); + + $dstPath = $this->getFullPath($this->name); + + if (is_uploaded_file($srcPath)) + move_uploaded_file($srcPath, $dstPath); + else + rename($srcPath, $dstPath); + } + + + + public function setContentFromUrl($srcUrl) + { + $this->orig_name = $srcUrl; + + if (!preg_match('/^https?:\/\//', $srcUrl)) + throw new SimpleException('Invalid URL "' . $srcUrl . '"'); + + if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $srcUrl, $matches)) + { + $origName = $matches[1]; + $this->orig_name = $origName; + $this->type = PostType::Youtube; + $this->mime_type = null; + $this->file_size = null; + $this->file_hash = null; + $this->image_width = null; + $this->image_height = null; + + $duplicatedPost = R::findOne('post', 'orig_name = ?', [$origName]); + if ($duplicatedPost !== null) + throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); + return; + } + + $srcPath = tempnam(sys_get_temp_dir(), 'upload') . '.dat'; + + //warning: low level sh*t ahead + //download the URL $srcUrl into $srcPath + $maxBytes = TextHelper::stripBytesUnits(ini_get('upload_max_filesize')); + set_time_limit(0); + $urlFP = fopen($srcUrl, 'rb'); + if (!$urlFP) + throw new SimpleException('Cannot open URL for reading'); + $srcFP = fopen($srcPath, 'w+b'); + if (!$srcFP) + { + fclose($urlFP); + throw new SimpleException('Cannot open file for writing'); + } + + try + { + while (!feof($urlFP)) + { + $buffer = fread($urlFP, 4 * 1024); + if (fwrite($srcFP, $buffer) === false) + throw new SimpleException('Cannot write into file'); + fflush($srcFP); + if (ftell($srcFP) > $maxBytes) + throw new SimpleException('File is too big (maximum allowed size: ' . TextHelper::useBytesUnits($maxBytes) . ')'); + } + } + finally + { + fclose($urlFP); + fclose($srcFP); + } + + try + { + $this->setContentFromPath($srcPath); + } + finally + { + if (file_exists($srcPath)) + unlink($srcPath); + } + } + + + + public function makeThumb($width = null, $height = null) + { + list ($width, $height) = self::validateThumbSize($width, $height); + $dstPath = self::getThumbDefaultPath($this->name, $width, $height); + $srcPath = self::getFullPath($this->name); + + if ($this->type == PostType::Youtube) + { + $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg'; + $contents = file_get_contents('http://img.youtube.com/vi/' . $this->orig_name . '/mqdefault.jpg'); + file_put_contents($tmpPath, $contents); + if (file_exists($tmpPath)) + $srcImage = imagecreatefromjpeg($tmpPath); + } + else switch ($this->mime_type) + { + case 'image/jpeg': + $srcImage = imagecreatefromjpeg($srcPath); + break; + case 'image/png': + $srcImage = imagecreatefrompng($srcPath); + break; + case 'image/gif': + $srcImage = imagecreatefromgif($srcPath); + break; + case 'application/x-shockwave-flash': + $srcImage = null; + exec('which dump-gnash', $tmp, $exitCode); + if ($exitCode == 0) + { + $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; + exec('dump-gnash --screenshot last --screenshot-file ' . $tmpPath . ' -1 -r1 --max-advances 15 ' . $srcPath); + if (file_exists($tmpPath)) + $srcImage = imagecreatefrompng($tmpPath); + } + if (!$srcImage) + { + exec('which swfrender', $tmp, $exitCode); + if ($exitCode == 0) + { + $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; + exec('swfrender ' . $srcPath . ' -o ' . $tmpPath); + if (file_exists($tmpPath)) + $srcImage = imagecreatefrompng($tmpPath); + } + } + break; + default: + break; + } + + if (isset($tmpPath)) + unlink($tmpPath); + + if (!isset($srcImage)) + return false; + + switch (self::$config->browsing->thumbStyle) + { + case 'outside': + $dstImage = ThumbnailHelper::cropOutside($srcImage, $width, $height); + break; + case 'inside': + $dstImage = ThumbnailHelper::cropInside($srcImage, $width, $height); + break; + default: + throw new SimpleException('Unknown thumbnail crop style'); + } + + imagejpeg($dstImage, $dstPath); + imagedestroy($srcImage); + imagedestroy($dstImage); + + return true; + } } + +Model_Post::initModel(); diff --git a/src/Models/Model_Tag.php b/src/Models/Model_Tag.php index e42dbac9..5f77b1a1 100644 --- a/src/Models/Model_Tag.php +++ b/src/Models/Model_Tag.php @@ -1,6 +1,18 @@ bean->getMeta('post_count')) - return $this->bean->getMeta('post_count'); - return $this->bean->countShared('post'); - } - - public static function validateTags($tags) { $tags = trim($tags); @@ -92,13 +98,10 @@ class Model_Tag extends AbstractModel return $tags; } - public static function getTableName() + public function getPostCount() { - return 'tag'; - } - - public static function getQueryBuilder() - { - return 'Model_Tag_Querybuilder'; + if ($this->bean->getMeta('post_count')) + return $this->bean->getMeta('post_count'); + return $this->bean->countShared('post'); } } diff --git a/src/Models/Model_User.php b/src/Models/Model_User.php index 59c47266..37940aa9 100644 --- a/src/Models/Model_User.php +++ b/src/Models/Model_User.php @@ -1,6 +1,23 @@ email_confirmed) - ? $this->email_confirmed - : $this->pass_salt . $this->name; - $hash = md5(strtolower(trim($subject))); - $url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro'; - return $url; + $user = R::dispense(self::getTableName()); + $user->pass_salt = md5(mt_rand() . uniqid()); + return $user; } - public function getSetting($key) + public static function remove($user) { - $settings = json_decode($this->settings, true); - return isset($settings[$key]) - ? $settings[$key] - : null; - } - - public function setSetting($key, $value) - { - $settings = json_decode($this->settings, true); - $settings[$key] = $value; - $settings = json_encode($settings); - if (strlen($settings) > 200) - throw new SimpleException('Too much data'); - $this->settings = $settings; - } - - - const SETTING_SAFETY = 1; - const SETTING_ENDLESS_SCROLLING = 2; - - public function hasEnabledSafety($safety) - { - $all = $this->getSetting(self::SETTING_SAFETY); - if (!$all) - return $safety == PostSafety::Safe; - return $all & PostSafety::toFlag($safety); - } - - public function enableSafety($safety, $enabled) - { - $all = $this->getSetting(self::SETTING_SAFETY); - if (!$all) - $all = PostSafety::toFlag(PostSafety::Safe); - - $new = $all; - if (!$enabled) + //remove stuff from auxiliary tables + R::trashAll(R::find('postscore', 'user_id = ?', [$user->id])); + foreach ($user->alias('commenter')->ownComment as $comment) { - $new &= ~PostSafety::toFlag($safety); - if (!$new) - $new = PostSafety::toFlag(PostSafety::Safe); + $comment->commenter = null; + R::store($comment); } - else + foreach ($user->alias('uploader')->ownPost as $post) { - $new |= PostSafety::toFlag($safety); + $post->uploader = null; + R::store($post); } - - $this->setSetting(self::SETTING_SAFETY, $new); + $user->ownFavoritee = []; + R::store($user); + R::trash($user); } - public function hasEnabledEndlessScrolling() + public static function save($user) { - $ret = $this->getSetting(self::SETTING_ENDLESS_SCROLLING); - if ($ret === null) - $ret = \Chibi\Registry::getConfig()->browsing->endlessScrollingDefault; - return $ret; + R::store($user); } - public function enableEndlessScrolling($enabled) + + + public static function getAnonymousName() { - $this->setSetting(self::SETTING_ENDLESS_SCROLLING, $enabled ? 1 : 0); + return '[Anonymous user]'; } - - public static function validateUserName($userName) { $userName = trim($userName); @@ -168,13 +147,126 @@ class Model_User extends AbstractModel return sha1($salt1 . $salt2 . $pass); } - public static function getTableName() + + + public function getAvatarUrl($size = 32) { - return 'user'; + $subject = !empty($this->email_confirmed) + ? $this->email_confirmed + : $this->pass_salt . $this->name; + $hash = md5(strtolower(trim($subject))); + $url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro'; + return $url; } - public static function getQueryBuilder() + public function getSetting($key) { - return 'Model_User_QueryBuilder'; + $settings = json_decode($this->settings, true); + return isset($settings[$key]) + ? $settings[$key] + : null; + } + + public function setSetting($key, $value) + { + $settings = json_decode($this->settings, true); + $settings[$key] = $value; + $settings = json_encode($settings); + if (strlen($settings) > 200) + throw new SimpleException('Too much data'); + $this->settings = $settings; + } + + public function hasEnabledSafety($safety) + { + $all = $this->getSetting(self::SETTING_SAFETY); + if (!$all) + return $safety == PostSafety::Safe; + return $all & PostSafety::toFlag($safety); + } + + public function enableSafety($safety, $enabled) + { + $all = $this->getSetting(self::SETTING_SAFETY); + if (!$all) + $all = PostSafety::toFlag(PostSafety::Safe); + + $new = $all; + if (!$enabled) + { + $new &= ~PostSafety::toFlag($safety); + if (!$new) + $new = PostSafety::toFlag(PostSafety::Safe); + } + else + { + $new |= PostSafety::toFlag($safety); + } + + $this->setSetting(self::SETTING_SAFETY, $new); + } + + public function hasEnabledEndlessScrolling() + { + $ret = $this->getSetting(self::SETTING_ENDLESS_SCROLLING); + if ($ret === null) + $ret = \Chibi\Registry::getConfig()->browsing->endlessScrollingDefault; + return $ret; + } + + public function enableEndlessScrolling($enabled) + { + $this->setSetting(self::SETTING_ENDLESS_SCROLLING, $enabled ? 1 : 0); + } + + public function hasFavorited($post) + { + foreach ($this->bean->ownFavoritee as $fav) + if ($fav->post->id == $post->id) + return true; + return false; + } + + public function getScore($post) + { + $s = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->id]); + if ($s) + return intval($s->score); + return null; + } + + public function addToFavorites($post) + { + R::preload($this->bean, ['favoritee' => 'post']); + foreach ($this->bean->ownFavoritee as $fav) + if ($fav->post_id == $post->id) + throw new SimpleException('Already in favorites'); + + $this->bean->link('favoritee')->post = $post; + } + + public function remFromFavorites($post) + { + $finalKey = null; + foreach ($this->bean->ownFavoritee as $key => $fav) + if ($fav->post_id == $post->id) + $finalKey = $key; + + if ($finalKey === null) + throw new SimpleException('Not in favorites'); + + unset($this->bean->ownFavoritee[$finalKey]); + } + + public function score($post, $score) + { + R::trashAll(R::find('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->id])); + $score = intval($score); + if ($score != 0) + { + $p = $this->bean->link('postscore'); + $p->post = $post; + $p->score = $score; + } } } diff --git a/src/Models/Privilege.php b/src/Models/Privilege.php index 759a2c1d..30a8a92e 100644 --- a/src/Models/Privilege.php +++ b/src/Models/Privilege.php @@ -11,6 +11,7 @@ class Privilege extends Enum const EditPostThumb = 8; const EditPostSource = 26; const EditPostRelations = 30; + const EditPostFile = 36; const HidePost = 9; const DeletePost = 10; const FeaturePost = 25; diff --git a/src/Views/post-edit.phtml b/src/Views/post-edit.phtml index b119987e..3090cdc5 100644 --- a/src/Views/post-edit.phtml +++ b/src/Views/post-edit.phtml @@ -17,7 +17,7 @@
- + context->transport->post->uploader))): ?> diff --git a/src/core.php b/src/core.php index 241ed27f..7d497dd6 100644 --- a/src/core.php +++ b/src/core.php @@ -11,7 +11,7 @@ setlocale(LC_CTYPE, 'en_US.UTF-8'); ini_set('memory_limit', '128M'); //extension sanity checks -$requiredExtensions = ['pdo', 'pdo_sqlite', 'gd', 'openssl']; +$requiredExtensions = ['pdo', 'pdo_sqlite', 'gd', 'openssl', 'fileinfo']; foreach ($requiredExtensions as $ext) if (!extension_loaded($ext)) die('PHP extension "' . $ext . '" must be enabled to continue.' . PHP_EOL);