|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
use App\Models\Revision;
|
|
|
|
use App\Models\Row;
|
|
|
|
use App\Models\Table;
|
|
|
|
use App\Models\User;
|
|
|
|
use App\Utils\Column;
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
use Illuminate\Validation\Rule;
|
|
|
|
use MightyPork\Exceptions\NotApplicableException;
|
|
|
|
|
|
|
|
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(json_decode($revision->columns)),
|
|
|
|
'rows' => $revision->rows()->paginate(25),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* SHow a form for creating a new table
|
|
|
|
*
|
|
|
|
* @return \Illuminate\Http\Response
|
|
|
|
*/
|
|
|
|
public function create()
|
|
|
|
{
|
|
|
|
$exampleColumns =
|
|
|
|
"latin,string,Latin Name\n" .
|
|
|
|
"common,string,Common Name\n" .
|
|
|
|
"lifespan,int,Lifespan (years)";
|
|
|
|
|
|
|
|
$exampleData =
|
|
|
|
"Mercenaria mercenaria,hard clam,40\n" .
|
|
|
|
"Magallana gigas,pacific oyster,30\n" .
|
|
|
|
"Patella vulgata,common limpet,20";
|
|
|
|
|
|
|
|
return view('table.create',
|
|
|
|
compact('exampleColumns', '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.");
|
|
|
|
|
|
|
|
return view('table.edit', [
|
|
|
|
'table' => $tableModel,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
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|string',
|
|
|
|
'data' => 'string|nullable',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Check if table name is unique for user
|
|
|
|
if ($u->tables()->where('name', $input->name)->exists()) {
|
|
|
|
return $this->backWithErrors([
|
|
|
|
'name' => "A table called \"$input->name\" already exists in your account.",
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse and validate the columns specification
|
|
|
|
/** @var Column[] $columns */
|
|
|
|
$columns = [];
|
|
|
|
$column_keys = []; // for checking duplicates
|
|
|
|
$colTable = array_map('str_getcsv', explode("\n", $input->columns));
|
|
|
|
|
|
|
|
// prevent griefing via long list of columns
|
|
|
|
if (count($colTable) > 100) return $this->backWithErrors(['columns' => "Too many columns"]);
|
|
|
|
|
|
|
|
foreach ($colTable as $col) {
|
|
|
|
$col = array_map('trim', $col);
|
|
|
|
if (count($col) < 2 || strlen($col[0])==0) {
|
|
|
|
return $this->backWithErrors(['columns' => "All columns must have at least name and type."]);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (in_array($col[0], $column_keys)) {
|
|
|
|
return $this->backWithErrors(['columns' => "Duplicate column: $col[0]"]);
|
|
|
|
}
|
|
|
|
$column_keys[] = $col[0];
|
|
|
|
|
|
|
|
$columns[] = new Column([
|
|
|
|
'name' => $col[0],
|
|
|
|
'type' => $col[1],
|
|
|
|
'title' => count($col) >= 3 ? $col[2] : $col[0], // title falls back to =name if not specified,
|
|
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return $this->backWithErrors(['columns' => $e->getMessage()]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (count($columns) == 0) return $this->backWithErrors(['columns' => "Define at least one column"]);
|
|
|
|
|
|
|
|
$rowTable = array_map('str_getcsv', explode("\n", $input->data));
|
|
|
|
|
|
|
|
// Preparing data to insert into the Rows table
|
|
|
|
$rowsData = null;
|
|
|
|
try {
|
|
|
|
$rowsData = array_map(function ($row) use ($columns) {
|
|
|
|
if (count($row) == 0 || count($row)==1&&$row[0]=='') return null;
|
|
|
|
if (count($row) != count($columns)) {
|
|
|
|
throw new NotApplicableException("All rows must have " . count($columns) . " fields.");
|
|
|
|
}
|
|
|
|
$parsed = [];
|
|
|
|
foreach ($row as $i => $val) {
|
|
|
|
$key = $columns[$i]->name;
|
|
|
|
if (strlen($val) > 255) {
|
|
|
|
// try to stop people inserting unstructured crap / malformed CSV
|
|
|
|
throw new NotApplicableException("Value for column $key too long.");
|
|
|
|
}
|
|
|
|
$parsed[$key] = $columns[$i]->cast($val);
|
|
|
|
}
|
|
|
|
return [
|
|
|
|
'data' => json_encode($parsed),
|
|
|
|
];
|
|
|
|
}, $rowTable);
|
|
|
|
|
|
|
|
$rowsData = array_filter($rowsData);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return $this->backWithErrors(['data' => $e->getMessage()]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$revisionFields = [
|
|
|
|
'note' => "Initial revision of table $u->name/$input->name",
|
|
|
|
'columns' => json_encode($columns),
|
|
|
|
'row_count' => count($rowsData),
|
|
|
|
];
|
|
|
|
|
|
|
|
$tableFields = [
|
|
|
|
'owner_id' => $u->id,
|
|
|
|
'revision_id' => 0,
|
|
|
|
'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);
|
|
|
|
|
|
|
|
// Spawn rows, linked to the revision
|
|
|
|
$revision->rows()->createMany($rowsData);
|
|
|
|
});
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|