datatable.directory codebase https://datatable.directory/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
datatable.directory/app/Http/Controllers/TableController.php

320 lines
11 KiB

<?php
namespace App\Http\Controllers;
6 years ago
use App\Models\Revision;
use App\Models\Row;
use App\Models\Table;
6 years ago
use App\Models\User;
use App\Tables\Column;
use App\Tables\CStructArrayExporter;
use App\Tables\CsvExporter;
use App\Tables\CXMacroExporter;
use App\Tables\JsonExporter;
use App\Tables\PhpExporter;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
6 years ago
use MightyPork\Exceptions\NotApplicableException;
use MightyPork\Utils\Utils;
class TableController extends Controller
{
public function view(Request $request, User $user, string $table)
{
$input = $this->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.");
// make it possible 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);
return view('table.view', [
'table' => $tableModel,
'revision' => $revision,
'columns' => Column::columnsFromJson($revision->columns),
'rows' => $revision->rows()->paginate(25),
]);
}
6 years ago
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 =
6 years ago
"Mercenaria mercenaria,hard clam,40\n" .
"Magallana gigas,pacific oyster,30\n" .
"Patella vulgata,common limpet,20";
$columns = Column::columnsFromJson([
['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,
]);
}
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,
]);
}
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)
{
6 years ago
/** @var User $u */
$u = \Auth::user();
6 years ago
$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
6 years ago
if ($u->tables()->where('name', $input->name)->exists()) {
return $this->backWithErrors([
6 years ago
'name' => "A table called \"$input->name\" already exists in your account.",
]);
}
// Parse and validate the columns specification
6 years ago
/** @var Column[] $columns */
$columns = [];
$column_keys = []; // 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 $col) {
if (!isset($col->name) || !isset($col->type) || empty($col->name) || empty($col->type)) {
return $this->backWithErrors(['columns' => "All columns must have at least name and type."]);
6 years ago
}
6 years ago
try {
if (in_array($col->name, $column_keys)) {
return $this->backWithErrors(['columns' => "Duplicate column: $col->name"]);
}
$column_keys[] = $col->name;
if (!isset($col->title)) $col->title = $col->name;
$columns[] = new Column($col);
6 years ago
} catch (\Exception $e) {
return $this->backWithErrors(['columns' => $e->getMessage()]);
}
}
if (count($columns) == 0) return $this->backWithErrors(['columns' => "Define at least one column"]);
6 years ago
6 years ago
$rowTable = array_map('str_getcsv', explode("\n", $input->data));
6 years ago
// Preparing data to insert into the Rows table
6 years ago
$rowsData = null;
try {
6 years ago
$grid_range = Row::allocateGRIDs(count($rowTable));
$grid_cnt = $grid_range[0];
$rowsData = array_map(function ($row) use ($columns, &$grid_cnt) {
if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null;
6 years ago
if (count($row) != count($columns)) {
throw new NotApplicableException("All rows must have " . count($columns) . " fields.");
6 years ago
}
6 years ago
$parsed = [
'_grid' => $grid_cnt++
];
6 years ago
foreach ($row as $i => $val) {
$key = $columns[$i]->name;
6 years ago
if (strlen($val) > 255) {
// try to stop people inserting unstructured crap / malformed CSV
throw new NotApplicableException("Value for column $key too long.");
}
6 years ago
$parsed[$key] = $columns[$i]->cast($val);
}
return [
'data' => $parsed,
6 years ago
];
}, $rowTable);
$rowsData = array_filter($rowsData);
} catch (\Exception $e) {
return $this->backWithErrors(['data' => $e->getMessage()]);
6 years ago
}
$revisionFields = [
6 years ago
'note' => "Initial revision of table $u->name/$input->name",
'columns' => $columns,
'row_count' => count($rowsData),
];
6 years ago
$tableFields = [
6 years ago
'owner_id' => $u->id,
'revision_id' => 0,
6 years ago
'name' => $input->name,
'title' => $input->title,
'description' => $input->description,
'license' => $input->license,
'origin' => $input->origin,
];
\DB::transaction(function () use ($revisionFields, $tableFields, $rowsData) {
$revision = Revision::create($revisionFields);
$tableFields['revision_id'] = $revision->id; // to meet the not-null constraint
$table = Table::create($tableFields);
// Attach the revision to the table, set as current
$table->revisions()->attach($revision);
$table->revision()->associate($revision);
6 years ago
// Spawn rows, linked to the revision
$revision->rows()->createMany($rowsData);
});
6 years ago
return redirect(route('table.view', ['user' => $u, 'table' => $input->name]));
}
/**
* 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->id";
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
}
}
}
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);
}
}