private function read($address) { step(); $position = $address * $this->fullLength; step(); return $this->storage->read($position, $this->fullLength); }
private function write($address, $key, $value = 1) { $position = $address * $this->fullLength; $key = $this->fix($key, $this->length); $value = $this->fix($value, $this->addressLength); $this->storage->write($position, $key . $value); }
private function updateParent($address, $value) { $position = ($address + 1) * $this->fullLength - $this->addressLength * 3; if ($value < 0) { $this->topAddress = $address; } $value = $this->fix($value, $this->addressLength); $this->storage->write($position, $value); }
/** * Run the migrations. * * @return void */ public function up() { Schema::table('files', function (Blueprint $table) { $table->boolean('has_thumbnail')->default(false); }); FileStorage::chunk(100, function ($files) { foreach ($files as $file) { if ($file->hasThumb()) { $file->has_thumbnail = true; $file->save(); } } }); }
public function validateFileIntegrity($attribute, $file, $parameters) { if ($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) { switch ($file->getClientMimeType()) { // For some reason, MP3 files routinely get scanned as octet-streams. // Attempt to evaluate it as music or a video. case "application/octet-stream": case "audio/mpeg": case "audio/mp3": case "video/mp4": case "video/flv": case "video/webm": return FileStorage::probe($file); case "application/x-shockwave-flash": // This is much slower than exif_imagetype but much more reliable with flash files. return getimagesize($file->getPathname())['mime'] == "application/x-shockwave-flash"; case "image/x-ms-bmp": return exif_imagetype($file->getPathname()) == IMAGETYPE_BMP; case "image/gif": return exif_imagetype($file->getPathname()) == IMAGETYPE_GIF; case "image/jpeg": case "image/jpg": return exif_imagetype($file->getPathname()) == IMAGETYPE_JPEG; case "image/png": return exif_imagetype($file->getPathname()) == IMAGETYPE_PNG; case "image/svg": case "image/svg+xml": try { $dom = new \DOMDocument(); $dom->Load($file->getPathname()); if ($dom->getElementsByTagName('script')->length > 0) { return false; } return $dom->saveXML() !== false; } catch (\Exception $error) { return false; } // Things we allow but can't validate. // Things we allow but can't validate. case "application/epub+zip": case "application/pdf": return true; default: return false; } } return false; dd($value); }
/** * Manage media. * * @return void */ protected function handleMediaFiles() { $this->comment(" Pruning media data..."); $mediaOrphanLife = (int) Settings::get('epheMediaPrune', 0); if ($mediaOrphanLife) { $carbonLife = Carbon::now()->subDays($mediaOrphanLife); $files = FileStorage::whereOrphan()->where('last_uploaded_at', '<=', $carbonLife)->get(); $affected = 0; foreach ($files as $file) { if ($file->hasFile()) { ++$affected; $file->deleteFile(); } } $this->comment(" Pruned {$affected} file(s)."); } }
/** * Run the migrations. * * @return void */ public function up() { Schema::table('files', function (Blueprint $table) { $table->integer('file_width')->nullable()->after('filesize'); $table->integer('file_height')->nullable()->after('file_width'); $table->integer('thumbnail_width')->nullable()->after('has_thumbnail'); $table->integer('thumbnail_height')->nullable()->after('thumbnail_width'); }); FileStorage::where('has_thumbnail', true)->where('mime', 'like', 'image/%')->chunk(100, function ($files) { echo "\tMeasuring 100 images.\n"; foreach ($files as $file) { $image = (new ImageManager())->make($file->getFullPath()); $file->file_height = $image->height(); $file->file_width = $image->width(); $thumb = (new ImageManager())->make($file->getFullPathThumb()); $file->thumbnail_height = $thumb->height(); $file->thumbnail_width = $thumb->width(); $file->save(); } }); }
/** * Delivers an image. * * @param string $hash * @param string $filename * @param boolean $thumbnail * @return Response */ public function getImage($hash = false, $filename = false, $thumbnail = false) { if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { header('HTTP/1.1 304 Not Modified'); die; } if ($hash !== false && $filename !== false) { $FileStorage = FileStorage::getHash($hash); $storagePath = !$thumbnail ? $FileStorage->getPath() : $FileStorage->getPathThumb(); $storagePathFull = !$thumbnail ? $FileStorage->getFullPath() : $FileStorage->getFullPathThumb(); $cacheTime = 315360000; /// 10 years if ($FileStorage instanceof FileStorage && Storage::exists($storagePath)) { $responseSize = Storage::size($storagePath); $responseHeaders = ['Cache-Control' => "public, max-age={$cacheTime}, pre-check={$cacheTime}", 'Expires' => gmdate(DATE_RFC1123, time() + $cacheTime), 'Last-Modified' => gmdate(DATE_RFC1123, File::lastModified($storagePathFull)), 'Content-Disposition' => "inline", 'Content-Length' => $responseSize, 'Content-Type' => $FileStorage->mime, 'Filename' => $filename]; $response = Response::stream(function () use($storagePathFull) { readfile($storagePathFull); }, 200, $responseHeaders); return $response; } } return abort(404); }
/** * Uploads a single file. * * @param Request $request * @param Board $board * @return json */ public function putFile(Request $request, Board $board) { $input = Input::all(); $rules = []; PostRequest::rulesForFiles($board, $rules); $rules['files'][] = "required"; $validator = Validator::make($input, $rules); if (!$validator->passes()) { return json_encode(['errors' => $validator->errors()]); } $storage = new Collection(); foreach ($input['files'] as $file) { $ip = new IP($request->ip()); $uploadSize = (int) Cache::get("upstream_data_for_" . $ip->toLong(), 0); if ($uploadSize <= 52430000) { Cache::increment("upstream_data_for_" . $ip->toLong(), $file->getSize(), 2); $newStorage = FileStorage::storeUpload($file); $storage[$newStorage->hash] = $newStorage; Cache::decrement("upstream_data_for_" . $ip->toLong(), $file->getSize()); } else { return abort(429); } } return $storage; }
/** * 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; }
public function importInfinityPostAttachments(Post $model, Board &$board, $post) { $post_id = $model->post_id; if (!$post_id) { return 0; } $attachments = @json_decode($post->files, true); if (!is_array($attachments)) { return 0; } $aModels = []; foreach ($attachments as $aIndex => $attachment) { if (isset($attachment['error']) && $attachment['error']) { continue; } if (!isset($attachment['file_path']) || !isset($attachment['thumb_path'])) { continue; } $storage = null; $path = "{$this->targetLocation}/{$attachment['file_path']}"; $thumb = "{$this->targetLocation}/{$attachment['thumb_path']}"; if (file_exists($path)) { if (!isset($attachment['type'])) { continue; } if (!isset($attachment['hash'])) { $attachment['hash'] = md5(file_get_contents($path)); } $storage = FileStorage::getHash($attachment['hash']); if (!$storage || (!file_exists($storage->getFullPath()) || !file_exists($storage->getFullPathThumb()))) { $height = null; $width = null; if (isset($attachment['width']) && isset($attachment['height'])) { $height = $attachment['height']; $width = $attachment['width']; } if (!$storage) { $storage = new FileStorage(['hash' => $attachment['hash'], 'banned' => false, 'filesize' => $attachment['size'], 'file_width' => $width, 'file_height' => $height, 'mime' => $attachment['type'], 'meta' => null, 'first_uploaded_at' => Carbon::now(), 'last_uploaded_at' => Carbon::now(), 'upload_count' => 1]); } Storage::makeDirectory($storage->getDirectory()); if (!file_exists($storage->getFullPath())) { if (is_link($storage->getFullPath())) { unlink($storage->getFullPath()); } symlink($path, $storage->getFullPath()); } if ($attachment['thumbwidth'] && file_exists($thumb)) { $storage->has_thumbnail = true; $storage->thumbnail_width = $attachment['thumbwidth']; $storage->thumbnail_height = $attachment['thumbheight']; Storage::makeDirectory($storage->getDirectoryThumb()); if (!file_exists($storage->getFullPathThumb())) { if (is_link($storage->getFullPathThumb())) { unlink($storage->getFullPathThumb()); } symlink($thumb, $storage->getFullPathThumb()); } } $storage->save(); } if ($storage && $storage->exists) { $aModel = ['post_id' => $post_id, 'file_id' => $storage->file_id, 'filename' => $attachment['filename'], 'is_spoiler' => false, 'is_deleted' => false, 'position' => $aIndex]; $aModels[] = $aModel; } else { ++$skips; } } } FileAttachment::insert($aModels); return count($aModels); }
/** * Display existing assets. * * @return Response */ public function putAssets(Request $request, Board $board) { if (!$board->canEditConfig($this->user)) { return abort(403); } $input = Input::all(); $validator = Validator::make($input, ['asset_type' => ["required", "in:board_banner,file_deleted,file_none,file_spoiler"], 'new_board_banner' => ["required_if:asset_type,board_banner", "image", "image_size:<=300,<=100"]]); if (!$validator->passes()) { return redirect()->back()->withErrors($validator->errors()); } // Fetch the asset. $upload = Input::file("new_{$input['asset_type']}"); if (file_exists($upload->getPathname())) { $storage = FileStorage::storeUpload($upload); $asset = new BoardAsset(); $asset->asset_type = "board_banner"; $asset->board_uri = $board->board_uri; $asset->file_id = $storage->file_id; $asset->save(); } return $this->view(static::VIEW_CONFIG, ['board' => $board, 'banners' => $board->getBanners(), 'tab' => "assets"]); }
/** * Uploads a single file. * * @param Request $request * @param Board $board * @return json */ public function putFile(Request $request, Board $board) { $input = Input::all(); $rules = []; PostRequest::rulesForFiles($board, $rules); $rules['files'][] = "required"; $validator = Validator::make($input, $rules); if (!$validator->passes()) { return json_encode(['errors' => $validator->errors()]); } $storage = new Collection(); foreach ($input['files'] as $file) { $newStorage = FileStorage::storeUpload($file); $storage[$newStorage->hash] = $newStorage; } return $storage; }
/** * Delivers an image. * * @param \App\FileAttachment $attachment * @param string $filename * @param boolean $thumbnail * @return Response */ public function getImage($hash = false, $filename = false, $thumbnail = false) { if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { header('HTTP/1.1 304 Not Modified'); die; } $FileStorage = null; if (is_string($hash) && is_string($filename)) { $FileStorage = FileStorage::getHash($hash); } if ($FileStorage instanceof FileStorage) { $storagePath = !$thumbnail ? $FileStorage->getPath() : $FileStorage->getPathThumb(); $storagePathFull = !$thumbnail ? $FileStorage->getFullPath() : $FileStorage->getFullPathThumb(); $cacheTime = 31536000; /// 1 year if ($thumbnail && (!file_exists($storagePathFull) || is_dir($storagePathFull))) { if (is_dir($storagePathFull)) { unlink($storagePathFull); } $FileStorage->processThumb(); } if (is_link($storagePathFull)) { if (!is_readable($storagePathFull)) { abort(500, "Symlink file is unreadable."); } $storageExists = file_exists($storagePathFull); } else { $storageExists = Storage::exists($storagePath); } if ($storageExists) { ini_set("zlib.output_compression", "Off"); $responseSize = filesize($storagePathFull); $responseCode = 200; $responseHeaders = ['Cache-Control' => "public, max-age={$cacheTime}, pre-check={$cacheTime}", 'Expires' => gmdate(DATE_RFC1123, time() + $cacheTime), 'Last-Modified' => gmdate(DATE_RFC1123, File::lastModified($storagePathFull)), 'Content-Disposition' => Request::get('disposition', "inline"), 'Content-Length' => $responseSize, 'Content-Type' => $FileStorage->mime, 'Filename' => urldecode($filename)]; if ($thumbnail) { if ($FileStorage->isImage()) { $responseHeaders['Content-Type'] = Settings::get('attachmentThumbnailJpeg') ? "image/jpg" : "image/png"; } else { if ($FileStorage->isVideo()) { $responseHeaders['Content-Type'] = "image/jpg"; } else { if ($FileStorage->isAudio()) { $responseHeaders['Content-Type'] = "image/png"; } } } } // Determine if we can skip PHP content distribution. // This is hugely important. $xSendFile = false; // APACHE // Relies on the mod_xsendfile module. if (function_exists("apache_get_modules") && in_array("mod_xsendfile", apache_get_modules())) { $xSendFile = true; $responseHeaders['X-Sendfile'] = $storagePathFull; } else { if (preg_match("/nginx\\/1(\\.[0-9]+)+/", $_SERVER['SERVER_SOFTWARE'])) { $xSendFile = true; $responseHeaders['X-Accel-Redirect'] = "/{$storagePath}"; } else { if (preg_match("/lighttpd\\/1(\\.[0-9]+)+/", $_SERVER['SERVER_SOFTWARE'])) { $xSendFile = true; $responseHeaders['X-LIGHTTPD-send-file'] = $storagePathFull; } } } // Seek Audio and Video files. $responseStart = 0; $responseEnd = $responseSize - 1; if ($FileStorage->isVideo()) { $responseHeaders['Accept-Ranges'] = "0-" . ($responseSize - 1); if (isset($_SERVER['HTTP_RANGE'])) { list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); if (strpos($range, ',') !== false) { return Response::make("Requested Range Not Satisfiable", 416, ['Content-Range' => "bytes {$responseStart}-{$responseEnd}/{$responseSize}"]); } if ($range == '-') { $responseStart = $this->size - substr($range, 1); } else { $range = explode('-', $range); $responseStart = $range[0]; $responseEnd = isset($range[1]) && is_numeric($range[1]) ? $range[1] : $responseEnd; } if ($responseStart > $responseEnd || $responseStart > $responseSize - 1 || $responseEnd >= $responseSize) { return Response::make("Requested Range Not Satisfiable", 416, ['Content-Range' => "bytes {$responseStart}-{$responseEnd}/{$responseSize}"]); } $responseCode = 206; $responseHeaders['Content-Length'] = $responseSize - $responseStart; $responseHeaders['Content-Range'] = "bytes {$responseStart}-{$responseEnd}/{$responseSize}"; unset($responseHeaders['Accept-Ranges']); unset($responseHeaders['Cache-Control']); unset($responseHeaders['Content-Disposition']); unset($responseHeaders['Expires']); } } // Are we using the webserver to send files? if ($xSendFile) { // Yes. // Send an empty 200 response with the headers. return Response::make("", $responseCode, $responseHeaders); } else { // No. // Get our hands dirty and stream the file. return Response::make(function () use($storagePathFull, $responseStart, $responseEnd) { if (!($responseStream = fopen($storagePathFull, 'rb'))) { abort(500, "Could not open requested file."); } if ($responseStart > 0) { fseek($responseStream, $responseStart); } $streamCurrent = 0; while (!feof($responseStream) && $streamCurrent < $responseEnd && connection_status() == 0) { echo fread($responseStream, min(1024 * 16, $responseEnd - $responseStart + 1)); $streamCurrent += 1024 * 16; } fclose($responseStream); }, $responseCode, $responseHeaders); } } } return abort(404); }
/** * Delivers an image. * * @param string $hash * @param string $filename * @param boolean $thumbnail * @return Response */ public function getImage($hash = false, $filename = false, $thumbnail = false) { if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { header('HTTP/1.1 304 Not Modified'); die; } if ($hash !== false && $filename !== false) { $FileStorage = FileStorage::getHash($hash); $storagePath = !$thumbnail ? $FileStorage->getPath() : $FileStorage->getPathThumb(); $storagePathFull = !$thumbnail ? $FileStorage->getFullPath() : $FileStorage->getFullPathThumb(); $cacheTime = 315360000; /// 10 years if ($FileStorage instanceof FileStorage && Storage::exists($storagePath)) { $responseSize = Storage::size($storagePath); $responseCode = 200; $responseHeaders = ['Cache-Control' => "public, max-age={$cacheTime}, pre-check={$cacheTime}", 'Expires' => gmdate(DATE_RFC1123, time() + $cacheTime), 'Last-Modified' => gmdate(DATE_RFC1123, File::lastModified($storagePathFull)), 'Content-Disposition' => "inline", 'Content-Length' => $responseSize, 'Content-Type' => $FileStorage->mime, 'Filename' => $filename]; $responseStart = 0; $responseEnd = $responseSize - 1; if ($FileStorage->isVideo()) { $responseHeaders['Accept-Ranges'] = "0-" . ($responseSize - 1); if (isset($_SERVER['HTTP_RANGE'])) { list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); if (strpos($range, ',') !== false) { return Response::make("Requested Range Not Satisfiable", 416, ['Content-Range' => "bytes {$responseStart}-{$responseEnd}/{$responseSize}"]); } if ($range == '-') { $responseStart = $this->size - substr($range, 1); } else { $range = explode('-', $range); $responseStart = $range[0]; $responseEnd = isset($range[1]) && is_numeric($range[1]) ? $range[1] : $responseEnd; } $responseEnd = $responseEnd > $responseEnd ? $responseEnd : $responseEnd; if ($responseStart > $responseEnd || $responseStart > $responseSize - 1 || $responseEnd >= $responseSize) { return Response::make("Requested Range Not Satisfiable", 416, ['Content-Range' => "bytes {$responseStart}-{$responseEnd}/{$responseSize}"]); } $responseCode = 206; $responseHeaders['Content-Length'] = $responseSize - $responseStart; $responseHeaders['Content-Range'] = "bytes {$responseStart}-{$responseEnd}/{$responseSize}"; unset($responseHeaders['Accept-Ranges']); unset($responseHeaders['Cache-Control']); unset($responseHeaders['Content-Disposition']); unset($responseHeaders['Expires']); } } return Response::stream(function () use($storagePathFull, $responseStart, $responseEnd) { if (!($responseStream = fopen($storagePathFull, 'rb'))) { abort(500, "Could not open requested file."); } if ($responseStart > 0) { fseek($responseStream, $responseStart); } $streamCurrent = 0; while (!feof($responseStream) && $streamCurrent < $responseEnd && connection_status() == 0) { echo fread($responseStream, min(1024 * 16, $responseEnd - $responseStart + 1)); $streamCurrent += 1024 * 16; } fclose($responseStream); }, $responseCode, $responseHeaders); } } return abort(404); }
/** * Add new assets. * * @return Response */ public function putAssets(Request $request, Board $board) { if (!$board->canEditConfig($this->user)) { return abort(403); } if (!!Input::get('delete', false)) { return $this->deleteAssets($request, $board); } $input = Input::all(); $assetType = Input::get('asset_type', false); $validator = Validator::make($input, ['asset_type' => ["required", "in:board_banner,board_banned,board_icon,file_deleted,file_spoiler"], 'new_board_banned' => ["required_if:asset_type,board_banned", "image", "image_size:100-500", "max:250"], 'new_board_banner' => ["required_if:asset_type,board_banner", "image", "image_size:<=300,<=100", "max:1024"], 'new_board_icon' => ["required_if:asset_type,board_icon", "image", "image_aspect:1", "image_size:64,64", "max:50"], 'new_file_deleted' => ["required_if:asset_type,file_deleted", "image", "image_size:100-500", "max:250"], 'new_file_spoiler' => ["required_if:asset_type,file_spoiler", "image", "image_size:100-500", "max:250"]]); if (!$validator->passes()) { return redirect()->back()->withErrors($validator->errors()); } // Fetch the asset. $upload = Input::file("new_{$input['asset_type']}"); $multiples = $assetType == "board_banner" || $assetType == "board_banned"; if (file_exists($upload->getPathname())) { $storage = FileStorage::storeUpload($upload); if ($storage->exists) { if (!$multiples) { $assets = $board->assets()->with('storage')->where('asset_type', $input['asset_type'])->get(); foreach ($assets as $asset) { $asset->delete(); $asset->storage->challengeExistence(); } } $asset = new BoardAsset(); $asset->asset_type = $input['asset_type']; $asset->board_uri = $board->board_uri; $asset->file_id = $storage->file_id; $asset->save(); } else { return redirect()->back()->withErrors(["validation.custom.file_generic"]); } } Event::fire(new BoardWasModified($board)); return $this->getAssets($board); }
/** * Add new assets. * * @return Response */ public function putAssets(Request $request, Board $board) { if (!$board->canEditConfig($this->user)) { return abort(403); } if (!!Input::get('delete', false)) { return $this->deleteAssets($request, $board); } $input = Input::all(); $assetType = Input::get('asset_type', false); $validator = Validator::make($input, ['asset_type' => ["required", "in:board_banner,board_banned,board_icon,board_flags,file_deleted,file_spoiler"], 'new_board_banned' => ["required_if:asset_type,board_banned", "image", "image_size:100-500", "max:250"], 'new_board_banner' => ["required_if:asset_type,board_banner", "image", "image_size:<=300,<=100", "max:1024"], 'new_board_flags' => ["required_if:asset_type,board_flags", "array", "min:1", "max:500"], 'new_board_icon' => ["required_if:asset_type,board_icon", "image", "image_aspect:1", "image_size:64,64", "max:50"], 'new_file_deleted' => ["required_if:asset_type,file_deleted", "image", "image_size:100-500", "max:250"], 'new_file_spoiler' => ["required_if:asset_type,file_spoiler", "image", "image_size:100-500", "max:250"]]); if (!$validator->passes()) { return redirect()->back()->withErrors($validator->errors()); } // Fetch the asset. $multiples = $assetType == "board_banner" || $assetType == "board_banned" || $assetType == "board_flags"; if ($assetType == "board_flags") { $new = $input["new_{$input['asset_type']}"]; $names = isset($new['name']) ? $new['name'] : []; $uploads = isset($new['file']) ? $new['file'] : []; $rules = []; $nameRules = ["required", "string", "between:1,128"]; $imageRules = array_merge(["required"], BoardAsset::getRulesForFlags($board)); foreach (range(0, count($uploads) - 1) as $index) { $rules["name.{$index}"] = $nameRules; $rules["file.{$index}"] = $imageRules; } $validator = Validator::make($new, $rules); if (!$validator->passes()) { return redirect()->back()->withErrors($validator->errors()); } } else { $uploads = [Input::file("new_{$input['asset_type']}")]; } foreach ((array) $uploads as $index => $upload) { if (file_exists($upload->getPathname())) { $storage = FileStorage::storeUpload($upload); if ($storage->exists) { if (!$multiples) { $assets = $board->assets()->with('storage')->where('asset_type', $input['asset_type'])->get(); foreach ($assets as $asset) { $asset->delete(); $asset->storage->challengeExistence(); } } $asset = new BoardAsset(); $asset->asset_type = $input['asset_type']; $asset->asset_name = isset($names[$index]) ? $names[$index] : null; $asset->board_uri = $board->board_uri; $asset->file_id = $storage->file_id; $asset->save(); } else { return redirect()->back()->withErrors(["validation.custom.file_generic"]); } } } Event::fire(new BoardWasModified($board)); return $this->getAssets($board); }
/** * 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)); } }
protected function validateOriginality() { $board = $this->board; $thread = $this->thread; $user = $this->user; $input = $this->all(); $validated = true; $validator = $this->getValidatorInstance(); $messages = $validator->errors(); // Process uploads. if (isset($input['files'])) { $uploads = $input['files']; if (count($uploads) > 0) { // Standard upload originality and integrity checks. if (!$this->dropzone) { foreach ($uploads as $uploadIndex => $upload) { // If a file is uploaded that has a specific filename, it breaks the process. if (method_exists($upload, "getPathname") && !file_exists($upload->getPathname())) { $validated = false; $messages->add("files.{$uploadIndex}", trans("validation.custom.file_corrupt", ["filename" => $upload->getClientOriginalName()])); } } } if ($board->getConfig('originalityImages')) { foreach ($uploads as $uploadIndex => $upload) { if (!$upload instanceof UploadedFile) { continue; } if ($board->getConfig('originalityImages') == "thread") { if ($thread instanceof Post && ($originalPost = FileStorage::checkUploadExists($upload, $board, $thread))) { $validated = false; $messages->add("files.{$uploadIndex}", trans("validation.custom.unoriginal_image_thread", ["filename" => $upload->getClientOriginalName(), "url" => $originalPost->getURL()])); } } else { if ($originalPost = FileStorage::checkUploadExists($upload, $board)) { $validated = false; $messages->add("files.{$uploadIndex}", trans("validation.custom.unoriginal_image_board", ["filename" => $upload->getClientOriginalName(), "url" => $originalPost->getURL()])); } } } } } else { if ($board->getConfig('originalityImages')) { foreach ($uploads['hash'] as $uploadIndex => $upload) { if ($board->getConfig('originalityImages') == "thread") { if ($thread instanceof Post && ($originalPost = FileStorage::checkHashExists($upload, $board, $thread))) { $validated = false; $messages->add("files.{$uploadIndex}", trans("validation.custom.unoriginal_image_thread", ["filename" => $uploads['name'][$uploadIndex], "url" => $originalPost->getURL()])); } } else { if ($originalPost = FileStorage::checkHashExists($upload, $board)) { $validated = false; $messages->add("files.{$uploadIndex}", trans("validation.custom.unoriginal_image_board", ["filename" => $uploads['name'][$uploadIndex], "url" => $originalPost->getURL()])); } } } } } } // Process body checksum for origianlity. $strictness = $board->getConfig('originalityPosts'); if (isset($input['body']) && $strictness) { $checksum = Post::makeChecksum($input['body']); if ($strictness == "board" || $strictness == "boardr9k") { $checksums = PostChecksum::getChecksum($checksum, $board); } else { if ($strictness == "site" || $strictness == "siter9k") { $checksums = PostChecksum::getChecksum($checksum); } } //dd($checksums); if ($checksums->count()) { $validated = false; $messages->add("body", trans("validation.custom.unoriginal_content")); // If we are in R9K mode, set $respectTheRobot property to to false. // This will trigger a Robot ban in failedValidation. $this->respectTheRobot = !($strictness == "boardr9k" || $strictness == "siter9k"); } } if ($validated !== true) { $this->failedValidation($validator); return; } }
/** * 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()); $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; } // 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_id = $this->makeAuthorId(); $this->save(); // Queries and locks are handled automatically after this closure ends. }); // Process uploads. $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::createAttachment($upload, $this); } } } // Finally fire event on OP, if it exists. if ($thread instanceof Post) { Event::fire(new ThreadNewReply($thread)); } }
/** * 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 = Request::getClientIp(); $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; } // Store attachments $uploads = []; if (is_array($files = Input::file('files'))) { $uploads = array_filter($files); } // Store the post in the database. DB::transaction(function () use($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. DB::table('boards')->where('board_uri', $this->board_uri)->increment('posts_total'); // 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; } $thread->reply_last = $this->created_at; $thread->reply_count += 1; $thread->save(); } // Finally, we set our board_id and save. $this->board_id = $posts_total; $this->save(); // Queries and locks are handled automatically after this closure ends. }); // Process uploads. if (count($uploads) > 0) { foreach ($uploads as $uploadIndex => $upload) { if (file_exists($upload->getPathname())) { $uploadName = urlencode($upload->getClientOriginalName()); $uploadExt = pathinfo($uploadName, PATHINFO_EXTENSION); $fileName = basename($uploadName, "." . $uploadExt); $fileExt = $upload->guessExtension(); $storage = FileStorage::storeUpload($upload); $attachment = new FileAttachment(); $attachment->post_id = $this->post_id; $attachment->file_id = $storage->file_id; $attachment->filename = urlencode("{$fileName}.{$fileExt}"); $attachment->save(); } } } // Finally fire event on OP, if it exists. if ($thread instanceof Post) { Event::fire(new ThreadNewReply($thread)); } }