validate($request, [ 'rev' => 'nullable|int' ]); /** @var Table $tableModel */ $tableModel = $user->tables()->withCount(['favourites', 'forks', 'revisions', 'comments', 'proposals']) ->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); // option to show other revisions if ($input->has('rev')) { $rev = (int)$input->rev; $revision = $tableModel->revisions()->orderBy('created_at')->skip($rev - 1)->first(); if ($revision === null) abort(404, "No such revision"); } else { $revision = $tableModel->revision; } $this->countTableVisit($request, $tableModel); $columns = Column::columnsFromJson($revision->columns); $rows = $revision->rowsData($columns)->paginate(25, []); return view('table.view', [ 'table' => $tableModel, 'revision' => $revision, 'columns' => $columns, 'rows' => $rows, ]); } public function draftChange(Request $request, User $user, string $table) { /** @var Table $tableModel */ $tableModel = $user->tables()->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); $session_key = "proposal_{$tableModel->id}"; /** @var Changeset $changeset */ $changeset = $request->session()->remember($session_key, function () use ($tableModel) { $changeset = new Changeset(); $changeset->table = $tableModel; $changeset->revision = $tableModel->revision; return $changeset; }); $revision = $changeset->revision; $columns = Column::columnsFromJson($revision->columns); $rows = $revision->rowsData($columns)->paginate(25, []); return view('table.propose', [ 'table' => $tableModel, 'revision' => $revision, 'columns' => $columns, 'rows' => $rows, 'changeset' => $changeset, ]); } public function delete(Request $request, User $user, string $table) { /** @var Table $tableModel */ $tableModel = $user->tables()->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); $this->authorize('delete', $tableModel); if ($request->get('table-name', null) !== $table) { return $this->backWithErrors(['table-name' => 'Fill table name']); } $tableModel->delete(); return redirect(route('profile.view', $user->name)); } /** * SHow a form for creating a new table * * @return \Illuminate\Http\Response */ public function create() { $exampleData = "Mercenaria mercenaria,hard clam,40\n" . "Magallana gigas,pacific oyster,30\n" . "Patella vulgata,common limpet,20"; $columns = Column::columnsFromJson([ // fake 'id' to satisfy the check in Column constructor ['name' => 'latin', 'type' => 'string', 'title' => 'Latin Name'], ['name' => 'common', 'type' => 'string', 'title' => 'Common Name'], ['name' => 'lifespan', 'type' => 'int', 'title' => 'Lifespan (years)'] ]); return view('table.create', [ 'exampleColumns' => '', 'columns' => $columns, 'exampleData' => $exampleData, ]); } /** * Show table settings edit form */ public function settings(Request $request, User $user, string $table) { /** @var Table $tableModel */ $tableModel = $user->tables()->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); $this->authorize('edit', $tableModel); return view('table.conf', [ 'table' => $tableModel, ]); } /** * Store modified table settings */ public function storeSettings(Request $request, User $user, string $table) { /** @var Table $tableModel */ $tableModel = $user->tables()->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); $this->authorize('edit', $tableModel); $input = $this->validate($request, [ 'name' => [ 'required', VALI_NAME, Rule::unique('tables')->ignoreModel($tableModel), ], 'title' => ['required', VALI_LINE], 'description' => ['nullable', VALI_TEXT], 'license' => ['nullable', VALI_TEXT], 'origin' => ['nullable', VALI_TEXT], ]); $tableModel->fill($input->all()); $tableModel->save(); flash()->success('Table settings saved'); return redirect($tableModel->viewRoute); // the route now changed } public function storeNew(Request $request) { /** @var User $u */ $u = \Auth::user(); $input = $this->validate($request, [ 'name' => [ 'required', VALI_NAME, Rule::unique('tables'), ], 'title' => ['required', VALI_LINE], 'description' => ['nullable', VALI_TEXT], 'license' => ['nullable', VALI_TEXT], 'origin' => ['nullable', VALI_TEXT], 'columns' => 'required|json', 'data' => 'string|nullable', ]); // Check if table name is unique for user if ($u->hasTable($input->name)) { return $this->backWithErrors([ 'name' => "A table called \"$input->name\" already exists in your account.", ]); } // --- COLUMNS --- // Parse and validate the columns specification /** @var Column[] $columns */ $columns = []; $col_names = []; // for checking duplicates $colsArray = fromJSON($input->columns); // prevent griefing via long list of columns if (count($colsArray) > 100) return $this->backWithErrors(['columns' => "Too many columns"]); foreach ($colsArray as $colObj) { // ensure column has a title if (!isset($colObj->title)) { $colObj->title = $colObj->name; } try { if (in_array($colObj->name, $col_names)) { return $this->backWithErrors(['columns' => "Duplicate column: $colObj->name"]); } $col_names[] = $colObj->name; $columns[] = new Column($colObj); } catch (\Exception $e) { // validation errors from the Column constructor return $this->backWithErrors(['columns' => $e->getMessage()]); } } if (count($columns) == 0) { return $this->backWithErrors(['columns' => "Define at least one column"]); } // Now assign column IDs $columnNumerator = new ColumnNumerator(count($columns)); foreach ($columns as $column) { $column->setID($columnNumerator->next()); } // --- DATA --- $dataCsvLines = array_map('str_getcsv', explode("\n", $input->data)); // Preparing data to insert into the Rows table $rowsToInsert = null; $rowNumerator = null; try { $rowsToInsert = collect($dataCsvLines)->map(function ($row) use ($columns) { if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null; // discard empty lines if (count($row) != count($columns)) { throw new NotApplicableException("All rows must have " . count($columns) . " fields."); } $data = []; foreach ($row as $i => $val) { $col = $columns[$i]; if (strlen($val) > 1000) { // try to stop people inserting unstructured crap / malformed CSV throw new NotApplicableException("Value for column {$col->name} too long."); } $data[$col->id] = $col->cast($val); } // return in a format prepared for filling eloquent return ['data' => $data]; })->filter()->all(); // remove nulls, to array // attach _id labels to all rows $rowNumerator = new RowNumerator(count($dataCsvLines)); foreach ($rowsToInsert as &$item) { $item['data']['_id'] = $rowNumerator->next(); } } catch (\Exception $e) { return $this->backWithErrors(['data' => $e->getMessage()]); } // --- STORE TO DB --- /* Fields for the new Revision instance */ $revisionFields = [ 'note' => "Initial revision of table $u->name/$input->name", 'columns' => $columns, 'row_count' => count($rowsToInsert), ]; /* Fields for the new Table instance */ $tableFields = [ 'owner_id' => $u->id, 'name' => $input->name, 'title' => $input->title, 'description' => $input->description, 'license' => $input->license, 'origin' => $input->origin, ]; /** @var Table $table */ $table = null; \DB::transaction(function () use ($revisionFields, $tableFields, $rowsToInsert, &$table) { $revision = Revision::create($revisionFields); $tableFields['revision_id'] = $revision->id; // current revision (not-null constraint on this FK) $table = Table::create($tableFields); // Attach the revision to the table, set as current $table->revisions()->attach($revision); $table->revision()->associate($revision); // Spawn rows, linked to the revision $revision->rows()->createMany($rowsToInsert); }); return redirect($table->viewRoute); } /** * Check unique visit, filter bots / scripts, and increment visits count. * * @param Request $request * @param Table $table */ private function countTableVisit(Request $request, Table $table) { $cookieName = "view_{$table->owner->name}_{$table->name}"; if (!$request->cookie($cookieName, false)) { $ua = $request->userAgent(); // Filter out suspicious user agents if (! str_contains(strtolower($ua), Controller::BOT_USER_AGENTS)) { $table->countVisit(); \Cookie::queue($cookieName, true, 24*60); // in minutes } } } /** * Simple export via a preset * * @param Request $request * @param User $user * @param string $table */ public function export(Request $request, User $user, string $table) { /** @var Table $tableModel */ $tableModel = $user->tables()->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); $exporter = null; switch ($request->get('format')) { case 'json': $exporter = new JsonExporter($tableModel); break; case 'csv': $exporter = new CsvExporter($tableModel); break; case 'csv-tab': $exporter = (new CsvExporter($tableModel))->withDelimiter("\t"); break; case 'c': $exporter = new CStructArrayExporter($tableModel); break; case 'xmacro': $noq = explode(',', $request->input('noq','')); $exporter = (new CXMacroExporter($tableModel))->noQuotesAround($noq); break; case 'php': $exporter = new PhpExporter($tableModel); break; case 'js': $exporter = (new JsonExporter($tableModel))->withJsWrapper(); break; default: abort(400, "Unspecified or unknown format."); } $dl = Utils::parseBool($request->get('dl', false)); $exporter->exportToBrowser($dl); } }