diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index f21741e..bc71626 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -49,16 +49,14 @@ class RegisterController extends Controller */ protected function validator(array $data) { - return Validator::make($data, [ + return $this->makeValidator($data, [ 'name' => [ - 'regex:/^[a-zA-Z0-9_.-]+$/', 'required', - 'string', - 'max:255', + VALI_NAME, 'unique:users' ], - 'email' => 'required|string|email|max:255|unique:users', - 'password' => 'required|string|min:6|max:1000|confirmed', // max len to foil DOS attempts + 'email' => ['required', 'unique:users', VALI_EMAIL], + 'password' => ['required', 'confirmed', VALI_PASSWORD], ]); } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 83b4f91..5b2cec2 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -3,13 +3,39 @@ namespace App\Http\Controllers; use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; class Controller extends BaseController { - use AuthorizesRequests, DispatchesJobs, ValidatesRequests; + use AuthorizesRequests, + DispatchesJobs, + ValidatesRequests { + ValidatesRequests::validate as validate_orig; + ValidatesRequests::validateWithBag as validateWithBag_orig; + } + + // Hacks to allow recursive nesting of validations in string and array format + + public function makeValidator($data, $rules, $messages = array(), $customAttributes = array()) + { + return \Validator::make($data, vali($rules), $messages, $customAttributes); + } + + public function validate(Request $request, array $rules, + array $messages = [], array $customAttributes = []) + { + return objBag($this->validate_orig($request, vali($rules), $messages, $customAttributes)); + } + + public function validateWithBag($errorBag, Request $request, array $rules, + array $messages = [], array $customAttributes = []) + { + return objBag($this->validateWithBag_orig($errorBag, $request, vali($rules), + $messages, $customAttributes)); + } protected function backWithErrors($errors) { diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index 2066139..7e9696d 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -54,21 +54,24 @@ class TableController extends Controller /** @var User $u */ $u = \Auth::user(); - $this->validate($request, [ - 'name' => 'required|string|max:255', - 'title' => 'string|string|max:255', - 'description' => 'string|nullable|max:4000', - 'license' => 'string|nullable|max:4000', - 'origin' => 'string|nullable|max:4000', + $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 - $tabName = $request->get('name'); - if ($u->tables()->where('name', $tabName)->exists()) { + if ($u->tables()->where('name', $input->name)->exists()) { return $this->backWithErrors([ - 'name' => "A table called \"$tabName\" already exists in your account.", + 'name' => "A table called \"$input->name\" already exists in your account.", ]); } @@ -76,7 +79,7 @@ class TableController extends Controller /** @var Column[] $columns */ $columns = []; $column_keys = []; // for checking duplicates - $colTable = array_map('str_getcsv', explode("\n", $request->get('columns'))); + $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"]); @@ -104,7 +107,7 @@ class TableController extends Controller } if (count($columns) == 0) return $this->backWithErrors(['columns' => "Define at least one column"]); - $rowTable = array_map('str_getcsv', explode("\n", $request->get('data'))); + $rowTable = array_map('str_getcsv', explode("\n", $input->data)); // Preparing data to insert into the Rows table $rowsData = null; @@ -116,6 +119,10 @@ class TableController extends Controller $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 [ @@ -127,7 +134,7 @@ class TableController extends Controller } $revisionFields = [ - 'note' => "Initial revision of table $u->name/$tabName", + 'note' => "Initial revision of table $u->name/$input->name", 'columns' => json_encode($columns), 'row_count' => count($rowsData), ]; @@ -135,11 +142,11 @@ class TableController extends Controller $tableFields = [ 'owner_id' => $u->id, 'revision_id' => 0, - 'name' => $tabName, - 'title' => $request->get('title'), - 'description' => $request->get('description'), - 'license' => $request->get('license'), - 'origin' => $request->get('origin'), + 'name' => $input->name, + 'title' => $input->title, + 'description' => $input->description, + 'license' => $input->license, + 'origin' => $input->origin, ]; \DB::transaction(function () use ($revisionFields, $tableFields, $rowsData) { @@ -156,6 +163,6 @@ class TableController extends Controller $revision->rows()->createMany($rowsData); }); - return redirect(route('table.view', ['user' => $u, 'table' => $tabName])); + return redirect(route('table.view', ['user' => $u, 'table' => $input->name])); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 0fa0d3d..c1ba545 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,7 +3,12 @@ namespace App\Http\Controllers; +use App\Models\EmailConfirmation; use App\Models\User; +use Hash; +use Illuminate\Http\Request; +use Illuminate\Validation\Rule; +use MightyPork\Utils\Str; class UserController extends Controller { @@ -22,18 +27,66 @@ class UserController extends Controller } /** - * Edit user profile + * Edit own profile * * @param User $user * @return \Illuminate\View\View */ public function edit() { - return view('user.edit')->with('user', \Auth::user()); + return view('user.edit')->with('user', \user()); } - public function store() + /** + * Store changed profile + */ + public function store(Request $request) { - echo "Not impl"; + $input = $this->validate($request, [ + 'name' => [ + 'required', + VALI_NAME, + Rule::unique('users')->ignoreModel(\user()), + ], + 'email' => [ + 'required', + VALI_EMAIL, + Rule::unique('users')->ignoreModel(\user()), + ], + 'bio' => ['nullable', VALI_TEXT], + 'title' => ['required', VALI_LINE], + 'website' => ['required', VALI_LINE], + 'new_password' => ['nullable', 'confirmed', VALI_PASSWORD], + ]); + + $user = user(); + + if ($input->email != $user->email) { + $confirmation = EmailConfirmation::create([ + 'user_id' => $user->id, + 'email' => $input->email, + 'token' => Str::random(60), + ]); + + flash()->warning("New e-mail confirmation sent to $input->email.")->important(); + + // TODO send the e-mail + + unset($input->email); + } + + $user->fill($input->all()); + + if ($input->has('new_password')) { + $user->password = Hash::make($input->new_password); + + flash()->warning('Password changed'); + } + + $user->save(); + + flash()->success('Settings saved'); + + return back(); } } diff --git a/app/Models/EmailConfirmation.php b/app/Models/EmailConfirmation.php index 31ba99e..3393ffe 100644 --- a/app/Models/EmailConfirmation.php +++ b/app/Models/EmailConfirmation.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Model; * @property \Carbon\Carbon $created_at * @property int $user_id * @property string $token + * @property string $email * @property-read User $user */ class EmailConfirmation extends Model diff --git a/app/Models/User.php b/app/Models/User.php index 67c85b7..8988e78 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -21,6 +21,7 @@ use MightyPork\Exceptions\NotExistException; * @property string $name - unique, for vanity URL * @property string $title - for display * @property string $bio - user bio + * @property string $website - custom url * @property string $email - unique, for login and social auth chaining * @property string $password - hashed pw * @property bool $confirmed - user e-mail is confirmed @@ -48,7 +49,7 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'name', 'title', 'email', 'password', + 'name', 'title', 'email', 'password', 'bio', 'website' ]; /** diff --git a/app/Providers/ValidationServiceProvider.php b/app/Providers/ValidationServiceProvider.php new file mode 100644 index 0000000..7e68a23 --- /dev/null +++ b/app/Providers/ValidationServiceProvider.php @@ -0,0 +1,29 @@ + $rules) { + // top level + if (is_array($rules)) { + $ar = []; + foreach ($rules as $rule) { + if (is_string($rule) && strpos($rule, '|') !== false) { + foreach (explode('|', $rule) as $rr) { + $ar[] = $rr; + } + } else if (is_array($rule)) { + // nested array, assume no further recursion + foreach ($rule as $rr) { + $ar[] = $rr; + } + } else { + // Rule + $ar[] = $rule; + } + } + $result[$key] = $ar; + } else { + // string or Rule + $result[$key] = $rules; + } + } + return $result; +} diff --git a/composer.json b/composer.json index 28d4ff5..2a16dbc 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "doctrine/dbal": "^2.7", "fideloper/proxy": "^4.0", "guzzlehttp/guzzle": "^6.0", + "laracasts/flash": "^3.0", "laravel/framework": "5.6.*", "laravel/socialite": "^3.0", "laravel/tinker": "^1.0", diff --git a/composer.lock b/composer.lock index 5d68f45..25b4447 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1bb552d1b383427434286ed0ced8293b", + "content-hash": "4a71a6ea4e0158136c11e0a93170b0c3", "packages": [ { "name": "barryvdh/laravel-debugbar", @@ -1051,6 +1051,60 @@ ], "time": "2015-04-20T18:58:01+00:00" }, + { + "name": "laracasts/flash", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laracasts/flash.git", + "reference": "10cd420ab63fd0796bf5e1e5b99f87636d2f4333" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laracasts/flash/zipball/10cd420ab63fd0796bf5e1e5b99f87636d2f4333", + "reference": "10cd420ab63fd0796bf5e1e5b99f87636d2f4333", + "shasum": "" + }, + "require": { + "illuminate/support": "~5.0", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "dev-master", + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laracasts\\Flash\\FlashServiceProvider" + ], + "aliases": { + "Flash": "Laracasts\\Flash\\Flash" + } + } + }, + "autoload": { + "psr-0": { + "Laracasts\\Flash": "src/" + }, + "files": [ + "src/Laracasts/Flash/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeffrey Way", + "email": "jeffrey@laracasts.com" + } + ], + "description": "Easy flash notifications", + "time": "2017-06-22T19:01:19+00:00" + }, { "name": "laravel/framework", "version": "v5.6.27", diff --git a/config/app.php b/config/app.php index 199b8ff..d154da3 100644 --- a/config/app.php +++ b/config/app.php @@ -159,6 +159,7 @@ return [ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\ValidationServiceProvider::class, MightyPork\Providers\BladeExtensionsProvider::class, MightyPork\Providers\MacroServiceProvider::class, diff --git a/database/migrations/2018_07_22_083900_create_email_confirmations_table.php b/database/migrations/2018_07_22_083900_create_email_confirmations_table.php index 76d1c5b..4d4bf6e 100644 --- a/database/migrations/2018_07_22_083900_create_email_confirmations_table.php +++ b/database/migrations/2018_07_22_083900_create_email_confirmations_table.php @@ -14,6 +14,7 @@ class CreateEmailConfirmationsTable extends Migration public function up() { Schema::create('email_confirmations', function (Blueprint $table) { + $table->increments('id'); $table->unsignedInteger('user_id')->index(); $table->timestamp('created_at')->nullable(); $table->string('email'); diff --git a/porklib/helpers.php b/porklib/helpers.php index 2bb2f40..a88be7c 100644 --- a/porklib/helpers.php +++ b/porklib/helpers.php @@ -65,7 +65,8 @@ function unless($cond, $then, $else = '') * - Undefined keys are returned as null. * - array and object values are wrapped in objBag when returned. */ -class objBag implements JsonSerializable { +class objBag implements JsonSerializable, ArrayAccess { + /** @var object */ private $wrapped; public function __construct($wrapped) @@ -86,7 +87,21 @@ class objBag implements JsonSerializable { return null; } - public function __isset($name) + public function __set($name, $value) + { + if ($this->wrapped) { + $this->wrapped->$name = $value; + } + } + + public function __unset($name) + { + if ($this->wrapped) { + unset($this->wrapped->$name); + } + } + + public function __isset($name) { return isset($this->wrapped->$name); } @@ -99,7 +114,7 @@ class objBag implements JsonSerializable { public function has($name) { - return isset($this->$name); + return isset($this->$name) && $this->$name !== null; } public function unpack() @@ -107,6 +122,16 @@ class objBag implements JsonSerializable { return $this->wrapped; } + public function toArray() + { + return(array)$this->wrapped; + } + + public function all() + { + return $this->toArray(); + } + /** * Specify data which should be serialized to JSON * @@ -119,6 +144,26 @@ class objBag implements JsonSerializable { { return $this->wrapped; } + + public function offsetExists($offset) + { + return isset($this->$offset); + } + + public function offsetGet($offset) + { + return $this->$offset; + } + + public function offsetSet($offset, $value) + { + $this->$offset = $value; + } + + public function offsetUnset($offset) + { + unset($this->$offset); + } } function objBag($obj) { diff --git a/public/fonts/fa-dtbl-1-preview.html b/public/fonts/fa-dtbl-1-preview.html index 2540925..9faf934 100644 --- a/public/fonts/fa-dtbl-1-preview.html +++ b/public/fonts/fa-dtbl-1-preview.html @@ -161,7 +161,8 @@ [data-icon]:before { content: attr(data-icon); } [data-icon]:before, - .fa-bell:before, + .fa-at:before, +.fa-bell:before, .fa-bell-o:before, .fa-calendar:before, .fa-check:before, @@ -177,8 +178,10 @@ .fa-floppy-o:before, .fa-gavel:before, .fa-github:before, +.fa-globe:before, .fa-google:before, .fa-history:before, +.fa-key-modern:before, .fa-link:before, .fa-pencil-square-o:before, .fa-question-circle:before, @@ -216,47 +219,50 @@ font-smoothing: antialiased; } - .fa-bell:before { content: "\f100"; } -.fa-bell-o:before { content: "\f101"; } -.fa-calendar:before { content: "\f102"; } -.fa-check:before { content: "\f103"; } -.fa-clock-o:before { content: "\f104"; } -.fa-cloud-upload:before { content: "\f105"; } -.fa-code-fork:before { content: "\f106"; } -.fa-download:before { content: "\f107"; } -.fa-eye:before { content: "\f108"; } -.fa-eye-slash:before { content: "\f109"; } -.fa-facebook-square:before { content: "\f10a"; } -.fa-filter:before { content: "\f10b"; } -.fa-flag:before { content: "\f10c"; } -.fa-floppy-o:before { content: "\f10d"; } -.fa-gavel:before { content: "\f10e"; } -.fa-github:before { content: "\f10f"; } -.fa-google:before { content: "\f110"; } -.fa-history:before { content: "\f111"; } -.fa-link:before { content: "\f112"; } -.fa-pencil-square-o:before { content: "\f113"; } -.fa-question-circle:before { content: "\f114"; } -.fa-quote-left:before { content: "\f115"; } -.fa-reply:before { content: "\f116"; } -.fa-rss:before { content: "\f117"; } -.fa-search:before { content: "\f118"; } -.fa-share-alt:before { content: "\f119"; } -.fa-sign-in:before { content: "\f11a"; } -.fa-sign-out:before { content: "\f11b"; } -.fa-sliders:before { content: "\f11c"; } -.fa-sort:before { content: "\f11d"; } -.fa-sort-asc:before { content: "\f11e"; } -.fa-sort-desc:before { content: "\f11f"; } -.fa-star:before { content: "\f120"; } -.fa-star-o:before { content: "\f121"; } -.fa-table:before { content: "\f122"; } -.fa-times:before { content: "\f123"; } -.fa-trash:before { content: "\f124"; } -.fa-trash-o:before { content: "\f125"; } -.fa-user-circle-o:before { content: "\f126"; } -.fa-user-plus:before { content: "\f127"; } -.fa-wrench:before { content: "\f128"; } + .fa-at:before { content: "\f100"; } +.fa-bell:before { content: "\f101"; } +.fa-bell-o:before { content: "\f102"; } +.fa-calendar:before { content: "\f103"; } +.fa-check:before { content: "\f104"; } +.fa-clock-o:before { content: "\f105"; } +.fa-cloud-upload:before { content: "\f106"; } +.fa-code-fork:before { content: "\f107"; } +.fa-download:before { content: "\f108"; } +.fa-eye:before { content: "\f109"; } +.fa-eye-slash:before { content: "\f10a"; } +.fa-facebook-square:before { content: "\f10b"; } +.fa-filter:before { content: "\f10c"; } +.fa-flag:before { content: "\f10d"; } +.fa-floppy-o:before { content: "\f10e"; } +.fa-gavel:before { content: "\f10f"; } +.fa-github:before { content: "\f110"; } +.fa-globe:before { content: "\f111"; } +.fa-google:before { content: "\f112"; } +.fa-history:before { content: "\f113"; } +.fa-key-modern:before { content: "\f114"; } +.fa-link:before { content: "\f115"; } +.fa-pencil-square-o:before { content: "\f116"; } +.fa-question-circle:before { content: "\f117"; } +.fa-quote-left:before { content: "\f118"; } +.fa-reply:before { content: "\f119"; } +.fa-rss:before { content: "\f11a"; } +.fa-search:before { content: "\f11b"; } +.fa-share-alt:before { content: "\f11c"; } +.fa-sign-in:before { content: "\f11d"; } +.fa-sign-out:before { content: "\f11e"; } +.fa-sliders:before { content: "\f11f"; } +.fa-sort:before { content: "\f120"; } +.fa-sort-asc:before { content: "\f121"; } +.fa-sort-desc:before { content: "\f122"; } +.fa-star:before { content: "\f123"; } +.fa-star-o:before { content: "\f124"; } +.fa-table:before { content: "\f125"; } +.fa-times:before { content: "\f126"; } +.fa-trash:before { content: "\f127"; } +.fa-trash-o:before { content: "\f128"; } +.fa-user-circle-o:before { content: "\f129"; } +.fa-user-plus:before { content: "\f12a"; } +.fa-wrench:before { content: "\f12b"; } @@ -272,11 +278,24 @@