/** * 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)); } }