diff --git a/.gitmodules b/.gitmodules index 4aee5fd7..9be025bc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "php-markdown"] path = lib/php-markdown url = https://github.com/michelf/php-markdown.git -[submodule "redbean"] - path = lib/redbean - url = https://github.com/gabordemooij/redbean.git diff --git a/lib/redbean b/lib/redbean deleted file mode 160000 index 95cf7d23..00000000 --- a/lib/redbean +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 95cf7d231b60acdeee050041d44503df1b288b74 diff --git a/public_html/media/css/core.css b/public_html/media/css/core.css index cd25470c..d89d76c2 100644 --- a/public_html/media/css/core.css +++ b/public_html/media/css/core.css @@ -402,8 +402,11 @@ button:hover { } pre.debug { + margin-left: 1em; text-align: left; color: black; + white-space: normal; + text-indent: -1em; } .spoiler:before, diff --git a/src/Bootstrap.php b/src/Bootstrap.php index fb30c729..0f00a2ff 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -58,7 +58,7 @@ class Bootstrap { StatusHelper::failure(rtrim($e->getMessage(), '.') . '.'); $this->context->transport->exception = $e; - $this->context->transport->queries = array_map(function($x) { return preg_replace('/\s+/', ' ', $x); }, queryLogger()->getLogs()); + $this->context->transport->queries = Database::getLogs(); $this->context->viewName = 'error-exception'; (new \Chibi\View())->renderFile($this->context->layoutName); } diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index 20217418..6cd94b9b 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -17,15 +17,15 @@ class AuthController $config = \Chibi\Registry::getConfig(); $context = \Chibi\Registry::getContext(); - $dbUser = Model_User::locate($name, false); + $dbUser = UserModel::findByNameOrEmail($name, false); if ($dbUser === null) throw new SimpleException('Invalid username'); - $passwordHash = Model_User::hashPassword($password, $dbUser->pass_salt); - if ($passwordHash != $dbUser->pass_hash) + $passwordHash = UserModel::hashPassword($password, $dbUser->passSalt); + if ($passwordHash != $dbUser->passHash) throw new SimpleException('Invalid password'); - if (!$dbUser->staff_confirmed and $config->registration->staffActivation) + if (!$dbUser->staffConfirmed and $config->registration->staffActivation) throw new SimpleException('Staff hasn\'t confirmed your registration yet'); if ($dbUser->banned) @@ -105,20 +105,20 @@ class AuthController { if (!empty($context->user) and $context->user->id) { - $dbUser = R::findOne('user', 'id = ?', [$context->user->id]); + $dbUser = UserModel::findById($context->user->id); $_SESSION['user'] = serialize($dbUser); } else { - $dummy = R::dispense('user'); - $dummy->name = Model_User::getAnonymousName(); - $dummy->access_rank = AccessRank::Anonymous; - $dummy->anonymous = true; + $dummy = UserModel::spawn(); + $dummy->name = UserModel::getAnonymousName(); + $dummy->accessRank = AccessRank::Anonymous; $_SESSION['user'] = serialize($dummy); } } + $context->user = unserialize($_SESSION['user']); - $context->loggedIn = $context->user->anonymous ? false : true; + $context->loggedIn = $context->user->accessRank != AccessRank::Anonymous; if (!$context->loggedIn) { try @@ -135,7 +135,7 @@ class AuthController { $context = \Chibi\Registry::getContext(); if ($context->user !== null) - $_SESSION['user'] = serialize($context->user); + self::doLogOut(); self::doLogIn(); } diff --git a/src/Controllers/CommentController.php b/src/Controllers/CommentController.php index 1b154c73..ad9822e8 100644 --- a/src/Controllers/CommentController.php +++ b/src/Controllers/CommentController.php @@ -21,9 +21,13 @@ class CommentController PrivilegesHelper::confirmWithException(Privilege::ListComments); $page = max(1, $page); - list ($comments, $commentCount) = Model_Comment::getEntitiesWithCount(null, $commentsPerPage, $page); + $comments = CommentSearchService::getEntities(null, $commentsPerPage, $page); + $commentCount = CommentSearchService::getEntityCount(null, $commentsPerPage, $page); $pageCount = ceil($commentCount / $commentsPerPage); - R::preload($comments, ['commenter' => 'user', 'post', 'post.uploader' => 'user', 'post.sharedTag']); + CommentModel::preloadCommenters($comments); + CommentModel::preloadPosts($comments); + $posts = array_map(function($comment) { return $comment->getPost(); }, $comments); + PostModel::preloadTags($posts); $this->context->postGroups = true; $this->context->transport->paginator = new StdClass; @@ -47,22 +51,24 @@ class CommentController if ($this->config->registration->needEmailForCommenting) PrivilegesHelper::confirmEmail($this->context->user); - $post = Model_Post::locate($postId); + $post = PostModel::findById($postId); if (InputHelper::get('submit')) { $text = InputHelper::get('text'); - $text = Model_Comment::validateText($text); + $text = CommentModel::validateText($text); - $comment = Model_Comment::create(); - $comment->post = $post; + $comment = CommentModel::spawn(); + $comment->setPost($post); if ($this->context->loggedIn) - $comment->commenter = $this->context->user; - $comment->comment_date = time(); + $comment->setCommenter($this->context->user); + else + $comment->setCommenter(null); + $comment->commentDate = time(); $comment->text = $text; if (InputHelper::get('sender') != 'preview') { - Model_Comment::save($comment); + CommentModel::save($comment); LogHelper::log('{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]); } $this->context->transport->textPreview = $comment->getText(); @@ -78,13 +84,12 @@ class CommentController */ public function deleteAction($id) { - $comment = Model_Comment::locate($id); - R::preload($comment, ['commenter' => 'user']); + $comment = CommentModel::findById($id); - PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->commenter)); - Model_Comment::remove($comment); + PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->getCommenter())); + CommentModel::remove($comment); - LogHelper::log('{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->post)]); + LogHelper::log('{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->getPost())]); StatusHelper::success(); } } diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index 39ead30b..6a072f66 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -9,14 +9,14 @@ class IndexController { $this->context->subTitle = 'home'; $this->context->stylesheets []= 'index-index.css'; - $this->context->transport->postCount = Model_Post::getAllPostCount(); + $this->context->transport->postCount = PostModel::getCount(); $featuredPost = $this->getFeaturedPost(); if ($featuredPost) { $this->context->featuredPost = $featuredPost; - $this->context->featuredPostDate = Model_Property::get(Model_Property::FeaturedPostDate); - $this->context->featuredPostUser = Model_User::locate(Model_Property::get(Model_Property::FeaturedPostUserName), false); + $this->context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate); + $this->context->featuredPostUser = UserModel::findByNameOrEmail(PropertyModel::get(PropertyModel::FeaturedPostUserName), false); $this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $featuredPost->name]); } } @@ -42,15 +42,15 @@ class IndexController { $featuredPostRotationTime = $this->config->misc->featuredPostMaxDays * 24 * 3600; - $featuredPostId = Model_Property::get(Model_Property::FeaturedPostId); - $featuredPostDate = Model_Property::get(Model_Property::FeaturedPostDate); + $featuredPostId = PropertyModel::get(PropertyModel::FeaturedPostId); + $featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate); //check if too old if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time()) return $this->featureNewPost(); //check if post was deleted - $featuredPost = Model_Post::locate($featuredPostId, false, false); + $featuredPost = PostModel::findById($featuredPostId, false); if (!$featuredPost) return $this->featureNewPost(); @@ -59,20 +59,20 @@ class IndexController private function featureNewPost() { - $featuredPostId = R::$f->begin() + $query = (new SqlQuery) ->select('id') ->from('post') ->where('type = ?')->put(PostType::Image) ->and('safety = ?')->put(PostSafety::Safe) ->orderBy($this->config->main->dbDriver == 'sqlite' ? 'random()' : 'rand()') - ->desc() - ->get('row')['id']; + ->desc(); + $featuredPostId = Database::fetchOne($query)['id']; if (!$featuredPostId) return null; - Model_Property::set(Model_Property::FeaturedPostId, $featuredPostId); - Model_Property::set(Model_Property::FeaturedPostDate, time()); - Model_Property::set(Model_Property::FeaturedPostUserName, null); - return Model_Post::locate($featuredPostId); + PropertyModel::set(PropertyModel::FeaturedPostId, $featuredPostId); + PropertyModel::set(PropertyModel::FeaturedPostDate, time()); + PropertyModel::set(PropertyModel::FeaturedPostUserName, null); + return PostModel::findById($featuredPostId); } } diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index da947ebb..a2148335 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -11,13 +11,13 @@ class PostController private static function serializePost($post) { $x = []; - foreach ($post->sharedTag as $tag) + foreach ($post->getTags() as $tag) $x []= TextHelper::reprTag($tag->name); - foreach ($post->via('crossref')->sharedPost as $relatedPost) + foreach ($post->getRelations() as $relatedPost) $x []= TextHelper::reprPost($relatedPost); $x []= $post->safety; $x []= $post->source; - $x []= $post->file_hash; + $x []= $post->fileHash; natcasesort($x); $x = join(' ', $x); return md5($x); @@ -104,10 +104,11 @@ class PostController } $page = max(1, $page); - list($posts, $postCount) = Model_Post::getEntitiesWithCount($query, $postsPerPage, $page); + $posts = PostSearchService::getEntities($query, $postsPerPage, $page); + $postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page); $pageCount = ceil($postCount / $postsPerPage); $page = min($pageCount, $page); - Model_Post::attachTags($posts); + PostModel::preloadTags($posts); $this->context->transport->paginator = new StdClass; $this->context->transport->paginator->page = $page; @@ -126,33 +127,40 @@ class PostController */ public function toggleTagAction($id, $tag, $enable) { - $post = Model_Post::locate($id); + $tagName = $tag; + $post = PostModel::findByIdOrName($id); $this->context->transport->post = $post; - $tagRow = Model_Tag::locate($tag, false); - if ($tagRow !== null) - $tag = $tagRow->name; - if (InputHelper::get('submit')) { PrivilegesHelper::confirmWithException(Privilege::MassTag); - $tags = array_map(function($x) { return $x->name; }, $post->sharedTag); - if (!$enable and in_array($tag, $tags)) + $tags = $post->getTags(); + + if (!$enable) { - $tags = array_diff($tags, [$tag]); + foreach ($tags as $i => $tag) + if ($tag->name == $tagName) + unset($tags[$i]); LogHelper::log('{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); } elseif ($enable) { - $tags += [$tag]; + $tag = TagModel::findByName($tagName, false); + if ($tag === null) + { + $tag = TagModel::spawn(); + $tag->name = $tagName; + TagModel::save($tag); + } + + $tags []= $tag; LogHelper::log('{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); } - $dbTags = Model_Tag::insertOrUpdate($tags); - $post->sharedTag = $dbTags; + $post->setTags($tags); - Model_Post::save($post); + PostModel::save($post); StatusHelper::success(); } } @@ -197,18 +205,18 @@ class PostController if (InputHelper::get('submit')) { - R::transaction(function() + Database::transaction(function() { - $post = Model_Post::create(); + $post = PostModel::spawn(); LogHelper::bufferChanges(); //basic stuff $anonymous = InputHelper::get('anonymous'); if ($this->context->loggedIn and !$anonymous) - $post->uploader = $this->context->user; + $post->setUploader($this->context->user); //store the post to get the ID in the logs - Model_Post::save($post); + PostModel::forgeId($post); //do the edits $this->doEdit($post, true); @@ -227,13 +235,13 @@ class PostController $fmt .= ' added {post} (tags: {tags}, safety: {safety}, source: {source})'; LogHelper::log($fmt, [ 'post' => TextHelper::reprPost($post), - 'tags' => join(', ', array_map(['TextHelper', 'reprTag'], $post->sharedTag)), + 'tags' => TextHelper::reprTags($post->getTags()), 'safety' => PostSafety::toString($post->safety), 'source' => $post->source]); //finish LogHelper::flush(); - Model_Post::save($post); + PostModel::save($post); }); StatusHelper::success(); @@ -247,7 +255,7 @@ class PostController */ public function editAction($id) { - $post = Model_Post::locate($id); + $post = PostModel::findByIdOrName($id); $this->context->transport->post = $post; if (InputHelper::get('submit')) @@ -260,8 +268,8 @@ class PostController $this->doEdit($post, false); LogHelper::flush(); - Model_Post::save($post); - Model_Tag::removeUnused(); + PostModel::save($post); + TagModel::removeUnused(); StatusHelper::success(); } @@ -274,7 +282,7 @@ class PostController */ public function flagAction($id) { - $post = Model_Post::locate($id); + $post = PostModel::findByIdOrName($id); PrivilegesHelper::confirmWithException(Privilege::FlagPost); if (InputHelper::get('submit')) @@ -299,15 +307,13 @@ class PostController */ public function hideAction($id) { - $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); - - PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + $post = PostModel::findByIdOrName($id); + PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); if (InputHelper::get('submit')) { $post->setHidden(true); - Model_Post::save($post); + PostModel::save($post); LogHelper::log('{user} hidden {post}', ['post' => TextHelper::reprPost($post)]); StatusHelper::success(); @@ -321,15 +327,13 @@ class PostController */ public function unhideAction($id) { - $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); - - PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + $post = PostModel::findByIdOrName($id); + PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); if (InputHelper::get('submit')) { $post->setHidden(false); - Model_Post::save($post); + PostModel::save($post); LogHelper::log('{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]); StatusHelper::success(); @@ -343,14 +347,12 @@ class PostController */ public function deleteAction($id) { - $post = Model_Post::locate($id); - R::preload($post, ['uploader' => 'user']); - - PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + $post = PostModel::findByIdOrName($id); + PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); if (InputHelper::get('submit')) { - Model_Post::remove($post); + PostModel::remove($post); LogHelper::log('{user} deleted {post}', ['post' => TextHelper::reprPost($id)]); StatusHelper::success(); @@ -365,7 +367,7 @@ class PostController */ public function addFavoriteAction($id) { - $post = Model_Post::locate($id); + $post = PostModel::findByIdOrName($id); PrivilegesHelper::confirmWithException(Privilege::FavoritePost); if (InputHelper::get('submit')) @@ -373,8 +375,7 @@ class PostController if (!$this->context->loggedIn) throw new SimpleException('Not logged in'); - $this->context->user->addToFavorites($post); - Model_User::save($this->context->user); + UserModel::addToUserFavorites($this->context->user, $post); StatusHelper::success(); } } @@ -385,7 +386,7 @@ class PostController */ public function remFavoriteAction($id) { - $post = Model_Post::locate($id); + $post = PostModel::findByIdOrName($id); PrivilegesHelper::confirmWithException(Privilege::FavoritePost); if (InputHelper::get('submit')) @@ -393,8 +394,7 @@ class PostController if (!$this->context->loggedIn) throw new SimpleException('Not logged in'); - $this->context->user->remFromFavorites($post); - Model_User::save($this->context->user); + UserModel::removeFromUserFavorites($this->context->user, $post); StatusHelper::success(); } } @@ -407,7 +407,7 @@ class PostController */ public function scoreAction($id, $score) { - $post = Model_Post::locate($id); + $post = PostModel::findByIdOrName($id); PrivilegesHelper::confirmWithException(Privilege::ScorePost); if (InputHelper::get('submit')) @@ -415,8 +415,7 @@ class PostController if (!$this->context->loggedIn) throw new SimpleException('Not logged in'); - $this->context->user->score($post, $score); - Model_User::save($this->context->user); + UserModel::updateUserScore($this->context->user, $post, $score); StatusHelper::success(); } } @@ -428,11 +427,11 @@ class PostController */ public function featureAction($id) { - $post = Model_Post::locate($id); + $post = PostModel::findByIdOrName($id); PrivilegesHelper::confirmWithException(Privilege::FeaturePost); - Model_Property::set(Model_Property::FeaturedPostId, $post->id); - Model_Property::set(Model_Property::FeaturedPostDate, time()); - Model_Property::set(Model_Property::FeaturedPostUserName, $this->context->user->name); + PropertyModel::set(PropertyModel::FeaturedPostId, $post->id); + PropertyModel::set(PropertyModel::FeaturedPostDate, time()); + PropertyModel::set(PropertyModel::FeaturedPostUserName, $this->context->user->name); StatusHelper::success(); LogHelper::log('{user} featured {post} on main page', ['post' => TextHelper::reprPost($post)]); } @@ -445,35 +444,31 @@ class PostController */ public function viewAction($id) { - $post = Model_Post::locate($id); - R::preload($post, [ - 'tag', - 'uploader' => 'user', - 'ownComment.commenter' => 'user']); - R::preload($this->context->user, ['ownFavoritee']); + $post = PostModel::findByIdOrName($id); + CommentModel::preloadCommenters($post->getComments()); if ($post->hidden) PrivilegesHelper::confirmWithException(Privilege::ViewPost, 'hidden'); PrivilegesHelper::confirmWithException(Privilege::ViewPost); PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety)); - Model_Post_QueryBuilder::enableTokenLimit(false); + PostSearchService::enableTokenLimit(false); try { $this->context->transport->lastSearchQuery = InputHelper::get('last-search-query'); $prevPostQuery = $this->context->transport->lastSearchQuery . ' prev:' . $id; $nextPostQuery = $this->context->transport->lastSearchQuery . ' next:' . $id; - $prevPost = current(Model_Post::getEntities($prevPostQuery, 1, 1)); - $nextPost = current(Model_Post::getEntities($nextPostQuery, 1, 1)); + $prevPost = current(PostSearchService::getEntities($prevPostQuery, 1, 1)); + $nextPost = current(PostSearchService::getEntities($nextPostQuery, 1, 1)); } #search for some reason was invalid, e.g. tag was deleted in the meantime catch (Exception $e) { $this->context->transport->lastSearchQuery = ''; - $prevPost = current(Model_Post::getEntities('prev:' . $id, 1, 1)); - $nextPost = current(Model_Post::getEntities('next:' . $id, 1, 1)); + $prevPost = current(PostModel::getEntities('prev:' . $id, 1, 1)); + $nextPost = current(PostModel::getEntities('next:' . $id, 1, 1)); } - Model_Post_QueryBuilder::enableTokenLimit(true); + PostSearchService::enableTokenLimit(true); $favorite = $this->context->user->hasFavorited($post); $score = $this->context->user->getScore($post); @@ -483,13 +478,13 @@ class PostController $this->context->stylesheets []= 'post-view.css'; $this->context->stylesheets []= 'comment-small.css'; $this->context->scripts []= 'post-view.js'; - $this->context->subTitle = 'showing ' . TextHelper::reprPost($post) . ' – ' . TextHelper::reprTags($post->sharedTag); + $this->context->subTitle = 'showing ' . TextHelper::reprPost($post) . ' – ' . TextHelper::reprTags($post->getTags()); $this->context->favorite = $favorite; $this->context->score = $score; $this->context->flagged = $flagged; $this->context->transport->post = $post; - $this->context->transport->prevPostId = $prevPost ? $prevPost['id'] : null; - $this->context->transport->nextPostId = $nextPost ? $nextPost['id'] : null; + $this->context->transport->prevPostId = $prevPost ? $prevPost->id : null; + $this->context->transport->nextPostId = $nextPost ? $nextPost->id : null; $this->context->transport->editToken = self::serializePost($post); } @@ -501,13 +496,13 @@ class PostController */ public function thumbAction($name, $width = null, $height = null) { - $path = Model_Post::getThumbCustomPath($name, $width, $height); + $path = PostModel::getThumbCustomPath($name, $width, $height); if (!file_exists($path)) { - $path = Model_Post::getThumbDefaultPath($name, $width, $height); + $path = PostModel::getThumbDefaultPath($name, $width, $height); if (!file_exists($path)) { - $post = Model_Post::locate($name); + $post = PostModel::findByIdOrName($name); PrivilegesHelper::confirmWithException(Privilege::ListPosts); PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety)); $post->makeThumb($width, $height); @@ -534,8 +529,7 @@ class PostController */ public function retrieveAction($name) { - $this->context->layoutName = 'layout-file'; - $post = Model_Post::locate($name, true); + $post = PostModel::findByName($name, true); PrivilegesHelper::confirmWithException(Privilege::RetrievePost); PrivilegesHelper::confirmWithException(Privilege::RetrievePost, PostSafety::toString($post->safety)); @@ -546,22 +540,23 @@ class PostController if (!is_readable($path)) throw new SimpleException('Post file is not readable'); - $ext = substr($post->orig_name, strrpos($post->orig_name, '.') + 1); - if (strpos($post->orig_name, '.') === false) - $ext = '.dat'; + $ext = substr($post->origName, strrpos($post->origName, '.') + 1); + if (strpos($post->origName, '.') === false) + $ext = 'dat'; $fn = sprintf('%s_%s_%s.%s', $this->config->main->title, $post->id, - join(',', array_map(function($tag) { return $tag->name; }, $post->sharedTag)), + join(',', array_map(function($tag) { return $tag->name; }, $post->getTags())), $ext); $fn = preg_replace('/[[:^print:]]/', '', $fn); $ttl = 60 * 60 * 24 * 14; + $this->context->layoutName = 'layout-file'; $this->context->transport->cacheDaysToLive = 14; $this->context->transport->customFileName = $fn; $this->context->transport->mimeType = $post->mimeType; - $this->context->transport->fileHash = 'post' . $post->file_hash; + $this->context->transport->fileHash = 'post' . $post->fileHash; $this->context->transport->filePath = $path; } @@ -569,14 +564,11 @@ class PostController private function doEdit($post, $isNew) { - if (!$isNew) - R::preload($post, ['uploader' => 'user']); - /* file contents */ if (!empty($_FILES['file']['name'])) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); $suppliedFile = $_FILES['file']; self::handleUploadErrors($suppliedFile); @@ -590,7 +582,7 @@ class PostController elseif (InputHelper::get('url')) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); $url = InputHelper::get('url'); $post->setContentFromUrl($url); @@ -604,7 +596,7 @@ class PostController if ($suppliedSafety !== null) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); $oldSafety = $post->safety; $post->setSafety($suppliedSafety); @@ -619,11 +611,11 @@ class PostController if ($suppliedTags !== null) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); - $oldTags = array_map(function($tag) { return $tag->name; }, $post->sharedTag); + $oldTags = array_map(function($tag) { return $tag->name; }, $post->getTags()); $post->setTagsFromText($suppliedTags); - $newTags = array_map(function($tag) { return $tag->name; }, $post->sharedTag); + $newTags = array_map(function($tag) { return $tag->name; }, $post->getTags()); foreach (array_diff($oldTags, $newTags) as $tag) LogHelper::log('{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]); @@ -637,7 +629,7 @@ class PostController if ($suppliedSource !== null) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); $oldSource = $post->source; $post->setSource($suppliedSource); @@ -652,11 +644,11 @@ class PostController if ($suppliedRelations !== null) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); - $oldRelatedIds = array_map(function($post) { return $post->id; }, $post->via('crossref')->sharedPost); + $oldRelatedIds = array_map(function($post) { return $post->id; }, $post->getRelations()); $post->setRelationsFromText($suppliedRelations); - $newRelatedIds = array_map(function($post) { return $post->id; }, $post->via('crossref')->sharedPost); + $newRelatedIds = array_map(function($post) { return $post->id; }, $post->getRelations()); foreach (array_diff($oldRelatedIds, $newRelatedIds) as $post2id) LogHelper::log('{user} removed relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]); @@ -669,7 +661,7 @@ class PostController if (!empty($_FILES['thumb']['name'])) { if (!$isNew) - PrivilegesHelper::confirmWithException(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($post->uploader)); + PrivilegesHelper::confirmWithException(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader())); $suppliedFile = $_FILES['thumb']; self::handleUploadErrors($suppliedFile); diff --git a/src/Controllers/TagController.php b/src/Controllers/TagController.php index 1df13082..f385bd2d 100644 --- a/src/Controllers/TagController.php +++ b/src/Controllers/TagController.php @@ -15,7 +15,7 @@ class TagController PrivilegesHelper::confirmWithException(Privilege::ListTags); $suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc'; - $tags = Model_Tag::getEntitiesRows($suppliedFilter, null, null); + $tags = TagSearchService::getEntitiesRows($suppliedFilter, null, null); $this->context->filter = $suppliedFilter; $this->context->transport->tags = $tags; @@ -39,30 +39,15 @@ class TagController PrivilegesHelper::confirmWithException(Privilege::MergeTags); if (InputHelper::get('submit')) { - Model_Tag::removeUnused(); + TagModel::removeUnused(); $suppliedSourceTag = InputHelper::get('source-tag'); - $suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag); - $sourceTag = Model_Tag::locate($suppliedSourceTag); + $suppliedSourceTag = TagModel::validateTag($suppliedSourceTag); $suppliedTargetTag = InputHelper::get('target-tag'); - $suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag); - $targetTag = Model_Tag::locate($suppliedTargetTag); + $suppliedTargetTag = TagModel::validateTag($suppliedTargetTag); - if ($sourceTag->id == $targetTag->id) - throw new SimpleException('Source and target tag are the same'); - - R::preload($sourceTag, 'post'); - - foreach ($sourceTag->sharedPost as $post) - { - foreach ($post->sharedTag as $key => $postTag) - if ($postTag->id == $sourceTag->id) - unset($post->sharedTag[$key]); - $post->sharedTag []= $targetTag; - Model_Post::save($post); - } - Model_Tag::remove($sourceTag); + TagModel::merge($suppliedSourceTag, $suppliedTargetTag); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list')); LogHelper::log('{user} merged {source} with {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]); @@ -82,21 +67,15 @@ class TagController PrivilegesHelper::confirmWithException(Privilege::MergeTags); if (InputHelper::get('submit')) { - Model_Tag::removeUnused(); + TagModel::removeUnused(); $suppliedSourceTag = InputHelper::get('source-tag'); - $suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag); - $sourceTag = Model_Tag::locate($suppliedSourceTag); + $suppliedSourceTag = TagModel::validateTag($suppliedSourceTag); $suppliedTargetTag = InputHelper::get('target-tag'); - $suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag); - $targetTag = Model_Tag::locate($suppliedTargetTag, false); + $suppliedTargetTag = TagModel::validateTag($suppliedTargetTag); - if ($targetTag and $targetTag->id != $sourceTag->id) - throw new SimpleException('Target tag already exists'); - - $sourceTag->name = $suppliedTargetTag; - Model_Tag::save($sourceTag); + TagModel::rename($suppliedSourceTag, $suppliedTargetTag); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list')); LogHelper::log('{user} renamed {source} to {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]); @@ -121,7 +100,7 @@ class TagController $suppliedQuery = ' '; $suppliedTag = InputHelper::get('tag'); if (!empty($suppliedTag)) - $suppliedTag = Model_Tag::validateTag($suppliedTag); + $suppliedTag = TagModel::validateTag($suppliedTag); \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', ['source' => 'mass-tag', 'query' => $suppliedQuery, 'additionalInfo' => $suppliedTag])); } } diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php index 8ac9857a..9a81971a 100644 --- a/src/Controllers/UserController.php +++ b/src/Controllers/UserController.php @@ -22,25 +22,20 @@ class UserController $linkActionName) { //prepare unique user token - do - { - $tokenText = md5(mt_rand() . uniqid()); - } - while (R::findOne('usertoken', 'token = ?', [$tokenText]) !== null); - $token = R::dispense('usertoken'); - $token->user = $user; - $token->token = $tokenText; + $token = TokenModel::spawn(); + $token->setUser($user); + $token->token = TokenModel::forgeUnusedToken(); $token->used = false; $token->expires = null; - R::store($token); + TokenModel::save($token); \Chibi\Registry::getContext()->mailSent = true; $tokens = []; $tokens['host'] = $_SERVER['HTTP_HOST']; - $tokens['token'] = $tokenText; + $tokens['token'] = $token->token; //gosh this code looks so silly $tokens['nl'] = PHP_EOL; if ($linkActionName !== null) - $tokens['link'] = \Chibi\UrlHelper::route('user', $linkActionName, ['token' => $tokenText]); + $tokens['link'] = \Chibi\UrlHelper::route('user', $linkActionName, ['token' => $token->token]); $body = wordwrap(TextHelper::replaceTokens($body, $tokens), 70); $subject = TextHelper::replaceTokens($subject, $tokens); @@ -73,8 +68,8 @@ class UserController $regConfig = \Chibi\Registry::getConfig()->registration; if (!$regConfig->confirmationEmailEnabled) { - $user->email_confirmed = $user->email_unconfirmed; - $user->email_unconfirmed = null; + $user->emailConfirmed = $user->emailUnconfirmed; + $user->emailUnconfirmed = null; return; } @@ -84,7 +79,7 @@ class UserController $regConfig->confirmationEmailSubject, $regConfig->confirmationEmailSenderName, $regConfig->confirmationEmailSenderEmail, - $user->email_unconfirmed, + $user->emailUnconfirmed, 'activation'); } @@ -98,7 +93,7 @@ class UserController $regConfig->passwordResetEmailSubject, $regConfig->passwordResetEmailSenderName, $regConfig->passwordResetEmailSenderEmail, - $user->email_confirmed, + $user->emailConfirmed, 'password-reset'); } @@ -130,7 +125,8 @@ class UserController PrivilegesHelper::confirmWithException(Privilege::ListUsers); $page = max(1, $page); - list ($users, $userCount) = Model_User::getEntitiesWithCount($sortStyle, $usersPerPage, $page); + $users = UserSearchService::getEntities($sortStyle, $usersPerPage, $page); + $userCount = UserSearchService::getEntityCount($sortStyle, $usersPerPage, $page); $pageCount = ceil($userCount / $usersPerPage); $this->context->sortStyle = $sortStyle; @@ -151,7 +147,7 @@ class UserController */ public function flagAction($name) { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::FlagUser); if (InputHelper::get('submit')) @@ -177,13 +173,13 @@ class UserController */ public function banAction($name) { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::BanUser, PrivilegesHelper::getIdentitySubPrivilege($user)); if (InputHelper::get('submit')) { $user->banned = true; - Model_User::save($user); + UserModel::save($user); LogHelper::log('{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]); StatusHelper::success(); @@ -198,13 +194,13 @@ class UserController */ public function unbanAction($name) { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::BanUser, PrivilegesHelper::getIdentitySubPrivilege($user)); if (InputHelper::get('submit')) { $user->banned = false; - Model_User::save($user); + UserModel::save($user); LogHelper::log('{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]); StatusHelper::success(); @@ -219,12 +215,12 @@ class UserController */ public function acceptRegistrationAction($name) { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::AcceptUserRegistration); if (InputHelper::get('submit')) { - $user->staff_confirmed = true; - Model_User::save($user); + $user->staffConfirmed = true; + UserModel::save($user); LogHelper::log('{user} confirmed {subject}\'s account', ['subject' => TextHelper::reprUser($user)]); StatusHelper::success(); } @@ -238,7 +234,7 @@ class UserController */ public function deleteAction($name) { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user)); PrivilegesHelper::confirmWithException(Privilege::DeleteUser, PrivilegesHelper::getIdentitySubPrivilege($user)); @@ -252,13 +248,13 @@ class UserController $name = $user->name; if ($this->context->user->id == $user->id) { - $suppliedPasswordHash = Model_User::hashPassword($suppliedCurrentPassword, $user->pass_salt); - if ($suppliedPasswordHash != $user->pass_hash) + $suppliedPasswordHash = UserModel::hashPassword($suppliedCurrentPassword, $user->passSalt); + if ($suppliedPasswordHash != $user->passHash) throw new SimpleException('Must supply valid password'); } $oldId = $user->id; - Model_User::remove($user); + UserModel::remove($user); if ($oldId == $this->context->user->id) AuthController::doLogOut(); @@ -276,7 +272,7 @@ class UserController */ public function settingsAction($name) { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user)); PrivilegesHelper::confirmWithException(Privilege::ChangeUserSettings, PrivilegesHelper::getIdentitySubPrivilege($user)); @@ -295,7 +291,8 @@ class UserController $user->enablePostTagTitles(InputHelper::get('post-tag-titles')); $user->enableHidingDislikedPosts(InputHelper::get('hide-disliked-posts')); - Model_User::save($user); + if ($user->accessRank != AccessRank::Anonymous) + UserModel::save($user); if ($user->id == $this->context->user->id) $this->context->user = $user; AuthController::doReLog(); @@ -313,7 +310,7 @@ class UserController { try { - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user)); $this->loadUserView($user); @@ -325,7 +322,7 @@ class UserController $this->context->suppliedPassword2 = $suppliedPassword2 = InputHelper::get('password2'); $this->context->suppliedEmail = $suppliedEmail = InputHelper::get('email'); $this->context->suppliedAccessRank = $suppliedAccessRank = InputHelper::get('access-rank'); - $currentPasswordHash = $user->pass_hash; + $currentPasswordHash = $user->passHash; if (InputHelper::get('submit')) { @@ -335,7 +332,7 @@ class UserController if ($suppliedName != '' and $suppliedName != $user->name) { PrivilegesHelper::confirmWithException(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($user)); - $suppliedName = Model_User::validateUserName($suppliedName); + $suppliedName = UserModel::validateUserName($suppliedName); $oldName = $user->name; $user->name = $suppliedName; LogHelper::log('{user} renamed {old} to {new}', ['old' => TextHelper::reprUser($oldName), 'new' => TextHelper::reprUser($suppliedName)]); @@ -346,45 +343,45 @@ class UserController PrivilegesHelper::confirmWithException(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($user)); if ($suppliedPassword1 != $suppliedPassword2) throw new SimpleException('Specified passwords must be the same'); - $suppliedPassword = Model_User::validatePassword($suppliedPassword1); - $user->pass_hash = Model_User::hashPassword($suppliedPassword, $user->pass_salt); + $suppliedPassword = UserModel::validatePassword($suppliedPassword1); + $user->passHash = UserModel::hashPassword($suppliedPassword, $user->passSalt); LogHelper::log('{user} changed {subject}\'s password', ['subject' => TextHelper::reprUser($user)]); } - if ($suppliedEmail != '' and $suppliedEmail != $user->email_confirmed) + if ($suppliedEmail != '' and $suppliedEmail != $user->emailConfirmed) { PrivilegesHelper::confirmWithException(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($user)); - $suppliedEmail = Model_User::validateEmail($suppliedEmail); + $suppliedEmail = UserModel::validateEmail($suppliedEmail); if ($this->context->user->id == $user->id) { - $user->email_unconfirmed = $suppliedEmail; - if (!empty($user->email_unconfirmed)) + $user->emailUnconfirmed = $suppliedEmail; + if (!empty($user->emailUnconfirmed)) $confirmMail = true; LogHelper::log('{user} changed e-mail to {mail}', ['mail' => $suppliedEmail]); } else { - $user->email_unconfirmed = null; - $user->email_confirmed = $suppliedEmail; + $user->emailUnconfirmed = null; + $user->emailConfirmed = $suppliedEmail; LogHelper::log('{user} changed {subject}\'s e-mail to {mail}', ['subject' => TextHelper::reprUser($user), 'mail' => $suppliedEmail]); } } - if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->access_rank) + if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->accessRank) { PrivilegesHelper::confirmWithException(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($user)); - $suppliedAccessRank = Model_User::validateAccessRank($suppliedAccessRank); - $user->access_rank = $suppliedAccessRank; + $suppliedAccessRank = UserModel::validateAccessRank($suppliedAccessRank); + $user->accessRank = $suppliedAccessRank; LogHelper::log('{user} changed {subject}\'s access rank to {rank}', ['subject' => TextHelper::reprUser($user), 'rank' => AccessRank::toString($suppliedAccessRank)]); } if ($this->context->user->id == $user->id) { - $suppliedPasswordHash = Model_User::hashPassword($suppliedCurrentPassword, $user->pass_salt); + $suppliedPasswordHash = UserModel::hashPassword($suppliedCurrentPassword, $user->passSalt); if ($suppliedPasswordHash != $currentPasswordHash) throw new SimpleException('Must supply valid current password'); } - Model_User::save($user); + UserModel::save($user); if ($confirmMail) self::sendEmailChangeConfirmation($user); @@ -398,7 +395,7 @@ class UserController } catch (Exception $e) { - $this->context->transport->user = Model_User::locate($name); + $this->context->transport->user = UserModel::findByNameOrEmail($name); throw $e; } } @@ -406,16 +403,16 @@ class UserController /** - * @route /user/{name} + * @route /user/{name}/{tab} * @route /user/{name}/{tab}/{page} * @validate name [^\/]+ * @validate tab favs|uploads * @validate page \d* */ - public function viewAction($name, $tab, $page) + public function viewAction($name, $tab = 'favs', $page) { $postsPerPage = intval($this->config->browsing->postsPerPage); - $user = Model_User::locate($name); + $user = UserModel::findByNameOrEmail($name); if ($tab === null) $tab = 'favs'; if ($page === null) @@ -439,9 +436,10 @@ class UserController throw new SimpleException('Wrong tab'); $page = max(1, $page); - list ($posts, $postCount) = Model_Post::getEntitiesWithCount($query, $postsPerPage, $page); + $posts = PostSearchService::getEntities($query, $postsPerPage, $page); + $postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page); $pageCount = ceil($postCount / $postsPerPage); - Model_Post::attachTags($posts); + PostModel::preloadTags($posts); $this->context->transport->tab = $tab; $this->context->transport->lastSearchQuery = $query; @@ -468,9 +466,9 @@ class UserController $this->context->user->enableSafety($safety, !$this->context->user->hasEnabledSafety($safety)); + if ($this->context->user->accessRank != AccessRank::Anonymous) + UserModel::save($this->context->user); AuthController::doReLog(); - if (!$this->context->user->anonymous) - Model_User::save($this->context->user); StatusHelper::success(); } @@ -504,42 +502,42 @@ class UserController if (InputHelper::get('submit')) { - $suppliedName = Model_User::validateUserName($suppliedName); + $suppliedName = UserModel::validateUserName($suppliedName); if ($suppliedPassword1 != $suppliedPassword2) throw new SimpleException('Specified passwords must be the same'); - $suppliedPassword = Model_User::validatePassword($suppliedPassword1); + $suppliedPassword = UserModel::validatePassword($suppliedPassword1); - $suppliedEmail = Model_User::validateEmail($suppliedEmail); + $suppliedEmail = UserModel::validateEmail($suppliedEmail); if (empty($suppliedEmail) and $this->config->registration->needEmailForRegistering) throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.'); //register the user - $dbUser = Model_User::create(); + $dbUser = UserModel::spawn(); $dbUser->name = $suppliedName; - $dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt); - $dbUser->email_unconfirmed = $suppliedEmail; + $dbUser->passHash = UserModel::hashPassword($suppliedPassword, $dbUser->passSalt); + $dbUser->emailUnconfirmed = $suppliedEmail; - $dbUser->join_date = time(); - if (R::findOne('user') === null) + $dbUser->joinDate = time(); + if (UserModel::getCount() == 0) { //very first user - $dbUser->access_rank = AccessRank::Admin; - $dbUser->staff_confirmed = true; - $dbUser->email_unconfirmed = null; - $dbUser->email_confirmed = $suppliedEmail; + $dbUser->accessRank = AccessRank::Admin; + $dbUser->staffConfirmed = true; + $dbUser->emailUnconfirmed = null; + $dbUser->emailConfirmed = $suppliedEmail; } else { - $dbUser->access_rank = AccessRank::Registered; - $dbUser->staff_confirmed = false; - $dbUser->staff_confirmed = null; + $dbUser->accessRank = AccessRank::Registered; + $dbUser->staffConfirmed = false; + $dbUser->staffConfirmed = null; } //save the user to db if everything went okay - Model_User::save($dbUser); + UserModel::save($dbUser); - if (!empty($dbUser->email_unconfirmed)) + if (!empty($dbUser->emailUnconfirmed)) self::sendEmailChangeConfirmation($dbUser); $message = 'Congratulations, your account was created.'; @@ -573,14 +571,15 @@ class UserController $this->context->subTitle = 'account activation'; $this->context->viewName = 'message'; - $dbToken = Model_Token::locate($token); + $dbToken = TokenModel::findByToken($token); + TokenModel::checkValidity($dbToken); - $dbUser = $dbToken->user; - $dbUser->email_confirmed = $dbUser->email_unconfirmed; - $dbUser->email_unconfirmed = null; + $dbUser = $dbToken->getUser(); + $dbUser->emailConfirmed = $dbUser->emailUnconfirmed; + $dbUser->emailUnconfirmed = null; $dbToken->used = true; - R::store($dbToken); - Model_User::save($dbUser); + TokenModel::save($dbToken); + UserModel::save($dbUser); LogHelper::log('{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]); $message = 'Activation completed successfully.'; @@ -605,7 +604,8 @@ class UserController $this->context->subTitle = 'password reset'; $this->context->viewName = 'message'; - $dbToken = Model_Token::locate($token); + $dbToken = TokenModel::findByToken($token); + TokenModel::checkValidity($dbToken); $alphabet = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9')); $randomPassword = join('', array_map(function($x) use ($alphabet) @@ -613,11 +613,11 @@ class UserController return $alphabet[$x]; }, array_rand($alphabet, 8))); - $dbUser = $dbToken->user; - $dbUser->pass_hash = Model_User::hashPassword($randomPassword, $dbUser->pass_salt); + $dbUser = $dbToken->getUser(); + $dbUser->passHash = UserModel::hashPassword($randomPassword, $dbUser->passSalt); $dbToken->used = true; - R::store($dbToken); - Model_User::save($dbUser); + TokenModel::save($dbToken); + UserModel::save($dbUser); LogHelper::log('{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]); $message = 'Password reset successful. Your new password is **' . $randomPassword . '**.'; @@ -642,8 +642,8 @@ class UserController if (InputHelper::get('submit')) { $name = InputHelper::get('name'); - $user = Model_User::locate($name); - if (empty($user->email_confirmed)) + $user = UserModel::findByNameOrEmail($name); + if (empty($user->emailConfirmed)) throw new SimpleException('This user has no e-mail confirmed; password reset cannot proceed'); self::sendPasswordResetConfirmation($user); @@ -663,10 +663,10 @@ class UserController if (InputHelper::get('submit')) { $name = InputHelper::get('name'); - $user = Model_User::locate($name); - if (empty($user->email_unconfirmed)) + $user = UserModel::findByNameOrEmail($name); + if (empty($user->emailUnconfirmed)) { - if (!empty($user->email_confirmed)) + if (!empty($user->emailConfirmed)) throw new SimpleException('E-mail was already confirmed; activation skipped'); else throw new SimpleException('This user has no e-mail specified; activation cannot proceed'); diff --git a/src/Database.php b/src/Database.php new file mode 100644 index 00000000..a3cc36a4 --- /dev/null +++ b/src/Database.php @@ -0,0 +1,110 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + } + catch (Exception $e) + { + self::$pdo = null; + throw $e; + } + } + + public static function makeStatement(SqlQuery $sqlQuery) + { + try + { + $stmt = self::$pdo->prepare($sqlQuery->getSql()); + } + catch (Exception $e) + { + throw new Exception('Problem with ' . $sqlQuery->getSql() . ' (' . $e->getMessage() . ')'); + } + foreach ($sqlQuery->getBindings() as $key => $value) + $stmt->bindValue(is_numeric($key) ? $key + 1 : $key, $value); + return $stmt; + } + + public static function disconnect() + { + self::$pdo = null; + } + + public static function connected() + { + return self::$pdo !== null; + } + + public static function query(SqlQuery $sqlQuery) + { + if (!self::connected()) + throw new Exception('Database is not connected'); + $statement = self::makeStatement($sqlQuery); + $statement->execute(); + self::$queries []= $sqlQuery; + return $statement; + } + + public static function fetchOne(SqlQuery $sqlQuery) + { + $statement = self::query($sqlQuery); + return $statement->fetch(); + } + + public static function fetchAll(SqlQuery $sqlQuery) + { + $statement = self::query($sqlQuery); + return $statement->fetchAll(); + } + + public static function getLogs() + { + return self::$queries; + } + + public static function inTransaction() + { + return self::$pdo->inTransaction(); + } + + public static function lastInsertId() + { + return self::$pdo->lastInsertId(); + } + + public static function transaction($func) + { + if (self::inTransaction()) + { + return $func(); + } + else + { + try + { + self::$pdo->beginTransaction(); + $ret = $func(); + self::$pdo->commit(); + return $ret; + } + catch (Exception $e) + { + self::$pdo->rollBack(); + throw $e; + } + } + } +} diff --git a/src/Helpers/LogHelper.php b/src/Helpers/LogHelper.php index f4b1f295..f1ad941a 100644 --- a/src/Helpers/LogHelper.php +++ b/src/Helpers/LogHelper.php @@ -73,7 +73,7 @@ class LogEvent $this->ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; $context = \Chibi\Registry::getContext(); - $tokens['anon'] = Model_User::getAnonymousName(); + $tokens['anon'] = UserModel::getAnonymousName(); if ($context->loggedIn and isset($context->user)) $tokens['user'] = TextHelper::reprUser($context->user->name); else diff --git a/src/Helpers/PrivilegesHelper.php b/src/Helpers/PrivilegesHelper.php index f058ac5d..19a48f62 100644 --- a/src/Helpers/PrivilegesHelper.php +++ b/src/Helpers/PrivilegesHelper.php @@ -42,7 +42,7 @@ class PrivilegesHelper } } - return intval($user->access_rank) >= $minAccessRank; + return intval($user->accessRank) >= $minAccessRank; } public static function confirmWithException($privilege, $subPrivilege = null) @@ -63,7 +63,7 @@ class PrivilegesHelper public static function confirmEmail($user) { - if (!$user->email_confirmed) + if (!$user->emailConfirmed) throw new SimpleException('Need e-mail address confirmation to continue'); } diff --git a/src/Helpers/TextHelper.php b/src/Helpers/TextHelper.php index ac9d1606..82dddb8f 100644 --- a/src/Helpers/TextHelper.php +++ b/src/Helpers/TextHelper.php @@ -17,6 +17,18 @@ class TextHelper return $text; } + //todo: convert to enum and make one method + public static function snakeCaseToCamelCase($string, $lower = false) + { + $string = preg_split('/_/', $string); + $string = array_map('trim', $string); + $string = array_map('ucfirst', $string); + $string = join('', $string); + if ($lower) + $string = lcfirst($string); + return $string; + } + public static function kebabCaseToCamelCase($string) { $string = preg_split('/-/', $string); @@ -152,13 +164,9 @@ class TextHelper foreach ($obj as $key => $val) { - if ($val instanceof RedBean_OODBBean) + if ($val instanceof Exception) { - $set($key, R::exportAll($val)); - } - elseif ($val instanceof Exception) - { - $set($key, ['message' => $val->getMessage(), 'trace' => $val->getTraceAsString()]); + $set($key, ['message' => $val->getMessage(), 'trace' => explode("\n", $val->getTraceAsString())]); } } diff --git a/src/Models/AbstractCrudModel.php b/src/Models/AbstractCrudModel.php new file mode 100644 index 00000000..871f684c --- /dev/null +++ b/src/Models/AbstractCrudModel.php @@ -0,0 +1,136 @@ +select('*') + ->from(static::getTableName()) + ->where('id = ?')->put($key); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('Invalid ' . static::getTableName() . ' ID "' . $key . '"'); + return null; + } + + public static function findByIds(array $ids) + { + $query = (new SqlQuery) + ->select('*') + ->from(static::getTableName()) + ->where('id')->in()->genSlots($ids)->put($ids); + + $rows = Database::fetchAll($query); + if ($rows) + return self::convertRows($rows); + + return []; + } + + public static function getCount() + { + $query = new SqlQuery(); + $query->select('count(1)')->as('count')->from(static::getTableName()); + return Database::fetchOne($query)['count']; + } + + + + + public static function getEntityClassName() + { + $modelClassName = get_called_class(); + $entityClassName = str_replace('Model', 'Entity', $modelClassName); + return $entityClassName; + } + + public static function convertRow($row) + { + $entity = self::spawn(); + foreach ($row as $key => $val) + { + $key = TextHelper::snakeCaseToCamelCase($key, true); + $entity->$key = $val; + } + return $entity; + } + + public static function convertRows(array $rows) + { + foreach ($rows as $i => $row) + $rows[$i] = self::convertRow($row); + return $rows; + } + + + + public static function forgeId($entity) + { + $table = static::getTableName(); + if (!Database::inTransaction()) + throw new Exception('Can be run only within transaction'); + if (!$entity->id) + { + $config = \Chibi\Registry::getConfig(); + $query = (new SqlQuery); + if ($config->main->dbDriver == 'sqlite') + $query->insertInto($table)->defaultValues(); + else + $query->insertInto($table)->values()->open()->close(); + Database::query($query); + $entity->id = Database::lastInsertId(); + } + } + + public static function preloadOneToMany($entities, + $foreignEntityLocalSelector, + $foreignEntityForeignSelector, + $foreignEntityProcessor, + $foreignEntitySetter) + { + if (empty($entities)) + return; + + $foreignIds = []; + $entityMap = []; + foreach ($entities as $entity) + { + $foreignId = $foreignEntityLocalSelector($entity); + if (!isset($entityMap[$foreignId])) + $entityMap[$foreignId] = []; + $entityMap[$foreignId] []= $entity; + $foreignIds []= $foreignId; + } + + $foreignEntities = $foreignEntityProcessor($foreignIds); + + foreach ($foreignEntities as $foreignEntity) + { + $key = $foreignEntityForeignSelector($foreignEntity); + foreach ($entityMap[$key] as $entity) + $foreignEntitySetter($entity, $foreignEntity); + } + } +} diff --git a/src/Models/AbstractModel.php b/src/Models/AbstractModel.php deleted file mode 100644 index fa5507f9..00000000 --- a/src/Models/AbstractModel.php +++ /dev/null @@ -1,100 +0,0 @@ -getNew()->begin(); - $dbQuery->select($table . '.*'); - $builder = static::getQueryBuilder(); - if ($builder) - $builder::build($dbQuery, $query); - else - $dbQuery->from($table); - if ($perPage !== null) - { - $dbQuery->limit('?')->put($perPage); - $dbQuery->offset('?')->put(($page - 1) * $perPage); - } - - $rows = $dbQuery->get(); - return $rows; - } - - protected static function convertRows($rows, $table, $fast = false) - { - if (empty($rows)) - return []; - - if (!$fast) - return R::convertToBeans($table, $rows); - - $entities = R::dispense($table, count($rows)); - if (count($rows) == 1) - $entities = [$entities]; - $entity = reset($entities); - foreach ($rows as $row) - { - $entity->import($row); - $entity = next($entities); - } - reset($entities); - return $entities; - } - - public static function getEntities($query, $perPage = null, $page = 1, $fast = false) - { - $table = static::getTableName(); - $rows = self::getEntitiesRows($query, $perPage, $page); - $entities = self::convertRows($rows, $table, $fast); - return $entities; - } - - public static function getEntityCount($query) - { - $table = static::getTableName(); - $dbQuery = R::$f->getNew()->begin(); - $dbQuery->select('COUNT(1)')->as('count'); - $builder = static::getQueryBuilder(); - if ($builder) - $builder::build($dbQuery, $query); - else - $dbQuery->from($table); - $ret = intval($dbQuery->get('row')['count']); - return $ret; - } - - public static function getEntitiesWithCount($query, $perPage = null, $page = 1) - { - return - [ - self::getEntities($query, $perPage, $page, true), - self::getEntityCount($query) - ]; - } - - 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); - } -} diff --git a/src/Models/AbstractQueryBuilder.php b/src/Models/AbstractQueryBuilder.php deleted file mode 100644 index f736b114..00000000 --- a/src/Models/AbstractQueryBuilder.php +++ /dev/null @@ -1,5 +0,0 @@ -commentDate = time(); + return $comment; + } + + public static function save($comment) + { + Database::transaction(function() use ($comment) + { + self::forgeId($comment); + + $bindings = [ + 'text' => $comment->text, + 'post_id' => $comment->postId, + 'comment_date' => $comment->commentDate, + 'commenter_id' => $comment->commenterId]; + + $query = (new SqlQuery) + ->update('comment') + ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) + ->put(array_values($bindings)) + ->where('id = ?')->put($comment->id); + + Database::query($query); + }); + } + + public static function remove($comment) + { + Database::transaction(function() use ($comment) + { + $query = (new SqlQuery) + ->deleteFrom('comment') + ->where('id = ?')->put($comment->id); + Database::query($query); + }); + } + + + + public static function findAllByPostId($key) + { + $query = new SqlQuery(); + $query + ->select('comment.*') + ->from('comment') + ->where('post_id = ?') + ->put($key); + + $rows = Database::fetchAll($query); + if ($rows) + return self::convertRows($rows); + return []; + } + + + + public static function preloadCommenters($comments) + { + self::preloadOneToMany($comments, + function($comment) { return $comment->commenterId; }, + function($user) { return $user->id; }, + function($userIds) { return UserModel::findByIds($userIds); }, + function($comment, $user) { return $comment->setCache('commenter', $user); }); + } + + public static function preloadPosts($comments) + { + self::preloadOneToMany($comments, + function($comment) { return $comment->postId; }, + function($post) { return $post->id; }, + function($postIds) { return PostModel::findByIds($postIds); }, + function($comment, $post) { $comment->setCache('post', $post); }); + } + + + + public static function validateText($text) + { + $text = trim($text); + $config = \Chibi\Registry::getConfig(); + + if (strlen($text) < $config->comments->minLength) + throw new SimpleException(sprintf('Comment must have at least %d characters', $config->comments->minLength)); + + if (strlen($text) > $config->comments->maxLength) + throw new SimpleException(sprintf('Comment must have at most %d characters', $config->comments->maxLength)); + + return $text; + } +} diff --git a/src/Models/Entities/AbstractEntity.php b/src/Models/Entities/AbstractEntity.php new file mode 100644 index 00000000..d83d4b13 --- /dev/null +++ b/src/Models/Entities/AbstractEntity.php @@ -0,0 +1,23 @@ +__cache[$key] = $value; + } + + public function getCache($key) + { + return isset($this->__cache[$key]) + ? $this->__cache[$key] + : null; + } + + public function hasCache($key) + { + return isset($this->__cache[$key]); + } +} diff --git a/src/Models/Entities/CommentEntity.php b/src/Models/Entities/CommentEntity.php new file mode 100644 index 00000000..98c544e7 --- /dev/null +++ b/src/Models/Entities/CommentEntity.php @@ -0,0 +1,43 @@ +text); + } + + public function setPost($post) + { + $this->setCache('post', $post); + $this->postId = $post->id; + } + + public function setCommenter($user) + { + $this->setCache('commenter', $user); + $this->commenterId = $user ? $user->id : null; + } + + public function getPost() + { + if ($this->hasCache('post')) + return $this->getCache('post'); + $post = PostModel::findById($this->postId); + $this->setCache('post', $post); + return $post; + } + + public function getCommenter() + { + if ($this->hasCache('commenter')) + return $this->getCache('commenter'); + $user = UserModel::findById($this->commenterId, false); + $this->setCache('commenter', $user); + return $user; + } +} diff --git a/src/Models/Entities/PostEntity.php b/src/Models/Entities/PostEntity.php new file mode 100644 index 00000000..4cf3a09c --- /dev/null +++ b/src/Models/Entities/PostEntity.php @@ -0,0 +1,423 @@ +hasCache('uploader')) + return $this->getCache('uploader'); + $uploader = UserModel::findById($this->uploaderId, false); + $this->setCache('uploader', $uploader); + return $uploader; + } + + public function setUploader($user) + { + $this->uploaderId = $user->id; + $this->setCache('uploader', $user); + } + + + + public function getComments() + { + if ($this->hasCache('comments')) + return $this->getCache('comments'); + $comments = CommentModel::findAllByPostId($this->id); + $this->setCache('comments', $comments); + return $comments; + } + + + + + public function getFavorites() + { + if ($this->hasCache('favoritee')) + return $this->getCache('favoritee'); + $query = (new SqlQuery) + ->select('user.*') + ->from('user') + ->innerJoin('favoritee')->on('favoritee.user_id = user.id') + ->where('favoritee.post_id = ?')->put($this->id); + $rows = Database::fetchAll($query); + $favorites = UserModel::convertRows($rows); + $this->setCache('favoritee', $favorites); + return $favorites; + } + + + + public function getRelations() + { + if ($this->hasCache('relations')) + return $this->getCache('relations'); + + $query = (new SqlQuery) + ->select('post.*') + ->from('post') + ->innerJoin('crossref') + ->on()->open()->raw('post.id = crossref.post2_id')->and('crossref.post_id = :id')->close() + ->or()->open()->raw('post.id = crossref.post_id')->and('crossref.post2_id = :id')->close() + ->put(['id' => $this->id]); + $rows = Database::fetchAll($query); + $posts = PostModel::convertRows($rows); + $this->setCache('relations', $posts); + return $posts; + } + + public function setRelations(array $relations) + { + foreach ($relations as $relatedPost) + if (!$relatedPost->id) + throw new Exception('All related posts must be saved'); + $uniqueRelations = []; + foreach ($relations as $relatedPost) + $uniqueRelations[$relatedPost->id] = $relatedPost; + $relations = array_values($uniqueRelations); + $this->setCache('relations', $relations); + } + + public function setRelationsFromText($relationsText) + { + $config = \Chibi\Registry::getConfig(); + $relatedIds = array_filter(preg_split('/\D/', $relationsText)); + + $relatedPosts = []; + foreach ($relatedIds as $relatedId) + { + if ($relatedId == $this->id) + continue; + + if (count($relatedPosts) > $config->browsing->maxRelatedPosts) + throw new SimpleException('Too many related posts (maximum: ' . $config->browsing->maxRelatedPosts . ')'); + + $relatedPosts []= PostModel::findById($relatedId); + } + + $this->setRelations($relatedPosts); + } + + + + public function getTags() + { + if ($this->hasCache('tags')) + return $this->getCache('tags'); + $tags = TagModel::findAllByPostId($this->id); + $this->setCache('tags', $tags); + return $tags; + } + + public function setTags(array $tags) + { + foreach ($tags as $tag) + if (!$tag->id) + throw new Exception('All tags must be saved'); + $uniqueTags = []; + foreach ($tags as $tag) + $uniqueTags[$tag->id] = $tag; + $tags = array_values($uniqueTags); + $this->setCache('tags', $tags); + } + + public function setTagsFromText($tagsText) + { + $tagNames = TagModel::validateTags($tagsText); + $tags = []; + foreach ($tagNames as $tagName) + { + $tag = TagModel::findByName($tagName, false); + if (!$tag) + { + $tag = TagModel::spawn(); + $tag->name = $tagName; + TagModel::save($tag); + } + $tags []= $tag; + } + $this->setTags($tags); + } + + public function isTaggedWith($tagName) + { + $tagName = trim(strtolower($tagName)); + foreach ($this->getTags() as $tag) + if (trim(strtolower($tag->name)) == $tagName) + return true; + return false; + } + + + + + public function setHidden($hidden) + { + $this->hidden = boolval($hidden); + } + + public function setSafety($safety) + { + $this->safety = PostModel::validateSafety($safety); + } + + public function setSource($source) + { + $this->source = PostModel::validateSource($source); + } + + + public function getThumbCustomPath($width = null, $height = null) + { + return PostModel::getThumbCustomPath($this->name, $width, $height); + } + + public function getThumbDefaultPath($width = null, $height = null) + { + return PostModel::getThumbDefaultPath($this->name, $width, $height); + } + + public function getFullPath() + { + return PostModel::getFullPath($this->name); + } + + public function hasCustomThumb($width = null, $height = null) + { + $thumbPath = $this->getThumbCustomPath($width, $height); + return file_exists($thumbPath); + } + + public function setCustomThumbnailFromPath($srcPath) + { + $config = \Chibi\Registry::getConfig(); + + $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 != $config->browsing->thumbWidth) + throw new SimpleException('Invalid thumbnail width (should be ' . $config->browsing->thumbWidth . ')'); + if ($imageHeight != $config->browsing->thumbHeight) + throw new SimpleException('Invalid thumbnail height (should be ' . $config->browsing->thumbHeight . ')'); + + $dstPath = $this->getThumbCustomPath(); + + if (is_uploaded_file($srcPath)) + move_uploaded_file($srcPath, $dstPath); + else + rename($srcPath, $dstPath); + } + + public function makeThumb($width = null, $height = null) + { + list ($width, $height) = PostModel::validateThumbSize($width, $height); + $dstPath = $this->getThumbDefaultPath($width, $height); + $srcPath = $this->getFullPath(); + + if ($this->type == PostType::Youtube) + { + $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg'; + $contents = file_get_contents('http://img.youtube.com/vi/' . $this->origName . '/mqdefault.jpg'); + file_put_contents($tmpPath, $contents); + if (file_exists($tmpPath)) + $srcImage = imagecreatefromjpeg($tmpPath); + } + else switch ($this->mimeType) + { + 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; + + $config = \Chibi\Registry::getConfig(); + switch ($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; + } + + + + public function setContentFromPath($srcPath) + { + $this->fileSize = filesize($srcPath); + $this->fileHash = md5_file($srcPath); + + if ($this->fileSize == 0) + throw new SimpleException('Specified file is empty'); + + $this->mimeType = mime_content_type($srcPath); + switch ($this->mimeType) + { + case 'image/gif': + case 'image/png': + case 'image/jpeg': + list ($imageWidth, $imageHeight) = getimagesize($srcPath); + $this->type = PostType::Image; + $this->imageWidth = $imageWidth; + $this->imageHeight = $imageHeight; + break; + case 'application/x-shockwave-flash': + list ($imageWidth, $imageHeight) = getimagesize($srcPath); + $this->type = PostType::Flash; + $this->imageWidth = $imageWidth; + $this->imageHeight = $imageHeight; + break; + default: + throw new SimpleException('Invalid file type "' . $this->mimeType . '"'); + } + + $this->origName = basename($srcPath); + $duplicatedPost = PostModel::findByHash($this->fileHash, false); + if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id)) + throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); + + $dstPath = $this->getFullPath(); + + if (is_uploaded_file($srcPath)) + move_uploaded_file($srcPath, $dstPath); + else + rename($srcPath, $dstPath); + + $thumbPath = $this->getThumbDefaultPath(); + if (file_exists($thumbPath)) + unlink($thumbPath); + } + + public function setContentFromUrl($srcUrl) + { + $this->origName = $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->origName = $origName; + $this->type = PostType::Youtube; + $this->mimeType = null; + $this->fileSize = null; + $this->fileHash = $origName; + $this->imageWidth = null; + $this->imageHeight = null; + + $thumbPath = $this->getThumbDefaultPath(); + if (file_exists($thumbPath)) + unlink($thumbPath); + + $duplicatedPost = PostModel::findByHash($origName, false); + if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id)) + 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); + } + } +} diff --git a/src/Models/Entities/TagEntity.php b/src/Models/Entities/TagEntity.php new file mode 100644 index 00000000..13647580 --- /dev/null +++ b/src/Models/Entities/TagEntity.php @@ -0,0 +1,14 @@ +select('count(*)')->as('count') + ->from('post_tag') + ->where('tag_id = ?')->put($this->id); + return Database::fetchOne($query)['count']; + } +} diff --git a/src/Models/Entities/TokenEntity.php b/src/Models/Entities/TokenEntity.php new file mode 100644 index 00000000..7ed81585 --- /dev/null +++ b/src/Models/Entities/TokenEntity.php @@ -0,0 +1,18 @@ +userId); + } + + public function setUser($user) + { + $this->userId = $user ? $user->id : null; + } +} diff --git a/src/Models/Entities/UserEntity.php b/src/Models/Entities/UserEntity.php new file mode 100644 index 00000000..9820b631 --- /dev/null +++ b/src/Models/Entities/UserEntity.php @@ -0,0 +1,151 @@ +emailConfirmed) + ? $this->emailConfirmed + : $this->passSalt . $this->name; + $hash = md5(strtolower(trim($subject))); + $url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro'; + return $url; + } + + public function getSetting($key) + { + $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(UserModel::SETTING_SAFETY); + if (!$all) + return $safety == PostSafety::Safe; + return $all & PostSafety::toFlag($safety); + } + + public function enableSafety($safety, $enabled) + { + $all = $this->getSetting(UserModel::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(UserModel::SETTING_SAFETY, $new); + } + + public function hasEnabledHidingDislikedPosts() + { + $ret = $this->getSetting(UserModel::SETTING_HIDE_DISLIKED_POSTS); + if ($ret === null) + $ret = !\Chibi\Registry::getConfig()->browsing->showDislikedPostsDefault; + return $ret; + } + + public function enableHidingDislikedPosts($enabled) + { + $this->setSetting(UserModel::SETTING_HIDE_DISLIKED_POSTS, $enabled ? 1 : 0); + } + + public function hasEnabledPostTagTitles() + { + $ret = $this->getSetting(UserModel::SETTING_POST_TAG_TITLES); + if ($ret === null) + $ret = \Chibi\Registry::getConfig()->browsing->showPostTagTitlesDefault; + return $ret; + } + + public function enablePostTagTitles($enabled) + { + $this->setSetting(UserModel::SETTING_POST_TAG_TITLES, $enabled ? 1 : 0); + } + + public function hasEnabledEndlessScrolling() + { + $ret = $this->getSetting(UserModel::SETTING_ENDLESS_SCROLLING); + if ($ret === null) + $ret = \Chibi\Registry::getConfig()->browsing->endlessScrollingDefault; + return $ret; + } + + public function enableEndlessScrolling($enabled) + { + $this->setSetting(UserModel::SETTING_ENDLESS_SCROLLING, $enabled ? 1 : 0); + } + + public function hasFavorited($post) + { + $query = (new SqlQuery) + ->select('count(1)')->as('count') + ->from('favoritee') + ->where('user_id = ?')->put($this->id) + ->and('post_id = ?')->put($post->id); + return Database::fetchOne($query)['count'] == 1; + } + + public function getScore($post) + { + $query = (new SqlQuery) + ->select('score') + ->from('post_score') + ->where('user_id = ?')->put($this->id) + ->and('post_id = ?')->put($post->id); + $row = Database::fetchOne($query); + if ($row) + return intval($row['score']); + return null; + } + + public function getFavoriteCount() + { + $sqlQuery = (new SqlQuery) + ->select('count(1)')->as('count') + ->from('favoritee') + ->where('user_id = ?')->put($this->id); + return Database::fetchOne($sqlQuery)['count']; + } + + public function getCommentCount() + { + $sqlQuery = (new SqlQuery) + ->select('count(1)')->as('count') + ->from('comment') + ->where('commenter_id = ?')->put($this->id); + return Database::fetchOne($sqlQuery)['count']; + } +} diff --git a/src/Models/AccessRank.php b/src/Models/Enums/AccessRank.php similarity index 100% rename from src/Models/AccessRank.php rename to src/Models/Enums/AccessRank.php diff --git a/src/Models/PostSafety.php b/src/Models/Enums/PostSafety.php similarity index 100% rename from src/Models/PostSafety.php rename to src/Models/Enums/PostSafety.php diff --git a/src/Models/PostType.php b/src/Models/Enums/PostType.php similarity index 100% rename from src/Models/PostType.php rename to src/Models/Enums/PostType.php diff --git a/src/Models/Privilege.php b/src/Models/Enums/Privilege.php similarity index 100% rename from src/Models/Privilege.php rename to src/Models/Enums/Privilege.php diff --git a/src/Models/IModel.php b/src/Models/IModel.php new file mode 100644 index 00000000..b4d9dc7b --- /dev/null +++ b/src/Models/IModel.php @@ -0,0 +1,5 @@ +comments->minLength) - throw new SimpleException(sprintf('Comment must have at least %d characters', $config->comments->minLength)); - - if (strlen($text) > $config->comments->maxLength) - throw new SimpleException(sprintf('Comment must have at most %d characters', $config->comments->maxLength)); - - return $text; - } - - public function getText() - { - return TextHelper::parseMarkdown($this->text); - } -} diff --git a/src/Models/Model_Comment_QueryBuilder.php b/src/Models/Model_Comment_QueryBuilder.php deleted file mode 100644 index e94a9b66..00000000 --- a/src/Models/Model_Comment_QueryBuilder.php +++ /dev/null @@ -1,13 +0,0 @@ -from('comment') - ->where('post_id') - ->is()->not('NULL') - ->orderBy('id') - ->desc(); - } -} diff --git a/src/Models/Model_Post.php b/src/Models/Model_Post.php deleted file mode 100644 index f39fca47..00000000 --- a/src/Models/Model_Post.php +++ /dev/null @@ -1,460 +0,0 @@ -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); - - if (!in_array($safety, PostSafety::getAll())) - throw new SimpleException('Invalid safety type "' . $safety . '"'); - - return $safety; - } - - public static function validateSource($source) - { - $source = trim($source); - - $maxLength = 200; - if (strlen($source) > $maxLength) - throw new SimpleException('Source must have at most ' . $maxLength . ' characters'); - - return $source; - } - - private static function validateThumbSize($width, $height) - { - $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 getAllPostCount() - { - 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::absolutePath(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 TextHelper::absolutePath(self::$config->main->filesPath . DS . $name); - } - - public static function attachTags($posts) - { - //slow!!! - //R::preload($posts, 'sharedTag|tag'); - $ids = array_map(function($x) { return $x->id; }, $posts); - $sql = 'SELECT post_tag.post_id, tag.* FROM tag INNER JOIN post_tag ON post_tag.tag_id = tag.id WHERE post_id IN (' . R::genSlots($ids) . ')'; - $rows = R::getAll($sql, $ids); - $postMap = array_fill_keys($ids, []); - foreach ($rows as $row) - { - $postMap[$row['post_id']] []= $row; - } - foreach ($posts as $post) - { - $tagRows = $postMap[$post->id]; - $tags = self::convertRows($tagRows, 'tag', true); - $post->setProperty('sharedTag', $tags, true, true); - } - } - - public function isTaggedWith($tagName) - { - $tagName = trim(strtolower($tagName)); - foreach ($this->sharedTag as $tag) - if (trim(strtolower($tag->name)) == $tagName) - return true; - return false; - } - - public function hasCustomThumb($width = null, $height = null) - { - $thumbPath = self::getThumbCustomPath($this->name, $width, $height); - return file_exists($thumbPath); - } - - - - 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->file_size = filesize($srcPath); - $this->file_hash = md5_file($srcPath); - - if ($this->file_size == 0) - throw new SimpleException('Specified file is empty'); - - $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); - $duplicatedPost = R::findOne('post', 'file_hash = ?', [$this->file_hash]); - if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id)) - 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); - - $thumbPath = self::getThumbDefaultPath($this->name); - if (file_exists($thumbPath)) - unlink($thumbPath); - } - - - - 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; - - $thumbPath = self::getThumbDefaultPath($this->name); - if (file_exists($thumbPath)) - unlink($thumbPath); - - $duplicatedPost = R::findOne('post', 'orig_name = ?', [$origName]); - if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id)) - 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_Property.php b/src/Models/Model_Property.php deleted file mode 100644 index 63b23dc9..00000000 --- a/src/Models/Model_Property.php +++ /dev/null @@ -1,38 +0,0 @@ -prop_id] = $prop->value; - } - } - return isset(self::$allProperties[$propertyId]) - ? self::$allProperties[$propertyId] - : null; - } - - public static function set($propertyId, $value) - { - $row = R::findOne('property', 'prop_id = ?', [$propertyId]); - if (!$row) - { - $row = R::dispense('property'); - $row->prop_id = $propertyId; - } - $row->value = $value; - self::$allProperties[$propertyId] = $value; - R::store($row); - } -} diff --git a/src/Models/Model_Tag.php b/src/Models/Model_Tag.php deleted file mode 100644 index a6373049..00000000 --- a/src/Models/Model_Tag.php +++ /dev/null @@ -1,107 +0,0 @@ -begin() - ->select('id, name') - ->from(self::getTableName()) - ->where() - ->not()->exists() - ->open() - ->select('1') - ->from('post_tag') - ->where('post_tag.tag_id = tag.id') - ->close(); - $rows = $dbQuery->get(); - $entities = R::convertToBeans(self::getTableName(), $rows); - R::trashAll($entities); - } - - public static function insertOrUpdate($tags) - { - $dbTags = []; - foreach ($tags as $tag) - { - $dbTag = self::locate($tag, false); - if (!$dbTag) - { - $dbTag = R::dispense(self::getTableName()); - $dbTag->name = $tag; - R::store($dbTag); - } - $dbTags []= $dbTag; - } - return $dbTags; - } - - public static function validateTag($tag) - { - $tag = trim($tag); - - $minLength = 1; - $maxLength = 64; - if (strlen($tag) < $minLength) - throw new SimpleException('Tag must have at least ' . $minLength . ' characters'); - if (strlen($tag) > $maxLength) - throw new SimpleException('Tag must have at most ' . $maxLength . ' characters'); - - if (!preg_match('/^[()\[\]a-zA-Z0-9_.-]+$/i', $tag)) - throw new SimpleException('Invalid tag "' . $tag . '"'); - - if (preg_match('/^\.\.?$/', $tag)) - throw new SimpleException('Invalid tag "' . $tag . '"'); - - return $tag; - } - - public static function validateTags($tags) - { - $tags = trim($tags); - $tags = preg_split('/[,;\s]+/', $tags); - $tags = array_filter($tags, function($x) { return $x != ''; }); - $tags = array_unique($tags); - - foreach ($tags as $key => $tag) - $tags[$key] = self::validateTag($tag); - - if (empty($tags)) - throw new SimpleException('No tags set'); - - return $tags; - } - - public function getPostCount() - { - if ($this->bean->getMeta('post_count')) - return $this->bean->getMeta('post_count'); - return $this->bean->countShared('post'); - } -} diff --git a/src/Models/Model_Token.php b/src/Models/Model_Token.php deleted file mode 100644 index aff9a16f..00000000 --- a/src/Models/Model_Token.php +++ /dev/null @@ -1,25 +0,0 @@ -used) - throw new SimpleException('This token was already used'); - - if ($token->expires !== null and time() > $token->expires) - throw new SimpleException('This token has expired'); - - return $token; - } -} diff --git a/src/Models/Model_User.php b/src/Models/Model_User.php deleted file mode 100644 index 85c98b15..00000000 --- a/src/Models/Model_User.php +++ /dev/null @@ -1,300 +0,0 @@ -pass_salt = md5(mt_rand() . uniqid()); - return $user; - } - - public static function remove($user) - { - //remove stuff from auxiliary tables - 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 = []; - R::store($user); - R::trash($user); - } - - public static function save($user) - { - R::store($user); - } - - - - public static function getAnonymousName() - { - return '[Anonymous user]'; - } - - public static function validateUserName($userName) - { - $userName = trim($userName); - - $dbUser = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$userName]); - if ($dbUser !== null) - { - if (!$dbUser->email_confirmed and \Chibi\Registry::getConfig()->registration->needEmailForRegistering) - throw new SimpleException('User with this name is already registered and awaits e-mail confirmation'); - - if (!$dbUser->staff_confirmed and \Chibi\Registry::getConfig()->registration->staffActivation) - throw new SimpleException('User with this name is already registered and awaits staff confirmation'); - - throw new SimpleException('User with this name is already registered'); - } - - $userNameMinLength = intval(\Chibi\Registry::getConfig()->registration->userNameMinLength); - $userNameMaxLength = intval(\Chibi\Registry::getConfig()->registration->userNameMaxLength); - $userNameRegex = \Chibi\Registry::getConfig()->registration->userNameRegex; - - if (strlen($userName) < $userNameMinLength) - throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength)); - - if (strlen($userName) > $userNameMaxLength) - throw new SimpleException(sprintf('User name must have at most %d characters', $userNameMaxLength)); - - if (!preg_match($userNameRegex, $userName)) - throw new SimpleException('User name contains invalid characters'); - - return $userName; - } - - public static function validatePassword($password) - { - $passMinLength = intval(\Chibi\Registry::getConfig()->registration->passMinLength); - $passRegex = \Chibi\Registry::getConfig()->registration->passRegex; - - if (strlen($password) < $passMinLength) - throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength)); - - if (!preg_match($passRegex, $password)) - throw new SimpleException('Password contains invalid characters'); - - return $password; - } - - public static function validateEmail($email) - { - $email = trim($email); - - if (!empty($email) and !TextHelper::isValidEmail($email)) - throw new SimpleException('E-mail address appears to be invalid'); - - return $email; - } - - public static function validateAccessRank($accessRank) - { - $accessRank = intval($accessRank); - - if (!in_array($accessRank, AccessRank::getAll())) - throw new SimpleException('Invalid access rank type "' . $accessRank . '"'); - - if ($accessRank == AccessRank::Nobody) - throw new SimpleException('Cannot set special accesss rank "' . $accessRank . '"'); - - return $accessRank; - } - - public static function hashPassword($pass, $salt2) - { - $salt1 = \Chibi\Registry::getConfig()->main->salt; - return sha1($salt1 . $salt2 . $pass); - } - - - - public function getAvatarUrl($size = 32) - { - $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 function getSetting($key) - { - $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 hasEnabledHidingDislikedPosts() - { - $ret = $this->getSetting(self::SETTING_HIDE_DISLIKED_POSTS); - if ($ret === null) - $ret = !\Chibi\Registry::getConfig()->browsing->showDislikedPostsDefault; - return $ret; - } - - public function enableHidingDislikedPosts($enabled) - { - $this->setSetting(self::SETTING_HIDE_DISLIKED_POSTS, $enabled ? 1 : 0); - } - - public function hasEnabledPostTagTitles() - { - $ret = $this->getSetting(self::SETTING_POST_TAG_TITLES); - if ($ret === null) - $ret = \Chibi\Registry::getConfig()->browsing->showPostTagTitlesDefault; - return $ret; - } - - public function enablePostTagTitles($enabled) - { - $this->setSetting(self::SETTING_POST_TAG_TITLES, $enabled ? 1 : 0); - } - - 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->via('favoritee')->sharedPost as $favPost) - if ($favPost->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/Model_User_QueryBuilder.php b/src/Models/Model_User_QueryBuilder.php deleted file mode 100644 index c1c44bea..00000000 --- a/src/Models/Model_User_QueryBuilder.php +++ /dev/null @@ -1,31 +0,0 @@ -from('user'); - - switch ($sortStyle) - { - case 'alpha,asc': - $dbQuery->orderBy('name')->asc(); - break; - case 'alpha,desc': - $dbQuery->orderBy('name')->desc(); - break; - case 'date,asc': - $dbQuery->orderBy('join_date')->asc(); - break; - case 'date,desc': - $dbQuery->orderBy('join_date')->desc(); - break; - case 'pending': - $dbQuery->where('staff_confirmed IS NULL'); - $dbQuery->or('staff_confirmed = 0'); - break; - default: - throw new SimpleException('Unknown sort style'); - } - } -} diff --git a/src/Models/PostModel.php b/src/Models/PostModel.php new file mode 100644 index 00000000..b64ee846 --- /dev/null +++ b/src/Models/PostModel.php @@ -0,0 +1,281 @@ +hidden = false; + $post->uploadDate = time(); + do + { + $post->name = md5(mt_rand() . uniqid()); + } + while (file_exists($post->getFullPath())); + return $post; + } + + public static function save($post) + { + Database::transaction(function() use ($post) + { + self::forgeId($post); + + $bindings = [ + 'type' => $post->type, + 'name' => $post->name, + 'orig_name' => $post->origName, + 'file_hash' => $post->fileHash, + 'file_size' => $post->fileSize, + 'mime_type' => $post->mimeType, + 'safety' => $post->safety, + 'hidden' => $post->hidden, + 'upload_date' => $post->uploadDate, + 'image_width' => $post->imageWidth, + 'image_height' => $post->imageHeight, + 'uploader_id' => $post->uploaderId, + 'source' => $post->source, + ]; + + $query = (new SqlQuery) + ->update('post') + ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) + ->put(array_values($bindings)) + ->where('id = ?')->put($post->id); + Database::query($query); + + //tags + $tags = $post->getTags(); + + $query = (new SqlQuery) + ->deleteFrom('post_tag') + ->where('post_id = ?')->put($post->id); + Database::query($query); + + foreach ($tags as $postTag) + { + $query = (new SqlQuery) + ->insertInto('post_tag') + ->surround('post_id, tag_id') + ->values()->surround('?, ?') + ->put([$post->id, $postTag->id]); + Database::query($query); + } + + //relations + $relations = $post->getRelations(); + + $query = (new SqlQuery) + ->deleteFrom('crossref') + ->where('post_id = ?')->put($post->id) + ->or('post2_id = ?')->put($post->id); + Database::query($query); + + foreach ($relations as $relatedPost) + { + $query = (new SqlQuery) + ->insertInto('crossref') + ->surround('post_id, post2_id') + ->values()->surround('?, ?') + ->put([$post->id, $relatedPost->id]); + Database::query($query); + } + }); + } + + public static function remove($post) + { + Database::transaction(function() use ($post) + { + $queries = []; + + $queries []= (new SqlQuery) + ->deleteFrom('post_score') + ->where('post_id = ?')->put($post->id); + + $queries []= (new SqlQuery) + ->deleteFrom('post_tag') + ->where('post_id = ?')->put($post->id); + + $queries []= (new SqlQuery) + ->deleteFrom('crossref') + ->where('post_id = ?')->put($post->id) + ->or('post2_id = ?')->put($post->id); + + $queries []= (new SqlQuery) + ->deleteFrom('favoritee') + ->where('post_id = ?')->put($post->id); + + $queries []= (new SqlQuery) + ->update('comment') + ->set('post_id = NULL') + ->where('post_id = ?')->put($post->id); + + $queries []= (new SqlQuery) + ->deleteFrom('post') + ->where('id = ?')->put($post->id); + + foreach ($queries as $query) + Database::query($query); + }); + } + + + + + public static function findByName($key, $throw = true) + { + $query = (new SqlQuery) + ->select('*') + ->from('post') + ->where('name = ?')->put($key); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('Invalid post name "' . $key . '"'); + return null; + } + + public static function findByIdOrName($key, $throw = true) + { + if (is_numeric($key)) + $post = self::findById($key, $throw); + else + $post = self::findByName($key, $throw); + return $post; + } + + public static function findByHash($key, $throw = true) + { + $query = (new SqlQuery) + ->select('*') + ->from('post') + ->where('file_hash = ?')->put($key); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('Invalid post hash "' . $hash . '"'); + return null; + } + + + + public static function preloadTags($posts) + { + if (empty($posts)) + return; + + $postMap = []; + $tagsMap = []; + foreach ($posts as $post) + { + $postId = $post->id; + $postMap[$postId] = $post; + $tagsMap[$postId] = []; + } + $postIds = array_keys($postMap); + + $sqlQuery = (new SqlQuery) + ->select('tag.*, post_id') + ->from('tag') + ->innerJoin('post_tag')->on('post_tag.tag_id = tag.id') + ->where('post_id')->in()->genSlots($postIds)->put($postIds); + $rows = Database::fetchAll($sqlQuery); + + foreach ($rows as $row) + { + if (isset($tags[$row['id']])) + continue; + unset($row['post_id']); + $tag = TagModel::convertRow($row); + $tags[$row['id']] = $tag; + } + + foreach ($rows as $row) + { + $postId = $row['post_id']; + $tagsMap[$postId] []= $tags[$row['id']]; + } + + foreach ($tagsMap as $postId => $tags) + { + $postMap[$postId]->setCache('tags', $tags); + } + } + + + + public static function validateSafety($safety) + { + $safety = intval($safety); + + if (!in_array($safety, PostSafety::getAll())) + throw new SimpleException('Invalid safety type "' . $safety . '"'); + + return $safety; + } + + public static function validateSource($source) + { + $source = trim($source); + + $maxLength = 200; + if (strlen($source) > $maxLength) + throw new SimpleException('Source must have at most ' . $maxLength . ' characters'); + + return $source; + } + + public static function validateThumbSize($width, $height) + { + $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]; + } + + private static function getThumbPathTokenized($text, $name, $width = null, $height = null) + { + list ($width, $height) = self::validateThumbSize($width, $height); + + return TextHelper::absolutePath(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 TextHelper::absolutePath(self::$config->main->filesPath . DS . $name); + } +} + +PostModel::init(); diff --git a/src/Models/PropertyModel.php b/src/Models/PropertyModel.php new file mode 100644 index 00000000..31e4eaa9 --- /dev/null +++ b/src/Models/PropertyModel.php @@ -0,0 +1,73 @@ +select('*')->from('property'); + foreach (Database::fetchAll($query) as $row) + self::$allProperties[$row['prop_id']] = $row['value']; + } + } + + public static function get($propertyId) + { + self::loadIfNecessary(); + return isset(self::$allProperties[$propertyId]) + ? self::$allProperties[$propertyId] + : null; + } + + public static function set($propertyId, $value) + { + self::loadIfNecessary(); + Database::transaction(function() use ($propertyId, $value) + { + $row = Database::query((new SqlQuery) + ->select('id') + ->from('property') + ->where('prop_id = ?') + ->put($propertyId)); + + $query = (new SqlQuery); + + if ($row) + { + $query + ->update('property') + ->set('value = ?') + ->put($value) + ->where('prop_id = ?') + ->put($propertyId); + } + else + { + $query + ->insertInto('property') + ->open()->raw('prop_id, value_id')->close() + ->open()->raw('?, ?')->close() + ->put([$propertyId, $value]); + } + + Database::query($query); + + self::$allProperties[$propertyId] = $value; + }); + } +} diff --git a/src/Models/SearchServices/AbstractQueryBuilder.php b/src/Models/SearchServices/AbstractQueryBuilder.php new file mode 100644 index 00000000..b8456494 --- /dev/null +++ b/src/Models/SearchServices/AbstractQueryBuilder.php @@ -0,0 +1,56 @@ +limit('?')->put($perPage); + $sqlQuery->offset('?')->put(($page - 1) * $perPage); + } + + static function getEntitiesRows($searchQuery, $perPage = null, $page = 1) + { + $modelClassName = self::getModelClassName(); + $table = $modelClassName::getTableName(); + + $sqlQuery = new SqlQuery(); + $sqlQuery->select($table . '.*'); + static::decorate($sqlQuery, $searchQuery); + self::decoratePager($sqlQuery, $perPage, $page); + + $rows = Database::fetchAll($sqlQuery); + return $rows; + } + + static function getEntities($searchQuery, $perPage = null, $page = 1) + { + $modelClassName = self::getModelClassName(); + $rows = static::getEntitiesRows($searchQuery, $perPage, $page); + return $modelClassName::convertRows($rows); + } + + static function getEntityCount($searchQuery) + { + $modelClassName = self::getModelClassName(); + $table = $modelClassName::getTableName(); + + $sqlQuery = new SqlQuery(); + $sqlQuery->select('count(1)')->as('count'); + static::decorate($sqlQuery, $searchQuery); + + return Database::fetchOne($sqlQuery)['count']; + } +} diff --git a/src/Models/SearchServices/CommentSearchService.php b/src/Models/SearchServices/CommentSearchService.php new file mode 100644 index 00000000..f4a606ee --- /dev/null +++ b/src/Models/SearchServices/CommentSearchService.php @@ -0,0 +1,13 @@ +from('comment') + ->where('post_id') + ->is()->not('NULL') + ->orderBy('id') + ->desc(); + } +} diff --git a/src/Models/Model_Post_QueryBuilder.php b/src/Models/SearchServices/PostSearchService.php similarity index 57% rename from src/Models/Model_Post_QueryBuilder.php rename to src/Models/SearchServices/PostSearchService.php index 07949122..a20ffcb7 100644 --- a/src/Models/Model_Post_QueryBuilder.php +++ b/src/Models/SearchServices/PostSearchService.php @@ -1,5 +1,5 @@ addSql('safety')->in('(' . R::genSlots($allowedSafety) . ')'); + $sqlQuery->raw('safety')->in()->genSlots($allowedSafety); foreach ($allowedSafety as $s) - $dbQuery->put($s); + $sqlQuery->put($s); } - protected static function filterUserHidden($dbQuery) + protected static function filterUserHidden(SqlQuery $sqlQuery) { if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden')) - $dbQuery->not()->addSql('hidden'); + $sqlQuery->not('hidden'); else - $dbQuery->addSql('1'); + $sqlQuery->raw('1'); } - protected static function filterChain($dbQuery) + protected static function filterChain(SqlQuery $sqlQuery) { - if (isset($dbQuery->__chained)) - $dbQuery->and(); + if (isset($sqlQuery->__chained)) + $sqlQuery->and(); else - $dbQuery->where(); - $dbQuery->__chained = true; + $sqlQuery->where(); + $sqlQuery->__chained = true; } - protected static function filterNegate($dbQuery) + protected static function filterNegate(SqlQuery $sqlQuery) { - $dbQuery->not(); + $sqlQuery->not(); } - protected static function filterTag($dbQuery, $val) + protected static function filterTag($sqlQuery, $val) { - $tag = Model_Tag::locate($val); - $dbQuery + $tag = TagModel::findByName($val); + $sqlQuery ->exists() ->open() ->select('1') @@ -51,66 +51,64 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder ->close(); } - protected static function filterTokenId($searchContext, $dbQuery, $val) + protected static function filterTokenId($searchContext, SqlQuery $sqlQuery, $val) { $ids = preg_split('/[;,]/', $val); $ids = array_map('intval', $ids); - $dbQuery->addSql('id')->in('(' . R::genSlots($ids) . ')'); - foreach ($ids as $id) - $dbQuery->put($id); + $sqlQuery->raw('id')->in()->genSlots($ids)->put($ids); } - protected static function filterTokenIdMin($searchContext, $dbQuery, $val) + protected static function filterTokenIdMin($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('id >= ?')->put(intval($val)); + $sqlQuery->raw('id >= ?')->put(intval($val)); } - protected static function filterTokenIdMax($searchContext, $dbQuery, $val) + protected static function filterTokenIdMax($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('id <= ?')->put(intval($val)); + $sqlQuery->raw('id <= ?')->put(intval($val)); } - protected static function filterTokenScoreMin($searchContext, $dbQuery, $val) + protected static function filterTokenScoreMin($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('score >= ?')->put(intval($val)); + $sqlQuery->raw('score >= ?')->put(intval($val)); } - protected static function filterTokenScoreMax($searchContext, $dbQuery, $val) + protected static function filterTokenScoreMax($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('score <= ?')->put(intval($val)); + $sqlQuery->raw('score <= ?')->put(intval($val)); } - protected static function filterTokenTagMin($searchContext, $dbQuery, $val) + protected static function filterTokenTagMin($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('tag_count >= ?')->put(intval($val)); + $sqlQuery->raw('tag_count >= ?')->put(intval($val)); } - protected static function filterTokenTagMax($searchContext, $dbQuery, $val) + protected static function filterTokenTagMax($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('tag_count <= ?')->put(intval($val)); + $sqlQuery->raw('tag_count <= ?')->put(intval($val)); } - protected static function filterTokenFavMin($searchContext, $dbQuery, $val) + protected static function filterTokenFavMin($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('fav_count >= ?')->put(intval($val)); + $sqlQuery->raw('fav_count >= ?')->put(intval($val)); } - protected static function filterTokenFavMax($searchContext, $dbQuery, $val) + protected static function filterTokenFavMax($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('fav_count <= ?')->put(intval($val)); + $sqlQuery->raw('fav_count <= ?')->put(intval($val)); } - protected static function filterTokenCommentMin($searchContext, $dbQuery, $val) + protected static function filterTokenCommentMin($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('comment_count >= ?')->put(intval($val)); + $sqlQuery->raw('comment_count >= ?')->put(intval($val)); } - protected static function filterTokenCommentMax($searchContext, $dbQuery, $val) + protected static function filterTokenCommentMax($searchContext, SqlQuery $sqlQuery, $val) { - $dbQuery->addSql('comment_count <= ?')->put(intval($val)); + $sqlQuery->raw('comment_count <= ?')->put(intval($val)); } - protected static function filterTokenSpecial($searchContext, $dbQuery, $val) + protected static function filterTokenSpecial($searchContext, SqlQuery $sqlQuery, $val) { $context = \Chibi\Registry::getContext(); @@ -118,11 +116,11 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder { case 'liked': case 'likes': - $dbQuery + $sqlQuery ->exists() ->open() ->select('1') - ->from('postscore') + ->from('post_score') ->where('post_id = post.id') ->and('score > 0') ->and('user_id = ?')->put($context->user->id) @@ -131,11 +129,11 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder case 'disliked': case 'dislikes': - $dbQuery + $sqlQuery ->exists() ->open() ->select('1') - ->from('postscore') + ->from('post_score') ->where('post_id = post.id') ->and('score < 0') ->and('user_id = ?')->put($context->user->id) @@ -147,7 +145,7 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder } } - protected static function filterTokenType($searchContext, $dbQuery, $val) + protected static function filterTokenType($searchContext, SqlQuery $sqlQuery, $val) { switch (strtolower($val)) { @@ -164,7 +162,7 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder default: throw new SimpleException('Unknown type "' . $val . '"'); } - $dbQuery->addSql('type = ?')->put($type); + $sqlQuery->raw('type = ?')->put($type); } protected static function __filterTokenDateParser($val) @@ -182,30 +180,30 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder return [$timeMin, $timeMax]; } - protected static function filterTokenDate($searchContext, $dbQuery, $val) + protected static function filterTokenDate($searchContext, SqlQuery $sqlQuery, $val) { list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - $dbQuery - ->addSql('upload_date >= ?')->put($timeMin) + $sqlQuery + ->raw('upload_date >= ?')->put($timeMin) ->and('upload_date <= ?')->put($timeMax); } - protected static function filterTokenDateMin($searchContext, $dbQuery, $val) + protected static function filterTokenDateMin($searchContext, SqlQuery $sqlQuery, $val) { list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - $dbQuery->addSql('upload_date >= ?')->put($timeMin); + $sqlQuery->raw('upload_date >= ?')->put($timeMin); } - protected static function filterTokenDateMax($searchContext, $dbQuery, $val) + protected static function filterTokenDateMax($searchContext, SqlQuery $sqlQuery, $val) { list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - $dbQuery->addSql('upload_date <= ?')->put($timeMax); + $sqlQuery->raw('upload_date <= ?')->put($timeMax); } - protected static function filterTokenFav($searchContext, $dbQuery, $val) + protected static function filterTokenFav($searchContext, SqlQuery $sqlQuery, $val) { - $user = Model_User::locate($val); - $dbQuery + $user = UserModel::findByNameOrEmail($val); + $sqlQuery ->exists() ->open() ->select('1') @@ -215,15 +213,15 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder ->close(); } - protected static function filterTokenFavs($searchContext, $dbQuery, $val) + protected static function filterTokenFavs($searchContext, SqlQuery $sqlQuery, $val) { - return self::filterTokenFav($searchContext, $dbQuery, $val); + return self::filterTokenFav($searchContext, $sqlQuery, $val); } - protected static function filterTokenComment($searchContext, $dbQuery, $val) + protected static function filterTokenComment($searchContext, SqlQuery $sqlQuery, $val) { - $user = Model_User::locate($val); - $dbQuery + $user = UserModel::findByNameOrEmail($val); + $sqlQuery ->exists() ->open() ->select('1') @@ -233,51 +231,51 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder ->close(); } - protected static function filterTokenCommenter($searchContext, $dbQuery, $val) + protected static function filterTokenCommenter($searchContext, SqlQuery $sqlQuery, $val) { - return self::filterTokenComment($searchContext, $dbQuery, $val); + return self::filterTokenComment($searchContext, $sqlQuery, $val); } - protected static function filterTokenSubmit($searchContext, $dbQuery, $val) + protected static function filterTokenSubmit($searchContext, SqlQuery $sqlQuery, $val) { - $user = Model_User::locate($val); - $dbQuery->addSql('uploader_id = ?')->put($user->id); + $user = UserModel::findByNameOrEmail($val); + $sqlQuery->raw('uploader_id = ?')->put($user->id); } - protected static function filterTokenUploader($searchContext, $dbQuery, $val) + protected static function filterTokenUploader($searchContext, SqlQuery $sqlQuery, $val) { - return self::filterTokenSubmit($searchContext, $dbQuery, $val); + return self::filterTokenSubmit($searchContext, $sqlQuery, $val); } - protected static function filterTokenUpload($searchContext, $dbQuery, $val) + protected static function filterTokenUpload($searchContext, SqlQuery $sqlQuery, $val) { - return self::filterTokenSubmit($searchContext, $dbQuery, $val); + return self::filterTokenSubmit($searchContext, $sqlQuery, $val); } - protected static function filterTokenUploaded($searchContext, $dbQuery, $val) + protected static function filterTokenUploaded($searchContext, SqlQuery $sqlQuery, $val) { - return self::filterTokenSubmit($searchContext, $dbQuery, $val); + return self::filterTokenSubmit($searchContext, $sqlQuery, $val); } - protected static function filterTokenPrev($searchContext, $dbQuery, $val) + protected static function filterTokenPrev($searchContext, SqlQuery $sqlQuery, $val) { - self::__filterTokenPrevNext($searchContext, $dbQuery, $val); + self::__filterTokenPrevNext($searchContext, $sqlQuery, $val); } - protected static function filterTokenNext($searchContext, $dbQuery, $val) + protected static function filterTokenNext($searchContext, SqlQuery $sqlQuery, $val) { $searchContext->orderDir *= -1; - self::__filterTokenPrevNext($searchContext, $dbQuery, $val); + self::__filterTokenPrevNext($searchContext, $sqlQuery, $val); } - protected static function __filterTokenPrevNext($searchContext, $dbQuery, $val) + protected static function __filterTokenPrevNext($searchContext, SqlQuery $sqlQuery, $val) { $op1 = $searchContext->orderDir == 1 ? '<' : '>'; $op2 = $searchContext->orderDir != 1 ? '<' : '>'; - $dbQuery + $sqlQuery ->open() ->open() - ->addSql($searchContext->orderColumn . ' ' . $op1 . ' ') + ->raw($searchContext->orderColumn . ' ' . $op1 . ' ') ->open() ->select($searchContext->orderColumn) ->from('post p2') @@ -287,7 +285,7 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder ->close() ->or() ->open() - ->addSql($searchContext->orderColumn . ' = ') + ->raw($searchContext->orderColumn . ' = ') ->open() ->select($searchContext->orderColumn) ->from('post p2') @@ -405,19 +403,19 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder return $unparsedTokens; } - public static function build($dbQuery, $query) + public static function decorate(SqlQuery $sqlQuery, $searchQuery) { $config = \Chibi\Registry::getConfig(); - $dbQuery->from('post'); + $sqlQuery->from('post'); - self::filterChain($dbQuery); - self::filterUserSafety($dbQuery); - self::filterChain($dbQuery); - self::filterUserHidden($dbQuery); + self::filterChain($sqlQuery); + self::filterUserSafety($sqlQuery); + self::filterChain($sqlQuery); + self::filterUserHidden($sqlQuery); /* query tokens */ - $tokens = array_filter(array_unique(explode(' ', $query)), function($x) { return $x != ''; }); + $tokens = array_filter(array_unique(explode(' ', $searchQuery)), function($x) { return $x != ''; }); if (self::$enableTokenLimit and count($tokens) > $config->browsing->maxSearchTokens) throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')'); @@ -428,7 +426,7 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder $searchContext->orderColumn = 'id'; $searchContext->orderDir = 1; - $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $dbQuery, &$orderToken) + $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery, &$orderToken) { if ($key != 'order') return false; @@ -443,47 +441,47 @@ class Model_Post_QueryBuilder implements AbstractQueryBuilder }); - $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $dbQuery) + $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery) { if ($key !== null) return false; - self::filterChain($dbQuery); + self::filterChain($sqlQuery); if ($neg) - self::filterNegate($dbQuery); - self::filterTag($dbQuery, $val); + self::filterNegate($sqlQuery); + self::filterTag($sqlQuery, $val); return true; }); - $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $dbQuery) + $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery) { $methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key); if (!method_exists(__CLASS__, $methodName)) return false; - self::filterChain($dbQuery); + self::filterChain($sqlQuery); if ($neg) - self::filterNegate($dbQuery); - self::$methodName($searchContext, $dbQuery, $val); + self::filterNegate($sqlQuery); + self::$methodName($searchContext, $sqlQuery, $val); return true; }); if (!empty($tokens)) throw new SimpleException('Unknown search token "' . array_shift($tokens) . '"'); - $dbQuery->orderBy($searchContext->orderColumn); + $sqlQuery->orderBy($searchContext->orderColumn); if ($searchContext->orderDir == 1) - $dbQuery->desc(); + $sqlQuery->desc(); else - $dbQuery->asc(); + $sqlQuery->asc(); if ($searchContext->orderColumn != 'id') { - $dbQuery->addSql(', id'); + $sqlQuery->raw(', id'); if ($searchContext->orderDir == 1) - $dbQuery->desc(); + $sqlQuery->desc(); else - $dbQuery->asc(); + $sqlQuery->asc(); } } } diff --git a/src/Models/Model_Tag_QueryBuilder.php b/src/Models/SearchServices/TagSearchService.php similarity index 65% rename from src/Models/Model_Tag_QueryBuilder.php rename to src/Models/SearchServices/TagSearchService.php index 490496b9..d0aa588d 100644 --- a/src/Models/Model_Tag_QueryBuilder.php +++ b/src/Models/SearchServices/TagSearchService.php @@ -1,27 +1,27 @@ addSql(', COUNT(post_tag.post_id)') + $sqlQuery + ->raw(', COUNT(post_tag.post_id)') ->as('post_count') ->from('tag') ->innerJoin('post_tag') ->on('tag.id = post_tag.tag_id') ->innerJoin('post') ->on('post.id = post_tag.post_id') - ->where('safety IN (' . R::genSlots($allowedSafety) . ')'); + ->where('safety')->in()->genSlots($allowedSafety); foreach ($allowedSafety as $s) - $dbQuery->put($s); + $sqlQuery->put($s); $orderToken = null; - if ($query !== null) + if ($searchQuery !== null) { - $tokens = preg_split('/\s+/', $query); + $tokens = preg_split('/\s+/', $searchQuery); foreach ($tokens as $token) { if (strpos($token, ':') !== false) @@ -39,7 +39,7 @@ class model_Tag_QueryBuilder implements AbstractQueryBuilder if (strlen($token) >= 3) $token = '%' . $token; $token .= '%'; - $dbQuery + $sqlQuery ->and('LOWER(tag.name)') ->like('LOWER(?)') ->put($token); @@ -47,16 +47,16 @@ class model_Tag_QueryBuilder implements AbstractQueryBuilder } } - $dbQuery->groupBy('tag.id'); + $sqlQuery->groupBy('tag.id'); if ($orderToken) - self::order($dbQuery,$orderToken); + self::order($sqlQuery,$orderToken); if ($limitQuery) - $dbQuery->limit(15); + $sqlQuery->limit(15); } - private static function order($dbQuery, $value) + private static function order(SqlQuery $sqlQuery, $value) { if (strpos($value, ',') !== false) { @@ -71,17 +71,17 @@ class model_Tag_QueryBuilder implements AbstractQueryBuilder switch ($orderColumn) { case 'popularity': - $dbQuery->orderBy('post_count'); + $sqlQuery->orderBy('post_count'); break; case 'alpha': - $dbQuery->orderBy('name'); + $sqlQuery->orderBy('name'); break; } if ($orderDir == 'asc') - $dbQuery->asc(); + $sqlQuery->asc(); else - $dbQuery->desc(); + $sqlQuery->desc(); } } diff --git a/src/Models/SearchServices/UserSearchService.php b/src/Models/SearchServices/UserSearchService.php new file mode 100644 index 00000000..e5836525 --- /dev/null +++ b/src/Models/SearchServices/UserSearchService.php @@ -0,0 +1,31 @@ +from('user'); + + $sortStyle = $searchQuery; + switch ($sortStyle) + { + case 'alpha,asc': + $sqlQuery->orderBy('name')->asc(); + break; + case 'alpha,desc': + $sqlQuery->orderBy('name')->desc(); + break; + case 'date,asc': + $sqlQuery->orderBy('join_date')->asc(); + break; + case 'date,desc': + $sqlQuery->orderBy('join_date')->desc(); + break; + case 'pending': + $sqlQuery->where('staff_confirmed IS NULL'); + $sqlQuery->or('staff_confirmed = 0'); + break; + default: + throw new SimpleException('Unknown sort style "' . $sortStyle . '"'); + } + } +} diff --git a/src/Models/TagModel.php b/src/Models/TagModel.php new file mode 100644 index 00000000..94fb191e --- /dev/null +++ b/src/Models/TagModel.php @@ -0,0 +1,186 @@ +update('tag') + ->set('name = ?')->put($tag->name) + ->where('id = ?')->put($tag->id); + + Database::query($query); + }); + return $tag->id; + } + + public static function remove($tag) + { + $query = (new SqlQuery) + ->deleteFrom('post_tag') + ->where('tag_id = ?')->put($tag->id); + Database::query($query); + + $query = (new SqlQuery) + ->deleteFrom('tag') + ->where('id = ?')->put($tag->id); + Database::query($query); + } + + public static function rename($sourceName, $targetName) + { + Database::transaction(function() use ($sourceName, $targetName) + { + $sourceTag = TagModel::findByName($sourceName); + $targetTag = TagModel::findByName($targetName, false); + + if ($targetTag and $targetTag->id != $sourceTag->id) + throw new SimpleException('Target tag already exists'); + + $sourceTag->name = $targetName; + self::save($sourceTag); + }); + } + + public static function merge($sourceName, $targetName) + { + Database::transaction(function() use ($sourceName, $targetName) + { + $sourceTag = TagModel::findByName($sourceName); + $targetTag = TagModel::findByName($targetName); + + if ($sourceTag->id == $targetTag->id) + throw new SimpleException('Source and target tag are the same'); + + $query = (new SqlQuery) + ->select('post.id') + ->from('post') + ->where() + ->exists() + ->open() + ->select('1') + ->from('post_tag') + ->where('post_tag.post_id = post.id') + ->and('post_tag.tag_id = ?')->put($sourceTag->id) + ->close() + ->and() + ->not()->exists() + ->open() + ->select('1') + ->from('post_tag') + ->where('post_tag.post_id = post.id') + ->and('post_tag.tag_id = ?')->put($targetTag->id) + ->close(); + $rows = Database::fetchAll($query); + $postIds = array_map(function($row) { return $row['id']; }, $rows); + + self::remove($sourceTag); + + foreach ($postIds as $postId) + { + $query = (new SqlQuery) + ->insertInto('post_tag') + ->surround('post_id, tag_id') + ->values()->surround('?, ?') + ->put([$postId, $targetTag->id]); + Database::query($query); + } + }); + } + + + public static function findAllByPostId($key) + { + $query = new SqlQuery(); + $query + ->select('tag.*') + ->from('tag') + ->innerJoin('post_tag') + ->on('post_tag.tag_id = tag.id') + ->where('post_tag.post_id = ?') + ->put($key); + + $rows = Database::fetchAll($query); + if ($rows) + return self::convertRows($rows); + return []; + } + + public static function findByName($key, $throw = true) + { + $query = (new SqlQuery) + ->select('*') + ->from('tag') + ->where('LOWER(name) = LOWER(?)')->put($key); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('Invalid tag name "' . $key . '"'); + return null; + } + + + + public static function removeUnused() + { + $query = (new SqlQuery) + ->deleteFrom('tag') + ->where() + ->not()->exists() + ->open() + ->select('1') + ->from('post_tag') + ->where('post_tag.tag_id = tag.id') + ->close(); + Database::query($query); + } + + + + public static function validateTag($tag) + { + $tag = trim($tag); + + $minLength = 1; + $maxLength = 64; + if (strlen($tag) < $minLength) + throw new SimpleException('Tag must have at least ' . $minLength . ' characters'); + if (strlen($tag) > $maxLength) + throw new SimpleException('Tag must have at most ' . $maxLength . ' characters'); + + if (!preg_match('/^[()\[\]a-zA-Z0-9_.-]+$/i', $tag)) + throw new SimpleException('Invalid tag "' . $tag . '"'); + + if (preg_match('/^\.\.?$/', $tag)) + throw new SimpleException('Invalid tag "' . $tag . '"'); + + return $tag; + } + + public static function validateTags($tags) + { + $tags = trim($tags); + $tags = preg_split('/[,;\s]+/', $tags); + $tags = array_filter($tags, function($x) { return $x != ''; }); + $tags = array_unique($tags); + + foreach ($tags as $key => $tag) + $tags[$key] = self::validateTag($tag); + + if (empty($tags)) + throw new SimpleException('No tags set'); + + return $tags; + } +} diff --git a/src/Models/TokenModel.php b/src/Models/TokenModel.php new file mode 100644 index 00000000..e3acec12 --- /dev/null +++ b/src/Models/TokenModel.php @@ -0,0 +1,81 @@ + $token->userId, + 'token' => $token->token, + 'used' => $token->used, + 'expires' => $token->expires, + ]; + + $query = (new SqlQuery) + ->update('user_token') + ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) + ->put(array_values($bindings)) + ->where('id = ?')->put($token->id); + Database::query($query); + + }); + } + + + + public static function findByToken($key, $throw = true) + { + if (empty($key)) + throw new SimpleException('Invalid security token'); + + $query = (new SqlQuery) + ->select('*') + ->from('user_token') + ->where('token = ?')->put($key); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('No user with such security token'); + return null; + } + + + + public static function checkValidity($token) + { + if (empty($token)) + throw new SimpleException('Invalid security token'); + + if ($token->used) + throw new SimpleException('This token was already used'); + + if ($token->expires !== null and time() > $token->expires) + throw new SimpleException('This token has expired'); + } + + + + public static function forgeUnusedToken() + { + $tokenText = ''; + while (true) + { + $tokenText = md5(mt_rand() . uniqid()); + $token = self::findByToken($tokenText, false); + if (!$token) + return $tokenText; + } + } +} diff --git a/src/Models/UserModel.php b/src/Models/UserModel.php new file mode 100644 index 00000000..a34ba105 --- /dev/null +++ b/src/Models/UserModel.php @@ -0,0 +1,253 @@ +passSalt = md5(mt_rand() . uniqid()); + return $user; + } + + public static function save($user) + { + if ($user->accessRank == AccessRank::Anonymous) + throw new Exception('Trying to save anonymous user into database'); + Database::transaction(function() use ($user) + { + self::forgeId($user); + + $bindings = [ + 'name' => $user->name, + 'pass_salt' => $user->passSalt, + 'pass_hash' => $user->passHash, + 'staff_confirmed' => $user->staffConfirmed, + 'email_unconfirmed' => $user->emailUnconfirmed, + 'email_confirmed' => $user->emailConfirmed, + 'join_date' => $user->joinDate, + 'access_rank' => $user->accessRank, + 'settings' => $user->settings, + 'banned' => $user->banned + ]; + + $query = (new SqlQuery) + ->update('user') + ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) + ->put(array_values($bindings)) + ->where('id = ?')->put($user->id); + Database::query($query); + }); + } + + public static function remove($user) + { + Database::transaction(function() use ($user) + { + $queries = []; + + $queries []= (new SqlQuery) + ->deleteFrom('post_score') + ->where('user_id = ?')->put($user->id); + + $queries []= (new SqlQuery) + ->update('comment') + ->set('commenter_id = NULL') + ->where('commenter_id = ?')->put($user->id); + + $queries []= (new SqlQuery) + ->update('post') + ->set('uploader_id = NULL') + ->where('uploader_id = ?')->put($user->id); + + $queries []= (new SqlQuery) + ->deleteFrom('favoritee') + ->where('user_id = ?')->put($user->id); + + $queries []= (new SqlQuery) + ->deleteFrom('user') + ->where('id = ?')->put($user->id); + + foreach ($queries as $query) + Database::query($query); + }); + } + + + + public static function findByName($key, $throw = true) + { + $query = (new SqlQuery) + ->select('*') + ->from('user') + ->where('LOWER(name) = LOWER(?)')->put(trim($key)); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('Invalid user name "' . $key . '"'); + return null; + } + + public static function findByNameOrEmail($key, $throw = true) + { + $query = new SqlQuery(); + $query->select('*') + ->from('user') + ->where('LOWER(name) = LOWER(:user)') + ->or('LOWER(email_confirmed) = LOWER(:user)') + ->put(['user' => trim($key)]); + + $row = Database::fetchOne($query); + if ($row) + return self::convertRow($row); + + if ($throw) + throw new SimpleException('Invalid user name "' . $key . '"'); + return null; + } + + + + public static function updateUserScore($user, $post, $score) + { + Database::transaction(function() use ($user, $post, $score) + { + $query = (new SqlQuery) + ->deleteFrom('post_score') + ->where('post_id = ?')->put($post->id) + ->and('user_id = ?')->put($user->id); + Database::query($query); + $score = intval($score); + if ($score != 0) + { + $query = (new SqlQuery); + $query->insertInto('post_score') + ->surround('post_id, user_id, score') + ->values()->surround('?, ?, ?') + ->put([$post->id, $user->id, $score]); + Database::query($query); + } + }); + } + + public static function addToUserFavorites($user, $post) + { + Database::transaction(function() use ($user, $post) + { + self::removeFromUserFavorites($user, $post); + $query = (new SqlQuery); + $query->insertInto('favoritee') + ->surround('post_id, user_id') + ->values()->surround('?, ?') + ->put([$post->id, $user->id]); + Database::query($query); + }); + } + + public static function removeFromUserFavorites($user, $post) + { + Database::transaction(function() use ($user, $post) + { + $query = (new SqlQuery) + ->deleteFrom('favoritee') + ->where('post_id = ?')->put($post->id) + ->and('user_id = ?')->put($user->id); + Database::query($query); + }); + } + + + + public static function validateUserName($userName) + { + $userName = trim($userName); + + $dbUser = self::findByName($userName, false); + if ($dbUser !== null) + { + if (!$dbUser->emailConfirmed and \Chibi\Registry::getConfig()->registration->needEmailForRegistering) + throw new SimpleException('User with this name is already registered and awaits e-mail confirmation'); + + if (!$dbUser->staffConfirmed and \Chibi\Registry::getConfig()->registration->staffActivation) + throw new SimpleException('User with this name is already registered and awaits staff confirmation'); + + throw new SimpleException('User with this name is already registered'); + } + + $userNameMinLength = intval(\Chibi\Registry::getConfig()->registration->userNameMinLength); + $userNameMaxLength = intval(\Chibi\Registry::getConfig()->registration->userNameMaxLength); + $userNameRegex = \Chibi\Registry::getConfig()->registration->userNameRegex; + + if (strlen($userName) < $userNameMinLength) + throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength)); + + if (strlen($userName) > $userNameMaxLength) + throw new SimpleException(sprintf('User name must have at most %d characters', $userNameMaxLength)); + + if (!preg_match($userNameRegex, $userName)) + throw new SimpleException('User name contains invalid characters'); + + return $userName; + } + + public static function validatePassword($password) + { + $passMinLength = intval(\Chibi\Registry::getConfig()->registration->passMinLength); + $passRegex = \Chibi\Registry::getConfig()->registration->passRegex; + + if (strlen($password) < $passMinLength) + throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength)); + + if (!preg_match($passRegex, $password)) + throw new SimpleException('Password contains invalid characters'); + + return $password; + } + + public static function validateEmail($email) + { + $email = trim($email); + + if (!empty($email) and !TextHelper::isValidEmail($email)) + throw new SimpleException('E-mail address appears to be invalid'); + + return $email; + } + + public static function validateAccessRank($accessRank) + { + $accessRank = intval($accessRank); + + if (!in_array($accessRank, AccessRank::getAll())) + throw new SimpleException('Invalid access rank type "' . $accessRank . '"'); + + if ($accessRank == AccessRank::Nobody) + throw new SimpleException('Cannot set special accesss rank "' . $accessRank . '"'); + + return $accessRank; + } + + + + public static function getAnonymousName() + { + return '[Anonymous user]'; + } + + public static function hashPassword($pass, $salt2) + { + $salt1 = \Chibi\Registry::getConfig()->main->salt; + return sha1($salt1 . $salt2 . $pass); + } +} diff --git a/src/SqlQuery.php b/src/SqlQuery.php new file mode 100644 index 00000000..0bb6a3cf --- /dev/null +++ b/src/SqlQuery.php @@ -0,0 +1,99 @@ +sql = ''; + $this->bindings = []; + } + + public function __call($name, array $arguments) + { + $name = TextHelper::camelCaseToKebabCase($name); + $name = str_replace('-', ' ', $name); + $this->sql .= $name . ' '; + + if (!empty($arguments)) + { + $arg = array_shift($arguments); + assert(empty($arguments)); + + if (is_object($arg)) + { + throw new Exception('Not implemented'); + } + else + { + $this->sql .= $arg . ' '; + } + } + + return $this; + } + + public function put($arg) + { + if (is_array($arg)) + { + foreach ($arg as $key => $val) + { + if (is_numeric($key)) + $this->bindings []= $val; + else + $this->bindings[$key] = $val; + } + } + else + { + $this->bindings []= $arg; + } + return $this; + } + + public function raw($raw) + { + $this->sql .= $raw . ' '; + return $this; + } + + public function open() + { + $this->sql .= '('; + return $this; + } + + public function close() + { + $this->sql .= ') '; + return $this; + } + + public function surround($raw) + { + $this->sql .= '(' . $raw . ') '; + return $this; + } + + public function genSlots($bindings) + { + if (empty($bindings)) + return $this; + $this->sql .= '('; + $this->sql .= join(',', array_fill(0, count($bindings), '?')); + $this->sql .= ') '; + return $this; + } + + public function getBindings() + { + return $this->bindings; + } + + public function getSql() + { + return trim($this->sql); + } +} diff --git a/src/Upgrades/mysql/Upgrade9.sql b/src/Upgrades/mysql/Upgrade9.sql new file mode 100644 index 00000000..743f1aa1 --- /dev/null +++ b/src/Upgrades/mysql/Upgrade9.sql @@ -0,0 +1,31 @@ +UPDATE post SET file_hash = orig_name WHERE type = 3; + +CREATE TRIGGER post_tag_update AFTER UPDATE ON post_tag FOR EACH ROW +BEGIN + UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id; + UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id; +END; + +CREATE TRIGGER favoritee_update AFTER UPDATE ON favoritee FOR EACH ROW +BEGIN + UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id; + UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id; +END; + +CREATE TRIGGER comment_update AFTER UPDATE ON comment FOR EACH ROW +BEGIN + UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id; + UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id; +END; + +ALTER TABLE usertoken RENAME TO user_token; + +DROP TRIGGER post_score_update; +ALTER TABLE postscore RENAME TO post_score; + +CREATE TRIGGER post_score_update AFTER UPDATE ON post_score FOR EACH ROW +BEGIN + UPDATE post SET score = post.score + new.score WHERE post.id = new.post_id; + UPDATE post SET score = post.score - old.score WHERE post.id = old.post_id; +END; + diff --git a/src/Upgrades/sqlite/Upgrade9.sql b/src/Upgrades/sqlite/Upgrade9.sql new file mode 100644 index 00000000..743f1aa1 --- /dev/null +++ b/src/Upgrades/sqlite/Upgrade9.sql @@ -0,0 +1,31 @@ +UPDATE post SET file_hash = orig_name WHERE type = 3; + +CREATE TRIGGER post_tag_update AFTER UPDATE ON post_tag FOR EACH ROW +BEGIN + UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id; + UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id; +END; + +CREATE TRIGGER favoritee_update AFTER UPDATE ON favoritee FOR EACH ROW +BEGIN + UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id; + UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id; +END; + +CREATE TRIGGER comment_update AFTER UPDATE ON comment FOR EACH ROW +BEGIN + UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id; + UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id; +END; + +ALTER TABLE usertoken RENAME TO user_token; + +DROP TRIGGER post_score_update; +ALTER TABLE postscore RENAME TO post_score; + +CREATE TRIGGER post_score_update AFTER UPDATE ON post_score FOR EACH ROW +BEGIN + UPDATE post SET score = post.score + new.score WHERE post.id = new.post_id; + UPDATE post SET score = post.score - old.score WHERE post.id = old.post_id; +END; + diff --git a/src/Views/comment-list.phtml b/src/Views/comment-list.phtml index 7c1b225c..03bbddcc 100644 --- a/src/Views/comment-list.phtml +++ b/src/Views/comment-list.phtml @@ -9,12 +9,12 @@ $currentGroup = null; foreach ($this->context->transport->comments as $comment) { - if ($comment->post_id != $currentGroupPostId) + if ($comment->postId != $currentGroupPostId) { unset($currentGroup); $currentGroup = []; - $currentGroupPostId = $comment->post_id; - $posts[$comment->post_id] = $comment->post; + $currentGroupPostId = $comment->postId; + $posts[$comment->postId] = $comment->getPost(); $groups[] = &$currentGroup; } $currentGroup []= $comment; @@ -23,7 +23,7 @@
- context->post = $posts[reset($group)->post_id] ?> + context->post = $posts[reset($group)->postId] ?> renderFile('post-small') ?>
diff --git a/src/Views/comment-small.phtml b/src/Views/comment-small.phtml index 5573d1f8..2ee5a09a 100644 --- a/src/Views/comment-small.phtml +++ b/src/Views/comment-small.phtml @@ -1,31 +1,32 @@
- context->comment->commenter): ?> - - <?php echo $this->context->comment->commenter->name ?> + context->comment->getCommenter() ?> + + + <?php echo $commenter->name ?> - <?php echo Model_User::getAnonymousName() ?> + <?php echo UserModel::getAnonymousName() ?>
- context->comment->commenter): ?> - - context->comment->commenter->name ?> + + + name ?> - + - context->comment->comment_date) ?> + context->comment->commentDate) ?> - context->comment->commenter))): ?> + delete diff --git a/src/Views/index-index.phtml b/src/Views/index-index.phtml index 3d377885..9daefc79 100644 --- a/src/Views/index-index.phtml +++ b/src/Views/index-index.phtml @@ -16,7 +16,7 @@
Tags: