|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
use App\Models\Revision;
|
|
|
|
use App\Models\Row;
|
|
|
|
use App\Models\Table;
|
|
|
|
use App\Models\User;
|
|
|
|
use App\Tables\Changeset;
|
|
|
|
use App\Tables\Column;
|
|
|
|
use App\Tables\ColumnNumerator;
|
|
|
|
use App\Tables\CStructArrayExporter;
|
|
|
|
use App\Tables\CsvExporter;
|
|
|
|
use App\Tables\CXMacroExporter;
|
|
|
|
use App\Tables\JsonExporter;
|
|
|
|
use App\Tables\PhpExporter;
|
|
|
|
use App\Tables\RowNumerator;
|
|
|
|
use DB;
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
use Illuminate\Validation\Rule;
|
|
|
|
use MightyPork\Exceptions\NotApplicableException;
|
|
|
|
use MightyPork\Exceptions\SimpleValidationException;
|
|
|
|
use MightyPork\Utils\Utils;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Table view, creation, settings
|
|
|
|
*/
|
|
|
|
class TableController extends Controller
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* Helper to fetch a table by user and table name
|
|
|
|
*
|
|
|
|
* @param Request $request
|
|
|
|
* @param User $user
|
|
|
|
* @param string $table
|
|
|
|
* @return Table
|
|
|
|
*/
|
|
|
|
private function resolveTable(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 $tableModel;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Favourite a table
|
|
|
|
*/
|
|
|
|
public function favouriteTable(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
\user()->favouriteTables()->attach($tableModel);
|
|
|
|
return redirect($tableModel->viewRoute);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Un-favourite a table
|
|
|
|
*/
|
|
|
|
public function unfavouriteTable(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
\user()->favouriteTables()->detach($tableModel);
|
|
|
|
return redirect($tableModel->viewRoute);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Switch table's current revision
|
|
|
|
*/
|
|
|
|
public function revertTo(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$input = $this->validate($request, [
|
|
|
|
'rev' => 'int'
|
|
|
|
]);
|
|
|
|
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
$this->authorize('edit', $tableModel);
|
|
|
|
|
|
|
|
$revisionNum = (int)$input->rev;
|
|
|
|
$revision = $tableModel->revisions()->orderBy('created_at')->skip($revisionNum - 1)->first();
|
|
|
|
if ($revision === null) abort(404, "No such revision");
|
|
|
|
|
|
|
|
$tableModel->revision()->associate($revision);
|
|
|
|
$tableModel->save();
|
|
|
|
|
|
|
|
return redirect($tableModel->revisionsRoute);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show a table
|
|
|
|
*/
|
|
|
|
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'])
|
|
|
|
->where('name', $table)->first();
|
|
|
|
if ($tableModel === null) abort(404, "No such table.");
|
|
|
|
|
|
|
|
// option to show other revisions
|
|
|
|
$revisionNum = $tableModel->revisions_count;
|
|
|
|
if ($input->has('rev')) {
|
|
|
|
$revisionNum = (int)$input->rev;
|
|
|
|
$revision = $tableModel->revisions()->orderBy('created_at')->skip($revisionNum - 1)->first();
|
|
|
|
if ($revision === null) abort(404, "No such revision");
|
|
|
|
} else {
|
|
|
|
$revision = $tableModel->revision;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->countTableVisit($request, $tableModel);
|
|
|
|
|
|
|
|
$columns = Column::columnsFromJson($revision->columns);
|
|
|
|
|
|
|
|
$rq = $revision->rowsData($columns);
|
|
|
|
if (count($columns)) $rq = $rq->sortByCol($columns[0]);
|
|
|
|
$rows = $rq->paginate(25, []);
|
|
|
|
|
|
|
|
Row::disableCasts();
|
|
|
|
return view('table.view', [
|
|
|
|
'table' => $tableModel,
|
|
|
|
'revision' => $revision,
|
|
|
|
'revisionNum' => $revisionNum,
|
|
|
|
'proposals_count' => $tableModel->proposals()->unmerged($tableModel)->count(),
|
|
|
|
'columns' => $columns,
|
|
|
|
'rows' => $rows,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a table
|
|
|
|
*/
|
|
|
|
public function delete(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $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
|
|
|
|
*/
|
|
|
|
public function create()
|
|
|
|
{
|
|
|
|
$exampleData = "";
|
|
|
|
|
|
|
|
$columns = Column::columnsFromJson([
|
|
|
|
['id' => 1, 'name' => 'column_1', 'type' => 'string', 'title' => 'First Column'],
|
|
|
|
]);
|
|
|
|
|
|
|
|
return view('table.create', [
|
|
|
|
'exampleColumns' => '',
|
|
|
|
'columns' => $columns,
|
|
|
|
'exampleData' => $exampleData,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show table settings edit form
|
|
|
|
*/
|
|
|
|
public function settings(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
$this->authorize('edit', $tableModel);
|
|
|
|
|
|
|
|
return view('table.conf', [
|
|
|
|
'table' => $tableModel,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Store modified table settings
|
|
|
|
*/
|
|
|
|
public function storeSettings(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
$this->authorize('edit', $tableModel);
|
|
|
|
|
|
|
|
$input = $this->validate($request, [
|
|
|
|
'name' => [
|
|
|
|
'required',
|
|
|
|
VALI_NAME,
|
|
|
|
],
|
|
|
|
'title' => ['required', VALI_LINE],
|
|
|
|
'description' => ['nullable', VALI_TEXT],
|
|
|
|
'license' => ['nullable', VALI_TEXT],
|
|
|
|
'origin' => ['nullable', VALI_TEXT],
|
|
|
|
]);
|
|
|
|
|
|
|
|
$otherTableNames = $user->tables()->get(['name'])->pluck('name')->diff([$table])->all();
|
|
|
|
if (in_array($input->name, $otherTableNames)) {
|
|
|
|
return $this->backWithErrors(['name' => "You already have a table called \"$input->name\""]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$tableModel->fill($input->all());
|
|
|
|
$tableModel->save();
|
|
|
|
|
|
|
|
flash()->success('Table settings saved');
|
|
|
|
|
|
|
|
return redirect($tableModel->viewRoute); // the route now changed
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List of table revisions
|
|
|
|
*/
|
|
|
|
public function viewRevisions(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
|
|
|
|
$revisions = $tableModel->revisions()->orderBy('created_at', 'desc')->get();
|
|
|
|
|
|
|
|
return view('table.revisions', [
|
|
|
|
'table' => $tableModel,
|
|
|
|
'revisions' => $revisions,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List of table favouriting users
|
|
|
|
*/
|
|
|
|
public function viewFavourites(Request $request, User $user, string $table)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $table);
|
|
|
|
|
|
|
|
$users = $tableModel->favouritingUsers()->orderBy('title')->get();
|
|
|
|
|
|
|
|
return view('table.favourites', [
|
|
|
|
'table' => $tableModel,
|
|
|
|
'users' => $users,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Store a newly created table (save form)
|
|
|
|
*/
|
|
|
|
public function storeNew(Request $request)
|
|
|
|
{
|
|
|
|
/** @var User $u */
|
|
|
|
$u = \Auth::user();
|
|
|
|
|
|
|
|
$input = $this->validate($request, [
|
|
|
|
'name' => [
|
|
|
|
'required',
|
|
|
|
VALI_NAME,
|
|
|
|
],
|
|
|
|
'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' => "You already have a table called \"$input->name\"",
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- 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 ---
|
|
|
|
$fname = 'csv-file';
|
|
|
|
$dataCsvLines = [];
|
|
|
|
if ($request->hasFile($fname)) {
|
|
|
|
try {
|
|
|
|
$file = $request->file($fname);
|
|
|
|
if ($file->isValid() && $file->isReadable()) {
|
|
|
|
$handle = $file->openFile();
|
|
|
|
$dataCsvLines = Utils::csvToArray($handle);
|
|
|
|
if (empty($dataCsvLines)) throw new \Exception("Failed to parse CSV file.");
|
|
|
|
$handle = null;
|
|
|
|
} else {
|
|
|
|
throw new \Exception("File not valid.");
|
|
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return $this->backWithErrors(['csv-file' => $e->getMessage()]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if ($input->data) {
|
|
|
|
try {
|
|
|
|
$text = trim($input->data);
|
|
|
|
if (!empty($text)) {
|
|
|
|
$dataCsvLines = Utils::csvToArray($text);
|
|
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return $this->backWithErrors(['data' => $e->getMessage()]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Preparing data to insert into the Rows table
|
|
|
|
$rowsToInsert = null;
|
|
|
|
$rowNumerator = null;
|
|
|
|
try {
|
|
|
|
$rowsToInsert = (new Changeset)->csvToRowsArray($columns, $dataCsvLines, false)->all();
|
|
|
|
} 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) {
|
|
|
|
$newRevision = Revision::create($revisionFields);
|
|
|
|
|
|
|
|
$tableFields['revision_id'] = $newRevision->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($newRevision);
|
|
|
|
$table->revision()->associate($newRevision);
|
|
|
|
|
|
|
|
// Spawn rows, linked to the revision
|
|
|
|
if (!empty($rowsToInsert)) {
|
|
|
|
// this replicates the code in Proposal->toRevision
|
|
|
|
$conn = DB::connection();
|
|
|
|
$prepared = $conn->getPdo()->prepare('INSERT INTO rows (data) VALUES (?)');
|
|
|
|
foreach (array_chunk($rowsToInsert, 10000) as $i => $chunk) {
|
|
|
|
$ids = [];
|
|
|
|
foreach ($chunk as $newRow) {
|
|
|
|
$prepared->execute([json_encode($newRow)]);
|
|
|
|
$ids[] = $conn->getPdo()->lastInsertId();
|
|
|
|
}
|
|
|
|
|
|
|
|
$qms = rtrim(str_repeat('('.$newRevision->id.', ?),', count($ids)), ',');
|
|
|
|
$conn->statement('INSERT INTO revision_row_pivot (revision_id, row_id) VALUES '.$qms.';',
|
|
|
|
$ids);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
$tableModel = $this->resolveTable($request, $user, $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);
|
|
|
|
}
|
|
|
|
}
|