/** * Returns rules specifically for dropzone files for a board. * * @param Board $board * @param array $rules (POINTER, MODIFIED) * @return array Validation rules. */ public static function rulesForFileHashes(Board $board, array &$rules) { global $app; $attachmentsMax = $board->getConfig('postAttachmentsMax', 1); $rules['files'][] = "array"; $rules['files'][] = "min:1"; $rules['files'][] = "max:{$attachmentsMax}"; // Create an additional rule for each possible file. for ($attachment = 0; $attachment < $attachmentsMax; ++$attachment) { $rules["files.name.{$attachment}"] = ["string", "required_with:files.hash.{$attachment}", "between:1,254", "file_name"]; $rules["files.hash.{$attachment}"] = ["string", "required_with:files.name.{$attachment}", "md5", "exists:files,hash,banned,0"]; $rules["files.spoiler.{$attachment}"] = ["boolean"]; } }
/** * Manage threads that have expired. * * @param App\Board $board * @param Carbon\Carbon $time Optional Carbon that will be the xed_at timestamps. Defaults to now. * @return void */ protected function handleThreadEphemeral(Board $board, Carbon $time = null) { if (is_null($time)) { $time = Carbon::now(); } // Get important settings. $threadsPerPage = (int) $board->getConfig('postsPerPage', 10); // Collect a list of threads which have been modified. $threadsToSave = []; // There are two groups of autoprune settings. // x on day since last reply $sageOnDay = (int) $board->getConfig('epheSageThreadDays', false); $lockOnDay = (int) $board->getConfig('epheLockThreadDays', false); $deleteOnDay = (int) $board->getConfig('epheDeleteThreadDays', false); // x on page (meaning the thread has fallen to this page) $sageOnPage = (int) $board->getConfig('epheSageThreadPage', false); $lockOnPage = (int) $board->getConfig('epheLockThreadPage', false); $deleteOnPage = (int) $board->getConfig('epheDeleteThreadPage', false); // Don't do anything unless we have to. if ($sageOnDay || $lockOnDay || $deleteOnDay || $sageOnPage || $lockOnPage || $deleteOnPage) { $this->comment(" Pruning /{$board->board_uri}/..."); // Modify threads based on these settings. foreach ($board->threads as $threadIndex => $thread) { $threadPage = (int) (floor($threadIndex / $threadsPerPage) + 1); $modified = false; $replyLast = clone $thread->reply_last; // x on day since last reply // This is asking if: // 1) The setting is set ($x > 0) // 2) the last reply date + the number of days permitted by each setting is < now. if (!$thread->isBumplocked() && ($sageOnDay > 0 && $replyLast->addDays($sageOnDay)->isPast() || $sageOnPage > 0 && $sageOnPage <= $threadPage)) { $this->comment(" Bumplocking #{$thread->board_id}"); $modified = true; $thread->bumplocked_at = $time; } if (!$thread->isLocked() && ($lockOnDay > 0 && $replyLast->addDays($lockOnDay)->isPast() || $lockOnPage > 0 && $lockOnPage <= $threadPage)) { $this->comment(" Locking #{$thread->board_id}"); $modified = true; $thread->locked_at = $time; } if (!$thread->isDeleted() && ($deleteOnDay > 0 && $replyLast->addDays($sageOnDay)->isPast() || $deleteOnPage > 0 && $deleteOnPage <= $threadPage)) { $this->comment(" Deleting #{$thread->board_id}"); $modified = true; $thread->deleted_at = $time; } if ($modified) { $threadsToSave[] = $thread; } } if (count($threadsToSave)) { // Save all at once. $board->threads()->saveMany($threadsToSave); } else { $this->comment(" Nothing to do."); } } }
/** * Renders the post edit form. */ public function getReport(Request $request, Board $board, Post $post, $global = false) { if (!$post->exists) { abort(404); } $actions = ["report"]; $ContentFormatter = new ContentFormatter(); $reportText = ""; if ($global === "global") { if (!$post->canReportGlobally($this->user)) { abort(403); } $actions[] = "global"; $reportText = $ContentFormatter->formatReportText($this->option('globalReportText')); } else { if (!$post->canReport($this->user)) { abort(403); } $reportText = $ContentFormatter->formatReportText($board->getConfig('boardReportText')); } if (!isset($report)) { $report = Report::where('post_id', '=', $post->post_id)->where('global', $global === "global")->where('board_uri', $board->board_uri)->whereByIPOrUser($this->user)->first(); } return $this->view(static::VIEW_MOD, ['actions' => $actions, 'form' => "report", 'board' => $board, 'post' => $post, 'report' => $report ?: false, 'reportText' => $reportText, 'reportGlobal' => $global === "global"]); }
/** * Pushes the post to the specified board, as a new thread or as a reply. * This autoatically handles concurrency issues. Creating a new reply without * using this method is forbidden by the `creating` event in ::boot. * * * @param App\Board &$board * @param App\Post &$thread * @return void */ public function submitTo(Board &$board, &$thread = null) { $this->board_uri = $board->board_uri; $this->author_ip = new IP(); $this->author_country = $board->getConfig('postsAuthorCountry', false) ? new Geolocation() : null; $this->reply_last = $this->freshTimestamp(); $this->bumped_last = $this->reply_last; $this->setCreatedAt($this->reply_last); $this->setUpdatedAt($this->reply_last); if (!is_null($thread) && !$thread instanceof Post) { $thread = $board->getLocalThread($thread); } if ($thread instanceof Post) { $this->reply_to = $thread->post_id; $this->reply_to_board_id = $thread->board_id; } // Handle tripcode, if any. if (preg_match('/^([^#]+)?(##|#)(.+)$/', $this->author, $match)) { // Remove password from name. $this->author = $match[1]; // Whether a secure tripcode was requested, currently unused. $secure_tripcode_requested = $match[2] == '##'; // Convert password to tripcode, store tripcode hash in DB. $this->insecure_tripcode = ContentFormatter::formatInsecureTripcode($match[3]); } // Ensure we're using a valid flag. if (!$this->flag_id || !$board->hasFlag($this->flag_id)) { $this->flag_id = null; } // Store the post in the database. DB::transaction(function () use($board, $thread) { // The objective of this transaction is to prevent concurrency issues in the database // on the unique joint index [`board_uri`,`board_id`] which is generated procedurally // alongside the primary autoincrement column `post_id`. // First instruction is to add +1 to posts_total and set the last_post_at on the Board table. DB::table('boards')->where('board_uri', $this->board_uri)->increment('posts_total', 1, ['last_post_at' => $this->reply_last]); // Second, we record this value and lock the table. $boards = DB::table('boards')->where('board_uri', $this->board_uri)->lockForUpdate()->select('posts_total')->get(); $posts_total = $boards[0]->posts_total; // Third, we store a unique checksum for this post for duplicate tracking. $board->checksums()->create(['checksum' => $this->getChecksum()]); // Optionally, we also expend the adventure. $adventure = BoardAdventure::getAdventure($board); if ($adventure) { $this->adventure_id = $adventure->adventure_id; $adventure->expended_at = $this->created_at; $adventure->save(); } // We set our board_id and save the post. $this->board_id = $posts_total; $this->author_id = $this->makeAuthorId(); $this->password = $this->makePassword($this->password); $this->save(); // Optionally, the OP of this thread needs a +1 to reply count. if ($thread instanceof static) { // We're not using the Model for this because it fails under high volume. $threadNewValues = ['updated_at' => $thread->updated_at, 'reply_last' => $this->created_at, 'reply_count' => $thread->replies()->count(), 'reply_file_count' => $thread->replyFiles()->count()]; if (!$this->isBumpless() && !$thread->isBumplocked()) { $threadNewValues['bumped_last'] = $this->created_at; } DB::table('posts')->where('post_id', $thread->post_id)->update($threadNewValues); } // Queries and locks are handled automatically after this closure ends. }); // Process uploads. $uploads = []; // Check file uploads. if (is_array($files = Input::file('files'))) { $uploads = array_filter($files); if (count($uploads) > 0) { foreach ($uploads as $uploadIndex => $upload) { if (file_exists($upload->getPathname())) { FileStorage::createAttachmentFromUpload($upload, $this); } } } } else { if (is_array($files = Input::get('files'))) { $uniques = []; $hashes = $files['hash']; $names = $files['name']; $spoilers = isset($files['spoiler']) ? $files['spoiler'] : []; $storages = FileStorage::whereIn('hash', $hashes)->get(); foreach ($hashes as $index => $hash) { if (!isset($uniques[$hash])) { $uniques[$hash] = true; $storage = $storages->where('hash', $hash)->first(); if ($storage && !$storage->banned) { $spoiler = isset($spoilers[$index]) ? $spoilers[$index] == 1 : false; $upload = $storage->createAttachmentWithThis($this, $names[$index], $spoiler, false); $upload->position = $index; $uploads[] = $upload; } } } $this->attachmentLinks()->saveMany($uploads); FileStorage::whereIn('hash', $hashes)->increment('upload_count'); } } // Finally fire event on OP, if it exists. if ($thread instanceof Post) { $thread->setRelation('board', $board); Event::fire(new ThreadNewReply($thread)); } return $this; }
/** * Pushes the post to the specified board, as a new thread or as a reply. * This autoatically handles concurrency issues. Creating a new reply without * using this method is forbidden by the `creating` event in ::boot. * * * @param App\Board &$board * @param App\Post &$thread * @return void */ public function submitTo(Board &$board, &$thread = null) { $this->board_uri = $board->board_uri; $this->author_ip = inet_pton(Request::ip()); // Hash the salt because I honestly don't know if $this->save() is safe enough to parse unsanized information into. $this->author_salt = hash(env('APP_HASH'), Cookie::get('author_salt')); $this->author_country = $board->getConfig('postsAuthorCountry', false) ? new Geolocation() : null; $this->reply_last = $this->freshTimestamp(); $this->bumped_last = $this->reply_last; $this->setCreatedAt($this->reply_last); $this->setUpdatedAt($this->reply_last); if (!is_null($thread) && !$thread instanceof Post) { $thread = $board->getLocalThread($thread); $this->reply_to = $thread->post_id; $this->reply_to_board_id = $thread->board_id; } // Handle tripcode, if any. if (preg_match('/^([^#]+)?(##|#)(.+)$/', $this->author, $match)) { // Remove password from name. $this->author = $match[1]; // Whether a secure tripcode was requested, currently unused. $secure_tripcode_requested = $match[2] == '##'; // Convert password to tripcode, store tripcode hash in DB. $this->insecure_tripcode = ContentFormatter::formatInsecureTripcode($match[3]); } // Store the post in the database. DB::transaction(function () use($board, $thread) { // The objective of this transaction is to prevent concurrency issues in the database // on the unique joint index [`board_uri`,`board_id`] which is generated procedurally // alongside the primary autoincrement column `post_id`. // First instruction is to add +1 to posts_total and set the last_post_at on the Board table. DB::table('boards')->where('board_uri', $this->board_uri)->increment('posts_total'); DB::table('boards')->where('board_uri', $this->board_uri)->update(['last_post_at' => $this->created_at]); // Second, we record this value and lock the table. $boards = DB::table('boards')->where('board_uri', $this->board_uri)->lockForUpdate()->select('posts_total')->get(); $posts_total = $boards[0]->posts_total; // Optionally, the OP of this thread needs a +1 to reply count. if ($thread instanceof Post) { if (!$this->isBumpless() && !$thread->isBumplocked()) { $thread->bumped_last = $this->created_at; // We explicitly set the updated_at to what it is now. // If we didn't, this would change. // We don't want that because it screws up the API and // makes it think the OP post has had its content edited. $thread->updated_at = $thread->updated_at; } $thread->reply_last = $this->created_at; $thread->reply_count += 1; $thread->save(); } // Optionally, we also expend the adventure. $adventure = BoardAdventure::getAdventure($board); if ($adventure) { $this->adventure_id = $adventure->adventure_id; $adventure->expended_at = $this->created_at; $adventure->save(); } // Finally, we set our board_id and save. $this->board_id = $posts_total; //$this->author_salt = $this->author_id = $this->makeAuthorId(); $this->save(); // Queries and locks are handled automatically after this closure ends. }); // Process uploads. $uploads = []; // Check file uploads. if (is_array($files = Input::file('files'))) { $uploads = array_filter($files); if (count($uploads) > 0) { foreach ($uploads as $uploadIndex => $upload) { if (file_exists($upload->getPathname())) { FileStorage::createAttachmentFromUpload($upload, $this); } } } } else { if (is_array($files = Input::get('files'))) { $uniques = []; $hashes = $files['hash']; $names = $files['name']; $spoilers = isset($files['spoiler']) ? $files['spoiler'] : []; $storages = FileStorage::whereIn('hash', $hashes)->get(); foreach ($hashes as $index => $hash) { if (!isset($uniques[$hash])) { $uniques[$hash] = true; $storage = $storages->where('hash', $hash)->first(); if ($storage && !$storage->banned) { $spoiler = isset($spoilers[$index]) ? $spoilers[$index] == 1 : false; $uploads[] = $storage->createAttachmentWithThis($this, $names[$index], $spoiler, false); } } } $this->attachmentLinks()->saveMany($uploads); FileStorage::whereIn('hash', $hashes)->increment('upload_count'); } } // Finally fire event on OP, if it exists. if ($thread instanceof Post) { Event::fire(new ThreadNewReply($thread)); } }