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
This commit is contained in:
Marcin Kurczewski 2013-11-22 21:20:56 +01:00
parent 0a5169a7d6
commit d8997edc57
14 changed files with 785 additions and 527 deletions

View file

@ -90,6 +90,8 @@ editPostThumb=moderator
editPostSource=moderator editPostSource=moderator
editPostRelations.own=registered editPostRelations.own=registered
editPostRelations.all=moderator editPostRelations.all=moderator
editPostFile.all=moderator
editPostFile.own=moderator
hidePost.own=moderator hidePost.own=moderator
hidePost.all=moderator hidePost.all=moderator
deletePost.own=moderator deletePost.own=moderator

View file

@ -25,7 +25,6 @@ class CommentController
$page = max(1, min($pageCount, $page)); $page = max(1, min($pageCount, $page));
$comments = Model_Comment::getEntities(null, $commentsPerPage, $page); $comments = Model_Comment::getEntities(null, $commentsPerPage, $page);
R::preload($comments, ['commenter' => 'user', 'post', 'post.uploader' => 'user']);
$this->context->postGroups = true; $this->context->postGroups = true;
$this->context->transport->paginator = new StdClass; $this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page; $this->context->transport->paginator->page = $page;
@ -55,7 +54,7 @@ class CommentController
$text = InputHelper::get('text'); $text = InputHelper::get('text');
$text = Model_Comment::validateText($text); $text = Model_Comment::validateText($text);
$comment = R::dispense('comment'); $comment = Model_Comment::create();
$comment->post = $post; $comment->post = $post;
if ($this->context->loggedIn) if ($this->context->loggedIn)
$comment->commenter = $this->context->user; $comment->commenter = $this->context->user;
@ -63,7 +62,7 @@ class CommentController
$comment->text = $text; $comment->text = $text;
if (InputHelper::get('sender') != 'preview') 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)]); LogHelper::logEvent('comment-add', '{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
} }
$this->context->transport->textPreview = $comment->getText(); $this->context->transport->textPreview = $comment->getText();
@ -80,10 +79,10 @@ class CommentController
public function deleteAction($id) public function deleteAction($id)
{ {
$comment = Model_Comment::locate($id); $comment = Model_Comment::locate($id);
R::preload($comment, ['commenter' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->commenter)); 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)]); LogHelper::logEvent('comment-del', '{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->post)]);
R::trash($comment);
StatusHelper::success(); StatusHelper::success();
} }
} }

View file

@ -9,7 +9,7 @@ class IndexController
{ {
$this->context->subTitle = 'home'; $this->context->subTitle = 'home';
$this->context->stylesheets []= 'index-index.css'; $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; $featuredPostRotationTime = $this->config->misc->featuredPostMaxDays * 24 * 3600;

View file

@ -8,13 +8,18 @@ class PostController
$callback(); $callback();
} }
private static function serializeTags($post) private static function serializePost($post)
{ {
$x = []; $x = [];
foreach ($post->sharedTag as $tag) 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); natcasesort($x);
$x = join('', $x); $x = join(' ', $x);
return md5($x); return md5($x);
} }
@ -120,7 +125,6 @@ class PostController
public function toggleTagAction($id, $tag) public function toggleTagAction($id, $tag)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['uploader' => 'user']);
$this->context->transport->post = $post; $this->context->transport->post = $post;
$tagRow = Model_Tag::locate($tag, false); $tagRow = Model_Tag::locate($tag, false);
@ -146,7 +150,7 @@ class PostController
$dbTags = Model_Tag::insertOrUpdate($tags); $dbTags = Model_Tag::insertOrUpdate($tags);
$post->sharedTag = $dbTags; $post->sharedTag = $dbTags;
R::store($post); Model_Post::save($post);
StatusHelper::success(); StatusHelper::success();
} }
} }
@ -192,173 +196,38 @@ class PostController
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
/* file contents */ R::transaction(function()
if (isset($_FILES['file']))
{ {
$suppliedFile = $_FILES['file']; $post = Model_Post::create();
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 . '"');
if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $url, $matches)) //basic stuff
{
$origName = $matches[1];
$postType = PostType::Youtube;
$sourcePath = null;
}
else
{
$sourcePath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
//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);
}
}
}
/* 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 . '"');
}
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);
}
}
do
{
$name = md5(mt_rand() . uniqid());
$path = $this->config->main->filesPath . DS . $name;
}
while (file_exists($path));
/* 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'); $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) if ($this->context->loggedIn and !$anonymous)
$dbPost->uploader = $this->context->user; $post->uploader = $this->context->user;
$dbPost->ownFavoritee = [];
$dbPost->sharedTag = $dbTags;
if ($sourcePath) //store the post to get the ID in the logs
{ Model_Post::save($post);
if (is_uploaded_file($sourcePath))
move_uploaded_file($sourcePath, $path);
else
rename($sourcePath, $path);
}
R::store($dbPost);
//log
LogHelper::bufferChanges();
$fmt = ($anonymous and !$this->config->misc->logAnonymousUploads) $fmt = ($anonymous and !$this->config->misc->logAnonymousUploads)
? 'someone' ? 'someone'
: '{user}'; : '{user}';
$fmt .= ' added {post} tagged with {tags} marked as {safety}'; $fmt .= ' added {post}';
LogHelper::logEvent('post-new', $fmt, [ LogHelper::logEvent('post-new', $fmt, ['post' => TextHelper::reprPost($post)]);
'post' => TextHelper::reprPost($dbPost),
'tags' => join(', ', array_map(['TextHelper', 'reprTag'], $dbTags)), //after logging basic info, do the editing stuff
'safety' => PostSafety::toString($dbPost->safety)]); $this->doEdit($post, true);
//this basically means that user didn't specify file nor url
if (empty($post->type))
throw new SimpleException('No post type detected; upload faled');
LogHelper::flush();
//finish
Model_Post::save($post);
});
StatusHelper::success(); StatusHelper::success();
} }
@ -372,111 +241,21 @@ class PostController
public function editAction($id) public function editAction($id)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['uploader' => 'user']);
$this->context->transport->post = $post; $this->context->transport->post = $post;
if (InputHelper::get('submit')) 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(); LogHelper::bufferChanges();
$this->doEdit($post, false);
LogHelper::flush();
/* safety */ Model_Post::save($post);
$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_Tag::removeUnused(); Model_Tag::removeUnused();
LogHelper::flush();
StatusHelper::success(); StatusHelper::success();
} }
} }
@ -514,13 +293,12 @@ class PostController
public function hideAction($id) public function hideAction($id)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['uploader' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$post->hidden = true; $post->setHidden(true);
R::store($post); Model_Post::save($post);
LogHelper::logEvent('post-hide', '{user} hidden {post}', ['post' => TextHelper::reprPost($post)]); LogHelper::logEvent('post-hide', '{user} hidden {post}', ['post' => TextHelper::reprPost($post)]);
StatusHelper::success(); StatusHelper::success();
@ -535,13 +313,12 @@ class PostController
public function unhideAction($id) public function unhideAction($id)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['uploader' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$post->hidden = false; $post->setHidden(false);
R::store($post); Model_Post::save($post);
LogHelper::logEvent('post-unhide', '{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]); LogHelper::logEvent('post-unhide', '{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]);
StatusHelper::success(); StatusHelper::success();
@ -556,23 +333,11 @@ class PostController
public function deleteAction($id) public function deleteAction($id)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['uploader' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
//remove stuff from auxiliary tables Model_Post::remove($post);
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);
LogHelper::logEvent('post-delete', '{user} deleted {post}', ['post' => TextHelper::reprPost($id)]); LogHelper::logEvent('post-delete', '{user} deleted {post}', ['post' => TextHelper::reprPost($id)]);
StatusHelper::success(); StatusHelper::success();
@ -588,7 +353,6 @@ class PostController
public function addFavoriteAction($id) public function addFavoriteAction($id)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['favoritee' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost); PrivilegesHelper::confirmWithException(Privilege::FavoritePost);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
@ -596,12 +360,8 @@ class PostController
if (!$this->context->loggedIn) if (!$this->context->loggedIn)
throw new SimpleException('Not logged in'); throw new SimpleException('Not logged in');
foreach ($post->via('favoritee')->sharedUser as $fav) $this->context->user->addToFavorites($post);
if ($fav->id == $this->context->user->id) Model_User::save($this->context->user);
throw new SimpleException('Already in favorites');
$post->link('favoritee')->user = $this->context->user;
R::store($post);
StatusHelper::success(); StatusHelper::success();
} }
} }
@ -613,7 +373,6 @@ class PostController
public function remFavoriteAction($id) public function remFavoriteAction($id)
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, ['favoritee' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost); PrivilegesHelper::confirmWithException(Privilege::FavoritePost);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
@ -621,16 +380,8 @@ class PostController
if (!$this->context->loggedIn) if (!$this->context->loggedIn)
throw new SimpleException('Not logged in'); throw new SimpleException('Not logged in');
$finalKey = null; $this->context->user->remFromFavorites($post);
foreach ($post->ownFavoritee as $key => $fav) Model_User::save($this->context->user);
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);
StatusHelper::success(); StatusHelper::success();
} }
} }
@ -651,15 +402,8 @@ class PostController
if (!$this->context->loggedIn) if (!$this->context->loggedIn)
throw new SimpleException('Not logged in'); throw new SimpleException('Not logged in');
$p = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->context->user->id]); $this->context->user->score($post, $score);
if (!$p) Model_User::save($this->context->user);
{
$p = R::dispense('postscore');
$p->post = $post;
$p->user = $this->context->user;
}
$p->score = $score;
R::store($p);
StatusHelper::success(); StatusHelper::success();
} }
} }
@ -690,7 +434,6 @@ class PostController
{ {
$post = Model_Post::locate($id); $post = Model_Post::locate($id);
R::preload($post, [ R::preload($post, [
'uploader' => 'user',
'tag', 'tag',
'comment', 'comment',
'ownComment.commenter' => 'user']); 'ownComment.commenter' => 'user']);
@ -728,19 +471,8 @@ class PostController
$buildNextPostQuery($nextPostQuery, $id, true); $buildNextPostQuery($nextPostQuery, $id, true);
$nextPost = $nextPostQuery->get('row'); $nextPost = $nextPostQuery->get('row');
$favorite = false; $favorite = $this->context->user->hasFavorited($post);
$score = null; $score = $this->context->user->getScore($post);
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);
}
$flagged = in_array(TextHelper::reprPost($post), SessionHelper::get('flagged', [])); $flagged = in_array(TextHelper::reprPost($post), SessionHelper::get('flagged', []));
$this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $post->name]); $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->post = $post;
$this->context->transport->prevPostId = $prevPost ? $prevPost['id'] : null; $this->context->transport->prevPostId = $prevPost ? $prevPost['id'] : null;
$this->context->transport->nextPostId = $nextPost ? $nextPost['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) public function thumbAction($name, $width = null, $height = null)
{ {
$dstWidth = $width === null ? $this->config->browsing->thumbWidth : $width; $path = Model_Post::getThumbCustomPath($name, $width, $height);
$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)) if (!file_exists($path))
$path = $this->config->main->thumbsPath . DS . $name . '-' . $dstWidth . 'x' . $dstHeight . '.default'; {
$path = Model_Post::getThumbDefaultPath($name, $width, $height);
if (!file_exists($path)) if (!file_exists($path))
{ {
$post = Model_Post::locate($name); $post = Model_Post::locate($name);
PrivilegesHelper::confirmWithException(Privilege::ListPosts); PrivilegesHelper::confirmWithException(Privilege::ListPosts);
PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety)); PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety));
$srcPath = $this->config->main->filesPath . DS . $post->name; $post->makeThumb($width, $height);
if (!file_exists($path))
if ($post->type == PostType::Youtube)
{
$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);
}
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'; $path = $this->config->main->mediaPath . DS . 'img' . DS . 'thumb.jpg';
} }
if (isset($tmpPath))
unlink($tmpPath);
} }
if (!is_readable($path)) if (!is_readable($path))
throw new SimpleException('Thumbnail file is not readable'); throw new SimpleException('Thumbnail file is not readable');
$this->context->layoutName = 'layout-file';
$this->context->transport->cacheDaysToLive = 30; $this->context->transport->cacheDaysToLive = 30;
$this->context->transport->mimeType = 'image/jpeg'; $this->context->transport->mimeType = 'image/jpeg';
$this->context->transport->fileHash = 'thumb' . md5($name . filemtime($path)); $this->context->transport->fileHash = 'thumb' . md5($name . filemtime($path));
@ -873,7 +532,6 @@ class PostController
{ {
$this->context->layoutName = 'layout-file'; $this->context->layoutName = 'layout-file';
$post = Model_Post::locate($name, true); $post = Model_Post::locate($name, true);
R::preload($post, ['tag']);
PrivilegesHelper::confirmWithException(Privilege::RetrievePost); PrivilegesHelper::confirmWithException(Privilege::RetrievePost);
PrivilegesHelper::confirmWithException(Privilege::RetrievePost, PostSafety::toString($post->safety)); 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->fileHash = 'post' . $post->file_hash;
$this->context->transport->filePath = $path; $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)]);
}
}
} }

View file

@ -41,6 +41,8 @@ class TagController
PrivilegesHelper::confirmWithException(Privilege::MergeTags); PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
Model_Tag::removeUnused();
$suppliedSourceTag = InputHelper::get('source-tag'); $suppliedSourceTag = InputHelper::get('source-tag');
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag); $suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
$sourceTag = Model_Tag::locate($suppliedSourceTag); $sourceTag = Model_Tag::locate($suppliedSourceTag);
@ -60,9 +62,9 @@ class TagController
if ($postTag->id == $sourceTag->id) if ($postTag->id == $sourceTag->id)
unset($post->sharedTag[$key]); unset($post->sharedTag[$key]);
$post->sharedTag []= $targetTag; $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')); \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)]); 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); PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
Model_Tag::removeUnused();
$suppliedSourceTag = InputHelper::get('source-tag'); $suppliedSourceTag = InputHelper::get('source-tag');
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag); $suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
$sourceTag = Model_Tag::locate($suppliedSourceTag); $sourceTag = Model_Tag::locate($suppliedSourceTag);
@ -95,7 +99,7 @@ class TagController
throw new SimpleException('Target tag already exists'); throw new SimpleException('Target tag already exists');
$sourceTag->name = $suppliedTargetTag; $sourceTag->name = $suppliedTargetTag;
R::store($sourceTag); Model_Tag::save($sourceTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list')); \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)]); LogHelper::logEvent('tag-rename', '{user} renamed {source} to {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]);

View file

@ -184,7 +184,7 @@ class UserController
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$user->banned = true; $user->banned = true;
R::store($user); Model_User::save($user);
LogHelper::logEvent('ban', '{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]); LogHelper::logEvent('ban', '{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success(); StatusHelper::success();
@ -205,7 +205,7 @@ class UserController
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$user->banned = false; $user->banned = false;
R::store($user); Model_User::save($user);
LogHelper::logEvent('unban', '{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]); LogHelper::logEvent('unban', '{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success(); StatusHelper::success();
@ -225,7 +225,7 @@ class UserController
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$user->staff_confirmed = true; $user->staff_confirmed = true;
R::store($user); Model_User::save($user);
LogHelper::logEvent('reg-accept', '{user} confirmed account for {subject}', ['subject' => TextHelper::reprUser($user)]); LogHelper::logEvent('reg-accept', '{user} confirmed account for {subject}', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success(); StatusHelper::success();
} }
@ -257,22 +257,11 @@ class UserController
if ($suppliedPasswordHash != $user->pass_hash) if ($suppliedPasswordHash != $user->pass_hash)
throw new SimpleException('Must supply valid password'); throw new SimpleException('Must supply valid password');
} }
R::trashAll(R::find('postscore', 'user_id = ?', [$user->id]));
foreach ($user->alias('commenter')->ownComment as $comment) $oldId = $user->id;
{ Model_User::remove($user);
$comment->commenter = null; if ($oldId == $this->context->user->id)
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)
AuthController::doLogOut(); AuthController::doLogOut();
R::store($user);
R::trash($user);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index')); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
LogHelper::logEvent('user-del', '{user} removed account for {subject}', ['subject' => TextHelper::reprUser($name)]); 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')); $user->enableEndlessScrolling(InputHelper::get('endless-scrolling'));
R::store($user); Model_User::save($user);
if ($user->id == $this->context->user->id) if ($user->id == $this->context->user->id)
$this->context->user = $user; $this->context->user = $user;
AuthController::doReLog(); AuthController::doReLog();
@ -394,7 +383,7 @@ class UserController
if ($suppliedPasswordHash != $currentPasswordHash) if ($suppliedPasswordHash != $currentPasswordHash)
throw new SimpleException('Must supply valid current password'); throw new SimpleException('Must supply valid current password');
} }
R::store($user); Model_User::save($user);
if ($confirmMail) if ($confirmMail)
self::sendEmailChangeConfirmation($user); self::sendEmailChangeConfirmation($user);
@ -478,7 +467,7 @@ class UserController
AuthController::doReLog(); AuthController::doReLog();
if (!$this->context->user->anonymous) if (!$this->context->user->anonymous)
R::store($this->context->user); Model_User::save($this->context->user);
StatusHelper::success(); StatusHelper::success();
} }
@ -523,9 +512,8 @@ class UserController
throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.'); throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.');
//register the user //register the user
$dbUser = R::dispense('user'); $dbUser = Model_User::create();
$dbUser->name = $suppliedName; $dbUser->name = $suppliedName;
$dbUser->pass_salt = md5(mt_rand() . uniqid());
$dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt); $dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt);
$dbUser->email_unconfirmed = $suppliedEmail; $dbUser->email_unconfirmed = $suppliedEmail;
@ -546,7 +534,7 @@ class UserController
} }
//save the user to db if everything went okay //save the user to db if everything went okay
R::store($dbUser); Model_User::save($dbUser);
if (!empty($dbUser->email_unconfirmed)) if (!empty($dbUser->email_unconfirmed))
self::sendEmailChangeConfirmation($dbUser); self::sendEmailChangeConfirmation($dbUser);
@ -589,7 +577,7 @@ class UserController
$dbUser->email_unconfirmed = null; $dbUser->email_unconfirmed = null;
$dbToken->used = true; $dbToken->used = true;
R::store($dbToken); R::store($dbToken);
R::store($dbUser); Model_User::save($dbUser);
LogHelper::logEvent('user-activation', '{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]); LogHelper::logEvent('user-activation', '{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]);
$message = 'Activation completed successfully.'; $message = 'Activation completed successfully.';
@ -626,7 +614,7 @@ class UserController
$dbUser->pass_hash = Model_User::hashPassword($randomPassword, $dbUser->pass_salt); $dbUser->pass_hash = Model_User::hashPassword($randomPassword, $dbUser->pass_salt);
$dbToken->used = true; $dbToken->used = true;
R::store($dbToken); R::store($dbToken);
R::store($dbUser); Model_User::save($dbUser);
LogHelper::logEvent('user-pass-reset', '{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]); LogHelper::logEvent('user-pass-reset', '{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]);
$message = 'Password reset successful. Your new password is **' . $randomPassword . '**.'; $message = 'Password reset successful. Your new password is **' . $randomPassword . '**.';

View file

@ -50,4 +50,34 @@ abstract class AbstractModel extends RedBean_SimpleModel
$dbQuery->from($table); $dbQuery->from($table);
return intval($dbQuery->get('row')['count']); 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()
{
}
} }

View file

@ -11,6 +11,13 @@ class Model_Comment extends AbstractModel
return 'Model_Comment_QueryBuilder'; return 'Model_Comment_QueryBuilder';
} }
public function preload()
{
R::preload($this->bean, ['commenter' => 'user', 'post', 'post.uploader' => 'user']);
}
public static function locate($key, $throw = true) public static function locate($key, $throw = true)
{ {
$comment = R::findOne(self::getTableName(), 'id = ?', [$key]); $comment = R::findOne(self::getTableName(), 'id = ?', [$key]);
@ -23,6 +30,8 @@ class Model_Comment extends AbstractModel
return $comment; return $comment;
} }
public static function validateText($text) public static function validateText($text)
{ {
$text = trim($text); $text = trim($text);

View file

@ -1,6 +1,30 @@
<?php <?php
class Model_Post extends AbstractModel class Model_Post extends AbstractModel
{ {
protected static $config;
public static function initModel()
{
self::$config = \Chibi\Registry::getConfig();
}
public static function getTableName()
{
return 'post';
}
public static function getQueryBuilder()
{
return 'Model_Post_QueryBuilder';
}
public function preload()
{
R::preload($this->bean, ['uploader' => 'user','favoritee' => 'user']);
}
public static function locate($key, $disallowNumeric = false, $throw = true) public static function locate($key, $disallowNumeric = false, $throw = true)
{ {
if (is_numeric($key) and !$disallowNumeric) if (is_numeric($key) and !$disallowNumeric)
@ -26,6 +50,42 @@ class Model_Post extends AbstractModel
return $post; 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) public static function validateSafety($safety)
{ {
$safety = intval($safety); $safety = intval($safety);
@ -47,14 +107,48 @@ class Model_Post extends AbstractModel
return $source; 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) public function isTaggedWith($tagName)
@ -65,4 +159,269 @@ class Model_Post extends AbstractModel
return true; return true;
return false; 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();

View file

@ -1,6 +1,18 @@
<?php <?php
class Model_Tag extends AbstractModel class Model_Tag extends AbstractModel
{ {
public static function getTableName()
{
return 'tag';
}
public static function getQueryBuilder()
{
return 'Model_Tag_Querybuilder';
}
public static function locate($key, $throw = true) public static function locate($key, $throw = true)
{ {
$tag = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]); $tag = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]);
@ -13,6 +25,8 @@ class Model_Tag extends AbstractModel
return $tag; return $tag;
} }
public static function removeUnused() public static function removeUnused()
{ {
$dbQuery = R::$f $dbQuery = R::$f
@ -68,14 +82,6 @@ class Model_Tag extends AbstractModel
return $tag; return $tag;
} }
public function getPostCount()
{
if ($this->bean->getMeta('post_count'))
return $this->bean->getMeta('post_count');
return $this->bean->countShared('post');
}
public static function validateTags($tags) public static function validateTags($tags)
{ {
$tags = trim($tags); $tags = trim($tags);
@ -92,13 +98,10 @@ class Model_Tag extends AbstractModel
return $tags; return $tags;
} }
public static function getTableName() public function getPostCount()
{ {
return 'tag'; if ($this->bean->getMeta('post_count'))
} return $this->bean->getMeta('post_count');
return $this->bean->countShared('post');
public static function getQueryBuilder()
{
return 'Model_Tag_Querybuilder';
} }
} }

View file

@ -1,6 +1,23 @@
<?php <?php
class Model_User extends AbstractModel class Model_User extends AbstractModel
{ {
const SETTING_SAFETY = 1;
const SETTING_ENDLESS_SCROLLING = 2;
public static function getTableName()
{
return 'user';
}
public static function getQueryBuilder()
{
return 'Model_User_QueryBuilder';
}
public static function locate($key, $throw = true) public static function locate($key, $throw = true)
{ {
$user = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]); $user = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]);
@ -17,82 +34,44 @@ class Model_User extends AbstractModel
return null; return null;
} }
public function getAvatarUrl($size = 32) public static function create()
{ {
$subject = !empty($this->email_confirmed) $user = R::dispense(self::getTableName());
? $this->email_confirmed $user->pass_salt = md5(mt_rand() . uniqid());
: $this->pass_salt . $this->name; return $user;
$hash = md5(strtolower(trim($subject)));
$url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro';
return $url;
} }
public function getSetting($key) public static function remove($user)
{ {
$settings = json_decode($this->settings, true); //remove stuff from auxiliary tables
return isset($settings[$key]) R::trashAll(R::find('postscore', 'user_id = ?', [$user->id]));
? $settings[$key] foreach ($user->alias('commenter')->ownComment as $comment)
: null; {
$comment->commenter = null;
R::store($comment);
}
foreach ($user->alias('uploader')->ownPost as $post)
{
$post->uploader = null;
R::store($post);
}
$user->ownFavoritee = [];
R::store($user);
R::trash($user);
} }
public function setSetting($key, $value) public static function save($user)
{ {
$settings = json_decode($this->settings, true); R::store($user);
$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) public static function getAnonymousName()
{ {
$all = $this->getSetting(self::SETTING_SAFETY); return '[Anonymous user]';
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 static function validateUserName($userName) public static function validateUserName($userName)
{ {
$userName = trim($userName); $userName = trim($userName);
@ -168,13 +147,126 @@ class Model_User extends AbstractModel
return sha1($salt1 . $salt2 . $pass); 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;
}
} }
} }

View file

@ -11,6 +11,7 @@ class Privilege extends Enum
const EditPostThumb = 8; const EditPostThumb = 8;
const EditPostSource = 26; const EditPostSource = 26;
const EditPostRelations = 30; const EditPostRelations = 30;
const EditPostFile = 36;
const HidePost = 9; const HidePost = 9;
const DeletePost = 10; const DeletePost = 10;
const FeaturePost = 25; const FeaturePost = 25;

View file

@ -17,7 +17,7 @@
<label class="left" for="tags">Tags:</label> <label class="left" for="tags">Tags:</label>
<div class="input-wrapper"><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> <div class="input-wrapper"><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>
</div> </div>
<input type="hidden" name="tags-token" id="tags-token" value="<?php echo $this->context->transport->tagsToken ?>"/> <input type="hidden" name="edit-token" id="edit-token" value="<?php echo $this->context->transport->editToken ?>"/>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>

View file

@ -11,7 +11,7 @@ setlocale(LC_CTYPE, 'en_US.UTF-8');
ini_set('memory_limit', '128M'); ini_set('memory_limit', '128M');
//extension sanity checks //extension sanity checks
$requiredExtensions = ['pdo', 'pdo_sqlite', 'gd', 'openssl']; $requiredExtensions = ['pdo', 'pdo_sqlite', 'gd', 'openssl', 'fileinfo'];
foreach ($requiredExtensions as $ext) foreach ($requiredExtensions as $ext)
if (!extension_loaded($ext)) if (!extension_loaded($ext))
die('PHP extension "' . $ext . '" must be enabled to continue.' . PHP_EOL); die('PHP extension "' . $ext . '" must be enabled to continue.' . PHP_EOL);